Source code for interfacy.argparse_backend.argument_parser

import argparse
import re
import sys
from argparse import Namespace
from collections.abc import Callable, Sequence
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Literal

from objinspect.typing import type_name
from typing_extensions import Never

from interfacy.appearance.layouts import StandardLayout
from interfacy.appearance.renderer import (
    SchemaHelpRenderer,
    command_has_grouped_subcommands,
    has_grouped_commands,
)
from interfacy.argparse_backend.help_formatter import InterfacyHelpFormatter
from interfacy.logger import get_logger
from interfacy.schema.schema import Argument, ArgumentKind, BooleanBehavior, Command, ValueShape

if TYPE_CHECKING:
    from interfacy.appearance.layout import HelpLayout
    from interfacy.schema.schema import ParserSchema

logger = get_logger(__name__)


DEST_KEY = "dest"
ActionType = Callable[[str], Any] | type[Any] | str | None
NargsPattern = Literal["?", "*", "+"]
SUBCOMMANDS_KEY = "_subcommands"


class ArgparseParseError(Exception):
    """Internal recoverable argparse parse error."""


def _uses_template_layout(layout: "HelpLayout | None") -> bool:
    if layout is None:
        return False

    use_template_layout = getattr(layout, "_use_template_layout", None)
    if not callable(use_template_layout):
        return False

    return bool(use_template_layout())


def _callable_type_name(value: Any, *, fallback: str = "value") -> str:
    if value is None:
        return fallback

    name = getattr(value, "__name__", None)
    if isinstance(name, str) and name:
        return name

    parsed_type = getattr(value, "_t", None)
    if parsed_type is not None:
        return type_name(str(parsed_type))

    keywords = getattr(value, "keywords", None)
    if isinstance(keywords, dict) and keywords.get("t") is not None:
        return type_name(str(keywords["t"]))

    return type_name(str(value))


def _set_callable_type_name(value: Any) -> None:
    if not callable(value) or getattr(value, "__name__", None):
        return

    try:
        value.__name__ = _callable_type_name(value)  # type: ignore[attr-defined]
    except (AttributeError, TypeError):
        pass


def namespace_to_dict(namespace: Namespace) -> dict[str, Any]:
    """
    Convert an argparse Namespace into a nested dictionary.

    Args:
        namespace (Namespace): Parsed namespace to convert.
    """
    result = {}
    for k, v in vars(namespace).items():
        if isinstance(v, Namespace):
            result[k] = namespace_to_dict(v)
        else:
            result[k] = v

    return result


def _action_to_help_argument(action: argparse.Action) -> Argument:
    help_text = action.help if isinstance(action.help, str) else None
    return Argument(
        name=action.dest or "help",
        display_name=action.dest or "help",
        kind=ArgumentKind.OPTION,
        value_shape=ValueShape.FLAG,
        flags=tuple(action.option_strings),
        required=False,
        default=action.default,
        help=help_text,
        type=None,
        parser=None,
        is_help_action=True,
    )


def _action_metavar(action: argparse.Action) -> str | None:
    metavar = action.metavar
    return metavar if isinstance(metavar, str) else None


def _action_value_shape(action: argparse.Action) -> ValueShape:
    if getattr(action, "nargs", None) == 0:
        return ValueShape.FLAG
    if isinstance(action, argparse._AppendAction):  # type: ignore[private-member-access]
        return ValueShape.LIST
    if isinstance(action.nargs, int) and action.nargs > 1:
        return ValueShape.TUPLE
    if action.nargs in ("*", "+"):
        return ValueShape.LIST

    return ValueShape.SINGLE


