automated terminal push

This commit is contained in:
lenape
2025-06-27 16:06:02 +00:00
parent 511dd3b36b
commit 6a954eb013
4221 changed files with 2916190 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
"""High level strategies for converters."""
from ._class_methods import use_class_methods
from ._subclasses import include_subclasses
from ._unions import configure_tagged_union, configure_union_passthrough
__all__ = [
"configure_tagged_union",
"configure_union_passthrough",
"include_subclasses",
"use_class_methods",
]

View File

@@ -0,0 +1,64 @@
"""Strategy for using class-specific (un)structuring methods."""
from inspect import signature
from typing import Any, Callable, Optional, Type, TypeVar
from .. import BaseConverter
T = TypeVar("T")
def use_class_methods(
converter: BaseConverter,
structure_method_name: Optional[str] = None,
unstructure_method_name: Optional[str] = None,
) -> None:
"""
Configure the converter such that dedicated methods are used for (un)structuring
the instance of a class if such methods are available. The default (un)structuring
will be applied if such an (un)structuring methods cannot be found.
:param converter: The `Converter` on which this strategy is applied. You can use
:class:`cattrs.BaseConverter` or any other derived class.
:param structure_method_name: Optional string with the name of the class method
which should be used for structuring. If not provided, no class method will be
used for structuring.
:param unstructure_method_name: Optional string with the name of the class method
which should be used for unstructuring. If not provided, no class method will
be used for unstructuring.
If you want to (un)structured nested objects, just append a converter parameter
to your (un)structuring methods and you will receive the converter there.
.. versionadded:: 23.2.0
"""
if structure_method_name:
def make_class_method_structure(cl: Type[T]) -> Callable[[Any, Type[T]], T]:
fn = getattr(cl, structure_method_name)
n_parameters = len(signature(fn).parameters)
if n_parameters == 1:
return lambda v, _: fn(v)
if n_parameters == 2:
return lambda v, _: fn(v, converter)
raise TypeError("Provide a class method with one or two arguments.")
converter.register_structure_hook_factory(
lambda t: hasattr(t, structure_method_name), make_class_method_structure
)
if unstructure_method_name:
def make_class_method_unstructure(cl: Type[T]) -> Callable[[T], T]:
fn = getattr(cl, unstructure_method_name)
n_parameters = len(signature(fn).parameters)
if n_parameters == 1:
return fn
if n_parameters == 2:
return lambda self_: fn(self_, converter)
raise TypeError("Provide a method with no or one argument.")
converter.register_unstructure_hook_factory(
lambda t: hasattr(t, unstructure_method_name), make_class_method_unstructure
)

View File

