Source code for argclass.store

"""Store metaclass and argument classes for argclass."""

import collections
from argparse import Action
from pathlib import Path
from typing import Any, Dict, Iterable, Iterator, Optional, Tuple, Type, Union

from .actions import (
    ConfigAction,
    INIConfigAction,
    JSONConfigAction,
    TOMLConfigAction,
)
from .types import Actions, ConverterType, Nargs
from .utils import merge_annotations


class StoreMeta(type):
    """Metaclass that collects and merges annotations from base classes."""

    def __new__(
        mcs,
        name: str,
        bases: Tuple[Type["StoreMeta"], ...],
        attrs: Dict[str, Any],
    ) -> "StoreMeta":
        # Create the class first to ensure annotations are available
        # Python 3.14+ (PEP 649) defers annotation evaluation
        cls = super().__new__(mcs, name, bases, attrs)

        annotations = merge_annotations(
            getattr(cls, "__annotations__", {}),
            *bases,
        )
        setattr(cls, "__annotations__", annotations)
        setattr(
            cls,
            "_fields",
            tuple(
                filter(
                    lambda x: not x.startswith("_"),
                    annotations.keys(),
                ),
            ),
        )
        return cls


[docs] class Store(metaclass=StoreMeta): """Base class for typed storage with field validation.""" _default_value = object() _fields: Tuple[str, ...] def __new__(cls, **kwargs: Any) -> "Store": obj = super().__new__(cls) type_map: Dict[str, Tuple[Type, Any]] = {} # Use cls.__annotations__ instead of obj.__annotations__ to avoid # triggering __getattr__ before _values is initialized (Python 3.14+) for key, value in cls.__annotations__.items(): if key.startswith("_"): continue type_map[key] = (value, getattr(cls, key, cls._default_value)) for key, (value_type, default) in type_map.items(): if default is cls._default_value and key not in kwargs: raise TypeError(f"required argument {key!r} must be passed") value = kwargs.get(key, default) setattr(obj, key, value) return obj
[docs] def copy(self, **overrides: Any) -> Any: kwargs = self.as_dict() for key, value in overrides.items(): kwargs[key] = value return self.__class__(**kwargs)
[docs] def as_dict(self) -> Dict[str, Any]: # noinspection PyProtectedMember return {field: getattr(self, field) for field in self._fields}
def __repr__(self) -> str: items = sorted(self.as_dict().items()) values = ", ".join([f"{k!s}={v!r}" for k, v in items]) return f"<{self.__class__.__name__}: {values}>"
class ArgumentBase(Store): """Base class for argument definitions.""" def __init__(self, **kwargs: Any): self._values = collections.OrderedDict() # noinspection PyUnresolvedReferences for key in self._fields: self._values[key] = kwargs.get(key, getattr(self.__class__, key)) def __getattr__(self, item: str) -> Any: try: return self._values[item] except KeyError as e: raise AttributeError from e @property def is_positional(self) -> bool: for alias in self.aliases: if alias.startswith("-"): return False return True def get_kwargs(self) -> Dict[str, Any]: nargs = self.nargs if isinstance(nargs, Nargs): nargs = nargs.value action = self.action kwargs = self.as_dict() if action in (Actions.STORE_TRUE, Actions.STORE_FALSE, Actions.COUNT): kwargs.pop("type", None) if isinstance(action, Actions): action = action.value kwargs.pop("aliases", None) kwargs.pop("converter", None) kwargs.pop("env_var", None) kwargs.pop("secret", None) kwargs.update(action=action, nargs=nargs) return {k: v for k, v in kwargs.items() if v is not None}
[docs] class TypedArgument(ArgumentBase): """Argument with type information.""" action: Union[Actions, Type[Action]] = Actions.default() aliases: Iterable[str] = frozenset() choices: Optional[Iterable[str]] = None const: Optional[Any] = None converter: Optional[ConverterType] = None default: Optional[Any] = None secret: bool = False env_var: Optional[str] = None help: Optional[str] = None metavar: Optional[str] = None nargs: Optional[Union[int, Nargs]] = None required: Optional[bool] = None type: Any = None @property def is_nargs(self) -> bool: if self.nargs is None: return False if isinstance(self.nargs, int): return self.nargs > 1 return True @property def has_default(self) -> bool: """Check if the argument has a meaningful default value. Returns False if default is None or Ellipsis (the "no default" sentinel). """ return self.default is not None and self.default is not ...
[docs] class ConfigArgument(TypedArgument): """Argument for configuration file loading.""" search_paths: Optional[Iterable[Union[Path, str]]] = None action: Type[ConfigAction]
[docs] class INIConfig(ConfigArgument): """Parse INI file and set results as a value.""" action: Type[ConfigAction] = INIConfigAction
[docs] class JSONConfig(ConfigArgument): """Parse JSON file and set results as a value.""" action: Type[ConfigAction] = JSONConfigAction
[docs] class TOMLConfig(ConfigArgument): """Parse TOML file and set results as a value. Uses stdlib tomllib (Python 3.11+) or tomli package as fallback. """ action: Type[ConfigAction] = TOMLConfigAction
class AbstractGroup: """Abstract base for argument groups.""" pass class AbstractParser: """Abstract base for parsers.""" __parent__: Union["AbstractParser", None] = None current_subparsers = () # type: Tuple["AbstractParser", ...] def _get_chain(self) -> Iterator["AbstractParser"]: yield self if self.__parent__ is None: return yield from self.__parent__._get_chain() def __call__(self) -> Any: raise NotImplementedError()