diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | cmd2/__init__.py | 12 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 12 | ||||
-rw-r--r-- | cmd2/cmd2.py | 36 | ||||
-rw-r--r-- | examples/custom_parser.py | 35 | ||||
-rwxr-xr-x | examples/override_parser.py | 24 | ||||
-rw-r--r-- | tests/test_argparse_custom.py | 17 |
7 files changed, 118 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8264049b..d78bf0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Enhancements * Added `read_input()` function that is used to read from stdin. Unlike the Python built-in `input()`, it also has an argument to disable tab completion while input is being entered. + * Added capability to override the argument parser class used by cmd2 built-in commands. See override_parser.py + example for more details. ## 0.9.20 (November 12, 2019) * Bug Fixes diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 8e8a8845..8fc5e9f2 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -11,7 +11,17 @@ except DistributionNotFound: pass from .ansi import style -from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem +from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem, set_default_argument_parser + +# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER +import argparse +cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None) +if cmd2_parser_module is not None: + import importlib + importlib.import_module(cmd2_parser_module) + +# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER +from .argparse_custom import DEFAULT_ARGUMENT_PARSER from .cmd2 import Cmd, EmptyStatement from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index f7dbc8a3..c6aa6550 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -182,7 +182,7 @@ import re import sys # noinspection PyUnresolvedReferences,PyProtectedMember from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _ -from typing import Callable, Optional, Tuple, Union +from typing import Callable, Optional, Tuple, Type, Union from .ansi import ansi_aware_write, style_error @@ -806,3 +806,13 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): if file is None: file = sys.stderr ansi_aware_write(file, message) + + +# The default ArgumentParser class for a cmd2 app +DEFAULT_ARGUMENT_PARSER = Cmd2ArgumentParser + + +def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None: + """Set the default ArgumentParser class for a cmd2 app""" + global DEFAULT_ARGUMENT_PARSER + DEFAULT_ARGUMENT_PARSER = parser diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 29bf9529..bd9dd4ff 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -47,7 +47,7 @@ from . import ansi from . import constants from . import plugin from . import utils -from .argparse_custom import Cmd2ArgumentParser, CompletionItem +from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .decorators import with_argparser from .history import History, HistoryItem @@ -2244,7 +2244,7 @@ class Cmd(cmd.Cmd): "An alias is a command that enables replacement of a word by another string.") alias_epilog = ("See also:\n" " macro") - alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog) + alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) # Add subcommands to alias alias_subparsers = alias_parser.add_subparsers(dest='subcommand') @@ -2421,7 +2421,7 @@ class Cmd(cmd.Cmd): "A macro is similar to an alias, but it can contain argument placeholders.") macro_epilog = ("See also:\n" " alias") - macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog) + macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog) # Add subcommands to macro macro_subparsers = macro_parser.add_subparsers(dest='subcommand') @@ -2537,8 +2537,8 @@ class Cmd(cmd.Cmd): completer = AutoCompleter(argparser, self) return completer.complete_subcommand_help(tokens, text, line, begidx, endidx) - help_parser = Cmd2ArgumentParser(description="List available commands or provide " - "detailed help for a specific command") + help_parser = DEFAULT_ARGUMENT_PARSER(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('subcommands', nargs=argparse.REMAINDER, help="subcommand(s) to retrieve help for", @@ -2707,7 +2707,7 @@ class Cmd(cmd.Cmd): command = '' self.stdout.write("\n") - @with_argparser(Cmd2ArgumentParser(description="List available shortcuts")) + @with_argparser(DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")) def do_shortcuts(self, _: argparse.Namespace) -> None: """List available shortcuts""" # Sort the shortcut tuples by name @@ -2715,13 +2715,13 @@ class Cmd(cmd.Cmd): result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts) self.poutput("Shortcuts for other commands:\n{}".format(result)) - @with_argparser(Cmd2ArgumentParser(epilog=INTERNAL_COMMAND_EPILOG)) + @with_argparser(DEFAULT_ARGUMENT_PARSER(epilog=INTERNAL_COMMAND_EPILOG)) def do_eof(self, _: argparse.Namespace) -> bool: """Called when <Ctrl>-D is pressed""" # Return True to stop the command loop return True - @with_argparser(Cmd2ArgumentParser(description="Exit this application")) + @with_argparser(DEFAULT_ARGUMENT_PARSER(description="Exit this application")) def do_quit(self, _: argparse.Namespace) -> bool: """Exit this application""" # Return True to stop the command loop @@ -2824,7 +2824,7 @@ class Cmd(cmd.Cmd): "Accepts abbreviated parameter names so long as there is no ambiguity.\n" "Call without arguments for a list of settable parameters with their values.") - set_parser = Cmd2ArgumentParser(description=set_description) + set_parser = DEFAULT_ARGUMENT_PARSER(description=set_description) set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well') set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter') set_parser.add_argument('param', nargs=argparse.OPTIONAL, help='parameter to set or view', @@ -2869,7 +2869,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(description="Execute a command as if at the OS prompt") + shell_parser = DEFAULT_ARGUMENT_PARSER(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) @@ -3034,7 +3034,7 @@ class Cmd(cmd.Cmd): "If you see strange parsing behavior, it's best to just open the Python shell\n" "by providing no arguments to py and run more complex statements there.") - py_parser = Cmd2ArgumentParser(description=py_description) + py_parser = DEFAULT_ARGUMENT_PARSER(description=py_description) py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run") py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command") @@ -3156,7 +3156,7 @@ class Cmd(cmd.Cmd): return py_bridge.stop - run_pyscript_parser = Cmd2ArgumentParser(description="Run a Python script file inside the console") + run_pyscript_parser = DEFAULT_ARGUMENT_PARSER(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) @@ -3201,7 +3201,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(description="Enter an interactive IPython shell")) + @with_argparser(DEFAULT_ARGUMENT_PARSER(description="Enter an interactive IPython shell")) def do_ipy(self, _: argparse.Namespace) -> None: """Enter an interactive IPython shell""" from .py_bridge import PyBridge @@ -3232,7 +3232,7 @@ class Cmd(cmd.Cmd): history_description = "View, run, edit, save, or clear previously entered commands" - history_parser = Cmd2ArgumentParser(description=history_description) + history_parser = DEFAULT_ARGUMENT_PARSER(description=history_description) history_action_group = history_parser.add_mutually_exclusive_group() history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_action_group.add_argument('-e', '--edit', action='store_true', @@ -3543,7 +3543,7 @@ class Cmd(cmd.Cmd): "\n" " set editor (program-name)") - edit_parser = Cmd2ArgumentParser(description=edit_description) + edit_parser = DEFAULT_ARGUMENT_PARSER(description=edit_description) edit_parser.add_argument('file_path', nargs=argparse.OPTIONAL, help="optional path to a file to open in editor", completer_method=path_complete) @@ -3584,7 +3584,7 @@ class Cmd(cmd.Cmd): "If the -t/--transcript flag is used, this command instead records\n" "the output of the script commands to a transcript for testing purposes.\n") - run_script_parser = Cmd2ArgumentParser(description=run_script_description) + run_script_parser = DEFAULT_ARGUMENT_PARSER(description=run_script_description) run_script_parser.add_argument('-t', '--transcript', metavar='TRANSCRIPT_FILE', help='record the output of the script as a transcript file', completer_method=path_complete) @@ -3658,8 +3658,8 @@ class Cmd(cmd.Cmd): relative_run_script_epilog = ("Notes:\n" " This command is intended to only be used within text file scripts.") - relative_run_script_parser = Cmd2ArgumentParser(description=relative_run_script_description, - epilog=relative_run_script_epilog) + relative_run_script_parser = DEFAULT_ARGUMENT_PARSER(description=relative_run_script_description, + epilog=relative_run_script_epilog) relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script') @with_argparser(relative_run_script_parser) diff --git a/examples/custom_parser.py b/examples/custom_parser.py new file mode 100644 index 00000000..efeb362e --- /dev/null +++ b/examples/custom_parser.py @@ -0,0 +1,35 @@ +# coding=utf-8 +""" +Defines the CustomParser used with override_parser.py example +""" +import sys + +from cmd2 import Cmd2ArgumentParser, set_default_argument_parser +from cmd2.ansi import style_warning + + +# First define the parser +class CustomParser(Cmd2ArgumentParser): + """Overrides error class""" + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def error(self, message: str) -> None: + """Custom override that applies custom formatting to the error message""" + lines = message.split('\n') + linum = 0 + formatted_message = '' + for line in lines: + if linum == 0: + formatted_message = 'Error: ' + line + else: + formatted_message += '\n ' + line + linum += 1 + + self.print_usage(sys.stderr) + formatted_message = style_warning(formatted_message) + self.exit(2, '{}\n\n'.format(formatted_message)) + + +# Now set the default parser for a cmd2 app +set_default_argument_parser(CustomParser) diff --git a/examples/override_parser.py b/examples/override_parser.py new file mode 100755 index 00000000..ddfa8323 --- /dev/null +++ b/examples/override_parser.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# coding=utf-8 +# flake8: noqa F402 +""" +The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser. +The following code shows how to override it with your own parser class. +""" + +# First set a value called argparse.cmd2_parser_module with the module that defines the custom parser +# See the code for custom_parser.py. It simply defines a parser and calls cmd2.set_default_argument_parser() +# with the custom parser's type. +import argparse +argparse.cmd2_parser_module = 'examples.custom_parser' + +# Next import stuff from cmd2. It will import your module just before the cmd2.Cmd class file is imported +# and therefore override the parser class it uses on its commands. +from cmd2 import cmd2 + +if __name__ == '__main__': + import sys + app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat') + app.locals_in_py = True # Enable access to "self" within the py command + app.debug = True # Show traceback if/when an exception occurs + sys.exit(app.cmdloop()) diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index 65cbc8da..ce789f8e 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -242,3 +242,20 @@ def test_apcustom_required_options(): parser = Cmd2ArgumentParser() parser.add_argument('--required_flag', required=True) assert 'required arguments' in parser.format_help() + + +def test_override_parser(): + import importlib + from cmd2 import DEFAULT_ARGUMENT_PARSER + + # The standard parser is Cmd2ArgumentParser + assert DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser + + # Set our parser module and force a reload of cmd2 so it loads the module + argparse.cmd2_parser_module = 'examples.custom_parser' + importlib.reload(cmd2) + from cmd2 import DEFAULT_ARGUMENT_PARSER + + # Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser + from examples.custom_parser import CustomParser + assert DEFAULT_ARGUMENT_PARSER == CustomParser |