|  | 
|  | 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