Source code for argclass.factory

"""Factory functions for creating arguments."""

from enum import Enum
from pathlib import Path
from types import MappingProxyType
from typing import (
    Any,
    Literal,
    TypeVar,
    cast,
    overload,
)
from collections.abc import Callable, Iterable

from argparse import Action

from .exceptions import EnumValueError
from .store import ConfigArgument, INIConfig, TypedArgument
from .types import (
    Actions,
    ConverterType,
    LogLevelEnum,
    MetavarType,
    Nargs,
    NargsType,
)
import builtins

T = TypeVar("T")


# noinspection PyShadowingBuiltins
[docs] def ArgumentSingle( *aliases: str, type: type[T], action: Actions | type[Action] = Actions.default(), choices: Iterable[str] | None = None, const: Any | None = None, converter: Callable[[T], T] | None = None, default: T | None = None, env_var: str | None = None, help: str | None = None, metavar: MetavarType | None = None, required: bool | None = None, secret: bool = False, **extra_kwargs: Any, ) -> T: """ Create a single-value argument with precise typing. Use this when you need exact type inference for a single value. The `type` parameter is required and determines the return type. Any extra keyword arguments are passed through to ``argparse.ArgumentParser.add_argument`` (e.g., ``version='1.0'`` for ``action=Actions.VERSION``). Example: class Parser(argclass.Parser): count: int = argclass.ArgumentSingle(type=int, default=10) name: str = argclass.ArgumentSingle(type=str) """ return cast( T, TypedArgument( action=action, aliases=aliases, choices=choices, const=const, converter=converter, default=default, secret=secret, env_var=env_var, extra_kwargs=MappingProxyType(dict(extra_kwargs or {})), help=help, metavar=metavar, nargs=None, required=required, type=type, ), )
# noinspection PyShadowingBuiltins
[docs] def ArgumentSequence( *aliases: str, type: type[T], nargs: NargsType = Nargs.ONE_OR_MORE, action: Actions | type[Action] = Actions.default(), choices: Iterable[str] | None = None, const: Any | None = None, converter: Callable[[list[T]], Any] | None = None, default: list[T] | None = None, env_var: str | None = None, help: str | None = None, metavar: MetavarType | None = None, required: bool | None = None, secret: bool = False, **extra_kwargs: Any, ) -> list[T]: """ Create a multi-value argument with precise typing. Use this when you need exact type inference for a list of values. The ``type`` parameter is required and determines the element type. Any extra keyword arguments are passed through to ``argparse.ArgumentParser.add_argument``. Args: nargs: Number of values. Defaults to "+" (one or more). Use "*" for zero or more, or an int for exact count. Example:: class Parser(argclass.Parser): files: list[str] = argclass.ArgumentSequence(type=str) numbers: list[int] = argclass.ArgumentSequence( type=int, nargs="*", default=[] ) """ return cast( list[T], TypedArgument( action=action, aliases=aliases, choices=choices, const=const, converter=converter, default=default, secret=secret, env_var=env_var, extra_kwargs=MappingProxyType(dict(extra_kwargs or {})), help=help, metavar=metavar, nargs=nargs, required=required, type=type, ), )
# Overload: type + nargs + converter → converter's return type (must be first!) R = TypeVar("R") @overload def Argument( *aliases: str, type: ConverterType, nargs: NargsType, converter: Callable[..., R], action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> R: ... # Overload: Type[T] + nargs="?" (optional single) → T @overload def Argument( *aliases: str, type: type[T], nargs: Literal["?"], action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> T: ... # Overload: Callable type + nargs="?" (optional single) → T @overload def Argument( *aliases: str, type: Callable[[str], T], nargs: Literal["?"], action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> T: ... # Overload: Type[T] + nargs (sequence) → List[T] @overload def Argument( *aliases: str, type: type[T], nargs: Literal["*", "+"] | int | Nargs, action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> list[T]: ... # Overload: Callable type + nargs (sequence) → List[T] @overload def Argument( *aliases: str, type: Callable[[str], T], nargs: Literal["*", "+"] | int | Nargs, action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> list[T]: ... # Overload: Type[T] without nargs (single) → T @overload def Argument( *aliases: str, type: type[T], action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: None = ..., default: T | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., nargs: None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> T: ... # Overload: Callable type without nargs (single) → T @overload def Argument( *aliases: str, type: Callable[[str], T], action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: None = ..., default: T | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., nargs: None = ..., required: bool | None = ..., secret: bool = ..., **extra_kwargs: Any, ) -> T: ... # Overload: converter determines return type @overload def Argument( *aliases: str, converter: Callable[..., T], action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., nargs: NargsType | None = ..., required: bool | None = ..., secret: bool = ..., type: ConverterType | None = ..., **extra_kwargs: Any, ) -> T: ... # Overload: fallback for dynamic/optional parameters (used by Secret, etc.). # Returns a bare TypeVar so the result adopts the annotated attribute type # (e.g. ``tags: list[str] = Argument(nargs="+")`` infers ``list[str]``) # instead of ``Any``, which keeps strict checkers (basedpyright's reportAny) # quiet on simple, untyped declarations. @overload def Argument( *aliases: str, action: Actions | type[Action] = ..., choices: Iterable[str] | None = ..., const: Any | None = ..., converter: ConverterType | None = ..., default: Any | None = ..., env_var: str | None = ..., help: str | None = ..., metavar: MetavarType | None = ..., nargs: NargsType | None = ..., required: bool | None = ..., secret: bool = ..., type: ConverterType | None = ..., **extra_kwargs: Any, ) -> T: ... # type: ignore[type-var] # noinspection PyShadowingBuiltins
[docs] def Argument( *aliases: str, action: Actions | type[Action] = Actions.default(), choices: Iterable[str] | None = None, const: Any | None = None, converter: ConverterType | None = None, default: Any | None = None, env_var: str | None = None, help: str | None = None, metavar: MetavarType | None = None, nargs: NargsType | None = None, required: bool | None = None, secret: bool = False, type: ConverterType | None = None, **extra_kwargs: Any, ) -> Any: """ Create a typed argument for a Parser or Group class. Dispatches to ArgumentSingle or ArgumentSequence based on nargs. Args: aliases: Command-line aliases (e.g., "-n", "--name"). action: How to handle the argument (store, store_true, etc.). choices: Restrict values to these options. const: Constant value for store_const/append_const actions. converter: Post-parse transform function. With nargs, receives the list. default: Default value if argument not provided. env_var: Environment variable to read default from. help: Help text for --help output. metavar: Placeholder name in help text. nargs: Number of values (int, "?", "*", "+", or Nargs enum). required: Whether the argument must be provided. secret: If True, hide value from help and wrap str in SecretString. type: Per-value converter for argparse. With nargs, called per value. Returns: TypedArgument instance. Example:: # type: converts each CLI value individually numbers = Argument(nargs="+", type=int) # Parsing ["1", "2"] -> calls int("1"), int("2") -> [1, 2] # converter: transforms the final result after type conversion unique = Argument(nargs="+", type=int, converter=set) # Parsing ["1", "2", "1"] -> [1, 2, 1] -> set([1, 2, 1]) -> {1, 2} # Combining type and converter for set[int]: class Parser(argclass.Parser): numbers: set[int] = Argument( type=int, # Convert each "1", "2" to int converter=set, # Convert [1, 2, 1] to {1, 2} nargs="+", ) # Alternative: single converter function for set[int]: class Parser(argclass.Parser): numbers: set[int] = Argument( converter=lambda vals: set(map(int, vals)), nargs="+", ) """ # Dispatch to typed functions when type is provided if type is not None: # Cast type to Type[Any] since we've verified it's not None # The actual type could be Type[T] or Callable[[str], T] type_func = cast(builtins.type[Any], type) if nargs in ("+", "*") or isinstance(nargs, (int, Nargs)): return ArgumentSequence( *aliases, type=type_func, nargs=nargs, action=action, choices=choices, const=const, converter=cast(Callable[[list[Any]], Any] | None, converter), default=default, env_var=env_var, help=help, metavar=metavar, required=required, secret=secret, **extra_kwargs, ) elif nargs == "?" or nargs == Nargs.ZERO_OR_ONE: # nargs="?" needs special handling - creates TypedArgument directly return TypedArgument( action=action, aliases=aliases, choices=choices, const=const, converter=converter, default=default, env_var=env_var, extra_kwargs=MappingProxyType(dict(extra_kwargs or {})), help=help, metavar=metavar, nargs=nargs, required=required, secret=secret, type=type, ) else: return ArgumentSingle( *aliases, type=type_func, action=action, choices=choices, const=const, converter=converter, default=default, env_var=env_var, help=help, metavar=metavar, required=required, secret=secret, **extra_kwargs, ) # Fallback for untyped arguments return TypedArgument( action=action, aliases=aliases, choices=choices, const=const, converter=converter, default=default, secret=secret, env_var=env_var, extra_kwargs=MappingProxyType(dict(extra_kwargs or {})), help=help, metavar=metavar, nargs=nargs, required=required, type=type, )
E = TypeVar("E", bound=Enum) # Overload: with default value (enum member or string) -> returns EnumType @overload def EnumArgument( enum_class: type[E], *aliases: str, default: E | str, action: Actions | type[Action] = ..., env_var: str | None = ..., help: str | None = ..., metavar: str | None = ..., nargs: NargsType | None = ..., required: bool | None = ..., use_value: bool = ..., lowercase: bool = ..., ) -> E: ... # Overload: without default value -> returns Optional[EnumType] @overload def EnumArgument( enum_class: type[E], *aliases: str, action: Actions | type[Action] = ..., default: None = ..., env_var: str | None = ..., help: str | None = ..., metavar: str | None = ..., nargs: NargsType | None = ..., required: bool | None = ..., use_value: bool = ..., lowercase: bool = ..., ) -> E | None: ...
[docs] def EnumArgument( enum_class: type[E], *aliases: str, action: Actions | type[Action] = Actions.default(), default: E | str | None = None, env_var: str | None = None, help: str | None = None, metavar: str | None = None, nargs: NargsType | None = None, required: bool | None = None, use_value: bool = False, lowercase: bool = False, ) -> Any: """ Create an argument from an Enum class. Args: enum_class: The Enum class to use for choices and conversion. aliases: Command-line aliases (e.g., "-l", "--level"). action: How to handle the argument. default: Default value as enum member or string name. env_var: Environment variable to read default from. help: Help text for --help output. metavar: Placeholder name in help text. nargs: Number of values. required: Whether the argument must be provided. use_value: If True, return enum.value instead of enum member. lowercase: If True, use lowercase choices and accept lowercase input. Returns: TypedArgument instance. """ # Map accepted spellings to members explicitly instead of relying # on ``str.upper()`` round-trips, which break for enums whose # member names are not fully uppercase (``Fast`` -> ``FAST`` -> # KeyError). ``__members__`` includes aliases (e.g. ``WARN`` for # ``WARNING``) so they keep working as inputs; ``choices`` lists # canonical names only. members = enum_class.__members__ if lowercase: name_map = {n.lower(): m for n, m in members.items()} choices = tuple(e.name.lower() for e in enum_class) else: name_map = dict(members) choices = tuple(e.name for e in enum_class) if default is not None: if isinstance(default, enum_class): pass # Valid enum member elif isinstance(default, str): # Validate string is a valid enum member name member = members.get(default) if member is None and lowercase: member = name_map.get(default.lower()) if member is None: raise EnumValueError( f"default {default!r} is not a valid {enum_class.__name__} " f"member", enum_class=enum_class, valid_values=tuple(e.name for e in enum_class), ) default = member else: raise EnumValueError( f"default must be {enum_class.__name__} member or string, " f"got {type(default).__name__}", enum_class=enum_class, valid_values=tuple(e.name for e in enum_class), hint="Pass an enum member or its string name", ) def converter(x: Any) -> Any: # Handle None - return as-is for optional arguments if x is None: return None # Handle existing enum members if isinstance(x, enum_class): return x.value if use_value else x # Convert string to enum member via the explicit name map member = name_map.get(x.lower() if lowercase else x) if member is None: # Same failure mode as enum_class[x]; parse_args wraps # converter errors into TypeConversionError. raise KeyError(x) return member.value if use_value else member return TypedArgument( action=action, aliases=aliases, choices=choices, converter=converter, default=default, env_var=env_var, help=help, metavar=metavar, nargs=nargs, required=required, )
# noinspection PyShadowingBuiltins
[docs] def Secret( *aliases: str, action: Actions | type[Action] = Actions.default(), choices: Iterable[str] | None = None, const: Any | None = None, converter: ConverterType | None = None, default: Any | None = None, env_var: str | None = None, help: str | None = None, metavar: str | None = None, nargs: NargsType | None = None, required: bool | None = None, type: ConverterType | None = None, ) -> Any: """ Create a secret argument that masks sensitive values. Secret arguments are wrapped in SecretString, which: - Returns ``'******'`` for repr() to prevent accidental logging - Supports equality comparison without exposing the value - Use str() to access the actual value when needed Use ``parser.sanitize_env()`` after parsing to remove secret environment variables before spawning subprocesses. Args: aliases: Command-line aliases (e.g., "-p", "--password"). env_var: Environment variable to read the secret from. default: Default value if not provided. help: Help text (the actual value is never shown). Returns: TypedArgument with secret=True. Example:: class Parser(argclass.Parser): api_key: str = argclass.Secret(env_var="API_KEY") password: str = argclass.Secret() parser = Parser() parser.parse_args() parser.sanitize_env() # Remove secrets from environment # Safe: shows '******' print(f"API key: {parser.api_key!r}") # Access actual value connect(api_key=str(parser.api_key)) """ return Argument( *aliases, action=action, choices=choices, const=const, converter=converter, default=default, env_var=env_var, help=help, metavar=metavar, nargs=nargs, required=required, secret=True, type=type, )
# noinspection PyShadowingBuiltins
[docs] def Config( *aliases: str, search_paths: Iterable[Path | str] | None = None, choices: Iterable[str] | None = None, converter: ConverterType | None = None, const: Any | None = None, default: Any | None = None, env_var: str | None = None, help: str | None = None, metavar: str | None = None, nargs: NargsType | None = None, required: bool | None = None, config_class: type[ConfigArgument] = INIConfig, ) -> Any: """ Create a configuration file argument. This creates a ``--config`` argument that loads structured data from a file. The loaded data is accessible as a dict-like object. Note: This is different from ``config_files`` parameter on Parser, which presets CLI argument defaults. This argument loads arbitrary configuration data for your application. Args: aliases: Command-line aliases (default: "--config"). search_paths: Default paths to search for config files. config_class: Parser class (INIConfig, JSONConfig, TOMLConfig). env_var: Environment variable for config file path. help: Help text for --help output. Returns: ConfigArgument instance. Example:: class Parser(argclass.Parser): config = argclass.Config(config_class=argclass.JSONConfig) parser = Parser() parser.parse_args(["--config", "settings.json"]) # Access loaded configuration db_host = parser.config["database"]["host"] """ return config_class( search_paths=search_paths, aliases=aliases, choices=choices, converter=converter, const=const, default=default, env_var=env_var, help=help, metavar=metavar, nargs=nargs, required=required, )
# Pre-built log level argument LogLevel = EnumArgument( LogLevelEnum, use_value=True, lowercase=True, default=LogLevelEnum.INFO, )