Skip to content

Commit ccbfd9b

Browse files
brianschubertAlexWaygoodDaraan
authored
Add some miscellaneous tests (python#659)
Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Daraan <[email protected]>
1 parent 944a351 commit ccbfd9b

File tree

3 files changed

+144
-23
lines changed

3 files changed

+144
-23
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Unreleased
2+
3+
- Raise `TypeError` when attempting to subclass `typing_extensions.ParamSpec` on
4+
Python 3.9. The `typing` implementation has always raised an error, and the
5+
`typing_extensions` implementation has raised an error on Python 3.10+ since
6+
`typing_extensions` v4.6.0. Patch by Brian Schubert.
7+
18
# Release 4.15.0 (August 25, 2025)
29

310
No user-facing changes since 4.15.0rc1.

src/test_typing_extensions.py

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,14 @@ def test_pickle(self):
531531
pickled = pickle.dumps(self.bottom_type, protocol=proto)
532532
self.assertIs(self.bottom_type, pickle.loads(pickled))
533533

534+
@skipUnless(TYPING_3_10_0, "PEP 604 has yet to be")
535+
def test_or(self):
536+
self.assertEqual(self.bottom_type | int, Union[self.bottom_type, int])
537+
self.assertEqual(int | self.bottom_type, Union[int, self.bottom_type])
538+
539+
self.assertEqual(get_args(self.bottom_type | int), (self.bottom_type, int))
540+
self.assertEqual(get_args(int | self.bottom_type), (int, self.bottom_type))
541+
534542

535543
class NoReturnTests(BottomTypeTestsMixin, BaseTestCase):
536544
bottom_type = NoReturn
@@ -2210,6 +2218,39 @@ def test_or_and_ror(self):
22102218
Union[typing_extensions.Generator, typing.Deque]
22112219
)
22122220

2221+
def test_setattr(self):
2222+
origin = collections.abc.Generator
2223+
alias = typing_extensions.Generator
2224+
original_name = alias._name
2225+
2226+
def cleanup():
2227+
for obj in origin, alias:
2228+
for attr in 'foo', '__dunder__':
2229+
try:
2230+
delattr(obj, attr)
2231+
except Exception:
2232+
pass
2233+
try:
2234+
alias._name = original_name
2235+
except Exception:
2236+
pass
2237+
2238+
self.addCleanup(cleanup)
2239+
2240+
# Attribute assignment on generic alias sets attribute on origin
2241+
alias.foo = 1
2242+
self.assertEqual(alias.foo, 1)
2243+
self.assertEqual(origin.foo, 1)
2244+
# Except for dunders...
2245+
alias.__dunder__ = 2
2246+
self.assertEqual(alias.__dunder__, 2)
2247+
self.assertRaises(AttributeError, lambda: origin.__dunder__)
2248+
2249+
# ...and certain known attributes
2250+
alias._name = "NewName"
2251+
self.assertEqual(alias._name, "NewName")
2252+
self.assertRaises(AttributeError, lambda: origin._name)
2253+
22132254

22142255
class OtherABCTests(BaseTestCase):
22152256

@@ -2379,6 +2420,16 @@ def test_error_message_when_subclassing(self):
23792420
class ProUserId(UserId):
23802421
...
23812422

2423+
def test_module_with_incomplete_sys(self):
2424+
def does_not_exist(*args):
2425+
raise AttributeError
2426+
with (
2427+
patch("sys._getframemodulename", does_not_exist, create=True),
2428+
patch("sys._getframe", does_not_exist, create=True),
2429+
):
2430+
X = NewType("X", int)
2431+
self.assertEqual(X.__module__, None)
2432+
23822433

23832434
class Coordinate(Protocol):
23842435
x: int
@@ -5297,6 +5348,17 @@ class A(TypedDict):
52975348
def test_dunder_dict(self):
52985349
self.assertIsInstance(TypedDict.__dict__, dict)
52995350

