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