Skip to content

Commit 028035e

Browse files
authored
Backport banning type parameters with defaults after TypeVarTuples (#389)
1 parent 0bdf828 commit 028035e

File tree

4 files changed

+73
-8
lines changed

4 files changed

+73
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
at runtime rather than `types.NoneType`.
1212
- Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python
1313
3.13.0b1 and newer.
14+
- It is now disallowed to use a `TypeVar` with a default value after a
15+
`TypeVarTuple` in a type parameter list. This matches the CPython
16+
implementation of PEP 696 on Python 3.13+.
1417
- Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new
1518
`__static_attributes__` attribute to all classes in Python,
1619
which broke some assumptions made by the implementation of

doc/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,12 @@ Special typing primitives
549549
TypeVarTuples now have a ``has_default()`` method, for compatibility
550550
with :py:class:`typing.TypeVarTuple` on Python 3.13+.
551551

552+
.. versionchanged:: 4.12.0
553+
554+
It is now disallowed to use a `TypeVar` with a default value after a
555+
`TypeVarTuple` in a type parameter list. This matches the CPython
556+
implementation of PEP 696 on Python 3.13+.
557+
552558
.. data:: Unpack
553559

554560
See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11.

src/test_typing_extensions.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6380,6 +6380,14 @@ class A(Generic[P]): ...
63806380
self.assertIs(P_default.__default__, ...)
63816381
self.assertTrue(P_default.has_default())
63826382

6383+
def test_paramspec_none(self):
6384+
U = ParamSpec('U')
6385+
U_None = ParamSpec('U_None', default=None)
6386+
self.assertIs(U.__default__, NoDefault)
6387+
self.assertFalse(U.has_default())
6388+
self.assertIs(U_None.__default__, None)
6389+
self.assertTrue(U_None.has_default())
6390+
63836391
def test_typevartuple(self):
63846392
Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]])
63856393
self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]])
@@ -6394,7 +6402,26 @@ def test_typevartuple(self):
63946402
class A(Generic[Unpack[Ts]]): ...
63956403
Alias = Optional[Unpack[Ts]]
63966404

6397-
def test_erroneous_generic(self):
6405+
def test_no_default_after_typevar_tuple(self):
6406+
T = TypeVar("T", default=int)
6407+
Ts = TypeVarTuple("Ts")
6408+
Ts_default = TypeVarTuple("Ts_default", default=Unpack[Tuple[str, int]])
6409+
6410+
with self.assertRaises(TypeError):
6411+
class X(Generic[Unpack[Ts], T]): ...
6412+
6413+
with self.assertRaises(TypeError):
6414+
class Y(Generic[Unpack[Ts_default], T]): ...
6415+
6416+
def test_typevartuple_none(self):
6417+
U = TypeVarTuple('U')
6418+
U_None = TypeVarTuple('U_None', default=None)
6419+
self.assertIs(U.__default__, NoDefault)
6420+
self.assertFalse(U.has_default())
6421+
self.assertIs(U_None.__default__, None)
6422+
self.assertTrue(U_None.has_default())
6423+
6424+
def test_no_default_after_non_default(self):
63986425
DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
63996426
T = TypeVar('T')
64006427

src/typing_extensions.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2856,6 +2856,21 @@ def _check_generic(cls, parameters, elen):
28562856
if not _PEP_696_IMPLEMENTED:
28572857
typing._check_generic = _check_generic
28582858

2859+
2860+
_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)}
2861+
2862+
2863+
def _is_unpacked_typevartuple(x) -> bool:
2864+
if get_origin(x) is not Unpack:
2865+
return False
2866+
args = get_args(x)
2867+
return (
2868+
bool(args)
2869+
and len(args) == 1
2870+
and type(args[0]) in _TYPEVARTUPLE_TYPES
2871+
)
2872+
2873+
28592874
# Python 3.11+ _collect_type_vars was renamed to _collect_parameters
28602875
if hasattr(typing, '_collect_type_vars'):
28612876
def _collect_type_vars(types, typevar_types=None):
@@ -2869,13 +2884,17 @@ def _collect_type_vars(types, typevar_types=None):
28692884
tvars = []
28702885
# required TypeVarLike cannot appear after TypeVarLike with default
28712886
default_encountered = False
2887+
# or after TypeVarTuple
2888+
type_var_tuple_encountered = False
28722889
for t in types:
2873-
if (
2874-
isinstance(t, typevar_types) and
2875-
t not in tvars and
2876-
not _is_unpack(t)
2877-
):
2878-
if getattr(t, '__default__', NoDefault) is not NoDefault:
2890+
if _is_unpacked_typevartuple(t):
2891+
type_var_tuple_encountered = True
2892+
elif isinstance(t, typevar_types) and t not in tvars:
2893+
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
2894+
if has_default:
2895+
if type_var_tuple_encountered:
2896+
raise TypeError('Type parameter with a default'
2897+
' follows TypeVarTuple')
28792898
default_encountered = True
28802899
elif default_encountered:
28812900
raise TypeError(f'Type parameter {t!r} without a default'
@@ -2899,6 +2918,8 @@ def _collect_parameters(args):
28992918
parameters = []
29002919
# required TypeVarLike cannot appear after TypeVarLike with default
29012920
default_encountered = False
2921+
# or after TypeVarTuple
2922+
type_var_tuple_encountered = False
29022923
for t in args:
29032924
if isinstance(t, type):
29042925
# We don't want __parameters__ descriptor of a bare Python class.
@@ -2912,14 +2933,22 @@ def _collect_parameters(args):
29122933
parameters.append(collected)
29132934
elif hasattr(t, '__typing_subst__'):
29142935
if t not in parameters:
2915-
if getattr(t, '__default__', NoDefault) is not NoDefault:
2936+
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
2937+
2938+
if type_var_tuple_encountered and has_default:
2939+
raise TypeError('Type parameter with a default'
2940+
' follows TypeVarTuple')
2941+
2942+
if has_default:
29162943
default_encountered = True
29172944
elif default_encountered:
29182945
raise TypeError(f'Type parameter {t!r} without a default'
29192946
' follows type parameter with a default')
29202947

29212948
parameters.append(t)
29222949
else:
2950+
if _is_unpacked_typevartuple(t):
2951+
type_var_tuple_encountered = True
29232952
for x in getattr(t, '__parameters__', ()):
29242953
if x not in parameters:
29252954
parameters.append(x)

0 commit comments

Comments
 (0)