Skip to content
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

bpo-44863: In TypedDict allow inherit from Generic and preserve bases #27663

Merged
merged 40 commits into from May 3, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6cfdc5f
Allow TypedDict to inherit from Generic
sransara Aug 7, 2021
c022e44
TypedDict preserve MRO
sransara Aug 7, 2021
2fa00b8
Fix base classes to be just generic and dict
sransara Sep 3, 2021
b829d8a
Simplify addiotion of Generic base class
sransara Sep 3, 2021
c5762df
Fully proxy origin by using types.GenericAlias
sransara Sep 3, 2021
4c5db0a
Add NEWS blurb
sransara Sep 3, 2021
34189dd
Add better NEWS blurb
sransara Sep 3, 2021
0cfd637
Add more testcases
sransara Sep 3, 2021
5e808af
Add implicit any test case
sransara Sep 4, 2021
a2482ff
Fix issue with plain generic inheritance
sransara Sep 4, 2021
071c1b6
Include bases assertion
sransara Sep 4, 2021
3ce517b
Update NEWS blurb with better formatting
sransara Sep 5, 2021
2f003e0
Revert overriding of getitem and not proxy dunders
sransara Sep 5, 2021
964f7d3
Use alternative way to find if Generic base is needed
sransara Sep 6, 2021
c3c0e51
Make it clear when Generic base is included
sransara Sep 6, 2021
d34c99e
Add getitem:so TD is subscriptable only if Generic
sransara Sep 6, 2021
cea66c4
Fix test consistency for empty params
sransara Sep 6, 2021
0ed4326
Test for adding new generic arg in child class
sransara Sep 6, 2021
b1fcd16
Update Lib/typing.py
sransara Apr 30, 2022
2905f29
Merge branch 'main' into py-generic-typeddict-simple
serhiy-storchaka Apr 30, 2022
afa5e51
Fix merge error.
serhiy-storchaka Apr 30, 2022
94138f6
Fix trailing spaces.
serhiy-storchaka Apr 30, 2022
2572930
Merge branch 'main' into py-generic-typeddict-simple
serhiy-storchaka May 1, 2022
c2e1d8d
Fix indentation
sransara May 2, 2022
d3d9456
Remove trailing commas in tuples
sransara May 2, 2022
311852c
Add test with flipped bases
sransara May 2, 2022
dc98753
Move implicit any test to on its own case
sransara May 2, 2022
9bed1d4
Add checks for orig_bases and mro
sransara May 2, 2022
6c152e7
Remove specialization from generic get_type_hints
sransara May 2, 2022
f88201b
Check type hints of inherited generic typeddict
sransara May 2, 2022
6ea95df
Remove unused statement
sransara May 2, 2022
80e9104
Add class method through the metaclass
sransara May 2, 2022
dbbb707
Fix generic base when inherited from implicit any
sransara May 2, 2022
56b69e0
Fix whitespacing with reindent.py
sransara May 2, 2022
4a40825
fix tuple
JelleZijlstra May 2, 2022
4b50ae2
remove unnecessary __class_getitem__ override
JelleZijlstra May 2, 2022
ecc7726
Merge remote-tracking branch 'upstream/main' into py-generic-typeddic…
JelleZijlstra May 2, 2022
5b5a983
docs
JelleZijlstra May 2, 2022
99a1430
Merge branch 'main' into py-generic-typeddict-simple
JelleZijlstra May 2, 2022
79c2bb4
Merge branch 'main' into py-generic-typeddict-simple
JelleZijlstra May 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion Lib/test/_typed_dict_helper.py
Expand Up @@ -10,9 +10,14 @@ class Bar(_typed_dict_helper.Foo, total=False):

from __future__ import annotations

from typing import Optional, TypedDict
from typing import Generic, Optional, TypedDict, TypeVar

OptionalIntType = Optional[int]

class Foo(TypedDict):
a: OptionalIntType

OptionableT = TypeVar("OptionableT")

class FooGeneric(TypedDict, Generic[OptionableT]):
a: Optional[OptionableT]
29 changes: 26 additions & 3 deletions Lib/test/test_typing.py
Expand Up @@ -2943,13 +2943,22 @@ def __add__(self, other):

Label = TypedDict('Label', [('label', str)])

TDG = TypeVar("TDG")

class Point2D(TypedDict):
x: int
y: int

class Point2DGeneric(Generic[TDG], TypedDict):
a: TDG
b: TDG

class Bar(_typed_dict_helper.Foo, total=False):
b: int

class BarGeneric(_typed_dict_helper.FooGeneric[TDG], total=False):
b: int

class LabelPoint2D(Point2D, Label): ...

class Options(TypedDict, total=False):
Expand Down Expand Up @@ -4070,7 +4079,7 @@ def test_basics_functional_syntax(self):
self.assertEqual(jim['id'], 1)
self.assertEqual(Emp.__name__, 'Emp')
self.assertEqual(Emp.__module__, __name__)
self.assertEqual(Emp.__bases__, (dict,))
self.assertIn(dict, Emp.__bases__)
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
self.assertEqual(Emp.__total__, True)

Expand All @@ -4085,7 +4094,7 @@ def test_basics_keywords_syntax(self):
self.assertEqual(jim['id'], 1)
self.assertEqual(Emp.__name__, 'Emp')
self.assertEqual(Emp.__module__, __name__)
self.assertEqual(Emp.__bases__, (dict,))
self.assertIn(dict, Emp.__bases__)
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
self.assertEqual(Emp.__total__, True)

Expand Down Expand Up @@ -4135,7 +4144,7 @@ def test_py36_class_syntax_usage(self):
self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D')
self.assertEqual(LabelPoint2D.__module__, __name__)
self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str})
self.assertEqual(LabelPoint2D.__bases__, (dict,))
self.assertIn(dict, LabelPoint2D.__bases__)
self.assertEqual(LabelPoint2D.__total__, True)
self.assertNotIsSubclass(LabelPoint2D, typing.Sequence)
not_origin = Point2D(x=0, y=1)
Expand All @@ -4157,6 +4166,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)

Expand Down Expand Up @@ -4228,6 +4248,9 @@ def test_get_type_hints(self):
{'a': typing.Optional[int], 'b': int}
)

def test_get_type_hints_generic(self):
assert set(get_type_hints(BarGeneric)) == {'a', 'b'}
sransara marked this conversation as resolved.
Show resolved Hide resolved


class IOTests(BaseTestCase):

Expand Down
12 changes: 10 additions & 2 deletions Lib/typing.py
Expand Up @@ -2252,10 +2252,18 @@ 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 not (type(base) is _TypedDictMeta or base is Generic):
sransara marked this conversation as resolved.
Show resolved Hide resolved
raise TypeError('cannot inherit from both a TypedDict type '
'and a non-TypedDict base class')
tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns)

generic_base = tuple(b for b in bases if b is Generic)
base_typevars = _collect_type_vars(ns.get("__orig_bases__", ()))
if not generic_base and base_typevars:
# If not directly inheriting from a Generic
# but inheriting from a generic TypedDict
generic_alias = Generic.__class_getitem__(base_typevars),
generic_base = types.resolve_bases(generic_alias)
tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns)
sransara marked this conversation as resolved.
Show resolved Hide resolved
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved

annotations = {}
own_annotations = ns.get('__annotations__', {})
Expand Down