diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-10-31 12:11:38 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-10-31 12:11:38 -0400 |
commit | 48420deeceaeb5793d4e2616eec7ca2f67c545e0 (patch) | |
tree | 825d550028788c8fd95c6785fdb5ece7174dc5d2 | |
parent | 245dc33730d2a27bc9744be7cf36f176f5a0c10a (diff) | |
download | cmd2-git-48420deeceaeb5793d4e2616eec7ca2f67c545e0.tar.gz |
Moving a bit of code around to shrink cmd2.py
This includes moving cmd2 decorators into a new file called decorators.py
Moved some constants from cmd2.py to constants.py
-rw-r--r-- | cmd2/__init__.py | 7 | ||||
-rw-r--r-- | cmd2/cmd2.py | 273 | ||||
-rw-r--r-- | cmd2/constants.py | 25 | ||||
-rw-r--r-- | cmd2/decorators.py | 203 | ||||
-rwxr-xr-x | examples/help_categories.py | 2 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 4 |
6 files changed, 261 insertions, 253 deletions
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..c303fd6f 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" \ @@ -93,231 +93,10 @@ 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""" @@ -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/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') |