diff --git a/.changelog/_unreleased.toml b/.changelog/_unreleased.toml new file mode 100644 index 0000000..314ee6b --- /dev/null +++ b/.changelog/_unreleased.toml @@ -0,0 +1,5 @@ +[[entries]] +id = "693674ea-b2b2-4733-bce6-4d5bae59b164" +type = "fix" +description = "Fix #66: dataclasses inheriting from uninstantiated Generic did not get all their fields serialized" +author = "@rhaps0dy" diff --git a/databind/src/databind/core/schema.py b/databind/src/databind/core/schema.py index dafd295..85b2147 100644 --- a/databind/src/databind/core/schema.py +++ b/databind/src/databind/core/schema.py @@ -245,7 +245,7 @@ class A(Generic[T]): pass # Continue with the base classes. - for base in hint.bases or hint.type.__bases__: + for base in (*hint.bases, *hint.type.__bases__): base_hint = TypeHint(base, source=hint.type).evaluate().parameterize(parameter_map) assert isinstance(base_hint, ClassTypeHint), f"nani? {base_hint}" if dataclasses.is_dataclass(base_hint.type): diff --git a/databind/src/databind/core/tests/schema_test.py b/databind/src/databind/core/tests/schema_test.py index 8387311..406d759 100644 --- a/databind/src/databind/core/tests/schema_test.py +++ b/databind/src/databind/core/tests/schema_test.py @@ -456,3 +456,27 @@ def test_parse_dataclass_with_forward_ref() -> None: ClassWithForwardRef, ClassWithForwardRef, ) + + +UnboundTypeVar = t.TypeVar("UnboundTypeVar") + + +@dataclasses.dataclass +class GenericClass(t.Generic[UnboundTypeVar]): + a_field: int + + +@dataclasses.dataclass +class InheritGeneric(GenericClass): # type: ignore[type-arg] + b_field: str + + +def test_schema_generic_dataclass() -> None: + """Regression test for #66: dataclasses inheriting from Generic with an uninstantiated TypeVar don't get their + parents' fields. + """ + assert convert_dataclass_to_schema(InheritGeneric) == Schema( + {"a_field": Field(TypeHint(int), True), "b_field": Field(TypeHint(str), True)}, + InheritGeneric, + InheritGeneric, + ) diff --git a/databind/src/databind/json/tests/converters_test.py b/databind/src/databind/json/tests/converters_test.py index 0de421c..41b902f 100644 --- a/databind/src/databind/json/tests/converters_test.py +++ b/databind/src/databind/json/tests/converters_test.py @@ -713,3 +713,31 @@ def of(cls, v: str) -> "MyCls": mapper = make_mapper([JsonConverterSupport()]) assert mapper.serialize(MyCls(), MyCls) == "MyCls" assert mapper.deserialize("MyCls", MyCls) == MyCls() + + +UnboundTypeVar = t.TypeVar("UnboundTypeVar") + + +@dataclasses.dataclass +class GenericClass(t.Generic[UnboundTypeVar]): + a_field: int + + +@dataclasses.dataclass +class InheritGeneric(GenericClass): # type: ignore[type-arg] + b_field: str + + +@pytest.mark.parametrize("direction", (Direction.SERIALIZE, Direction.DESERIALIZE)) +def test_convert_generic_dataclass(direction: Direction) -> None: + """Regression test for #66: dataclasses inheriting from Generic with an uninstantiated TypeVar don't get their + parents' fields. + """ + mapper = make_mapper([SchemaConverter(), PlainDatatypeConverter()]) + + if direction == Direction.SERIALIZE: + obj = InheritGeneric(2, "hi") + assert mapper.convert(direction, obj, InheritGeneric) == {"a_field": obj.a_field, "b_field": obj.b_field} + else: + obj = InheritGeneric(4, "something") + assert mapper.convert(direction, {"a_field": obj.a_field, "b_field": obj.b_field}, InheritGeneric) == obj