From f5700534d7bee2a3fd7789c30f7099fa31d9f2ec Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 14 May 2025 06:24:33 -0700 Subject: [PATCH] gh-133701: Fix incorrect `__annotations__` on TypedDict defined under PEP 563 (GH-133772) (cherry picked from commit 9836503b48e047db117b3bef3a812c40ed3e988a) Co-authored-by: Jelle Zijlstra --- Lib/test/support/__init__.py | 4 ++- Lib/test/test_typing.py | 30 +++++++++++++++++++ Lib/typing.py | 8 +++-- ...-05-09-08-49-03.gh-issue-133701.KI8tGz.rst | 3 ++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index c74c3a3190947b..9b6e80fdad9747 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -696,9 +696,11 @@ def sortdict(dict): return "{%s}" % withcommas -def run_code(code: str) -> dict[str, object]: +def run_code(code: str, extra_names: dict[str, object] | None = None) -> dict[str, object]: """Run a piece of code after dedenting it, and return its global namespace.""" ns = {} + if extra_names: + ns.update(extra_names) exec(textwrap.dedent(code), ns) return ns diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a4fab1d5e14805..b34bfcc90b1577 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -8538,6 +8538,36 @@ class Child(Base1, Base2): self.assertEqual(Child.__required_keys__, frozenset(['a'])) self.assertEqual(Child.__optional_keys__, frozenset()) + def test_inheritance_pep563(self): + def _make_td(future, class_name, annos, base, extra_names=None): + lines = [] + if future: + lines.append('from __future__ import annotations') + lines.append('from typing import TypedDict') + lines.append(f'class {class_name}({base}):') + for name, anno in annos.items(): + lines.append(f' {name}: {anno}') + code = '\n'.join(lines) + ns = run_code(code, extra_names) + return ns[class_name] + + for base_future in (True, False): + for child_future in (True, False): + with self.subTest(base_future=base_future, child_future=child_future): + base = _make_td( + base_future, "Base", {"base": "int"}, "TypedDict" + ) + self.assertIsNotNone(base.__annotate__) + child = _make_td( + child_future, "Child", {"child": "int"}, "Base", {"Base": base} + ) + base_anno = ForwardRef("int", module="builtins") if base_future else int + child_anno = ForwardRef("int", module="builtins") if child_future else int + self.assertEqual(base.__annotations__, {'base': base_anno}) + self.assertEqual( + child.__annotations__, {'child': child_anno, 'base': base_anno} + ) + def test_required_notrequired_keys(self): self.assertEqual(NontotalMovie.__required_keys__, frozenset({"title"})) diff --git a/Lib/typing.py b/Lib/typing.py index 0695a825283c7a..30d53cb46f8a18 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3087,14 +3087,16 @@ def __new__(cls, name, bases, ns, total=True): else: generic_base = () + ns_annotations = ns.pop('__annotations__', None) + tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns) if not hasattr(tp_dict, '__orig_bases__'): tp_dict.__orig_bases__ = bases - if "__annotations__" in ns: + if ns_annotations is not None: own_annotate = None - own_annotations = ns["__annotations__"] + own_annotations = ns_annotations elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None: own_annotations = _lazy_annotationlib.call_annotate_function( own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict @@ -3165,7 +3167,7 @@ def __annotate__(format): if base_annotate is None: continue base_annos = _lazy_annotationlib.call_annotate_function( - base.__annotate__, format, owner=base) + base_annotate, format, owner=base) annos.update(base_annos) if own_annotate is not None: own = _lazy_annotationlib.call_annotate_function( diff --git a/Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst b/Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst new file mode 100644 index 00000000000000..163d9b331d1f8a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst @@ -0,0 +1,3 @@ +Fix bug where :class:`typing.TypedDict` classes defined under ``from +__future__ import annotations`` and inheriting from another ``TypedDict`` +had an incorrect ``__annotations__`` attribute.