edq.core.argparser

A place to handle common CLI arguments. "parsers" in this file are always assumed to be argparse parsers.

The general idea is that callers can register callbacks to be called before and after parsing CLI arguments. Pre-callbacks are generally intended to add arguments to the parser, while post-callbacks are generally intended to act on the results of parsing.

  1"""
  2A place to handle common CLI arguments.
  3"parsers" in this file are always assumed to be argparse parsers.
  4
  5The general idea is that callers can register callbacks to be called before and after parsing CLI arguments.
  6Pre-callbacks are generally intended to add arguments to the parser,
  7while post-callbacks are generally intended to act on the results of parsing.
  8"""
  9
 10import argparse
 11import typing
 12
 13import edq.core.log
 14
 15@typing.runtime_checkable
 16class PreParseFunction(typing.Protocol):
 17    """
 18    A function that can be called before parsing arguments.
 19    """
 20
 21    def __call__(self, parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
 22        """
 23        Prepare a parser for parsing.
 24        This is generally used for adding your module's arguments to the parser,
 25        for example a logging module may add arguments to set a logging level.
 26
 27        The extra state is shared between all pre-parse functions
 28        and will be placed in the final parsed output under `_pre_extra_state_`.
 29        """
 30
 31@typing.runtime_checkable
 32class PostParseFunction(typing.Protocol):
 33    """
 34    A function that can be called after parsing arguments.
 35    """
 36
 37    def __call__(self,
 38            parser: argparse.ArgumentParser,
 39            args: argparse.Namespace,
 40            extra_state: typing.Dict[str, typing.Any]) -> None:
 41        """
 42        Take actions after arguments are parsed.
 43        This is generally used for initializing your module with options,
 44        for example a logging module may set a logging level.
 45
 46        The extra state is shared between all post-parse functions
 47        and will be placed in the final parsed output under `_post_extra_state_`.
 48        """
 49
 50class Parser(argparse.ArgumentParser):
 51    """
 52    Extend an argparse parser to call the pre and post functions.
 53    """
 54
 55    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
 56        super().__init__(*args, **kwargs)
 57
 58        self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
 59        self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
 60
 61    def register_callbacks(self,
 62            key: str,
 63            pre_parse_callback: typing.Union[PreParseFunction, None] = None,
 64            post_parse_callback: typing.Union[PostParseFunction, None] = None,
 65            ) -> None:
 66        """
 67        Register callback functions to run before/after argument parsing.
 68        Any existing callbacks under the specified key will be replaced.
 69        """
 70
 71        if (pre_parse_callback is not None):
 72            self._pre_parse_callbacks[key] = pre_parse_callback
 73
 74        if (post_parse_callback is not None):
 75            self._post_parse_callbacks[key] = post_parse_callback
 76
 77    def parse_args(self,  # type: ignore[override]
 78            *args: typing.Any,
 79            skip_keys: typing.Union[typing.List[str], None] = None,
 80            **kwargs: typing.Any) -> argparse.Namespace:
 81        if (skip_keys is None):
 82            skip_keys = []
 83
 84        # Call pre-parse callbacks.
 85        pre_extra_state: typing.Dict[str, typing.Any] = {}
 86        for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
 87            if (key not in skip_keys):
 88                pre_parse_callback(self, pre_extra_state)
 89
 90        # Parse the args.
 91        parsed_args = super().parse_args(*args, **kwargs)
 92
 93        # Call post-parse callbacks.
 94        post_extra_state: typing.Dict[str, typing.Any] = {}
 95        for (key, post_parse_callback) in self._post_parse_callbacks.items():
 96            if (key not in skip_keys):
 97                post_parse_callback(self, parsed_args, post_extra_state)
 98
 99        # Attach the additional state to the args.
100        setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
101        setattr(parsed_args, '_post_extra_state_', post_extra_state)
102
103        return parsed_args  # type: ignore[no-any-return]
104
105def get_default_parser(description: str) -> Parser:
106    """ Get a parser with the default callbacks already attached. """
107
108    parser = Parser(description = description)
109
110    parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
111
112    return parser
@typing.runtime_checkable
class PreParseFunction(typing.Protocol):
16@typing.runtime_checkable
17class PreParseFunction(typing.Protocol):
18    """
19    A function that can be called before parsing arguments.
20    """
21
22    def __call__(self, parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
23        """
24        Prepare a parser for parsing.
25        This is generally used for adding your module's arguments to the parser,
26        for example a logging module may add arguments to set a logging level.
27
28        The extra state is shared between all pre-parse functions
29        and will be placed in the final parsed output under `_pre_extra_state_`.
30        """

A function that can be called before parsing arguments.

PreParseFunction(*args, **kwargs)
1953def _no_init_or_replace_init(self, *args, **kwargs):
1954    cls = type(self)
1955
1956    if cls._is_protocol:
1957        raise TypeError('Protocols cannot be instantiated')
1958
1959    # Already using a custom `__init__`. No need to calculate correct
1960    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1961    if cls.__init__ is not _no_init_or_replace_init:
1962        return
1963
1964    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1965    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1966    # searches for a proper new `__init__` in the MRO. The new `__init__`
1967    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1968    # instantiation of the protocol subclass will thus use the new
1969    # `__init__` and no longer call `_no_init_or_replace_init`.
1970    for base in cls.__mro__:
1971        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1972        if init is not _no_init_or_replace_init:
1973            cls.__init__ = init
1974            break
1975    else:
1976        # should not happen
1977        cls.__init__ = object.__init__
1978
1979    cls.__init__(self, *args, **kwargs)
@typing.runtime_checkable
class PostParseFunction(typing.Protocol):
32@typing.runtime_checkable
33class PostParseFunction(typing.Protocol):
34    """
35    A function that can be called after parsing arguments.
36    """
37
38    def __call__(self,
39            parser: argparse.ArgumentParser,
40            args: argparse.Namespace,
41            extra_state: typing.Dict[str, typing.Any]) -> None:
42        """
43        Take actions after arguments are parsed.
44        This is generally used for initializing your module with options,
45        for example a logging module may set a logging level.
46
47        The extra state is shared between all post-parse functions
48        and will be placed in the final parsed output under `_post_extra_state_`.
49        """

A function that can be called after parsing arguments.

PostParseFunction(*args, **kwargs)
1953def _no_init_or_replace_init(self, *args, **kwargs):
1954    cls = type(self)
1955
1956    if cls._is_protocol:
1957        raise TypeError('Protocols cannot be instantiated')
1958
1959    # Already using a custom `__init__`. No need to calculate correct
1960    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1961    if cls.__init__ is not _no_init_or_replace_init:
1962        return
1963
1964    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1965    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1966    # searches for a proper new `__init__` in the MRO. The new `__init__`
1967    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1968    # instantiation of the protocol subclass will thus use the new
1969    # `__init__` and no longer call `_no_init_or_replace_init`.
1970    for base in cls.__mro__:
1971        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1972        if init is not _no_init_or_replace_init:
1973            cls.__init__ = init
1974            break
1975    else:
1976        # should not happen
1977        cls.__init__ = object.__init__
1978
1979    cls.__init__(self, *args, **kwargs)
class Parser(argparse.ArgumentParser):
 51class Parser(argparse.ArgumentParser):
 52    """
 53    Extend an argparse parser to call the pre and post functions.
 54    """
 55
 56    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
 57        super().__init__(*args, **kwargs)
 58
 59        self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
 60        self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
 61
 62    def register_callbacks(self,
 63            key: str,
 64            pre_parse_callback: typing.Union[PreParseFunction, None] = None,
 65            post_parse_callback: typing.Union[PostParseFunction, None] = None,
 66            ) -> None:
 67        """
 68        Register callback functions to run before/after argument parsing.
 69        Any existing callbacks under the specified key will be replaced.
 70        """
 71
 72        if (pre_parse_callback is not None):
 73            self._pre_parse_callbacks[key] = pre_parse_callback
 74
 75        if (post_parse_callback is not None):
 76            self._post_parse_callbacks[key] = post_parse_callback
 77
 78    def parse_args(self,  # type: ignore[override]
 79            *args: typing.Any,
 80            skip_keys: typing.Union[typing.List[str], None] = None,
 81            **kwargs: typing.Any) -> argparse.Namespace:
 82        if (skip_keys is None):
 83            skip_keys = []
 84
 85        # Call pre-parse callbacks.
 86        pre_extra_state: typing.Dict[str, typing.Any] = {}
 87        for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
 88            if (key not in skip_keys):
 89                pre_parse_callback(self, pre_extra_state)
 90
 91        # Parse the args.
 92        parsed_args = super().parse_args(*args, **kwargs)
 93
 94        # Call post-parse callbacks.
 95        post_extra_state: typing.Dict[str, typing.Any] = {}
 96        for (key, post_parse_callback) in self._post_parse_callbacks.items():
 97            if (key not in skip_keys):
 98                post_parse_callback(self, parsed_args, post_extra_state)
 99
100        # Attach the additional state to the args.
101        setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
102        setattr(parsed_args, '_post_extra_state_', post_extra_state)
103
104        return parsed_args  # type: ignore[no-any-return]

Extend an argparse parser to call the pre and post functions.

Parser(*args: Any, **kwargs: Any)
56    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
57        super().__init__(*args, **kwargs)
58
59        self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
60        self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
def register_callbacks( self, key: str, pre_parse_callback: Optional[PreParseFunction] = None, post_parse_callback: Optional[PostParseFunction] = None) -> None:
62    def register_callbacks(self,
63            key: str,
64            pre_parse_callback: typing.Union[PreParseFunction, None] = None,
65            post_parse_callback: typing.Union[PostParseFunction, None] = None,
66            ) -> None:
67        """
68        Register callback functions to run before/after argument parsing.
69        Any existing callbacks under the specified key will be replaced.
70        """
71
72        if (pre_parse_callback is not None):
73            self._pre_parse_callbacks[key] = pre_parse_callback
74
75        if (post_parse_callback is not None):
76            self._post_parse_callbacks[key] = post_parse_callback

Register callback functions to run before/after argument parsing. Any existing callbacks under the specified key will be replaced.

def parse_args( self, *args: Any, skip_keys: Optional[List[str]] = None, **kwargs: Any) -> argparse.Namespace:
 78    def parse_args(self,  # type: ignore[override]
 79            *args: typing.Any,
 80            skip_keys: typing.Union[typing.List[str], None] = None,
 81            **kwargs: typing.Any) -> argparse.Namespace:
 82        if (skip_keys is None):
 83            skip_keys = []
 84
 85        # Call pre-parse callbacks.
 86        pre_extra_state: typing.Dict[str, typing.Any] = {}
 87        for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
 88            if (key not in skip_keys):
 89                pre_parse_callback(self, pre_extra_state)
 90
 91        # Parse the args.
 92        parsed_args = super().parse_args(*args, **kwargs)
 93
 94        # Call post-parse callbacks.
 95        post_extra_state: typing.Dict[str, typing.Any] = {}
 96        for (key, post_parse_callback) in self._post_parse_callbacks.items():
 97            if (key not in skip_keys):
 98                post_parse_callback(self, parsed_args, post_extra_state)
 99
100        # Attach the additional state to the args.
101        setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
102        setattr(parsed_args, '_post_extra_state_', post_extra_state)
103
104        return parsed_args  # type: ignore[no-any-return]
def get_default_parser(description: str) -> Parser:
106def get_default_parser(description: str) -> Parser:
107    """ Get a parser with the default callbacks already attached. """
108
109    parser = Parser(description = description)
110
111    parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
112
113    return parser

Get a parser with the default callbacks already attached.