Skip to content

Commit 1fd474f

Browse files
authored
Merge pull request #681 from python-cmd2/ns_provider
Ns provider
2 parents 3ee97d1 + 35d25d5 commit 1fd474f

File tree

4 files changed

+93
-23
lines changed

4 files changed

+93
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
scroll the actual error message off the screen.
2020
* Exceptions occurring in tab completion functions are now printed to stderr before returning control back to
2121
readline. This makes debugging a lot easier since readline suppresses these exceptions.
22+
* Added support for custom Namespaces in the argparse decorators. See description of `ns_provider` argument
23+
for more information.
2224
* Potentially breaking changes
2325
* Replaced `unquote_redirection_tokens()` with `unquote_specific_tokens()`. This was to support the fix
2426
that allows terminators in alias and macro values.
25-
* Changed `Statement.pipe_to` to a string instead of a list
27+
* Changed `Statement.pipe_to` to a string instead of a list
28+
* `preserve_quotes` is now a keyword-only argument in the argparse decorators
2629
* **Python 3.4 EOL notice**
2730
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019
2831
* This is the last release of `cmd2` which will support Python 3.4

cmd2/cmd2.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,17 @@ def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
191191
return arg_decorator
192192

193193

194-
def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve_quotes: bool = False) -> \
194+
def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
195+
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
196+
preserve_quotes: bool = False) -> \
195197
Callable[[argparse.Namespace, List], Optional[bool]]:
196198
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
197199
instance of argparse.ArgumentParser, but also returning unknown args as a list.
198200
199201
:param argparser: unique instance of ArgumentParser
202+
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
203+
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
204+
state data that affects parsing.
200205
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
201206
:return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings
202207
A member called __statement__ is added to the Namespace to provide command functions access to the
@@ -213,8 +218,13 @@ def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
213218
statement,
214219
preserve_quotes)
215220

221+
if ns_provider is None:
222+
namespace = None
223+
else:
224+
namespace = ns_provider(cmd2_instance)
225+
216226
try:
217-
args, unknown = argparser.parse_known_args(parsed_arglist)
227+
args, unknown = argparser.parse_known_args(parsed_arglist, namespace)
218228
except SystemExit:
219229
return
220230
else:
@@ -241,12 +251,16 @@ def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
241251
return arg_decorator
242252

243253

