Skip to content

Commit

Permalink
Tin/copy (#284)
Browse files Browse the repository at this point in the history
* Copying WIP

* More copy work

* More copy

* Add test for copying

* Finish up copy

* Remove unused imports

* Add a HISTORY fragment

* Add copying docs
  • Loading branch information
Tinche authored Sep 11, 2022
1 parent b2be538 commit 2c65938
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 9 deletions.
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

0 comments on commit 2c65938

Please sign in to comment.