5351+
@skipUnless(TYPING_3_10_0, "PEP 604 has yet to be")
5352+
def test_or(self):
5353+
class TD(TypedDict):
5354+
a: int
5355+
5356+
self.assertEqual(TD | int, Union[TD, int])
5357+
self.assertEqual(int | TD, Union[int, TD])
5358+
5359+
self.assertEqual(get_args(TD | int), (TD, int))
5360+
self.assertEqual(get_args(int | TD), (int, TD))
5361+
53005362
class AnnotatedTests(BaseTestCase):
53015363

53025364
def test_repr(self):
@@ -5519,6 +5581,19 @@ def barfoo3(x: BA2): ...
55195581
BA2
55205582
)
55215583

5584+
@skipUnless(TYPING_3_11_0, "TODO: evaluate nested forward refs in Python < 3.11")
5585+
def test_get_type_hints_genericalias(self):
5586+
def foobar(x: list['X']): ...
5587+
X = Annotated[int, (1, 10)]
5588+
self.assertEqual(
5589+
get_type_hints(foobar, globals(), locals()),
5590+
{'x': list[int]}
5591+
)
5592+
self.assertEqual(
5593+
get_type_hints(foobar, globals(), locals(), include_extras=True),
5594+
{'x': list[Annotated[int, (1, 10)]]}
5595+
)
5596+
55225597
def test_get_type_hints_refs(self):
55235598

55245599
Const = Annotated[T, "Const"]
@@ -5973,6 +6048,11 @@ def run():
59736048
# The actual test:
59746049
self.assertEqual(result1, result2)
59756050

6051+
def test_subclass(self):
6052+
with self.assertRaises(TypeError):
6053+
class MyParamSpec(ParamSpec):
6054+
pass
6055+
59766056

59776057
class ConcatenateTests(BaseTestCase):
59786058
def test_basics(self):
@@ -6335,6 +6415,14 @@ def test_pickle(self):
63356415
pickled = pickle.dumps(LiteralString, protocol=proto)
63366416
self.assertIs(LiteralString, pickle.loads(pickled))
63376417

6418+
@skipUnless(TYPING_3_10_0, "PEP 604 has yet to be")
6419+
def test_or(self):
6420+
self.assertEqual(LiteralString | int, Union[LiteralString, int])
6421+
self.assertEqual(int | LiteralString, Union[int, LiteralString])
6422+
6423+
self.assertEqual(get_args(LiteralString | int), (LiteralString, int))
6424+
self.assertEqual(get_args(int | LiteralString), (int, LiteralString))
6425+
63386426

63396427
class SelfTests(BaseTestCase):
63406428
def test_basics(self):
@@ -6382,6 +6470,14 @@ def test_pickle(self):
63826470
pickled = pickle.dumps(Self, protocol=proto)
63836471
self.assertIs(Self, pickle.loads(pickled))
63846472

6473+
@skipUnless(TYPING_3_10_0, "PEP 604 has yet to be")
6474+
def test_or(self):
6475+
self.assertEqual(Self | int, Union[Self, int])
6476+
self.assertEqual(int | Self, Union[int, Self])
6477+
6478+
self.assertEqual(get_args(Self | int), (Self, int))
6479+
self.assertEqual(get_args(int | Self), (int, Self))
6480+
63856481

63866482
class UnpackTests(BaseTestCase):
63876483
def test_basic_plain(self):
@@ -7711,42 +7807,61 @@ class A(Generic[T, P, U]): ...
77117807
self.assertEqual(A[float, [range], int].__args__, (float, (range,), int))
77127808

77137809

7714-
class NoDefaultTests(BaseTestCase):
7810+
class SentinelTestsMixin:
77157811
@skip_if_py313_beta_1
77167812
def test_pickling(self):
77177813
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
7718-
s = pickle.dumps(NoDefault, proto)
7814+
s = pickle.dumps(self.sentinel_type, proto)
77197815
loaded = pickle.loads(s)
7720-
self.assertIs(NoDefault, loaded)
7816+
self.assertIs(self.sentinel_type, loaded)
77217817

77227818
@skip_if_py313_beta_1
77237819
def test_doc(self):
7724-
self.assertIsInstance(NoDefault.__doc__, str)
7820+
self.assertIsInstance(self.sentinel_type.__doc__, str)
77257821

