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 functools
 12import typing
 13
 14import edq.config.argparser
 15import edq.config.constants
 16import edq.core.log
 17import edq.net.cli
 18
 19@typing.runtime_checkable
 20class PreParseFunction(typing.Protocol):
 21    """
 22    A function that can be called before parsing arguments.
 23    """
 24
 25    def __call__(self, parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
 26        """
 27        Prepare a parser for parsing.
 28        This is generally used for adding your module's arguments to the parser,
 29        for example a logging module may add arguments to set a logging level.
 30
 31        The extra state is shared between all pre-parse functions
 32        and will be placed in the final parsed output under `_pre_extra_state_`.
 33        """
 34
 35@typing.runtime_checkable
 36class PostParseFunction(typing.Protocol):
 37    """
 38    A function that can be called after parsing arguments.
 39    """
 40
 41    def __call__(self,
 42            parser: argparse.ArgumentParser,
 43            args: argparse.Namespace,
 44            extra_state: typing.Dict[str, typing.Any]) -> None:
 45        """
 46        Take actions after arguments are parsed.
 47        This is generally used for initializing your module with options,
 48        for example a logging module may set a logging level.
 49
 50        The extra state is shared between all post-parse functions
 51        and will be placed in the final parsed output under `_post_extra_state_`.
 52        """
 53
 54class HelpFormatter(argparse.RawDescriptionHelpFormatter):
 55    """ A default formatter that modifiers indents and does not strip descriptions. """
 56
 57    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
 58        if ('indent_increment' not in kwargs):
 59            kwargs['indent_increment'] = edq.config.constants.DEFAULT_INDENT
 60
 61        super().__init__(*args, **kwargs)
 62
 63class Parser(argparse.ArgumentParser):
 64    """
 65    Extend an argparse parser to call the pre and post functions.
 66    """
 67
 68    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
 69        if ('formatter_class' not in kwargs):
 70            kwargs['formatter_class'] = HelpFormatter
 71
 72        super().__init__(*args, **kwargs)
 73
 74        self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
 75        self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
 76
 77    def register_callbacks(self,
 78            key: str,
 79            pre_parse_callback: typing.Union[PreParseFunction, None] = None,
 80            post_parse_callback: typing.Union[PostParseFunction, None] = None,
 81            ) -> None:
 82        """
 83        Register callback functions to run before/after argument parsing.
 84        Any existing callbacks under the specified key will be replaced.
 85        """
 86
 87        if (pre_parse_callback is not None):
 88            self._pre_parse_callbacks[key] = pre_parse_callback
 89
 90        if (post_parse_callback is not None):
 91            self._post_parse_callbacks[key] = post_parse_callback
 92
 93    def parse_args(self,  # type: ignore[override]
 94            *args: typing.Any,
 95            skip_keys: typing.Union[typing.List[str], None] = None,
 96            **kwargs: typing.Any) -> argparse.Namespace:
 97        if (skip_keys is None):
 98            skip_keys = []
 99
100        # Call pre-parse callbacks.
101        pre_extra_state: typing.Dict[str, typing.Any] = {}
102        for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
103            if (key not in skip_keys):
104                pre_parse_callback(self, pre_extra_state)
105
106        # Parse the args.
107        parsed_args = super().parse_args(*args, **kwargs)
108
109        # Call post-parse callbacks.
110        post_extra_state: typing.Dict[str, typing.Any] = {}
111        for (key, post_parse_callback) in self._post_parse_callbacks.items():
112            if (key not in skip_keys):
113                post_parse_callback(self, parsed_args, post_extra_state)
114
115        # Attach the additional state to the args.
116        setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
117        setattr(parsed_args, '_post_extra_state_', post_extra_state)
118
119        return parsed_args  # type: ignore[no-any-return]
120
121def get_default_parser(description: str,
122        version: typing.Union[str, None] = None,
123        include_log: bool = True,
124        include_config: bool = True,
125        include_net: bool = False,
126        config_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
127        ) -> Parser:
128    """ Get a parser with the requested default callbacks already attached. """
129
130    if (config_options is None):
131        config_options = {}
132
133    parser = Parser(description = description)
134
135    if (version is not None):
136        parser.add_argument('--version',
137                action = 'version', version = version)
138
139    if (include_log):
140        parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
141
142    if (include_config):
143        config_pre_func = functools.partial(edq.config.argparser.set_cli_args, **config_options)
144        config_post_func = functools.partial(edq.config.argparser.load_config_into_args, **config_options)
145        parser.register_callbacks('config', config_pre_func, config_post_func)
146
147    if (include_net):
148        parser.register_callbacks('net', edq.net.cli.set_cli_args, edq.net.cli.init_from_args)
149
150    return parser
@typing.runtime_checkable
class PreParseFunction(typing.Protocol):
20@typing.runtime_checkable
21class PreParseFunction(typing.Protocol):
22    """
23    A function that can be called before parsing arguments.
24    """
25
26    def __call__(self, parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
27        """
28        Prepare a parser for parsing.
29        This is generally used for adding your module's arguments to the parser,
30        for example a logging module may add arguments to set a logging level.
31
32        The extra state is shared between all pre-parse functions
33        and will be placed in the final parsed output under `_pre_extra_state_`.
34        """

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):
36@typing.runtime_checkable
37class PostParseFunction(typing.Protocol):
38    """
39    A function that can be called after parsing arguments.
40    """
41
42    def __call__(self,
43            parser: argparse.ArgumentParser,
44            args: argparse.Namespace,
45            extra_state: typing.Dict[str, typing.Any]) -> None:
46        """
47        Take actions after arguments are parsed.
48        This is generally used for initializing your module with options,
49        for example a logging module may set a logging level.
50
51        The extra state is shared between all post-parse functions
52        and will be placed in the final parsed output under `_post_extra_state_`.
53        """

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 HelpFormatter(argparse.RawDescriptionHelpFormatter):
55class HelpFormatter(argparse.RawDescriptionHelpFormatter):
56    """ A default formatter that modifiers indents and does not strip descriptions. """
57
58    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
59        if ('indent_increment' not in kwargs):
60            kwargs['indent_increment'] = edq.config.constants.DEFAULT_INDENT
61
62        super().__init__(*args, **kwargs)

A default formatter that modifiers indents and does not strip descriptions.

HelpFormatter(*args: Any, **kwargs: Any)
58    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
59        if ('indent_increment' not in kwargs):
60            kwargs['indent_increment'] = edq.config.constants.DEFAULT_INDENT
61
62        super().__init__(*args, **kwargs)
class Parser(argparse.ArgumentParser):
 64class Parser(argparse.ArgumentParser):
 65    """
 66    Extend an argparse parser to call the pre and post functions.
 67    """
 68
 69    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
 70        if ('formatter_class' not in kwargs):
 71            kwargs['formatter_class'] = HelpFormatter
 72
 73        super().__init__(*args, **kwargs)
 74
 75        self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
 76        self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
 77
 78    def register_callbacks(self,
 79            key: str,
 80            pre_parse_callback: typing.Union[PreParseFunction, None] = None,
 81            post_parse_callback: typing.Union[PostParseFunction, None] = None,
 82            ) -> None:
 83        """
 84        Register callback functions to run before/after argument parsing.
 85        Any existing callbacks under the specified key will be replaced.
 86        """
 87
 88        if (pre_parse_callback is not None):
 89            self._pre_parse_callbacks[key] = pre_parse_callback
 90
 91        if (post_parse_callback is not None):
 92            self._post_parse_callbacks[key] = post_parse_callback
 93
 94    def parse_args(self,  # type: ignore[override]
 95            *args: typing.Any,
 96            skip_keys: typing.Union[typing.List[str], None] = None,
 97            **kwargs: typing.Any) -> argparse.Namespace:
 98        if (skip_keys is None):
 99            skip_keys = []
100
101        # Call pre-parse callbacks.
102        pre_extra_state: typing.Dict[str, typing.Any] = {}
103        for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
104            if (key not in skip_keys):
105                pre_parse_callback(self, pre_extra_state)
106
107        # Parse the args.
108        parsed_args = super().parse_args(*args, **kwargs)
109
110        # Call post-parse callbacks.
111        post_extra_state: typing.Dict[str, typing.Any] = {}
112        for (key, post_parse_callback) in self._post_parse_callbacks.items():
113            if (key not in skip_keys):
114                post_parse_callback(self, parsed_args, post_extra_state)
115
116        # Attach the additional state to the args.
117        setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
118        setattr(parsed_args, '_post_extra_state_', post_extra_state)
119
120        return parsed_args  # type: ignore[no-any-return]

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

Parser(*args: Any, **kwargs: Any)
69    def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
70        if ('formatter_class' not in kwargs):
71            kwargs['formatter_class'] = HelpFormatter
72
73        super().__init__(*args, **kwargs)
74
75        self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
76        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:
78    def register_callbacks(self,
79            key: str,
80            pre_parse_callback: typing.Union[PreParseFunction, None] = None,
81            post_parse_callback: typing.Union[PostParseFunction, None] = None,
82            ) -> None:
83        """
84        Register callback functions to run before/after argument parsing.
85        Any existing callbacks under the specified key will be replaced.
86        """
87
88        if (pre_parse_callback is not None):
89            self._pre_parse_callbacks[key] = pre_parse_callback
90
91        if (post_parse_callback is not None):
92            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:
 94    def parse_args(self,  # type: ignore[override]
 95            *args: typing.Any,
 96            skip_keys: typing.Union[typing.List[str], None] = None,
 97            **kwargs: typing.Any) -> argparse.Namespace:
 98        if (skip_keys is None):
 99            skip_keys = []
100
101        # Call pre-parse callbacks.
102        pre_extra_state: typing.Dict[str, typing.Any] = {}
103        for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
104            if (key not in skip_keys):
105                pre_parse_callback(self, pre_extra_state)
106
107        # Parse the args.
108        parsed_args = super().parse_args(*args, **kwargs)
109
110        # Call post-parse callbacks.
111        post_extra_state: typing.Dict[str, typing.Any] = {}
112        for (key, post_parse_callback) in self._post_parse_callbacks.items():
113            if (key not in skip_keys):
114                post_parse_callback(self, parsed_args, post_extra_state)
115
116        # Attach the additional state to the args.
117        setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
118        setattr(parsed_args, '_post_extra_state_', post_extra_state)
119
120        return parsed_args  # type: ignore[no-any-return]
def get_default_parser( description: str, version: Optional[str] = None, include_log: bool = True, include_config: bool = True, include_net: bool = False, config_options: Optional[Dict[str, Any]] = None) -> Parser:
122def get_default_parser(description: str,
123        version: typing.Union[str, None] = None,
124        include_log: bool = True,
125        include_config: bool = True,
126        include_net: bool = False,
127        config_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
128        ) -> Parser:
129    """ Get a parser with the requested default callbacks already attached. """
130
131    if (config_options is None):
132        config_options = {}
133
134    parser = Parser(description = description)
135
136    if (version is not None):
137        parser.add_argument('--version',
138                action = 'version', version = version)
139
140    if (include_log):
141        parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
142
143    if (include_config):
144        config_pre_func = functools.partial(edq.config.argparser.set_cli_args, **config_options)
145        config_post_func = functools.partial(edq.config.argparser.load_config_into_args, **config_options)
146        parser.register_callbacks('config', config_pre_func, config_post_func)
147
148    if (include_net):
149        parser.register_callbacks('net', edq.net.cli.set_cli_args, edq.net.cli.init_from_args)
150
151    return parser

Get a parser with the requested default callbacks already attached.