244-
def with_argparser(argparser: argparse.ArgumentParser,
254+
def with_argparser(argparser: argparse.ArgumentParser, *,
255+
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
245256
preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
246257
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
247258
with the given instance of argparse.ArgumentParser.
248259
249260
:param argparser: unique instance of ArgumentParser
261+
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
262+
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
263+
state data that affects parsing.
250264
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
251265
:return: function that gets passed the argparse-parsed args in a Namespace
252266
A member called __statement__ is added to the Namespace to provide command functions access to the
@@ -261,8 +275,14 @@ def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
261275
statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
262276
statement,
263277
preserve_quotes)
278+
279+
if ns_provider is None:
280+
namespace = None
281+
else:
282+
namespace = ns_provider(cmd2_instance)
283+
264284
try:
265-
args = argparser.parse_args(parsed_arglist)
285+
args = argparser.parse_args(parsed_arglist, namespace)
266286
except SystemExit:
267287
return
268288
else:

docs/argument_processing.rst

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ argument list instead of a string::
247247
pass
248248

249249

250-
Using the argument parser decorator and also receiving a a list of unknown positional arguments
250+
Using the argument parser decorator and also receiving a list of unknown positional arguments
251251
===============================================================================================
252252
If you want all unknown arguments to be passed to your command as a list of strings, then
253253
decorate the command method with the ``@with_argparser_and_unknown_args`` decorator.
@@ -275,6 +275,31 @@ Here's what it looks like::
275275

276276
...
277277

278+
Using custom argparse.Namespace with argument parser decorators
279+
===============================================================================================
280+
In some cases, it may be necessary to write custom ``argparse`` code that is dependent on state data of your
281+
application. To support this ability while still allowing use of the decorators, both ``@with_argparser`` and
282+
``@with_argparser_and_unknown_args`` have an optional argument called ``ns_provider``.
283+
284+
``ns_provider`` is a Callable that accepts a ``cmd2.Cmd`` object as an argument and returns an ``argparse.Namespace``::
285+
286+
Callable[[cmd2.Cmd], argparse.Namespace]
287+
288+
For example::
289+
290+
def settings_ns_provider(self) -> argparse.Namespace:
291+
"""Populate an argparse Namespace with current settings"""
292+
ns = argparse.Namespace()
293+
ns.app_settings = self.settings
294+
return ns
295+
296+
To use this function with the argparse decorators, do the following::
297+
298+
@with_argparser(my_parser, ns_provider=settings_ns_provider)
299+
300+
The Namespace is passed by the decorators to the ``argparse`` parsing functions which gives your custom code access
301+
to the state data it needs for its parsing logic.
302+
278303
Sub-commands
279304
============
280305
Sub-commands are supported for commands using either the ``@with_argparser`` or

tests/test_argparse.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def __init__(self):
2929
self.maxrepeats = 3
3030
cmd2.Cmd.__init__(self)
3131

32+
def namespace_provider(self) -> argparse.Namespace:
33+
ns = argparse.Namespace()
34+
ns.custom_stuff = "custom"
35+
return ns
36+
3237
say_parser = argparse.ArgumentParser()
3338
say_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
3439
say_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
@@ -56,11 +61,15 @@ def do_say(self, args):
5661
tag_parser.add_argument('tag', help='tag')
5762
tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
5863

59-
@cmd2.with_argparser(tag_parser)
64+
@cmd2.with_argparser(tag_parser, preserve_quotes=True)
6065
def do_tag(self, args):
6166
self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
6267
self.stdout.write('\n')
6368

69+
@cmd2.with_argparser(argparse.ArgumentParser(), ns_provider=namespace_provider)
70+
def do_test_argparse_ns(self, args):
71+
self.stdout.write('{}'.format(args.custom_stuff))
72+
6473
@cmd2.with_argument_list
6574
def do_arglist(self, arglist):
6675
if isinstance(arglist, list):
@@ -93,21 +102,14 @@ def do_speak(self, args, extra):
93102
self.stdout.write(' '.join(words))
94103
self.stdout.write('\n')
95104

96-
@cmd2.with_argparser_and_unknown_args(known_parser)
97-
def do_talk(self, args, extra):
98-
words = []
99-
for word in extra:
100-
if word is None:
101-
word = ''
102-
if args.piglatin:
103-
word = '%s%say' % (word[1:], word[0])
104-
if args.shout:
105-
word = word.upper()
106-
words.append(word)
107-
repetitions = args.repeat or 1
108-
for i in range(min(repetitions, self.maxrepeats)):
109-
self.stdout.write(' '.join(words))
110-
self.stdout.write('\n')
105+
@cmd2.with_argparser_and_unknown_args(argparse.ArgumentParser(), preserve_quotes=True)
106+
def do_test_argparse_with_list_quotes(self, args, extra):
107+
self.stdout.write('{}'.format(' '.join(extra)))
108+
109+
@cmd2.with_argparser_and_unknown_args(argparse.ArgumentParser(), ns_provider=namespace_provider)
110+
def do_test_argparse_with_list_ns(self, args, extra):
111+
self.stdout.write('{}'.format(args.custom_stuff))
112+
111113

112114
@pytest.fixture
113115
def argparse_app():
@@ -123,14 +125,34 @@ def test_argparse_basic_command(argparse_app):
123125
out, err = run_cmd(argparse_app, 'say hello')
124126
assert out == ['hello']
125127

126-
def test_argparse_quoted_arguments(argparse_app):
128+
def test_argparse_remove_quotes(argparse_app):
127129
out, err = run_cmd(argparse_app, 'say "hello there"')
128130
assert out == ['hello there']
129131

132+
def test_argparse_preserve_quotes(argparse_app):
133+
out, err = run_cmd(argparse_app, 'tag mytag "hello"')
134+
assert out[0] == '<mytag>"hello"</mytag>'
135+
136+
def test_argparse_custom_namespace(argparse_app):
137+
out, err = run_cmd(argparse_app, 'test_argparse_ns')
138+
assert out[0] == 'custom'
139+
130140
def test_argparse_with_list(argparse_app):
131141
out, err = run_cmd(argparse_app, 'speak -s hello world!')
132142
assert out == ['HELLO WORLD!']
133143

144+
def test_argparse_with_list_remove_quotes(argparse_app):
145+
out, err = run_cmd(argparse_app, 'speak -s hello "world!"')
146+
assert out == ['HELLO WORLD!']
147+
148+
def test_argparse_with_list_preserve_quotes(argparse_app):
149+
out, err = run_cmd(argparse_app, 'test_argparse_with_list_quotes "hello" person')
150+
assert out[0] == '"hello" person'
151+
152+
def test_argparse_with_list_custom_namespace(argparse_app):
153+
out, err = run_cmd(argparse_app, 'test_argparse_with_list_ns')
154+
assert out[0] == 'custom'
155+
134156
def test_argparse_with_list_and_empty_doc(argparse_app):
135157
out, err = run_cmd(argparse_app, 'speak -s hello world!')
136158
assert out == ['HELLO WORLD!']

0 commit comments

Comments
 (0)