77267822
def test_constructor(self):
7727-
self.assertIs(NoDefault, type(NoDefault)())
7823+
self.assertIs(self.sentinel_type, type(self.sentinel_type)())
77287824
with self.assertRaises(TypeError):
7729-
type(NoDefault)(1)
7730-
7731-
def test_repr(self):
7732-
self.assertRegex(repr(NoDefault), r'typing(_extensions)?\.NoDefault')
7825+
type(self.sentinel_type)(1)
77337826

77347827
def test_no_call(self):
77357828
with self.assertRaises(TypeError):
7736-
NoDefault()
7829+
self.sentinel_type()
77377830

77387831
@skip_if_py313_beta_1
77397832
def test_immutable(self):
77407833
with self.assertRaises(AttributeError):
7741-
NoDefault.foo = 'bar'
7834+
self.sentinel_type.foo = 'bar'
77427835
with self.assertRaises(AttributeError):
7743-
NoDefault.foo
7836+
self.sentinel_type.foo
77447837

77457838
# TypeError is consistent with the behavior of NoneType
77467839
with self.assertRaises(TypeError):
7747-
type(NoDefault).foo = 3
7840+
type(self.sentinel_type).foo = 3
77487841
with self.assertRaises(AttributeError):
7749-
type(NoDefault).foo
7842+
type(self.sentinel_type).foo
7843+
7844+
7845+
class NoDefaultTests(SentinelTestsMixin, BaseTestCase):
7846+
sentinel_type = NoDefault
7847+
7848+
def test_repr(self):
7849+
if hasattr(typing, 'NoDefault'):
7850+
mod_name = 'typing'
7851+
else:
7852+
mod_name = "typing_extensions"
7853+
self.assertEqual(repr(NoDefault), f"{mod_name}.NoDefault")
7854+
7855+
7856+
class NoExtraItemsTests(SentinelTestsMixin, BaseTestCase):
7857+
sentinel_type = NoExtraItems
7858+
7859+
def test_repr(self):
7860+
if hasattr(typing, 'NoExtraItems'):
7861+
mod_name = 'typing'
7862+
else:
7863+
mod_name = "typing_extensions"
7864+
self.assertEqual(repr(NoExtraItems), f"{mod_name}.NoExtraItems")
77507865

77517866

77527867
class TypeVarInferVarianceTests(BaseTestCase):

src/typing_extensions.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,9 @@ def _is_dunder(attr):
555555

556556

557557
class _SpecialGenericAlias(typing._SpecialGenericAlias, _root=True):
558-
def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()):
558+
def __init__(self, origin, nparams, *, defaults, inst=True, name=None):
559+
assert nparams > 0, "`nparams` must be a positive integer"
560+
assert defaults, "Must always specify a non-empty sequence for `defaults`"
559561
super().__init__(origin, nparams, inst=inst, name=name)
560562
self._defaults = defaults
561563

@@ -573,20 +575,14 @@ def __getitem__(self, params):
573575
msg = "Parameters to generic types must be types."
574576
params = tuple(typing._type_check(p, msg) for p in params)
575577
if (
576-
self._defaults
577-
and len(params) < self._nparams
578+
len(params) < self._nparams
578579
and len(params) + len(self._defaults) >= self._nparams
579580
):
580581
params = (*params, *self._defaults[len(params) - self._nparams:])
581582
actual_len = len(params)
582583

583584
if actual_len != self._nparams:
584-
if self._defaults:
585-
expected = f"at least {self._nparams - len(self._defaults)}"
586-
else:
587-
expected = str(self._nparams)
588-
if not self._nparams:
589-
raise TypeError(f"{self} is not a generic class")
585+
expected = f"at least {self._nparams - len(self._defaults)}"
590586
raise TypeError(
591587
f"Too {'many' if actual_len > self._nparams else 'few'}"
592588
f" arguments for {self};"
@@ -1960,6 +1956,9 @@ def __reduce__(self):
19601956
def __call__(self, *args, **kwargs):
19611957
pass
19621958

1959+
def __init_subclass__(cls) -> None:
1960+
raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type")
1961+
19631962

19641963
# 3.9
19651964
if not hasattr(typing, 'Concatenate'):

0 commit comments

Comments
 (0)