Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ History
* Fix `Converter.register_structure_hook_factory` and `cattrs.gen.make_dict_unstructure_fn` type annotations.
(`#281 <https://github.com/python-attrs/cattrs/issues/281>`_)
* Expose all error classes in the `cattr.errors` namespace. Note that it is deprecated, just use `cattrs.errors`. (`#252 <https://github.com/python-attrs/cattrs/issues/252>`_)
* ``cattrs.Converter`` and ``cattrs.BaseConverter`` can now copy themselves using the ``copy`` method.
(`#284 <https://github.com/python-attrs/cattrs/pull/284>`_)

22.1.0 (2022-04-03)
-------------------
Expand Down
5 changes: 4 additions & 1 deletion docs/converters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Converters
==========

All ``cattrs`` functionality is exposed through a ``cattrs.Converter`` object.
All ``cattrs`` functionality is exposed through a :py:class:`cattrs.Converter` object.
Global ``cattrs`` functions, such as ``cattrs.unstructure()``, use a single
global converter. Changes done to this global converter, such as registering new
``structure`` and ``unstructure`` hooks, affect all code using the global
Expand Down Expand Up @@ -38,6 +38,9 @@ Currently, a converter contains the following state:
* a ``dict_factory`` callable, used for creating ``dicts`` when dumping
``attrs`` classes using ``AS_DICT``.

Converters may be cloned using the :py:attr:`cattrs.Converter.copy` method.
The new copy may be changed through the `copy` arguments, but will retain all manually registered hooks from the original.

``cattrs.Converter``
--------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Contents:

