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
[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()