diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a633bd..415c944e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ at runtime rather than `types.NoneType`. - Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python 3.13.0b1 and newer. +- It is now disallowed to use a `TypeVar` with a default value after a + `TypeVarTuple` in a type parameter list. This matches the CPython + implementation of PEP 696 on Python 3.13+. - Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of diff --git a/doc/index.rst b/doc/index.rst index 5863fe4c..132d5dc8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -549,6 +549,12 @@ Special typing primitives TypeVarTuples now have a ``has_default()`` method, for compatibility with :py:class:`typing.TypeVarTuple` on Python 3.13+. + .. versionchanged:: 4.12.0 + + It is now disallowed to use a `TypeVar` with a default value after a + `TypeVarTuple` in a type parameter list. This matches the CPython + implementation of PEP 696 on Python 3.13+. + .. data:: Unpack See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f3bfae1c..3a5c1c67 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6380,6 +6380,14 @@ class A(Generic[P]): ... self.assertIs(P_default.__default__, ...) self.assertTrue(P_default.has_default()) + def test_paramspec_none(self): + U = ParamSpec('U') + U_None = ParamSpec('U_None', default=None) + self.assertIs(U.__default__, NoDefault) + self.assertFalse(U.has_default()) + self.assertIs(U_None.__default__, None) + self.assertTrue(U_None.has_default()) + def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) @@ -6394,7 +6402,26 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] - def test_erroneous_generic(self): + def test_no_default_after_typevar_tuple(self): + T = TypeVar("T", default=int) + Ts = TypeVarTuple("Ts") + Ts_default = TypeVarTuple("Ts_default", default=Unpack[Tuple[str, int]]) + + with self.assertRaises(TypeError): + class X(Generic[Unpack[Ts], T]): ... + + with self.assertRaises(TypeError): + class Y(Generic[Unpack[Ts_default], T]): ... + + def test_typevartuple_none(self): + U = TypeVarTuple('U') + U_None = TypeVarTuple('U_None', default=None) + self.assertIs(U.__default__, NoDefault) + self.assertFalse(U.has_default()) + self.assertIs(U_None.__default__, None) + self.assertTrue(U_None.has_default()) + + def test_no_default_after_non_default(self): DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) T = TypeVar('T') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b4ca1bc2..d9323723 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2847,6 +2847,21 @@ def _check_generic(cls, parameters, elen): if not _PEP_696_IMPLEMENTED: typing._check_generic = _check_generic + +_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} + + +def _is_unpacked_typevartuple(x) -> bool: + if get_origin(x) is not Unpack: + return False + args = get_args(x) + return ( + bool(args) + and len(args) == 1 + and type(args[0]) in _TYPEVARTUPLE_TYPES + ) + + # Python 3.11+ _collect_type_vars was renamed to _collect_parameters if hasattr(typing, '_collect_type_vars'): def _collect_type_vars(types, typevar_types=None): @@ -2860,13 +2875,17 @@ def _collect_type_vars(types, typevar_types=None): tvars = [] # required TypeVarLike cannot appear after TypeVarLike with default default_encountered = False + # or after TypeVarTuple + type_var_tuple_encountered = False for t in types: - if ( - isinstance(t, typevar_types) and - t not in tvars and - not _is_unpack(t) - ): - if getattr(t, '__default__', NoDefault) is not NoDefault: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True + elif isinstance(t, typevar_types) and t not in tvars: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if has_default: + if type_var_tuple_encountered: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' @@ -2890,6 +2909,8 @@ def _collect_parameters(args): parameters = [] # required TypeVarLike cannot appear after TypeVarLike with default default_encountered = False + # or after TypeVarTuple + type_var_tuple_encountered = False for t in args: if isinstance(t, type): # We don't want __parameters__ descriptor of a bare Python class. @@ -2903,7 +2924,13 @@ def _collect_parameters(args): parameters.append(collected) elif hasattr(t, '__typing_subst__'): if t not in parameters: - if getattr(t, '__default__', NoDefault) is not NoDefault: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + + if type_var_tuple_encountered and has_default: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + + if has_default: default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' @@ -2911,6 +2938,8 @@ def _collect_parameters(args): parameters.append(t) else: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True for x in getattr(t, '__parameters__', ()): if x not in parameters: parameters.append(x)