add updates libs

This commit is contained in:
Christoph Brandau
2026-06-18 17:21:58 +02:00
parent 91ce762570
commit 41f331d4c4
150 changed files with 8249 additions and 2550 deletions
+44 -42
View File
@@ -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
)
+2 -2
View File
@@ -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
View File
@@ -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()
]
+34 -13
View File
@@ -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(
+1 -2
View File
@@ -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,
+36
View File
@@ -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)
+16 -10
View File
@@ -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))}"
)
+108 -24
View File
@@ -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 = {}
+17 -1
View File
@@ -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:
+61 -35
View File
@@ -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]:
+2 -7
View File
@@ -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):
+2 -2
View File
@@ -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
)
+1 -1
View File
@@ -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
)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
)
+14 -3
View File
@@ -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))
+2 -2
View File
@@ -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
+9 -3
View File
@@ -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
)
+94
View File
@@ -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
+1 -1
View File
@@ -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
)
+20 -7
View File
@@ -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)
+65 -45
View File
@@ -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))
+27
View File
@@ -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__")
)
]