From df994abde84c4679ff2c12387989746a498fbc41 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 23 May 2025 18:15:29 -0700 Subject: [PATCH 1/2] More fixes for 3.14 and 3.15 --- CHANGELOG.md | 7 ++++--- src/test_typing_extensions.py | 35 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 8 +++++--- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a19a3a..84f4969f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ - Do not attempt to re-export names that have been removed from `typing`, anticipating the removal of `typing.no_type_check_decorator` in Python 3.15. Patch by Jelle Zijlstra. -- Update `typing_extensions.Format` and `typing_extensions.evaluate_forward_ref` to align - with changes in Python 3.14. Patch by Jelle Zijlstra. -- Fix tests for Python 3.14. Patch by Jelle Zijlstra. +- Update `typing_extensions.Format`, `typing_extensions.evaluate_forward_ref`, and + `typing_extensions.TypedDict` to align + with changes in Python 3.14. Patches by Jelle Zijlstra. +- Fix tests for Python 3.14 and 3.15. Patches by Jelle Zijlstra. New features: diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index c23e94b7..de31b178 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4402,6 +4402,38 @@ class Cat(Animal): 'voice': str, } + 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 = {**extra_names} if extra_names else {} + exec(code, ns) + 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" + ) + if sys.version_info >= (3, 14): + self.assertIsNotNone(base.__annotate__) + child = _make_td( + child_future, "Child", {"child": "int"}, "Base", {"Base": base} + ) + base_anno = typing.ForwardRef("int", module="builtins") if base_future else int + child_anno = typing.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"})) @@ -7014,6 +7046,7 @@ class Group(NamedTuple): self.assertIs(type(a), Group) self.assertEqual(a, (1, [2])) + @skipUnless(sys.version_info <= (3, 15), "Behavior removed in 3.15") def test_namedtuple_keyword_usage(self): with self.assertWarnsRegex( DeprecationWarning, @@ -7049,6 +7082,7 @@ def test_namedtuple_keyword_usage(self): ): NamedTuple('Name', None, x=int) + @skipUnless(sys.version_info <= (3, 15), "Behavior removed in 3.15") def test_namedtuple_special_keyword_names(self): with self.assertWarnsRegex( DeprecationWarning, @@ -7064,6 +7098,7 @@ def test_namedtuple_special_keyword_names(self): self.assertEqual(a.typename, 'foo') self.assertEqual(a.fields, [('bar', tuple)]) + @skipUnless(sys.version_info <= (3, 15), "Behavior removed in 3.15") def test_empty_namedtuple(self): expected_warning = re.escape( "Failing to pass a value for the 'fields' parameter is deprecated " diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 84ff0e2e..92e79def 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1016,6 +1016,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, else: generic_base = () + ns_annotations = ns.pop('__annotations__', None) + # typing.py generally doesn't let you inherit from plain Generic, unless # the name of the class happens to be "Protocol" tp_dict = type.__new__(_TypedDictMeta, "Protocol", (*generic_base, dict), ns) @@ -1028,8 +1030,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, annotations = {} own_annotate = None - if "__annotations__" in ns: - own_annotations = ns["__annotations__"] + if ns_annotations is not None: + own_annotations = ns_annotations elif sys.version_info >= (3, 14): if hasattr(annotationlib, "get_annotate_from_class_namespace"): own_annotate = annotationlib.get_annotate_from_class_namespace(ns) @@ -1119,7 +1121,7 @@ def __annotate__(format): if base_annotate is None: continue base_annos = 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 = annotationlib.call_annotate_function( From effe26484c453aa401051b23d0e02687b5d7b419 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 24 May 2025 09:30:21 -0700 Subject: [PATCH 2/2] skip on beta1 --- src/test_typing_extensions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index de31b178..60f6a1d9 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4402,6 +4402,7 @@ class Cat(Animal): 'voice': str, } + @skipIf(sys.version_info == (3, 14, 0, "beta", 1), "Broken on beta 1, fixed in beta 2") def test_inheritance_pep563(self): def _make_td(future, class_name, annos, base, extra_names=None): lines = []