""" 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: str) -> str: return re.sub('_', '-', label) def _clean_func(func: str) -> Optional[str]: if func is None: return None else: return re.sub('-', '_', func) class ArgparseArgumentHandler(ArgumentParser, ArgumentHandler): """ This class implements the Argument Handler interface, and sub-classes from :py:class:`argparse.ArgumentParser`. Please reference the argparse documentation for full usage of the class. Arguments and keyword arguments are passed directly to ArgumentParser on initialization. """ class Meta(ArgumentHandler.Meta): """Handler meta-data.""" interface = 'argument' """The interface that this class implements.""" label = 'argparse' """The string identifier of the handler.""" ignore_unknown_arguments = False """ Whether or not to ignore any arguments passed that are not defined. Default behavoir by Argparse is to raise an "unknown argument" exception by Argparse. This affectively triggers the difference between using ``parse_args`` and ``parse_known_args``. Unknown arguments will be accessible as ``unknown_args``. """ _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, *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: args (list): A list of arguments (generally sys.argv) to be parsed. Returns: object: Instance object whose members are the arguments parsed. """ if self._meta.ignore_unknown_arguments is True: known_args, unknown_args = self.parse_known_args(*args) self.parsed_args = known_args # type: ignore self.unknown_args = unknown_args # type: ignore else: self.parsed_args = self.parse_args(*args) # type: ignore return self.parsed_args 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()``. See the :py:class:`argparse.ArgumentParser` documentation for help. """ 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): """ Used to expose functions to be listed as sub-commands under the controller namespace. It also decorates the function with meta-data for the argument parser. Keyword Args: hide (bool): Whether the command should be visible. arguments (list): List of tuples that define arguments to add to this commands sub-parser. parser_options (dict): Additional options to pass to Argparse. label (str): String identifier for the command. Example: .. code-block:: python class Base(ArgparseController): class Meta: label = 'base' # Note: Default functions only work in Python > 3.4 @expose(hide=True) def default(self): print("In Base.default()") @expose( help='this is the help message for my_command', aliases=['my_cmd'], # only available in Python 3+ arguments=[ (['-f', '--foo'], dict(help='foo option', action='store', dest='foo')), ] ) def my_command(self): print("In Base.my_command()") """ # pylint: disable=W0622 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: Callable) -> Callable: if self.label is None: self.label = func.__name__ 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 # shortcut for cleaner controllers ex = expose class ArgparseController(ControllerHandler): """ This is an implementation of the Controller handler interface, and is a base class that application controllers should subclass from. Registering it directly as a handler is useless. NOTE: This handler **requires** that the applications ``arg_handler`` be ``argparse``. If using an alternative argument handler you will need to write your own controller base class or modify this one. Example: .. code-block:: python from cement.ext.ext_argparse import ArgparseController class Base(ArgparseController): class Meta: label = 'base' description = 'description at the top of --help' epilog = "the text at the bottom of --help." arguments = [ ( ['-f', '--foo'], { 'help' : 'my foo option', 'dest' : 'foo' } ), ] class Second(ArgparseController): class Meta: label = 'second' stacked_on = 'base' stacked_type = 'embedded' arguments = [ ( ['--foo2'], { 'help' : 'my foo2 option', 'dest' : 'foo2' } ), ] """ class Meta(ControllerHandler.Meta): """ Controller meta-data (can be passed as keyword arguments to the parent class). """ # The interface this class implements. interface = 'controller' #: The string identifier for the controller. label: str = None # type: ignore #: A list of aliases for the controller/sub-parser. **Only available #: in Python > 3**. aliases: List[str] = [] #: A config [section] to merge config_defaults into. Cement will #: default to controller.