Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 7 additions & 2 deletions src/cattr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,9 @@ def unstructure_attrs_asdict(self, obj: Any) -> Dict[str, Any]:
omit_if_default=self.omit_if_default,
**attrib_overrides
)
self.register_unstructure_hook(obj.__class__, h)
self._unstructure_func.register_cls_list(
[(obj.__class__, h)], no_singledispatch=True
)
return h(obj)

def structure_attrs_fromdict(
Expand All @@ -524,5 +526,8 @@ def structure_attrs_fromdict(
if a.type in self.type_overrides
}
h = make_dict_structure_fn(cl, self, **attrib_overrides)
self.register_structure_hook(cl, h)
self._structure_func.register_cls_list(
[(cl, h)], no_singledispatch=True
)
# only direct dispatch so that subclasses get separately generated
return h(obj, cl)
30 changes: 23 additions & 7 deletions src/cattr/multistrategy_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,50 @@ class _DispatchNotFound(object):
class MultiStrategyDispatch(object):
"""
MultiStrategyDispatch uses a
combination of FunctionDispatch and singledispatch.
combination of exact-match dispatch, singledispatch, and FunctionDispatch.

singledispatch is attempted first. If nothing is
registered for singledispatch, or an exception occurs,
Exact match dispatch is attempted first, based on a direct
lookup of the exact class type, if the hook was registered to avoid singledispatch.
singledispatch is attempted next - it will handle subclasses of base classes using MRO
If nothing is registered for singledispatch, or an exception occurs,
the FunctionDispatch instance is then used.
"""

__slots__ = ("_function_dispatch", "_single_dispatch", "dispatch")
__slots__ = (
"_direct_dispatch",
"_function_dispatch",
"_single_dispatch",
"dispatch",
)

def __init__(self, fallback_func):
self._direct_dispatch = dict()
Comment thread
petergaultney marked this conversation as resolved.
Outdated
self._function_dispatch = FunctionDispatch()
self._function_dispatch.register(lambda _: True, fallback_func)
self._single_dispatch = singledispatch(_DispatchNotFound)
self.dispatch = lru_cache(maxsize=None)(self._dispatch)

def _dispatch(self, cl):
try:
direct_dispatch = self._direct_dispatch.get(cl)
if direct_dispatch:
Comment thread
petergaultney marked this conversation as resolved.
Outdated
return direct_dispatch
dispatch = self._single_dispatch.dispatch(cl)
if dispatch is not _DispatchNotFound:
return dispatch
except Exception:
pass
return self._function_dispatch.dispatch(cl)

def register_cls_list(self, cls_and_handler):
""" register a class to singledispatch """
def register_cls_list(
self, cls_and_handler, no_singledispatch: bool = False
):
""" register a class to direct or singledispatch """
for cls, handler in cls_and_handler:
self._single_dispatch.register(cls, handler)
if no_singledispatch:
self._direct_dispatch[cls] = handler
else:
self._single_dispatch.register(cls, handler)
self.dispatch.cache_clear()

def register_func_list(self, func_and_handler):
Expand Down
20 changes: 20 additions & 0 deletions tests/test_genconverter_inheritance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from cattr.converters import GenConverter
import attr


def test_inheritance():
@attr.s(auto_attribs=True)
class A:
i: int

@attr.s(auto_attribs=True)
class B(A):
j: int

converter = GenConverter()

# succeeds
assert A(1) == converter.structure(dict(i=1), A)

# fails
assert B(1, 2) == converter.structure(dict(i=1, j=2), B)