From ce087d638276bb9ee884da0af586bcdbf5909b4d 2010-11-03 02:42:53 From: Robert Kern Date: 2010-11-03 02:42:53 Subject: [PATCH] ENH: Add the argparse-based option parsing for magics. --- diff --git a/IPython/core/magic_arguments.py b/IPython/core/magic_arguments.py new file mode 100644 index 0000000..a45803d --- /dev/null +++ b/IPython/core/magic_arguments.py @@ -0,0 +1,222 @@ +''' A decorator-based method of constructing IPython magics with `argparse` +option handling. + +New magic functions can be defined like so:: + + from IPython.core.magic_arguments import (argument, magic_arguments, + parse_argstring) + + @magic_arguments() + @argument('-o', '--option', help='An optional argument.') + @argument('arg', type=int, help='An integer positional argument.') + def magic_cool(self, arg): + """ A really cool magic command. + + """ + args = parse_argstring(magic_cool, arg) + ... + +The `@magic_arguments` decorator marks the function as having argparse arguments. +The `@argument` decorator adds an argument using the same syntax as argparse's +`add_argument()` method. More sophisticated uses may also require the +`@argument_group` or `@kwds` decorator to customize the formatting and the +parsing. + +Help text for the magic is automatically generated from the docstring and the +arguments:: + + In[1]: %cool? + %cool [-o OPTION] arg + + A really cool magic command. + + positional arguments: + arg An integer positional argument. + + optional arguments: + -o OPTION, --option OPTION + An optional argument. + +''' +#----------------------------------------------------------------------------- +# Copyright (c) 2010, IPython Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +# Stdlib imports +import shlex + +# Our own imports +from IPython.external import argparse +from IPython.core.error import UsageError + + +class MagicArgumentParser(argparse.ArgumentParser): + """ An ArgumentParser tweaked for use by IPython magics. + """ + def __init__(self, + prog=None, + usage=None, + description=None, + epilog=None, + version=None, + parents=None, + formatter_class=argparse.HelpFormatter, + prefix_chars='-', + argument_default=None, + conflict_handler='error', + add_help=False): + if parents is None: + parents = [] + super(MagicArgumentParser, self).__init__(prog=prog, usage=usage, + description=description, epilog=epilog, version=version, + parents=parents, formatter_class=formatter_class, + prefix_chars=prefix_chars, argument_default=argument_default, + conflict_handler=conflict_handler, add_help=add_help) + + def error(self, message): + """ Raise a catchable error instead of exiting. + """ + raise UsageError(message) + + def parse_argstring(self, argstring): + """ Split a string into an argument list and parse that argument list. + """ + argv = shlex.split(argstring) + return self.parse_args(argv) + + +def construct_parser(magic_func): + """ Construct an argument parser using the function decorations. + """ + kwds = getattr(magic_func, 'argcmd_kwds', {}) + if 'description' not in kwds: + kwds['description'] = getattr(magic_func, '__doc__', None) + arg_name = real_name(magic_func) + parser = MagicArgumentParser(arg_name, **kwds) + # Reverse the list of decorators in order to apply them in the + # order in which they appear in the source. + group = None + for deco in magic_func.decorators[::-1]: + result = deco.add_to_parser(parser, group) + if result is not None: + group = result + + # Replace the starting 'usage: ' with IPython's %. + help_text = parser.format_help() + if help_text.startswith('usage: '): + help_text = help_text.replace('usage: ', '%', 1) + else: + help_text = '%' + help_text + + # Replace the magic function's docstring with the full help text. + magic_func.__doc__ = help_text + + return parser + + +def parse_argstring(magic_func, argstring): + """ Parse the string of arguments for the given magic function. + """ + args = magic_func.parser.parse_argstring(argstring) + return args + + +def real_name(magic_func): + """ Find the real name of the magic. + """ + magic_name = magic_func.__name__ + if magic_name.startswith('magic_'): + magic_name = magic_name[len('magic_'):] + arg_name = getattr(magic_func, 'argcmd_name', magic_name) + return arg_name + + +class ArgDecorator(object): + """ Base class for decorators to add ArgumentParser information to a method. + """ + + def __call__(self, func): + if not getattr(func, 'has_arguments', False): + func.has_arguments = True + func.decorators = [] + func.decorators.append(self) + return func + + def add_to_parser(self, parser, group): + """ Add this object's information to the parser, if necessary. + """ + pass + + +class magic_arguments(ArgDecorator): + """ Mark the magic as having argparse arguments and possibly adjust the + name. + """ + + def __init__(self, name=None): + self.name = name + + def __call__(self, func): + if not getattr(func, 'has_arguments', False): + func.has_arguments = True + func.decorators = [] + if self.name is not None: + func.argcmd_name = self.name + # This should be the first decorator in the list of decorators, thus the + # last to execute. Build the parser. + func.parser = construct_parser(func) + return func + + +class argument(ArgDecorator): + """ Store arguments and keywords to pass to add_argument(). + + Instances also serve to decorate command methods. + """ + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + + def add_to_parser(self, parser, group): + """ Add this object's information to the parser. + """ + if group is not None: + parser = group + parser.add_argument(*self.args, **self.kwds) + return None + + +class argument_group(ArgDecorator): + """ Store arguments and keywords to pass to add_argument_group(). + + Instances also serve to decorate command methods. + """ + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + + def add_to_parser(self, parser, group): + """ Add this object's information to the parser. + """ + group = parser.add_argument_group(*self.args, **self.kwds) + return group + + +class kwds(ArgDecorator): + """ Provide other keywords to the sub-parser constructor. + """ + def __init__(self, **kwds): + self.kwds = kwds + + def __call__(self, func): + func = super(kwds, self).__call__(func) + func.argcmd_kwds = self.kwds + return func + + +__all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds', + 'parse_argstring'] diff --git a/IPython/core/tests/test_magic_arguments.py b/IPython/core/tests/test_magic_arguments.py new file mode 100644 index 0000000..470c314 --- /dev/null +++ b/IPython/core/tests/test_magic_arguments.py @@ -0,0 +1,113 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010, IPython Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +from nose.tools import assert_equal, assert_true + +from IPython.external import argparse +from IPython.core.magic_arguments import (argument, argument_group, kwds, + magic_arguments, parse_argstring, real_name) + + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +def magic_foo1(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo1, args) + +@magic_arguments() +def magic_foo2(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo2, args) + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +@argument_group('Group') +@argument('-b', '--bar', help="a grouped argument") +@argument_group('Second Group') +@argument('-z', '--baz', help="another grouped argument") +def magic_foo3(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo3, args) + +@magic_arguments() +@kwds(argument_default=argparse.SUPPRESS) +@argument('-f', '--foo', help="an argument") +def magic_foo4(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo4, args) + +@magic_arguments('frobnicate') +@argument('-f', '--foo', help="an argument") +def magic_foo5(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo5, args) + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +def magic_magic_foo(self, args): + """ A docstring. + """ + return parse_argstring(magic_magic_foo, args) + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +def foo(self, args): + """ A docstring. + """ + return parse_argstring(foo, args) + +def test_magic_arguments(): + # Ideally, these would be doctests, but I could not get it to work. + yield assert_equal, magic_foo1.__doc__, '%foo1 [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n' + yield assert_equal, getattr(magic_foo1, 'argcmd_name', None), None + yield assert_equal, real_name(magic_foo1), 'foo1' + yield assert_equal, magic_foo1(None, ''), argparse.Namespace(foo=None) + yield assert_true, hasattr(magic_foo1, 'has_arguments') + + yield assert_equal, magic_foo2.__doc__, '%foo2\n\nA docstring.\n' + yield assert_equal, getattr(magic_foo2, 'argcmd_name', None), None + yield assert_equal, real_name(magic_foo2), 'foo2' + yield assert_equal, magic_foo2(None, ''), argparse.Namespace() + yield assert_true, hasattr(magic_foo2, 'has_arguments') + + yield assert_equal, magic_foo3.__doc__, '%foo3 [-f FOO] [-b BAR] [-z BAZ]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n\nGroup:\n -b BAR, --bar BAR a grouped argument\n\nSecond Group:\n -z BAZ, --baz BAZ another grouped argument\n' + yield assert_equal, getattr(magic_foo3, 'argcmd_name', None), None + yield assert_equal, real_name(magic_foo3), 'foo3' + yield assert_equal, magic_foo3(None, ''), argparse.Namespace(bar=None, baz=None, foo=None) + yield assert_true, hasattr(magic_foo3, 'has_arguments') + + yield assert_equal, magic_foo4.__doc__, '%foo4 [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n' + yield assert_equal, getattr(magic_foo4, 'argcmd_name', None), None + yield assert_equal, real_name(magic_foo4), 'foo4' + yield assert_equal, magic_foo4(None, ''), argparse.Namespace() + yield assert_true, hasattr(magic_foo4, 'has_arguments') + + yield assert_equal, magic_foo5.__doc__, '%frobnicate [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n' + yield assert_equal, getattr(magic_foo5, 'argcmd_name', None), 'frobnicate' + yield assert_equal, real_name(magic_foo5), 'frobnicate' + yield assert_equal, magic_foo5(None, ''), argparse.Namespace(foo=None) + yield assert_true, hasattr(magic_foo5, 'has_arguments') + + yield assert_equal, magic_magic_foo.__doc__, '%magic_foo [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n' + yield assert_equal, getattr(magic_magic_foo, 'argcmd_name', None), None + yield assert_equal, real_name(magic_magic_foo), 'magic_foo' + yield assert_equal, magic_magic_foo(None, ''), argparse.Namespace(foo=None) + yield assert_true, hasattr(magic_magic_foo, 'has_arguments') + + yield assert_equal, foo.__doc__, '%foo [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n' + yield assert_equal, getattr(foo, 'argcmd_name', None), None + yield assert_equal, real_name(foo), 'foo' + yield assert_equal, foo(None, ''), argparse.Namespace(foo=None) + yield assert_true, hasattr(foo, 'has_arguments') + +