Format: text/x-python
⬇ Download File (9.5 KB)
   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  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  
  72  Inheritance 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  #-----------------------------------------------------------------------------
  85  import argparse
  86  import re
  87  
  88  # Our own imports
  89  from IPython.core.error import UsageError
  90  from IPython.utils.decorators import undoc
  91  from IPython.utils.process import arg_split
  92  from IPython.utils.text import dedent
  93  
  94  NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
  95  
  96  @undoc
  97  class 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  
 137  class 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):
 165          """ Split a string into an argument list and parse that argument list.
 166          """
 167          argv = arg_split(argstring)
 168          return self.parse_args(argv)
 169  
 170  
 171  def construct_parser(magic_func):
 172      """ Construct an argument parser using the function decorations.
 173      """
 174      kwds = getattr(magic_func, 'argcmd_kwds', {})
 175      if 'description' not in kwds:
 176          kwds['description'] = getattr(magic_func, '__doc__', None)
 177      arg_name = real_name(magic_func)
 178      parser = MagicArgumentParser(arg_name, **kwds)
 179      # Reverse the list of decorators in order to apply them in the
 180      # order in which they appear in the source.
 181      group = None
 182      for deco in magic_func.decorators[::-1]:
 183          result = deco.add_to_parser(parser, group)
 184          if result is not None:
 185              group = result
 186  
 187      # Replace the magic function's docstring with the full help text.
 188      magic_func.__doc__ = parser.format_help()
 189  
 190      return parser
 191  
 192  
 193  def parse_argstring(magic_func, argstring):
 194      """ Parse the string of arguments for the given magic function.
 195      """
 196      return magic_func.parser.parse_argstring(argstring)
 197  
 198  
 199  def real_name(magic_func):
 200      """ Find the real name of the magic.
 201      """
 202      magic_name = magic_func.__name__
 203      if magic_name.startswith('magic_'):
 204          magic_name = magic_name[len('magic_'):]
 205      return getattr(magic_func, 'argcmd_name', magic_name)
 206  
 207  
 208  class ArgDecorator(object):
 209      """ Base class for decorators to add ArgumentParser information to a method.
 210      """
 211  
 212      def __call__(self, func):
 213          if not getattr(func, 'has_arguments', False):
 214              func.has_arguments = True
 215              func.decorators = []
 216          func.decorators.append(self)
 217          return func
 218  
 219      def add_to_parser(self, parser, group):
 220          """ Add this object's information to the parser, if necessary.
 221          """
 222          pass
 223  
 224  
 225  class magic_arguments(ArgDecorator):
 226      """ Mark the magic as having argparse arguments and possibly adjust the
 227      name.
 228      """
 229  
 230      def __init__(self, name=None):
 231          self.name = name
 232  
 233      def __call__(self, func):
 234          if not getattr(func, 'has_arguments', False):
 235              func.has_arguments = True
 236              func.decorators = []
 237          if self.name is not None:
 238              func.argcmd_name = self.name
 239          # This should be the first decorator in the list of decorators, thus the
 240          # last to execute. Build the parser.
 241          func.parser = construct_parser(func)
 242          return func
 243  
 244  
 245  class ArgMethodWrapper(ArgDecorator):
 246  
 247      """
 248      Base class to define a wrapper for ArgumentParser method.
 249  
 250      Child class must define either `_method_name` or `add_to_parser`.
 251  
 252      """
 253  
 254      _method_name: str
 255  
 256      def __init__(self, *args, **kwds):
 257          self.args = args
 258          self.kwds = kwds
 259  
 260      def add_to_parser(self, parser, group):
 261          """ Add this object's information to the parser.
 262          """
 263          if group is not None:
 264              parser = group
 265          getattr(parser, self._method_name)(*self.args, **self.kwds)
 266          return None
 267  
 268  
 269  class argument(ArgMethodWrapper):
 270      """ Store arguments and keywords to pass to add_argument().
 271  
 272      Instances also serve to decorate command methods.
 273      """
 274      _method_name = 'add_argument'
 275  
 276  
 277  class defaults(ArgMethodWrapper):
 278      """ Store arguments and keywords to pass to set_defaults().
 279  
 280      Instances also serve to decorate command methods.
 281      """
 282      _method_name = 'set_defaults'
 283  
 284  
 285  class argument_group(ArgMethodWrapper):
 286      """ Store arguments and keywords to pass to add_argument_group().
 287  
 288      Instances also serve to decorate command methods.
 289      """
 290  
 291      def add_to_parser(self, parser, group):
 292          """ Add this object's information to the parser.
 293          """
 294          return parser.add_argument_group(*self.args, **self.kwds)
 295  
 296  
 297  class kwds(ArgDecorator):
 298      """ Provide other keywords to the sub-parser constructor.
 299      """
 300      def __init__(self, **kwds):
 301          self.kwds = kwds
 302  
 303      def __call__(self, func):
 304          func = super(kwds, self).__call__(func)
 305          func.argcmd_kwds = self.kwds
 306          return func
 307  
 308  
 309  __all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
 310      'parse_argstring']
 311