diff options
author | steven.bethard <devnull@localhost> | 2009-10-24 21:32:10 +0000 |
---|---|---|
committer | steven.bethard <devnull@localhost> | 2009-10-24 21:32:10 +0000 |
commit | f08b0c6c32930d948e79b1d82abaacdd88757d7e (patch) | |
tree | 8a6f44a345fed08ef581aeb8b862954d1a82ae43 | |
parent | 4165c3d6900e4b3e675e5087d27175485d237c46 (diff) | |
download | argparse-f08b0c6c32930d948e79b1d82abaacdd88757d7e.tar.gz |
Add modified version of Yuvgoog Greenle's initial patch.
-rw-r--r-- | argparse.py | 228 | ||||
-rw-r--r-- | test/test_argparse.py | 112 |
2 files changed, 340 insertions, 0 deletions
diff --git a/argparse.py b/argparse.py index bd03f29..9ebf845 100644 --- a/argparse.py +++ b/argparse.py @@ -94,6 +94,7 @@ import os as _os import re as _re import sys as _sys import textwrap as _textwrap +import inspect as _inspect from gettext import gettext as _ @@ -1111,6 +1112,31 @@ class _SubParsersAction(Action): parser.parse_args(arg_strings, namespace) +class _PrepareFunctionAction(Action): + + def __init__(self, option_strings, dest, function, required=True): + super(_PrepareFunctionAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + required=required, + help=SUPPRESS) + self.function = function + + def __call__(self, parser, namespace, values, option_string=None): + + def run_parsed_func(): + # _signature_from_namespace happens here only after all + # parsing is completed - when the user calls dest(). + # Otherwise, the action might be called before parse_args + # finished and the namespace isn't correct yet. + args, kwargs = _signature_from_namespace(self.function, namespace) + return self.function(*args, **kwargs) + + # pin the function to the namespace at dest. + setattr(namespace, self.dest, run_parsed_func) + + # ============== # Type classes # ============== @@ -1672,6 +1698,89 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): if not action.option_strings] # ===================================== + # Auto generating options from function introspection + # ===================================== + def add_function_arguments(self, function, dest=None): + (arg_names_list, varargs, varkw, defaults, + kwonlyargs, kwonlydefaults, annotations) = _getfunctionspec(function) + description, help_dict = _parse_docstring(function) + + if defaults is None: + defaults = () + + if dest is None: + dest = function.__name__ + + defaulted_count = len(defaults) + not_defaulted_count = len(arg_names_list) - defaulted_count + not_defaulted = arg_names_list[:not_defaulted_count] + defaulted_args = arg_names_list[not_defaulted_count:] + defaults_dict = dict(zip(defaulted_args, defaults)) + + options = _set() + + for arg_name in arg_names_list: + arg_kwopts = {} + arg_opts = [] + + arg_help = help_dict.get(arg_name, '') + arg_kwopts['help'] = arg_help + + if arg_name in annotations: + cast = annotations[arg_name] + if cast is bool: + arg_kwopts['action'] = 'store_true' + options.add(arg_name) + else: + arg_kwopts['type'] = cast + + if arg_name in defaults_dict: + default_value = defaults_dict[arg_name] + arg_kwopts['default'] = default_value + + if arg_name not in annotations: + # if annotations exist, they take precedence over assuming + # typing with the default values. + + # This parameter isn't annotated a type so we'll assume + # the type is the same as the default argument and that + # the ctor can parse a string. + cast = type(default_value) + + # handle special cases: + if cast == bool: + # if the default is True, then the action is + # store_false and vice versa. + to_store = str(not default_value).lower() + arg_kwopts['action'] = 'store_' + to_store + options.add(arg_name) + else: + arg_kwopts['type'] = cast + + options.add(arg_name) + + if arg_name in options: + # options are whatever was given a default and type bools + arg_opts.append('-' + arg_name[0]) + arg_opts.append('--' + arg_name) + else: + arg_opts.append(arg_name) + + self.add_argument(*arg_opts, **arg_kwopts) + + # handle variable length argument list like - def func(eggs, *varargs) + if varargs is not None: + self.add_argument(varargs, nargs='*') + + # add an action that uses the results of the actions above to provide + # the arguments to the function + self.add_argument(dest, function=function, + action=_PrepareFunctionAction) + + # return the original function to allow method use as a decorator + return function + + # ===================================== # Command line argument parsing methods # ===================================== def parse_args(self, args=None, namespace=None): @@ -2296,3 +2405,122 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): """ self.print_usage(_sys.stderr) self.exit(2, _('%s: error: %s\n') % (self.prog, message)) + + +def _getfunctionspec(function): + if hasattr(function, '__annotations__'): # python 3 only + (arg_names, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, + annotations) = _inspect.getfullargspec(function) + else: + arg_names, varargs, varkw, defaults = _inspect.getargspec(function) + kwonlyargs, kwonlydefaults, annotations = [], {}, {} + return (arg_names, varargs, varkw, defaults, + kwonlyargs, kwonlydefaults, annotations) + + +def _signature_from_namespace(function, namespace): + "Translate argument Namespace to args and kwargs" + (arg_names_list, varargs, varkw, defaults, + kwonlyargs, kwonlydefaults, annotations) = _getfunctionspec(function) + + # remove *, variable list of arguments + kwargs = vars(namespace).copy() + if varargs is None: + args = [] + else: + args = kwargs.pop(varargs) + + # remove names in namespace that aren't parameters + unrelated_names = _set(kwargs.keys()) - _set(arg_names_list) + for name in unrelated_names: + del kwargs[name] + + return args, kwargs + + +def _parse_docstring(function): + """Parses a function's docstring for a description of the function and for + help on the individual parameters. + + The parsing algorithm currently looks for lines that start with a parameter + name immediately followed by any amount of whitespace, hyphens or colons. + The rest of the line following the colon/hyphen/whitespace is the help. + + Keyword Arguments: + function - the function whose docstring is parsed. + + Returns a (description, help_dict) tuple: + description - all text before the first documented parameter + help_dict - a dictionary mapping parameter names to their help strings + """ + (arg_names_list, varargs, varkw, defaults, + kwonlyargs, kwonlydefaults, annotations) = _getfunctionspec(function) + + # get the docstring and split it into lines + docs = function.__doc__ + if docs is None: + docs = '' + lines = docs.splitlines() + + # find parameter documentation: + names = '|'.join(arg_names_list) + var_doc_re = _re.compile(r'(%s)[ \t-:]*(.*)' % names) + help_dict = {} + + # collect parameter help strings and identify the first line where + # one appeared + first_line = len(docs) + for i, line in enumerate(lines): + stripped = line.strip() + match = var_doc_re.match(stripped) + if match is not None: + if i < first_line: + first_line = i + name, help = match.groups() + help_dict[name] = help.strip() + + # the description is all text preceding the first parameter help line + if first_line < len(docs): + description = '\n'.join(lines[:first_line]).strip() + else: + description = None + + return description, help_dict + + +def run(*functions, **kwargs): + """Create an ArgumentParser based on function parameters, parse arguments + for the function(s) from the command line, and call a function. + + Arguments: + functions -- A list of functions whose parameters define the interface. + If more than one function is provided, a sub-command will be + generated for each function, named by the function name. + args -- The command line arguments, defaulting to sys.argv[1:] + """ + # check parameters + args = kwargs.pop('args', _sys.argv[1:]) + if kwargs: + raise TypeError("unexpected keyword arguments: %s" % list(kwargs)) + if not functions: + raise TypeError("expected at least one function") + + # create a parser for a single function + if len(functions) == 1: + func, = functions + description, _ = _parse_docstring(func) + parser = ArgumentParser(description=description) + parser.add_function_arguments(func, dest='__run__') + + # create a parser for multiple functions + else: + parser = ArgumentParser() + subparsers = parser.add_subparsers() + for func in functions: + name = func.__name__ + desc, _ = _parse_docstring(func) + func_parser = subparsers.add_parser(name, description=desc) + func_parser.add_function_arguments(func, dest='__run__') + + # parse the args and call the function + return parser.parse_args(args).__run__() diff --git a/test/test_argparse.py b/test/test_argparse.py index c9ec289..462b065 100644 --- a/test/test_argparse.py +++ b/test/test_argparse.py @@ -4031,6 +4031,118 @@ class TestParseKnownArgs(TestCase): self.failUnlessEqual(NS(v=3, spam=True, badger="B"), args) self.failUnlessEqual(["C", "--foo", "4"], extras) +# ====================== +# add_function tests +# ====================== + +class TestAddFunction(TestCase): + + def test_defaults(self): + + def func(foo, bar, c=2, do_it=False, e=[42]): + return (foo, bar, c, do_it, e) + + parser = argparse.ArgumentParser() + parser.add_function_arguments(func) + + # There was once a bug that occurred when the positional arguments + # came first and not last. So lets make sure it works both ways. + for args_str in ['funky qqq ext -d -c 33', '-d -c 33 funky qqq ext']: + args, extras = parser.parse_known_args(args_str.split()) + expected_results = ('funky', 'qqq', 33, True, [42]) + results = args.func() + self.failUnlessEqual(expected_results, results) + + # del args.func because failUnlessEqual can't compare the function. + # It's not really func, it's just a stub function that calls func. + del args.func + expected = NS(foo='funky', bar='qqq', c=33, do_it=True, e=[42]) + self.failUnlessEqual(expected, args) + self.failUnlessEqual(['ext'], extras) + + def test_runner(self): + + def func(swimming, bacon='42'): + return swimming, bacon + + def proc(flying, spaghetti='...'): + return flying, spaghetti + + result = argparse.run(func, args="-b 13 bloop".split()) + self.failUnlessEqual(('bloop', '13'), result) + + result = argparse.run(proc, func, args="func -b 13 bloop".split()) + self.failUnlessEqual(('bloop', '13'), result) + + result = argparse.run(func, proc, args="proc monster".split()) + self.failUnlessEqual(('monster', '...'), result) + + # only compile and test annotations if this is Python >= 3 + if sys.version_info[0] >= 3: + + def test_annotations(self): + func_source = textwrap.dedent(''' + def func(foo:bool, bar:int, spam:int=101): + return foo, bar + ''') + exec_dict = {} + exec(func_source, exec_dict) + func = exec_dict['func'] + + parser = argparse.ArgumentParser() + parser.add_function_arguments(func) + + args = parser.parse_args(['42']) + del args.func + self.failUnlessEqual(NS(foo=False, bar=42, spam=101), args) + + args = parser.parse_args('--foo 27'.split()) + del args.func + self.failUnlessEqual(NS(foo=True, bar=27, spam=101), args) + + args = parser.parse_args('27 -f'.split()) + del args.func + self.failUnlessEqual(NS(foo=True, bar=27, spam=101), args) + + args = parser.parse_args('27 -f -s 321'.split()) + del args.func + self.failUnlessEqual(NS(foo=True, bar=27, spam=321), args) + + def test_help(self): + + def funky(sparrow, arrow, fox): + ''' + funky puts the func into function. + sparrow - A bird with a certain air speed velocity + fox - a small animal + ''' + pass + + old_stderr = sys.stderr + sys.stderr = StringIO() + try: + try: + argparse.run(funky, args=['-h']) + except SystemExit: + expected = textwrap.dedent('''\ + usage: test_argparse.py [-h] sparrow arrow fox + + funky puts the func into function. + + positional arguments: + sparrow A bird with a certain air speed velocity + arrow + fox a small animal + + optional arguments: + -h, --help show this help message and exit + ''') + message = sys.stderr.getvalue() + self.failUnlessEqual(expected, message) + finally: + sys.stderr = old_stderr + + # ============================ # from argparse import * tests # ============================ |