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