Skip to content

Commit

Permalink
fix MultiStrategyDispatch to work with new GenConverter and attrs inh…
Browse files Browse the repository at this point in the history
…eritance

Because the GenConverter specifically handles generating code for
attrs classes, and because it registers hooks that don't require
function dispatch, singledispatch was preventing subclasses from
having code generated for them, because they would trigger the
previously-generated base class's structure/unstructure code.

This changes MultiStrategyDispatch to allow for a class-based hook to
specifically avoid single-dispatch. Since we know that a given attrs
class can always have code generated for it, we don't really want to
share dispatch across subclasses - like the original Converter, we
want to make sure we're structuring each individual attrs class as its
own separate type.
  • Loading branch information
Peter Gaultney committed Jan 24, 2021
1 parent 61c9445 commit a25ca9a
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 9 deletions.
7 changes: 5 additions & 2 deletions src/cattr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ 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)])
return h(obj)

def structure_attrs_fromdict(
Expand All @@ -524,5 +524,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()
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:
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)

0 comments on commit a25ca9a

Please sign in to comment.