##// END OF EJS Templates
Merge pull request #13433 from kolibril13/magic_argument...
Matthias Bussonnier -
r27442:26054024 merge
parent child Browse files
Show More
@@ -1,278 +1,310 b''
1 ''' A decorator-based method of constructing IPython magics with `argparse`
1 ''' A decorator-based method of constructing IPython magics with `argparse`
2 option handling.
2 option handling.
3
3
4 New magic functions can be defined like so::
4 New magic functions can be defined like so::
5
5
6 from IPython.core.magic_arguments import (argument, magic_arguments,
6 from IPython.core.magic_arguments import (argument, magic_arguments,
7 parse_argstring)
7 parse_argstring)
8
8
9 @magic_arguments()
9 @magic_arguments()
10 @argument('-o', '--option', help='An optional argument.')
10 @argument('-o', '--option', help='An optional argument.')
11 @argument('arg', type=int, help='An integer positional argument.')
11 @argument('arg', type=int, help='An integer positional argument.')
12 def magic_cool(self, arg):
12 def magic_cool(self, arg):
13 """ A really cool magic command.
13 """ A really cool magic command.
14
14
15 """
15 """
16 args = parse_argstring(magic_cool, arg)
16 args = parse_argstring(magic_cool, arg)
17 ...
17 ...
18
18
19 The `@magic_arguments` decorator marks the function as having argparse arguments.
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
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
21 `add_argument()` method. More sophisticated uses may also require the
22 `@argument_group` or `@kwds` decorator to customize the formatting and the
22 `@argument_group` or `@kwds` decorator to customize the formatting and the
23 parsing.
23 parsing.
24
24
25 Help text for the magic is automatically generated from the docstring and the
25 Help text for the magic is automatically generated from the docstring and the
26 arguments::
26 arguments::
27
27
28 In[1]: %cool?
28 In[1]: %cool?
29 %cool [-o OPTION] arg
29 %cool [-o OPTION] arg
30
30
31 A really cool magic command.
31 A really cool magic command.
32
32
33 positional arguments:
33 positional arguments:
34 arg An integer positional argument.
34 arg An integer positional argument.
35
35
36 optional arguments:
36 optional arguments:
37 -o OPTION, --option OPTION
37 -o OPTION, --option OPTION
38 An optional argument.
38 An optional argument.
39
39
40 Here is an elaborated example that uses default parameters in `argument` and calls the `args` in the cell magic::
41
42 from IPython.core.magic import register_cell_magic
43 from IPython.core.magic_arguments import (argument, magic_arguments,
44 parse_argstring)
45
46
47 @magic_arguments()
48 @argument(
49 "--option",
50 "-o",
51 help=("Add an option here"),
52 )
53 @argument(
54 "--style",
55 "-s",
56 default="foo",
57 help=("Add some style arguments"),
58 )
59 @register_cell_magic
60 def my_cell_magic(line, cell):
61 args = parse_argstring(my_cell_magic, line)
62 print(f"{args.option=}")
63 print(f"{args.style=}")
64 print(f"{cell=}")
65
66 In a jupyter notebook, this cell magic can be executed like this::
67
68 %%my_cell_magic -o Hello
69 print("bar")
70 i = 42
71
40 Inheritance diagram:
72 Inheritance diagram:
41
73
42 .. inheritance-diagram:: IPython.core.magic_arguments
74 .. inheritance-diagram:: IPython.core.magic_arguments
43 :parts: 3
75 :parts: 3
44
76
45 '''
77 '''
46 #-----------------------------------------------------------------------------
78 #-----------------------------------------------------------------------------
47 # Copyright (C) 2010-2011, IPython Development Team.
79 # Copyright (C) 2010-2011, IPython Development Team.
48 #
80 #
49 # Distributed under the terms of the Modified BSD License.
81 # Distributed under the terms of the Modified BSD License.
50 #
82 #
51 # The full license is in the file COPYING.txt, distributed with this software.
83 # The full license is in the file COPYING.txt, distributed with this software.
52 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
53 import argparse
85 import argparse
54 import re
86 import re
55
87
56 # Our own imports
88 # Our own imports
57 from IPython.core.error import UsageError
89 from IPython.core.error import UsageError
58 from IPython.utils.decorators import undoc
90 from IPython.utils.decorators import undoc
59 from IPython.utils.process import arg_split
91 from IPython.utils.process import arg_split
60 from IPython.utils.text import dedent
92 from IPython.utils.text import dedent
61
93
62 NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
94 NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
63
95
64 @undoc
96 @undoc
65 class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter):
97 class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter):
66 """A HelpFormatter with a couple of changes to meet our needs.
98 """A HelpFormatter with a couple of changes to meet our needs.
67 """
99 """
68 # Modified to dedent text.
100 # Modified to dedent text.
69 def _fill_text(self, text, width, indent):
101 def _fill_text(self, text, width, indent):
70 return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent)
102 return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent)
71
103
72 # Modified to wrap argument placeholders in <> where necessary.
104 # Modified to wrap argument placeholders in <> where necessary.
73 def _format_action_invocation(self, action):
105 def _format_action_invocation(self, action):
74 if not action.option_strings:
106 if not action.option_strings:
75 metavar, = self._metavar_formatter(action, action.dest)(1)
107 metavar, = self._metavar_formatter(action, action.dest)(1)
76 return metavar
108 return metavar
77
109
78 else:
110 else:
79 parts = []
111 parts = []
80
112
81 # if the Optional doesn't take a value, format is:
113 # if the Optional doesn't take a value, format is:
82 # -s, --long
114 # -s, --long
83 if action.nargs == 0:
115 if action.nargs == 0:
84 parts.extend(action.option_strings)
116 parts.extend(action.option_strings)
85
117
86 # if the Optional takes a value, format is:
118 # if the Optional takes a value, format is:
87 # -s ARGS, --long ARGS
119 # -s ARGS, --long ARGS
88 else:
120 else:
89 default = action.dest.upper()
121 default = action.dest.upper()
90 args_string = self._format_args(action, default)
122 args_string = self._format_args(action, default)
91 # IPYTHON MODIFICATION: If args_string is not a plain name, wrap
123 # IPYTHON MODIFICATION: If args_string is not a plain name, wrap
92 # it in <> so it's valid RST.
124 # it in <> so it's valid RST.
93 if not NAME_RE.match(args_string):
125 if not NAME_RE.match(args_string):
94 args_string = "<%s>" % args_string
126 args_string = "<%s>" % args_string
95 for option_string in action.option_strings:
127 for option_string in action.option_strings:
96 parts.append('%s %s' % (option_string, args_string))
128 parts.append('%s %s' % (option_string, args_string))
97
129
98 return ', '.join(parts)
130 return ', '.join(parts)
99
131
100 # Override the default prefix ('usage') to our % magic escape,
132 # Override the default prefix ('usage') to our % magic escape,
101 # in a code block.
133 # in a code block.
102 def add_usage(self, usage, actions, groups, prefix="::\n\n %"):
134 def add_usage(self, usage, actions, groups, prefix="::\n\n %"):
103 super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix)
135 super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix)
104
136
105 class MagicArgumentParser(argparse.ArgumentParser):
137 class MagicArgumentParser(argparse.ArgumentParser):
106 """ An ArgumentParser tweaked for use by IPython magics.
138 """ An ArgumentParser tweaked for use by IPython magics.
107 """
139 """
108 def __init__(self,
140 def __init__(self,
109 prog=None,
141 prog=None,
110 usage=None,
142 usage=None,
111 description=None,
143 description=None,
112 epilog=None,
144 epilog=None,
113 parents=None,
145 parents=None,
114 formatter_class=MagicHelpFormatter,
146 formatter_class=MagicHelpFormatter,
115 prefix_chars='-',
147 prefix_chars='-',
116 argument_default=None,
148 argument_default=None,
117 conflict_handler='error',
149 conflict_handler='error',
118 add_help=False):
150 add_help=False):
119 if parents is None:
151 if parents is None:
120 parents = []
152 parents = []
121 super(MagicArgumentParser, self).__init__(prog=prog, usage=usage,
153 super(MagicArgumentParser, self).__init__(prog=prog, usage=usage,
122 description=description, epilog=epilog,
154 description=description, epilog=epilog,
123 parents=parents, formatter_class=formatter_class,
155 parents=parents, formatter_class=formatter_class,
124 prefix_chars=prefix_chars, argument_default=argument_default,
156 prefix_chars=prefix_chars, argument_default=argument_default,
125 conflict_handler=conflict_handler, add_help=add_help)
157 conflict_handler=conflict_handler, add_help=add_help)
126
158
127 def error(self, message):
159 def error(self, message):
128 """ Raise a catchable error instead of exiting.
160 """ Raise a catchable error instead of exiting.
129 """
161 """
130 raise UsageError(message)
162 raise UsageError(message)
131
163
132 def parse_argstring(self, argstring):
164 def parse_argstring(self, argstring):
133 """ Split a string into an argument list and parse that argument list.
165 """ Split a string into an argument list and parse that argument list.
134 """
166 """
135 argv = arg_split(argstring)
167 argv = arg_split(argstring)
136 return self.parse_args(argv)
168 return self.parse_args(argv)
137
169
138
170
139 def construct_parser(magic_func):
171 def construct_parser(magic_func):
140 """ Construct an argument parser using the function decorations.
172 """ Construct an argument parser using the function decorations.
141 """
173 """
142 kwds = getattr(magic_func, 'argcmd_kwds', {})
174 kwds = getattr(magic_func, 'argcmd_kwds', {})
143 if 'description' not in kwds:
175 if 'description' not in kwds:
144 kwds['description'] = getattr(magic_func, '__doc__', None)
176 kwds['description'] = getattr(magic_func, '__doc__', None)
145 arg_name = real_name(magic_func)
177 arg_name = real_name(magic_func)
146 parser = MagicArgumentParser(arg_name, **kwds)
178 parser = MagicArgumentParser(arg_name, **kwds)
147 # Reverse the list of decorators in order to apply them in the
179 # Reverse the list of decorators in order to apply them in the
148 # order in which they appear in the source.
180 # order in which they appear in the source.
149 group = None
181 group = None
150 for deco in magic_func.decorators[::-1]:
182 for deco in magic_func.decorators[::-1]:
151 result = deco.add_to_parser(parser, group)
183 result = deco.add_to_parser(parser, group)
152 if result is not None:
184 if result is not None:
153 group = result
185 group = result
154
186
155 # Replace the magic function's docstring with the full help text.
187 # Replace the magic function's docstring with the full help text.
156 magic_func.__doc__ = parser.format_help()
188 magic_func.__doc__ = parser.format_help()
157
189
158 return parser
190 return parser
159
191
160
192
161 def parse_argstring(magic_func, argstring):
193 def parse_argstring(magic_func, argstring):
162 """ Parse the string of arguments for the given magic function.
194 """ Parse the string of arguments for the given magic function.
163 """
195 """
164 return magic_func.parser.parse_argstring(argstring)
196 return magic_func.parser.parse_argstring(argstring)
165
197
166
198
167 def real_name(magic_func):
199 def real_name(magic_func):
168 """ Find the real name of the magic.
200 """ Find the real name of the magic.
169 """
201 """
170 magic_name = magic_func.__name__
202 magic_name = magic_func.__name__
171 if magic_name.startswith('magic_'):
203 if magic_name.startswith('magic_'):
172 magic_name = magic_name[len('magic_'):]
204 magic_name = magic_name[len('magic_'):]
173 return getattr(magic_func, 'argcmd_name', magic_name)
205 return getattr(magic_func, 'argcmd_name', magic_name)
174
206
175
207
176 class ArgDecorator(object):
208 class ArgDecorator(object):
177 """ Base class for decorators to add ArgumentParser information to a method.
209 """ Base class for decorators to add ArgumentParser information to a method.
178 """
210 """
179
211
180 def __call__(self, func):
212 def __call__(self, func):
181 if not getattr(func, 'has_arguments', False):
213 if not getattr(func, 'has_arguments', False):
182 func.has_arguments = True
214 func.has_arguments = True
183 func.decorators = []
215 func.decorators = []
184 func.decorators.append(self)
216 func.decorators.append(self)
185 return func
217 return func
186
218
187 def add_to_parser(self, parser, group):
219 def add_to_parser(self, parser, group):
188 """ Add this object's information to the parser, if necessary.
220 """ Add this object's information to the parser, if necessary.
189 """
221 """
190 pass
222 pass
191
223
192
224
193 class magic_arguments(ArgDecorator):
225 class magic_arguments(ArgDecorator):
194 """ Mark the magic as having argparse arguments and possibly adjust the
226 """ Mark the magic as having argparse arguments and possibly adjust the
195 name.
227 name.
196 """
228 """
197
229
198 def __init__(self, name=None):
230 def __init__(self, name=None):
199 self.name = name
231 self.name = name
200
232
201 def __call__(self, func):
233 def __call__(self, func):
202 if not getattr(func, 'has_arguments', False):
234 if not getattr(func, 'has_arguments', False):
203 func.has_arguments = True
235 func.has_arguments = True
204 func.decorators = []
236 func.decorators = []
205 if self.name is not None:
237 if self.name is not None:
206 func.argcmd_name = self.name
238 func.argcmd_name = self.name
207 # This should be the first decorator in the list of decorators, thus the
239 # This should be the first decorator in the list of decorators, thus the
208 # last to execute. Build the parser.
240 # last to execute. Build the parser.
209 func.parser = construct_parser(func)
241 func.parser = construct_parser(func)
210 return func
242 return func
211
243
212
244
213 class ArgMethodWrapper(ArgDecorator):
245 class ArgMethodWrapper(ArgDecorator):
214
246
215 """
247 """
216 Base class to define a wrapper for ArgumentParser method.
248 Base class to define a wrapper for ArgumentParser method.
217
249
218 Child class must define either `_method_name` or `add_to_parser`.
250 Child class must define either `_method_name` or `add_to_parser`.
219
251
220 """
252 """
221
253
222 _method_name = None
254 _method_name = None
223
255
224 def __init__(self, *args, **kwds):
256 def __init__(self, *args, **kwds):
225 self.args = args
257 self.args = args
226 self.kwds = kwds
258 self.kwds = kwds
227
259
228 def add_to_parser(self, parser, group):
260 def add_to_parser(self, parser, group):
229 """ Add this object's information to the parser.
261 """ Add this object's information to the parser.
230 """
262 """
231 if group is not None:
263 if group is not None:
232 parser = group
264 parser = group
233 getattr(parser, self._method_name)(*self.args, **self.kwds)
265 getattr(parser, self._method_name)(*self.args, **self.kwds)
234 return None
266 return None
235
267
236
268
237 class argument(ArgMethodWrapper):
269 class argument(ArgMethodWrapper):
238 """ Store arguments and keywords to pass to add_argument().
270 """ Store arguments and keywords to pass to add_argument().
239
271
240 Instances also serve to decorate command methods.
272 Instances also serve to decorate command methods.
241 """
273 """
242 _method_name = 'add_argument'
274 _method_name = 'add_argument'
243
275
244
276
245 class defaults(ArgMethodWrapper):
277 class defaults(ArgMethodWrapper):
246 """ Store arguments and keywords to pass to set_defaults().
278 """ Store arguments and keywords to pass to set_defaults().
247
279
248 Instances also serve to decorate command methods.
280 Instances also serve to decorate command methods.
249 """
281 """
250 _method_name = 'set_defaults'
282 _method_name = 'set_defaults'
251
283
252
284
253 class argument_group(ArgMethodWrapper):
285 class argument_group(ArgMethodWrapper):
254 """ Store arguments and keywords to pass to add_argument_group().
286 """ Store arguments and keywords to pass to add_argument_group().
255
287
256 Instances also serve to decorate command methods.
288 Instances also serve to decorate command methods.
257 """
289 """
258
290
259 def add_to_parser(self, parser, group):
291 def add_to_parser(self, parser, group):
260 """ Add this object's information to the parser.
292 """ Add this object's information to the parser.
261 """
293 """
262 return parser.add_argument_group(*self.args, **self.kwds)
294 return parser.add_argument_group(*self.args, **self.kwds)
263
295
264
296
265 class kwds(ArgDecorator):
297 class kwds(ArgDecorator):
266 """ Provide other keywords to the sub-parser constructor.
298 """ Provide other keywords to the sub-parser constructor.
267 """
299 """
268 def __init__(self, **kwds):
300 def __init__(self, **kwds):
269 self.kwds = kwds
301 self.kwds = kwds
270
302
271 def __call__(self, func):
303 def __call__(self, func):
272 func = super(kwds, self).__call__(func)
304 func = super(kwds, self).__call__(func)
273 func.argcmd_kwds = self.kwds
305 func.argcmd_kwds = self.kwds
274 return func
306 return func
275
307
276
308
277 __all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
309 __all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
278 'parse_argstring']
310 'parse_argstring']
General Comments 0
You need to be logged in to leave comments. Login now