@@ -0,0 +1,238 @@
"""Strategies for customizing subclass behaviors."""
from __future__ import annotations
from gc import collect
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
def _make_subclasses_tree(cl: type) -> list[type]:
return [cl] + [
sscl for scl in cl.__subclasses__() for sscl in _make_subclasses_tree(scl)
]
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
"""Whether the given class has subclasses from `given_subclasses`."""
actual = set(cl.__subclasses__())
given = set(given_subclasses)
return bool(actual & given)
def _get_union_type(cl: type, given_subclasses_tree: tuple[type]) -> type | None:
actual_subclass_tree = tuple(_make_subclasses_tree(cl))
class_tree = tuple(set(actual_subclass_tree) & set(given_subclasses_tree))
return Union[class_tree] if len(class_tree) >= 2 else None
C = TypeVar("C", bound=BaseConverter)
def include_subclasses(
cl: type,
converter: C,
subclasses: tuple[type, ...] | None = None,
union_strategy: Callable[[Any, C], Any] | None = None,
overrides: dict[str, AttributeOverride] | None = None,
) -> None:
"""
Configure the converter so that the attrs/dataclass `cl` is un/structured as if it
was a union of itself and all its subclasses that are defined at the time when this
strategy is applied.
:param cl: A base `attrs` or `dataclass` class.
:param converter: The `Converter` on which this strategy is applied. Do note that
the strategy does not work for a :class:`cattrs.BaseConverter`.
:param subclasses: A tuple of sublcasses whose ancestor is `cl`. If left as `None`,
subclasses are detected using recursively the `__subclasses__` method of `cl`
and its descendents.
:param union_strategy: A callable of two arguments passed by position
(`subclass_union`, `converter`) that defines the union strategy to use to
disambiguate the subclasses union. If `None` (the default), the automatic unique
field disambiguation is used which means that every single subclass
participating in the union must have an attribute name that does not exist in
any other sibling class.
:param overrides: a mapping of `cl` attribute names to overrides (instantiated with
:func:`cattrs.gen.override`) to customize un/structuring.
.. versionadded:: 23.1.0
.. 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.
"""
# Due to https://github.com/python-attrs/attrs/issues/1047
collect()
if subclasses is not None:
parent_subclass_tree = (cl, *subclasses)
else:
parent_subclass_tree = tuple(_make_subclasses_tree(cl))
if union_strategy is None:
_include_subclasses_without_union_strategy(
cl, converter, parent_subclass_tree, overrides
)
else:
_include_subclasses_with_union_strategy(
converter, parent_subclass_tree, union_strategy, overrides
)
def _include_subclasses_without_union_strategy(
cl,
converter: BaseConverter,
parent_subclass_tree: tuple[type],
overrides: dict[str, AttributeOverride] | None,
):
# The iteration approach is required if subclasses are more than one level deep:
for cl in parent_subclass_tree:
# We re-create a reduced union type to handle the following case:
#
# converter.structure(d, as=Child)
#
# In the above, the `as=Child` argument will be transformed to a union type of
# itself and its subtypes, that way we guarantee that the returned object will
# not be the parent.
subclass_union = _get_union_type(cl, parent_subclass_tree)
def cls_is_cl(cls, _cl=cl):
return cls is _cl
if overrides is not None:
base_struct_hook = make_dict_structure_fn(cl, converter, **overrides)
base_unstruct_hook = make_dict_unstructure_fn(cl, converter, **overrides)
else:
base_struct_hook = converter.get_structure_hook(cl)
base_unstruct_hook = converter.get_unstructure_hook(cl)
if subclass_union is None:
def struct_hook(val: dict, _, _cl=cl, _base_hook=base_struct_hook) -> cl:
return _base_hook(val, _cl)
else:
dis_fn = converter._get_dis_func(subclass_union, overrides=overrides)
def struct_hook(
val: dict,
_,
_c=converter,
_cl=cl,
_base_hook=base_struct_hook,
_dis_fn=dis_fn,
) -> cl:
"""
If val is disambiguated to the class `cl`, use its base hook.
If val is disambiguated to a subclass, dispatch on its exact runtime
type.
"""
dis_cl = _dis_fn(val)
if dis_cl is _cl:
return _base_hook(val, _cl)
return _c.structure(val, dis_cl)
def unstruct_hook(
val: parent_subclass_tree[0],
_c=converter,
_cl=cl,
_base_hook=base_unstruct_hook,
) -> dict:
"""
If val is an instance of the class `cl`, use the hook.
If val is an instance of a subclass, dispatch on its exact runtime type.
"""
if val.__class__ is _cl:
return _base_hook(val)
return _c.unstructure(val, unstructure_as=val.__class__)
# This needs to use function dispatch, using singledispatch will again
# match A and all subclasses, which is not what we want.
converter.register_structure_hook_func(cls_is_cl, struct_hook)
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
def _include_subclasses_with_union_strategy(
converter: C,
union_classes: tuple[type, ...],
union_strategy: Callable[[Any, C], Any],
overrides: dict[str, AttributeOverride] | None,
):
"""
This function is tricky because we're dealing with what is essentially a circular
reference.
We need to generate a structure hook for a class that is both:
* specific for that particular class and its own fields
* but should handle specific functions for all its descendants too
Hence the dance with registering below.
"""
parent_classes = [cl for cl in union_classes if _has_subclasses(cl, union_classes)]
if not parent_classes:
return
original_unstruct_hooks = {}
original_struct_hooks = {}
for cl in union_classes:
# In the first pass, every class gets its own unstructure function according to
# the overrides.
# We just generate the hooks, and do not register them. This allows us to
# manipulate the _already_generating set to force runtime dispatch.
already_generating.working_set = set(union_classes) - {cl}
try:
if overrides is not None:
unstruct_hook = make_dict_unstructure_fn(cl, converter, **overrides)
struct_hook = make_dict_structure_fn(cl, converter, **overrides)
else:
unstruct_hook = converter.get_unstructure_hook(cl, cache_result=False)
struct_hook = converter.get_structure_hook(cl, cache_result=False)
finally:
already_generating.working_set = set()
original_unstruct_hooks[cl] = unstruct_hook
original_struct_hooks[cl] = struct_hook
# Now that's done, we can register all the hooks and generate the
# union handler. The union handler needs them.
final_union = Union[union_classes] # type: ignore
for cl, hook in original_unstruct_hooks.items():
def cls_is_cl(cls, _cl=cl):
return cls is _cl
converter.register_unstructure_hook_func(cls_is_cl, hook)
for cl, hook in original_struct_hooks.items():
def cls_is_cl(cls, _cl=cl):
return cls is _cl
converter.register_structure_hook_func(cls_is_cl, hook)
union_strategy(final_union, converter)
unstruct_hook = converter.get_unstructure_hook(final_union)
struct_hook = converter.get_structure_hook(final_union)
for cl in union_classes:
# In the second pass, we overwrite the hooks with the union hook.
def cls_is_cl(cls, _cl=cl):
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)])
if len(subclasses) > 1:
u = Union[subclasses] # type: ignore
union_strategy(u, converter)
struct_hook = converter.get_structure_hook(u)
def sh(payload: dict, _, _u=u, _s=struct_hook) -> cl:
return _s(payload, _u)
converter.register_structure_hook_func(cls_is_cl, sh)

