Source code for argclass._utils

"""Utility functions for argclass."""

import configparser
import os
import types
from pathlib import Path
from typing import (
    Any,
    Dict,
    Mapping,
    Optional,
    Tuple,
    Type,
    Union,
    get_args,
    get_origin,
)

from ._types import (
    CONTAINER_TYPES,
    TEXT_TRUE_VALUES,
    NoneType,
    UnionClass,
)


[docs] def read_configs( *paths: Union[str, Path], **kwargs: Any, ) -> Tuple[Mapping[str, Any], Tuple[Path, ...]]: """Read configuration from INI files.""" kwargs.setdefault("allow_no_value", True) kwargs.setdefault("strict", False) parser = configparser.ConfigParser(**kwargs) filenames = [] for path in paths: path_obj = Path(path).expanduser().resolve() # check the access first, because the parent # directory may not be readable if not os.access(path_obj, os.R_OK) or not path_obj.exists(): continue filenames.append(path_obj) config_paths = parser.read(filenames) result: Dict[str, Union[str, Dict[str, str]]] = dict( parser.items(parser.default_section, raw=True), ) for section in parser.sections(): config = dict(parser.items(section, raw=True)) result[section] = config return result, tuple(map(Path, config_paths))
def deep_getattr(name: str, attrs: Dict[str, Any], *bases: Type) -> Any: """Get attribute from attrs dict or base classes.""" if name in attrs: return attrs[name] for base in bases: if hasattr(base, name): return getattr(base, name) raise KeyError(f"Key {name} was not declared") def merge_annotations( annotations: Dict[str, Any], *bases: Type, ) -> Dict[str, Any]: """Merge annotations from base classes following MRO.""" result: Dict[str, Any] = {} # Walk the full MRO to collect all inherited annotations for base in bases: for cls in reversed(base.__mro__): result.update(getattr(cls, "__annotations__", {})) result.update(annotations) return result
[docs] def parse_bool(value: str) -> bool: """Parse a string to boolean.""" return value.lower() in TEXT_TRUE_VALUES
def _is_union_type(typespec: Any) -> bool: """Check if typespec is a Union type (typing.Union or PEP 604).""" if typespec.__class__ == UnionClass: return True # PEP 604: float | None creates types.UnionType in Python 3.10+ if hasattr(types, "UnionType") and isinstance(typespec, types.UnionType): return True return False def unwrap_optional(typespec: Any) -> Optional[Any]: """Unwrap Optional[T] to T, return None if not Optional.""" if not _is_union_type(typespec): return None union_args = [a for a in typespec.__args__ if a is not NoneType] if len(union_args) != 1: raise TypeError( "Complex types mustn't be used in short form. You have to " "specify argclass.Argument with converter or type function.", ) return union_args[0] def _is_container_type(typespec: Any) -> bool: """Check if typespec is a container type like list[str], List[str], etc.""" origin = get_origin(typespec) if origin is None: return False # Handle typing.List, typing.Set, etc. and built-in list[str], set[int] return origin in CONTAINER_TYPES def _unwrap_container_type(typespec: Any) -> Optional[Tuple[type, type]]: """ Unwrap a container type and return (container_origin, element_type). For list[str] or List[str], returns (list, str). For set[int] or Set[int], returns (set, int). Returns None if not a container type. """ if not _is_container_type(typespec): return None origin = get_origin(typespec) args = get_args(typespec) if not args: # list without type parameter - use str as default return (origin, str) # type: ignore # For tuple, we handle specially - just use the first type for now # (full tuple handling would need nargs=N for Tuple[int, str, bool]) element_type = args[0] # Handle nested optionals like list[str | None] optional_inner = unwrap_optional(element_type) if optional_inner is not None: element_type = optional_inner return (origin, element_type) # type: ignore