summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-07-24 13:58:42 -0400
committerGitHub <noreply@github.com>2019-07-24 13:58:42 -0400
commitb823665299593fa2f12a5f1a86af11dbb6b7bc4b (patch)
treeb59fc95248a707e0c3ef69874cfccf24c9c5b392
parent346589e7e81adcd2aff883776249778d57eb4faf (diff)
parent29eeef6829fc7fc4a7e706d90871e8c347de773a (diff)
downloadcmd2-git-b823665299593fa2f12a5f1a86af11dbb6b7bc4b.tar.gz
Merge pull request #739 from python-cmd2/presentation_stuff
Presentation stuff
-rw-r--r--CHANGELOG.md2
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--cmd2/cmd2.py227
-rw-r--r--cmd2/parsing.py34
-rw-r--r--cmd2/utils.py17
-rwxr-xr-x[-rw-r--r--]examples/migrating.py0
-rwxr-xr-xexamples/unicode_commands.py27
-rw-r--r--tests/pyscript/raises_exception.py (renamed from tests/scripts/raises_exception.py)0
-rw-r--r--tests/pyscript/recursive.py11
-rw-r--r--tests/pyscript/run.py6
-rw-r--r--tests/pyscript/to_run.py2
-rw-r--r--tests/scripts/recursive.py8
-rw-r--r--tests/test_cmd2.py222
-rw-r--r--tests/test_history.py15
-rw-r--r--tests/test_run_pyscript.py43
-rw-r--r--tests/test_utils.py12
16 files changed, 457 insertions, 173 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98f04eeb..f9622e62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@
* Fixed bug where multiline commands were having leading and ending spaces stripped. This would mess up quoted
strings that crossed multiple lines.
* Fixed a bug when appending to the clipboard where contents were in reverse order
+ * Fixed issue where run_pyscript failed if the script's filename had 2 or more consecutive spaces
+ * Fixed issue where completer function of disabled command would still run
* Enhancements
* Greatly simplified using argparse-based tab completion. The new interface is a complete overhaul that breaks
the previous way of specifying completion and choices functions. See header of [argparse_custom.py](https://github.com/python-cmd2/cmd2/blob/master/cmd2/argparse_custom.py)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e0ae7625..6055fa7d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -192,7 +192,7 @@ $ git push origin [name_of_your_new_branch]
### Setting up for cmd2 development
-For doing cmd2 development, it is recommended you create a virutal environment using Conda or Virtualenv and install the
+For doing cmd2 development, it is recommended you create a virtual environment using Conda or Virtualenv and install the
package from the source.
#### Create a new environment for cmd2 using Pipenv
@@ -234,7 +234,7 @@ pyenv versions
# Install python version defined
pyenv install 3.6.3
```
-With the Python version installed, you can set the virutalenv properly.
+With the Python version installed, you can set the virtualenv properly.
```sh
$ cd ~/src/cmd2
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index dbdb55ef..adadbdf8 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -99,6 +99,9 @@ COMMAND_FUNC_PREFIX = 'do_'
# All help functions start with this
HELP_FUNC_PREFIX = 'help_'
+# All command completer functions start with this
+COMPLETER_FUNC_PREFIX = 'complete_'
+
# Sorting keys for strings
ALPHABETICAL_SORT_KEY = utils.norm_fold
NATURAL_SORT_KEY = utils.natural_keys
@@ -164,8 +167,10 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
return cmd_wrapper
if len(args) == 1 and callable(args[0]):
+ # noinspection PyTypeChecker
return arg_decorator(args[0])
else:
+ # noinspection PyTypeChecker
return arg_decorator
@@ -188,7 +193,6 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
"""
import functools
- # noinspection PyProtectedMember
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
@@ -226,6 +230,7 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
return cmd_wrapper
+ # noinspection PyTypeChecker
return arg_decorator
@@ -246,7 +251,6 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
"""
import functools
- # noinspection PyProtectedMember
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
@@ -284,6 +288,7 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
return cmd_wrapper
+ # noinspection PyTypeChecker
return arg_decorator
@@ -316,7 +321,7 @@ class EmptyStatement(Exception):
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
-DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])
+DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function'])
class Cmd(cmd.Cmd):
@@ -427,14 +432,14 @@ class Cmd(cmd.Cmd):
# Defines app-specific variables/functions available in Python shells and pyscripts
self.py_locals = {}
+ # True if running inside a Python script or interactive console, False otherwise
+ self._in_py = False
+
self.statement_parser = StatementParser(allow_redirection=allow_redirection,
terminators=terminators,
multiline_commands=multiline_commands,
shortcuts=shortcuts)
- # True if running inside a Python script or interactive console, False otherwise
- self._in_py = False
-
# Stores results from the last command run to enable usage of results in a Python script or interactive console
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
self.last_result = None
@@ -1459,10 +1464,14 @@ class Cmd(cmd.Cmd):
text = text_to_remove + text
begidx = actual_begidx
- # Check if a valid command was entered
- if command in self.get_all_commands():
+ # Check if a macro was entered
+ if command in self.macros:
+ compfunc = self.path_complete
+
+ # Check if a command was entered
+ elif command in self.get_all_commands():
# Get the completer function for this command
- compfunc = getattr(self, 'complete_' + command, None)
+ compfunc = getattr(self, COMPLETER_FUNC_PREFIX + command, None)
if compfunc is None:
# There's no completer function, next see if the command uses argparse
@@ -1476,11 +1485,7 @@ class Cmd(cmd.Cmd):
else:
compfunc = self.completedefault
- # Check if a macro was entered
- elif command in self.macros:
- compfunc = self.path_complete
-
- # A valid command was not entered
+ # Not a recognized macro or command
else:
# Check if this command should be run as a shell command
if self.default_to_shell and command in utils.get_exes_in_path(command):
@@ -1724,10 +1729,13 @@ class Cmd(cmd.Cmd):
statement = self.statement_parser.parse_command_only(line)
return statement.command, statement.args, statement.command_and_args
- def onecmd_plus_hooks(self, line: str, *, add_to_history: bool = True, py_bridge_call: bool = False) -> bool:
+ def onecmd_plus_hooks(self, line: str, *, expand: bool = True, add_to_history: bool = True,
+ py_bridge_call: bool = False) -> bool:
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
- :param line: line of text read from input
+ :param line: command line to run
+ :param expand: If True, then aliases, macros, and shortcuts will be expanded.
+ Set this to False if the command token should not be altered. Defaults to True.
:param add_to_history: If True, then add this command to history. Defaults to True.
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
of an app() call from Python. It is used to enable/disable the storage of the
@@ -1738,7 +1746,7 @@ class Cmd(cmd.Cmd):
stop = False
try:
- statement = self._input_line_to_statement(line)
+ statement = self._input_line_to_statement(line, expand=expand)
except EmptyStatement:
return self._run_cmdfinalization_hooks(stop, None)
except ValueError as ex:
@@ -1853,12 +1861,15 @@ class Cmd(cmd.Cmd):
except Exception as ex:
self.pexcept(ex)
- def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *, add_to_history: bool = True) -> bool:
+ def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *,
+ expand: bool = True, add_to_history: bool = True) -> bool:
"""
Used when commands are being run in an automated fashion like text scripts or history replays.
The prompt and command line for each command will be printed if echo is True.
:param cmds: commands to run
+ :param expand: If True, then aliases, macros, and shortcuts will be expanded.
+ Set this to False if the command token should not be altered. Defaults to True.
:param add_to_history: If True, then add these commands to history. Defaults to True.
:return: True if running of commands should stop
"""
@@ -1869,12 +1880,12 @@ class Cmd(cmd.Cmd):
if self.echo:
self.poutput('{}{}'.format(self.prompt, line))
- if self.onecmd_plus_hooks(line, add_to_history=add_to_history):
+ if self.onecmd_plus_hooks(line, expand=expand, add_to_history=add_to_history):
return True
return False
- def _complete_statement(self, line: str) -> Statement:
+ def _complete_statement(self, line: str, *, expand: bool = True) -> Statement:
"""Keep accepting lines of input until the command is complete.
There is some pretty hacky code here to handle some quirks of
@@ -1883,11 +1894,13 @@ class Cmd(cmd.Cmd):
backwards compatibility with the standard library version of cmd.
:param line: the line being parsed
+ :param expand: If True, then aliases and shortcuts will be expanded.
+ Set this to False if the command token should not be altered. Defaults to True.
:return: the completed Statement
"""
while True:
try:
- statement = self.statement_parser.parse(line)
+ statement = self.statement_parser.parse(line, expand=expand)
if statement.multiline_command and statement.terminator:
# we have a completed multiline command, we are done
break
@@ -1898,7 +1911,7 @@ class Cmd(cmd.Cmd):
except ValueError:
# we have unclosed quotation marks, lets parse only the command
# and see if it's a multiline
- statement = self.statement_parser.parse_command_only(line)
+ statement = self.statement_parser.parse_command_only(line, expand=expand)
if not statement.multiline_command:
# not a multiline command, so raise the exception
raise
@@ -1935,11 +1948,13 @@ class Cmd(cmd.Cmd):
raise EmptyStatement()
return statement
- def _input_line_to_statement(self, line: str) -> Statement:
+ def _input_line_to_statement(self, line: str, *, expand: bool = True) -> Statement:
"""
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
:param line: the line being parsed
+ :param expand: If True, then aliases, macros, and shortcuts will be expanded.
+ Set this to False if the command token should not be altered. Defaults to True.
:return: parsed command line as a Statement
"""
used_macros = []
@@ -1948,14 +1963,14 @@ class Cmd(cmd.Cmd):
# Continue until all macros are resolved
while True:
# Make sure all input has been read and convert it to a Statement
- statement = self._complete_statement(line)
+ statement = self._complete_statement(line, expand=expand)
# Save the fully entered line if this is the first loop iteration
if orig_line is None:
orig_line = statement.raw
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
- if statement.command in self.macros.keys() and statement.command not in used_macros:
+ if expand and statement.command in self.macros.keys() and statement.command not in used_macros:
used_macros.append(statement.command)
line = self._resolve_macro(statement)
if line is None:
@@ -1976,8 +1991,7 @@ class Cmd(cmd.Cmd):
suffix=statement.suffix,
pipe_to=statement.pipe_to,
output=statement.output,
- output_to=statement.output_to,
- )
+ output_to=statement.output_to)
return statement
def _resolve_macro(self, statement: Statement) -> Optional[str]:
@@ -2170,19 +2184,22 @@ class Cmd(cmd.Cmd):
return target if callable(getattr(self, target, None)) else ''
# noinspection PyMethodOverriding
- def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
+ def onecmd(self, statement: Union[Statement, str], *,
+ expand: bool = True, add_to_history: bool = True) -> bool:
""" This executes the actual do_* method for a command.
If the command provided doesn't exist, then it executes default() instead.
:param statement: intended to be a Statement instance parsed command from the input stream, alternative
acceptance of a str is present only for backward compatibility with cmd
+ :param expand: If True, then aliases, macros, and shortcuts will be expanded.
+ Set this to False if the command token should not be altered. Defaults to True.
:param add_to_history: If True, then add this command to history. Defaults to True.
:return: a flag indicating whether the interpretation of commands should stop
"""
# For backwards compatibility with cmd, allow a str to be passed in
if not isinstance(statement, Statement):
- statement = self._input_line_to_statement(statement)
+ statement = self._input_line_to_statement(statement, expand=expand)
func = self.cmd_func(statement.command)
if func:
@@ -2211,6 +2228,7 @@ class Cmd(cmd.Cmd):
if 'shell' not in self.exclude_from_history:
self.history.append(statement)
+ # noinspection PyTypeChecker
return self.do_shell(statement.command_and_args)
else:
err_msg = self.default_error.format(statement.command)
@@ -2486,7 +2504,7 @@ class Cmd(cmd.Cmd):
# Call whatever subcommand function was selected
func(self, args)
else:
- # No subcommand was provided, so call help
+ # noinspection PyTypeChecker
self.do_help('alias')
# ----- Macro subcommand functions -----
@@ -2500,8 +2518,8 @@ class Cmd(cmd.Cmd):
self.perror("Invalid macro name: {}".format(errmsg))
return
- if args.name in self.get_all_commands():
- self.perror("Macro cannot have the same name as a command")
+ if args.name in self.statement_parser.multiline_commands:
+ self.perror("Macro cannot have the same name as a multiline command")
return
if args.name in self.aliases:
@@ -2688,7 +2706,7 @@ class Cmd(cmd.Cmd):
# Call whatever subcommand function was selected
func(self, args)
else:
- # No subcommand was provided, so call help
+ # noinspection PyTypeChecker
self.do_help('macro')
def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
@@ -2737,7 +2755,8 @@ class Cmd(cmd.Cmd):
return matches
- help_parser = Cmd2ArgumentParser()
+ help_parser = Cmd2ArgumentParser(description="List available commands or provide "
+ "detailed help for a specific command")
help_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to retrieve help for",
completer_method=complete_help_command)
help_parser.add_argument('subcommand', nargs=argparse.REMAINDER, help="subcommand to retrieve help for",
@@ -2906,7 +2925,7 @@ class Cmd(cmd.Cmd):
command = ''
self.stdout.write("\n")
- @with_argparser(Cmd2ArgumentParser())
+ @with_argparser(Cmd2ArgumentParser(description="List available shortcuts"))
def do_shortcuts(self, _: argparse.Namespace) -> None:
"""List available shortcuts"""
# Sort the shortcut tuples by name
@@ -2920,7 +2939,7 @@ class Cmd(cmd.Cmd):
# Return True to stop the command loop
return True
- @with_argparser(Cmd2ArgumentParser())
+ @with_argparser(Cmd2ArgumentParser(description="Exit this application"))
def do_quit(self, _: argparse.Namespace) -> bool:
"""Exit this application"""
# Return True to stop the command loop
@@ -3060,7 +3079,7 @@ class Cmd(cmd.Cmd):
if onchange_hook is not None:
onchange_hook(old=orig_value, new=new_value) # pylint: disable=not-callable
- shell_parser = Cmd2ArgumentParser()
+ shell_parser = Cmd2ArgumentParser(description="Execute a command as if at the OS prompt")
shell_parser.add_argument('command', help='the command to run', completer_method=shell_cmd_complete)
shell_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command',
completer_method=path_complete)
@@ -3229,42 +3248,59 @@ class Cmd(cmd.Cmd):
py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run")
py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command")
+ # This is a hidden flag for telling do_py to run a pyscript. It is intended only to be used by run_pyscript
+ # after it sets up sys.argv for the script being run. When this flag is present, it takes precedence over all
+ # other arguments. run_pyscript uses this method instead of "py run('file')" because file names with
+ # 2 or more consecutive spaces cause issues with our parser, which isn't meant to parse Python statements.
+ py_parser.add_argument('--pyscript', help=argparse.SUPPRESS)
+
# Preserve quotes since we are passing these strings to Python
@with_argparser(py_parser, preserve_quotes=True)
- def do_py(self, args: argparse.Namespace) -> bool:
- """Invoke Python command or shell"""
+ def do_py(self, args: argparse.Namespace) -> Optional[bool]:
+ """
+ Enter an interactive Python shell
+ :return: True if running of commands should stop
+ """
from .py_bridge import PyBridge
if self._in_py:
err = "Recursively entering interactive Python consoles is not allowed."
self.perror(err)
- return False
+ return
py_bridge = PyBridge(self)
+ py_code_to_run = ''
+
+ # Handle case where we were called by run_pyscript
+ if args.pyscript:
+ args.pyscript = utils.strip_quotes(args.pyscript)
+
+ # Run the script - use repr formatting to escape things which
+ # need to be escaped to prevent issues on Windows
+ py_code_to_run = 'run({!r})'.format(args.pyscript)
+
+ elif args.command:
+ py_code_to_run = args.command
+ if args.remainder:
+ py_code_to_run += ' ' + ' '.join(args.remainder)
+
+ # Set cmd_echo to True so PyBridge statements like: py app('help')
+ # run at the command line will print their output.
+ py_bridge.cmd_echo = True
try:
self._in_py = True
- # Support the run command even if called prior to invoking an interactive interpreter
def py_run(filename: str):
"""Run a Python script file in the interactive console.
- :param filename: filename of *.py script file to run
+ :param filename: filename of script file to run
"""
expanded_filename = os.path.expanduser(filename)
- if not expanded_filename.endswith('.py'):
- self.pwarning("'{}' does not have a .py extension".format(expanded_filename))
- selection = self.select('Yes No', 'Continue to try to run it as a Python script? ')
- if selection != 'Yes':
- return
-
- # cmd_echo defaults to False for scripts. The user can always toggle this value in their script.
- py_bridge.cmd_echo = False
-
try:
with open(expanded_filename) as f:
interp.runcode(f.read())
except OSError as ex:
- self.pexcept("Error opening script file '{}': {}".format(expanded_filename, ex))
+ self.pexcept("Error reading script file '{}': {}".format(expanded_filename, ex))
def py_quit():
"""Function callable from the interactive Python console to exit that environment"""
@@ -3285,24 +3321,16 @@ class Cmd(cmd.Cmd):
interp = InteractiveConsole(locals=localvars)
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
- # Check if the user is running a Python statement on the command line
- if args.command:
- full_command = args.command
- if args.remainder:
- full_command += ' ' + ' '.join(args.remainder)
-
- # Set cmd_echo to True so PyBridge statements like: py app('help')
- # run at the command line will print their output.
- py_bridge.cmd_echo = True
-
+ # Check if we are running Python code
+ if py_code_to_run:
# noinspection PyBroadException
try:
- interp.runcode(full_command)
+ interp.runcode(py_code_to_run)
except BaseException:
- # We don't care about any exception that happened in the interactive console
+ # We don't care about any exception that happened in the Python code
pass
- # If there are no args, then we will open an interactive Python shell
+ # Otherwise we will open an interactive Python shell
else:
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n'
@@ -3338,15 +3366,28 @@ class Cmd(cmd.Cmd):
return py_bridge.stop
- run_pyscript_parser = Cmd2ArgumentParser()
+ run_pyscript_parser = Cmd2ArgumentParser(description="Run a Python script file inside the console")
run_pyscript_parser.add_argument('script_path', help='path to the script file', completer_method=path_complete)
run_pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
help='arguments to pass to script', completer_method=path_complete)
@with_argparser(run_pyscript_parser)
- def do_run_pyscript(self, args: argparse.Namespace) -> bool:
- """Run a Python script file inside the console"""
- script_path = os.path.expanduser(args.script_path)
+ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:
+ """
+ Run a Python script file inside the console
+ :return: True if running of commands should stop
+ """
+ # Expand ~ before placing this path in sys.argv just as a shell would
+ args.script_path = os.path.expanduser(args.script_path)
+
+ # Add some protection against accidentally running a non-Python file. The happens when users
+ # mix up run_script and run_pyscript.
+ if not args.script_path.endswith('.py'):
+ self.pwarning("'{}' does not have a .py extension".format(args.script_path))
+ selection = self.select('Yes No', 'Continue to try to run it as a Python script? ')
+ if selection != 'Yes':
+ return
+
py_return = False
# Save current command line arguments
@@ -3354,11 +3395,10 @@ class Cmd(cmd.Cmd):
try:
# Overwrite sys.argv to allow the script to take command line arguments
- sys.argv = [script_path] + args.script_arguments
+ sys.argv = [args.script_path] + args.script_arguments
- # Run the script - use repr formatting to escape things which
- # need to be escaped to prevent issues on Windows
- py_return = self.do_py("run({!r})".format(script_path))
+ # noinspection PyTypeChecker
+ py_return = self.do_py('--pyscript {}'.format(utils.quote_string(args.script_path)))
except KeyboardInterrupt:
pass
@@ -3371,7 +3411,7 @@ class Cmd(cmd.Cmd):
# Only include the do_ipy() method if IPython is available on the system
if ipython_available: # pragma: no cover
- @with_argparser(Cmd2ArgumentParser())
+ @with_argparser(Cmd2ArgumentParser(description="Enter an interactive IPython shell"))
def do_ipy(self, _: argparse.Namespace) -> None:
"""Enter an interactive IPython shell"""
from .py_bridge import PyBridge
@@ -3513,8 +3553,14 @@ class Cmd(cmd.Cmd):
else:
fobj.write('{}\n'.format(command.raw))
try:
- self.do_edit(fname)
- return self.do_run_script(fname)
+ # Handle potential edge case where the temp file needs to be quoted on the command line
+ quoted_fname = utils.quote_string(fname)
+
+ # noinspection PyTypeChecker
+ self.do_edit(quoted_fname)
+
+ # noinspection PyTypeChecker
+ self.do_run_script(quoted_fname)
finally:
os.remove(fname)
elif args.output_file:
@@ -3711,10 +3757,11 @@ class Cmd(cmd.Cmd):
if not self.editor:
raise EnvironmentError("Please use 'set editor' to specify your text editing program of choice.")
- command = utils.quote_string_if_needed(os.path.expanduser(self.editor))
+ command = utils.quote_string(os.path.expanduser(self.editor))
if args.file_path:
- command += " " + utils.quote_string_if_needed(os.path.expanduser(args.file_path))
+ command += " " + utils.quote_string(os.path.expanduser(args.file_path))
+ # noinspection PyTypeChecker
self.do_shell(command)
@property
@@ -3767,6 +3814,8 @@ class Cmd(cmd.Cmd):
self.perror("'{}' is not an ASCII or UTF-8 encoded text file".format(expanded_path))
return
+ # Add some protection against accidentally running a Python file. The happens when users
+ # mix up run_script and run_pyscript.
if expanded_path.endswith('.py'):
self.pwarning("'{}' appears to be a Python file".format(expanded_path))
selection = self.select('Yes No', 'Continue to try to run it as a text script? ')
@@ -3819,7 +3868,9 @@ class Cmd(cmd.Cmd):
file_path = args.file_path
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
relative_path = os.path.join(self._current_script_dir or '', file_path)
- return self.do_run_script(relative_path)
+
+ # noinspection PyTypeChecker
+ return self.do_run_script(utils.quote_string(relative_path))
def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
"""Runs transcript tests for provided file(s).
@@ -3858,6 +3909,7 @@ class Cmd(cmd.Cmd):
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
testcase = TestMyAppCase()
stream = utils.StdSim(sys.stderr)
+ # noinspection PyTypeChecker
runner = unittest.TextTestRunner(stream=stream)
start_time = time.time()
test_results = runner.run(testcase)
@@ -3999,16 +4051,24 @@ class Cmd(cmd.Cmd):
return
help_func_name = HELP_FUNC_PREFIX + command
+ completer_func_name = COMPLETER_FUNC_PREFIX + command
- # Restore the command and help functions to their original values
+ # Restore the command function to its original value
dc = self.disabled_commands[command]
setattr(self, self._cmd_func_name(command), dc.command_function)
+ # Restore the help function to its original value
if dc.help_function is None:
delattr(self, help_func_name)
else:
setattr(self, help_func_name, dc.help_function)
+ # Restore the completer function to its original value
+ if dc.completer_function is None:
+ delattr(self, completer_func_name)
+ else:
+ setattr(self, completer_func_name, dc.completer_function)
+
# Remove the disabled command entry
del self.disabled_commands[command]
@@ -4044,10 +4104,12 @@ class Cmd(cmd.Cmd):
raise AttributeError("{} does not refer to a command".format(command))
help_func_name = HELP_FUNC_PREFIX + command
+ completer_func_name = COMPLETER_FUNC_PREFIX + command
# Add the disabled command record
self.disabled_commands[command] = DisabledCommand(command_function=command_function,
- help_function=getattr(self, help_func_name, None))
+ help_function=getattr(self, help_func_name, None),
+ completer_function=getattr(self, completer_func_name, None))
# Overwrite the command and help functions to print the message
new_func = functools.partial(self._report_disabled_command_usage,
@@ -4055,6 +4117,9 @@ class Cmd(cmd.Cmd):
setattr(self, self._cmd_func_name(command), new_func)
setattr(self, help_func_name, new_func)
+ # Set the completer to a function that returns a blank list
+ setattr(self, completer_func_name, lambda *args, **kwargs: [])
+
def disable_category(self, category: str, message_to_print: str) -> None:
"""Disable an entire category of commands.
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index dbfabc80..dfa248e9 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -359,15 +359,13 @@ class StatementParser:
errmsg = ''
return valid, errmsg
- def tokenize(self, line: str, expand: bool = True) -> List[str]:
+ def tokenize(self, line: str, *, expand: bool = True) -> List[str]:
"""
Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed
:param line: the command line being lexed
:param expand: If True, then aliases and shortcuts will be expanded.
- Set this to False if no expansion should occur because the command name is already known.
- Otherwise the command could be expanded if it matched an alias name. This is for cases where
- a do_* method was called manually (e.g do_help('alias').
+ Set this to False if the command token should not be altered. Defaults to True.
:return: A list of tokens
:raises ValueError if there are unclosed quotation marks.
"""
@@ -387,7 +385,7 @@ class StatementParser:
tokens = self._split_on_punctuation(tokens)
return tokens
- def parse(self, line: str, expand: bool = True) -> Statement:
+ def parse(self, line: str, *, expand: bool = True) -> Statement:
"""
Tokenize the input and parse it into a Statement object, stripping
comments, expanding aliases and shortcuts, and extracting output
@@ -395,10 +393,8 @@ class StatementParser:
:param line: the command line being parsed
:param expand: If True, then aliases and shortcuts will be expanded.
- Set this to False if no expansion should occur because the command name is already known.
- Otherwise the command could be expanded if it matched an alias name. This is for cases where
- a do_* method was called manually (e.g do_help('alias').
- :return: A parsed Statement
+ Set this to False if the command token should not be altered. Defaults to True.
+ :return: the created Statement
:raises ValueError if there are unclosed quotation marks
"""
@@ -414,7 +410,7 @@ class StatementParser:
arg_list = []
# lex the input into a list of tokens
- tokens = self.tokenize(line, expand)
+ tokens = self.tokenize(line, expand=expand)
# of the valid terminators, find the first one to occur in the input
terminator_pos = len(tokens) + 1
@@ -533,11 +529,10 @@ class StatementParser:
suffix=suffix,
pipe_to=pipe_to,
output=output,
- output_to=output_to,
- )
+ output_to=output_to)
return statement
- def parse_command_only(self, rawinput: str) -> Statement:
+ def parse_command_only(self, rawinput: str, *, expand: bool = True) -> Statement:
"""Partially parse input into a Statement object.
The command is identified, and shortcuts and aliases are expanded.
@@ -560,9 +555,17 @@ class StatementParser:
Different from parse(), this method does not remove redundant whitespace
within args. However, it does ensure args has no leading or trailing
whitespace.
+
+ :param rawinput: the command line as entered by the user
+ :param expand: If True, then aliases and shortcuts will be expanded.
+ Set this to False if the command token should not be altered. Defaults to True.
+ :return: the created Statement
"""
+ line = rawinput
+
# expand shortcuts and aliases
- line = self._expand(rawinput)
+ if expand:
+ line = self._expand(rawinput)
command = ''
args = ''
@@ -591,8 +594,7 @@ class StatementParser:
statement = Statement(args,
raw=rawinput,
command=command,
- multiline_command=multiline_command,
- )
+ multiline_command=multiline_command)
return statement
def get_command_arg_list(self, command_name: str, to_parse: Union[Statement, str],
diff --git a/cmd2/utils.py b/cmd2/utils.py
index b19c2b49..d0ce10bc 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -25,11 +25,8 @@ def is_quoted(arg: str) -> bool:
return len(arg) > 1 and arg[0] == arg[-1] and arg[0] in constants.QUOTES
-def quote_string_if_needed(arg: str) -> str:
- """ Quotes a string if it contains spaces and isn't already quoted """
- if is_quoted(arg) or ' ' not in arg:
- return arg
-
+def quote_string(arg: str) -> str:
+ """Quote a string"""
if '"' in arg:
quote = "'"
else:
@@ -38,8 +35,16 @@ def quote_string_if_needed(arg: str) -> str:
return quote + arg + quote
+def quote_string_if_needed(arg: str) -> str:
+ """Quote a string if it contains spaces and isn't already quoted"""
+ if is_quoted(arg) or ' ' not in arg:
+ return arg
+
+ return quote_string(arg)
+
+
def strip_quotes(arg: str) -> str:
- """ Strip outer quotes from a string.
+ """Strip outer quotes from a string.
Applies to both single and double quotes.
diff --git a/examples/migrating.py b/examples/migrating.py
index 3a25b8c8..3a25b8c8 100644..100755
--- a/examples/migrating.py
+++ b/examples/migrating.py
diff --git a/examples/unicode_commands.py b/examples/unicode_commands.py
new file mode 100755
index 00000000..f8381e50
--- /dev/null
+++ b/examples/unicode_commands.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""A simple example demonstrating support for unicode command names.
+"""
+import math
+import cmd2
+
+
+class UnicodeApp(cmd2.Cmd):
+ """Example cmd2 application with unicode command names."""
+
+ def __init__(self):
+ super().__init__()
+ self.intro = 'Welcome the Unicode example app. Note the full Unicode support: 😇 💩'
+
+ def do_𝛑print(self, _):
+ """This command prints 𝛑 to 5 decimal places."""
+ self.poutput("𝛑 = {0:.6}".format(math.pi))
+
+ def do_你好(self, arg):
+ """This command says hello in Chinese (Mandarin)."""
+ self.poutput("你好 " + arg)
+
+
+if __name__ == '__main__':
+ app = UnicodeApp()
+ app.cmdloop()
diff --git a/tests/scripts/raises_exception.py b/tests/pyscript/raises_exception.py
index 738edaf2..738edaf2 100644
--- a/tests/scripts/raises_exception.py
+++ b/tests/pyscript/raises_exception.py
diff --git a/tests/pyscript/recursive.py b/tests/pyscript/recursive.py
new file mode 100644
index 00000000..21550592
--- /dev/null
+++ b/tests/pyscript/recursive.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+# coding=utf-8
+# flake8: noqa F821
+"""
+Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed
+"""
+import os
+
+app.cmd_echo = True
+my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))
+app('run_pyscript {}'.format(os.path.join(my_dir, 'stop.py')))
diff --git a/tests/pyscript/run.py b/tests/pyscript/run.py
new file mode 100644
index 00000000..47250a10
--- /dev/null
+++ b/tests/pyscript/run.py
@@ -0,0 +1,6 @@
+# flake8: noqa F821
+import os
+
+app.cmd_echo = True
+my_dir = (os.path.dirname(os.path.realpath(sys.argv[0])))
+run(os.path.join(my_dir, 'to_run.py'))
diff --git a/tests/pyscript/to_run.py b/tests/pyscript/to_run.py
new file mode 100644
index 00000000..b207952d
--- /dev/null
+++ b/tests/pyscript/to_run.py
@@ -0,0 +1,2 @@
+# flake8: noqa F821
+print("I have been run")
diff --git a/tests/scripts/recursive.py b/tests/scripts/recursive.py
deleted file mode 100644
index 7d37e540..00000000
--- a/tests/scripts/recursive.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python
-# coding=utf-8
-# flake8: noqa F821
-"""
-Example demonstrating that running a Python script recursively inside another Python script isn't allowed
-"""
-app.cmd_echo = True
-app('run_pyscript ../script.py')
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index d4dbfe55..16c5eed4 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -5,11 +5,11 @@ Cmd2 unit/functional testing
"""
import argparse
import builtins
-from code import InteractiveConsole
import io
import os
import sys
import tempfile
+from code import InteractiveConsole
import pytest
@@ -21,7 +21,8 @@ except ImportError:
import cmd2
from cmd2 import ansi, clipboard, constants, utils
-from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
+from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY
+from .conftest import SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, complete_tester
def CreateOutsimApp():
c = cmd2.Cmd()
@@ -236,6 +237,7 @@ def test_base_shell(base_app, monkeypatch):
assert out == []
assert m.called
+
def test_base_py(base_app):
# Create a variable and make sure we can see it
out, err = run_cmd(base_app, 'py qqq=3')
@@ -263,17 +265,6 @@ def test_base_py(base_app):
assert "NameError: name 'self' is not defined" in err
-@pytest.mark.skipif(sys.platform == 'win32',
- reason="Unit test doesn't work on win32, but feature does")
-def test_py_run_script(base_app, request):
- test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'script.py')
- expected = 'This is a python script running ...'
-
- out, err = run_cmd(base_app, "py run('{}')".format(python_script))
- assert expected in out
-
-
def test_base_error(base_app):
out, err = run_cmd(base_app, 'meow')
assert "is not a recognized command" in err[0]
@@ -436,6 +427,32 @@ def test_relative_run_script(base_app, request):
assert script_out == manual_out
assert script_err == manual_err
+def test_relative_run_script_with_odd_file_names(base_app, monkeypatch):
+ """Test file names with various patterns"""
+ # Mock out the do_run_script call to see what args are passed to it
+ run_script_mock = mock.MagicMock(name='do_run_script')
+ monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock)
+
+ file_name = utils.quote_string('nothingweird.txt')
+ out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
+ run_script_mock.assert_called_once_with('"nothingweird.txt"')
+ run_script_mock.reset_mock()
+
+ file_name = utils.quote_string('has spaces.txt')
+ out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
+ run_script_mock.assert_called_once_with('"has spaces.txt"')
+ run_script_mock.reset_mock()
+
+ file_name = utils.quote_string('"is_double_quoted.txt"')
+ out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
+ run_script_mock.assert_called_once_with('\'"is_double_quoted.txt"\'')
+ run_script_mock.reset_mock()
+
+ file_name = utils.quote_string("'is_single_quoted.txt'")
+ out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
+ run_script_mock.assert_called_once_with('"\'is_single_quoted.txt\'"')
+ run_script_mock.reset_mock()
+
def test_relative_run_script_requires_an_argument(base_app):
out, err = run_cmd(base_app, '_relative_run_script')
assert 'Error: the following arguments' in err[1]
@@ -665,6 +682,36 @@ def test_edit_file(base_app, request, monkeypatch):
# We think we have an editor, so should expect a Popen call
m.assert_called_once()
+def test_edit_file_with_odd_file_names(base_app, monkeypatch):
+ """Test editor and file names with various patterns"""
+ # Mock out the do_shell call to see what args are passed to it
+ shell_mock = mock.MagicMock(name='do_shell')
+ monkeypatch.setattr("cmd2.Cmd.do_shell", shell_mock)
+
+ base_app.editor = 'fooedit'
+ file_name = utils.quote_string('nothingweird.py')
+ out, err = run_cmd(base_app, "edit {}".format(file_name))
+ shell_mock.assert_called_once_with('"fooedit" "nothingweird.py"')
+ shell_mock.reset_mock()
+
+ base_app.editor = 'foo edit'
+ file_name = utils.quote_string('has spaces.py')
+ out, err = run_cmd(base_app, "edit {}".format(file_name))
+ shell_mock.assert_called_once_with('"foo edit" "has spaces.py"')
+ shell_mock.reset_mock()
+
+ base_app.editor = '"fooedit"'
+ file_name = utils.quote_string('"is_double_quoted.py"')
+ out, err = run_cmd(base_app, "edit {}".format(file_name))
+ shell_mock.assert_called_once_with('\'"fooedit"\' \'"is_double_quoted.py"\'')
+ shell_mock.reset_mock()
+
+ base_app.editor = "'fooedit'"
+ file_name = utils.quote_string("'is_single_quoted.py'")
+ out, err = run_cmd(base_app, "edit {}".format(file_name))
+ shell_mock.assert_called_once_with('"\'fooedit\'" "\'is_single_quoted.py\'"')
+ shell_mock.reset_mock()
+
def test_edit_file_with_spaces(base_app, request, monkeypatch):
# Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
base_app.editor = 'fooedit'
@@ -995,7 +1042,7 @@ class SelectApp(cmd2.Cmd):
def do_procrastinate(self, arg):
"""Waste time in your manner of choice."""
# Pass in a list of tuples for selections
- leisure_activity = self.select([('Netflix and chill', 'Netflix'), ('Porn', 'WebSurfing')],
+ leisure_activity = self.select([('Netflix and chill', 'Netflix'), ('YouTube', 'WebSurfing')],
'How would you like to procrastinate? ')
result = 'Have fun procrasinating with {}!\n'.format(leisure_activity)
self.stdout.write(result)
@@ -1107,7 +1154,7 @@ def test_select_list_of_tuples(select_app):
1. Netflix
2. WebSurfing
Have fun procrasinating with {}!
-""".format('Porn'))
+""".format('YouTube'))
# Make sure our mock was called with the expected arguments
m.assert_called_once_with('How would you like to procrastinate? ')
@@ -1672,9 +1719,12 @@ def test_macro_create_with_alias_name(base_app):
out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
assert "Macro cannot have the same name as an alias" in err[0]
-def test_macro_create_with_command_name(base_app):
- out, err = run_cmd(base_app, 'macro create help stuff')
- assert "Macro cannot have the same name as a command" in err[0]
+def test_macro_create_with_command_name(multiline_app):
+ out, err = run_cmd(multiline_app, 'macro create help stuff')
+ assert out == normalize("Macro 'help' created")
+
+ out, err = run_cmd(multiline_app, 'macro create orate stuff')
+ assert "Macro cannot have the same name as a multiline command" in err[0]
def test_macro_create_with_args(base_app):
# Create the macro
@@ -1793,6 +1843,36 @@ def test_nonexistent_macro(base_app):
assert exception is not None
+def test_input_line_to_statement_expand(base_app):
+ # Enable/Disable expansion of shortcuts
+ line = '!ls'
+ statement = base_app._input_line_to_statement(line, expand=True)
+ assert statement.command == 'shell'
+
+ statement = base_app._input_line_to_statement(line, expand=False)
+ assert statement.command == '!ls'
+
+ # Enable/Disable expansion of aliases
+ run_cmd(base_app, 'alias create help macro')
+
+ line = 'help'
+ statement = base_app._input_line_to_statement(line, expand=True)
+ assert statement.command == 'macro'
+
+ statement = base_app._input_line_to_statement(line, expand=False)
+ assert statement.command == 'help'
+
+ run_cmd(base_app, 'alias delete help')
+
+ # Enable/Disable expansion of macros
+ run_cmd(base_app, 'macro create help alias')
+
+ line = 'help'
+ statement = base_app._input_line_to_statement(line, expand=True)
+ assert statement.command == 'alias'
+
+ statement = base_app._input_line_to_statement(line, expand=False)
+ assert statement.command == 'help'
def test_ppaged(outsim_app):
msg = 'testing...'
@@ -2077,16 +2157,19 @@ class DisableCommandsApp(cmd2.Cmd):
super().__init__(*args, **kwargs)
@cmd2.with_category(category_name)
- def do_has_help_func(self, arg):
- self.poutput("The real has_help_func")
+ def do_has_helper_funcs(self, arg):
+ self.poutput("The real has_helper_funcs")
+
+ def help_has_helper_funcs(self):
+ self.poutput('Help for has_helper_funcs')
- def help_has_help_func(self):
- self.poutput('Help for has_help_func')
+ def complete_has_helper_funcs(self, *args):
+ return ['result']
@cmd2.with_category(category_name)
- def do_has_no_help_func(self, arg):
- """Help for has_no_help_func"""
- self.poutput("The real has_no_help_func")
+ def do_has_no_helper_funcs(self, arg):
+ """Help for has_no_helper_funcs"""
+ self.poutput("The real has_no_helper_funcs")
@pytest.fixture
@@ -2096,51 +2179,92 @@ def disable_commands_app():
def test_disable_and_enable_category(disable_commands_app):
+ ##########################################################################
# Disable the category
+ ##########################################################################
message_to_print = 'These commands are currently disabled'
disable_commands_app.disable_category(disable_commands_app.category_name, message_to_print)
# Make sure all the commands and help on those commands displays the message
- out, err = run_cmd(disable_commands_app, 'has_help_func')
+ out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
assert err[0].startswith(message_to_print)
- out, err = run_cmd(disable_commands_app, 'help has_help_func')
+ out, err = run_cmd(disable_commands_app, 'help has_helper_funcs')
assert err[0].startswith(message_to_print)
- out, err = run_cmd(disable_commands_app, 'has_no_help_func')
+ out, err = run_cmd(disable_commands_app, 'has_no_helper_funcs')
assert err[0].startswith(message_to_print)
- out, err = run_cmd(disable_commands_app, 'help has_no_help_func')
+ out, err = run_cmd(disable_commands_app, 'help has_no_helper_funcs')
assert err[0].startswith(message_to_print)
+ # Make sure neither function completes
+ text = ''
+ line = 'has_helper_funcs'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
+ assert first_match is None
+
+ text = ''
+ line = 'has_no_helper_funcs'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
+ assert first_match is None
+
+ # Make sure both commands are invisible
visible_commands = disable_commands_app.get_visible_commands()
- assert 'has_help_func' not in visible_commands
- assert 'has_no_help_func' not in visible_commands
+ assert 'has_helper_funcs' not in visible_commands
+ assert 'has_no_helper_funcs' not in visible_commands
+ ##########################################################################
# Enable the category
+ ##########################################################################
disable_commands_app.enable_category(disable_commands_app.category_name)
# Make sure all the commands and help on those commands are restored
- out, err = run_cmd(disable_commands_app, 'has_help_func')
- assert out[0] == "The real has_help_func"
+ out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
+ assert out[0] == "The real has_helper_funcs"
+
+ out, err = run_cmd(disable_commands_app, 'help has_helper_funcs')
+ assert out[0] == "Help for has_helper_funcs"
+
+ out, err = run_cmd(disable_commands_app, 'has_no_helper_funcs')
+ assert out[0] == "The real has_no_helper_funcs"
+
+ out, err = run_cmd(disable_commands_app, 'help has_no_helper_funcs')
+ assert out[0] == "Help for has_no_helper_funcs"
+
+ # has_helper_funcs should complete now
+ text = ''
+ line = 'has_helper_funcs'
+ endidx = len(line)
+ begidx = endidx - len(text)
- out, err = run_cmd(disable_commands_app, 'help has_help_func')
- assert out[0] == "Help for has_help_func"
+ first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
+ assert first_match is not None and disable_commands_app.completion_matches == ['result ']
- out, err = run_cmd(disable_commands_app, 'has_no_help_func')
- assert out[0] == "The real has_no_help_func"
+ # has_no_helper_funcs had no completer originally, so there should be no results
+ text = ''
+ line = 'has_no_helper_funcs'
+ endidx = len(line)
+ begidx = endidx - len(text)
- out, err = run_cmd(disable_commands_app, 'help has_no_help_func')
- assert out[0] == "Help for has_no_help_func"
+ first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
+ assert first_match is None
+ # Make sure both commands are visible
visible_commands = disable_commands_app.get_visible_commands()
- assert 'has_help_func' in visible_commands
- assert 'has_no_help_func' in visible_commands
+ assert 'has_helper_funcs' in visible_commands
+ assert 'has_no_helper_funcs' in visible_commands
def test_enable_enabled_command(disable_commands_app):
# Test enabling a command that is not disabled
saved_len = len(disable_commands_app.disabled_commands)
- disable_commands_app.enable_command('has_help_func')
+ disable_commands_app.enable_command('has_helper_funcs')
# The number of disabled_commands should not have changed
assert saved_len == len(disable_commands_app.disabled_commands)
@@ -2152,7 +2276,7 @@ def test_disable_fake_command(disable_commands_app):
def test_disable_command_twice(disable_commands_app):
saved_len = len(disable_commands_app.disabled_commands)
message_to_print = 'These commands are currently disabled'
- disable_commands_app.disable_command('has_help_func', message_to_print)
+ disable_commands_app.disable_command('has_helper_funcs', message_to_print)
# The length of disabled_commands should have increased one
new_len = len(disable_commands_app.disabled_commands)
@@ -2160,24 +2284,24 @@ def test_disable_command_twice(disable_commands_app):
saved_len = new_len
# Disable again and the length should not change
- disable_commands_app.disable_command('has_help_func', message_to_print)
+ disable_commands_app.disable_command('has_helper_funcs', message_to_print)
new_len = len(disable_commands_app.disabled_commands)
assert saved_len == new_len
def test_disabled_command_not_in_history(disable_commands_app):
message_to_print = 'These commands are currently disabled'
- disable_commands_app.disable_command('has_help_func', message_to_print)
+ disable_commands_app.disable_command('has_helper_funcs', message_to_print)
saved_len = len(disable_commands_app.history)
- run_cmd(disable_commands_app, 'has_help_func')
+ run_cmd(disable_commands_app, 'has_helper_funcs')
assert saved_len == len(disable_commands_app.history)
def test_disabled_message_command_name(disable_commands_app):
message_to_print = '{} is currently disabled'.format(cmd2.cmd2.COMMAND_NAME)
- disable_commands_app.disable_command('has_help_func', message_to_print)
+ disable_commands_app.disable_command('has_helper_funcs', message_to_print)
- out, err = run_cmd(disable_commands_app, 'has_help_func')
- assert err[0].startswith('has_help_func is currently disabled')
+ out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
+ assert err[0].startswith('has_helper_funcs is currently disabled')
def test_startup_script(request):
diff --git a/tests/test_history.py b/tests/test_history.py
index 88f38172..2da34cfd 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -477,16 +477,21 @@ def test_history_edit(base_app, monkeypatch):
# going to call it due to the mock
base_app.editor = 'fooedit'
- # Mock out the Popen call so we don't actually open an editor
- m = mock.MagicMock(name='Popen')
- monkeypatch.setattr("subprocess.Popen", m)
+ # Mock out the edit call so we don't actually open an editor
+ edit_mock = mock.MagicMock(name='do_edit')
+ monkeypatch.setattr("cmd2.Cmd.do_edit", edit_mock)
+
+ # Mock out the run_script call since the mocked edit won't produce a file
+ run_script_mock = mock.MagicMock(name='do_run_script')
+ monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock)
# Run help command just so we have a command in history
run_cmd(base_app, 'help')
run_cmd(base_app, 'history -e 1')
- # We have an editor, so should expect a Popen call
- m.assert_called_once()
+ # Make sure both functions were called
+ edit_mock.assert_called_once()
+ run_script_mock.assert_called_once()
def test_history_run_all_commands(base_app):
# make sure we refuse to run all commands as a default
diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py
index ded95225..a4ff097f 100644
--- a/tests/test_run_pyscript.py
+++ b/tests/test_run_pyscript.py
@@ -6,7 +6,7 @@ Unit/functional testing for run_pytest in cmd2
import builtins
import os
-from cmd2 import plugin
+from cmd2 import plugin, utils
from .conftest import run_cmd
# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
@@ -32,7 +32,7 @@ def test_run_pyscript(base_app, request):
def test_run_pyscript_recursive_not_allowed(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'scripts', 'recursive.py')
+ python_script = os.path.join(test_dir, 'pyscript', 'recursive.py')
expected = 'Recursively entering interactive Python consoles is not allowed.'
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
@@ -41,7 +41,7 @@ def test_run_pyscript_recursive_not_allowed(base_app, request):
def test_run_pyscript_with_nonexist_file(base_app):
python_script = 'does_not_exist.py'
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
- assert "Error opening script file" in err[0]
+ assert "Error reading script file" in err[0]
def test_run_pyscript_with_non_python_file(base_app, request):
m = mock.MagicMock(name='input', return_value='2')
@@ -52,9 +52,34 @@ def test_run_pyscript_with_non_python_file(base_app, request):
out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename))
assert "does not have a .py extension" in err[0]
+def test_run_pyscript_with_odd_file_names(base_app):
+ """
+ Pass in file names with various patterns. Since these files don't exist, we will rely
+ on the error text to make sure the file names were processed correctly.
+ """
+ python_script = utils.quote_string('nothingweird.py')
+ out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
+ assert "Error reading script file 'nothingweird.py'" in err[0]
+
+ python_script = utils.quote_string('has spaces.py')
+ out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
+ assert "Error reading script file 'has spaces.py'" in err[0]
+
+ # For remaining tests, mock input to get us passed the warning about not ending in .py
+ input_mock = mock.MagicMock(name='input', return_value='1')
+ builtins.input = input_mock
+
+ python_script = utils.quote_string('"is_double_quoted.py"')
+ out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
+ assert "Error reading script file '\"is_double_quoted.py\"'" in err[1]
+
+ python_script = utils.quote_string("'is_single_quoted.py'")
+ out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
+ assert "Error reading script file ''is_single_quoted.py''" in err[1]
+
def test_run_pyscript_with_exception(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
- python_script = os.path.join(test_dir, 'scripts', 'raises_exception.py')
+ python_script = os.path.join(test_dir, 'pyscript', 'raises_exception.py')
out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
assert err[0].startswith('Traceback')
assert "TypeError: unsupported operand type(s) for +: 'int' and 'str'" in err[-1]
@@ -91,7 +116,7 @@ def test_run_pyscript_stop(base_app, request):
# Verify onecmd_plus_hooks() returns True if any commands in a pyscript return True for stop
test_dir = os.path.dirname(request.module.__file__)
- # help.py doesn't run any commands that returns True for stop
+ # help.py doesn't run any commands that return True for stop
python_script = os.path.join(test_dir, 'pyscript', 'help.py')
stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script))
assert not stop
@@ -100,3 +125,11 @@ def test_run_pyscript_stop(base_app, request):
python_script = os.path.join(test_dir, 'pyscript', 'stop.py')
stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script))
assert stop
+
+def test_run_pyscript_run(base_app, request):
+ test_dir = os.path.dirname(request.module.__file__)
+ python_script = os.path.join(test_dir, 'pyscript', 'run.py')
+ expected = 'I have been run'
+
+ out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
+ assert expected in out
diff --git a/tests/test_utils.py b/tests/test_utils.py
index edb6ca68..1890a753 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -93,13 +93,23 @@ def test_is_quoted_no():
simple_str = "hello world"
assert not cu.is_quoted(simple_str)
+def test_quote_string():
+ my_str = "Hello World"
+ assert cu.quote_string(my_str) == '"' + my_str + '"'
+
+ my_str = "'Hello World'"
+ assert cu.quote_string(my_str) == '"' + my_str + '"'
+
+ my_str = '"Hello World"'
+ assert cu.quote_string(my_str) == "'" + my_str + "'"
+
def test_quote_string_if_needed_yes():
my_str = "Hello World"
assert cu.quote_string_if_needed(my_str) == '"' + my_str + '"'
your_str = '"foo" bar'
assert cu.quote_string_if_needed(your_str) == "'" + your_str + "'"
-def test_quot_string_if_needed_no():
+def test_quote_string_if_needed_no():
my_str = "HelloWorld"
assert cu.quote_string_if_needed(my_str) == my_str
your_str = "'Hello World'"