readme
installation
usage
converters
usage
structuring
unstructuring
validation
Expand Down
89 changes: 86 additions & 3 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def __init__(
# Per-instance register of to-attrs converters.
# Singledispatch dispatches based on the first argument, so we
# store the function and switch the arguments in self.loads.
self._structure_func = MultiStrategyDispatch(self._structure_error)
self._structure_func = MultiStrategyDispatch(BaseConverter._structure_error)
self._structure_func.register_func_list(
[
(lambda cl: cl is Any or cl is Optional or cl is None, lambda v, _: v),
Expand Down Expand Up @@ -309,7 +309,8 @@ def _unstructure_enum(self, obj):
"""Convert an enum to its value."""
return obj.value

def _unstructure_identity(self, obj):
@staticmethod
def _unstructure_identity(obj):
"""Just pass it through."""
return obj

Expand Down Expand Up @@ -340,7 +341,8 @@ def _unstructure_union(self, obj):

# Python primitives to classes.

def _structure_error(self, _, cl):
@staticmethod
def _structure_error(_, cl):
"""At the bottom of the condition stack, we explode if we can't handle it."""
msg = "Unsupported type: {0!r}. Register a structure hook for " "it.".format(cl)
raise StructureHandlerNotFoundError(msg, type_=cl)
Expand Down Expand Up @@ -615,6 +617,38 @@ def _get_dis_func(union) -> Callable[..., Type]:
)
return create_uniq_field_dis_func(*union_types)

def __deepcopy__(self, _) -> "BaseConverter":
return self.copy()

def copy(
self,
dict_factory: Optional[Callable[[], Any]] = None,
unstruct_strat: Optional[UnstructureStrategy] = None,
prefer_attrib_converters: Optional[bool] = None,
detailed_validation: Optional[bool] = None,
) -> "BaseConverter":
res = self.__class__(
dict_factory if dict_factory is not None else self._dict_factory,
unstruct_strat
if unstruct_strat is not None
else (
UnstructureStrategy.AS_DICT
if self._unstructure_attrs == self.unstructure_attrs_asdict
else UnstructureStrategy.AS_TUPLE
),
prefer_attrib_converters
if prefer_attrib_converters is not None
else self._prefer_attrib_converters,
detailed_validation
if detailed_validation is not None
else self.detailed_validation,
)

self._unstructure_func.copy_to(res._unstructure_func)
self._structure_func.copy_to(res._structure_func)

return res


class Converter(BaseConverter):
"""A converter which generates specialized un/structuring functions."""
Expand All @@ -624,6 +658,8 @@ class Converter(BaseConverter):
"forbid_extra_keys",
"type_overrides",
"_unstruct_collection_overrides",
"_struct_copy_skip",
"_unstruct_copy_skip",
)

def __init__(
Expand Down Expand Up @@ -736,6 +772,10 @@ def __init__(
lambda t: get_newtype_base(t) is not None, self.get_structure_newtype
)

# We keep these so we can more correctly copy the hooks.
self._struct_copy_skip = self._structure_func.get_num_fns()
self._unstruct_copy_skip = self._unstructure_func.get_num_fns()

def get_structure_newtype(self, type: Type[T]) -> Callable[[Any, Any], T]:
base = get_newtype_base(type)
return self._structure_func.dispatch(base)
Expand Down Expand Up @@ -836,5 +876,48 @@ def gen_structure_mapping(self, cl: Any):
self._structure_func.register_cls_list([(cl, h)], direct=True)
return h

def copy(
self,
dict_factory: Optional[Callable[[], Any]] = None,
unstruct_strat: Optional[UnstructureStrategy] = None,
omit_if_default: Optional[bool] = None,
forbid_extra_keys: Optional[bool] = None,
type_overrides: Optional[Mapping[Type, AttributeOverride]] = None,
unstruct_collection_overrides: Optional[Mapping[Type, Callable]] = None,
prefer_attrib_converters: Optional[bool] = None,
detailed_validation: Optional[bool] = None,
) -> "Converter":
res = self.__class__(
dict_factory if dict_factory is not None else self._dict_factory,
unstruct_strat
if unstruct_strat is not None
else (
UnstructureStrategy.AS_DICT
if self._unstructure_attrs == self.unstructure_attrs_asdict
else UnstructureStrategy.AS_TUPLE
),
omit_if_default if omit_if_default is not None else self.omit_if_default,
forbid_extra_keys
if forbid_extra_keys is not None
else self.forbid_extra_keys,
type_overrides if type_overrides is not None else self.type_overrides,
unstruct_collection_overrides
if unstruct_collection_overrides is not None
else self._unstruct_collection_overrides,
prefer_attrib_converters
if prefer_attrib_converters is not None
else self._prefer_attrib_converters,
detailed_validation
if detailed_validation is not None
else self.detailed_validation,
)

self._unstructure_func.copy_to(
res._unstructure_func, skip=self._unstruct_copy_skip
)
self._structure_func.copy_to(res._structure_func, skip=self._struct_copy_skip)

return res


GenConverter = Converter
16 changes: 15 additions & 1 deletion src/cattrs/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def _dispatch(self, cl):

return self._function_dispatch.dispatch(cl)

def register_cls_list(self, cls_and_handler, direct: bool = False):
def register_cls_list(self, cls_and_handler, direct: bool = False) -> None:
"""Register a class to direct or singledispatch."""
for cls, handler in cls_and_handler:
if direct:
Expand Down Expand Up @@ -89,6 +89,14 @@ def clear_cache(self):
self._direct_dispatch.clear()
self.dispatch.cache_clear()

def get_num_fns(self) -> int:
return self._function_dispatch.get_num_fns()

def copy_to(self, other: "MultiStrategyDispatch", skip: int = 0):
self._function_dispatch.copy_to(other._function_dispatch, skip=skip)
for cls, fn in self._single_dispatch.registry.items():
other._single_dispatch.register(cls, fn)


@attr.s(slots=True)
class FunctionDispatch:
Expand Down Expand Up @@ -125,3 +133,9 @@ def dispatch(self, typ):
raise StructureHandlerNotFoundError(
f"unable to find handler for {typ}", type_=typ
)

def get_num_fns(self) -> int:
return len(self._handler_pairs)

def copy_to(self, other: "FunctionDispatch", skip: int = 0):
other._handler_pairs.extend(self._handler_pairs[skip:])
5 changes: 5 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import os

from hypothesis import HealthCheck, settings
from hypothesis.strategies import just, one_of

from cattrs import UnstructureStrategy

settings.register_profile(
"CI", settings(suppress_health_check=[HealthCheck.too_slow]), deadline=None
)

if "CI" in os.environ:
settings.load_profile("CI")

unstructure_strats = one_of(just(s) for s in UnstructureStrategy)
Loading