automated terminal push
This commit is contained in:
@@ -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",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
)
|
@@ -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)
|
@@ -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
|
||||
)
|
Reference in New Issue
Block a user