magic_arguments.py
310 lines
| 9.5 KiB
| text/x-python
|
PythonLexer
Robert Kern
|
r3229 | ''' 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. | ||||
Jan-Hendrik Müller
|
r27401 | Here is an elaborated example that uses default parameters in `argument` and calls the `args` in the cell magic:: | ||
from IPython.core.magic import register_cell_magic | ||||
from IPython.core.magic_arguments import (argument, magic_arguments, | ||||
parse_argstring) | ||||
@magic_arguments() | ||||
@argument( | ||||
"--option", | ||||
"-o", | ||||
help=("Add an option here"), | ||||
) | ||||
@argument( | ||||
"--style", | ||||
"-s", | ||||
default="foo", | ||||
help=("Add some style arguments"), | ||||
) | ||||
@register_cell_magic | ||||
def my_cell_magic(line, cell): | ||||
args = parse_argstring(my_cell_magic, line) | ||||
print(f"{args.option=}") | ||||
print(f"{args.style=}") | ||||
print(f"{cell=}") | ||||
In a jupyter notebook, this cell magic can be executed like this:: | ||||
%%my_cell_magic -o Hello | ||||
print("bar") | ||||
i = 42 | ||||
Thomas Kluyver
|
r8795 | Inheritance diagram: | ||
.. inheritance-diagram:: IPython.core.magic_arguments | ||||
:parts: 3 | ||||
Robert Kern
|
r3229 | ''' | ||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2010-2011, IPython Development Team. | ||
Robert Kern
|
r3229 | # | ||
# Distributed under the terms of the Modified BSD License. | ||||
# | ||||
# The full license is in the file COPYING.txt, distributed with this software. | ||||
#----------------------------------------------------------------------------- | ||||
Thomas Kluyver
|
r12547 | import argparse | ||
Thomas Kluyver
|
r13603 | import re | ||
Robert Kern
|
r3229 | |||
# Our own imports | ||||
from IPython.core.error import UsageError | ||||
Thomas Kluyver
|
r13603 | from IPython.utils.decorators import undoc | ||
Robert Kern
|
r3292 | from IPython.utils.process import arg_split | ||
Bradley M. Froehle
|
r7490 | from IPython.utils.text import dedent | ||
Robert Kern
|
r3229 | |||
Thomas Kluyver
|
r13603 | NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$") | ||
@undoc | ||||
Bradley M. Froehle
|
r7490 | class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter): | ||
Thomas Kluyver
|
r13603 | """A HelpFormatter with a couple of changes to meet our needs. | ||
Bradley M. Froehle
|
r7490 | """ | ||
Thomas Kluyver
|
r13603 | # Modified to dedent text. | ||
Bradley M. Froehle
|
r7490 | def _fill_text(self, text, width, indent): | ||
return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent) | ||||
Robert Kern
|
r3229 | |||
Thomas Kluyver
|
r13603 | # Modified to wrap argument placeholders in <> where necessary. | ||
def _format_action_invocation(self, action): | ||||
if not action.option_strings: | ||||
metavar, = self._metavar_formatter(action, action.dest)(1) | ||||
return metavar | ||||
else: | ||||
parts = [] | ||||
# if the Optional doesn't take a value, format is: | ||||
# -s, --long | ||||
if action.nargs == 0: | ||||
parts.extend(action.option_strings) | ||||
# if the Optional takes a value, format is: | ||||
# -s ARGS, --long ARGS | ||||
else: | ||||
default = action.dest.upper() | ||||
args_string = self._format_args(action, default) | ||||
# IPYTHON MODIFICATION: If args_string is not a plain name, wrap | ||||
# it in <> so it's valid RST. | ||||
if not NAME_RE.match(args_string): | ||||
args_string = "<%s>" % args_string | ||||
for option_string in action.option_strings: | ||||
parts.append('%s %s' % (option_string, args_string)) | ||||
return ', '.join(parts) | ||||
# Override the default prefix ('usage') to our % magic escape, | ||||
# in a code block. | ||||
def add_usage(self, usage, actions, groups, prefix="::\n\n %"): | ||||
super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix) | ||||
Robert Kern
|
r3229 | class MagicArgumentParser(argparse.ArgumentParser): | ||
""" An ArgumentParser tweaked for use by IPython magics. | ||||
""" | ||||
def __init__(self, | ||||
prog=None, | ||||
usage=None, | ||||
description=None, | ||||
epilog=None, | ||||
parents=None, | ||||
Bradley M. Froehle
|
r7490 | formatter_class=MagicHelpFormatter, | ||
Robert Kern
|
r3229 | prefix_chars='-', | ||
argument_default=None, | ||||
conflict_handler='error', | ||||
add_help=False): | ||||
if parents is None: | ||||
parents = [] | ||||
super(MagicArgumentParser, self).__init__(prog=prog, usage=usage, | ||||
Andrew Spiers
|
r8295 | description=description, epilog=epilog, | ||
Robert Kern
|
r3229 | 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. | ||||
""" | ||||
Robert Kern
|
r3292 | argv = arg_split(argstring) | ||
Robert Kern
|
r3229 | 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 magic function's docstring with the full help text. | ||||
Thomas Kluyver
|
r13603 | magic_func.__doc__ = parser.format_help() | ||
Robert Kern
|
r3229 | |||
return parser | ||||
def parse_argstring(magic_func, argstring): | ||||
""" Parse the string of arguments for the given magic function. | ||||
""" | ||||
Fernando Perez
|
r3431 | return magic_func.parser.parse_argstring(argstring) | ||
Robert Kern
|
r3229 | |||
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_'):] | ||||
Fernando Perez
|
r3431 | return getattr(magic_func, 'argcmd_name', magic_name) | ||
Robert Kern
|
r3229 | |||
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 | ||||
Takafumi Arakaki
|
r8427 | class ArgMethodWrapper(ArgDecorator): | ||
Robert Kern
|
r3229 | |||
""" | ||||
Takafumi Arakaki
|
r8427 | Base class to define a wrapper for ArgumentParser method. | ||
Child class must define either `_method_name` or `add_to_parser`. | ||||
""" | ||||
Matthias Bussonnier
|
r28167 | _method_name: str | ||
Takafumi Arakaki
|
r8427 | |||
Robert Kern
|
r3229 | 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 | ||||
Takafumi Arakaki
|
r8427 | getattr(parser, self._method_name)(*self.args, **self.kwds) | ||
Robert Kern
|
r3229 | return None | ||
Takafumi Arakaki
|
r8427 | class argument(ArgMethodWrapper): | ||
""" Store arguments and keywords to pass to add_argument(). | ||||
Takafumi Arakaki
|
r8426 | |||
Instances also serve to decorate command methods. | ||||
""" | ||||
Takafumi Arakaki
|
r8427 | _method_name = 'add_argument' | ||
Takafumi Arakaki
|
r8426 | |||
Takafumi Arakaki
|
r8427 | class defaults(ArgMethodWrapper): | ||
""" Store arguments and keywords to pass to set_defaults(). | ||||
Takafumi Arakaki
|
r8426 | |||
Takafumi Arakaki
|
r8427 | Instances also serve to decorate command methods. | ||
""" | ||||
_method_name = 'set_defaults' | ||||
class argument_group(ArgMethodWrapper): | ||||
Robert Kern
|
r3229 | """ Store arguments and keywords to pass to add_argument_group(). | ||
Instances also serve to decorate command methods. | ||||
""" | ||||
def add_to_parser(self, parser, group): | ||||
""" Add this object's information to the parser. | ||||
""" | ||||
Fernando Perez
|
r3431 | return parser.add_argument_group(*self.args, **self.kwds) | ||
Robert Kern
|
r3229 | |||
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'] | ||||