class NestedSubParsersAction(argparse._SubParsersAction):  # type: ignore[private-member-access]
    """
    Subparser action that supports nested destination paths.

    Args:
        option_strings (list[str]): Option strings that trigger the action.
        prog (str): Program name for help output.
        base_nest_path (list[str]): Base nesting path components.
        nest_separator (str): Separator for nested destination keys.
        parser_class (type[ArgumentParser] | None): Parser class for children.
        dest (str): Destination key for the subparser.
        required (bool): Whether a subcommand is required.
        help (str | None): Help text for the action.
        metavar (str | None): Metavar for help output.
        formatter_class (type[argparse.HelpFormatter] | None): Formatter class for children.
        help_layout (Any | None): Layout configuration passed to children.
    """

    def __init__(
        self,
        option_strings: list[str],
        prog: str,
        base_nest_path: list[str],
        nest_separator: str,
        parser_class: type["ArgumentParser"] | None = None,
        dest: str = argparse.SUPPRESS,
        required: bool = False,
        help: str | None = None,  # noqa: A002 - argparse API compatibility
        metavar: str | None = None,
        formatter_class: type[argparse.HelpFormatter] | None = None,
        help_layout: "HelpLayout | None" = None,
        help_flags: Sequence[str] = ("--help",),
    ) -> None:
        super().__init__(
            option_strings,
            prog,
            parser_class or ArgumentParser,
            dest=dest,
            required=required,
            help=help,
            metavar=metavar,
        )

        self.base_nest_path_components = base_nest_path
        self.nest_separator = nest_separator
        self._child_formatter_class = formatter_class or InterfacyHelpFormatter
        self._child_help_layout = help_layout
        self._child_help_flags = tuple(help_flags)

    def add_parser(  # type: ignore[override]
        self,
        name: str,
        *,
        help: str | None = None,  # noqa: A002 - argparse API compatibility
        aliases: Sequence[str] = (),
        prog: str | None = None,
        usage: str | None = None,
        description: str | None = None,
        epilog: str | None = None,
        parents: Sequence[argparse.ArgumentParser] = (),
        formatter_class: type[argparse.HelpFormatter] | None = None,
        prefix_chars: str = "-",
        fromfile_prefix_chars: str | None = None,
        argument_default: Any = None,
        conflict_handler: str = "error",
        add_help: bool = True,
        allow_abbrev: bool = True,
        exit_on_error: bool = True,
        nest_dir: str | None = None,
        **kwargs: Any,
    ) -> "ArgumentParser":
        """
        Creates and returns a new parser for a subcommand with nesting support.

        Args:
            name (str): Name of the subcommand.
            help (str | None, optional): Help message for the subcommand. Defaults to None.
            aliases (Sequence[str], optional): Alternative names for the subcommand. Defaults to ().
            prog (str | None, optional): Program name. Defaults to None.
            usage (str | None, optional): Usage message. Defaults to None.
            description (str | None, optional): Description of the subcommand. Defaults to None.
            epilog (str | None, optional): Text following the argument descriptions. Defaults to None.
            parents (Sequence[ArgumentParser], optional): Parent parsers. Defaults to ().
            formatter_class (Type[HelpFormatter], optional): Help message formatter. Defaults to HelpFormatter.
            prefix_chars (str, optional): Characters that prefix optional arguments. Defaults to "-".
            fromfile_prefix_chars (str | None, optional): Characters prefixing files with arguments. Defaults to None.
            argument_default (Any, optional): Default value for all arguments. Defaults to None.
            conflict_handler (str, optional): How to handle conflicts. Defaults to "error".
            add_help (bool, optional): Add a --help option. Defaults to True.
            allow_abbrev (bool, optional): Allow abbreviated long options. Defaults to True.
            exit_on_error (bool, optional): Exit with error info on error. Defaults to True.
            nest_dir (str | None, optional): Custom nesting directory name. Defaults to name if not provided.
            **kwargs: Additional arguments passed to parent class.

        Returns:
            NestedArgumentParser: A new parser for the subcommand.
        """
        kwargs.setdefault("help_layout", self._child_help_layout)
        nested_components = [*self.base_nest_path_components]
        if nested_components:
            nested_components.extend([SUBCOMMANDS_KEY, nest_dir or name])
        else:
            nested_components.append(nest_dir or name)

        parser: ArgumentParser = super().add_parser(  # type: ignore[assignment]
            name,
            help=help,
            aliases=aliases,
            prog=prog,
            usage=usage,
            description=description,
            epilog=epilog,
            parents=parents,
            formatter_class=formatter_class or self._child_formatter_class,
            prefix_chars=prefix_chars,
            fromfile_prefix_chars=fromfile_prefix_chars,
            argument_default=argument_default,
            conflict_handler=conflict_handler,
            add_help=add_help,
            allow_abbrev=allow_abbrev,
            nest_path=nested_components,
            nest_separator=self.nest_separator,
            exit_on_error=exit_on_error,
            help_flags=self._child_help_flags,
            **kwargs,
        )
        return parser


