summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2021-08-23 14:20:09 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2021-08-23 14:20:09 -0400
commit16e145a27ea7b4c2dc348699c8f4cc11be4dc0b6 (patch)
treeeb0520a93dfd388360d5d816a53cf5f8a3b35d01
parent7f07f5ef66a4a3d986d265b8f7fc9d014f6d5541 (diff)
parent9d818100f3b6dfa647e58b8a0df182ea6729a197 (diff)
downloadcmd2-git-16e145a27ea7b4c2dc348699c8f4cc11be4dc0b6.tar.gz
Merge branch 'master' into topic_width
-rw-r--r--CHANGELOG.md21
-rw-r--r--cmd2/__init__.py20
-rw-r--r--cmd2/argparse_completer.py45
-rw-r--r--cmd2/argparse_custom.py84
-rw-r--r--cmd2/cmd2.py69
-rw-r--r--cmd2/constants.py1
-rw-r--r--cmd2/decorators.py7
-rw-r--r--examples/custom_parser.py2
-rwxr-xr-xexamples/override_parser.py9
-rwxr-xr-xsetup.py1
-rw-r--r--tests/test_argparse_completer.py53
-rw-r--r--tests/test_argparse_custom.py10
12 files changed, 257 insertions, 65 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e117b17..3bb53bef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,19 @@
## 2.2.0 (TBD, 2021)
* Enhancements
- * Using `SimpleTable` in the output for the following commands to improve appearance.
- * help
- * set (command and tab completion of Settables)
- * alias tab completion
- * macro tab completion
- * Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character.
- * Removed `--verbose` flag from set command since descriptions always show now.
+ * New function `set_default_command_completer_type()` allows developer to extend and modify the
+ behavior of `ArgparseCompleter`.
+ * New function `register_argparse_argument_parameter()` allows developers to specify custom
+ parameters to be passed to the argparse parser's `add_argument()` method. These parameters will
+ become accessible in the resulting argparse Action object when modifying `ArgparseCompleter` behavior.
+ * Using `SimpleTable` in the output for the following commands to improve appearance.
+ * help
+ * set (command and tab completion of Settables)
+ * alias tab completion
+ * macro tab completion
+ * Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character.
+ * Removed `--verbose` flag from set command since descriptions always show now.
* Deletions (potentially breaking changes)
- * Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
+ * Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
## 2.1.2 (July 5, 2021)
* Enhancements
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index e545f394..a23b7a36 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -5,7 +5,7 @@
import sys
-# For python 3.8 and late
+# For python 3.8 and later
if sys.version_info >= (3, 8):
import importlib.metadata as importlib_metadata
else:
@@ -20,9 +20,16 @@ except importlib_metadata.PackageNotFoundError: # pragma: no cover
from typing import List
from .ansi import style, fg, bg
-from .argparse_custom import Cmd2ArgumentParser, Cmd2AttributeWrapper, CompletionItem, set_default_argument_parser
+from .argparse_custom import (
+ Cmd2ArgumentParser,
+ Cmd2AttributeWrapper,
+ CompletionItem,
+ register_argparse_argument_parameter,
+ set_default_argument_parser,
+)
-# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
+# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER.
+# Do this before loading cmd2.Cmd class so its commands use the custom parser.
import argparse
cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None)
@@ -31,8 +38,8 @@ if cmd2_parser_module is not None:
importlib.import_module(cmd2_parser_module)
-# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
-from .argparse_custom import DEFAULT_ARGUMENT_PARSER
+from .argparse_completer import set_default_command_completer_type
+
from .cmd2 import Cmd
from .command_definition import CommandSet, with_default_category
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
@@ -46,7 +53,6 @@ from .utils import categorize, CompletionMode, CustomCompletionSettings, Settabl
__all__: List[str] = [
'COMMAND_NAME',
- 'DEFAULT_ARGUMENT_PARSER',
'DEFAULT_SHORTCUTS',
# ANSI Style exports
'bg',
@@ -56,7 +62,9 @@ __all__: List[str] = [
'Cmd2ArgumentParser',
'Cmd2AttributeWrapper',
'CompletionItem',
+ 'register_argparse_argument_parameter',
'set_default_argument_parser',
+ 'set_default_command_completer_type',
# Cmd2
'Cmd',
'CommandResult',
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 967e3f1c..ebc49a8c 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -13,18 +13,28 @@ from collections import (
deque,
)
from typing import (
+ TYPE_CHECKING,
Dict,
List,
Optional,
+ Type,
Union,
cast,
)
-from . import (
- ansi,
- cmd2,
- constants,
+from .ansi import (
+ style_aware_wcswidth,
+ widest_line,
)
+from .constants import (
+ INFINITY,
+)
+
+if TYPE_CHECKING:
+ from .cmd2 import (
+ Cmd,
+ )
+
from .argparse_custom import (
ChoicesCallable,
ChoicesProviderFuncWithTokens,
@@ -124,10 +134,10 @@ class _ArgumentState:
self.max = 1
elif self.action.nargs == argparse.ZERO_OR_MORE or self.action.nargs == argparse.REMAINDER:
self.min = 0
- self.max = constants.INFINITY
+ self.max = INFINITY
elif self.action.nargs == argparse.ONE_OR_MORE:
self.min = 1
- self.max = constants.INFINITY
+ self.max = INFINITY
else:
self.min = self.action.nargs
self.max = self.action.nargs
@@ -165,7 +175,7 @@ class ArgparseCompleter:
"""Automatic command line tab completion based on argparse parameters"""
def __init__(
- self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *, parent_tokens: Optional[Dict[str, List[str]]] = None
+ self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None
) -> None:
"""
Create an ArgparseCompleter
@@ -564,15 +574,15 @@ class ArgparseCompleter:
desc_header = desc_header.replace('\t', four_spaces)
# Calculate needed widths for the token and description columns of the table
- token_width = ansi.style_aware_wcswidth(destination)
- desc_width = ansi.widest_line(desc_header)
+ token_width = style_aware_wcswidth(destination)
+ desc_width = widest_line(desc_header)
for item in completion_items:
- token_width = max(ansi.style_aware_wcswidth(item), token_width)
+ token_width = max(style_aware_wcswidth(item), token_width)
# Replace tabs with 4 spaces so we can calculate width
item.description = item.description.replace('\t', four_spaces)
- desc_width = max(ansi.widest_line(item.description), desc_width)
+ desc_width = max(widest_line(item.description), desc_width)
cols = list()
cols.append(Column(destination.upper(), width=token_width))
@@ -728,3 +738,16 @@ class ArgparseCompleter:
return []
return self._format_completions(arg_state, results)
+
+
+DEFAULT_COMMAND_COMPLETER: Type[ArgparseCompleter] = ArgparseCompleter
+
+
+def set_default_command_completer_type(completer_type: Type[ArgparseCompleter]) -> None:
+ """
+ Set the default command completer type. It must be a sub-class of the ArgparseCompleter.
+
+ :param completer_type: Type that is a subclass of ArgparseCompleter.
+ """
+ global DEFAULT_COMMAND_COMPLETER
+ DEFAULT_COMMAND_COMPLETER = completer_type
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 461b4bba..44e7a90b 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -240,6 +240,7 @@ from typing import (
NoReturn,
Optional,
Sequence,
+ Set,
Tuple,
Type,
Union,
@@ -641,9 +642,74 @@ setattr(argparse.Action, 'set_suppress_tab_hint', _action_set_suppress_tab_hint)
############################################################################################################
+# Allow developers to add custom action attributes
+############################################################################################################
+
+CUSTOM_ACTION_ATTRIBS: Set[str] = set()
+_CUSTOM_ATTRIB_PFX = '_attr_'
+
+
+def register_argparse_argument_parameter(param_name: str, param_type: Optional[Type[Any]]) -> None:
+ """
+ Registers a custom argparse argument parameter.
+
+ The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function.
+
+ An accessor functions will be added to the parameter's Action object in the form of: ``get_{param_name}()``
+ and ``set_{param_name}(value)``.
+
+ :param param_name: Name of the parameter to add.
+ """
+ attr_name = f'{_CUSTOM_ATTRIB_PFX}{param_name}'
+ if param_name in CUSTOM_ACTION_ATTRIBS or hasattr(argparse.Action, attr_name):
+ raise KeyError(f'Custom parameter {param_name} already exists')
+ if not re.search('^[A-Za-z_][A-Za-z0-9_]*$', param_name):
+ raise KeyError(f'Invalid parameter name {param_name} - cannot be used as a python identifier')
+
+ getter_name = f'get_{param_name}'
+
+ def _action_get_custom_parameter(self: argparse.Action) -> Any:
+ f"""
+ Get the custom {param_name} attribute of an argparse Action.
+
+ This function is added by cmd2 as a method called ``{getter_name}()`` to ``argparse.Action`` class.
+
+ To call: ``action.{getter_name}()``
+
+ :param self: argparse Action being queried
+ :return: The value of {param_name} or None if attribute does not exist
+ """
+ return getattr(self, attr_name, None)
+
+ setattr(argparse.Action, getter_name, _action_get_custom_parameter)
+
+ setter_name = f'set_{param_name}'
+
+ def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None:
+ f"""
+ Set the custom {param_name} attribute of an argparse Action.
+
+ This function is added by cmd2 as a method called ``{setter_name}()`` to ``argparse.Action`` class.
+
+ To call: ``action.{setter_name}({param_name})``
+
+ :param self: argparse Action being updated
+ :param value: value being assigned
+ """
+ if param_type and not isinstance(value, param_type):
+ raise TypeError(f'{param_name} must be of type {param_type}, got: {value} ({type(value)})')
+ setattr(self, attr_name, value)
+
+ setattr(argparse.Action, setter_name, _action_set_custom_parameter)
+
+ CUSTOM_ACTION_ATTRIBS.add(param_name)
+
+
+############################################################################################################
# Patch _ActionsContainer.add_argument with our wrapper to support more arguments
############################################################################################################
+
# Save original _ActionsContainer.add_argument so we can call it in our wrapper
# noinspection PyProtectedMember
orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
@@ -753,6 +819,14 @@ def _add_argument_wrapper(
# Add the argparse-recognized version of nargs to kwargs
kwargs['nargs'] = nargs_adjusted
+ # Extract registered custom keyword arguments
+ custom_attribs: Dict[str, Any] = {}
+ for keyword, value in kwargs.items():
+ if keyword in CUSTOM_ACTION_ATTRIBS:
+ custom_attribs[keyword] = value
+ for keyword in custom_attribs:
+ del kwargs[keyword]
+
# Create the argument using the original add_argument function
new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
@@ -767,6 +841,11 @@ def _add_argument_wrapper(
new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined]
new_arg.set_descriptive_header(descriptive_header) # type: ignore[attr-defined]
+ for keyword, value in custom_attribs.items():
+ attr_setter = getattr(new_arg, f'set_{keyword}', None)
+ if attr_setter is not None:
+ attr_setter(value)
+
return new_arg
@@ -1243,6 +1322,9 @@ DEFAULT_ARGUMENT_PARSER: Type[argparse.ArgumentParser] = Cmd2ArgumentParser
def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None:
- """Set the default ArgumentParser class for a cmd2 app"""
+ """
+ Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if
+ you want to override the parser for cmd2's built-in commands. See examples/override_parser.py.
+ """
global DEFAULT_ARGUMENT_PARSER
DEFAULT_ARGUMENT_PARSER = parser
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 35398088..32fbc731 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -71,12 +71,13 @@ from typing import (
from . import (
ansi,
+ argparse_completer,
+ argparse_custom,
constants,
plugin,
utils,
)
from .argparse_custom import (
- DEFAULT_ARGUMENT_PARSER,
ChoicesProviderFunc,
CompleterFunc,
CompletionItem,
@@ -409,7 +410,7 @@ class Cmd(cmd.Cmd):
# Check for command line args
if allow_cli_args:
- parser = DEFAULT_ARGUMENT_PARSER()
+ parser = argparse_custom.DEFAULT_ARGUMENT_PARSER()
parser.add_argument('-t', '--test', action="store_true", help='Test against transcript(s) in FILE (wildcards OK)')
callopts, callargs = parser.parse_known_args()
@@ -1904,10 +1905,16 @@ class Cmd(cmd.Cmd):
# There's no completer function, next see if the command uses argparse
func = self.cmd_func(command)
argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None)
+ completer_type = getattr(func, constants.CMD_ATTR_COMPLETER, argparse_completer.DEFAULT_COMMAND_COMPLETER)
+ if completer_type is None:
+ completer_type = argparse_completer.DEFAULT_COMMAND_COMPLETER
if func is not None and argparser is not None:
cmd_set = self._cmd_to_command_sets[command] if command in self._cmd_to_command_sets else None
- completer = ArgparseCompleter(argparser, self)
+ if completer_type is not None:
+ completer = completer_type(argparser, self)
+ else:
+ completer = ArgparseCompleter(argparser, self)
preserve_quotes = getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES)
completer_func = functools.partial(
@@ -2076,7 +2083,7 @@ class Cmd(cmd.Cmd):
break
else:
# No shortcut was found. Complete the command token.
- parser = DEFAULT_ARGUMENT_PARSER(add_help=False)
+ parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(add_help=False)
parser.add_argument(
'command',
metavar="COMMAND",
@@ -2910,7 +2917,7 @@ class Cmd(cmd.Cmd):
# Set custom completion settings
else:
if parser is None:
- parser = DEFAULT_ARGUMENT_PARSER(add_help=False)
+ parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(add_help=False)
parser.add_argument(
'arg',
suppress_tab_hint=True,
@@ -3112,7 +3119,7 @@ class Cmd(cmd.Cmd):
# Top-level parser for alias
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 = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
+ 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
@@ -3141,7 +3148,9 @@ class Cmd(cmd.Cmd):
" alias create save_results print_results \">\" out.txt\n"
)
- alias_create_parser = DEFAULT_ARGUMENT_PARSER(description=alias_create_description, epilog=alias_create_epilog)
+ alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
+ description=alias_create_description, epilog=alias_create_epilog
+ )
alias_create_parser.add_argument('name', help='name of this alias')
alias_create_parser.add_argument(
'command', help='what the alias resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
@@ -3187,7 +3196,7 @@ class Cmd(cmd.Cmd):
alias_delete_help = "delete aliases"
alias_delete_description = "Delete specified aliases or all aliases if --all is used"
- alias_delete_parser = DEFAULT_ARGUMENT_PARSER(description=alias_delete_description)
+ alias_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_delete_description)
alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
alias_delete_parser.add_argument(
'names',
@@ -3222,7 +3231,7 @@ class Cmd(cmd.Cmd):
"Without arguments, all aliases will be listed."
)
- alias_list_parser = DEFAULT_ARGUMENT_PARSER(description=alias_list_description)
+ alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description)
alias_list_parser.add_argument(
'names',
nargs=argparse.ZERO_OR_MORE,
@@ -3270,7 +3279,7 @@ class Cmd(cmd.Cmd):
# Top-level parser for macro
macro_description = "Manage macros\n" "\n" "A macro is similar to an alias, but it can contain argument placeholders."
macro_epilog = "See also:\n" " alias"
- macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
+ macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
macro_subparsers.required = True
@@ -3323,7 +3332,9 @@ class Cmd(cmd.Cmd):
" will only complete paths while typing a macro."
)
- macro_create_parser = DEFAULT_ARGUMENT_PARSER(description=macro_create_description, epilog=macro_create_epilog)
+ macro_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
+ description=macro_create_description, epilog=macro_create_epilog
+ )
macro_create_parser.add_argument('name', help='name of this macro')
macro_create_parser.add_argument(
'command', help='what the macro resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
@@ -3413,7 +3424,7 @@ class Cmd(cmd.Cmd):
# macro -> delete
macro_delete_help = "delete macros"
macro_delete_description = "Delete specified macros or all macros if --all is used"
- macro_delete_parser = DEFAULT_ARGUMENT_PARSER(description=macro_delete_description)
+ macro_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_delete_description)
macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
macro_delete_parser.add_argument(
'names',
@@ -3448,7 +3459,7 @@ class Cmd(cmd.Cmd):
"Without arguments, all macros will be listed."
)
- macro_list_parser = DEFAULT_ARGUMENT_PARSER(description=macro_list_description)
+ macro_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_list_description)
macro_list_parser.add_argument(
'names',
nargs=argparse.ZERO_OR_MORE,
@@ -3521,7 +3532,7 @@ class Cmd(cmd.Cmd):
completer = ArgparseCompleter(argparser, self)
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
- help_parser = DEFAULT_ARGUMENT_PARSER(
+ help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
description="List available commands or provide " "detailed help for a specific command"
)
help_parser.add_argument(
@@ -3777,7 +3788,7 @@ class Cmd(cmd.Cmd):
self.poutput(table_str_buf.getvalue())
- shortcuts_parser = DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")
+ shortcuts_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")
@with_argparser(shortcuts_parser)
def do_shortcuts(self, _: argparse.Namespace) -> None:
@@ -3787,7 +3798,9 @@ class Cmd(cmd.Cmd):
result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts)
self.poutput(f"Shortcuts for other commands:\n{result}")
- eof_parser = DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG)
+ eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
+ description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG
+ )
@with_argparser(eof_parser)
def do_eof(self, _: argparse.Namespace) -> Optional[bool]:
@@ -3800,7 +3813,7 @@ class Cmd(cmd.Cmd):
# noinspection PyTypeChecker
return self.do_quit('')
- quit_parser = DEFAULT_ARGUMENT_PARSER(description="Exit this application")
+ quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application")
@with_argparser(quit_parser)
def do_quit(self, _: argparse.Namespace) -> Optional[bool]:
@@ -3868,7 +3881,7 @@ class Cmd(cmd.Cmd):
raise CompletionError(param + " is not a settable parameter")
# Create a parser with a value field based on this settable
- settable_parser = DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent])
+ settable_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent])
# Settables with choices list the values of those choices instead of the arg name
# in help text and this shows in tab completion hints. Set metavar to avoid this.
@@ -3899,7 +3912,7 @@ class Cmd(cmd.Cmd):
"Call without arguments for a list of all settable parameters with their values.\n"
"Call with just param to view that parameter's value."
)
- set_parser_parent = DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False)
+ set_parser_parent = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False)
set_parser_parent.add_argument(
'param',
nargs=argparse.OPTIONAL,
@@ -3909,7 +3922,7 @@ class Cmd(cmd.Cmd):
)
# Create the parser for the set command
- set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
+ set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
set_parser.add_argument(
'value', nargs=argparse.OPTIONAL, help='new value for settable', completer=complete_set_value, suppress_tab_hint=True
)
@@ -3967,7 +3980,7 @@ class Cmd(cmd.Cmd):
row_data = [param, settable.get_value(), settable.description]
self.poutput(table.generate_data_row(row_data))
- shell_parser = DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
+ shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
shell_parser.add_argument('command', help='the command to run', completer=shell_cmd_complete)
shell_parser.add_argument(
'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
@@ -4274,7 +4287,7 @@ class Cmd(cmd.Cmd):
return py_bridge.stop
- py_parser = DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell")
+ py_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell")
@with_argparser(py_parser)
def do_py(self, _: argparse.Namespace) -> Optional[bool]:
@@ -4284,7 +4297,7 @@ class Cmd(cmd.Cmd):
"""
return self._run_python()
- run_pyscript_parser = DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console")
+ run_pyscript_parser = argparse_custom.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=path_complete)
run_pyscript_parser.add_argument(
'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer=path_complete
@@ -4321,7 +4334,7 @@ class Cmd(cmd.Cmd):
return py_return
- ipython_parser = DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
+ ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
# noinspection PyPackageRequirements
@with_argparser(ipython_parser)
@@ -4391,7 +4404,7 @@ class Cmd(cmd.Cmd):
history_description = "View, run, edit, save, or clear previously entered commands"
- history_parser = DEFAULT_ARGUMENT_PARSER(description=history_description)
+ history_parser = argparse_custom.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', help='edit and then run selected history items')
@@ -4728,7 +4741,7 @@ class Cmd(cmd.Cmd):
" set editor (program-name)"
)
- edit_parser = DEFAULT_ARGUMENT_PARSER(description=edit_description)
+ edit_parser = argparse_custom.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=path_complete
)
@@ -4773,7 +4786,7 @@ class Cmd(cmd.Cmd):
"the output of the script commands to a transcript for testing purposes.\n"
)
- run_script_parser = DEFAULT_ARGUMENT_PARSER(description=run_script_description)
+ run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description)
run_script_parser.add_argument(
'-t',
'--transcript',
@@ -4842,7 +4855,7 @@ 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 = DEFAULT_ARGUMENT_PARSER(
+ relative_run_script_parser = argparse_custom.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')
diff --git a/cmd2/constants.py b/cmd2/constants.py
index 9f29be86..7656ae58 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -43,6 +43,7 @@ CLASS_ATTR_DEFAULT_HELP_CATEGORY = 'cmd2_default_help_category'
# The argparse parser for the command
CMD_ATTR_ARGPARSER = 'argparser'
+CMD_ATTR_COMPLETER = 'command_completer'
# 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
index 1ff0bdbe..644a8add 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -10,12 +10,16 @@ from typing import (
Optional,
Sequence,
Tuple,
+ Type,
Union,
)
from . import (
constants,
)
+from .argparse_completer import (
+ ArgparseCompleter,
+)
from .argparse_custom import (
Cmd2AttributeWrapper,
)
@@ -271,6 +275,7 @@ def with_argparser(
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False,
with_unknown_args: bool = False,
+ completer: Optional[Type[ArgparseCompleter]] = None,
) -> Callable[[ArgparseCommandFunc], RawCommandFuncOptionalBoolReturn]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.
@@ -281,6 +286,7 @@ def with_argparser(
state data that affects parsing.
:param preserve_quotes: if ``True``, then arguments passed to argparse maintain their quotes
:param with_unknown_args: if true, then capture unknown args
+ :param completer: CommandCompleter type. Defaults to ArgparseCompleter if unspecified.
:return: function that gets passed argparse-parsed args in a ``Namespace``
A :class:`cmd2.argparse_custom.Cmd2AttributeWrapper` called ``cmd2_statement`` is included
in the ``Namespace`` to provide access to the :class:`cmd2.Statement` object that was created when
@@ -391,6 +397,7 @@ def with_argparser(
# Set some custom attributes for this command
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
+ setattr(cmd_wrapper, constants.CMD_ATTR_COMPLETER, completer)
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
return cmd_wrapper
diff --git a/examples/custom_parser.py b/examples/custom_parser.py
index 6df68bfa..ea66e7e1 100644
--- a/examples/custom_parser.py
+++ b/examples/custom_parser.py
@@ -31,6 +31,8 @@ class CustomParser(Cmd2ArgumentParser):
linum += 1
self.print_usage(sys.stderr)
+
+ # Format errors with style_warning()
formatted_message = ansi.style_warning(formatted_message)
self.exit(2, '{}\n\n'.format(formatted_message))
diff --git a/examples/override_parser.py b/examples/override_parser.py
index 00161ef6..2e778c07 100755
--- a/examples/override_parser.py
+++ b/examples/override_parser.py
@@ -6,20 +6,19 @@ 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
+# 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
-# Next import stuff from cmd2. It will import your module just before the cmd2.Cmd class file is imported
+argparse.cmd2_parser_module = 'examples.custom_parser'
+
+# Next import 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,
)
-argparse.cmd2_parser_module = 'examples.custom_parser'
-
-
if __name__ == '__main__':
import sys
diff --git a/setup.py b/setup.py
index d2d2e955..15b0ae7b 100755
--- a/setup.py
+++ b/setup.py
@@ -83,6 +83,7 @@ EXTRAS_REQUIRE = {
'validate': [
'flake8',
'mypy==0.902',
+ 'types-pkg-resources',
],
}
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
index 25a13157..9c816e5e 100644
--- a/tests/test_argparse_completer.py
+++ b/tests/test_argparse_completer.py
@@ -16,6 +16,8 @@ from cmd2 import (
Cmd2ArgumentParser,
CompletionError,
CompletionItem,
+ argparse_completer,
+ argparse_custom,
with_argparser,
)
from cmd2.utils import (
@@ -1152,3 +1154,54 @@ def test_complete_standalone(ac_app, flag, completions):
first_match = complete_tester(text, line, begidx, endidx, ac_app)
assert first_match is not None
assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key)
+
+
+class CustomCompleter(argparse_completer.ArgparseCompleter):
+ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]:
+ """Override so arguments with 'always_complete' set to True will always be completed"""
+ for flag in matched_flags:
+ action = self._flag_to_action[flag]
+ if action.get_always_complete() is True:
+ matched_flags.remove(flag)
+ return super(CustomCompleter, self)._complete_flags(text, line, begidx, endidx, matched_flags)
+
+
+argparse_custom.register_argparse_argument_parameter('always_complete', bool)
+
+
+class CustomCompleterApp(cmd2.Cmd):
+ _parser = Cmd2ArgumentParser(description="Testing manually wrapping")
+ _parser.add_argument('--myflag', always_complete=True, nargs=1)
+
+ @with_argparser(_parser)
+ def do_mycommand(self, cmd: 'CustomCompleterApp', args: argparse.Namespace) -> None:
+ """Test command that will be manually wrapped to use argparse"""
+ print(args)
+
+
+@pytest.fixture
+def custom_completer_app():
+
+ argparse_completer.set_default_command_completer_type(CustomCompleter)
+ app = CustomCompleterApp()
+ app.stdout = StdSim(app.stdout)
+ yield app
+ argparse_completer.set_default_command_completer_type(argparse_completer.ArgparseCompleter)
+
+
+@pytest.mark.parametrize(
+ 'command_and_args, text, output_contains, first_match',
+ [
+ ('mycommand', '--my', '', '--myflag '),
+ ('mycommand --myflag 5', '--my', '', '--myflag '),
+ ],
+)
+def test_custom_completer_type(custom_completer_app, command_and_args, text, output_contains, first_match, capsys):
+ line = '{} {}'.format(command_and_args, text)
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert first_match == complete_tester(text, line, begidx, endidx, custom_completer_app)
+
+ out, err = capsys.readouterr()
+ assert output_contains in out
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py
index dcfb7d9f..fccccd43 100644
--- a/tests/test_argparse_custom.py
+++ b/tests/test_argparse_custom.py
@@ -247,28 +247,26 @@ def test_apcustom_required_options():
def test_override_parser():
+ """Test overriding argparse_custom.DEFAULT_ARGUMENT_PARSER"""
import importlib
from cmd2 import (
- DEFAULT_ARGUMENT_PARSER,
+ argparse_custom,
)
# The standard parser is Cmd2ArgumentParser
- assert DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser
+ assert argparse_custom.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
+ assert argparse_custom.DEFAULT_ARGUMENT_PARSER == CustomParser
def test_apcustom_metavar_tuple():