Skip to content

Commit a18eef6

Browse files
authored
Merge pull request #798 from python-cmd2/refactoring
Refactoring
2 parents 245dc33 + 4ea04bc commit a18eef6

File tree

11 files changed

+285
-273
lines changed

11 files changed

+285
-273
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
* It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically
1111
sets the prog value of it and all its subparsers so that all usage statements contain the top level command name
1212
and not sys.argv[0].
13+
* Breaking changes
14+
* Some constants were moved from cmd2.py to constants.py
15+
* cmd2 command decorators were moved to decorators.py. If you were importing them via cmd2's __init__.py, then
16+
there will be no issues.
1317

1418
## 0.9.19 (October 14, 2019)
1519
* Bug Fixes

cmd2/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
from .ansi import style
1414
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem
15-
from .cmd2 import Cmd, Statement, EmptyStatement, categorize
16-
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
17-
from .constants import DEFAULT_SHORTCUTS
15+
from .cmd2 import Cmd, EmptyStatement
16+
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
17+
from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
18+
from .parsing import Statement
1819
from .py_bridge import CommandResult

cmd2/cmd2.py

Lines changed: 36 additions & 257 deletions
Large diffs are not rendered by default.

cmd2/constants.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,28 @@
1616
LINE_FEED = '\n'
1717

1818
DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}
19+
20+
# Used as the command name placeholder in disabled command messages.
21+
COMMAND_NAME = "<COMMAND_NAME>"
22+
23+
# All command functions start with this
24+
COMMAND_FUNC_PREFIX = 'do_'
25+
26+
# All help functions start with this
27+
HELP_FUNC_PREFIX = 'help_'
28+
29+
# All command completer functions start with this
30+
COMPLETER_FUNC_PREFIX = 'complete_'
31+
32+
############################################################################################################
33+
# The following are optional attributes added to do_* command functions
34+
############################################################################################################
35+
36+
# The custom help category a command belongs to
37+
CMD_ATTR_HELP_CATEGORY = 'help_category'
38+
39+
# The argparse parser for the command
40+
CMD_ATTR_ARGPARSER = 'argparser'
41+
42+
# Whether or not tokens are unquoted before sending to argparse
43+
CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes'

cmd2/decorators.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# coding=utf-8
2+
"""Decorators for cmd2 commands"""
3+
import argparse
4+
from typing import Callable, Iterable, List, Optional, Union
5+
6+
from . import constants
7+
from .parsing import Statement
8+
9+
10+
def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None:
11+
"""Categorize a function.
12+
13+
The help command output will group this function under the specified category heading
14+
15+
:param func: function or list of functions to categorize
16+
:param category: category to put it in
17+
"""
18+
if isinstance(func, Iterable):
19+
for item in func:
20+
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
21+
else:
22+
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
23+
24+
25+
def with_category(category: str) -> Callable:
26+
"""A decorator to apply a category to a command function."""
27+
def cat_decorator(func):
28+
categorize(func, category)
29+
return func
30+
return cat_decorator
31+
32+
33+
def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]:
34+
"""A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user
35+
typed. With this decorator, the decorated method will receive a list of arguments parsed from user input.
36+
37+
:param args: Single-element positional argument list containing do_* method this decorator is wrapping
38+
:param preserve_quotes: if True, then argument quotes will not be stripped
39+
:return: function that gets passed a list of argument strings
40+
"""
41+
import functools
42+
43+
def arg_decorator(func: Callable):
44+
@functools.wraps(func)
45+
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
46+
_, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
47+
statement,
48+
preserve_quotes)
49+
50+
return func(cmd2_app, parsed_arglist)
51+
52+
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
53+
cmd_wrapper.__doc__ = func.__doc__
54+
return cmd_wrapper
55+
56+
if len(args) == 1 and callable(args[0]):
57+
# noinspection PyTypeChecker
58+
return arg_decorator(args[0])
59+
else:
60+
# noinspection PyTypeChecker
61+
return arg_decorator
62+
63+
64+
# noinspection PyProtectedMember
65+
def set_parser_prog(parser: argparse.ArgumentParser, prog: str):
66+
"""
67+
Recursively set prog attribute of a parser and all of its subparsers so that the root command
68+
is a command name and not sys.argv[0].
69+
:param parser: the parser being edited
70+
:param prog: value for the current parsers prog attribute
71+
"""
72+
# Set the prog value for this parser
73+
parser.prog = prog
74+
75+
# Set the prog value for the parser's subcommands
76+
for action in parser._actions:
77+
if isinstance(action, argparse._SubParsersAction):
78+
79+
# Set the prog value for each subcommand
80+
for sub_cmd, sub_cmd_parser in action.choices.items():
81+
sub_cmd_prog = parser.prog + ' ' + sub_cmd
82+
set_parser_prog(sub_cmd_parser, sub_cmd_prog)
83+
84+
# We can break since argparse only allows 1 group of subcommands per level
85+
break
86+
87+
88+
def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
89+
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
90+
preserve_quotes: bool = False) -> \
91+
Callable[[argparse.Namespace, List], Optional[bool]]:
92+
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
93+
instance of argparse.ArgumentParser, but also returning unknown args as a list.
94+
95+
:param parser: unique instance of ArgumentParser
96+
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
97+
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
98+
state data that affects parsing.
99+
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
100+
:return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings
101+
A member called __statement__ is added to the Namespace to provide command functions access to the
102+
Statement object. This can be useful if the command function needs to know the command line.
103+
104+
"""
105+
import functools
106+
107+
def arg_decorator(func: Callable):
108+
@functools.wraps(func)
109+
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
110+
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
111+
statement,
112+
preserve_quotes)
113+
114+
if ns_provider is None:
115+
namespace = None
116+
else:
117+
namespace = ns_provider(cmd2_app)
118+
119+
try:
120+
args, unknown = parser.parse_known_args(parsed_arglist, namespace)
121+
except SystemExit:
122+
return
123+
else:
124+
setattr(args, '__statement__', statement)
125+
return func(cmd2_app, args, unknown)
126+
127+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
128+
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
129+
set_parser_prog(parser, command_name)
130+
131+
# If the description has not been set, then use the method docstring if one exists
132+
if parser.description is None and func.__doc__:
133+
parser.description = func.__doc__
134+
135+
# Set the command's help text as argparser.description (which can be None)
136+
cmd_wrapper.__doc__ = parser.description
137+
138+
# Set some custom attributes for this command
139+
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
140+
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
141+
142+
return cmd_wrapper
143+
144+
# noinspection PyTypeChecker
145+
return arg_decorator
146+
147+
148+
def with_argparser(parser: argparse.ArgumentParser, *,
149+
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
150+
preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
151+
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
152+
with the given instance of argparse.ArgumentParser.
153+
154+
:param parser: unique instance of ArgumentParser
155+
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
156+
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
157+
state data that affects parsing.
158+
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
159+
:return: function that gets passed the argparse-parsed args in a Namespace
160+
A member called __statement__ is added to the Namespace to provide command functions access to the
161+
Statement object. This can be useful if the command function needs to know the command line.
162+
"""
163+
import functools
164+
165+
def arg_decorator(func: Callable):
166+
@functools.wraps(func)
167+
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
168+
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
169+
statement,
170+
preserve_quotes)
171+
172+
if ns_provider is None:
173+
namespace = None
174+
else:
175+
namespace = ns_provider(cmd2_app)
176+
177+
try:
178+
args = parser.parse_args(parsed_arglist, namespace)
179+
except SystemExit:
180+
return
181+
else:
182+
setattr(args, '__statement__', statement)
183+
return func(cmd2_app, args)
184+
185+
# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
186+
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
187+
set_parser_prog(parser, command_name)
188+
189+
# If the description has not been set, then use the method docstring if one exists
190+
if parser.description is None and func.__doc__:
191+
parser.description = func.__doc__
192+
193+
# Set the command's help text as argparser.description (which can be None)
194+
cmd_wrapper.__doc__ = parser.description
195+
196+
# Set some custom attributes for this command
197+
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
198+
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
199+
200+
return cmd_wrapper
201+
202+
# noinspection PyTypeChecker
203+
return arg_decorator

