From b57805e12c14578ba436300d64a62514255f3b1b Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 23 Aug 2025 16:14:47 -0400 Subject: [PATCH 01/12] Add miscellaneous tests --- src/test_typing_extensions.py | 131 ++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 0986427c..cfe8fccd 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2254,6 +2254,52 @@ def test_asynccontextmanager_type_params(self): cm2 = typing_extensions.AsyncContextManager[int, None] self.assertEqual(get_args(cm2), (int, NoneType)) + def test_setattr(self): + if hasattr(typing_extensions, "_SpecialGenericAlias"): + mod = typing_extensions + else: + mod = typing + class Foo: + _name = "Foo" + Alias = mod._SpecialGenericAlias(Foo, 1) + + # Attribute assignment on generic alias sets attribute on origin + Alias.foo = 1 + self.assertEqual(Alias.foo, 1) + self.assertEqual(Foo.foo, 1) + + # Except for dunders... + Alias.__dunder__ = 2 + self.assertEqual(Alias.__dunder__, 2) + with self.assertRaises(AttributeError): + Foo.__dunder__ + + # ...and certain known attributes + Alias._name = "NewName" + self.assertEqual(Alias._name, "NewName") + self.assertEqual(Foo._name, "Foo") + + def test_invalid_specialization(self): + if hasattr(typing_extensions, "_SpecialGenericAlias"): + mod = typing_extensions + else: + mod = typing + class Foo: ... + Alias = mod._SpecialGenericAlias(Foo, 2) + + msg = re.escape("Too few arguments for typing.Foo; actual 1, expected 2") + with self.assertRaisesRegex(TypeError, msg): + Alias[int] + + msg = re.escape("Too many arguments for typing.Foo; actual 3, expected 2") + with self.assertRaisesRegex(TypeError, msg): + Alias[int, int, int] + + Alias0 = mod._SpecialGenericAlias(Foo, 0) + msg = re.escape("typing.Foo is not a generic class") + with self.assertRaisesRegex(TypeError, msg): + Alias0[int] + class TypeTests(BaseTestCase): @@ -2379,6 +2425,16 @@ def test_error_message_when_subclassing(self): class ProUserId(UserId): ... + def test_module_with_incomplete_sys(self): + def does_not_exist(*args): + raise AttributeError + with ( + patch("sys._getframemodulename", does_not_exist, create=True), + patch("sys._getframe", does_not_exist, create=True), + ): + X = NewType("X", int) + self.assertEqual(X.__module__, None) + class Coordinate(Protocol): x: int @@ -5499,6 +5555,25 @@ def foobar(x: List['X']): ... get_type_hints(foobar, globals(), locals(), include_extras=True), {'x': List[Annotated[int, (1, 10)]]} ) + def foobar2(x: list['X']): ... + if sys.version_info >= (3, 11): + self.assertEqual( + get_type_hints(foobar2, globals(), locals()), + {'x': list[int]} + ) + self.assertEqual( + get_type_hints(foobar2, globals(), locals(), include_extras=True), + {'x': list[Annotated[int, (1, 10)]]} + ) + else: # TODO: evaluate nested forward refs in Python < 3.11 + self.assertEqual( + get_type_hints(foobar2, globals(), locals()), + {'x': list['X']} + ) + self.assertEqual( + get_type_hints(foobar2, globals(), locals(), include_extras=True), + {'x': list['X']} + ) BA = Tuple[Annotated[T, (1, 0)], ...] def barfoo(x: BA): ... self.assertEqual(get_type_hints(barfoo, globals(), locals())['x'], Tuple[T, ...]) @@ -5973,6 +6048,15 @@ def run(): # The actual test: self.assertEqual(result1, result2) + def test_subclass(self): + if sys.version_info >= (3, 10): + with self.assertRaises(TypeError): + class MyParamSpec(ParamSpec): + pass + else: + class MyParamSpec(ParamSpec): # Does not raise + pass + class ConcatenateTests(BaseTestCase): def test_basics(self): @@ -6382,6 +6466,14 @@ def test_pickle(self): pickled = pickle.dumps(Self, protocol=proto) self.assertIs(Self, pickle.loads(pickled)) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + self.assertEqual(Self | int, Union[Self, int]) + self.assertEqual(int | Self, Union[int, Self]) + + self.assertEqual(get_args(Self | int), (Self, int)) + self.assertEqual(get_args(int | Self), (int, Self)) + class UnpackTests(BaseTestCase): def test_basic_plain(self): @@ -7749,6 +7841,45 @@ def test_immutable(self): type(NoDefault).foo +class NoExtraItemsTests(BaseTestCase): + def test_pickling(self): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + s = pickle.dumps(NoExtraItems, proto) + loaded = pickle.loads(s) + self.assertIs(NoExtraItems, loaded) + + def test_doc(self): + self.assertIsInstance(NoExtraItems.__doc__, str) + + def test_constructor(self): + self.assertIs(NoExtraItems, type(NoExtraItems)()) + with self.assertRaises(TypeError): + type(NoExtraItems)(1) + + def test_repr(self): + if hasattr(typing, 'NoExtraItems'): + mod_name = 'typing' + else: + mod_name = "typing_extensions" + self.assertEqual(repr(NoExtraItems), f"{mod_name}.NoExtraItems") + + def test_no_call(self): + with self.assertRaises(TypeError): + NoExtraItems() + + def test_immutable(self): + with self.assertRaises(AttributeError): + NoExtraItems.foo = 'bar' + with self.assertRaises(AttributeError): + NoExtraItems.foo + + # TypeError is consistent with the behavior of NoneType + with self.assertRaises(TypeError): + type(NoExtraItems).foo = 3 + with self.assertRaises(AttributeError): + type(NoExtraItems).foo + + class TypeVarInferVarianceTests(BaseTestCase): def test_typevar(self): T = typing_extensions.TypeVar('T') From 611cb740efb9d7d59728d7e463339cda5f82e494 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Tue, 26 Aug 2025 18:53:03 -0400 Subject: [PATCH 02/12] Remove dead branches in _SpecialGenericAlias.__getitem__ --- src/test_typing_extensions.py | 21 --------------------- src/typing_extensions.py | 14 +++++--------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index cfe8fccd..a1097571 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2279,27 +2279,6 @@ class Foo: self.assertEqual(Alias._name, "NewName") self.assertEqual(Foo._name, "Foo") - def test_invalid_specialization(self): - if hasattr(typing_extensions, "_SpecialGenericAlias"): - mod = typing_extensions - else: - mod = typing - class Foo: ... - Alias = mod._SpecialGenericAlias(Foo, 2) - - msg = re.escape("Too few arguments for typing.Foo; actual 1, expected 2") - with self.assertRaisesRegex(TypeError, msg): - Alias[int] - - msg = re.escape("Too many arguments for typing.Foo; actual 3, expected 2") - with self.assertRaisesRegex(TypeError, msg): - Alias[int, int, int] - - Alias0 = mod._SpecialGenericAlias(Foo, 0) - msg = re.escape("typing.Foo is not a generic class") - with self.assertRaisesRegex(TypeError, msg): - Alias0[int] - class TypeTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 77f33e16..01dfbffc 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -524,7 +524,9 @@ def _is_dunder(attr): class _SpecialGenericAlias(typing._SpecialGenericAlias, _root=True): - def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()): + def __init__(self, origin, nparams, *, defaults, inst=True, name=None): + assert nparams > 0, "`nparams` must be a positive integer" + assert defaults, "Must always specify a non-empty sequence for `defaults`" super().__init__(origin, nparams, inst=inst, name=name) self._defaults = defaults @@ -542,20 +544,14 @@ def __getitem__(self, params): msg = "Parameters to generic types must be types." params = tuple(typing._type_check(p, msg) for p in params) if ( - self._defaults - and len(params) < self._nparams + len(params) < self._nparams and len(params) + len(self._defaults) >= self._nparams ): params = (*params, *self._defaults[len(params) - self._nparams:]) actual_len = len(params) if actual_len != self._nparams: - if self._defaults: - expected = f"at least {self._nparams - len(self._defaults)}" - else: - expected = str(self._nparams) - if not self._nparams: - raise TypeError(f"{self} is not a generic class") + expected = f"at least {self._nparams - len(self._defaults)}" raise TypeError( f"Too {'many' if actual_len > self._nparams else 'few'}" f" arguments for {self};" From 168aff08f0886c288d0cfd2a786a97471e6364c4 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 11:30:38 -0400 Subject: [PATCH 03/12] Copy `ParamSpec.__init_subclass__` implementation to Python 3.9 branch The Python 3.10+ implementation started raising in PR#165 --- src/test_typing_extensions.py | 8 ++------ src/typing_extensions.py | 3 +++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a1097571..4d561fac 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6028,12 +6028,8 @@ def run(): self.assertEqual(result1, result2) def test_subclass(self): - if sys.version_info >= (3, 10): - with self.assertRaises(TypeError): - class MyParamSpec(ParamSpec): - pass - else: - class MyParamSpec(ParamSpec): # Does not raise + with self.assertRaises(TypeError): + class MyParamSpec(ParamSpec): pass diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 01dfbffc..bf5a0ecb 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1925,6 +1925,9 @@ def __reduce__(self): def __call__(self, *args, **kwargs): pass + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") + # 3.9 if not hasattr(typing, 'Concatenate'): From 69672c27eb3379954be07d5bd90c271b7dea8a3e Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 11:30:38 -0400 Subject: [PATCH 04/12] Add `__[r]or__` tests to `_SpecialForm` instances with meaningful unions --- src/test_typing_extensions.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 4d561fac..bf4c1d78 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -531,6 +531,14 @@ def test_pickle(self): pickled = pickle.dumps(self.bottom_type, protocol=proto) self.assertIs(self.bottom_type, pickle.loads(pickled)) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + self.assertEqual(self.bottom_type | int, Union[self.bottom_type, int]) + self.assertEqual(int | self.bottom_type, Union[int, self.bottom_type]) + + self.assertEqual(get_args(self.bottom_type | int), (self.bottom_type, int)) + self.assertEqual(get_args(int | self.bottom_type), (int, self.bottom_type)) + class NoReturnTests(BottomTypeTestsMixin, BaseTestCase): bottom_type = NoReturn @@ -5332,6 +5340,17 @@ class A(TypedDict): def test_dunder_dict(self): self.assertIsInstance(TypedDict.__dict__, dict) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + class TD(TypedDict): + a: int + + self.assertEqual(TD | int, Union[TD, int]) + self.assertEqual(int | TD, Union[int, TD]) + + self.assertEqual(get_args(TD | int), (TD, int)) + self.assertEqual(get_args(int | TD), (int, TD)) + class AnnotatedTests(BaseTestCase): def test_repr(self): @@ -6394,6 +6413,14 @@ def test_pickle(self): pickled = pickle.dumps(LiteralString, protocol=proto) self.assertIs(LiteralString, pickle.loads(pickled)) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + self.assertEqual(LiteralString | int, Union[LiteralString, int]) + self.assertEqual(int | LiteralString, Union[int, LiteralString]) + + self.assertEqual(get_args(LiteralString | int), (LiteralString, int)) + self.assertEqual(get_args(int | LiteralString), (int, LiteralString)) + class SelfTests(BaseTestCase): def test_basics(self): From 8cb78509612ec6123c09f5f2c56d0bb54ebc4517 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 11:30:38 -0400 Subject: [PATCH 05/12] Split off `get_type_hints` w/ types.GenericAlias` test --- src/test_typing_extensions.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index bf4c1d78..5b50ff86 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5553,25 +5553,6 @@ def foobar(x: List['X']): ... get_type_hints(foobar, globals(), locals(), include_extras=True), {'x': List[Annotated[int, (1, 10)]]} ) - def foobar2(x: list['X']): ... - if sys.version_info >= (3, 11): - self.assertEqual( - get_type_hints(foobar2, globals(), locals()), - {'x': list[int]} - ) - self.assertEqual( - get_type_hints(foobar2, globals(), locals(), include_extras=True), - {'x': list[Annotated[int, (1, 10)]]} - ) - else: # TODO: evaluate nested forward refs in Python < 3.11 - self.assertEqual( - get_type_hints(foobar2, globals(), locals()), - {'x': list['X']} - ) - self.assertEqual( - get_type_hints(foobar2, globals(), locals(), include_extras=True), - {'x': list['X']} - ) BA = Tuple[Annotated[T, (1, 0)], ...] def barfoo(x: BA): ... self.assertEqual(get_type_hints(barfoo, globals(), locals())['x'], Tuple[T, ...]) @@ -5592,6 +5573,19 @@ def barfoo3(x: BA2): ... BA2 ) + @skipUnless(sys.version_info >= (3, 11), "TODO: evaluate nested forward refs in Python < 3.11") + def test_get_type_hints_genericalias(self): + def foobar(x: list['X']): ... + X = Annotated[int, (1, 10)] + self.assertEqual( + get_type_hints(foobar, globals(), locals()), + {'x': list[int]} + ) + self.assertEqual( + get_type_hints(foobar, globals(), locals(), include_extras=True), + {'x': list[Annotated[int, (1, 10)]]} + ) + def test_get_type_hints_refs(self): Const = Annotated[T, "Const"] From 50b55af96566fc97444e779a90fa4549a60a4449 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 11:30:38 -0400 Subject: [PATCH 06/12] Use public API for testing `_SpecialGenericAlias.__setattr__` --- src/test_typing_extensions.py | 55 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 5b50ff86..460e52a2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2218,6 +2218,36 @@ def test_or_and_ror(self): Union[typing_extensions.Generator, typing.Deque] ) + def test_setattr(self): + origin = collections.abc.Generator + alias = typing_extensions.Generator + + # Attribute assignment on generic alias sets attribute on origin + alias.foo = 1 + self.assertEqual(alias.foo, 1) + self.assertEqual(origin.foo, 1) + # cleanup + del origin.foo + self.assertRaises(AttributeError, lambda: alias.foo) + self.assertRaises(AttributeError, lambda: origin.foo) + + # Except for dunders... + alias.__dunder__ = 2 + self.assertEqual(alias.__dunder__, 2) + self.assertRaises(AttributeError, lambda: origin.__dunder__) + # cleanup + del alias.__dunder__ + self.assertRaises(AttributeError, lambda: alias.__dunder___) + + # ...and certain known attributes + old_name = alias._name + alias._name = "NewName" + self.assertEqual(alias._name, "NewName") + self.assertRaises(AttributeError, lambda: origin._name) + # cleanup + alias._name = old_name + self.assertEqual(alias._name, old_name) + class OtherABCTests(BaseTestCase): @@ -2262,31 +2292,6 @@ def test_asynccontextmanager_type_params(self): cm2 = typing_extensions.AsyncContextManager[int, None] self.assertEqual(get_args(cm2), (int, NoneType)) - def test_setattr(self): - if hasattr(typing_extensions, "_SpecialGenericAlias"): - mod = typing_extensions - else: - mod = typing - class Foo: - _name = "Foo" - Alias = mod._SpecialGenericAlias(Foo, 1) - - # Attribute assignment on generic alias sets attribute on origin - Alias.foo = 1 - self.assertEqual(Alias.foo, 1) - self.assertEqual(Foo.foo, 1) - - # Except for dunders... - Alias.__dunder__ = 2 - self.assertEqual(Alias.__dunder__, 2) - with self.assertRaises(AttributeError): - Foo.__dunder__ - - # ...and certain known attributes - Alias._name = "NewName" - self.assertEqual(Alias._name, "NewName") - self.assertEqual(Foo._name, "Foo") - class TypeTests(BaseTestCase): From ed273227f6d14d7a3f73cd8939d11b41c67f8d36 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 12:33:09 -0400 Subject: [PATCH 07/12] Use mixin for common NoDefault/NoExtraItems tests --- src/test_typing_extensions.py | 64 ++++++++++++----------------------- 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 460e52a2..940a3e03 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7804,58 +7804,54 @@ class A(Generic[T, P, U]): ... self.assertEqual(A[float, [range], int].__args__, (float, (range,), int)) -class NoDefaultTests(BaseTestCase): +class TypedDictSentinelMixin: @skip_if_py313_beta_1 def test_pickling(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): - s = pickle.dumps(NoDefault, proto) + s = pickle.dumps(self.sentinel_type, proto) loaded = pickle.loads(s) - self.assertIs(NoDefault, loaded) + self.assertIs(self.sentinel_type, loaded) @skip_if_py313_beta_1 def test_doc(self): - self.assertIsInstance(NoDefault.__doc__, str) + self.assertIsInstance(self.sentinel_type.__doc__, str) def test_constructor(self): - self.assertIs(NoDefault, type(NoDefault)()) + self.assertIs(self.sentinel_type, type(self.sentinel_type)()) with self.assertRaises(TypeError): - type(NoDefault)(1) - - def test_repr(self): - self.assertRegex(repr(NoDefault), r'typing(_extensions)?\.NoDefault') + type(self.sentinel_type)(1) def test_no_call(self): with self.assertRaises(TypeError): - NoDefault() + self.sentinel_type() @skip_if_py313_beta_1 def test_immutable(self): with self.assertRaises(AttributeError): - NoDefault.foo = 'bar' + self.sentinel_type.foo = 'bar' with self.assertRaises(AttributeError): - NoDefault.foo + self.sentinel_type.foo # TypeError is consistent with the behavior of NoneType with self.assertRaises(TypeError): - type(NoDefault).foo = 3 + type(self.sentinel_type).foo = 3 with self.assertRaises(AttributeError): - type(NoDefault).foo + type(self.sentinel_type).foo -class NoExtraItemsTests(BaseTestCase): - def test_pickling(self): - for proto in range(pickle.HIGHEST_PROTOCOL + 1): - s = pickle.dumps(NoExtraItems, proto) - loaded = pickle.loads(s) - self.assertIs(NoExtraItems, loaded) +class NoDefaultTests(TypedDictSentinelMixin, BaseTestCase): + sentinel_type = NoDefault - def test_doc(self): - self.assertIsInstance(NoExtraItems.__doc__, str) + def test_repr(self): + if hasattr(typing, 'NoDefault'): + mod_name = 'typing' + else: + mod_name = "typing_extensions" + self.assertEqual(repr(NoDefault), f"{mod_name}.NoDefault") - def test_constructor(self): - self.assertIs(NoExtraItems, type(NoExtraItems)()) - with self.assertRaises(TypeError): - type(NoExtraItems)(1) + +class NoExtraItemsTests(TypedDictSentinelMixin, BaseTestCase): + sentinel_type = NoExtraItems def test_repr(self): if hasattr(typing, 'NoExtraItems'): @@ -7864,22 +7860,6 @@ def test_repr(self): mod_name = "typing_extensions" self.assertEqual(repr(NoExtraItems), f"{mod_name}.NoExtraItems") - def test_no_call(self): - with self.assertRaises(TypeError): - NoExtraItems() - - def test_immutable(self): - with self.assertRaises(AttributeError): - NoExtraItems.foo = 'bar' - with self.assertRaises(AttributeError): - NoExtraItems.foo - - # TypeError is consistent with the behavior of NoneType - with self.assertRaises(TypeError): - type(NoExtraItems).foo = 3 - with self.assertRaises(AttributeError): - type(NoExtraItems).foo - class TypeVarInferVarianceTests(BaseTestCase): def test_typevar(self): From 80324143865a259eebcd2874c29f1fe4e2f183ad Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 12:38:45 -0400 Subject: [PATCH 08/12] name --- src/test_typing_extensions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 940a3e03..4ec2b931 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7804,7 +7804,7 @@ class A(Generic[T, P, U]): ... self.assertEqual(A[float, [range], int].__args__, (float, (range,), int)) -class TypedDictSentinelMixin: +class SentinelTestsMixin: @skip_if_py313_beta_1 def test_pickling(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): @@ -7839,7 +7839,7 @@ def test_immutable(self): type(self.sentinel_type).foo -class NoDefaultTests(TypedDictSentinelMixin, BaseTestCase): +class NoDefaultTests(SentinelTestsMixin, BaseTestCase): sentinel_type = NoDefault def test_repr(self): @@ -7850,7 +7850,7 @@ def test_repr(self): self.assertEqual(repr(NoDefault), f"{mod_name}.NoDefault") -class NoExtraItemsTests(TypedDictSentinelMixin, BaseTestCase): +class NoExtraItemsTests(SentinelTestsMixin, BaseTestCase): sentinel_type = NoExtraItems def test_repr(self): From 5d7f6375f8740141641c4b17c867f98b65fc5472 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 13:30:19 -0400 Subject: [PATCH 09/12] Tidy test_setattr cleanup Co-authored-by: Alex Waygood --- src/test_typing_extensions.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 4ec2b931..7876f306 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2221,32 +2221,36 @@ def test_or_and_ror(self): def test_setattr(self): origin = collections.abc.Generator alias = typing_extensions.Generator + original_name = alias._name + + def cleanup(): + for obj in origin, alias: + for attr in 'foo', '__dunder__': + try: + delattr(obj, attr) + except Exception: + pass + try: + alias._name = original_name + except Exception: + pass + + self.addCleanup(cleanup) # Attribute assignment on generic alias sets attribute on origin alias.foo = 1 self.assertEqual(alias.foo, 1) self.assertEqual(origin.foo, 1) - # cleanup - del origin.foo - self.assertRaises(AttributeError, lambda: alias.foo) - self.assertRaises(AttributeError, lambda: origin.foo) - # Except for dunders... alias.__dunder__ = 2 self.assertEqual(alias.__dunder__, 2) self.assertRaises(AttributeError, lambda: origin.__dunder__) - # cleanup - del alias.__dunder__ - self.assertRaises(AttributeError, lambda: alias.__dunder___) # ...and certain known attributes old_name = alias._name alias._name = "NewName" self.assertEqual(alias._name, "NewName") self.assertRaises(AttributeError, lambda: origin._name) - # cleanup - alias._name = old_name - self.assertEqual(alias._name, old_name) class OtherABCTests(BaseTestCase): From 4a0d76ae9db26cb0efe476f1f8e0f74e2d43cca9 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 13:32:09 -0400 Subject: [PATCH 10/12] Use TYPING_3_11_0 Co-authored-by: Daraan --- src/test_typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7876f306..3ad6a1c0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5582,7 +5582,7 @@ def barfoo3(x: BA2): ... BA2 ) - @skipUnless(sys.version_info >= (3, 11), "TODO: evaluate nested forward refs in Python < 3.11") + @skipUnless(TYPING_3_11_0, "TODO: evaluate nested forward refs in Python < 3.11") def test_get_type_hints_genericalias(self): def foobar(x: list['X']): ... X = Annotated[int, (1, 10)] From ed5ee71b9e70f1544cb31d7a944188589597bd2a Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 13:43:08 -0400 Subject: [PATCH 11/12] Add changelog entry --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd98ad7..dd3fd944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Unreleased + +- Raise `TypeError` when attempting to subclass `typing_extensions.ParamSpec` on + Python 3.9. The `typing` implementation has always raised an error, and the + `typing_extensions` implementation has raised an error on Python 3.10+ since + `typing_extensions` v4.6.0. Patch by Brian Schubert. + # Release 4.15.0rc1 (August 18, 2025) - Add the `@typing_extensions.disjoint_base` decorator, as specified From 2e23850e6776ad7d5d84cbc7e7af8cd01723607b Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sat, 30 Aug 2025 13:49:11 -0400 Subject: [PATCH 12/12] Remove leftover variable --- src/test_typing_extensions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 3ad6a1c0..551579dc 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2247,7 +2247,6 @@ def cleanup(): self.assertRaises(AttributeError, lambda: origin.__dunder__) # ...and certain known attributes - old_name = alias._name alias._name = "NewName" self.assertEqual(alias._name, "NewName") self.assertRaises(AttributeError, lambda: origin._name)