Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.0 (August 25, 2025)

No user-facing changes since 4.15.0rc1.
Expand Down
143 changes: 129 additions & 14 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2210,6 +2218,39 @@ def test_or_and_ror(self):
Union[typing_extensions.Generator, typing.Deque]
)

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)
# Except for dunders...
alias.__dunder__ = 2
self.assertEqual(alias.__dunder__, 2)
self.assertRaises(AttributeError, lambda: origin.__dunder__)

# ...and certain known attributes
alias._name = "NewName"
self.assertEqual(alias._name, "NewName")
self.assertRaises(AttributeError, lambda: origin._name)


class OtherABCTests(BaseTestCase):

Expand Down Expand Up @@ -2379,6 +2420,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
Expand Down Expand Up @@ -5297,6 +5348,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):
Expand Down Expand Up @@ -5519,6 +5581,19 @@ def barfoo3(x: BA2): ...
BA2
)

@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)]
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"]
Expand Down Expand Up @@ -5973,6 +6048,11 @@ def run():
# The actual test:
self.assertEqual(result1, result2)

def test_subclass(self):
with self.assertRaises(TypeError):
class MyParamSpec(ParamSpec):
pass


class ConcatenateTests(BaseTestCase):
def test_basics(self):
Expand Down Expand Up @@ -6335,6 +6415,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):
Expand Down Expand Up @@ -6382,6 +6470,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):
Expand Down Expand Up @@ -7711,42 +7807,61 @@ class A(Generic[T, P, U]): ...
self.assertEqual(A[float, [range], int].__args__, (float, (range,), int))


class NoDefaultTests(BaseTestCase):
class SentinelTestsMixin:
@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 NoDefaultTests(SentinelTestsMixin, BaseTestCase):
sentinel_type = NoDefault

def test_repr(self):
if hasattr(typing, 'NoDefault'):
mod_name = 'typing'
else:
mod_name = "typing_extensions"
self.assertEqual(repr(NoDefault), f"{mod_name}.NoDefault")


class NoExtraItemsTests(SentinelTestsMixin, BaseTestCase):
sentinel_type = NoExtraItems

def test_repr(self):
if hasattr(typing, 'NoExtraItems'):
mod_name = 'typing'
else:
mod_name = "typing_extensions"
self.assertEqual(repr(NoExtraItems), f"{mod_name}.NoExtraItems")


class TypeVarInferVarianceTests(BaseTestCase):
Expand Down
17 changes: 8 additions & 9 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,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

Expand All @@ -573,20 +575,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};"
Expand Down Expand Up @@ -1960,6 +1956,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'):
Expand Down
Loading