docs/api/decorators.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
Decorators
22
==========
33

4-
.. autofunction:: cmd2.cmd2.with_category
4+
.. autofunction:: cmd2.decorators.with_category
55

6-
.. autofunction:: cmd2.cmd2.with_argument_list
6+
.. autofunction:: cmd2.decorators.with_argument_list
77

8-
.. autofunction:: cmd2.cmd2.with_argparser_and_unknown_args
8+
.. autofunction:: cmd2.decorators.with_argparser_and_unknown_args
99

10-
.. autofunction:: cmd2.cmd2.with_argparser
10+
.. autofunction:: cmd2.decorators.with_argparser

docs/api/utility_functions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Utility Functions
77

88
.. autofunction:: cmd2.utils.strip_quotes
99

10-
.. autofunction:: cmd2.cmd2.categorize
10+
.. autofunction:: cmd2.decorators.categorize
1111

1212
.. autofunction:: cmd2.utils.center_text
1313

docs/features/argument_processing.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ Decorators provided by cmd2 for argument processing
4040
``cmd2`` provides the following decorators for assisting with parsing arguments
4141
passed to commands:
4242

43-
.. automethod:: cmd2.cmd2.with_argument_list
43+
.. automethod:: cmd2.decorators.with_argument_list
4444
:noindex:
45-
.. automethod:: cmd2.cmd2.with_argparser
45+
.. automethod:: cmd2.decorators.with_argparser
4646
:noindex:
47-
.. automethod:: cmd2.cmd2.with_argparser_and_unknown_args
47+
.. automethod:: cmd2.decorators.with_argparser_and_unknown_args
4848
:noindex:
4949

5050
All of these decorators accept an optional **preserve_quotes** argument which

examples/help_categories.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import argparse
88

99
import cmd2
10-
from cmd2.cmd2 import COMMAND_NAME
10+
from cmd2 import COMMAND_NAME
1111

1212

1313
class HelpCategories(cmd2.Cmd):

tests/test_cmd2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from unittest import mock
2121

2222
import cmd2
23-
from cmd2 import ansi, clipboard, constants, plugin, utils
23+
from cmd2 import ansi, clipboard, constants, plugin, utils, COMMAND_NAME
2424
from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY
2525
from .conftest import SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, complete_tester
2626

@@ -2342,7 +2342,7 @@ def test_disabled_command_not_in_history(disable_commands_app):
23422342
assert saved_len == len(disable_commands_app.history)
23432343

23442344
def test_disabled_message_command_name(disable_commands_app):
2345-
message_to_print = '{} is currently disabled'.format(cmd2.cmd2.COMMAND_NAME)
2345+
message_to_print = '{} is currently disabled'.format(COMMAND_NAME)
23462346
disable_commands_app.disable_command('has_helper_funcs', message_to_print)
23472347

23482348
out, err = run_cmd(disable_commands_app, 'has_helper_funcs')

0 commit comments

Comments
 (0)