diff --git a/CHANGELOG.md b/CHANGELOG.md index 5572a87a..88445bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ at runtime rather than `types.NoneType`. - Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python 3.13.0b1 and newer. +- Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774), + allowing type parameters without default values to follow those with + default values in some type parameter lists. Patch by Alex Waygood, + backporting a CPython PR by Jelle Zijlstra. - 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+. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 820833a9..a0e55f0d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6457,6 +6457,25 @@ def test_pickle(self): self.assertEqual(z.__bound__, typevar.__bound__) self.assertEqual(z.__default__, typevar.__default__) + @skip_if_py313_beta_1 + def test_allow_default_after_non_default_in_alias(self): + T_default = TypeVar('T_default', default=int) + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + a1 = Callable[[T_default], T] + self.assertEqual(a1.__args__, (T_default, T)) + + if sys.version_info >= (3, 9): + a2 = dict[T_default, T] + self.assertEqual(a2.__args__, (T_default, T)) + + a3 = typing.Dict[T_default, T] + self.assertEqual(a3.__args__, (T_default, T)) + + a4 = Callable[[Unpack[Ts]], T] + self.assertEqual(a4.__args__, (Unpack[Ts], T)) + class NoDefaultTests(BaseTestCase): @skip_if_py313_beta_1 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index cf8bde47..f6039883 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2857,6 +2857,18 @@ def _check_generic(cls, parameters, elen): typing._check_generic = _check_generic +def _has_generic_or_protocol_as_origin() -> bool: + try: + frame = sys._getframe(2) + # not all platforms have sys._getframe() + except AttributeError: + return False # err on the side of leniency + else: + return frame.f_locals.get("origin") in { + typing.Generic, Protocol, typing.Protocol + } + + _TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} @@ -2882,23 +2894,29 @@ def _collect_type_vars(types, typevar_types=None): if typevar_types is None: typevar_types = typing.TypeVar tvars = [] - # required TypeVarLike cannot appear after TypeVarLike with default + + # A required TypeVarLike cannot appear after a TypeVarLike with a default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() default_encountered = False - # or after TypeVarTuple + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple type_var_tuple_encountered = False + for t in types: 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' - ' follows type parameter with a default') + if enforce_default_ordering: + 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' + ' follows type parameter with a default') tvars.append(t) if _should_collect_from_parameters(t): @@ -2916,10 +2934,15 @@ def _collect_parameters(args): assert _collect_parameters((T, Callable[P, T])) == (T, P) """ parameters = [] - # required TypeVarLike cannot appear after TypeVarLike with default + + # A required TypeVarLike cannot appear after a TypeVarLike with default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() default_encountered = False - # or after TypeVarTuple + + # Also, a TypeVarLike with a default cannot appear after a 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. @@ -2933,17 +2956,20 @@ def _collect_parameters(args): parameters.append(collected) elif hasattr(t, '__typing_subst__'): if t not in parameters: - has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if enforce_default_ordering: + 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 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' - ' follows type parameter with a default') + if has_default: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') parameters.append(t) else: