diff options
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | cmd2/__init__.py | 7 | ||||
-rw-r--r-- | cmd2/cmd2.py | 293 | ||||
-rw-r--r-- | cmd2/constants.py | 25 | ||||
-rw-r--r-- | cmd2/decorators.py | 203 | ||||
-rw-r--r-- | docs/api/decorators.rst | 8 | ||||
-rw-r--r-- | docs/api/utility_functions.rst | 2 | ||||
-rw-r--r-- | docs/features/argument_processing.rst | 6 | ||||
-rwxr-xr-x | examples/help_categories.py | 2 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 4 | ||||
-rwxr-xr-x | tests/test_completion.py | 4 |
11 files changed, 285 insertions, 273 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 93bede62..8492b687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ * It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically sets the prog value of it and all its subparsers so that all usage statements contain the top level command name and not sys.argv[0]. +* Breaking changes + * Some constants were moved from cmd2.py to constants.py + * cmd2 command decorators were moved to decorators.py. If you were importing them via cmd2's __init__.py, then + there will be no issues. ## 0.9.19 (October 14, 2019) * Bug Fixes diff --git a/cmd2/__init__.py b/cmd2/__init__.py index c496a4f7..8e8a8845 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -12,7 +12,8 @@ except DistributionNotFound: from .ansi import style from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem -from .cmd2 import Cmd, Statement, EmptyStatement, categorize -from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category -from .constants import DEFAULT_SHORTCUTS +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 +from .parsing import Statement from .py_bridge import CommandResult diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 0a7097ba..23f45024 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -43,18 +43,18 @@ from collections import namedtuple from contextlib import redirect_stdout from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Type, Union -from . import Cmd2ArgumentParser, CompletionItem from . import ansi from . import constants from . import plugin from . import utils +from .argparse_custom import Cmd2ArgumentParser, CompletionItem from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer +from .decorators import with_argparser from .history import History, HistoryItem from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split - -# Set up readline from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt +# Set up readline if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ @@ -89,235 +89,6 @@ try: except ImportError: # pragma: no cover ipython_available = False -INTERNAL_COMMAND_EPILOG = ("Notes:\n" - " This command is for internal use and is not intended to be called from the\n" - " command line.") - -# All command functions start with this -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 - -# Used as the command name placeholder in disabled command messages. -COMMAND_NAME = "<COMMAND_NAME>" - -############################################################################################################ -# The following are optional attributes added to do_* command functions -############################################################################################################ - -# The custom help category a command belongs to -CMD_ATTR_HELP_CATEGORY = 'help_category' - -# The argparse parser for the command -CMD_ATTR_ARGPARSER = 'argparser' - -# Whether or not tokens are unquoted before sending to argparse -CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes' - - -def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None: - """Categorize a function. - - The help command output will group this function under the specified category heading - - :param func: function or list of functions to categorize - :param category: category to put it in - """ - if isinstance(func, Iterable): - for item in func: - setattr(item, CMD_ATTR_HELP_CATEGORY, category) - else: - setattr(func, CMD_ATTR_HELP_CATEGORY, category) - - -def with_category(category: str) -> Callable: - """A decorator to apply a category to a command function.""" - def cat_decorator(func): - categorize(func, category) - return func - return cat_decorator - - -def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]: - """A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user - typed. With this decorator, the decorated method will receive a list of arguments parsed from user input. - - :param args: Single-element positional argument list containing do_* method this decorator is wrapping - :param preserve_quotes: if True, then argument quotes will not be stripped - :return: function that gets passed a list of argument strings - """ - import functools - - def arg_decorator(func: Callable): - @functools.wraps(func) - def cmd_wrapper(cmd2_app, statement: Union[Statement, str]): - _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, - statement, - preserve_quotes) - - return func(cmd2_app, parsed_arglist) - - command_name = func.__name__[len(COMMAND_FUNC_PREFIX):] - cmd_wrapper.__doc__ = func.__doc__ - return cmd_wrapper - - if len(args) == 1 and callable(args[0]): - # noinspection PyTypeChecker - return arg_decorator(args[0]) - else: - # noinspection PyTypeChecker - return arg_decorator - - -# noinspection PyProtectedMember -def set_parser_prog(parser: argparse.ArgumentParser, prog: str): - """ - Recursively set prog attribute of a parser and all of its subparsers so that the root command - is a command name and not sys.argv[0]. - :param parser: the parser being edited - :param prog: value for the current parsers prog attribute - """ - # Set the prog value for this parser - parser.prog = prog - - # Set the prog value for the parser's subcommands - for action in parser._actions: - if isinstance(action, argparse._SubParsersAction): - - # Set the prog value for each subcommand - for sub_cmd, sub_cmd_parser in action.choices.items(): - sub_cmd_prog = parser.prog + ' ' + sub_cmd - set_parser_prog(sub_cmd_parser, sub_cmd_prog) - - # We can break since argparse only allows 1 group of subcommands per level - break - - -def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False) -> \ - Callable[[argparse.Namespace, List], Optional[bool]]: - """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given - instance of argparse.ArgumentParser, but also returning unknown args as a list. - - :param parser: unique instance of ArgumentParser - :param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an - argparse.Namespace. This is useful if the Namespace needs to be prepopulated with - state data that affects parsing. - :param preserve_quotes: if True, then arguments passed to argparse maintain their quotes - :return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings - A member called __statement__ is added to the Namespace to provide command functions access to the - Statement object. This can be useful if the command function needs to know the command line. - - """ - import functools - - def arg_decorator(func: Callable): - @functools.wraps(func) - def cmd_wrapper(cmd2_app, statement: Union[Statement, str]): - statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, - statement, - preserve_quotes) - - if ns_provider is None: - namespace = None - else: - namespace = ns_provider(cmd2_app) - - try: - args, unknown = parser.parse_known_args(parsed_arglist, namespace) - except SystemExit: - return - else: - setattr(args, '__statement__', statement) - return func(cmd2_app, args, unknown) - - # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command - command_name = func.__name__[len(COMMAND_FUNC_PREFIX):] - set_parser_prog(parser, command_name) - - # If the description has not been set, then use the method docstring if one exists - if parser.description is None and func.__doc__: - parser.description = func.__doc__ - - # Set the command's help text as argparser.description (which can be None) - cmd_wrapper.__doc__ = parser.description - - # Set some custom attributes for this command - setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser) - setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes) - - return cmd_wrapper - - # noinspection PyTypeChecker - return arg_decorator - - -def with_argparser(parser: argparse.ArgumentParser, *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]: - """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments - with the given instance of argparse.ArgumentParser. - - :param parser: unique instance of ArgumentParser - :param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an - argparse.Namespace. This is useful if the Namespace needs to be prepopulated with - state data that affects parsing. - :param preserve_quotes: if True, then arguments passed to argparse maintain their quotes - :return: function that gets passed the argparse-parsed args in a Namespace - A member called __statement__ is added to the Namespace to provide command functions access to the - Statement object. This can be useful if the command function needs to know the command line. - """ - import functools - - def arg_decorator(func: Callable): - @functools.wraps(func) - def cmd_wrapper(cmd2_app, statement: Union[Statement, str]): - statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, - statement, - preserve_quotes) - - if ns_provider is None: - namespace = None - else: - namespace = ns_provider(cmd2_app) - - try: - args = parser.parse_args(parsed_arglist, namespace) - except SystemExit: - return - else: - setattr(args, '__statement__', statement) - return func(cmd2_app, args) - - # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command - command_name = func.__name__[len(COMMAND_FUNC_PREFIX):] - set_parser_prog(parser, command_name) - - # If the description has not been set, then use the method docstring if one exists - if parser.description is None and func.__doc__: - parser.description = func.__doc__ - - # Set the command's help text as argparser.description (which can be None) - cmd_wrapper.__doc__ = parser.description - - # Set some custom attributes for this command - setattr(cmd_wrapper, CMD_ATTR_ARGPARSER, parser) - setattr(cmd_wrapper, CMD_ATTR_PRESERVE_QUOTES, preserve_quotes) - - return cmd_wrapper - - # noinspection PyTypeChecker - return arg_decorator - class _SavedReadlineSettings: """readline settings that are backed up when switching between readline environments""" @@ -361,6 +132,14 @@ class Cmd(cmd.Cmd): """ DEFAULT_EDITOR = utils.find_editor() + INTERNAL_COMMAND_EPILOG = ("Notes:\n" + " This command is for internal use and is not intended to be called from the\n" + " command line.") + + # Sorting keys for strings + ALPHABETICAL_SORT_KEY = utils.norm_fold + NATURAL_SORT_KEY = utils.natural_keys + def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, persistent_history_file: str = '', persistent_history_length: int = 1000, startup_script: str = '', use_ipython: bool = False, @@ -415,7 +194,7 @@ class Cmd(cmd.Cmd): self.continuation_prompt = '> ' self.debug = False self.echo = False - self.editor = self.DEFAULT_EDITOR + self.editor = Cmd.DEFAULT_EDITOR self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing) self.locals_in_py = False @@ -551,7 +330,7 @@ class Cmd(cmd.Cmd): # command and category names # alias, macro, settable, and shortcut names # tab completion results when self.matches_sorted is False - self.default_sort_key = ALPHABETICAL_SORT_KEY + self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY ############################################################################################################ # The following variables are used by tab-completion functions. They are reset each time complete() is run @@ -1443,18 +1222,18 @@ class Cmd(cmd.Cmd): # Check if a command was entered elif command in self.get_all_commands(): # Get the completer function for this command - compfunc = getattr(self, COMPLETER_FUNC_PREFIX + command, None) + compfunc = getattr(self, constants.COMPLETER_FUNC_PREFIX + command, None) if compfunc is None: # There's no completer function, next see if the command uses argparse func = self.cmd_func(command) - argparser = getattr(func, CMD_ATTR_ARGPARSER, None) + argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None) if func is not None and argparser is not None: import functools compfunc = functools.partial(self._autocomplete_default, argparser=argparser, - preserve_quotes=getattr(func, CMD_ATTR_PRESERVE_QUOTES)) + preserve_quotes=getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES)) else: compfunc = self.completedefault @@ -1642,8 +1421,8 @@ class Cmd(cmd.Cmd): def get_all_commands(self) -> List[str]: """Return a list of all commands""" - return [name[len(COMMAND_FUNC_PREFIX):] for name in self.get_names() - if name.startswith(COMMAND_FUNC_PREFIX) and callable(getattr(self, name))] + return [name[len(constants.COMMAND_FUNC_PREFIX):] for name in self.get_names() + if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name))] def get_visible_commands(self) -> List[str]: """Return a list of commands that have not been hidden or disabled""" @@ -1671,8 +1450,8 @@ class Cmd(cmd.Cmd): def get_help_topics(self) -> List[str]: """Return a list of help topics""" - all_topics = [name[len(HELP_FUNC_PREFIX):] for name in self.get_names() - if name.startswith(HELP_FUNC_PREFIX) and callable(getattr(self, name))] + all_topics = [name[len(constants.HELP_FUNC_PREFIX):] for name in self.get_names() + if name.startswith(constants.HELP_FUNC_PREFIX) and callable(getattr(self, name))] # Filter out hidden and disabled commands return [topic for topic in all_topics @@ -2157,7 +1936,7 @@ class Cmd(cmd.Cmd): :param command: command to look up method name which implements it :return: method name which implements the given command """ - target = COMMAND_FUNC_PREFIX + command + target = constants.COMMAND_FUNC_PREFIX + command return target if callable(getattr(self, target, None)) else '' # noinspection PyMethodOverriding @@ -2700,7 +2479,7 @@ class Cmd(cmd.Cmd): # Check if this command uses argparse func = self.cmd_func(command) - argparser = getattr(func, CMD_ATTR_ARGPARSER, None) + argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None) if func is None or argparser is None: return [] @@ -2733,8 +2512,8 @@ class Cmd(cmd.Cmd): else: # Getting help for a specific command func = self.cmd_func(args.command) - help_func = getattr(self, HELP_FUNC_PREFIX + args.command, None) - argparser = getattr(func, CMD_ATTR_ARGPARSER, None) + help_func = getattr(self, constants.HELP_FUNC_PREFIX + args.command, None) + argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None) # If the command function uses argparse, then use argparse's help if func is not None and argparser is not None: @@ -2778,11 +2557,11 @@ class Cmd(cmd.Cmd): help_topics.remove(command) # Non-argparse commands can have help_functions for their documentation - if not hasattr(func, CMD_ATTR_ARGPARSER): + if not hasattr(func, constants.CMD_ATTR_ARGPARSER): has_help_func = True - if hasattr(func, CMD_ATTR_HELP_CATEGORY): - category = getattr(func, CMD_ATTR_HELP_CATEGORY) + if hasattr(func, constants.CMD_ATTR_HELP_CATEGORY): + category = getattr(func, constants.CMD_ATTR_HELP_CATEGORY) cmds_cats.setdefault(category, []) cmds_cats[category].append(command) elif func.__doc__ or has_help_func: @@ -2835,8 +2614,8 @@ class Cmd(cmd.Cmd): cmd_func = self.cmd_func(command) # Non-argparse commands can have help_functions for their documentation - if not hasattr(cmd_func, CMD_ATTR_ARGPARSER) and command in topics: - help_func = getattr(self, HELP_FUNC_PREFIX + command) + if not hasattr(cmd_func, constants.CMD_ATTR_ARGPARSER) and command in topics: + help_func = getattr(self, constants.HELP_FUNC_PREFIX + command) result = io.StringIO() # try to redirect system stdout @@ -4052,8 +3831,8 @@ class Cmd(cmd.Cmd): if command not in self.disabled_commands: return - help_func_name = HELP_FUNC_PREFIX + command - completer_func_name = COMPLETER_FUNC_PREFIX + command + help_func_name = constants.HELP_FUNC_PREFIX + command + completer_func_name = constants.COMPLETER_FUNC_PREFIX + command # Restore the command function to its original value dc = self.disabled_commands[command] @@ -4081,7 +3860,7 @@ class Cmd(cmd.Cmd): """ for cmd_name in list(self.disabled_commands): func = self.disabled_commands[cmd_name].command_function - if getattr(func, CMD_ATTR_HELP_CATEGORY, None) == category: + if getattr(func, constants.CMD_ATTR_HELP_CATEGORY, None) == category: self.enable_command(cmd_name) def disable_command(self, command: str, message_to_print: str) -> None: @@ -4105,8 +3884,8 @@ class Cmd(cmd.Cmd): if command_function is None: raise AttributeError("{} does not refer to a command".format(command)) - help_func_name = HELP_FUNC_PREFIX + command - completer_func_name = COMPLETER_FUNC_PREFIX + command + help_func_name = constants.HELP_FUNC_PREFIX + command + completer_func_name = constants.COMPLETER_FUNC_PREFIX + command # Add the disabled command record self.disabled_commands[command] = DisabledCommand(command_function=command_function, @@ -4115,7 +3894,7 @@ class Cmd(cmd.Cmd): # Overwrite the command and help functions to print the message new_func = functools.partial(self._report_disabled_command_usage, - message_to_print=message_to_print.replace(COMMAND_NAME, command)) + message_to_print=message_to_print.replace(constants.COMMAND_NAME, command)) setattr(self, self._cmd_func_name(command), new_func) setattr(self, help_func_name, new_func) @@ -4135,7 +3914,7 @@ class Cmd(cmd.Cmd): for cmd_name in all_commands: func = self.cmd_func(cmd_name) - if getattr(func, CMD_ATTR_HELP_CATEGORY, None) == category: + if getattr(func, constants.CMD_ATTR_HELP_CATEGORY, None) == category: self.disable_command(cmd_name, message_to_print) def _report_disabled_command_usage(self, *_args, message_to_print: str, **_kwargs) -> None: diff --git a/cmd2/constants.py b/cmd2/constants.py index 9fd58b01..9e8e7780 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -16,3 +16,28 @@ MULTILINE_TERMINATOR = ';' LINE_FEED = '\n' DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'} + +# Used as the command name placeholder in disabled command messages. +COMMAND_NAME = "<COMMAND_NAME>" + +# All command functions start with this +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_' + +############################################################################################################ +# The following are optional attributes added to do_* command functions +############################################################################################################ + +# The custom help category a command belongs to +CMD_ATTR_HELP_CATEGORY = 'help_category' + +# The argparse parser for the command +CMD_ATTR_ARGPARSER = 'argparser' + +# Whether or not tokens are unquoted before sending to argparse +CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes' diff --git a/cmd2/decorators.py b/cmd2/decorators.py new file mode 100644 index 00000000..2c812345 --- /dev/null +++ b/cmd2/decorators.py @@ -0,0 +1,203 @@ +# coding=utf-8 +"""Decorators for cmd2 commands""" +import argparse +from typing import Callable, Iterable, List, Optional, Union + +from . import constants +from .parsing import Statement + + +def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None: + """Categorize a function. + + The help command output will group this function under the specified category heading + + :param func: function or list of functions to categorize + :param category: category to put it in + """ + if isinstance(func, Iterable): + for item in func: + setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category) + else: + setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) + + +def with_category(category: str) -> Callable: + """A decorator to apply a category to a command function.""" + def cat_decorator(func): + categorize(func, category) + return func + return cat_decorator + + +def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]: + """A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user + typed. With this decorator, the decorated method will receive a list of arguments parsed from user input. + + :param args: Single-element positional argument list containing do_* method this decorator is wrapping + :param preserve_quotes: if True, then argument quotes will not be stripped + :return: function that gets passed a list of argument strings + """ + import functools + + def arg_decorator(func: Callable): + @functools.wraps(func) + def cmd_wrapper(cmd2_app, statement: Union[Statement, str]): + _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, + statement, + preserve_quotes) + + return func(cmd2_app, parsed_arglist) + + command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):] + cmd_wrapper.__doc__ = func.__doc__ + return cmd_wrapper + + if len(args) == 1 and callable(args[0]): + # noinspection PyTypeChecker + return arg_decorator(args[0]) + else: + # noinspection PyTypeChecker + return arg_decorator + + +# noinspection PyProtectedMember +def set_parser_prog(parser: argparse.ArgumentParser, prog: str): + """ + Recursively set prog attribute of a parser and all of its subparsers so that the root command + is a command name and not sys.argv[0]. + :param parser: the parser being edited + :param prog: value for the current parsers prog attribute + """ + # Set the prog value for this parser + parser.prog = prog + + # Set the prog value for the parser's subcommands + for action in parser._actions: + if isinstance(action, argparse._SubParsersAction): + + # Set the prog value for each subcommand + for sub_cmd, sub_cmd_parser in action.choices.items(): + sub_cmd_prog = parser.prog + ' ' + sub_cmd + set_parser_prog(sub_cmd_parser, sub_cmd_prog) + + # We can break since argparse only allows 1 group of subcommands per level + break + + +def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *, + ns_provider: Optional[Callable[..., argparse.Namespace]] = None, + preserve_quotes: bool = False) -> \ + Callable[[argparse.Namespace, List], Optional[bool]]: + """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given + instance of argparse.ArgumentParser, but also returning unknown args as a list. + + :param parser: unique instance of ArgumentParser + :param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an + argparse.Namespace. This is useful if the Namespace needs to be prepopulated with + state data that affects parsing. + :param preserve_quotes: if True, then arguments passed to argparse maintain their quotes + :return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings + A member called __statement__ is added to the Namespace to provide command functions access to the + Statement object. This can be useful if the command function needs to know the command line. + + """ + import functools + + def arg_decorator(func: Callable): + @functools.wraps(func) + def cmd_wrapper(cmd2_app, statement: Union[Statement, str]): + statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, + statement, + preserve_quotes) + + if ns_provider is None: + namespace = None + else: + namespace = ns_provider(cmd2_app) + + try: + args, unknown = parser.parse_known_args(parsed_arglist, namespace) + except SystemExit: + return + else: + setattr(args, '__statement__', statement) + return func(cmd2_app, args, unknown) + + # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command + command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):] + set_parser_prog(parser, command_name) + + # If the description has not been set, then use the method docstring if one exists + if parser.description is None and func.__doc__: + parser.description = func.__doc__ + + # Set the command's help text as argparser.description (which can be None) + cmd_wrapper.__doc__ = parser.description + + # Set some custom attributes for this command + setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser) + setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes) + + return cmd_wrapper + + # noinspection PyTypeChecker + return arg_decorator + + +def with_argparser(parser: argparse.ArgumentParser, *, + ns_provider: Optional[Callable[..., argparse.Namespace]] = None, + preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]: + """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments + with the given instance of argparse.ArgumentParser. + + :param parser: unique instance of ArgumentParser + :param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an + argparse.Namespace. This is useful if the Namespace needs to be prepopulated with + state data that affects parsing. + :param preserve_quotes: if True, then arguments passed to argparse maintain their quotes + :return: function that gets passed the argparse-parsed args in a Namespace + A member called __statement__ is added to the Namespace to provide command functions access to the + Statement object. This can be useful if the command function needs to know the command line. + """ + import functools + + def arg_decorator(func: Callable): + @functools.wraps(func) + def cmd_wrapper(cmd2_app, statement: Union[Statement, str]): + statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, + statement, + preserve_quotes) + + if ns_provider is None: + namespace = None + else: + namespace = ns_provider(cmd2_app) + + try: + args = parser.parse_args(parsed_arglist, namespace) + except SystemExit: + return + else: + setattr(args, '__statement__', statement) + return func(cmd2_app, args) + + # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command + command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):] + set_parser_prog(parser, command_name) + + # If the description has not been set, then use the method docstring if one exists + if parser.description is None and func.__doc__: + parser.description = func.__doc__ + + # Set the command's help text as argparser.description (which can be None) + cmd_wrapper.__doc__ = parser.description + + # Set some custom attributes for this command + setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser) + setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes) + + return cmd_wrapper + + # noinspection PyTypeChecker + return arg_decorator diff --git a/docs/api/decorators.rst b/docs/api/decorators.rst index d7bfa138..a5fc022e 100644 --- a/docs/api/decorators.rst +++ b/docs/api/decorators.rst @@ -1,10 +1,10 @@ Decorators ========== -.. autofunction:: cmd2.cmd2.with_category +.. autofunction:: cmd2.decorators.with_category -.. autofunction:: cmd2.cmd2.with_argument_list +.. autofunction:: cmd2.decorators.with_argument_list -.. autofunction:: cmd2.cmd2.with_argparser_and_unknown_args +.. autofunction:: cmd2.decorators.with_argparser_and_unknown_args -.. autofunction:: cmd2.cmd2.with_argparser +.. autofunction:: cmd2.decorators.with_argparser diff --git a/docs/api/utility_functions.rst b/docs/api/utility_functions.rst index 57a720bf..86fb656c 100644 --- a/docs/api/utility_functions.rst +++ b/docs/api/utility_functions.rst @@ -7,7 +7,7 @@ Utility Functions .. autofunction:: cmd2.utils.strip_quotes -.. autofunction:: cmd2.cmd2.categorize +.. autofunction:: cmd2.decorators.categorize .. autofunction:: cmd2.utils.center_text diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst index 4d87282c..a3d4a3aa 100644 --- a/docs/features/argument_processing.rst +++ b/docs/features/argument_processing.rst @@ -40,11 +40,11 @@ Decorators provided by cmd2 for argument processing ``cmd2`` provides the following decorators for assisting with parsing arguments passed to commands: -.. automethod:: cmd2.cmd2.with_argument_list +.. automethod:: cmd2.decorators.with_argument_list :noindex: -.. automethod:: cmd2.cmd2.with_argparser +.. automethod:: cmd2.decorators.with_argparser :noindex: -.. automethod:: cmd2.cmd2.with_argparser_and_unknown_args +.. automethod:: cmd2.decorators.with_argparser_and_unknown_args :noindex: All of these decorators accept an optional **preserve_quotes** argument which diff --git a/examples/help_categories.py b/examples/help_categories.py index 80f367fa..602bf441 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -7,7 +7,7 @@ A sample application for tagging categories on commands. import argparse import cmd2 -from cmd2.cmd2 import COMMAND_NAME +from cmd2 import COMMAND_NAME class HelpCategories(cmd2.Cmd): diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 635e7ebd..cb66ac9b 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -20,7 +20,7 @@ except ImportError: from unittest import mock import cmd2 -from cmd2 import ansi, clipboard, constants, plugin, utils +from cmd2 import ansi, clipboard, constants, plugin, utils, COMMAND_NAME from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY from .conftest import SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, complete_tester @@ -2342,7 +2342,7 @@ def test_disabled_command_not_in_history(disable_commands_app): 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) + message_to_print = '{} is currently disabled'.format(COMMAND_NAME) disable_commands_app.disable_command('has_helper_funcs', message_to_print) out, err = run_cmd(disable_commands_app, 'has_helper_funcs') diff --git a/tests/test_completion.py b/tests/test_completion.py index 3b26b044..475b44dd 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -187,13 +187,13 @@ def test_default_sort_key(cmd2_app): begidx = endidx - len(text) # First do alphabetical sorting - cmd2_app.default_sort_key = cmd2.cmd2.ALPHABETICAL_SORT_KEY + cmd2_app.default_sort_key = cmd2.Cmd.ALPHABETICAL_SORT_KEY expected = ['1', '11', '2'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected # Now switch to natural sorting - cmd2_app.default_sort_key = cmd2.cmd2.NATURAL_SORT_KEY + cmd2_app.default_sort_key = cmd2.Cmd.NATURAL_SORT_KEY expected = ['1', '2', '11'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected |