@@ -543,3 +543,74 @@ class Nt(t.NamedTuple):
543
543
544
544
assert mapper .serialize (Nt (1 , "2" ), Nt ) == {"a" : 1 , "b" : "2" }
545
545
assert mapper .deserialize ({"a" : 1 , "b" : "2" }, Nt ) == Nt (1 , "2" )
546
+
547
+
548
+ T_Page = t .TypeVar ("T_Page" , bound = "Page[t.Any]" )
549
+
550
+
551
+ @dataclasses .dataclass
552
+ class Page (t .Generic [T_Page ]):
553
+ name : str
554
+ children : t .List [T_Page ]
555
+
556
+
557
+ @dataclasses .dataclass
558
+ class SpecificPage (Page ["SpecificPage" ]):
559
+ pass
560
+
561
+
562
+ def test__parameterized_base_type_with_forward_ref () -> None :
563
+ mapper = make_mapper ([SchemaConverter (), PlainDatatypeConverter (), CollectionConverter ()])
564
+ payload = {"name" : "root" , "children" : [{"name" : "child" , "children" : []}]}
565
+ expected = SpecificPage ("root" , [SpecificPage ("child" , [])])
566
+ assert mapper .deserialize (payload , SpecificPage ) == expected
567
+ mapper .serialize (expected , SpecificPage ) == payload
568
+
569
+
570
+ def test__parameterized_self_seferential_generic_cannot_be_processed () -> None :
571
+ """
572
+ A self-referential generic type (or nested generic type) cannot be properly parameterized in Mypy. [1]
573
+
574
+ For the page type above, if you would like to use it as-is, you would need to infinitely parameterized it,
575
+ as in `Page[Page[Page[Page[Page[... etc]]]]]`. As far as I am aware (@NiklasRosenstein, 2023.06.10), this can
576
+ only be solved by creating a dedicated specialized type that is self-referential via its base-class:
577
+
578
+ ```py
579
+ class MyPage(Page["MyPage"]):
580
+ pass
581
+ ```
582
+
583
+ When we encounter a partially parameterized type like `Page[Page]` (the inner `Page` is missing a type parameter),
584
+ databind will not have a way of knowing the value for the type parameter of the inner `Page` and will therefore
585
+ fail with an error like this:
586
+
587
+ ```
588
+ databind.core.converter.NoMatchingConverter: no deserializer for `TypeHint(~T_Page)` and payload of type `dict`
589
+ ```
590
+
591
+ [1]: https://github.com/python/mypy/issues/13693
592
+ """
593
+
594
+ mapper = make_mapper ([SchemaConverter (), PlainDatatypeConverter (), CollectionConverter ()])
595
+
596
+ payload = {
597
+ "name" : "root" ,
598
+ "children" : [
599
+ {
600
+ "name" : "child" ,
601
+ "children" : [
602
+ # This is the level at which the deserialization will fail.
603
+ {"name" : "grandchild" , "children" : []}
604
+ ],
605
+ }
606
+ ],
607
+ }
608
+
609
+ with pytest .raises (NoMatchingConverter ) as excinfo :
610
+ mapper .deserialize (payload , Page [Page ]) # type: ignore[type-arg]
611
+ assert str (excinfo .value ).splitlines ()[0 ] == "no deserializer for `TypeHint(~T_Page)` and payload of type `dict`"
612
+
613
+ # It works with an additional level of page parameterization.
614
+ assert mapper .deserialize (payload , Page [Page [Page [Page ]]]) == Page ( # type: ignore[type-arg]
615
+ "root" , [Page ("child" , [Page ("grandchild" , [])])]
616
+ )
0 commit comments