Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34k
bpo-44863: In TypedDict allow inherit from Generic and preserve bases#27663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
6cfdc5fc022e442fa00b8b829d8ac5762df4c5db0a34189dd0cfd6375e808afa2482ff071c1b63ce517b2f003e0964f7d3c3c0e51d34c99ecea66c40ed4326b1fcd162905f29afa5e5194138f62572930c2e1d8dd3d9456311852cdc987539bed1d46c152e7f88201b6ea95df80e9104dbbb70756b69e04a408254b50ae2ecc77265b5a98399a143079c2bb4File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4530,9 +4530,16 @@ class Point2D(TypedDict): | ||
| x: int | ||
| y: int | ||
| class Point2DGeneric(Generic[T], TypedDict): | ||
| a: T | ||
| b: T | ||
| class Bar(_typed_dict_helper.Foo, total=False): | ||
| b: int | ||
| class BarGeneric(_typed_dict_helper.FooGeneric[T], total=False): | ||
| b: int | ||
| class LabelPoint2D(Point2D, Label): ... | ||
| class Options(TypedDict, total=False): | ||
| @@ -5879,6 +5886,17 @@ def test_pickle(self): | ||
| EmpDnew = pickle.loads(ZZ) | ||
| self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane) | ||
| def test_pickle_generic(self): | ||
| point = Point2DGeneric(a=5.0, b=3.0) | ||
| for proto in range(pickle.HIGHEST_PROTOCOL + 1): | ||
| z = pickle.dumps(point, proto) | ||
| point2 = pickle.loads(z) | ||
| self.assertEqual(point2, point) | ||
| self.assertEqual(point2,{'a': 5.0, 'b': 3.0}) | ||
| ZZ = pickle.dumps(Point2DGeneric, proto) | ||
| Point2DGenericNew = pickle.loads(ZZ) | ||
| self.assertEqual(Point2DGenericNew({'a': 5.0, 'b': 3.0}), point) | ||
| def test_optional(self): | ||
| EmpD = TypedDict('EmpD',{'name': str, 'id': int}) | ||
| @@ -6063,6 +6081,124 @@ def test_get_type_hints(self): | ||
| {'a': typing.Optional[int], 'b': int} | ||
| ) | ||
| def test_get_type_hints_generic(self): | ||
| self.assertEqual( | ||
| get_type_hints(BarGeneric), | ||
| {'a': typing.Optional[T], 'b': int} | ||
| ) | ||
| class FooBarGeneric(BarGeneric[int]): | ||
| c: str | ||
| self.assertEqual( | ||
| get_type_hints(FooBarGeneric), | ||
| {'a': typing.Optional[T], 'b': int, 'c': str} | ||
| ) | ||
| def test_generic_inheritance(self): | ||
| class A(TypedDict, Generic[T]): | ||
JelleZijlstra marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| a: T | ||
| self.assertEqual(A.__bases__, (Generic, dict)) | ||
| self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) | ||
| self.assertEqual(A.__mro__, (A, Generic, dict, object)) | ||
| self.assertEqual(A.__parameters__, (T,)) | ||
| self.assertEqual(A[str].__parameters__, ()) | ||
| self.assertEqual(A[str].__args__, (str,)) | ||
| class A2(Generic[T], TypedDict): | ||
| a: T | ||
| self.assertEqual(A2.__bases__, (Generic, dict)) | ||
| self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict)) | ||
| self.assertEqual(A2.__mro__, (A2, Generic, dict, object)) | ||
| self.assertEqual(A2.__parameters__, (T,)) | ||
| self.assertEqual(A2[str].__parameters__, ()) | ||
| self.assertEqual(A2[str].__args__, (str,)) | ||
| class B(A[KT], total=False): | ||
| b: KT | ||
| self.assertEqual(B.__bases__, (Generic, dict)) | ||
| self.assertEqual(B.__orig_bases__, (A[KT],)) | ||
| self.assertEqual(B.__mro__, (B, Generic, dict, object)) | ||
| self.assertEqual(B.__parameters__, (KT,)) | ||
| self.assertEqual(B.__total__, False) | ||
| self.assertEqual(B.__optional_keys__, frozenset(['b'])) | ||
| self.assertEqual(B.__required_keys__, frozenset(['a'])) | ||
| self.assertEqual(B[str].__parameters__, ()) | ||
| self.assertEqual(B[str].__args__, (str,)) | ||
| self.assertEqual(B[str].__origin__, B) | ||
| class C(B[int]): | ||
| c: int | ||
| self.assertEqual(C.__bases__, (Generic, dict)) | ||
| self.assertEqual(C.__orig_bases__, (B[int],)) | ||
| self.assertEqual(C.__mro__, (C, Generic, dict, object)) | ||
| self.assertEqual(C.__parameters__, ()) | ||
| self.assertEqual(C.__total__, True) | ||
| self.assertEqual(C.__optional_keys__, frozenset(['b'])) | ||
| self.assertEqual(C.__required_keys__, frozenset(['a', 'c'])) | ||
| assert C.__annotations__ =={ | ||
JelleZijlstra marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| 'a': T, | ||
| 'b': KT, | ||
| 'c': int, | ||
| } | ||
| with self.assertRaises(TypeError): | ||
| C[str] | ||
| class Point3D(Point2DGeneric[T], Generic[T, KT]): | ||
| c: KT | ||
| self.assertEqual(Point3D.__bases__, (Generic, dict)) | ||
| self.assertEqual(Point3D.__orig_bases__, (Point2DGeneric[T], Generic[T, KT])) | ||
| self.assertEqual(Point3D.__mro__, (Point3D, Generic, dict, object)) | ||
| self.assertEqual(Point3D.__parameters__, (T, KT)) | ||
| self.assertEqual(Point3D.__total__, True) | ||
| self.assertEqual(Point3D.__optional_keys__, frozenset()) | ||
| self.assertEqual(Point3D.__required_keys__, frozenset(['a', 'b', 'c'])) | ||
| assert Point3D.__annotations__ =={ | ||
| 'a': T, | ||
| 'b': T, | ||
| 'c': KT, | ||
| } | ||
| self.assertEqual(Point3D[int, str].__origin__, Point3D) | ||
| with self.assertRaises(TypeError): | ||
| Point3D[int] | ||
| with self.assertRaises(TypeError): | ||
| class Point3D(Point2DGeneric[T], Generic[KT]): | ||
| c: KT | ||
| def test_implicit_any_inheritance(self): | ||
| class A(TypedDict, Generic[T]): | ||
| a: T | ||
| class B(A[KT], total=False): | ||
| b: KT | ||
| class WithImplicitAny(B): | ||
sransara marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| c: int | ||
| self.assertEqual(WithImplicitAny.__bases__, (Generic, dict,)) | ||
| self.assertEqual(WithImplicitAny.__mro__, (WithImplicitAny, Generic, dict, object)) | ||
| # Consistent with GenericTests.test_implicit_any | ||
| self.assertEqual(WithImplicitAny.__parameters__, ()) | ||
| self.assertEqual(WithImplicitAny.__total__, True) | ||
| self.assertEqual(WithImplicitAny.__optional_keys__, frozenset(['b'])) | ||
| self.assertEqual(WithImplicitAny.__required_keys__, frozenset(['a', 'c'])) | ||
| assert WithImplicitAny.__annotations__ =={ | ||
| 'a': T, | ||
| 'b': KT, | ||
| 'c': int, | ||
| } | ||
| with self.assertRaises(TypeError): | ||
| WithImplicitAny[str] | ||
| def test_non_generic_subscript(self): | ||
| # For backward compatibility, subscription works | ||
| # on arbitrary TypedDict types. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1775,7 +1775,9 @@ def __init_subclass__(cls, *args, **kwargs): | ||
| if '__orig_bases__' in cls.__dict__: | ||
| error = Generic in cls.__orig_bases__ | ||
| else: | ||
| error = Generic in cls.__bases__ and cls.__name__ != 'Protocol' | ||
| error = (Generic in cls.__bases__ and | ||
| cls.__name__ != 'Protocol' and | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the existing code did this already, but checking the class name only seems wrong. Let me see if I can find a user-visible bug based on this. Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be written as | ||
| type(cls) != _TypedDictMeta) | ||
| if error: | ||
| raise TypeError("Cannot inherit from plain Generic") | ||
| if '__orig_bases__' in cls.__dict__: | ||
| @@ -2848,14 +2850,19 @@ def __new__(cls, name, bases, ns, total=True): | ||
| Subclasses and instances of TypedDict return actual dictionaries. | ||
| """ | ||
| for base in bases: | ||
| if type(base) is not _TypedDictMeta: | ||
| if type(base) is not _TypedDictMeta and base is not Generic: | ||
| raise TypeError('cannot inherit from both a TypedDict type ' | ||
| 'and a non-TypedDict base class') | ||
| tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns) | ||
| if any(issubclass(b, Generic) for b in bases): | ||
| generic_base = (Generic,) | ||
| else: | ||
| generic_base = () | ||
| tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns) | ||
| annotations ={} | ||
| own_annotations = ns.get('__annotations__',{}) | ||
| own_annotation_keys = set(own_annotations.keys()) | ||
| msg = "TypedDict('Name',{f0: t0, f1: t1, ...}); each t must be a type" | ||
| own_annotations ={ | ||
| n: _type_check(tp, msg, module=tp_dict.__module__) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Allow :class:`~typing.TypedDict` subclasses to also include | ||
| :class:`~typing.Generic` as a base class in class based syntax. Thereby allowing | ||
| the user to define a generic ``TypedDict``, just like a user-defined generic but | ||
| with ``TypedDict`` semantics. |
Uh oh!
There was an error while loading. Please reload this page.