##// END OF EJS Templates
Merge branch 'rkern-magic-arguments'
Fernando Perez -
r3433:2fdab06a merge
parent child Browse files
Show More
@@ -0,0 +1,217 b''
1 ''' A decorator-based method of constructing IPython magics with `argparse`
2 option handling.
3
4 New magic functions can be defined like so::
5
6 from IPython.core.magic_arguments import (argument, magic_arguments,
7 parse_argstring)
8
9 @magic_arguments()
10 @argument('-o', '--option', help='An optional argument.')
11 @argument('arg', type=int, help='An integer positional argument.')
12 def magic_cool(self, arg):
13 """ A really cool magic command.
14
15 """
16 args = parse_argstring(magic_cool, arg)
17 ...
18
19 The `@magic_arguments` decorator marks the function as having argparse arguments.
20 The `@argument` decorator adds an argument using the same syntax as argparse's
21 `add_argument()` method. More sophisticated uses may also require the
22 `@argument_group` or `@kwds` decorator to customize the formatting and the
23 parsing.
24
25 Help text for the magic is automatically generated from the docstring and the
26 arguments::
27
28 In[1]: %cool?
29 %cool [-o OPTION] arg
30
31 A really cool magic command.
32
33 positional arguments:
34 arg An integer positional argument.
35
36 optional arguments:
37 -o OPTION, --option OPTION
38 An optional argument.
39
40 '''
41 #-----------------------------------------------------------------------------
42 # Copyright (c) 2010, IPython Development Team.
43 #
44 # Distributed under the terms of the Modified BSD License.
45 #
46 # The full license is in the file COPYING.txt, distributed with this software.
47 #-----------------------------------------------------------------------------
48
49 # Our own imports
50 from IPython.external import argparse
51 from IPython.core.error import UsageError
52 from IPython.utils.process import arg_split
53
54
55 class MagicArgumentParser(argparse.ArgumentParser):
56 """ An ArgumentParser tweaked for use by IPython magics.
57 """
58 def __init__(self,
59 prog=None,
60 usage=None,
61 description=None,
62 epilog=None,
63 version=None,
64 parents=None,
65 formatter_class=argparse.HelpFormatter,
66 prefix_chars='-',
67 argument_default=None,
68 conflict_handler='error',
69 add_help=False):
70 if parents is None:
71 parents = []
72 super(MagicArgumentParser, self).__init__(prog=prog, usage=usage,
73 description=description, epilog=epilog, version=version,
74 parents=parents, formatter_class=formatter_class,
75 prefix_chars=prefix_chars, argument_default=argument_default,
76 conflict_handler=conflict_handler, add_help=add_help)
77
78 def error(self, message):
79 """ Raise a catchable error instead of exiting.
80 """
81 raise UsageError(message)
82
83 def parse_argstring(self, argstring):
84 """ Split a string into an argument list and parse that argument list.
85 """
86 argv = arg_split(argstring)
87 return self.parse_args(argv)
88
89
90 def construct_parser(magic_func):
91 """ Construct an argument parser using the function decorations.
92 """
93 kwds = getattr(magic_func, 'argcmd_kwds', {})
94 if 'description' not in kwds:
95 kwds['description'] = getattr(magic_func, '__doc__', None)
96 arg_name = real_name(magic_func)
97 parser = MagicArgumentParser(arg_name, **kwds)
98 # Reverse the list of decorators in order to apply them in the
99 # order in which they appear in the source.
100 group = None
101 for deco in magic_func.decorators[::-1]:
102 result = deco.add_to_parser(parser, group)
103 if result is not None:
104 group = result
105
106 # Replace the starting 'usage: ' with IPython's %.
107 help_text = parser.format_help()
108 if help_text.startswith('usage: '):
109 help_text = help_text.replace('usage: ', '%', 1)
110 else:
111 help_text = '%' + help_text
112
113 # Replace the magic function's docstring with the full help text.
114 magic_func.__doc__ = help_text
115
116 return parser
117
118
119 def parse_argstring(magic_func, argstring):
120 """ Parse the string of arguments for the given magic function.
121 """
122 return magic_func.parser.parse_argstring(argstring)
123
124
125 def real_name(magic_func):
126 """ Find the real name of the magic.
127 """
128 magic_name = magic_func.__name__
129 if magic_name.startswith('magic_'):
130 magic_name = magic_name[len('magic_'):]
131 return getattr(magic_func, 'argcmd_name', magic_name)
132
133
134 class ArgDecorator(object):
135 """ Base class for decorators to add ArgumentParser information to a method.
136 """
137
138 def __call__(self, func):
139 if not getattr(func, 'has_arguments', False):
140 func.has_arguments = True
141 func.decorators = []
142 func.decorators.append(self)
143 return func
144
145 def add_to_parser(self, parser, group):
146 """ Add this object's information to the parser, if necessary.
147 """
148 pass
149
150
151 class magic_arguments(ArgDecorator):
152 """ Mark the magic as having argparse arguments and possibly adjust the
153 name.
154 """
155
156 def __init__(self, name=None):
157 self.name = name
158
159 def __call__(self, func):
160 if not getattr(func, 'has_arguments', False):
161 func.has_arguments = True
162 func.decorators = []
163 if self.name is not None:
164 func.argcmd_name = self.name
165 # This should be the first decorator in the list of decorators, thus the
166 # last to execute. Build the parser.
167 func.parser = construct_parser(func)
168 return func
169
170
171 class argument(ArgDecorator):
172 """ Store arguments and keywords to pass to add_argument().
173
174 Instances also serve to decorate command methods.
175 """
176 def __init__(self, *args, **kwds):
177 self.args = args
178 self.kwds = kwds
179
180 def add_to_parser(self, parser, group):
181 """ Add this object's information to the parser.
182 """
183 if group is not None:
184 parser = group
185 parser.add_argument(*self.args, **self.kwds)
186 return None
187
188
189 class argument_group(ArgDecorator):
190 """ Store arguments and keywords to pass to add_argument_group().
191
192 Instances also serve to decorate command methods.
193 """
194 def __init__(self, *args, **kwds):
195 self.args = args
196 self.kwds = kwds
197
198 def add_to_parser(self, parser, group):
199 """ Add this object's information to the parser.
200 """
201 return parser.add_argument_group(*self.args, **self.kwds)
202
203
204 class kwds(ArgDecorator):
205 """ Provide other keywords to the sub-parser constructor.
206 """
207 def __init__(self, **kwds):
208 self.kwds = kwds
209
210 def __call__(self, func):
211 func = super(kwds, self).__call__(func)
212 func.argcmd_kwds = self.kwds
213 return func
214
215
216 __all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
217 'parse_argstring']
@@ -0,0 +1,121 b''
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
3 #
4 # Distributed under the terms of the Modified BSD License.
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
8
9 from nose.tools import assert_equal, assert_true
10
11 from IPython.external import argparse
12 from IPython.core.magic_arguments import (argument, argument_group, kwds,
13 magic_arguments, parse_argstring, real_name)
14 from IPython.testing.decorators import parametric
15
16
17 @magic_arguments()
18 @argument('-f', '--foo', help="an argument")
19 def magic_foo1(self, args):
20 """ A docstring.
21 """
22 return parse_argstring(magic_foo1, args)
23
24
25 @magic_arguments()
26 def magic_foo2(self, args):
27 """ A docstring.
28 """
29 return parse_argstring(magic_foo2, args)
30
31
32 @magic_arguments()
33 @argument('-f', '--foo', help="an argument")
34 @argument_group('Group')
35 @argument('-b', '--bar', help="a grouped argument")
36 @argument_group('Second Group')
37 @argument('-z', '--baz', help="another grouped argument")
38 def magic_foo3(self, args):
39 """ A docstring.
40 """
41 return parse_argstring(magic_foo3, args)
42
43
44 @magic_arguments()
45 @kwds(argument_default=argparse.SUPPRESS)
46 @argument('-f', '--foo', help="an argument")
47 def magic_foo4(self, args):
48 """ A docstring.
49 """
50 return parse_argstring(magic_foo4, args)
51
52
53 @magic_arguments('frobnicate')
54 @argument('-f', '--foo', help="an argument")
55 def magic_foo5(self, args):
56 """ A docstring.
57 """
58 return parse_argstring(magic_foo5, args)
59
60
61 @magic_arguments()
62 @argument('-f', '--foo', help="an argument")
63 def magic_magic_foo(self, args):
64 """ A docstring.
65 """
66 return parse_argstring(magic_magic_foo, args)
67
68
69 @magic_arguments()
70 @argument('-f', '--foo', help="an argument")
71 def foo(self, args):
72 """ A docstring.
73 """
74 return parse_argstring(foo, args)
75
76
77 @parametric
78 def test_magic_arguments():
79 # Ideally, these would be doctests, but I could not get it to work.
80 yield assert_equal(magic_foo1.__doc__, '%foo1 [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n')
81 yield assert_equal(getattr(magic_foo1, 'argcmd_name', None), None)
82 yield assert_equal(real_name(magic_foo1), 'foo1')
83 yield assert_equal(magic_foo1(None, ''), argparse.Namespace(foo=None))
84 yield assert_true(hasattr(magic_foo1, 'has_arguments'))
85
86 yield assert_equal(magic_foo2.__doc__, '%foo2\n\nA docstring.\n')
87 yield assert_equal(getattr(magic_foo2, 'argcmd_name', None), None)
88 yield assert_equal(real_name(magic_foo2), 'foo2')
89 yield assert_equal(magic_foo2(None, ''), argparse.Namespace())
90 yield assert_true(hasattr(magic_foo2, 'has_arguments'))
91
92 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')
93 yield assert_equal(getattr(magic_foo3, 'argcmd_name', None), None)
94 yield assert_equal(real_name(magic_foo3), 'foo3')
95 yield assert_equal(magic_foo3(None, ''),
96 argparse.Namespace(bar=None, baz=None, foo=None))
97 yield assert_true(hasattr(magic_foo3, 'has_arguments'))
98
99 yield assert_equal(magic_foo4.__doc__, '%foo4 [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n')
100 yield assert_equal(getattr(magic_foo4, 'argcmd_name', None), None)
101 yield assert_equal(real_name(magic_foo4), 'foo4')
102 yield assert_equal(magic_foo4(None, ''), argparse.Namespace())
103 yield assert_true(hasattr(magic_foo4, 'has_arguments'))
104
105 yield assert_equal(magic_foo5.__doc__, '%frobnicate [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n')
106 yield assert_equal(getattr(magic_foo5, 'argcmd_name', None), 'frobnicate')
107 yield assert_equal(real_name(magic_foo5), 'frobnicate')
108 yield assert_equal(magic_foo5(None, ''), argparse.Namespace(foo=None))
109 yield assert_true(hasattr(magic_foo5, 'has_arguments'))
110
111 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')
112 yield assert_equal(getattr(magic_magic_foo, 'argcmd_name', None), None)
113 yield assert_equal(real_name(magic_magic_foo), 'magic_foo')
114 yield assert_equal(magic_magic_foo(None, ''), argparse.Namespace(foo=None))
115 yield assert_true(hasattr(magic_magic_foo, 'has_arguments'))
116
117 yield assert_equal(foo.__doc__, '%foo [-f FOO]\n\nA docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n')
118 yield assert_equal(getattr(foo, 'argcmd_name', None), None)
119 yield assert_equal(real_name(foo), 'foo')
120 yield assert_equal(foo(None, ''), argparse.Namespace(foo=None))
121 yield assert_true(hasattr(foo, 'has_arguments'))
@@ -114,11 +114,17 b' def arg_split(s, posix=False):'
114 # http://bugs.python.org/issue1170
114 # http://bugs.python.org/issue1170
115 # At least encoding the input when it's unicode seems to help, but there
115 # At least encoding the input when it's unicode seems to help, but there
116 # may be more problems lurking. Apparently this is fixed in python3.
116 # may be more problems lurking. Apparently this is fixed in python3.
117 is_unicode = False
117 if isinstance(s, unicode):
118 if isinstance(s, unicode):
118 s = s.encode(sys.stdin.encoding)
119 is_unicode = True
120 s = s.encode('utf-8')
119 lex = shlex.shlex(s, posix=posix)
121 lex = shlex.shlex(s, posix=posix)
120 lex.whitespace_split = True
122 lex.whitespace_split = True
121 return list(lex)
123 tokens = list(lex)
124 if is_unicode:
125 # Convert the tokens back to unicode.
126 tokens = [x.decode('utf-8') for x in tokens]
127 return tokens
122
128
123
129
124 def abbrev_cwd():
130 def abbrev_cwd():
@@ -66,6 +66,9 b' def test_arg_split():'
66 """Ensure that argument lines are correctly split like in a shell."""
66 """Ensure that argument lines are correctly split like in a shell."""
67 tests = [['hi', ['hi']],
67 tests = [['hi', ['hi']],
68 [u'hi', [u'hi']],
68 [u'hi', [u'hi']],
69 ['hello there', ['hello', 'there']],
70 [u'h\N{LATIN SMALL LETTER A WITH CARON}llo', [u'h\N{LATIN SMALL LETTER A WITH CARON}llo']],
71 ['something "with quotes"', ['something', '"with quotes"']],
69 ]
72 ]
70 for argstr, argv in tests:
73 for argstr, argv in tests:
71 nt.assert_equal(arg_split(argstr), argv)
74 nt.assert_equal(arg_split(argstr), argv)
General Comments 0
You need to be logged in to leave comments. Login now