|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import argparse |
| 4 | +import collections.abc as C |
| 5 | +import typing as T |
| 6 | + |
| 7 | + |
| 8 | +class SubDec: |
| 9 | + """ |
| 10 | + This class provides a way to decorate functions as subcommands for |
| 11 | + argparse. |
| 12 | +
|
| 13 | + It provides an implementation of `__getattr__` that allows one to annotate |
| 14 | + a function (which represents a subcommand) multiple times as if calling |
| 15 | + methods on a sub-parser for that command. |
| 16 | +
|
| 17 | + The example below creates two subcommands:: |
| 18 | +
|
| 19 | + >>> sd = SubDec() |
| 20 | +
|
| 21 | + >>> @sd.add_argument('--option', default='123') |
| 22 | + ... @sd.add_argument('--another') |
| 23 | + ... def foo(args): |
| 24 | + ... print(f'foo subcommand: --option={args.option!r} and --another={args.another!r}') |
| 25 | +
|
| 26 | + >>> @sd.cmd(prefix_chars=':') |
| 27 | + ... @sd.add_argument('positional_arg', type=int) |
| 28 | + ... def foo_bar(args): |
| 29 | + ... print(f'foo-bar subcommand: {args.positional_arg!r}') |
| 30 | +
|
| 31 | + Note that the second command uses a special decorator ``cmd()``, which |
| 32 | + passes all of its arguments down to ``add_parser()`` when creating the |
| 33 | + sub-parser. |
| 34 | +
|
| 35 | + In order to create the sub-parsers, you must call ``sd.create_parsers()`` |
| 36 | + passing a ``subparsers`` object as argument:: |
| 37 | +
|
| 38 | + >>> parser = argparse.ArgumentParser() |
| 39 | + >>> subparsers = parser.add_subparsers() |
| 40 | + >>> sd.create_parsers(subparsers) |
| 41 | +
|
| 42 | + >>> args = parser.parse_args(['foo', '--another', 'hello']) |
| 43 | +
|
| 44 | + By default, the subcommand function is assigned to ``args.fn``:: |
| 45 | +
|
| 46 | + >>> args.fn(args) |
| 47 | + foo subcommand: --option='123' and --another='hello' |
| 48 | +
|
| 49 | + Calling the second command:: |
| 50 | +
|
| 51 | + >>> args = parser.parse_args(['foo-bar', '42']) |
| 52 | + >>> args.fn(args) |
| 53 | + foo-bar subcommand: 42 |
| 54 | + """ |
| 55 | + |
| 56 | + def __init__(self, |
| 57 | + name_prefix: str = '', |
| 58 | + fn_dest: str = 'fn', |
| 59 | + sep: str = '-', |
| 60 | + ): |
| 61 | + """ |
| 62 | + Initialize this object. |
| 63 | +
|
| 64 | + If ``name_prefix`` is not empty, that prefix is removed from the |
| 65 | + function name when defining the name of that function's subparser. |
| 66 | +
|
| 67 | + The parameter ``fn_dest`` (default: "fn") defines the name of the |
| 68 | + attribute in the object returned by ``parse_args()`` the decorated |
| 69 | + function will be assigned to. |
| 70 | +
|
| 71 | + The parameter ``sep`` (default: "-") defines the string that will |
| 72 | + replace the underscore character ("_") when converting the name of the |
| 73 | + decorated function to a subcommand name. |
| 74 | + """ |
| 75 | + self.__decorators_cache = {} |
| 76 | + self.__commands = {} |
| 77 | + self.__name_prefix = name_prefix |
| 78 | + self.__fn_dest = fn_dest |
| 79 | + self.__sep = sep |
| 80 | + |
| 81 | + def create_parsers(self, subparsers): |
| 82 | + """ |
| 83 | + Create subparsers by calling ``subparsers.add_parser()`` for each |
| 84 | + decorated function. |
| 85 | + """ |
| 86 | + for cmd in self.__commands.values(): |
| 87 | + self.__create_parser(cmd, subparsers) |
| 88 | + |
| 89 | + def cmd(self, *k, **kw): |
| 90 | + """ |
| 91 | + Special decorator to register arguments to be passed do |
| 92 | + ``add_parser()``. |
| 93 | + """ |
| 94 | + def decorator(fn): |
| 95 | + cmd = self.__get_command(fn) |
| 96 | + cmd['add_parser_args'] = (k, kw) |
| 97 | + return fn |
| 98 | + return decorator |
| 99 | + |
| 100 | + def __getattr__(self, name: str): |
| 101 | + if name in self.__decorators_cache: |
| 102 | + return self.__decorators_cache[name] |
| 103 | + |
| 104 | + def decorator_wrapper(*k, **kw): |
| 105 | + def decorator(fn): |
| 106 | + cmd = self.__get_command(fn) |
| 107 | + cmd['subparser_call_stack'].append({ |
| 108 | + 'method_name': name, |
| 109 | + 'args': k, |
| 110 | + 'kwargs': kw, |
| 111 | + }) |
| 112 | + return fn |
| 113 | + return decorator |
| 114 | + |
| 115 | + self.__decorators_cache[name] = decorator_wrapper |
| 116 | + return decorator_wrapper |
| 117 | + |
| 118 | + def __get_command(self, fn: T.Callable): |
| 119 | + if fn not in self.__commands: |
| 120 | + self.__commands[fn] = { |
| 121 | + 'name': None, |
| 122 | + 'fn': fn, |
| 123 | + 'subparser_call_stack': [], |
| 124 | + 'add_parser_args': None, |
| 125 | + } |
| 126 | + return self.__commands[fn] |
| 127 | + |
| 128 | + def __create_parser(self, cmd: dict, subparsers: T.Any): |
| 129 | + name = cmd['name'] |
| 130 | + if not name: |
| 131 | + name = cmd['fn'].__name__ |
| 132 | + if name.startswith(self.__name_prefix): |
| 133 | + name = name[len(self.__name_prefix):] |
| 134 | + if self.__sep is not None: |
| 135 | + name = name.replace('_', self.__sep) |
| 136 | + |
| 137 | + if cmd['add_parser_args']: |
| 138 | + add_parser_args, add_parser_kwargs = cmd['add_parser_args'] |
| 139 | + if not add_parser_args: |
| 140 | + add_parser_args = (name,) |
| 141 | + else: |
| 142 | + add_parser_args = (name,) |
| 143 | + add_parser_kwargs = {} |
| 144 | + |
| 145 | + parser = subparsers.add_parser(*add_parser_args, **add_parser_kwargs) |
| 146 | + parser.set_defaults(**{self.__fn_dest: cmd['fn']}) |
| 147 | + |
| 148 | + for call_data in reversed(cmd['subparser_call_stack']): |
| 149 | + method = getattr(parser, call_data['method_name']) |
| 150 | + method(*call_data['args'], **call_data['kwargs']) |
0 commit comments