From 54644c4ff7a1cf1d2a2ab4cceb194d25429ffc3b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 14 May 2024 16:05:49 -0400 Subject: [PATCH 1/6] Allow type parameters without default values to follow those with default values in some situations --- CHANGELOG.md | 4 ++++ src/test_typing_extensions.py | 18 +++++++++++++++ src/typing_extensions.py | 43 +++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a633bd..565d174c 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. - 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/src/test_typing_extensions.py b/src/test_typing_extensions.py index f3bfae1c..fc682675 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6430,6 +6430,24 @@ def test_pickle(self): self.assertEqual(z.__bound__, typevar.__bound__) self.assertEqual(z.__default__, typevar.__default__) + 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 b4ca1bc2..6b02eccd 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2847,6 +2847,19 @@ def _check_generic(cls, parameters, elen): if not _PEP_696_IMPLEMENTED: 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 + } + + # 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): @@ -2858,19 +2871,24 @@ 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 + # if it was a direct call to `Generic[]` or `Protocol[]` default_encountered = False + enforce_default_ordering = _has_generic_or_protocol_as_origin() + 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: - 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: + if getattr(t, '__default__', NoDefault) is not NoDefault: + 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): @@ -2888,8 +2906,12 @@ def _collect_parameters(args): assert _collect_parameters((T, Callable[P, T])) == (T, P) """ parameters = [] + # required TypeVarLike cannot appear after TypeVarLike with default + # if it was a direct call to `Generic[]` or `Protocol[]` default_encountered = False + enforce_default_ordering = _has_generic_or_protocol_as_origin() + for t in args: if isinstance(t, type): # We don't want __parameters__ descriptor of a bare Python class. @@ -2903,11 +2925,12 @@ 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: - 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: + if getattr(t, '__default__', NoDefault) is not NoDefault: + 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: From 6f91012b64eb683ed17398521780b3cde40dfbc6 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 14 May 2024 16:09:20 -0400 Subject: [PATCH 2/6] skip on py313 b1 --- src/test_typing_extensions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fc682675..fa436397 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6430,6 +6430,7 @@ 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') From c05fdf6786e7fed3627078c97750eb3353353728 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 15 May 2024 08:36:59 -0400 Subject: [PATCH 3/6] fixup --- src/typing_extensions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1c9c3a3c..2263b0fa 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2896,10 +2896,10 @@ def _collect_type_vars(types, typevar_types=None): tvars = [] # required TypeVarLike cannot appear after TypeVarLike with default - # if it was a direct call to `Generic[]` or `Protocol[]` default_encountered = False # or after TypeVarTuple type_var_tuple_encountered = False + # if it was a direct call to `Generic[]` or `Protocol[]` enforce_default_ordering = _has_generic_or_protocol_as_origin() for t in types: if _is_unpacked_typevartuple(t): @@ -2954,7 +2954,9 @@ def _collect_parameters(args): elif hasattr(t, '__typing_subst__'): if t not in parameters: if enforce_default_ordering: - has_default = 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' From aafe92e550c6269afd7053d5410b8c09141326da Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 15 May 2024 08:37:29 -0400 Subject: [PATCH 4/6] here too --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 2263b0fa..5c2514f0 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2934,8 +2934,8 @@ def _collect_parameters(args): parameters = [] # required TypeVarLike cannot appear after TypeVarLike with default - # if it was a direct call to `Generic[]` or `Protocol[]` default_encountered = False + # if it was a direct call to `Generic[]` or `Protocol[]` enforce_default_ordering = _has_generic_or_protocol_as_origin() # or after TypeVarTuple From 6ba787115b4d5bac7d92a124350379b6384014b7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 15 May 2024 13:35:05 -0400 Subject: [PATCH 5/6] more merge fixup --- src/typing_extensions.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5c2514f0..1af2442d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2895,12 +2895,14 @@ def _collect_type_vars(types, typevar_types=None): typevar_types = typing.TypeVar tvars = [] - # required TypeVarLike cannot appear after TypeVarLike with default - default_encountered = False - # or after TypeVarTuple - type_var_tuple_encountered = False + # A required TypeVarLike cannot appear after 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 + + # A TypeVarLike with a default also cannot appear after a TypeVarTuple + type_var_tuple_encountered = False + for t in types: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True @@ -2934,12 +2936,12 @@ def _collect_parameters(args): parameters = [] # required TypeVarLike cannot appear after TypeVarLike with default - default_encountered = False # 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 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. From 3f66c82afa41b5c3b452ae62efcd114c84ca4548 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 15 May 2024 13:37:36 -0400 Subject: [PATCH 6/6] oops --- src/typing_extensions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1af2442d..f6039883 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2895,12 +2895,12 @@ def _collect_type_vars(types, typevar_types=None): typevar_types = typing.TypeVar tvars = [] - # A 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 - # A TypeVarLike with a default also cannot appear after a TypeVarTuple + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple type_var_tuple_encountered = False for t in types: @@ -2935,11 +2935,12 @@ def _collect_parameters(args): """ 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 - # Also, a TypeVarLike with a default cannot appear after TypeVarTuple + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple type_var_tuple_encountered = False for t in args: