Type Annotations: ext.argparse

- Resolves Issue #710
    - Related to PR #628
This commit is contained in:
BJ Dierkes 2024-07-14 23:46:10 -05:00
parent 5b86bc2286
commit 29eb84c96b
2 changed files with 118 additions and 90 deletions

View File

@ -44,6 +44,7 @@ Refactoring:
- `[core.plugin]` [Issue #707](https://github.com/datafolklabs/cement/issues/707)
- `[core.template]` [Issue #708](https://github.com/datafolklabs/cement/issues/708)
- `[ext.alarm]` [Issue #709](https://github.com/datafolklabs/cement/issues/709)
- `[ext.argparse]` [Issue #710](https://github.com/datafolklabs/cement/issues/710)
- `[utils.fs]` [Issue #688](https://github.com/datafolklabs/cement/issues/688)
- `[utils.misc]` [Issue #689](https://github.com/datafolklabs/cement/issues/689)
- `[utils.shell]` [Issue #690](https://github.com/datafolklabs/cement/issues/690)

View File

@ -2,21 +2,27 @@
Cement argparse extension module.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from argparse import ArgumentParser, RawDescriptionHelpFormatter, SUPPRESS
from typing import Any, Callable, List, Dict, Tuple, Optional, TYPE_CHECKING
from ..core.arg import ArgumentHandler
from ..core.controller import ControllerHandler
from ..core.exc import FrameworkError
from ..utils.misc import minimal_logger
if TYPE_CHECKING:
from ..core.foundation import App, ArgparseArgumentType # pragma: nocover
LOG = minimal_logger(__name__)
def _clean_label(label):
def _clean_label(label: str) -> str:
return re.sub('_', '-', label)
def _clean_func(func):
def _clean_func(func: str) -> Optional[str]:
if func is None:
return None
else:
@ -35,7 +41,7 @@ class ArgparseArgumentHandler(ArgumentParser, ArgumentHandler):
on initialization.
"""
class Meta:
class Meta(ArgumentHandler.Meta):
"""Handler meta-data."""
@ -56,19 +62,21 @@ class ArgparseArgumentHandler(ArgumentParser, ArgumentHandler):
``unknown_args``.
"""
def __init__(self, *args, **kw):
_meta: Meta # type: ignore
def __init__(self, *args: Any, **kw: Any) -> None:
super().__init__(*args, **kw)
self.config = None
self.unknown_args = None
self.parsed_args = None
def parse(self, arg_list):
def parse(self, *args: List[str]) -> object:
"""
Parse a list of arguments, and return them as an object. Meaning an
argument name of 'foo' will be stored as parsed_args.foo.
Args:
arg_list (list): A list of arguments (generally sys.argv) to be
args (list): A list of arguments (generally sys.argv) to be
parsed.
Returns:
@ -77,15 +85,14 @@ class ArgparseArgumentHandler(ArgumentParser, ArgumentHandler):
"""
if self._meta.ignore_unknown_arguments is True:
args, unknown = self.parse_known_args(arg_list)
self.parsed_args = args
self.unknown_args = unknown
known_args, unknown_args = self.parse_known_args(*args)
self.parsed_args = known_args # type: ignore
self.unknown_args = unknown_args # type: ignore
else:
args = self.parse_args(arg_list)
self.parsed_args = args
self.parsed_args = self.parse_args(*args) # type: ignore
return self.parsed_args
def add_argument(self, *args, **kw):
def add_argument(self, *args: Any, **kw: Any) -> None: # type: ignore
"""
Add an argument to the parser. Arguments and keyword arguments are
passed directly to ``ArgumentParser.add_argument()``.
@ -94,6 +101,17 @@ class ArgparseArgumentHandler(ArgumentParser, ArgumentHandler):
super().add_argument(*args, **kw)
@dataclass
class CommandMeta:
label: str
func_name: str
exposed: bool
hide: bool
arguments: List[ArgparseArgumentType]
parser_options: Dict[str, Any]
controller: ArgparseController
class expose(object):
"""
@ -135,25 +153,31 @@ class expose(object):
"""
# pylint: disable=W0622
def __init__(self, hide=False, arguments=[], label=None, **parser_options):
def __init__(self,
hide: bool = False,
arguments: List[ArgparseArgumentType] = [],
label: Optional[str] = None,
**parser_options: Any) -> None:
self.hide = hide
self.arguments = arguments
self.label = label
self.parser_options = parser_options
def __call__(self, func):
def __call__(self, func: Callable) -> Callable:
if self.label is None:
self.label = func.__name__
metadict = {}
metadict['label'] = _clean_label(self.label)
metadict['func_name'] = func.__name__
metadict['exposed'] = True
metadict['hide'] = self.hide
metadict['arguments'] = self.arguments
metadict['parser_options'] = self.parser_options
metadict['controller'] = None # added by the controller
func.__cement_meta__ = metadict
meta = CommandMeta(
label=_clean_label(self.label),
func_name=func.__name__,
exposed=True,
hide=self.hide,
arguments=self.arguments,
parser_options=self.parser_options,
controller=None # type: ignore
)
func.__cement_meta__ = meta
return func
@ -206,7 +230,7 @@ class ArgparseController(ControllerHandler):
"""
class Meta:
class Meta(ControllerHandler.Meta):
"""
Controller meta-data (can be passed as keyword arguments to the parent
@ -218,19 +242,19 @@ class ArgparseController(ControllerHandler):
interface = 'controller'
#: The string identifier for the controller.
label = None
label: str = None # type: ignore
#: A list of aliases for the controller/sub-parser. **Only available
#: in Python > 3**.
aliases = []
aliases: List[str] = []
#: A config [section] to merge config_defaults into. Cement will
#: default to controller.<label> if None is set.
config_section = None
config_section: str = None # type: ignore
#: Configuration defaults (type: dict) that are merged into the
#: applications config object for the config_section mentioned above.
config_defaults = {}
config_defaults: Dict[str, Any] = {}
#: Arguments to pass to the argument_handler. The format is a list
#: of tuples whos items are a ( list, dict ). Meaning:
@ -241,7 +265,7 @@ class ArgparseController(ControllerHandler):
#: parser as in the following example:
#:
#: ``add_argument('-f', '--foo', help='foo option', dest='foo')``
arguments = []
arguments: List[ArgparseArgumentType] = []
#: A label of another controller to 'stack' commands/arguments on top
#: of.
@ -261,17 +285,17 @@ class ArgparseController(ControllerHandler):
#: Text for the controller/sub-parser group in help output (for
#: nested stacked controllers only).
help = None
help: str = None # type: ignore
#: Whether or not to hide the controller entirely.
hide = False
#: The text that is displayed at the bottom when ``--help`` is passed.
epilog = None
epilog: Optional[str] = None
#: The text that is displayed at the top when ``--help`` is passed.
#: Defaults to Argparse standard usage.
usage = None
usage: Optional[str] = None
#: The argument formatter class to use to display ``--help`` output.
argument_formatter = RawDescriptionHelpFormatter
@ -281,14 +305,14 @@ class ArgparseController(ControllerHandler):
#: controller namespace. WARNING: This could break things, use at
#: your own risk. Useful if you need additional features from
#: Argparse that is not built into the controller Meta-data.
subparser_options = {}
subparser_options: Dict = {}
#: Additional keyword arguments passed when
#: ``ArgumentParser.add_parser()`` is called to create this
#: controller sub-parser. WARNING: This could break things, use at
#: your own risk. Useful if you need additional features from
#: Argparse that is not built into the controller Meta-data.
parser_options = {}
parser_options: Dict = {}
#: Function to call if no sub-command is passed. By default this is
#: ``_default``, which is equivelant to passing ``-h/--help``. It
@ -301,26 +325,26 @@ class ArgparseController(ControllerHandler):
#: Note: Currently, default function/sub-command only works on
#: Python > 3.4. Previous versions of Python/Argparse will throw the
#: exception ``error: too few arguments``.
default_func = '_default'
default_func: str = '_default'
def __init__(self, *args, **kw):
def __init__(self, *args: Any, **kw: Any) -> None:
super().__init__(*args, **kw)
self.app = None
self._parser = None
self.app: App = None # type: ignore
self._parser: ArgumentParser = None # type: ignore
if self._meta.label == 'base':
self._sub_parser_parents = dict()
self._sub_parsers = dict()
self._controllers = []
self._controllers_map = {}
self._sub_parser_parents: Dict[str, Any] = dict()
self._sub_parsers: Dict[str, Any] = dict()
self._controllers: List[ArgparseController] = []
self._controllers_map: Dict[str, ArgparseController] = {}
if self._meta.help is None:
self._meta.help = f'{_clean_label(self._meta.label)} controller'
def _default(self):
def _default(self) -> None:
self._parser.print_help()
def _validate(self):
def _validate(self) -> None:
try:
assert self._meta.stacked_type in ['embedded', 'nested'], \
f"Invalid stacked type {self._meta.stacked_type}. " \
@ -328,22 +352,24 @@ class ArgparseController(ControllerHandler):
except AssertionError as e:
raise FrameworkError(e.args[0])
def _setup_controllers(self):
def _setup_controllers(self) -> None:
# need a list to maintain order
resolved_controllers = []
resolved_controllers: List[ArgparseController] = []
# need a dict to do key/label based lookup
resolved_controllers_map = {}
resolved_controllers_map: Dict[str, ArgparseController] = {}
# list to maintain which controllers we haven't resolved yet
unresolved_controllers = []
for contr in self.app.handler.list('controller'):
unresolved_controllers: List[ArgparseController] = []
for ctrl in self.app.handler.list('controller'):
# don't include self/base
if contr == self.__class__:
if ctrl == self.__class__:
continue
contr = self.app.handler.resolve('controller', contr, setup=True)
unresolved_controllers.append(contr)
handler: ArgparseController
handler = self.app.handler.resolve('controller', ctrl, setup=True) # type: ignore
unresolved_controllers.append(handler)
# treat self/base separately
resolved_controllers.append(self)
@ -360,8 +386,9 @@ class ArgparseController(ControllerHandler):
LOG.debug(f'current parent > {current_parent}')
# handle all controllers nested on parent
current_children = []
resolved_child_controllers = []
current_children: List[ArgparseController] = []
resolved_child_controllers: List[ArgparseController] = []
for contr in list(unresolved_controllers):
# if stacked_on is the current parent, we want to process
# its children in this run first
@ -418,11 +445,11 @@ class ArgparseController(ControllerHandler):
self._controllers = resolved_controllers
self._controllers_map = resolved_controllers_map
def _process_parsed_arguments(self):
def _process_parsed_arguments(self) -> None:
pass
def _get_subparser_options(self, contr):
kwargs = contr._meta.subparser_options.copy()
def _get_subparser_options(self, contr: ArgparseController) -> Dict[str, Any]:
kwargs: Dict[str, Any] = contr._meta.subparser_options.copy()
if 'title' not in kwargs.keys():
kwargs['title'] = contr._meta.title
@ -431,8 +458,8 @@ class ArgparseController(ControllerHandler):
return kwargs
def _get_parser_options(self, contr):
kwargs = contr._meta.parser_options.copy()
def _get_parser_options(self, contr: ArgparseController) -> Dict[str, Any]:
kwargs: Dict[str, Any] = contr._meta.parser_options.copy()
if 'aliases' not in kwargs.keys():
kwargs['aliases'] = contr._meta.aliases
@ -454,13 +481,13 @@ class ArgparseController(ControllerHandler):
return kwargs
def _get_command_parser_options(self, command):
kwargs = command['parser_options'].copy()
def _get_command_parser_options(self, command: CommandMeta) -> Dict[str, Any]:
kwargs: Dict[str, Any] = command.parser_options.copy()
contr = command['controller']
contr = command.controller
hide_it = False
if command['hide'] is True:
if command.hide is True:
hide_it = True
# only hide commands from embedded controllers if the controller is
@ -475,7 +502,7 @@ class ArgparseController(ControllerHandler):
return kwargs
def _setup_parsers(self):
def _setup_parsers(self) -> None:
# this should only be run by the base controller
from cement.utils.misc import rando
@ -577,7 +604,7 @@ class ArgparseController(ControllerHandler):
parsers[label] = parsers[stacked_on]
contr._parser = parsers[stacked_on]
def _get_parser_by_controller(self, controller):
def _get_parser_by_controller(self, controller: ArgparseController) -> ArgumentParser:
if controller._meta.stacked_type == 'embedded':
parser = self._get_parser(controller._meta.stacked_on)
else:
@ -585,7 +612,7 @@ class ArgparseController(ControllerHandler):
return parser
def _get_parser_parent_by_controller(self, controller):
def _get_parser_parent_by_controller(self, controller: ArgparseController) -> ArgumentParser:
if controller._meta.stacked_type == 'embedded':
parent = self._get_parser_parent(controller._meta.stacked_on)
else:
@ -593,13 +620,13 @@ class ArgparseController(ControllerHandler):
return parent
def _get_parser_parent(self, label):
return self._sub_parser_parents[label]
def _get_parser_parent(self, label: str) -> ArgumentParser:
return self._sub_parser_parents[label] # type: ignore
def _get_parser(self, label):
return self._sub_parsers[label]
def _get_parser(self, label: str) -> ArgumentParser:
return self._sub_parsers[label] # type: ignore
def _process_arguments(self, controller):
def _process_arguments(self, controller: ArgparseController) -> None:
label = controller._meta.label
LOG.debug(f"processing arguments for '{label}' " +
@ -611,7 +638,7 @@ class ArgparseController(ControllerHandler):
LOG.debug(f'adding argument (args={arg}, kwargs={kw})')
parser.add_argument(*arg, **kw)
def _process_commands(self, controller):
def _process_commands(self, controller: ArgparseController) -> None:
label = controller._meta.label
LOG.debug(f"processing commands for '{label}' " +
"controller namespace")
@ -620,17 +647,17 @@ class ArgparseController(ControllerHandler):
for command in commands:
kwargs = self._get_command_parser_options(command)
func_name = command['func_name']
LOG.debug(f"adding command '{command['label']}' " +
func_name = command.func_name
LOG.debug(f"adding command '{command.label}' " +
f"(controller={controller._meta.label}, func={func_name})")
cmd_parent = self._get_parser_parent_by_controller(controller)
command_parser = cmd_parent.add_parser(command['label'], **kwargs)
command_parser = cmd_parent.add_parser(command.label, **kwargs)
# add an invisible dispatch option so we can figure out what to
# call later in self._dispatch
default_contr_func = "%s.%s" % (command['controller']._meta.label,
command['func_name'])
default_contr_func = "%s.%s" % (command.controller._meta.label,
command.func_name)
command_parser.add_argument(self._dispatch_option,
action='store',
default=default_contr_func,
@ -639,24 +666,24 @@ class ArgparseController(ControllerHandler):
)
# add additional arguments to the sub-command namespace
LOG.debug(f"processing arguments for '{command['label']}' " +
LOG.debug(f"processing arguments for '{command.label}' " +
"command namespace")
for arg, kw in command['arguments']:
for arg, kw in command.arguments:
LOG.debug(f'adding argument (args={arg}, kwargs={kw})')
command_parser.add_argument(*arg, **kw)
def _collect(self):
def _collect(self) -> Tuple[List[ArgparseArgumentType], List[CommandMeta]]:
arguments = self._collect_arguments()
commands = self._collect_commands()
return (arguments, commands)
def _collect_arguments(self):
def _collect_arguments(self) -> List[ArgparseArgumentType]:
LOG.debug(f"collecting arguments from {self} " +
"(stacked_on='%s', stacked_type='%s')" %
(self._meta.stacked_on, self._meta.stacked_type))
return self._meta.arguments
return self._meta.arguments # type: ignore
def _collect_commands(self):
def _collect_commands(self) -> List[CommandMeta]:
LOG.debug(f"collecting commands from {self} " +
"(stacked_on='%s', stacked_type='%s')" %
(self._meta.stacked_on, self._meta.stacked_type))
@ -666,13 +693,13 @@ class ArgparseController(ControllerHandler):
if member.startswith('_'):
continue
elif hasattr(getattr(self, member), '__cement_meta__'):
func = getattr(self.__class__, member).__cement_meta__
func['controller'] = self
func: CommandMeta = getattr(self.__class__, member).__cement_meta__
func.controller = self
commands.append(func)
return commands
def _get_exposed_commands(self):
def _get_exposed_commands(self) -> List[str]:
"""
Get a list of exposed commands for this controller
@ -688,7 +715,7 @@ class ArgparseController(ControllerHandler):
exposed.append(_clean_label(member_key))
return exposed
def _pre_argument_parsing(self):
def _pre_argument_parsing(self) -> None:
"""
Called on every controller just before arguments are parsed.
Provides an alternative means of adding arguments to the controller,
@ -715,7 +742,7 @@ class ArgparseController(ControllerHandler):
"""
pass
def _post_argument_parsing(self):
def _post_argument_parsing(self) -> None:
"""
Called on every controller just after arguments are parsed (assuming
that the parser hasn't thrown an exception). Provides an alternative
@ -758,7 +785,7 @@ class ArgparseController(ControllerHandler):
"""
pass
def _dispatch(self):
def _dispatch(self) -> Any:
LOG.debug(f"controller dispatch passed off to {self}")
self._setup_controllers()
self._setup_parsers()
@ -817,5 +844,5 @@ class ArgparseController(ControllerHandler):
(contr.__class__.__name__, func_name)) # pragma: nocover
def load(app):
def load(app: App) -> None:
app.handler.register(ArgparseArgumentHandler)