diff options
-rwxr-xr-x | cmd2.py | 27 | ||||
-rw-r--r-- | docs/argument_processing.rst | 141 | ||||
-rw-r--r-- | docs/index.rst | 1 | ||||
-rwxr-xr-x | examples/argparse_example.py | 26 | ||||
-rw-r--r-- | tests/test_argparse.py | 54 |
5 files changed, 248 insertions, 1 deletions
@@ -241,6 +241,33 @@ def strip_quotes(arg): return arg +def with_argument_parser(argparser): + """A decorator to alter a cmd2 method to populate its ``opts`` + argument by parsing arguments with the given instance of + argparse.ArgumentParser. + """ + def arg_decorator(func): + def cmd_wrapper(instance, arg): + #print("before command") + # Use shlex to split the command line into a list of arguments based on shell rules + opts = argparser.parse_args(shlex.split(arg, posix=POSIX_SHLEX)) + #import ipdb; ipdb.set_trace() + + + # If not using POSIX shlex, make sure to strip off outer quotes for convenience + if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX: + newopts = opts +# for key, val in vars(opts): +# if isinstance(val, str): +# newopts[key] = strip_quotes(val) + opts = newopts +### opts = argparser.parse_args(shlex.split(arg, posix=POSIX_SHLEX)) + func(instance, arg, opts) + #print("after command") + return cmd_wrapper + return arg_decorator + + def options(option_list, arg_desc="arg"): """Used as a decorator and passed a list of optparse-style options, alters a cmd2 method to populate its ``opts`` argument from its diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst new file mode 100644 index 00000000..279dbe47 --- /dev/null +++ b/docs/argument_processing.rst @@ -0,0 +1,141 @@ +=================== +Argument Processing +=================== + +cmd2 currently includes code which makes it easier to add arguments to the +commands in your cmd2 subclass. This support utilizes the optparse library, +which has been deprecated since Python 2.7 (released on July 3rd 2010) and +Python 3.2 (released on February 20th, 2011). Optparse is still included in the +python standard library, but the documentation recommends using argparse +instead. It's time to modernize cmd2 to utilize argparse. + +I don't believe there is a way to change cmd2 to use argparse instead of +optparse without requiring subclasses of cmd2 to make some change. The long +recomended way to use optparse in cmd2 includes the use of the +optparse.make_option, which must be changed in order to use argparse. + +There are two potential ways to use argument parsing with cmd2: to parse +options given at the shell prompt when invoked, and to parse options given at +the cmd2 prompt prior to executing a command. + +optparse example +================ + +Here's an example of the current optparse support: + + opts = [make_option('-p', '--piglatin', action="store_true", help="atinLay"), + make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"), + make_option('-r', '--repeat', type="int", help="output [n] times")] + + @options(opts, arg_desc='(text to say)') + def do_speak(self, arg, opts=None): + """Repeats what you tell me to.""" + arg = ''.join(arg) + if opts.piglatin: + arg = '%s%say' % (arg[1:], arg[0]) + if opts.shout: + arg = arg.upper() + repetitions = opts.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.poutput(arg) + +The current optparse decorator performs the following key functions for you: + +1. Use `shlex` to split the arguments entered by the user. +2. Parse the arguments using the given optparse options. +3. Replace the `__doc__` string of the decorated function (i.e. do_speak) with +the help string generated by optparse. +4. Call the decorated function (i.e. do_speak) passing an additional parameter +which contains the parsed options. + +Here are several options for replacing this functionality with argparse. + + +No cmd2 support +=============== + +The easiest option would be to just remove the cmd2 specific support for +argument parsing. The above example would then look something like this: + + argparser = argparse.ArgumentParser( + prog='speak', + description='Repeats what you tell me to' + ) + argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + argparser.add_argument('r', '--repeat', type='int', help='output [n] times') + argparser.add_argument('word', nargs='?', help='word to say') + + def do_speak(self, argv) + """Repeats what you tell me to.""" + opts = argparser.parse_args(shlex.split(argv, posix=POSIX_SHLEX)) + arg = opts.word + if opts.piglatin: + arg = '%s%say' % (arg[1:], arg[0]) + if opts.shout: + arg = arg.upper() + repetitions = opts.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.poutput(arg) + +Using shlex in this example is technically not necessary because the `do_speak` +command only expects a single word argument. It is included here to show what +would be required to replicate the current optparse based functionality. + + +A single argparse specific decorator +==================================== + +In this approach, we would create one new decorator, perhaps called +`with_argument_parser`. This single decorator would take as it's argument a fully +defined `argparse.ArgumentParser`. This decorator would shelx the user input, +apply the ArgumentParser, and pass the resulting object to the decorated method, like so: + + argparser = argparse.ArgumentParser( + prog='speak', + description='Repeats what you tell me to' + ) + argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + argparser.add_argument('-r', '--repeat', type=int, help='output [n] times') + argparser.add_argument('word', nargs='?', help='word to say') + + @with_argument_parser(argparser) + def do_speak(self, argv, opts) + """Repeats what you tell me to.""" + arg = opts.word + if opts.piglatin: + arg = '%s%say' % (arg[1:], arg[0]) + if opts.shout: + arg = arg.upper() + repetitions = opts.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.poutput(arg) + +Compared to the no argparse support in cmd2 approach, this replaces a line of +code with a nested function with a decorator without a nested function. + + +A whole bunch of argparse specific decorators +============================================= + +This approach would turn out something like the climax library +(https://github.com/miguelgrinberg/climax), which includes a decorator for each method available +on the `ArgumentParser()` object. Our `do_speak` command would look like this: + + @command() + @argument('-p', '--piglatin', action='store_true', help='atinLay') + @argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + @argument('r', '--repeat', type='int', help='output [n] times') + @add_argument('word', nargs='?', help='word to say') + def do_speak(self, argv, piglatin, shout, repeat, word) + """Repeats what you tell me to.""" + arg = word + if piglatin: + arg = '%s%say' % (arg[1:], arg[0]) + if shout: + arg = arg.upper() + repetitions = repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.poutput(arg) + diff --git a/docs/index.rst b/docs/index.rst index 206a58ef..2ef787e1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,6 +65,7 @@ Contents: settingchanges unfreefeatures transcript + argument_parsing integrating hooks alternatives diff --git a/examples/argparse_example.py b/examples/argparse_example.py index 6fc2b15b..805bab77 100755 --- a/examples/argparse_example.py +++ b/examples/argparse_example.py @@ -13,7 +13,7 @@ argparse_example.py, verifying that the output produced matches the transcript. import argparse import sys -from cmd2 import Cmd, make_option, options +from cmd2 import Cmd, make_option, options, with_argument_parser class CmdLineApp(Cmd): @@ -57,6 +57,30 @@ class CmdLineApp(Cmd): # self.stdout.write is better than "print", because Cmd can be # initialized with a non-standard output destination + argparser = argparse.ArgumentParser( + prog='sspeak', + description='Repeats what you tell me to' + ) + argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + argparser.add_argument('-r', '--repeat', type=int, help='output [n] times') + argparser.add_argument('word', nargs='?', help='word to say') + @with_argument_parser(argparser) + def do_sspeak(self, rawarg, args=None): + """Repeats what you tell me to.""" + word = args.word + if word is None: + word = '' + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + repetitions = args.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.stdout.write(word) + self.stdout.write('\n') + # self.stdout.write is better than "print", because Cmd can be + # initialized with a non-standard output destination do_say = do_speak # now "say" is a synonym for "speak" do_orate = do_speak # another synonym, but this one takes multi-line input diff --git a/tests/test_argparse.py b/tests/test_argparse.py new file mode 100644 index 00000000..82932e6d --- /dev/null +++ b/tests/test_argparse.py @@ -0,0 +1,54 @@ +# coding=utf-8 +""" +Cmd2 testing for argument parsing +""" +import argparse +import pytest + +import cmd2 +from conftest import run_cmd, StdOut + +class ArgparseApp(cmd2.Cmd): + def __init__(self): + self.maxrepeats = 3 + cmd2.Cmd.__init__(self) + + argparser = argparse.ArgumentParser( + prog='say', + description='Repeats what you tell me to' + ) + argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + argparser.add_argument('-r', '--repeat', type=int, help='output [n] times') + argparser.add_argument('word', nargs='?', help='word to say') + @cmd2.with_argument_parser(argparser) + def do_say(self, cmdline, args=None): + word = args.word + if word is None: + word = '' + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + repetitions = args.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.stdout.write(word) + self.stdout.write('\n') + +@pytest.fixture +def argparse_app(): + app = ArgparseApp() + app.stdout = StdOut() + return app + +def test_argparse_basic_command(argparse_app): + out = run_cmd(argparse_app, 'say hello') + assert out == ['hello'] + +#def test_argparse_quoted_arguments(argparse_app): +# out = run_cmd(argparse_app, 'say "hello there"') +# assert out == ['hello there'] + +#def test_pargparse_quoted_arguments_too_many(argparse_app): +# out = run_cmd(argparse_app, 'say "hello there" morty') +# assert out == ['hello there morty'] |