diff options
-rw-r--r-- | cmd2/cmd2.py | 144 | ||||
-rw-r--r-- | docs/freefeatures.rst | 10 | ||||
-rw-r--r-- | tests/conftest.py | 12 | ||||
-rw-r--r-- | tests/test_cmd2.py | 26 | ||||
-rw-r--r-- | tests/test_pyscript.py | 2 |
5 files changed, 91 insertions, 103 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 838164d4..f40b1508 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2721,18 +2721,21 @@ class Cmd(cmd.Cmd): command = '' self.stdout.write("\n") - def do_shortcuts(self, _: str) -> None: - """Lists shortcuts available""" + @with_argparser(ACArgumentParser()) + def do_shortcuts(self, _: argparse.Namespace) -> None: + """List shortcuts available""" result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts)) self.poutput("Shortcuts for other commands:\n{}\n".format(result)) - def do_eof(self, _: str) -> bool: + @with_argparser(ACArgumentParser()) + def do_eof(self, _: argparse.Namespace) -> bool: """Called when <Ctrl>-D is pressed""" # End of script should not exit app, but <Ctrl>-D should. return self._STOP_AND_EXIT - def do_quit(self, _: str) -> bool: - """Exits this application""" + @with_argparser(ACArgumentParser()) + def do_quit(self, _: argparse.Namespace) -> bool: + """Exit this application""" self._should_quit = True return self._STOP_AND_EXIT @@ -2818,7 +2821,7 @@ class Cmd(cmd.Cmd): else: raise LookupError("Parameter '{}' not supported (type 'set' for list of parameters).".format(param)) - set_description = ("Sets a settable parameter or shows current settings of parameters.\n" + set_description = ("Set a settable parameter or show current settings of parameters.\n" "\n" "Accepts abbreviated parameter names so long as there is no ambiguity.\n" "Call without arguments for a list of settable parameters with their values.") @@ -2832,7 +2835,7 @@ class Cmd(cmd.Cmd): @with_argparser(set_parser) def do_set(self, args: argparse.Namespace) -> None: - """Sets a settable parameter or shows current settings of parameters""" + """Set a settable parameter or show current settings of parameters""" # Check if param was passed in if not args.param: @@ -2929,16 +2932,14 @@ class Cmd(cmd.Cmd): sys.displayhook = sys.__displayhook__ sys.excepthook = sys.__excepthook__ - def do_py(self, arg: str) -> bool: - """ - Invoke python command, shell, or script + py_description = "Invoke python command or shell" + py_parser = ACArgumentParser(description=py_description) + py_parser.add_argument('command', help="command to run", nargs='?') + py_parser.add_argument('remainder', help="remainder of command", nargs=argparse.REMAINDER) - py <command>: Executes a Python command. - py: Enters interactive Python mode. - End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``. - Non-python commands can be issued with ``pyscript_name("your command")``. - Run python code from external script files with ``run("script.py")`` - """ + @with_argparser(py_parser) + def do_py(self, args: argparse.Namespace) -> bool: + """Invoke python command or shell""" from .pyscript_bridge import PyscriptBridge, CommandResult if self._in_py: err = "Recursively entering interactive Python consoles is not allowed." @@ -2949,19 +2950,18 @@ class Cmd(cmd.Cmd): # noinspection PyBroadException try: - arg = arg.strip() - # Support the run command even if called prior to invoking an interactive interpreter def run(filename: str): """Run a Python script file in the interactive console. :param filename: filename of *.py script file to run """ + expanded_filename = os.path.expanduser(filename) try: - with open(filename) as f: + with open(expanded_filename) as f: interp.runcode(f.read()) except OSError as ex: - error_msg = "Error opening script file '{}': {}".format(filename, ex) + error_msg = "Error opening script file '{}': {}".format(expanded_filename, ex) self.perror(error_msg, traceback_war=False) bridge = PyscriptBridge(self) @@ -2976,8 +2976,12 @@ class Cmd(cmd.Cmd): interp = InteractiveConsole(locals=localvars) interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())') - if arg: - interp.runcode(arg) + if args.command: + full_command = utils.quote_string_if_needed(args.command) + for cur_token in args.remainder: + full_command += ' ' + utils.quote_string_if_needed(cur_token) + + interp.runcode(full_command) # If there are no args, then we will open an interactive Python console else: @@ -3042,7 +3046,9 @@ class Cmd(cmd.Cmd): sys.stdin = self.stdin cprt = 'Type "help", "copyright", "credits" or "license" for more information.' - docstr = self.do_py.__doc__.replace('pyscript_name', self.pyscript_name) + docstr = 'End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n' + docstr += 'Non-python commands can be issued with: {}("your command")\n'.format(self.pyscript_name) + docstr += 'Run python code from external script files with: run("script.py")\n' try: interp.interact(banner="Python {} on {}\n{}\n({})\n{}". @@ -3089,7 +3095,7 @@ class Cmd(cmd.Cmd): @with_argument_list def do_pyscript(self, arglist: List[str]) -> None: - """\nRuns a python script file inside the console + """\nRun a python script file inside the console Usage: pyscript <script_path> [script_arguments] @@ -3102,7 +3108,6 @@ Paths or arguments that contain spaces must be enclosed in quotes self.do_help(['pyscript']) return - # Get the absolute path of the script script_path = os.path.expanduser(arglist[0]) # Save current command line arguments @@ -3125,26 +3130,22 @@ Paths or arguments that contain spaces must be enclosed in quotes # Only include the do_ipy() method if IPython is available on the system if ipython_available: - # noinspection PyMethodMayBeStatic,PyUnusedLocal - def do_ipy(self, arg: str) -> None: - """Enters an interactive IPython shell. - - Run python code from external files with ``run filename.py`` - End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``. - """ + @with_argparser(ACArgumentParser()) + def do_ipy(self, _: argparse.Namespace) -> None: + """Enter an interactive IPython shell""" from .pyscript_bridge import PyscriptBridge bridge = PyscriptBridge(self) + banner = 'Entering an embedded IPython shell. Type quit() or <Ctrl>-d to exit ...\n' + banner += 'Run python code from external files with: run filename.py\n' + exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0]) + if self.locals_in_py: def load_ipy(self, app): - banner = 'Entering an embedded IPython shell. Type quit() or <Ctrl>-d to exit ...' - exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0]) embed(banner1=banner, exit_msg=exit_msg) load_ipy(self, bridge) else: def load_ipy(app): - banner = 'Entering an embedded IPython shell. Type quit() or <Ctrl>-d to exit ...' - exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0]) embed(banner1=banner, exit_msg=exit_msg) load_ipy(bridge) @@ -3308,29 +3309,27 @@ a..b, a:b, a:, ..b items by indices (inclusive) msg = '{} {} saved to transcript file {!r}' self.pfeedback(msg.format(len(history), plural, transcript_file)) - @with_argument_list - def do_edit(self, arglist: List[str]) -> None: - """Edit a file in a text editor + edit_description = "Edit a file in a text editor\n" + edit_description += "\n" + edit_description += "The editor used is determined by the ``editor`` settable parameter.\n" + edit_description += "`set editor (program-name)` to change or set the EDITOR environment variable.\n" -Usage: edit [file_path] - Where: - * file_path - path to a file to open in editor + edit_parser = ACArgumentParser(description=edit_description) + setattr(edit_parser.add_argument('file_path', help="path to a file to open in editor", nargs="?"), + ACTION_ARG_CHOICES, ('path_complete',)) -The editor used is determined by the ``editor`` settable parameter. -"set editor (program-name)" to change or set the EDITOR environment variable. -""" + @with_argparser(edit_parser) + def do_edit(self, args: argparse.Namespace) -> None: + """Edit a file in a text editor""" if not self.editor: raise EnvironmentError("Please use 'set editor' to specify your text editing program of choice.") - filename = arglist[0] if arglist else '' - if filename: - os.system('"{}" "{}"'.format(self.editor, filename)) - else: - os.system('"{}"'.format(self.editor)) - def complete_edit(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """Enable tab-completion for edit command.""" - index_dict = {1: self.path_complete} - return self.index_based_complete(text, line, begidx, endidx, index_dict) + editor = utils.quote_string_if_needed(self.editor) + if args.file_path: + expanded_path = utils.quote_string_if_needed(os.path.expanduser(args.file_path)) + os.system('{} {}'.format(editor, expanded_path)) + else: + os.system('{}'.format(editor)) @property def _current_script_dir(self) -> Optional[str]: @@ -3364,30 +3363,26 @@ NOTE: This command is intended to only be used within text file scripts. file_path = arglist[0].strip() # 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) - self.do_load([relative_path]) + self.do_load(relative_path) - def do_eos(self, _: str) -> None: - """Handles cleanup when a script has finished executing""" + @with_argparser(ACArgumentParser()) + def do_eos(self, _: argparse.Namespace) -> None: + """Handle cleanup when a script has finished executing""" if self._script_dir: self._script_dir.pop() - @with_argument_list - def do_load(self, arglist: List[str]) -> None: - """Runs commands in script file that is encoded as either ASCII or UTF-8 text - - Usage: load <file_path> + load_description = "Run commands in script file that is encoded as either ASCII or UTF-8 text\n" + load_description += "\n" + load_description += "Script should contain one command per line, just like command would be typed in console" - * file_path - a file path pointing to a script - -Script should contain one command per line, just like command would be typed in console. - """ - # If arg is None or arg is an empty string this is an error - if not arglist: - self.perror('load command requires a file path', traceback_war=False) - return + load_parser = ACArgumentParser(description=load_description) + setattr(load_parser.add_argument('script_path', help="path to the script file"), + ACTION_ARG_CHOICES, ('path_complete',)) - file_path = arglist[0].strip() - expanded_path = os.path.abspath(os.path.expanduser(file_path)) + @with_argparser(load_parser) + def do_load(self, args: argparse.Namespace) -> None: + """Run commands in script file that is encoded as either ASCII or UTF-8 text""" + expanded_path = os.path.abspath(os.path.expanduser(args.script_path)) # Make sure the path exists and we can access it if not os.path.exists(expanded_path): @@ -3421,11 +3416,6 @@ Script should contain one command per line, just like command would be typed in self._script_dir.append(os.path.dirname(expanded_path)) - def complete_load(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """Enable tab-completion for load command.""" - index_dict = {1: self.path_complete} - return self.index_based_complete(text, line, begidx, endidx, index_dict) - def run_transcript_tests(self, callargs: List[str]) -> None: """Runs transcript tests for provided file(s). diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst index a03a1d08..0a95a829 100644 --- a/docs/freefeatures.rst +++ b/docs/freefeatures.rst @@ -174,13 +174,9 @@ More Python examples: Type "help", "copyright", "credits" or "license" for more information. (CmdLineApp) - Invoke python command, shell, or script - - py <command>: Executes a Python command. - py: Enters interactive Python mode. - End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``. - Non-python commands can be issued with ``app("your command")``. - Run python code from external script files with ``run("script.py")`` + End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`. + Non-python commands can be issued with: app("your command") + Run python code from external script files with: run("script.py") >>> import os >>> os.uname() diff --git a/tests/conftest.py b/tests/conftest.py index b86622ac..da6ffcd6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,14 +40,14 @@ alias Manage aliases edit Edit a file in a text editor help List available commands with "help" or detailed help with "help cmd" history View, run, edit, save, or clear previously entered commands -load Runs commands in script file that is encoded as either ASCII or UTF-8 text +load Run commands in script file that is encoded as either ASCII or UTF-8 text macro Manage macros -py Invoke python command, shell, or script -pyscript Runs a python script file inside the console -quit Exits this application -set Sets a settable parameter or shows current settings of parameters +py Invoke python command or shell +pyscript Run a python script file inside the console +quit Exit this application +set Set a settable parameter or show current settings of parameters shell Execute a command as if at the OS prompt -shortcuts Lists shortcuts available +shortcuts List shortcuts available """ # Help text for the history command diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 3cbde311..d3d1d585 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -70,7 +70,7 @@ def test_base_argparse_help(base_app, capsys): assert out1 == out2 assert out1[0].startswith('Usage: set') assert out1[1] == '' - assert out1[2].startswith('Sets a settable parameter') + assert out1[2].startswith('Set a settable parameter') def test_base_invalid_option(base_app, capsys): run_cmd(base_app, 'set -z') @@ -477,7 +477,7 @@ def test_load_with_empty_args(base_app, capsys): out, err = capsys.readouterr() # The load command requires a file path argument, so we should get an error message - assert "load command requires a file path" in str(err) + assert "the following arguments are required" in str(err) assert base_app.cmdqueue == [] @@ -858,7 +858,8 @@ def test_edit_file(base_app, request, monkeypatch): run_cmd(base_app, 'edit {}'.format(filename)) # We think we have an editor, so should expect a system call - m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename)) + m.assert_called_once_with('{} {}'.format(utils.quote_string_if_needed(base_app.editor), + utils.quote_string_if_needed(filename))) 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 @@ -874,7 +875,8 @@ def test_edit_file_with_spaces(base_app, request, monkeypatch): run_cmd(base_app, 'edit "{}"'.format(filename)) # We think we have an editor, so should expect a system call - m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename)) + m.assert_called_once_with('{} {}'.format(utils.quote_string_if_needed(base_app.editor), + utils.quote_string_if_needed(filename))) def test_edit_blank(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock @@ -1252,14 +1254,14 @@ Other alias Manage aliases help List available commands with "help" or detailed help with "help cmd" history View, run, edit, save, or clear previously entered commands -load Runs commands in script file that is encoded as either ASCII or UTF-8 text +load Run commands in script file that is encoded as either ASCII or UTF-8 text macro Manage macros -py Invoke python command, shell, or script -pyscript Runs a python script file inside the console -quit Exits this application -set Sets a settable parameter or shows current settings of parameters +py Invoke python command or shell +pyscript Run a python script file inside the console +quit Exit this application +set Set a settable parameter or show current settings of parameters shell Execute a command as if at the OS prompt -shortcuts Lists shortcuts available +shortcuts List shortcuts available Undocumented commands: ====================== @@ -1556,7 +1558,7 @@ def test_is_text_file_bad_input(base_app): def test_eof(base_app): # Only thing to verify is that it returns True - assert base_app.do_eof('dont care') + assert base_app.do_eof('') def test_eos(base_app): sdir = 'dummy_dir' @@ -1564,7 +1566,7 @@ def test_eos(base_app): assert len(base_app._script_dir) == 1 # Assert that it does NOT return true - assert not base_app.do_eos('dont care') + assert not base_app.do_eos('') # And make sure it reduced the length of the script dir list assert len(base_app._script_dir) == 0 diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py index 84abc965..c7769c7a 100644 --- a/tests/test_pyscript.py +++ b/tests/test_pyscript.py @@ -84,7 +84,7 @@ class PyscriptExample(Cmd): @with_argparser(foo_parser) def do_foo(self, args): - self.poutput('foo ' + str(args.__dict__)) + self.poutput('foo ' + str(sorted(args.__dict__))) if self._in_py: FooResult = namedtuple_with_defaults('FooResult', ['counter', 'trueval', 'constval', |