Last active
October 15, 2021 13:39
-
-
Save altescy/7bd6d9abd16c2abb19d22d635ac1b70c to your computer and use it in GitHub Desktop.
Example of subcommand with argparse
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import argparse | |
import re | |
from collections import defaultdict | |
from typing import (Callable, ClassVar, Dict, NamedTuple, Optional, Type, | |
TypeVar, Union, cast) | |
Subclass = TypeVar("Subclass", bound="Subcommand") | |
Registry = Dict[Type["Subcommand"], Dict[str, Type["Subcommand"]]] | |
class SubcommandInfo(NamedTuple): | |
name: str | |
usage: Optional[str] = None | |
description: Optional[str] = None | |
epilog: Optional[str] = None | |
class Subcommand: | |
_registry: ClassVar[Registry] = defaultdict(dict) | |
_func_key: ClassVar[str] = "__func" | |
_cmd_info: ClassVar[SubcommandInfo] | |
@classmethod | |
def register( | |
cls, | |
name: Optional[str] = None, | |
usage: Optional[str] = None, | |
description: Optional[str] = None, | |
epilog: Optional[str] = None, | |
exist_ok: bool = False, | |
) -> Callable[[Type[Subclass]], Type[Subclass]]: | |
registry = Subcommand._registry[cls] | |
def wrapper(subclass: Type[Subclass]) -> Type[Subclass]: | |
info = SubcommandInfo( | |
name=name or cls.camel_to_snake(subclass.__name__), | |
usage=usage, | |
description=description or subclass.__doc__, | |
epilog=epilog, | |
) | |
subclass._cmd_info = info | |
if not exist_ok and name in registry: | |
raise ValueError(f"Subcommand '{name}' was already registered.") | |
registry[info.name] = subclass | |
return subclass | |
return wrapper | |
@classmethod | |
def get_info(cls) -> SubcommandInfo: | |
if not hasattr(cls, "_cmd_info"): | |
cls._cmd_info = SubcommandInfo( | |
name=cls.camel_to_snake(cls.__name__), | |
description=cls.__doc__, | |
) | |
return cls._cmd_info | |
@staticmethod | |
def camel_to_snake(text: str) -> str: | |
underscored = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text) | |
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", underscored).lower() | |
def __init__( | |
self, | |
parser_or_subparsers: Union[ | |
argparse.ArgumentParser, | |
argparse._SubParsersAction, | |
None, | |
] = None, | |
subcommand_info: Optional[SubcommandInfo] = None, | |
) -> None: | |
cls = type(self) | |
info = subcommand_info or self.get_info() | |
if isinstance(parser_or_subparsers, argparse.ArgumentParser): | |
self._parser = parser_or_subparsers | |
elif isinstance(parser_or_subparsers, argparse._SubParsersAction): | |
self._parser = parser_or_subparsers.add_parser( | |
name=info.name, | |
usage=info.usage, | |
description=info.description, | |
epilog=info.epilog, | |
) | |
else: | |
self._parser = argparse.ArgumentParser() | |
self.setup() | |
self._parser.set_defaults(**{self._func_key: self.run}) | |
registry = Subcommand._registry[cls] | |
if registry: | |
subparsers = self.parser.add_subparsers() | |
for subclass in registry.values(): | |
subclass(subparsers) | |
def __call__(self, args: Optional[argparse.Namespace] = None) -> None: | |
if not args: | |
args = self.parser.parse_args() | |
func = cast( | |
Optional[Callable[[argparse.Namespace], None]], | |
getattr(args, self._func_key, None), | |
) | |
if func: | |
func(args) | |
else: | |
self.parser.print_help() | |
@property | |
def parser(self) -> argparse.ArgumentParser: | |
return self._parser | |
def setup(self) -> None: | |
"""setup parser""" | |
def run(self, args: argparse.Namespace) -> None: | |
self.parser.print_help() | |
if __name__ == "__main__": | |
@Subcommand.register() | |
class Foo(Subcommand): | |
"""foo command""" | |
def setup(self) -> None: | |
self.parser.add_argument("--name", type=str) | |
def run(self, args: argparse.Namespace) -> None: | |
print(f"Hello, {args.name} from {type(self).__name__}!") | |
@Foo.register() | |
class Bar(Foo): | |
"""bar command""" | |
@Foo.register() | |
class Baz(Foo): | |
"""baz command""" | |
app = Subcommand() | |
app() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment