To implement an analogue of argparse , docopt , it is enough to specify the way how to set the expected command line parameters and recognize the actual arguments passed to the program, which correspond to these parameters.
For specification of parameters, you can use the annotation of the main() function, which is called when the script starts:
REQUIRED = object() @simple_cli(__doc__) def main(*, number: int = REQUIRED, flag: bool = False): # some filler code that uses the parsed args print(number + 1, flag, sep=' | ') if __name__ == '__main__': main()
This says that the script takes two parameters --number and --flag . --number takes an integer value and is required, and --flag is a simple boolean flag (obviously, which is optional and disabled by default).
Here, simple_cli() is the decorator, which turns sys.argv (command line arguments) into the actual parameters of the main() function:
#!/usr/bin/env python3 """Usage: parse-args --number=<int> [--flag]""" import inspect import functools import sys def simple_cli(description): """Implement a simple annotation-based command-line parser.""" def decorator(main): @functools.wraps(main) def wrapper(argv=None): if argv is None: argv = sys.argv if '--help' in argv: sys.exit(description) params = inspect.signature(main).parameters parser = ArgumentParser.from_params(*params.values()) try: return main(**parser.parse_args(argv[1:])) except ArgumentError as e: sys.exit(f"Error: {e}\n{description}") return wrapper return decorator
Examples of using:
$ parse-args.py --help Usage: parse-args --number=<int> [--flag] $ parse-args --number=1 --flag 2 | True
Here the docstring module is used as the --help message. If desired, you can generate the usage automatically from the declaration of the main() function to eliminate duplication of information about the parameters. Or vice versa: from the docstring generate a parser for command line arguments like this docopt does.
The actual recognition of the arguments is handled by the ArgumentParser class:
from collections import namedtuple Parameter = namedtuple('Parameter', 'name type required') class ArgumentError(Exception): pass class ArgumentParser: """(--option[=value])*""" def __init__(self, params): self.params = params # parameters specification @classmethod def from_params(cls, *params): return cls({p.name: Parameter(p.name, str if p.annotation is p.empty else p.annotation, p.default is REQUIRED) for p in params if p.kind == p.KEYWORD_ONLY}) def parse_args(self, argv): args = {name[2:]: value if '=' in arg else None for arg in argv for name, value in [arg.partition('=')[::2]]} required_params = { p.name: p for p in self.params.values() if p.required} if not (required_params.keys() <= args.keys()): raise ArgumentError("missing required parameters") if not (args.keys() <= self.params.keys()): raise ArgumentError("unknown parameters") for name, value in args.items(): param = self.params[name] if param.type is bool: if value is not None: raise ArgumentError("flag with a value") args[name] = True elif param.type is not str: if value is None: raise ArgumentError(f"--{name} option is missing value") try: args[name] = param.type(value) except ValueError as e: raise ArgumentError(str(e)) return args
From the point of view of Argparser each argument has the form --option[=value] , where square brackets say that the value may not be present. --option value not supported (can be added).
The specification of possible parameters is represented by the Parameter list of objects. from_params() converts the specification from the inspect.Parameter format to Parameter(name, type, required) .
parse_args() turns the command line arguments ( sys.argv[1:] ) into arguments that can be passed to main() . The code is straightforward. The noticeable part is devoted to throwing an ArgumentError exception if the input arguments do not match the specified specification.
argparsemodule - they want you to write a simplified version of this module ... - MaxU