[docs] class ArgumentParser(argparse.ArgumentParser): """ ArgumentParser with nested destinations and custom help formatting. Args: prog (str | None): Program name used in help output. usage (str | None): Custom usage string. description (str | None): Description text shown in help. epilog (str | None): Epilog text shown after help. parents (list[argparse.ArgumentParser] | None): Parent parsers to inherit args. formatter_class (type[argparse.HelpFormatter]): Help formatter class. prefix_chars (str): Prefix characters for options. fromfile_prefix_chars (str | None): Prefix for args-from-file. argument_default (Any): Default value for all arguments. conflict_handler (str): Conflict resolution strategy. add_help (bool): Whether to add a help option. allow_abbrev (bool): Whether to allow abbreviations of long options. nest_dir (str | None): Base nesting directory label. nest_separator (str): Separator for nested destinations. nest_path (list[str] | None): Explicit nesting path components. exit_on_error (bool): Whether to exit on parse errors. help_layout (Any | None): Layout configuration for help rendering. help_position (int | None): Absolute column where help descriptions begin. color (bool | None): Force colorized help output when supported. """ def __init__( self, prog: str | None = None, usage: str | None = None, description: str | None = None, epilog: str | None = None, parents: list[argparse.ArgumentParser] | None = None, formatter_class: type[argparse.HelpFormatter] = InterfacyHelpFormatter, prefix_chars: str = "-", fromfile_prefix_chars: str | None = None, argument_default: Any = None, conflict_handler: str = "error", add_help: bool = True, allow_abbrev: bool = True, nest_dir: str | None = None, nest_separator: str = "__", nest_path: list[str] | None = None, exit_on_error: bool = True, *, help_layout: "HelpLayout | None" = None, help_position: int | None = None, help_flags: Sequence[str] = ("--help",), color: bool | None = None, ) -> None: if parents is None: parents = [] self.nest_path_components = nest_path or ([nest_dir] if nest_dir else []) self.nest_dir = self.nest_path_components[-1] if self.nest_path_components else None self.nest_separator = nest_separator self._original_destinations: dict[str, str] = {} # nested_dest: original_dest base_init_kwargs: dict[str, Any] = { "prog": prog, "usage": usage, "description": description, "epilog": epilog, "parents": parents, "formatter_class": formatter_class, "prefix_chars": prefix_chars, "fromfile_prefix_chars": fromfile_prefix_chars, "argument_default": argument_default, "conflict_handler": conflict_handler, "add_help": False, "exit_on_error": exit_on_error, "allow_abbrev": allow_abbrev, } if color is None and sys.version_info >= (3, 14): color = False if color is not None: base_init_kwargs["color"] = color try: super().__init__(**base_init_kwargs) except TypeError as exc: if "color" not in base_init_kwargs or "color" not in str(exc): raise base_init_kwargs.pop("color") super().__init__(**base_init_kwargs) self._interfacy_help_layout = ( deepcopy(help_layout) if help_layout is not None else StandardLayout() ) if help_position is not None: self._interfacy_help_layout.help_position = help_position self._schema_command: Command | None = None self._schema: ParserSchema | None = None self._interfacy_raise_parse_errors = False self.add_help = add_help self.help_flags = tuple(help_flags) if add_help: if "-" in self.prefix_chars: help_flags_to_add = [flag for flag in self.help_flags if flag.startswith("-")] or [ "--help" ] else: default_prefix = self.prefix_chars[0] help_flags_to_add = [default_prefix * 2 + "help"] self.add_argument( *help_flags_to_add, action="help", default=argparse.SUPPRESS, help=argparse._("Show this help message and exit"), ) self.register("action", "parsers", NestedSubParsersAction) def format_help(self) -> str: """Render help text using schema-aware layout rendering when available.""" layout = self._interfacy_help_layout if layout is None: return super().format_help() has_grouped_help = False if self._schema is not None: has_grouped_help = has_grouped_commands(self._schema.commands) elif self._schema_command is not None: has_grouped_help = command_has_grouped_subcommands(self._schema_command) if not _uses_template_layout(layout) and not has_grouped_help: return super().format_help() renderer = SchemaHelpRenderer(layout, help_argument=self._get_help_argument_for_schema()) if self._schema is not None: return renderer.render_parser_help(self._schema, self.prog) if self._schema_command is not None: return renderer.render_command_help(self._schema_command, self.prog) if _uses_template_layout(layout): return renderer.render_command_help(self._build_implicit_schema_command(), self.prog) return super().format_help() def set_schema_command(self, command: "Command | None") -> None: """ Store the active command schema for schema-aware help rendering. Args: command (Command | None): Command schema tied to this parser. """ self._schema_command = command def set_schema(self, schema: "ParserSchema | None") -> None: """ Store the parser schema for schema-aware help rendering. Args: schema (ParserSchema | None): Full parser schema tied to this parser. """ self._schema = schema def add_subparsers(self, **kwargs: Any) -> NestedSubParsersAction: """ Create a nested subparser group with remapped destinations. Args: **kwargs (Any): Arguments forwarded to argparse add_subparsers. """ logger.info("Adding subparsers with kwarg keys=%s", sorted(kwargs)) if DEST_KEY in kwargs: dest = kwargs[DEST_KEY] nested_dest = self._get_nested_destination(dest.replace("-", "_"), store=True) kwargs[DEST_KEY] = nested_dest kwargs.update( { "base_nest_path": self.nest_path_components, "nest_separator": self.nest_separator, "formatter_class": self.formatter_class, "help_layout": self._interfacy_help_layout, "help_flags": self.help_flags, } ) action = super().add_subparsers(**kwargs) if not isinstance(action, NestedSubParsersAction): raise TypeError("Nested subparser factory returned an unexpected action type") return action def parse_known_args( # type: ignore[override] self, args: Sequence[str] | None = None, namespace: Namespace | None = None, ) -> tuple[Namespace, list[str]]: """ Parse known args and deflatten nested destinations. Args: args (Sequence[str] | None): Argument list to parse. Defaults to sys.argv. namespace (Namespace | None): Optional namespace to populate. """ parsed_args, unknown_args = super().parse_known_args(args=args, namespace=namespace) logger.info( "Initial parse keys: %s, unknown count=%d", sorted(vars(parsed_args)), len(unknown_args), ) if parsed_args is None: raise ValueError("No parsed arguments found.") deflattened_args = self._deflatten_namespace(parsed_args) logger.info("Deflattened keys: %s", sorted(vars(deflattened_args))) return deflattened_args, unknown_args def set_defaults(self, **kwargs: Any) -> None: """ Set defaults while respecting nested destinations. Args: **kwargs (Any): Default values keyed by original destination names. """ nested_kwargs = { self._get_nested_destination(dest, store=True): value for dest, value in kwargs.items() } logger.info("Nested default keys: %s", sorted(nested_kwargs)) super().set_defaults(**nested_kwargs) def get_default(self, dest: str) -> Any: """ Return the default value for a destination name. Args: dest (str): Original destination name. """ nested_dest = self._get_nested_destination(dest) value = super().get_default(nested_dest) return value def error(self, message: str) -> Never: """ Override argparse's default error output for missing required subcommands. By default, argparse prints only a short usage line on errors. For CLIs built around subcommands, a missing subcommand is much more useful when the full help is displayed. """ if self._interfacy_raise_parse_errors: raise ArgparseParseError(message) marker = "the following arguments are required:" if marker in message: subparser_actions = [ action for action in self._actions if isinstance(action, argparse._SubParsersAction) ] subparser_dests: set[str] = {action.dest for action in subparser_actions} subparser_choices: set[str] = set() for action in subparser_actions: subparser_choices.update(action.choices.keys()) if subparser_dests: missing_part = message.split(marker, 1)[1].strip() missing_names = [name.strip() for name in missing_part.split(",") if name.strip()] denested_missing = [ self._original_destinations.get(name, name) for name in missing_names ] denested_subparser_dests = { self._original_destinations.get(dest, dest) for dest in subparser_dests } brace_choices: set[str] = set() for grouped in re.findall(r"\{([^}]*)\}", missing_part): brace_choices.update(choice.strip() for choice in grouped.split(",") if choice) is_missing_subcommand = any( name in denested_subparser_dests for name in denested_missing ) or bool(brace_choices & subparser_choices) if is_missing_subcommand: self.print_help(sys.stderr) raise SystemExit(2) super().error(message) def _get_formatter(self) -> argparse.HelpFormatter: # type: ignore[override] formatter = self.formatter_class(str(self.prog)) set_help_layout = getattr(formatter, "set_help_layout", None) if callable(set_help_layout): try: set_help_layout(self._interfacy_help_layout) except TypeError: logger.debug("Formatter rejected help layout", exc_info=True) return formatter def _get_help_argument_for_schema(self) -> Argument | None: for action in self._actions: if isinstance(action, argparse._HelpAction): # type: ignore[private-member-access] return _action_to_help_argument(action) return None def _build_implicit_schema_command(self) -> Command: layout = self._interfacy_help_layout if layout is None: raise ValueError("Cannot synthesize a schema command without a help layout.") parameters: list[Argument] = [] subcommands: dict[str, Command] | None = None for action in self._actions: if isinstance(action, argparse._HelpAction): # type: ignore[private-member-access] continue if isinstance(action, argparse._SubParsersAction): # type: ignore[private-member-access] subcommands = self._subcommands_from_action(action) continue parameters.append(self._argument_from_action(action)) command_name = self._command_name_for_schema() return Command( obj=None, canonical_name=command_name, cli_name=command_name, aliases=(), raw_description=self.description, help_layout=layout, parameters=parameters, subcommands=subcommands, raw_epilog=self.epilog, is_leaf=not bool(subcommands), ) def _command_name_for_schema(self) -> str: prog = str(self.prog or "command").strip() if not prog: return "command" return prog.split()[-1] def _original_dest_name(self, dest: str) -> str: original = self._original_destinations.get(dest) if original is not None: return original if self.nest_separator in dest: return dest.rsplit(self.nest_separator, maxsplit=1)[-1] return dest def _display_name_for_dest(self, dest: str) -> str: return self._original_dest_name(dest).replace("_", "-") def _subcommands_from_action( self, action: argparse._SubParsersAction, # type: ignore[private-member-access] ) -> dict[str, Command] | None: layout = self._interfacy_help_layout if layout is None: return None parser_names: dict[int, list[str]] = {} for name, parser in action.choices.items(): parser_names.setdefault(id(parser), []).append(name) commands: dict[str, Command] = {} for choice_action in getattr(action, "_choices_actions", ()): choice_name = getattr(choice_action, "dest", None) if not isinstance(choice_name, str): continue parser = action.choices.get(choice_name) if not isinstance(parser, ArgumentParser): continue aliases = tuple( name for name in parser_names.get(id(parser), ()) if name != choice_name ) raw_description = ( choice_action.help if isinstance(choice_action.help, str) and choice_action.help != argparse.SUPPRESS else parser.description ) commands[choice_name] = Command( obj=None, canonical_name=choice_name, cli_name=choice_name, aliases=aliases, raw_description=raw_description, help_layout=parser._interfacy_help_layout or layout, is_leaf=not parser._has_subcommands_action(), ) return commands or None def _has_subcommands_action(self) -> bool: return any( isinstance(action, argparse._SubParsersAction) # type: ignore[private-member-access] for action in self._actions ) def _argument_from_action(self, action: argparse.Action) -> Argument: value_shape = _action_value_shape(action) dest_name = self._original_dest_name(action.dest) display_name = self._display_name_for_dest(action.dest) kind = ArgumentKind.OPTION if action.option_strings else ArgumentKind.POSITIONAL arg_type = action.type if isinstance(action.type, type) else None parser = action.type if callable(action.type) else None if arg_type is None and value_shape != ValueShape.FLAG: arg_type = str boolean_behavior: BooleanBehavior | None = None if value_shape == ValueShape.FLAG and action.option_strings: negative_form = next( (flag for flag in action.option_strings if flag.startswith("--no-")), None, ) boolean_behavior = BooleanBehavior( supports_negative=negative_form is not None, negative_form=negative_form, default=action.default, ) choices = tuple(action.choices) if action.choices is not None else None nargs = action.nargs if isinstance(action.nargs, (str, int)) else None return Argument( name=dest_name, display_name=display_name, kind=kind, value_shape=value_shape, flags=tuple(action.option_strings) if action.option_strings else (display_name,), required=bool(getattr(action, "required", False)), default=action.default, help=action.help if isinstance(action.help, str) else None, type=arg_type, parser=parser, metavar=_action_metavar(action), nargs=nargs, boolean_behavior=boolean_behavior, choices=choices, ) def _add_container_actions(self, container: argparse._ActionsContainer) -> None: self._remap_container_destinations(container) return super()._add_container_actions(container) def _get_positional_kwargs(self, dest: str, **kwargs: Any) -> dict[str, Any]: logger.debug("Getting positional kwargs for dest='%s'", dest) nested_dest = self._get_nested_destination(dest.replace("-", "_"), store=True) kwargs = self._edit_arguments(dest, **kwargs) return super()._get_positional_kwargs(nested_dest, **kwargs) def _get_optional_kwargs(self, *args: str, **kwargs: Any) -> dict[str, Any]: logger.debug("Getting optional kwargs for args=%s", args) dest = self._extract_destination(*args, **kwargs) nested_dest = self._get_nested_destination(dest.replace("-", "_"), store=True) kwargs[DEST_KEY] = nested_dest kwargs = self._edit_arguments(dest, **kwargs) return super()._get_optional_kwargs(*args, **kwargs) def _deflatten_namespace(self, namespace: Namespace) -> Namespace: root = Namespace() for key, value in vars(namespace).items(): components = key.split(self.nest_separator) current = root # Navigate through component hierarchy for component in components[:-1]: if not hasattr(current, component): logger.debug("Creating new namespace for '%s'", component) setattr(current, component, Namespace()) current = getattr(current, component) # Set or merge final value final_component = components[-1] if hasattr(current, final_component): logger.debug("Merging nested namespaces at %s", final_component) existing_value = getattr(current, final_component) if isinstance(existing_value, Namespace) and isinstance(value, Namespace): self._recursively_merge_namespaces(existing_value, value) else: raise ValueError(f'Cannot merge namespaces due to conflict at key "{key}"') else: setattr(current, final_component, value) return root def _recursively_merge_namespaces(self, destination: Namespace, source: Namespace) -> Namespace: for name, value in vars(source).items(): if hasattr(destination, name): dest_value = getattr(destination, name) if isinstance(dest_value, Namespace) and isinstance(value, Namespace): logger.info("Recursively merging at attribute: %s", name) self._recursively_merge_namespaces(dest_value, value) else: raise ValueError( f'Cannot merge namespaces due to conflict at attribute "{name}".' ) else: logger.info("Setting new attribute: %s", name) setattr(destination, name, value) return destination @staticmethod def _container_defaults(container: argparse._ActionsContainer) -> dict[str, Any]: defaults = getattr(container, "_defaults", None) if isinstance(defaults, dict): return defaults return {} @staticmethod def _set_container_defaults( container: argparse._ActionsContainer, defaults: dict[str, Any] ) -> None: container._defaults = defaults @staticmethod def _iter_container_actions(container: argparse._ActionsContainer) -> list[argparse.Action]: actions = getattr(container, "_actions", ()) if not isinstance(actions, list): actions = list(actions) return [action for action in actions if isinstance(action, argparse.Action)] def _remap_container_destinations(self, container: argparse._ActionsContainer) -> None: defaults = self._container_defaults(container) logger.info("Remapping container destination keys: %s", sorted(defaults)) remapped_defaults = { self._get_nested_destination(dest): value for dest, value in defaults.items() } self._set_container_defaults(container, remapped_defaults) logger.info("Remapped container destination keys: %s", sorted(remapped_defaults)) for action in self._iter_container_actions(container): self._remap_action_destinations(action) def _remap_action_destinations(self, action: argparse.Action) -> None: logger.info("Remapping action dest: %s", action.dest) if action.dest is not None: old_dest = action.dest action.dest = self._get_nested_destination(action.dest, store=True) logger.info("Remapped action dest from %s to %s", old_dest, action.dest) if isinstance(action, NestedSubParsersAction) and action.choices is not None: for subparser in action.choices.values(): if isinstance(subparser, ArgumentParser): self._remap_container_destinations(subparser) def _extract_destination(self, *args: str, **kwargs: Any) -> str: if DEST_KEY in kwargs and kwargs[DEST_KEY] is not None: return str(kwargs[DEST_KEY]) # Find first long option string, falling back to first short option option_strings = ((s, len(s) > 2) for s in args if s[0] in self.prefix_chars) for option_string, is_long in option_strings: if is_long and option_string[1] in self.prefix_chars: logger.debug("Using long option string for dest: %s", option_string) return option_string.lstrip(self.prefix_chars) # If no long option found, use first short option dest = next(s.lstrip(self.prefix_chars) for s in args if s[0] in self.prefix_chars) logger.debug("Using short option string for dest: %s", dest) return dest def _get_nested_destination(self, dest: str, *, store: bool = False) -> str: if not self.nest_path_components: return dest nested = f"{self.nest_separator.join(self.nest_path_components)}{self.nest_separator}{dest}" logger.info("Generated nested dest: %s -> %s", nested, dest) if store: self._original_destinations[nested] = dest return nested def _edit_arguments(self, original_dest: str, **kwargs: Any) -> dict[str, Any]: _set_callable_type_name(kwargs.get("type")) action = kwargs.get("action", "store") action_name = action if isinstance(action, str) else getattr(action, "__name__", "") no_metavar_actions = { "store_true", "store_false", "store_const", "append_const", "count", "help", "version", "BooleanOptionalAction", } if action_name not in no_metavar_actions and "metavar" not in kwargs: dest_for_metavar = original_dest if self.nest_separator in dest_for_metavar: dest_for_metavar = dest_for_metavar.split(self.nest_separator)[-1] kwargs["metavar"] = dest_for_metavar.replace("_", "-").upper() return kwargs def _get_value(self, action: argparse.Action, arg_string: str) -> Any: parse_func = self._registry_get("type", action.type, action.type) if not callable(parse_func): raise argparse.ArgumentError(action, f"{parse_func!r} is not callable") try: result = parse_func(arg_string) except argparse.ArgumentTypeError as exc: raise argparse.ArgumentError(action, str(sys.exc_info()[1])) from exc except (TypeError, ValueError) as exc: t_name = _callable_type_name(parse_func, fallback="value") raise argparse.ArgumentError(action, f"invalid {t_name} value: '{arg_string}'") from exc return result
__all__ = [ "DEST_KEY", "ActionType", "ArgparseParseError", "ArgumentParser", "NargsPattern", "NestedSubParsersAction", "namespace_to_dict", ]