add updates libs
This commit is contained in:
@@ -29,7 +29,6 @@ from typing import (
|
||||
_AnnotatedAlias,
|
||||
_GenericAlias,
|
||||
_SpecialGenericAlias,
|
||||
_UnionGenericAlias,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
@@ -209,10 +208,7 @@ def get_final_base(type) -> Optional[type]:
|
||||
OriginAbstractSet = AbcSet
|
||||
OriginMutableSet = AbcMutableSet
|
||||
|
||||
signature = _signature
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
signature = partial(_signature, eval_str=True)
|
||||
signature = partial(_signature, eval_str=True)
|
||||
|
||||
|
||||
try:
|
||||
@@ -256,10 +252,25 @@ def is_tuple(type):
|
||||
)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
if sys.version_info >= (3, 14):
|
||||
|
||||
def is_union_type(obj):
|
||||
from types import UnionType
|
||||
from types import UnionType # noqa: PLC0415
|
||||
|
||||
return obj is Union or isinstance(obj, UnionType)
|
||||
|
||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
||||
if typ is NewType or isinstance(typ, NewType):
|
||||
return typ.__supertype__
|
||||
return None
|
||||
|
||||
from typing import NotRequired, Required
|
||||
|
||||
else:
|
||||
from typing import _UnionGenericAlias
|
||||
|
||||
def is_union_type(obj):
|
||||
from types import UnionType # noqa: PLC0415
|
||||
|
||||
return (
|
||||
obj is Union
|
||||
@@ -277,25 +288,6 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
else:
|
||||
# 3.9
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
def is_union_type(obj):
|
||||
return obj is Union or (
|
||||
isinstance(obj, _UnionGenericAlias) and obj.__origin__ is Union
|
||||
)
|
||||
|
||||
def get_newtype_base(typ: Any) -> Optional[type]:
|
||||
supertype = getattr(typ, "__supertype__", None)
|
||||
if (
|
||||
supertype is not None
|
||||
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
|
||||
and typ.__module__ in ("typing", "typing_extensions")
|
||||
):
|
||||
return supertype
|
||||
return None
|
||||
|
||||
|
||||
def get_notrequired_base(type) -> Union[Any, NothingType]:
|
||||
if is_annotated(type):
|
||||
@@ -306,6 +298,25 @@ def get_notrequired_base(type) -> Union[Any, NothingType]:
|
||||
return NOTHING
|
||||
|
||||
|
||||
def is_mutable_sequence(type: Any) -> bool:
|
||||
"""A predicate function for mutable sequences.
|
||||
|
||||
Matches lists, mutable sequences, and deques.
|
||||
"""
|
||||
origin = getattr(type, "__origin__", None)
|
||||
return (
|
||||
type in (List, list, TypingMutableSequence, AbcMutableSequence, deque, Deque)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and (
|
||||
((origin is not tuple) and is_subclass(origin, TypingMutableSequence))
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
)
|
||||
or (origin in (list, deque, AbcMutableSequence))
|
||||
)
|
||||
|
||||
|
||||
def is_sequence(type: Any) -> bool:
|
||||
"""A predicate function for sequences.
|
||||
|
||||
@@ -313,19 +324,8 @@ def is_sequence(type: Any) -> bool:
|
||||
tuples.
|
||||
"""
|
||||
origin = getattr(type, "__origin__", None)
|
||||
return (
|
||||
type
|
||||
in (
|
||||
List,
|
||||
list,
|
||||
TypingSequence,
|
||||
TypingMutableSequence,
|
||||
AbcMutableSequence,
|
||||
tuple,
|
||||
Tuple,
|
||||
deque,
|
||||
Deque,
|
||||
)
|
||||
return is_mutable_sequence(type) or (
|
||||
type in (TypingSequence, tuple, Tuple)
|
||||
or (
|
||||
type.__class__ is _GenericAlias
|
||||
and (
|
||||
@@ -333,7 +333,7 @@ def is_sequence(type: Any) -> bool:
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
)
|
||||
or (origin in (list, deque, AbcMutableSequence, AbcSequence))
|
||||
or (origin is AbcSequence)
|
||||
or (origin is tuple and type.__args__[1] is ...)
|
||||
)
|
||||
|
||||
@@ -403,8 +403,10 @@ def is_generic(type) -> bool:
|
||||
"""Whether `type` is a generic type."""
|
||||
# Inheriting from protocol will inject `Generic` into the MRO
|
||||
# without `__orig_bases__`.
|
||||
return isinstance(type, (_GenericAlias, GenericAlias)) or (
|
||||
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
|
||||
return (
|
||||
isinstance(type, (_GenericAlias, GenericAlias))
|
||||
or (is_subclass(type, Generic) and hasattr(type, "__orig_bases__"))
|
||||
or type.__class__ is Union # On 3.14, unions are no longer typing._GenericAlias
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import Any, get_args
|
||||
|
||||
from attrs import NOTHING
|
||||
from typing_extensions import Self
|
||||
|
||||
from ._compat import copy_with, get_args, is_annotated, is_generic
|
||||
from ._compat import copy_with, is_annotated, is_generic
|
||||
|
||||
|
||||
def deep_copy_with(t, mapping: Mapping[str, Any], self_is=NOTHING):
|
||||
|
||||
+54
-10
@@ -5,25 +5,20 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
DefaultDict,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
TypeVar,
|
||||
get_type_hints,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, DefaultDict, Literal, NamedTuple, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute, NothingType
|
||||
|
||||
from ._compat import (
|
||||
ANIES,
|
||||
AbcSet,
|
||||
get_args,
|
||||
get_full_type_hints,
|
||||
get_origin,
|
||||
is_bare,
|
||||
is_frozenset,
|
||||
is_mapping,
|
||||
is_mutable_sequence,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
)
|
||||
@@ -47,10 +42,13 @@ if TYPE_CHECKING:
|
||||
|
||||
__all__ = [
|
||||
"defaultdict_structure_factory",
|
||||
"homogenous_tuple_structure_factory",
|
||||
"is_abstract_set",
|
||||
"is_any_set",
|
||||
"is_defaultdict",
|
||||
"is_frozenset",
|
||||
"is_mapping",
|
||||
"is_mutable_sequence",
|
||||
"is_namedtuple",
|
||||
"is_sequence",
|
||||
"is_set",
|
||||
@@ -70,6 +68,11 @@ def is_any_set(type) -> bool:
|
||||
return is_set(type) or is_frozenset(type)
|
||||
|
||||
|
||||
def is_abstract_set(type) -> bool:
|
||||
"""A predicate function for abstract (collection.abc) sets."""
|
||||
return type is AbcSet or (getattr(type, "__origin__", None) is AbcSet)
|
||||
|
||||
|
||||
def is_namedtuple(type: Any) -> bool:
|
||||
"""A predicate function for named tuples."""
|
||||
|
||||
@@ -151,6 +154,47 @@ def list_structure_factory(type: type, converter: BaseConverter) -> StructureHoo
|
||||
return structure_list
|
||||
|
||||
|
||||
def homogenous_tuple_structure_factory(
|
||||
type: type, converter: BaseConverter
|
||||
) -> StructureHook:
|
||||
"""A hook factory for homogenous (all elements the same, indeterminate length) tuples.
|
||||
|
||||
Converts any given iterable into a tuple.
|
||||
"""
|
||||
|
||||
if is_bare(type) or type.__args__[0] in ANIES:
|
||||
|
||||
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
|
||||
return tuple(obj)
|
||||
|
||||
return structure_tuple
|
||||
|
||||
elem_type = type.__args__[0]
|
||||
|
||||
try:
|
||||
handler = converter.get_structure_hook(elem_type)
|
||||
except RecursionError:
|
||||
# Break the cycle by using late binding.
|
||||
handler = converter.structure
|
||||
|
||||
if converter.detailed_validation:
|
||||
|
||||
# We have to structure into a list first anyway.
|
||||
list_structure = list_structure_factory(type, converter)
|
||||
|
||||
def structure_tuple(obj: Iterable[T], _: type = type) -> tuple[T, ...]:
|
||||
return tuple(list_structure(obj, _))
|
||||
|
||||
else:
|
||||
|
||||
def structure_tuple(
|
||||
obj: Iterable[T], _: type = type, _handler=handler, _elem_type=elem_type
|
||||
) -> tuple[T, ...]:
|
||||
return tuple([_handler(e, _elem_type) for e in obj])
|
||||
|
||||
return structure_tuple
|
||||
|
||||
|
||||
def namedtuple_unstructure_factory(
|
||||
cl: type[tuple], converter: BaseConverter, unstructure_to: Any = None
|
||||
) -> UnstructureHook:
|
||||
@@ -195,7 +239,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
|
||||
type=a,
|
||||
alias=name,
|
||||
)
|
||||
for name, a in get_type_hints(cl).items()
|
||||
for name, a in get_full_type_hints(cl).items()
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ from ._compat import (
|
||||
is_hetero_tuple,
|
||||
is_literal,
|
||||
is_mapping,
|
||||
is_mutable_sequence,
|
||||
is_mutable_set,
|
||||
is_optional,
|
||||
is_protocol,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
is_tuple,
|
||||
is_typeddict,
|
||||
is_union_type,
|
||||
@@ -54,8 +55,11 @@ from ._compat import (
|
||||
)
|
||||
from .cols import (
|
||||
defaultdict_structure_factory,
|
||||
homogenous_tuple_structure_factory,
|
||||
is_abstract_set,
|
||||
is_defaultdict,
|
||||
is_namedtuple,
|
||||
is_sequence,
|
||||
iterable_unstructure_factory,
|
||||
list_structure_factory,
|
||||
mapping_structure_factory,
|
||||
@@ -73,6 +77,7 @@ from .dispatch import (
|
||||
UnstructuredValue,
|
||||
UnstructureHook,
|
||||
)
|
||||
from .enums import enum_structure_factory, enum_unstructure_factory
|
||||
from .errors import (
|
||||
IterableValidationError,
|
||||
IterableValidationNote,
|
||||
@@ -225,6 +230,10 @@ class BaseConverter:
|
||||
)
|
||||
self._unstructure_func.register_func_list(
|
||||
[
|
||||
(
|
||||
lambda t: get_newtype_base(t) is not None,
|
||||
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
||||
),
|
||||
(
|
||||
is_protocol,
|
||||
lambda o: self.unstructure(o, unstructure_as=o.__class__),
|
||||
@@ -239,12 +248,12 @@ class BaseConverter:
|
||||
lambda t: self.get_unstructure_hook(get_type_alias_base(t)),
|
||||
True,
|
||||
),
|
||||
(is_literal_containing_enums, self.unstructure),
|
||||
(is_mapping, self._unstructure_mapping),
|
||||
(is_sequence, self._unstructure_seq),
|
||||
(is_mutable_set, self._unstructure_seq),
|
||||
(is_frozenset, self._unstructure_seq),
|
||||
(lambda t: issubclass(t, Enum), self._unstructure_enum),
|
||||
(is_literal_containing_enums, self.unstructure),
|
||||
(lambda t: is_subclass(t, Enum), enum_unstructure_factory, "extended"),
|
||||
(has, self._unstructure_attrs),
|
||||
(is_union_type, self._unstructure_union),
|
||||
(lambda t: t in ANIES, self.unstructure),
|
||||
@@ -271,20 +280,27 @@ class BaseConverter:
|
||||
),
|
||||
(is_literal, self._structure_simple_literal),
|
||||
(is_literal_containing_enums, self._structure_enum_literal),
|
||||
(is_sequence, list_structure_factory, "extended"),
|
||||
(is_sequence, homogenous_tuple_structure_factory, "extended"),
|
||||
(is_mutable_sequence, list_structure_factory, "extended"),
|
||||
(is_deque, self._structure_deque),
|
||||
(is_mutable_set, self._structure_set),
|
||||
(is_abstract_set, self._structure_frozenset),
|
||||
(is_frozenset, self._structure_frozenset),
|
||||
(is_tuple, self._structure_tuple),
|
||||
(is_namedtuple, namedtuple_structure_factory, "extended"),
|
||||
(is_mapping, self._structure_dict),
|
||||
(is_supported_union, self._gen_attrs_union_structure, True),
|
||||
*(
|
||||
[(is_supported_union, self._gen_attrs_union_structure, True)]
|
||||
if unstruct_strat is UnstructureStrategy.AS_DICT
|
||||
else []
|
||||
),
|
||||
(is_optional, self._structure_optional),
|
||||
(
|
||||
lambda t: is_union_type(t) and t in self._union_struct_registry,
|
||||
self._union_struct_registry.__getitem__,
|
||||
True,
|
||||
),
|
||||
(lambda t: is_subclass(t, Enum), enum_structure_factory, "extended"),
|
||||
(has, self._structure_attrs),
|
||||
]
|
||||
)
|
||||
@@ -295,7 +311,6 @@ class BaseConverter:
|
||||
(bytes, self._structure_call),
|
||||
(int, self._structure_call),
|
||||
(float, self._structure_call),
|
||||
(Enum, self._structure_call),
|
||||
(Path, self._structure_call),
|
||||
]
|
||||
)
|
||||
@@ -617,10 +632,6 @@ class BaseConverter:
|
||||
res.append(dispatch(a.type or v.__class__)(v))
|
||||
return tuple(res)
|
||||
|
||||
def _unstructure_enum(self, obj: Enum) -> Any:
|
||||
"""Convert an enum to its value."""
|
||||
return obj.value
|
||||
|
||||
def _unstructure_seq(self, seq: Sequence[T]) -> Sequence[T]:
|
||||
"""Convert a sequence to primitive equivalents."""
|
||||
# We can reuse the sequence class, so tuples stay tuples.
|
||||
@@ -1034,6 +1045,7 @@ class Converter(BaseConverter):
|
||||
"forbid_extra_keys",
|
||||
"omit_if_default",
|
||||
"type_overrides",
|
||||
"use_alias",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -1050,6 +1062,7 @@ class Converter(BaseConverter):
|
||||
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
|
||||
None, t
|
||||
),
|
||||
use_alias: bool = False,
|
||||
):
|
||||
"""
|
||||
:param detailed_validation: Whether to use a slightly slower mode for detailed
|
||||
@@ -1058,12 +1071,15 @@ class Converter(BaseConverter):
|
||||
registered unstructuring hooks match.
|
||||
:param structure_fallback_factory: A hook factory to be called when no
|
||||
registered structuring hooks match.
|
||||
:param use_alias: Whether to use the field alias instead of the field name as
|
||||
the un/structured dictionary key by default.
|
||||
|
||||
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
|
||||
.. versionadded:: 23.2.0 *structure_fallback_factory*
|
||||
.. versionchanged:: 24.2.0
|
||||
The default `structure_fallback_factory` now raises errors for missing handlers
|
||||
more eagerly, surfacing problems earlier.
|
||||
.. versionadded:: 25.2.0 *use_alias*
|
||||
"""
|
||||
super().__init__(
|
||||
dict_factory=dict_factory,
|
||||
@@ -1076,6 +1092,7 @@ class Converter(BaseConverter):
|
||||
self.omit_if_default = omit_if_default
|
||||
self.forbid_extra_keys = forbid_extra_keys
|
||||
self.type_overrides = dict(type_overrides)
|
||||
self.use_alias = use_alias
|
||||
|
||||
unstruct_collection_overrides = {
|
||||
get_origin(k) or k: v for k, v in unstruct_collection_overrides.items()
|
||||
@@ -1246,7 +1263,7 @@ class Converter(BaseConverter):
|
||||
attribs = fields(origin or cl)
|
||||
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
||||
# PEP 563 annotations - need to be resolved.
|
||||
resolve_types(cl)
|
||||
resolve_types(origin or cl)
|
||||
attrib_overrides = {
|
||||
a.name: self.type_overrides[a.type]
|
||||
for a in attribs
|
||||
@@ -1284,10 +1301,11 @@ class Converter(BaseConverter):
|
||||
def gen_structure_attrs_fromdict(
|
||||
self, cl: type[T]
|
||||
) -> Callable[[Mapping[str, Any], Any], T]:
|
||||
attribs = fields(get_origin(cl) or cl if is_generic(cl) else cl)
|
||||
origin = get_origin(cl)
|
||||
attribs = fields(origin or cl if is_generic(cl) else cl)
|
||||
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
|
||||
# PEP 563 annotations - need to be resolved.
|
||||
resolve_types(cl)
|
||||
resolve_types(origin or cl)
|
||||
attrib_overrides = {
|
||||
a.name: self.type_overrides[a.type]
|
||||
for a in attribs
|
||||
@@ -1299,6 +1317,7 @@ class Converter(BaseConverter):
|
||||
_cattrs_forbid_extra_keys=self.forbid_extra_keys,
|
||||
_cattrs_prefer_attrib_converters=self._prefer_attrib_converters,
|
||||
_cattrs_detailed_validation=self.detailed_validation,
|
||||
_cattrs_use_alias=self.use_alias,
|
||||
**attrib_overrides,
|
||||
)
|
||||
|
||||
@@ -1377,6 +1396,7 @@ class Converter(BaseConverter):
|
||||
unstruct_collection_overrides: Mapping[type, UnstructureHook] | None = None,
|
||||
prefer_attrib_converters: bool | None = None,
|
||||
detailed_validation: bool | None = None,
|
||||
use_alias: bool | None = None,
|
||||
) -> Self:
|
||||
"""Create a copy of the converter, keeping all existing custom hooks.
|
||||
|
||||
@@ -1416,6 +1436,7 @@ class Converter(BaseConverter):
|
||||
if detailed_validation is not None
|
||||
else self.detailed_validation
|
||||
),
|
||||
use_alias=(use_alias if use_alias is not None else self.use_alias),
|
||||
)
|
||||
|
||||
self._unstructure_func.copy_to(
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping
|
||||
from dataclasses import MISSING
|
||||
from functools import reduce
|
||||
from operator import or_
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, get_origin
|
||||
|
||||
from attrs import NOTHING, Attribute, AttrsInstance
|
||||
|
||||
@@ -16,7 +16,6 @@ from ._compat import (
|
||||
adapted_fields,
|
||||
fields_dict,
|
||||
get_args,
|
||||
get_origin,
|
||||
has,
|
||||
is_literal,
|
||||
is_union_type,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .converters import BaseConverter
|
||||
|
||||
|
||||
def enum_unstructure_factory(
|
||||
type: type[Enum], converter: "BaseConverter"
|
||||
) -> Callable[[Enum], Any]:
|
||||
"""A factory for generating enum unstructure hooks.
|
||||
|
||||
If the enum is a typed enum (has `_value_`), we use the underlying value's hook.
|
||||
Otherwise, we use the value directly.
|
||||
"""
|
||||
if "_value_" in type.__annotations__:
|
||||
return lambda e: converter.unstructure(e.value)
|
||||
|
||||
return lambda e: e.value
|
||||
|
||||
|
||||
def enum_structure_factory(
|
||||
type: type[Enum], converter: "BaseConverter"
|
||||
) -> Callable[[Any, type[Enum]], Enum]:
|
||||
"""A factory for generating enum structure hooks.
|
||||
|
||||
If the enum is a typed enum (has `_value_`), we structure the value first.
|
||||
Otherwise, we use the value directly.
|
||||
"""
|
||||
if "_value_" in type.__annotations__:
|
||||
val_type = type.__annotations__["_value_"]
|
||||
val_hook = converter.get_structure_hook(val_type)
|
||||
return lambda v, _: type(val_hook(v, val_type))
|
||||
|
||||
return lambda v, _: type(v)
|
||||
@@ -13,14 +13,18 @@ class StructureHandlerNotFoundError(Exception):
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, type_: type) -> None:
|
||||
super().__init__(message)
|
||||
super().__init__(message, type_)
|
||||
self.message = message
|
||||
self.type_ = type_
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class BaseValidationError(ExceptionGroup):
|
||||
cl: type
|
||||
|
||||
def __new__(cls, message: str, excs: Sequence[Exception], cl: type):
|
||||
def __new__(cls, message: str, excs: Sequence[Exception], cl: type) -> Self:
|
||||
obj = super().__new__(cls, message, excs)
|
||||
obj.cl = cl
|
||||
return obj
|
||||
@@ -35,9 +39,7 @@ class IterableValidationNote(str):
|
||||
index: Union[int, str] # Ints for list indices, strs for dict keys
|
||||
type: Any
|
||||
|
||||
def __new__(
|
||||
cls, string: str, index: Union[int, str], type: Any
|
||||
) -> "IterableValidationNote":
|
||||
def __new__(cls, string: str, index: Union[int, str], type: Any) -> Self:
|
||||
instance = str.__new__(cls, string)
|
||||
instance.index = index
|
||||
instance.type = type
|
||||
@@ -76,7 +78,7 @@ class AttributeValidationNote(str):
|
||||
name: str
|
||||
type: Any
|
||||
|
||||
def __new__(cls, string: str, name: str, type: Any) -> "AttributeValidationNote":
|
||||
def __new__(cls, string: str, name: str, type: Any) -> Self:
|
||||
instance = str.__new__(cls, string)
|
||||
instance.name = name
|
||||
instance.type = type
|
||||
@@ -122,11 +124,15 @@ class ForbiddenExtraKeysError(Exception):
|
||||
def __init__(
|
||||
self, message: Optional[str], cl: type, extra_fields: set[str]
|
||||
) -> None:
|
||||
self.message = message
|
||||
self.cl = cl
|
||||
self.extra_fields = extra_fields
|
||||
cln = cl.__name__
|
||||
|
||||
super().__init__(
|
||||
message
|
||||
or f"Extra fields in constructor for {cln}: {', '.join(extra_fields)}"
|
||||
super().__init__(message, cl, extra_fields)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
self.message
|
||||
or f"Extra fields in constructor for {self.cl.__name__}: "
|
||||
f"{', '.join(sorted(self.extra_fields))}"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute, Factory
|
||||
from attrs import NOTHING, Attribute, Converter, Factory, evolve
|
||||
from typing_extensions import NoDefault
|
||||
|
||||
from .._compat import (
|
||||
@@ -33,7 +33,7 @@ from ..types import SimpleStructureHook
|
||||
from ._consts import AttributeOverride, already_generating, neutral
|
||||
from ._generics import generate_mapping
|
||||
from ._lc import generate_unique_filename
|
||||
from ._shared import find_structure_handler
|
||||
from ._shared import _annotated_override_or_default, find_structure_handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
@@ -74,7 +74,7 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
typevar_map: dict[str, Any] = {},
|
||||
_cattrs_omit_if_default: bool = False,
|
||||
_cattrs_use_linecache: bool = True,
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> Callable[[T], dict[str, Any]]:
|
||||
@@ -95,7 +95,17 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
.. versionchanged:: 26.1.0
|
||||
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
|
||||
specified, the converter is applied to the default value before checking if it
|
||||
is equal to the attribute's value.
|
||||
"""
|
||||
|
||||
fn_name = "unstructure_" + cl.__name__
|
||||
@@ -104,15 +114,27 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
invocation_lines = []
|
||||
internal_arg_parts = {}
|
||||
|
||||
if _cattrs_use_alias == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||
|
||||
for a in attrs:
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
if attr_name in kwargs:
|
||||
override = kwargs[attr_name]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[attr_name] = override
|
||||
|
||||
if override.omit:
|
||||
continue
|
||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||
continue
|
||||
if override.rename is None:
|
||||
kn = attr_name if not _cattrs_use_alias else a.alias
|
||||
if kn != attr_name:
|
||||
kwargs[attr_name] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
d = a.default
|
||||
@@ -170,16 +192,32 @@ def make_dict_unstructure_fn_from_attrs(
|
||||
if isinstance(d, Factory):
|
||||
globs[def_name] = d.factory
|
||||
internal_arg_parts[def_name] = d.factory
|
||||
if d.takes_self:
|
||||
lines.append(f" if instance.{attr_name} != {def_name}(instance):")
|
||||
else:
|
||||
lines.append(f" if instance.{attr_name} != {def_name}():")
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
def_str = f"{def_name}(instance)" if d.takes_self else f"{def_name}()"
|
||||
else:
|
||||
globs[def_name] = d
|
||||
internal_arg_parts[def_name] = d
|
||||
lines.append(f" if instance.{attr_name} != {def_name}:")
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
def_str = def_name
|
||||
|
||||
c = a.converter
|
||||
if c is not None:
|
||||
conv_name = f"__c_conv_{attr_name}"
|
||||
if isinstance(c, Converter):
|
||||
globs[conv_name] = c
|
||||
internal_arg_parts[conv_name] = c
|
||||
field_name = f"__c_field_{attr_name}"
|
||||
globs[field_name] = a
|
||||
internal_arg_parts[field_name] = a
|
||||
def_str = f"{conv_name}({def_str}, instance, {field_name})"
|
||||
elif isinstance(d, Factory):
|
||||
globs[conv_name] = c
|
||||
internal_arg_parts[conv_name] = c
|
||||
def_str = f"{conv_name}({def_str})"
|
||||
else:
|
||||
globs[def_name] = c(d)
|
||||
internal_arg_parts[def_name] = c(d)
|
||||
|
||||
lines.append(f" if instance.{attr_name} != {def_str}:")
|
||||
lines.append(f" res['{kn}'] = {invoke}")
|
||||
|
||||
else:
|
||||
# No default or no override.
|
||||
@@ -217,7 +255,7 @@ def make_dict_unstructure_fn(
|
||||
converter: BaseConverter,
|
||||
_cattrs_omit_if_default: bool = False,
|
||||
_cattrs_use_linecache: bool = True,
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> Callable[[T], dict[str, Any]]:
|
||||
@@ -235,13 +273,22 @@ def make_dict_unstructure_fn(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
"""
|
||||
origin = get_origin(cl)
|
||||
attrs = adapted_fields(origin or cl) # type: ignore
|
||||
|
||||
mapping = {}
|
||||
if _cattrs_use_alias == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||
if is_generic(cl):
|
||||
mapping = generate_mapping(cl, mapping)
|
||||
|
||||
@@ -289,7 +336,7 @@ def make_dict_structure_fn_from_attrs(
|
||||
bool | Literal["from_converter"]
|
||||
) = "from_converter",
|
||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
||||
@@ -314,7 +361,13 @@ def make_dict_structure_fn_from_attrs(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionadded:: 24.1.0
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
"""
|
||||
|
||||
cl_name = cl.__name__
|
||||
@@ -350,6 +403,9 @@ def make_dict_structure_fn_from_attrs(
|
||||
if _cattrs_forbid_extra_keys == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_forbid_extra_keys = getattr(converter, "forbid_extra_keys", False)
|
||||
if _cattrs_use_alias == "from_converter":
|
||||
# BaseConverter doesn't have it so we're careful.
|
||||
_cattrs_use_alias = getattr(converter, "use_alias", False)
|
||||
if _cattrs_detailed_validation == "from_converter":
|
||||
_cattrs_detailed_validation = converter.detailed_validation
|
||||
if _cattrs_prefer_attrib_converters == "from_converter":
|
||||
@@ -367,7 +423,13 @@ def make_dict_structure_fn_from_attrs(
|
||||
internal_arg_parts["__c_avn"] = AttributeValidationNote
|
||||
for a in attrs:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
|
||||
if override.omit:
|
||||
continue
|
||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||
@@ -396,6 +458,8 @@ def make_dict_structure_fn_from_attrs(
|
||||
ian = a.alias
|
||||
if override.rename is None:
|
||||
kn = an if not _cattrs_use_alias else a.alias
|
||||
if kn != an:
|
||||
kwargs[an] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
|
||||
@@ -496,14 +560,24 @@ def make_dict_structure_fn_from_attrs(
|
||||
# The first loop deals with required args.
|
||||
for a in attrs:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
|
||||
if override.omit:
|
||||
continue
|
||||
if override.omit is None and not a.init and not _cattrs_include_init_false:
|
||||
continue
|
||||
|
||||
if a.default is not NOTHING:
|
||||
non_required.append(a)
|
||||
# The next loop will handle it.
|
||||
continue
|
||||
|
||||
t = a.type
|
||||
if isinstance(t, TypeVar):
|
||||
t = typevar_map.get(t.__name__, t)
|
||||
@@ -523,6 +597,8 @@ def make_dict_structure_fn_from_attrs(
|
||||
|
||||
if override.rename is None:
|
||||
kn = an if not _cattrs_use_alias else a.alias
|
||||
if kn != an:
|
||||
kwargs[an] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
allowed_fields.add(kn)
|
||||
@@ -592,6 +668,8 @@ def make_dict_structure_fn_from_attrs(
|
||||
|
||||
if override.rename is None:
|
||||
kn = an if not _cattrs_use_alias else a.alias
|
||||
if kn != an:
|
||||
kwargs[an] = evolve(override, rename=kn)
|
||||
else:
|
||||
kn = override.rename
|
||||
allowed_fields.add(kn)
|
||||
@@ -682,7 +760,7 @@ def make_dict_structure_fn(
|
||||
bool | Literal["from_converter"]
|
||||
) = "from_converter",
|
||||
_cattrs_detailed_validation: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_use_alias: bool = False,
|
||||
_cattrs_use_alias: bool | Literal["from_converter"] = "from_converter",
|
||||
_cattrs_include_init_false: bool = False,
|
||||
**kwargs: AttributeOverride,
|
||||
) -> SimpleStructureHook[Mapping[str, Any], T]:
|
||||
@@ -706,14 +784,20 @@ def make_dict_structure_fn(
|
||||
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
|
||||
will be included.
|
||||
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionchanged:: 23.2.0
|
||||
.. versionadded:: 23.2.0 *_cattrs_use_alias*
|
||||
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
|
||||
.. versionchanged:: 23.2.0
|
||||
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
|
||||
take their values from the given converter by default.
|
||||
.. versionchanged:: 24.1.0
|
||||
.. versionchanged:: 24.1.0
|
||||
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
|
||||
converter by default.
|
||||
.. versionchanged:: 25.2.0
|
||||
The `_cattrs_use_alias` parameter takes its value from the given converter
|
||||
by default.
|
||||
.. versionchanged:: 26.1.0
|
||||
`typing.Annotated[T, override()]` is now recognized and can be used to customize
|
||||
unstructuring.
|
||||
"""
|
||||
|
||||
mapping = {}
|
||||
|
||||
@@ -4,15 +4,31 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from attrs import NOTHING, Attribute, Factory
|
||||
|
||||
from .._compat import is_bare_final
|
||||
from .._compat import get_args, is_annotated, is_bare_final
|
||||
from ..dispatch import StructureHook
|
||||
from ..errors import StructureHandlerNotFoundError
|
||||
from ..fns import raise_error
|
||||
from ._consts import AttributeOverride
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
|
||||
|
||||
def _annotated_override_or_default(
|
||||
type: Any, default: AttributeOverride
|
||||
) -> AttributeOverride:
|
||||
"""
|
||||
If the type is Annotated containing an AttributeOverride, return it.
|
||||
Otherwise, return the default.
|
||||
"""
|
||||
if is_annotated(type):
|
||||
for arg in get_args(type):
|
||||
if isinstance(arg, AttributeOverride):
|
||||
return arg
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def find_structure_handler(
|
||||
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
|
||||
) -> StructureHook | None:
|
||||
|
||||
@@ -3,23 +3,12 @@ from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from inspect import get_annotations
|
||||
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
||||
|
||||
from attrs import NOTHING, Attribute
|
||||
from typing_extensions import _TypedDictMeta
|
||||
|
||||
try:
|
||||
from inspect import get_annotations
|
||||
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return get_annotations(cl, eval_str=True)
|
||||
|
||||
except ImportError:
|
||||
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return cl.__dict__.get("__annotations__", {})
|
||||
|
||||
|
||||
from .._compat import (
|
||||
get_full_type_hints,
|
||||
get_notrequired_base,
|
||||
@@ -40,7 +29,7 @@ from . import AttributeOverride
|
||||
from ._consts import already_generating, neutral
|
||||
from ._generics import generate_mapping
|
||||
from ._lc import generate_unique_filename
|
||||
from ._shared import find_structure_handler
|
||||
from ._shared import _annotated_override_or_default, find_structure_handler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..converters import BaseConverter
|
||||
@@ -50,6 +39,10 @@ __all__ = ["make_dict_structure_fn", "make_dict_unstructure_fn"]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def get_annots(cl) -> dict[str, Any]:
|
||||
return get_annotations(cl, eval_str=True)
|
||||
|
||||
|
||||
def make_dict_unstructure_fn(
|
||||
cl: type[T],
|
||||
converter: BaseConverter,
|
||||
@@ -109,11 +102,20 @@ def make_dict_unstructure_fn(
|
||||
# * all attributes resolve to `converter._unstructure_identity`
|
||||
for a in attrs:
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
t = a.type
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if attr_name in kwargs:
|
||||
override = kwargs[attr_name]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[attr_name] = override
|
||||
if override != neutral:
|
||||
break
|
||||
handler = None
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__name__ in mapping:
|
||||
@@ -125,9 +127,6 @@ def make_dict_unstructure_fn(
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if handler is None:
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
try:
|
||||
handler = converter.get_unstructure_hook(t)
|
||||
except RecursionError:
|
||||
@@ -142,10 +141,22 @@ def make_dict_unstructure_fn(
|
||||
|
||||
for ix, a in enumerate(attrs):
|
||||
attr_name = a.name
|
||||
override = kwargs.get(attr_name, neutral)
|
||||
t = a.type
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if attr_name in kwargs:
|
||||
override = kwargs[attr_name]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[attr_name] = override
|
||||
|
||||
if override.omit:
|
||||
lines.append(f" res.pop('{attr_name}', None)")
|
||||
continue
|
||||
|
||||
if override.rename is not None:
|
||||
# We also need to pop when renaming, since we're copying
|
||||
# the original.
|
||||
@@ -160,8 +171,6 @@ def make_dict_unstructure_fn(
|
||||
if override.unstruct_hook is not None:
|
||||
handler = override.unstruct_hook
|
||||
else:
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__name__ in mapping:
|
||||
t = mapping[t.__name__]
|
||||
@@ -171,9 +180,6 @@ def make_dict_unstructure_fn(
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
if handler is None:
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
try:
|
||||
handler = converter.get_unstructure_hook(t)
|
||||
except RecursionError:
|
||||
@@ -220,12 +226,15 @@ def make_dict_unstructure_fn(
|
||||
)
|
||||
|
||||
eval(compile(script, fname, "exec"), globs)
|
||||
|
||||
res = globs[fn_name]
|
||||
res.overrides = kwargs
|
||||
finally:
|
||||
working_set.remove(cl)
|
||||
if not working_set:
|
||||
del already_generating.working_set
|
||||
|
||||
return globs[fn_name]
|
||||
return res
|
||||
|
||||
|
||||
def make_dict_structure_fn(
|
||||
@@ -326,20 +335,25 @@ def make_dict_structure_fn(
|
||||
for ix, a in enumerate(attrs):
|
||||
an = a.name
|
||||
attr_required = an in req_keys
|
||||
override = kwargs.get(an, neutral)
|
||||
t = a.type
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
if override.omit:
|
||||
continue
|
||||
t = a.type
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
t = deep_copy_with(t, mapping, cl)
|
||||
|
||||
@@ -399,7 +413,12 @@ def make_dict_structure_fn(
|
||||
for ix, a in enumerate(attrs):
|
||||
an = a.name
|
||||
attr_required = an in req_keys
|
||||
override = kwargs.get(an, neutral)
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(a.type, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
if override.omit:
|
||||
continue
|
||||
if not attr_required:
|
||||
@@ -448,13 +467,18 @@ def make_dict_structure_fn(
|
||||
if non_required:
|
||||
for ix, a in non_required:
|
||||
an = a.name
|
||||
override = kwargs.get(an, neutral)
|
||||
t = a.type
|
||||
|
||||
nrb = get_notrequired_base(t)
|
||||
if nrb is not NOTHING:
|
||||
t = nrb
|
||||
|
||||
if an in kwargs:
|
||||
override = kwargs[an]
|
||||
else:
|
||||
override = _annotated_override_or_default(t, neutral)
|
||||
if override != neutral:
|
||||
kwargs[an] = override
|
||||
|
||||
if isinstance(t, TypeVar):
|
||||
t = mapping.get(t.__name__, t)
|
||||
elif is_generic(t) and not is_bare(t) and not is_annotated(t):
|
||||
@@ -514,7 +538,9 @@ def make_dict_structure_fn(
|
||||
)
|
||||
|
||||
eval(compile(script, fname, "exec"), globs)
|
||||
return globs[fn_name]
|
||||
res = globs[fn_name]
|
||||
res.overrides = kwargs
|
||||
return res
|
||||
|
||||
|
||||
def _adapted_fields(cls: Any) -> list[Attribute]:
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, TypeVar, get_args
|
||||
from typing import Any, ParamSpec, TypeVar, get_args
|
||||
|
||||
from .._compat import is_subclass
|
||||
from ..converters import Converter, UnstructureHook
|
||||
from ..fns import identity
|
||||
|
||||
if sys.version_info[:2] < (3, 10):
|
||||
from typing_extensions import ParamSpec
|
||||
else:
|
||||
from typing import ParamSpec
|
||||
|
||||
|
||||
def validate_datetime(v, _):
|
||||
if not isinstance(v, datetime):
|
||||
|
||||
@@ -99,11 +99,11 @@ def configure_converter(converter: BaseConverter):
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_unstructure_hook(datetime, identity)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ def configure_converter(converter: BaseConverter):
|
||||
)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ def configure_converter(converter: BaseConverter) -> None:
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda _: identity)
|
||||
configure_union_passthrough(Union[str, bool, int, float, None], converter)
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def configure_converter(converter: BaseConverter) -> None:
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: datetime.fromtimestamp(v, timezone.utc).date()
|
||||
)
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -3,18 +3,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode
|
||||
from collections.abc import Callable
|
||||
from dataclasses import is_dataclass
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import Any, Callable, TypeVar, Union, get_type_hints
|
||||
from typing import Any, TypeVar, Union, get_type_hints
|
||||
|
||||
from attrs import has as attrs_has
|
||||
from attrs import resolve_types
|
||||
from msgspec import Struct, convert, to_builtins
|
||||
from msgspec.json import Encoder, decode
|
||||
|
||||
from .._compat import fields, get_args, get_origin, is_bare, is_mapping, is_sequence
|
||||
from .._compat import (
|
||||
fields,
|
||||
get_args,
|
||||
get_origin,
|
||||
is_bare,
|
||||
is_mapping,
|
||||
is_sequence,
|
||||
is_subclass,
|
||||
)
|
||||
from ..cols import is_namedtuple
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..dispatch import UnstructureHook
|
||||
@@ -74,7 +83,9 @@ def configure_converter(converter: Converter) -> None:
|
||||
configure_passthroughs(converter)
|
||||
|
||||
converter.register_unstructure_hook(Struct, to_builtins)
|
||||
converter.register_unstructure_hook(Enum, identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
lambda t: is_subclass(t, Enum), lambda t, c: identity
|
||||
)
|
||||
|
||||
converter.register_structure_hook(Struct, convert)
|
||||
converter.register_structure_hook(bytes, lambda v, _: b64decode(v))
|
||||
|
||||
@@ -87,8 +87,8 @@ def configure_converter(converter: Converter) -> None:
|
||||
),
|
||||
]
|
||||
)
|
||||
converter.register_unstructure_hook_func(
|
||||
partial(is_primitive_enum, include_bare_enums=True), identity
|
||||
converter.register_unstructure_hook_factory(
|
||||
partial(is_primitive_enum, include_bare_enums=True), lambda t: identity
|
||||
)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
|
||||
@@ -12,6 +12,7 @@ from tomlkit.items import Float, Integer, String
|
||||
|
||||
from .._compat import is_mapping, is_subclass
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import validate_datetime, wrap
|
||||
|
||||
@@ -37,6 +38,9 @@ def configure_converter(converter: BaseConverter):
|
||||
* sets are serialized as lists
|
||||
* tuples are serializas as lists
|
||||
* mapping keys are coerced into strings when unstructuring
|
||||
|
||||
.. versionchanged:: 26.1.0
|
||||
date objects are now passed through to tomlkit without unstructuring.
|
||||
"""
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook(
|
||||
@@ -67,10 +71,12 @@ def configure_converter(converter: BaseConverter):
|
||||
|
||||
# datetime inherits from date, so identity unstructure hook used
|
||||
# here to prevent the date unstructure hook running.
|
||||
converter.register_unstructure_hook(datetime, lambda v: v)
|
||||
converter.register_unstructure_hook(datetime, identity)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook(date, identity)
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
|
||||
)
|
||||
configure_union_passthrough(
|
||||
Union[str, String, bool, int, Integer, float, Float], converter
|
||||
)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Preconfigured converters for tomllib."""
|
||||
|
||||
from base64 import b85decode, b85encode
|
||||
from collections.abc import Set
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from operator import attrgetter
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
try:
|
||||
from tomllib import loads
|
||||
except ImportError:
|
||||
from tomli import loads
|
||||
|
||||
try:
|
||||
from tomli_w import dumps
|
||||
except ImportError: # pragma: nocover
|
||||
dumps = None
|
||||
|
||||
from .._compat import is_mapping, is_subclass
|
||||
from ..converters import BaseConverter, Converter
|
||||
from ..fns import identity
|
||||
from ..strategies import configure_union_passthrough
|
||||
from . import validate_datetime, wrap
|
||||
|
||||
__all__ = ["TomllibConverter", "configure_converter", "make_converter"]
|
||||
|
||||
T = TypeVar("T")
|
||||
_enum_value_getter = attrgetter("_value_")
|
||||
|
||||
|
||||
class TomllibConverter(Converter):
|
||||
"""A converter subclass specialized for tomllib."""
|
||||
|
||||
if dumps is not None:
|
||||
|
||||
def dumps(self, obj: Any, unstructure_as: Any = None, **kwargs: Any) -> str:
|
||||
return dumps(self.unstructure(obj, unstructure_as=unstructure_as), **kwargs)
|
||||
|
||||
def loads(self, data: str, cl: type[T], **kwargs: Any) -> T:
|
||||
return self.structure(loads(data, **kwargs), cl)
|
||||
|
||||
|
||||
def configure_converter(converter: BaseConverter):
|
||||
"""
|
||||
Configure the converter for use with the tomllib library.
|
||||
|
||||
* bytes are serialized as base85 strings
|
||||
* sets are serialized as lists
|
||||
* tuples are serializas as lists
|
||||
* mapping keys are coerced into strings when unstructuring
|
||||
* dates and datetimes are left for tomllib to handle
|
||||
"""
|
||||
converter.register_structure_hook(bytes, lambda v, _: b85decode(v))
|
||||
converter.register_unstructure_hook(
|
||||
bytes, lambda v: (b85encode(v) if v else b"").decode("utf8")
|
||||
)
|
||||
|
||||
@converter.register_unstructure_hook_factory(is_mapping)
|
||||
def gen_unstructure_mapping(cl: Any, unstructure_to=None):
|
||||
key_handler = str
|
||||
args = getattr(cl, "__args__", None)
|
||||
if args:
|
||||
if is_subclass(args[0], str):
|
||||
key_handler = _enum_value_getter if is_subclass(args[0], Enum) else None
|
||||
elif is_subclass(args[0], bytes):
|
||||
|
||||
def key_handler(k: bytes):
|
||||
return b85encode(k).decode("utf8")
|
||||
|
||||
return converter.gen_unstructure_mapping(
|
||||
cl, unstructure_to=unstructure_to, key_handler=key_handler
|
||||
)
|
||||
|
||||
converter.register_unstructure_hook(datetime, identity)
|
||||
converter.register_structure_hook(datetime, validate_datetime)
|
||||
converter.register_unstructure_hook(date, identity)
|
||||
converter.register_structure_hook(
|
||||
date, lambda v, _: v if isinstance(v, date) else date.fromisoformat(v)
|
||||
)
|
||||
configure_union_passthrough(Union[str, int, float, bool], converter)
|
||||
|
||||
|
||||
@wrap(TomllibConverter)
|
||||
def make_converter(*args: Any, **kwargs: Any) -> TomllibConverter:
|
||||
kwargs["unstruct_collection_overrides"] = {
|
||||
Set: list,
|
||||
tuple: list,
|
||||
**kwargs.get("unstruct_collection_overrides", {}),
|
||||
}
|
||||
res = TomllibConverter(*args, **kwargs)
|
||||
configure_converter(res)
|
||||
|
||||
return res
|
||||
@@ -47,7 +47,7 @@ def configure_converter(converter: BaseConverter):
|
||||
converter.register_structure_hook(datetime, lambda v, _: datetime.fromisoformat(v))
|
||||
converter.register_unstructure_hook(date, lambda v: v.isoformat())
|
||||
converter.register_structure_hook(date, lambda v, _: date.fromisoformat(v))
|
||||
converter.register_unstructure_hook_func(is_primitive_enum, identity)
|
||||
converter.register_unstructure_hook_factory(is_primitive_enum, lambda t: identity)
|
||||
converter.register_unstructure_hook_factory(
|
||||
is_literal_containing_enums, literals_with_enums_unstructure_factory
|
||||
)
|
||||
|
||||
@@ -9,21 +9,25 @@ from typing import Any, Callable, TypeVar, Union
|
||||
from ..converters import BaseConverter
|
||||
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
|
||||
from ..gen._consts import already_generating
|
||||
from ..subclasses import subclasses
|
||||
|
||||
|
||||
def _make_subclasses_tree(cl: type) -> list[type]:
|
||||
# get class origin for accessing subclasses (see #648 for more info)
|
||||
cls_origin = typing.get_origin(cl) or cl
|
||||
return [cl] + [
|
||||
sscl
|
||||
for scl in cls_origin.__subclasses__()
|
||||
for sscl in _make_subclasses_tree(scl)
|
||||
]
|
||||
|
||||
# Use a dict to deduplicate and keep insertion order.
|
||||
seen = {cl: None}
|
||||
for scl in subclasses(cls_origin):
|
||||
for sscl in _make_subclasses_tree(scl):
|
||||
seen[sscl] = None
|
||||
return list(seen)
|
||||
|
||||
|
||||
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
|
||||
"""Whether the given class has subclasses from `given_subclasses`."""
|
||||
actual = set(cl.__subclasses__())
|
||||
cls_origin = typing.get_origin(cl) or cl
|
||||
actual = set(subclasses(cls_origin))
|
||||
given = set(given_subclasses)
|
||||
return bool(actual & given)
|
||||
|
||||
@@ -68,6 +72,9 @@ def include_subclasses(
|
||||
.. versionchanged:: 24.1.0
|
||||
When overrides are not provided, hooks for individual classes are retrieved from
|
||||
the converter instead of generated with no overrides, using converter defaults.
|
||||
.. versionchanged:: 25.2.0
|
||||
Slotted dataclasses work on Python 3.14 via :func:`cattrs.subclasses.subclasses`,
|
||||
which filters out duplicate classes caused by slotting.
|
||||
"""
|
||||
# Due to https://github.com/python-attrs/attrs/issues/1047
|
||||
collect()
|
||||
@@ -231,7 +238,13 @@ def _include_subclasses_with_union_strategy(
|
||||
return cls is _cl
|
||||
|
||||
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
|
||||
subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
|
||||
subclasses = tuple(
|
||||
[
|
||||
c
|
||||
for c in union_classes
|
||||
if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)
|
||||
]
|
||||
)
|
||||
if len(subclasses) > 1:
|
||||
u = Union[subclasses] # type: ignore
|
||||
union_strategy(u, converter)
|
||||
|
||||
@@ -52,23 +52,10 @@ def configure_tagged_union(
|
||||
if is_type_alias(union):
|
||||
union = union.__value__
|
||||
args = union.__args__
|
||||
|
||||
tag_to_hook = {}
|
||||
exact_cl_unstruct_hooks = {}
|
||||
for cl in args:
|
||||
tag = tag_generator(cl)
|
||||
struct_handler = converter.get_structure_hook(cl)
|
||||
unstruct_handler = converter.get_unstructure_hook(cl)
|
||||
|
||||
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
|
||||
return _h(val, _cl)
|
||||
|
||||
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
|
||||
return _h(val)
|
||||
|
||||
tag_to_hook[tag] = structure_union_member
|
||||
exact_cl_unstruct_hooks[cl] = unstructure_union_member
|
||||
|
||||
cl_to_tag = {cl: tag_generator(cl) for cl in args}
|
||||
cl_to_tag = {}
|
||||
|
||||
if default is not NOTHING:
|
||||
default_handler = converter.get_structure_hook(default)
|
||||
@@ -76,36 +63,9 @@ def configure_tagged_union(
|
||||
def structure_default(val: dict, _cl=default, _h=default_handler):
|
||||
return _h(val, _cl)
|
||||
|
||||
tag_to_hook = defaultdict(lambda: structure_default, tag_to_hook)
|
||||
cl_to_tag = defaultdict(lambda: default, cl_to_tag)
|
||||
tag_to_hook = defaultdict(lambda: structure_default)
|
||||
cl_to_tag = defaultdict(lambda: default)
|
||||
|
||||
def unstructure_tagged_union(
|
||||
val: union,
|
||||
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
|
||||
_cl_to_tag=cl_to_tag,
|
||||
_tag_name=tag_name,
|
||||
) -> dict:
|
||||
res = _exact_cl_unstruct_hooks[val.__class__](val)
|
||||
res[_tag_name] = _cl_to_tag[val.__class__]
|
||||
return res
|
||||
|
||||
if default is NOTHING:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
val = val.copy()
|
||||
return _tag_to_cl[val.pop(_tag_name)](val)
|
||||
|
||||
else:
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
return _tag_to_cl[val[_tag_name]](val)
|
||||
|
||||
else:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
@@ -135,11 +95,54 @@ def configure_tagged_union(
|
||||
return _tag_to_hook[val[_tag_name]](val)
|
||||
return _dh(val, _default)
|
||||
|
||||
else:
|
||||
if getattr(converter, "forbid_extra_keys", False):
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
val = val.copy()
|
||||
return _tag_to_cl[val.pop(_tag_name)](val)
|
||||
|
||||
else:
|
||||
|
||||
def structure_tagged_union(
|
||||
val: dict, _, _tag_to_cl=tag_to_hook, _tag_name=tag_name
|
||||
) -> union:
|
||||
return _tag_to_cl[val[_tag_name]](val)
|
||||
|
||||
def unstructure_tagged_union(
|
||||
val: union,
|
||||
_exact_cl_unstruct_hooks=exact_cl_unstruct_hooks,
|
||||
_cl_to_tag=cl_to_tag,
|
||||
_tag_name=tag_name,
|
||||
) -> dict:
|
||||
res = _exact_cl_unstruct_hooks[val.__class__](val)
|
||||
res[_tag_name] = _cl_to_tag[val.__class__]
|
||||
return res
|
||||
|
||||
converter.register_unstructure_hook(union, unstructure_tagged_union)
|
||||
converter.register_structure_hook(union, structure_tagged_union)
|
||||
|
||||
for cl in args:
|
||||
tag = tag_generator(cl)
|
||||
struct_handler = converter.get_structure_hook(cl)
|
||||
unstruct_handler = converter.get_unstructure_hook(cl)
|
||||
|
||||
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
def structure_union_member(val: dict, _cl=cl, _h=struct_handler) -> cl:
|
||||
return _h(val, _cl)
|
||||
|
||||
def unstructure_union_member(val: union, _h=unstruct_handler) -> dict:
|
||||
return _h(val)
|
||||
|
||||
tag_to_hook[tag] = structure_union_member
|
||||
exact_cl_unstruct_hooks[cl] = unstructure_union_member
|
||||
cl_to_tag[cl] = tag
|
||||
|
||||
|
||||
def configure_union_passthrough(
|
||||
union: Any, converter: BaseConverter, accept_ints_as_floats: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Configure the converter to support validating and passing through unions of the
|
||||
provided types and their subsets.
|
||||
@@ -162,7 +165,14 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
If the union contains a class and one or more of its subclasses, the subclasses
|
||||
will also be included when validating the superclass.
|
||||
|
||||
:param accept_ints_as_floats: When set (the default), if the provided union
|
||||
contains both ints and floats, actual unions containing only floats will also accept
|
||||
ints. See https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
for more information.
|
||||
|
||||
.. versionadded:: 23.2.0
|
||||
.. versionchanged:: 25.2.0
|
||||
Introduced the `accept_ints_as_floats` parameter.
|
||||
"""
|
||||
args = set(union.__args__)
|
||||
|
||||
@@ -205,6 +215,16 @@ def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
|
||||
and not is_literal(a)
|
||||
}
|
||||
|
||||
# By default, when floats are part of the union, accept ints too.
|
||||
if (
|
||||
accept_ints_as_floats
|
||||
and int in args
|
||||
and float in args
|
||||
and float in non_literal_classes
|
||||
and int not in non_literal_classes
|
||||
):
|
||||
non_literal_classes.add(int)
|
||||
|
||||
if spillover:
|
||||
spillover_type = (
|
||||
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info <= (3, 13):
|
||||
|
||||
def subclasses(cls: type) -> list[type]:
|
||||
"""A proxy for `cls.__subclasses__()` on older Pythons."""
|
||||
return cls.__subclasses__()
|
||||
|
||||
else:
|
||||
|
||||
def subclasses(cls: type) -> list[type]:
|
||||
"""A helper for getting subclasses of a class.
|
||||
|
||||
Filters out duplicate subclasses of slot dataclasses and attrs classes.
|
||||
"""
|
||||
return [
|
||||
cl
|
||||
for cl in cls.__subclasses__()
|
||||
if (
|
||||
not (
|
||||
"__slots__" not in cl.__dict__
|
||||
and hasattr(cls, "__dataclass_params__")
|
||||
and cls.__dataclass_params__.slots
|
||||
)
|
||||
and not hasattr(cls, "__attrs_base_of_slotted__")
|
||||
)
|
||||
]
|
||||
Reference in New Issue
Block a user