diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e960c99..2f20fba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,11 +2,11 @@ CHANGELOG ========= -3.11.1.2 +3.11.1.3 ======== * Fixed bug in Union of literals serialisation. -* Fixed bug in concrete subclasses of generic classes. +* Fixed parsing and serialisation of concrete subclasses of generic types. 3.11.0.0 diff --git a/docs/conf.py b/docs/conf.py index 913f8f6..4e4fb53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # The short X.Y version. version = '3.11' # The full version, including alpha/beta/rc tags. -release = '3.11.1.2' +release = '3.11.1.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index dc1cea9..25cb86b 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def requirements(at_path: Path): # ---------------------------- setup(name='typeit', - version='3.11.1.2', + version='3.11.1.3', description='typeit brings typed data into your project', long_description=README, classifiers=[ diff --git a/tests/test_generics.py b/tests/test_generics.py index d3d6df8..8cf55bc 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -1,5 +1,8 @@ from dataclasses import dataclass from typing import Generic, TypeVar, NamedTuple, Sequence +import json + +from pvectorc import pvector import typeit @@ -7,31 +10,51 @@ A = TypeVar('A') @dataclass(frozen=True, slots=True) -class X(Generic[A]): +class Entity(Generic[A]): + """ A generic representation of a database-stored entity. + """ pk: int entry: A -def test_generic(): - class Item(NamedTuple): - value: str +class InnerEntry(NamedTuple): + entry_id: int + entry_name: str + + +class Item(NamedTuple): + item_id: int + inner: InnerEntry + - class Concrete(X[Item]): - pass +class PersistedItem(Entity[Item]): + pass - class Wrapper(NamedTuple): - vals: Sequence[Concrete] - mk_wrapper, serialize_wrapper = typeit.TypeConstructor ^ Wrapper +class DatabaseResponse(NamedTuple): + name: str + items: Sequence[PersistedItem] = pvector() + + +def test_generic(): + + mk_response, serialize_response = typeit.TypeConstructor ^ DatabaseResponse serialized = { - "vals": [ + "name": "response", + "items": [ { "pk": 1, "entry": { - "value": "item value" + "item_id": 1, + "inner": { + "entry_id": 2, + "entry_name": "entry_name", + } } } ] } - assert serialize_wrapper(mk_wrapper(serialized)) == serialized + x = serialize_response(mk_response(serialized)) + json.dumps(x) + assert x == serialized diff --git a/typeit/parser/__init__.py b/typeit/parser/__init__.py index 6cc1bfd..553a131 100644 --- a/typeit/parser/__init__.py +++ b/typeit/parser/__init__.py @@ -107,6 +107,10 @@ def _maybe_node_for_primitive( return schema.nodes.SchemaNode(schema_type), memo, forward_refs +def is_type_var_placeholder(t) -> bool: + return isinstance(t, TypeVar) + + def _maybe_node_for_type_var( typ: Type, overrides: OverridesT, @@ -118,7 +122,7 @@ def _maybe_node_for_type_var( as TypeVar. Since it's an indicator of a generic collection, we can treat it as typing.Any. """ - if isinstance(typ, TypeVar): + if is_type_var_placeholder(typ): return _maybe_node_for_primitive(Any, overrides, memo, forward_refs) return None, memo, forward_refs @@ -487,25 +491,38 @@ def _maybe_node_for_user_type( hints_source = get_origin_39(typ) or typ # now we need to map generic type variables to the bound class types, - # e.g. we map Generic[T,U,V, ...] to actual types of MyClass[int, float, str, ...] + # e.g. we map Entity[T,U,V, ...] to actual types of Entity[int, float, str, ...] generic_repr = insp.get_generic_bases(hints_source) - generic_vars_ordered = (insp.get_args(x)[0] for x in generic_repr) + generic_vars_ordered = [insp.get_args(x)[0] for x in generic_repr] bound_type_args = insp.get_args(typ) type_var_to_type = pmap(zip(generic_vars_ordered, bound_type_args)) - # Resolve type hints. - # We have to match all generic type parameter placeholders with the actual types passed as implementations - # of the interface. However, we need to keep in mind that not all attributes of the generic type have generic - # placeholders. Hence, in places where we cannot find the generic placeholder name, we just assume that there's - # no placeholder, and therefore ``type_var`` is automatically a concrete type - attribute_hints = [ - ( field_name - , type_var_to_type.get(type_var_or_concrete_type) or type_var_or_concrete_type - ) - for field_name, type_var_or_concrete_type in ( - (x, raw_type) - for x, _resolved_type, raw_type in get_type_attribute_info(hints_source) - ) - ] + if type_var_to_type: + # Resolve type hints. + # We have to match all generic type parameter placeholders with the actual types passed as implementations + # of the interface. However, we need to keep in mind that not all attributes of the generic type have generic + # placeholders. Hence, in places where we cannot find the generic placeholder name, we just assume that there's + # no placeholder, and therefore ``type_var`` is automatically a concrete type + attribute_hints = [ + ( field_name + , type_var_to_type.get(type_var_or_concrete_type) or type_var_or_concrete_type + ) + for field_name, type_var_or_concrete_type in ( + (x, raw_type) + for x, _resolved_type, raw_type in get_type_attribute_info(hints_source) + ) + ] + else: + # Here we have a situation with a concrete type after the generic type is clarified + # into a concrete type, i.e. when we have Entity[T] and later: + # class MyType(Entity[MyOtherType]): ... + # + # In this situation type_var_to_type will be empty, and we need to infer attributes + # of MyType via concrete types + clarified = iter(generic_vars_ordered) + attribute_hints = [ + (attr_name, next(clarified) if is_type_var_placeholder(raw_type) else raw_type) + for attr_name, _resolved_type, raw_type in get_type_attribute_info(hints_source) + ] # Generic types should not have default values defaults_source = lambda: () # Overrides should be the same as class-based ones, as Generics are not NamedTuple classes,