Skip to content

Instantly share code, notes, and snippets.

@hr3lxphr6j
Last active April 19, 2022 04:50
Show Gist options
  • Save hr3lxphr6j/26826f722dd63dbe3e4e2e51bc512598 to your computer and use it in GitHub Desktop.
Save hr3lxphr6j/26826f722dd63dbe3e4e2e51bc512598 to your computer and use it in GitHub Desktop.
from argparse import ArgumentParser, Namespace
from functools import partial
from inspect import Parameter, signature
from typing import Callable
class Cli:
class Arg:
def __init__(self, *args, **kwargs) -> None:
self.args = args
self.kwargs = kwargs
_root_parser = None
_sps = None
__root_func = None
@staticmethod
def wrapper(fn: Callable = None, sub_command: str = None, root_parser: bool = False, **kwargs):
if fn is None:
return partial(Cli.wrapper, sub_command=sub_command, root_parser=root_parser, **kwargs)
if not Cli._root_parser:
Cli._root_parser = ArgumentParser(**(kwargs if root_parser else {}))
if not root_parser:
if not Cli._sps:
Cli._sps = Cli._root_parser.add_subparsers()
parser = Cli._sps.add_parser(sub_command or fn.__name__, **kwargs)
else:
if Cli.__root_func:
raise RuntimeError('root parser already set')
parser = Cli._root_parser
Cli.__root_func = fn
for k, v in signature(fn).parameters.items():
if v.default and isinstance(v.default, Cli.Arg):
_args = v.default.args
_kwargs = v.default.kwargs
if _args and _args[0].startswith('-'):
_kwargs['dest'] = k
if not _args:
_args = ['--' + k.replace('_', '-')]
if 'type' not in _kwargs and 'action' not in _kwargs and isinstance(v.annotation, Callable):
_kwargs['type'] = v.annotation
else:
_args = ['--' + k.replace('_', '-')]
_kwargs = {
'type': v.annotation if isinstance(v.annotation, Callable) else None,
'default': str(v.default) if v.default and v.default != Parameter.empty else None,
'required': v.default == Parameter.empty,
'help': f'{k}, default: {str(v.default)}' if v.default and v.default != Parameter.empty else None,
'dest': k
}
parser.add_argument(*_args, **_kwargs)
if not parser.get_default('_func'):
parser.set_defaults(_func=fn)
return partial(fn,
**{k: v if not isinstance(v.default, Cli.Arg) else v.default.kwargs.get('default') for k, v in
signature(fn).parameters.items() if v.default != Parameter.empty})
@staticmethod
def parse(*args, **kwargs) -> Namespace:
return Cli._root_parser.parse_args(*args, **kwargs)
@staticmethod
def run(*args, **kwargs) -> None:
args = Cli.parse(*args, **kwargs)
if '_func' not in args:
Cli._root_parser.print_help()
exit(1)
args._func(**{k: v for k, v in args.__dict__.items() if k != '_func'})
@hr3lxphr6j
Copy link
Author

Example

@Cli.wrapper
def cp(input_dir: pathlib.Path,
       output_dir: pathlib.Path,
       recursive: bool = Cli.Arg('-r', '--recursive', action='store_true', help='copy directories and their contents recursively')):
    print(input_dir)
    print(output_dir)
    print(recursive)

def main():
    Cli.run()

if __name__ == '__main__':
    main()
$ test.py cp -h

usage: test.py cp [-h] --input-dir INPUT_DIR --output-dir OUTPUT_DIR [-r]

optional arguments:
  -h, --help            show this help message and exit
  --input-dir INPUT_DIR
  --output-dir OUTPUT_DIR
  -r, --recursive       copy directories and their contents recursively

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment