1'''A decorator-based method of constructing IPython magics with `argparse`
2option handling.
3
4New 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
19The `@magic_arguments` decorator marks the function as having argparse arguments.
20The `@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
23parsing.
24
25Help text for the magic is automatically generated from the docstring and the
26arguments::
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
40Here 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
66In 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
72Inheritance diagram:
73
74.. inheritance-diagram:: IPython.core.magic_arguments
75 :parts: 3
76
77'''
78#-----------------------------------------------------------------------------
79# Copyright (C) 2010-2011, IPython Development Team.
80#
81# Distributed under the terms of the Modified BSD License.
82#
83# The full license is in the file COPYING.txt, distributed with this software.
84#-----------------------------------------------------------------------------
85import argparse
86import re
87
88# Our own imports
89from IPython.core.error import UsageError
90from IPython.utils.decorators import undoc
91from IPython.utils.process import arg_split
92from IPython.utils.text import dedent
93
94NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
95
96@undoc
97class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter):
98 """A HelpFormatter with a couple of changes to meet our needs.
99 """
100 # Modified to dedent text.
101 def _fill_text(self, text, width, indent):
102 return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent)
103
104 # Modified to wrap argument placeholders in <> where necessary.
105 def _format_action_invocation(self, action):
106 if not action.option_strings:
107 metavar, = self._metavar_formatter(action, action.dest)(1)
108 return metavar
109
110 else:
111 parts = []
112
113 # if the Optional doesn't take a value, format is:
114 # -s, --long
115 if action.nargs == 0:
116 parts.extend(action.option_strings)
117
118 # if the Optional takes a value, format is:
119 # -s ARGS, --long ARGS
120 else:
121 default = action.dest.upper()
122 args_string = self._format_args(action, default)
123 # IPYTHON MODIFICATION: If args_string is not a plain name, wrap
124 # it in <> so it's valid RST.
125 if not NAME_RE.match(args_string):
126 args_string = "<%s>" % args_string
127 for option_string in action.option_strings:
128 parts.append('%s %s' % (option_string, args_string))
129
130 return ', '.join(parts)
131
132 # Override the default prefix ('usage') to our % magic escape,
133 # in a code block.
134 def add_usage(self, usage, actions, groups, prefix="::\n\n %"):
135 super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix)
136
137class MagicArgumentParser(argparse.ArgumentParser):
138 """ An ArgumentParser tweaked for use by IPython magics.
139 """
140 def __init__(self,
141 prog=None,
142 usage=None,
143 description=None,
144 epilog=None,
145 parents=None,
146 formatter_class=MagicHelpFormatter,
147 prefix_chars='-',
148 argument_default=None,
149 conflict_handler='error',
150 add_help=False):
151 if parents is None:
152 parents = []
153 super(MagicArgumentParser, self).__init__(prog=prog, usage=usage,
154 description=description, epilog=epilog,
155 parents=parents, formatter_class=formatter_class,
156 prefix_chars=prefix_chars, argument_default=argument_default,
157 conflict_handler=conflict_handler, add_help=add_help)
158
159 def error(self, message):
160 """ Raise a catchable error instead of exiting.
161 """
162 raise UsageError(message)
163
164 def parse_argstring(self, argstring, *, partial=False):
165 """ Split a string into an argument list and parse that argument list.
166 """
167 argv = arg_split(argstring)
168 if partial:
169 return self.parse_known_args(argv)
170 return self.parse_args(argv)
171
172
173def construct_parser(magic_func):
174 """ Construct an argument parser using the function decorations.
175 """
176 kwds = getattr(magic_func, 'argcmd_kwds', {})
177 if 'description' not in kwds:
178 kwds['description'] = getattr(magic_func, '__doc__', None)
179 arg_name = real_name(magic_func)
180 parser = MagicArgumentParser(arg_name, **kwds)
181 # Reverse the list of decorators in order to apply them in the
182 # order in which they appear in the source.
183 group = None
184 for deco in magic_func.decorators[::-1]:
185 result = deco.add_to_parser(parser, group)
186 if result is not None:
187 group = result
188
189 # Replace the magic function's docstring with the full help text.
190 magic_func.__doc__ = parser.format_help()
191
192 return parser
193
194
195def parse_argstring(magic_func, argstring, *, partial=False):
196 """ Parse the string of arguments for the given magic function.
197 """
198 return magic_func.parser.parse_argstring(argstring, partial=partial)
199
200
201def real_name(magic_func):
202 """ Find the real name of the magic.
203 """
204 magic_name = magic_func.__name__
205 if magic_name.startswith('magic_'):
206 magic_name = magic_name[len('magic_'):]
207 return getattr(magic_func, 'argcmd_name', magic_name)
208
209
210class ArgDecorator:
211 """ Base class for decorators to add ArgumentParser information to a method.
212 """
213
214 def __call__(self, func):
215 if not getattr(func, 'has_arguments', False):
216 func.has_arguments = True
217 func.decorators = []
218 func.decorators.append(self)
219 return func
220
221 def add_to_parser(self, parser, group):
222 """ Add this object's information to the parser, if necessary.
223 """
224 pass
225
226
227class magic_arguments(ArgDecorator):
228 """ Mark the magic as having argparse arguments and possibly adjust the
229 name.
230 """
231
232 def __init__(self, name=None):
233 self.name = name
234
235 def __call__(self, func):
236 if not getattr(func, 'has_arguments', False):
237 func.has_arguments = True
238 func.decorators = []
239 if self.name is not None:
240 func.argcmd_name = self.name
241 # This should be the first decorator in the list of decorators, thus the
242 # last to execute. Build the parser.
243 func.parser = construct_parser(func)
244 return func
245
246
247class ArgMethodWrapper(ArgDecorator):
248
249 """
250 Base class to define a wrapper for ArgumentParser method.
251
252 Child class must define either `_method_name` or `add_to_parser`.
253
254 """
255
256 _method_name: str
257
258 def __init__(self, *args, **kwds):
259 self.args = args
260 self.kwds = kwds
261
262 def add_to_parser(self, parser, group):
263 """ Add this object's information to the parser.
264 """
265 if group is not None:
266 parser = group
267 getattr(parser, self._method_name)(*self.args, **self.kwds)
268 return None
269
270
271class argument(ArgMethodWrapper):
272 """ Store arguments and keywords to pass to add_argument().
273
274 Instances also serve to decorate command methods.
275 """
276 _method_name = 'add_argument'
277
278
279class defaults(ArgMethodWrapper):
280 """ Store arguments and keywords to pass to set_defaults().
281
282 Instances also serve to decorate command methods.
283 """
284 _method_name = 'set_defaults'
285
286
287class argument_group(ArgMethodWrapper):
288 """ Store arguments and keywords to pass to add_argument_group().
289
290 Instances also serve to decorate command methods.
291 """
292
293 def add_to_parser(self, parser, group):
294 """ Add this object's information to the parser.
295 """
296 return parser.add_argument_group(*self.args, **self.kwds)
297
298
299class kwds(ArgDecorator):
300 """ Provide other keywords to the sub-parser constructor.
301 """
302 def __init__(self, **kwds):
303 self.kwds = kwds
304
305 def __call__(self, func):
306 func = super(kwds, self).__call__(func)
307 func.argcmd_kwds = self.kwds
308 return func
309
310
311__all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
312 'parse_argstring']