Skip to content

Commit 16e145a

Browse files
committed
Merge branch 'master' into topic_width
2 parents 7f07f5e + 9d81810 commit 16e145a

12 files changed

Lines changed: 257 additions & 65 deletions

CHANGELOG.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
## 2.2.0 (TBD, 2021)
22
* Enhancements
3-
* Using `SimpleTable` in the output for the following commands to improve appearance.
4-
* help
5-
* set (command and tab completion of Settables)
6-
* alias tab completion
7-
* macro tab completion
8-
* Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character.
9-
* Removed `--verbose` flag from set command since descriptions always show now.
3+
* New function `set_default_command_completer_type()` allows developer to extend and modify the
4+
behavior of `ArgparseCompleter`.
5+
* New function `register_argparse_argument_parameter()` allows developers to specify custom
6+
parameters to be passed to the argparse parser's `add_argument()` method. These parameters will
7+
become accessible in the resulting argparse Action object when modifying `ArgparseCompleter` behavior.
8+
* Using `SimpleTable` in the output for the following commands to improve appearance.
9+
* help
10+
* set (command and tab completion of Settables)
11+
* alias tab completion
12+
* macro tab completion
13+
* Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character.
14+
* Removed `--verbose` flag from set command since descriptions always show now.
1015
* Deletions (potentially breaking changes)
11-
* Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
16+
* Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
1217

1318
## 2.1.2 (July 5, 2021)
1419
* Enhancements

cmd2/__init__.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import sys
77

8-
# For python 3.8 and late
8+
# For python 3.8 and later
99
if sys.version_info >= (3, 8):
1010
import importlib.metadata as importlib_metadata
1111
else:
@@ -20,9 +20,16 @@
2020
from typing import List
2121

2222
from .ansi import style, fg, bg
23-
from .argparse_custom import Cmd2ArgumentParser, Cmd2AttributeWrapper, CompletionItem, set_default_argument_parser
23+
from .argparse_custom import (
24+
Cmd2ArgumentParser,
25+
Cmd2AttributeWrapper,
26+
CompletionItem,
27+
register_argparse_argument_parameter,
28+
set_default_argument_parser,
29+
)
2430

25-
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
31+
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER.
32+
# Do this before loading cmd2.Cmd class so its commands use the custom parser.
2633
import argparse
2734

2835
cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None)
@@ -31,8 +38,8 @@
3138

3239
importlib.import_module(cmd2_parser_module)
3340

34-
# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
35-
from .argparse_custom import DEFAULT_ARGUMENT_PARSER
41+
from .argparse_completer import set_default_command_completer_type
42+
3643
from .cmd2 import Cmd
3744
from .command_definition import CommandSet, with_default_category
3845
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
@@ -46,7 +53,6 @@
4653

4754
__all__: List[str] = [
4855
'COMMAND_NAME',
49-
'DEFAULT_ARGUMENT_PARSER',
5056
'DEFAULT_SHORTCUTS',
5157
# ANSI Style exports
5258
'bg',
@@ -56,7 +62,9 @@
5662
'Cmd2ArgumentParser',
5763
'Cmd2AttributeWrapper',
5864
'CompletionItem',
65+
'register_argparse_argument_parameter',
5966
'set_default_argument_parser',
67+
'set_default_command_completer_type',
6068
# Cmd2
6169
'Cmd',
6270
'CommandResult',

cmd2/argparse_completer.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@
1313
deque,
1414
)
1515
from typing import (
16+
TYPE_CHECKING,
1617
Dict,
1718
List,
1819
Optional,
20+
Type,
1921
Union,
2022
cast,
2123
)
2224

