From d43e9ac09c9326ce70641044a8ef3896d2a0d78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0brahim=20Sait=20Akar=C3=A7e=C5=9Fme?= <72226290+saitakarcesme@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:46:00 +0200 Subject: [PATCH] Preserve TypedDict annotations on Python 3.14 --- .github/workflows/test.yml | 1 + mypy_extensions.py | 8 +++++-- tests/testextensions.py | 47 ++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7168992..845f614 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,7 @@ jobs: "3.11", "3.12", "3.13", + "3.14", ] steps: diff --git a/mypy_extensions.py b/mypy_extensions.py index 1910000..74e8dfc 100644 --- a/mypy_extensions.py +++ b/mypy_extensions.py @@ -71,11 +71,15 @@ def __new__(cls, name, bases, ns, total=True, _from_functional_call=False): ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new tp_dict = super(_TypedDictMeta, cls).__new__(cls, name, (dict,), ns) - anns = ns.get('__annotations__', {}) + if sys.version_info >= (3, 14): + import annotationlib + anns = annotationlib.get_annotations(tp_dict, format=annotationlib.Format.FORWARDREF) + else: + anns = ns.get('__annotations__', {}) msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" anns = {n: _type_check(tp, msg) for n, tp in anns.items()} for base in bases: - anns.update(base.__dict__.get('__annotations__', {})) + anns.update(getattr(base, '__annotations__', {})) tp_dict.__annotations__ = anns if not hasattr(tp_dict, '__total__'): tp_dict.__total__ = total diff --git a/tests/testextensions.py b/tests/testextensions.py index 72a4a49..01ad99d 100644 --- a/tests/testextensions.py +++ b/tests/testextensions.py @@ -112,6 +112,53 @@ def test_py36_class_syntax_usage(self): other = LabelPoint2D(x=0, y=1, label='hi') # noqa self.assertEqual(other['label'], 'hi') + def test_class_annotations(self): + LocalType = int + with self.assert_typeddict_deprecated(): + class Local(TypedDict): + value: LocalType + + self.assertEqual(Local.__annotations__, {'value': int}) + with self.assert_typeddict_deprecated(): + class Child(Local): + label: str + + self.assertEqual(Child.__annotations__, {'value': int, 'label': str}) + self.assertEqual(Local.__annotations__, {'value': int}) + + def test_class_invalid_annotation(self): + with self.assertRaises(TypeError), self.assert_typeddict_deprecated(): + class Invalid(TypedDict): + value: () + + def test_class_forward_reference(self): + with self.assert_typeddict_deprecated(): + class Forward(TypedDict): + value: 'Later' + + class Later: + pass + + self.assertEqual(typing.get_type_hints(Forward, localns={'Later': Later}), + {'value': Later}) + + def test_class_deferred_self_reference(self): + if sys.version_info < (3, 14): + self.skipTest('requires deferred annotations') + namespace = {'TypedDict': TypedDict} + with self.assert_typeddict_deprecated(): + exec('class Node(TypedDict):\n child: Node', namespace) + Node = namespace['Node'] + self.assertEqual(typing.get_type_hints(Node, globalns=namespace), {'child': Node}) + + def test_class_future_annotations(self): + namespace = {'TypedDict': TypedDict} + with self.assert_typeddict_deprecated(): + exec('from __future__ import annotations\n' + 'class Future(TypedDict):\n value: int', namespace) + self.assertEqual(typing.get_type_hints(namespace['Future'], globalns=namespace), + {'value': int}) + def test_py36_class_usage_emits_deprecations(self): with self.assert_typeddict_deprecated(): class Foo(TypedDict): diff --git a/tox.ini b/tox.ini index 3412839..f71e281 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 4.4.4 skip_missing_interpreters = true -envlist = py38, py39, py310, py311, py312, py313 +envlist = py38, py39, py310, py311, py312, py313, py314 [testenv] description = run the test driver with {basepython}