From 70713213763310d1e2715db17f315eaf6cd1282b Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Wed, 14 Feb 2024 12:46:52 -0500 Subject: [PATCH 1/6] Add required args to subcommand program --- cmd2/decorators.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 3a163fda..c9fd159e 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -209,6 +209,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: """ # Set the prog value for this parser parser.prog = prog + req_args = [] # Set the prog value for the parser's subcommands for action in parser._actions: @@ -233,13 +234,18 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: if subcmd_parser in processed_parsers: continue - subcmd_prog = parser.prog + ' ' + subcmd_name + subcmd_prog = parser.prog + if req_args: + subcmd_prog += " " + " ".join(req_args) + subcmd_prog += " " + subcmd_name _set_parser_prog(subcmd_parser, subcmd_prog) processed_parsers.append(subcmd_parser) # We can break since argparse only allows 1 group of subcommands per level break - + # need to save required args so they can be prepended to the subcommand usage + elif action.required: + req_args.append(action.dest) #: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input #: and optionally returns a boolean From acd6806a2f012381fd5e605dfaabae855168acc9 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Thu, 15 Feb 2024 15:23:12 -0500 Subject: [PATCH 2/6] Address formatting issues --- cmd2/argparse_custom.py | 12 ++++-------- cmd2/decorators.py | 13 +++++-------- cmd2/history.py | 6 ++---- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2371fa54..595737c5 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -352,8 +352,7 @@ class ChoicesProviderFuncBase(Protocol): Function that returns a list of choices in support of tab completion """ - def __call__(self) -> List[str]: - ... # pragma: no cover + def __call__(self) -> List[str]: ... # pragma: no cover @runtime_checkable @@ -362,8 +361,7 @@ class ChoicesProviderFuncWithTokens(Protocol): Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments. """ - def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]: - ... # pragma: no cover + def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]: ... # pragma: no cover ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens] @@ -381,8 +379,7 @@ def __call__( line: str, begidx: int, endidx: int, - ) -> List[str]: - ... # pragma: no cover + ) -> List[str]: ... # pragma: no cover @runtime_checkable @@ -400,8 +397,7 @@ def __call__( endidx: int, *, arg_tokens: Dict[str, List[str]] = {}, - ) -> List[str]: - ... # pragma: no cover + ) -> List[str]: ... # pragma: no cover CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens] diff --git a/cmd2/decorators.py b/cmd2/decorators.py index c9fd159e..fdebdd4e 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -247,6 +247,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: elif action.required: req_args.append(action.dest) + #: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input #: and optionally returns a boolean ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]] @@ -272,8 +273,7 @@ def with_argparser( ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover @overload @@ -283,8 +283,7 @@ def with_argparser( ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover def with_argparser( @@ -424,8 +423,7 @@ def as_subcommand_to( *, help: Optional[str] = None, aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover @overload @@ -436,8 +434,7 @@ def as_subcommand_to( *, help: Optional[str] = None, aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover def as_subcommand_to( diff --git a/cmd2/history.py b/cmd2/history.py index a7d6baff..c79a19dd 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -154,12 +154,10 @@ def _zero_based_index(self, onebased: Union[int, str]) -> int: return result @overload - def append(self, new: HistoryItem) -> None: - ... # pragma: no cover + def append(self, new: HistoryItem) -> None: ... # pragma: no cover @overload - def append(self, new: Statement) -> None: - ... # pragma: no cover + def append(self, new: Statement) -> None: ... # pragma: no cover def append(self, new: Union[Statement, HistoryItem]) -> None: """Append a new statement to the end of the History list. From 19ed6ae98bc7d36e9485efadb8d8041810798b77 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Thu, 15 Feb 2024 15:28:03 -0500 Subject: [PATCH 3/6] Add type hint --- cmd2/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index fdebdd4e..6d0a139f 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -209,7 +209,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: """ # Set the prog value for this parser parser.prog = prog - req_args = [] + req_args: List[str] = [] # Set the prog value for the parser's subcommands for action in parser._actions: From 57212c980cc0cfbc47815e67eebbb289fe726f41 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Mon, 8 Jul 2024 07:49:55 -0400 Subject: [PATCH 4/6] Ignore E704 per PR feedback --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5d42015c..5f26578f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ addopts = [flake8] count = True -ignore = E203,W503 +ignore = E203,W503,E704 max-complexity = 26 max-line-length = 127 show-source = True From a6d63c68e110c16c8fc4ca044abb804551e80357 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Wed, 10 Jul 2024 08:00:29 -0400 Subject: [PATCH 5/6] Address mypy errors --- cmd2/cmd2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c5c0db78..c22fdf33 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -778,7 +778,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: cmdset.on_unregister() self._unregister_subcommands(cmdset) - methods = inspect.getmembers( + methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') @@ -809,7 +809,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: self._installed_command_sets.remove(cmdset) def _check_uninstallable(self, cmdset: CommandSet) -> None: - methods = inspect.getmembers( + methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') From ecbe0845b3735ce1a11d02b284c7e507ce9d83a3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 13 Sep 2024 11:35:29 -0400 Subject: [PATCH 6/6] Removed Python version check related to argparse parser deep copies since we only support Python 3.8+ now. --- cmd2/__init__.py | 8 ++------ cmd2/cmd2.py | 26 +++++++++----------------- cmd2/decorators.py | 3 ++- docs/features/argument_processing.rst | 7 ------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 9b3fdbc3..8f1f030e 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -5,12 +5,8 @@ import sys -# For python 3.8 and later -if sys.version_info >= (3, 8): - import importlib.metadata as importlib_metadata -else: - # For everyone else - import importlib_metadata +import importlib.metadata as importlib_metadata + try: __version__ = importlib_metadata.version(__name__) except importlib_metadata.PackageNotFoundError: # pragma: no cover diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6b9ea792..5f95b2ab 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -700,10 +700,7 @@ def _build_parser( elif callable(parser_builder): parser = parser_builder() elif isinstance(parser_builder, argparse.ArgumentParser): - if sys.version_info >= (3, 6, 4): - parser = copy.deepcopy(parser_builder) - else: # pragma: no cover - parser = parser_builder + parser = copy.deepcopy(parser_builder) return parser def _register_command_parser(self, command: str, command_method: Callable[..., Any]) -> None: @@ -780,7 +777,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, - predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type, var-annotated] + predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX), ) @@ -811,7 +808,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: def _check_uninstallable(self, cmdset: CommandSet) -> None: methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, - predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type, var-annotated] + predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX), ) @@ -3328,19 +3325,14 @@ def _cmdloop(self) -> None: ############################################################# # Top-level parser for alias - @staticmethod - def _build_alias_parser() -> argparse.ArgumentParser: - alias_description = ( - "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string." - ) - alias_epilog = "See also:\n" " macro" - alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) - alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - alias_subparsers.required = True - return alias_parser + alias_description = "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string." + alias_epilog = "See also:\n" " macro" + alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) + alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') + alias_subparsers.required = True # Preserve quotes since we are passing strings to other commands - @with_argparser(_build_alias_parser, preserve_quotes=True) + @with_argparser(alias_parser, preserve_quotes=True) def do_alias(self, args: argparse.Namespace) -> None: """Manage aliases""" # Call handler for whatever subcommand was selected diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 6d0a139f..3aca8f0a 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -243,7 +243,8 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: # We can break since argparse only allows 1 group of subcommands per level break - # need to save required args so they can be prepended to the subcommand usage + + # Need to save required args so they can be prepended to the subcommand usage elif action.required: req_args.append(action.dest) diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst index 16031200..d6090465 100644 --- a/docs/features/argument_processing.rst +++ b/docs/features/argument_processing.rst @@ -75,13 +75,6 @@ Here's what it looks like:: for i in range(min(repetitions, self.maxrepeats)): self.poutput(arg) -.. warning:: - - It is important that each command which uses the ``@with_argparser`` - decorator be passed a unique instance of a parser since command-specific - changes could be made to it. - - .. note:: The ``@with_argparser`` decorator sets the ``prog`` variable in the argument