View File

@@ -0,0 +1,258 @@
from collections import defaultdict
from typing import Any, Callable, Dict, Literal, Type, Union
from attrs import NOTHING
from cattrs import BaseConverter
from cattrs._compat import get_newtype_base, is_literal, is_subclass, is_union_type
__all__ = [
"default_tag_generator",
"configure_tagged_union",
"configure_union_passthrough",
]
def default_tag_generator(typ: Type) -> str:
"""Return the class name."""
return typ.__name__
def configure_tagged_union(
union: Any,
converter: BaseConverter,
tag_generator: Callable[[Type], str] = default_tag_generator,
tag_name: str = "_type",
default: Union[Type, Literal[NOTHING]] = NOTHING,
) -> None:
"""
Configure the converter so that `union` (which should be a union) is
un/structured with the help of an additional piece of data in the
unstructured payload, the tag.
:param converter: The converter to apply the strategy to.
:param tag_generator: A `tag_generator` function is used to map each
member of the union to a tag, which is then included in the
unstructured payload. The default tag generator returns the name of
the class.
:param tag_name: The key under which the tag will be set in the
unstructured payload. By default, `'_type'`.
:param default: An optional class to be used if the tag information
is not present when structuring.
The tagged union strategy currently only works with the dict
un/structuring base strategy.
.. versionadded:: 23.1.0
"""
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}
if default is not NOTHING:
default_handler = converter.get_structure_hook(default)
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)
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(
val: dict,
_,
_tag_to_hook=tag_to_hook,
_tag_name=tag_name,
_dh=default_handler,
_default=default,
) -> union:
if _tag_name in val:
val = val.copy()
return _tag_to_hook[val.pop(_tag_name)](val)
return _dh(val, _default)
else:
def structure_tagged_union(
val: dict,
_,
_tag_to_hook=tag_to_hook,
_tag_name=tag_name,
_dh=default_handler,
_default=default,
) -> union:
if _tag_name in val:
return _tag_to_hook[val[_tag_name]](val)
return _dh(val, _default)
converter.register_unstructure_hook(union, unstructure_tagged_union)
converter.register_structure_hook(union, structure_tagged_union)
def configure_union_passthrough(union: Any, converter: BaseConverter) -> None:
"""
Configure the converter to support validating and passing through unions of the
provided types and their subsets.
For example, all mature JSON libraries natively support producing unions of ints,
floats, Nones, and strings. Using this strategy, a converter can be configured
to efficiently validate and pass through unions containing these types.
The most important point is that another library (in this example the JSON
library) handles producing the union, and the converter is configured to just
validate it.
Literals of provided types are also supported, and are checked by value.
NewTypes of provided types are also supported.
The strategy is designed to be O(1) in execution time, and independent of the
ordering of types in the union.
If the union contains a class and one or more of its subclasses, the subclasses
will also be included when validating the superclass.
.. versionadded:: 23.2.0
"""
args = set(union.__args__)
def make_structure_native_union(exact_type: Any) -> Callable:
# `exact_type` is likely to be a subset of the entire configured union (`args`).
literal_values = {
v for t in exact_type.__args__ if is_literal(t) for v in t.__args__
}
# We have no idea what the actual type of `val` will be, so we can't
# use it blindly with an `in` check since it might not be hashable.
# So we do an additional check when handling literals.
# Note: do no use `literal_values` here, since {0, False} gets reduced to {0}
literal_classes = {
v.__class__
for t in exact_type.__args__
if is_literal(t)
for v in t.__args__
}
non_literal_classes = {
get_newtype_base(t) or t
for t in exact_type.__args__
if not is_literal(t) and ((get_newtype_base(t) or t) in args)
}
# We augment the set of allowed classes with any configured subclasses of
# the exact subclasses.
non_literal_classes |= {
a for a in args if any(is_subclass(a, c) for c in non_literal_classes)
}
# We check for spillover - union types not handled by the strategy.
# If spillover exists and we fail to validate our types, we call
# further into the converter with the rest.
spillover = {
a
for a in exact_type.__args__
if (get_newtype_base(a) or a) not in non_literal_classes
and not is_literal(a)
}
if spillover:
spillover_type = (
Union[tuple(spillover)] if len(spillover) > 1 else next(iter(spillover))
)
def structure_native_union(
val: Any,
_: Any,
classes=non_literal_classes,
vals=literal_values,
converter=converter,
spillover=spillover_type,
) -> exact_type:
if val.__class__ in literal_classes and val in vals:
return val
if val.__class__ in classes:
return val
return converter.structure(val, spillover)
else:
def structure_native_union(
val: Any, _: Any, classes=non_literal_classes, vals=literal_values
) -> exact_type:
if val.__class__ in literal_classes and val in vals:
return val
if val.__class__ in classes:
return val
raise TypeError(f"{val} ({val.__class__}) not part of {_}")
return structure_native_union
def contains_native_union(exact_type: Any) -> bool:
"""Can we handle this type?"""
if is_union_type(exact_type):
type_args = set(exact_type.__args__)
# We special case optionals, since they are very common
# and are handled a little more efficiently by default.
if len(type_args) == 2 and type(None) in type_args:
return False
literal_classes = {
lit_arg.__class__
for t in type_args
if is_literal(t)
for lit_arg in t.__args__
}
non_literal_types = {
get_newtype_base(t) or t for t in type_args if not is_literal(t)
}
return (literal_classes | non_literal_types) & args
return False
converter.register_structure_hook_factory(
contains_native_union, make_structure_native_union
)