23-
from . import (
24-
ansi,
25-
cmd2,
26-
constants,
25+
from .ansi import (
26+
style_aware_wcswidth,
27+
widest_line,
2728
)
29+
from .constants import (
30+
INFINITY,
31+
)
32+
33+
if TYPE_CHECKING:
34+
from .cmd2 import (
35+
Cmd,
36+
)
37+
2838
from .argparse_custom import (
2939
ChoicesCallable,
3040
ChoicesProviderFuncWithTokens,
@@ -124,10 +134,10 @@ def __init__(self, arg_action: argparse.Action) -> None:
124134
self.max = 1
125135
elif self.action.nargs == argparse.ZERO_OR_MORE or self.action.nargs == argparse.REMAINDER:
126136
self.min = 0
127-
self.max = constants.INFINITY
137+
self.max = INFINITY
128138
elif self.action.nargs == argparse.ONE_OR_MORE:
129139
self.min = 1
130-
self.max = constants.INFINITY
140+
self.max = INFINITY
131141
else:
132142
self.min = self.action.nargs
133143
self.max = self.action.nargs
@@ -165,7 +175,7 @@ class ArgparseCompleter:
165175
"""Automatic command line tab completion based on argparse parameters"""
166176

167177
def __init__(
168-
self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *, parent_tokens: Optional[Dict[str, List[str]]] = None
178+
self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None
169179
) -> None:
170180
"""
171181
Create an ArgparseCompleter
@@ -564,15 +574,15 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List
564574
desc_header = desc_header.replace('\t', four_spaces)
565575

566576
# Calculate needed widths for the token and description columns of the table
567-
token_width = ansi.style_aware_wcswidth(destination)
568-
desc_width = ansi.widest_line(desc_header)
577+
token_width = style_aware_wcswidth(destination)
578+
desc_width = widest_line(desc_header)
569579

570580
for item in completion_items:
571-
token_width = max(ansi.style_aware_wcswidth(item), token_width)
581+
token_width = max(style_aware_wcswidth(item), token_width)
572582

573583
# Replace tabs with 4 spaces so we can calculate width
574584
item.description = item.description.replace('\t', four_spaces)
575-
desc_width = max(ansi.widest_line(item.description), desc_width)
585+
desc_width = max(widest_line(item.description), desc_width)
576586

577587
cols = list()
578588
cols.append(Column(destination.upper(), width=token_width))
@@ -728,3 +738,16 @@ def _complete_arg(
728738
return []
729739

730740
return self._format_completions(arg_state, results)
741+
742+
743+
DEFAULT_COMMAND_COMPLETER: Type[ArgparseCompleter] = ArgparseCompleter
744+
745+
746+
def set_default_command_completer_type(completer_type: Type[ArgparseCompleter]) -> None:
747+
"""
748+
Set the default command completer type. It must be a sub-class of the ArgparseCompleter.
749+
750+
:param completer_type: Type that is a subclass of ArgparseCompleter.
751+
"""
752+
global DEFAULT_COMMAND_COMPLETER
753+
DEFAULT_COMMAND_COMPLETER = completer_type

cmd2/argparse_custom.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
240240
NoReturn,
241241
Optional,
242242
Sequence,
243+
Set,
243244
Tuple,
244245
Type,
245246
Union,
@@ -640,10 +641,75 @@ def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool
640641
setattr(argparse.Action, 'set_suppress_tab_hint', _action_set_suppress_tab_hint)
641642

642643

644+
############################################################################################################
645+
# Allow developers to add custom action attributes
646+
############################################################################################################
647+
648+
CUSTOM_ACTION_ATTRIBS: Set[str] = set()
649+
_CUSTOM_ATTRIB_PFX = '_attr_'
650+
651+
652+
def register_argparse_argument_parameter(param_name: str, param_type: Optional[Type[Any]]) -> None:
653+
"""
654+
Registers a custom argparse argument parameter.
655+
656+
The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function.
657+
658+
An accessor functions will be added to the parameter's Action object in the form of: ``get_{param_name}()``
659+
and ``set_{param_name}(value)``.
660+
661+
:param param_name: Name of the parameter to add.
662+
"""
663+
attr_name = f'{_CUSTOM_ATTRIB_PFX}{param_name}'
664+
if param_name in CUSTOM_ACTION_ATTRIBS or hasattr(argparse.Action, attr_name):
665+
raise KeyError(f'Custom parameter {param_name} already exists')
666+
if not re.search('^[A-Za-z_][A-Za-z0-9_]*$', param_name):
667+
raise KeyError(f'Invalid parameter name {param_name} - cannot be used as a python identifier')
668+
669+
getter_name = f'get_{param_name}'
670+
671+
def _action_get_custom_parameter(self: argparse.Action) -> Any:
672+
f"""
673+
Get the custom {param_name} attribute of an argparse Action.
674+
675+
This function is added by cmd2 as a method called ``{getter_name}()`` to ``argparse.Action`` class.
676+
677+
To call: ``action.{getter_name}()``
678+
679+
:param self: argparse Action being queried
680+
:return: The value of {param_name} or None if attribute does not exist
681+
"""
682+
return getattr(self, attr_name, None)
683+
684+
setattr(argparse.Action, getter_name, _action_get_custom_parameter)
685+
686+
setter_name = f'set_{param_name}'
687+
688+
def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None:
689+
f"""
690+
Set the custom {param_name} attribute of an argparse Action.
691+
692+
This function is added by cmd2 as a method called ``{setter_name}()`` to ``argparse.Action`` class.
693+
694+
To call: ``action.{setter_name}({param_name})``
695+
696+
:param self: argparse Action being updated
697+
:param value: value being assigned
698+
"""
699+
if param_type and not isinstance(value, param_type):
700+
raise TypeError(f'{param_name} must be of type {param_type}, got: {value} ({type(value)})')
701+
setattr(self, attr_name, value)
702+
703+
setattr(argparse.Action, setter_name, _action_set_custom_parameter)
704+
705+
CUSTOM_ACTION_ATTRIBS.add(param_name)
706+
707+
643708
############################################################################################################
644709
# Patch _ActionsContainer.add_argument with our wrapper to support more arguments
645710
############################################################################################################
646711

712+
647713
# Save original _ActionsContainer.add_argument so we can call it in our wrapper
648714
# noinspection PyProtectedMember
649715
orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
@@ -753,6 +819,14 @@ def _add_argument_wrapper(
753819
# Add the argparse-recognized version of nargs to kwargs
754820
kwargs['nargs'] = nargs_adjusted
755821

822+
# Extract registered custom keyword arguments
823+
custom_attribs: Dict[str, Any] = {}
824+
for keyword, value in kwargs.items():
825+
if keyword in CUSTOM_ACTION_ATTRIBS:
826+
custom_attribs[keyword] = value
827+
for keyword in custom_attribs:
828+
del kwargs[keyword]
829+
756830
# Create the argument using the original add_argument function
757831
new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
758832

@@ -767,6 +841,11 @@ def _add_argument_wrapper(
767841
new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined]
768842
new_arg.set_descriptive_header(descriptive_header) # type: ignore[attr-defined]
769843

844+
for keyword, value in custom_attribs.items():
845+
attr_setter = getattr(new_arg, f'set_{keyword}', None)
846+
if attr_setter is not None:
847+
attr_setter(value)
848+
770849
return new_arg
771850

772851

@@ -1243,6 +1322,9 @@ def set(self, new_val: Any) -> None:
12431322

12441323

12451324
def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None:
1246-
"""Set the default ArgumentParser class for a cmd2 app"""
1325+
"""
1326+
Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if
1327+
you want to override the parser for cmd2's built-in commands. See examples/override_parser.py.
1328+
"""
12471329
global DEFAULT_ARGUMENT_PARSER
12481330
DEFAULT_ARGUMENT_PARSER = parser

0 commit comments

Comments
 (0)