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",
]