Skip to content

Commit 479ac33

Browse files
authored
Allow type parameters without default values to follow those with default values in some situations (#392)
1 parent 028035e commit 479ac33

File tree

3 files changed

+71
-22
lines changed

3 files changed

+71
-22
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
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+
- Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774),
15+
allowing type parameters without default values to follow those with
16+
default values in some type parameter lists. Patch by Alex Waygood,
17+
backporting a CPython PR by Jelle Zijlstra.
1418
- It is now disallowed to use a `TypeVar` with a default value after a
1519
`TypeVarTuple` in a type parameter list. This matches the CPython
1620
implementation of PEP 696 on Python 3.13+.

src/test_typing_extensions.py

+19
Original file line numberDiff line numberDiff line change
@@ -6457,6 +6457,25 @@ def test_pickle(self):
64576457
self.assertEqual(z.__bound__, typevar.__bound__)
64586458
self.assertEqual(z.__default__, typevar.__default__)
64596459

6460+
@skip_if_py313_beta_1
6461+
def test_allow_default_after_non_default_in_alias(self):
6462+
T_default = TypeVar('T_default', default=int)
6463+
T = TypeVar('T')
6464+
Ts = TypeVarTuple('Ts')
6465+
6466+
a1 = Callable[[T_default], T]
6467+
self.assertEqual(a1.__args__, (T_default, T))
6468+
6469+
if sys.version_info >= (3, 9):
6470+
a2 = dict[T_default, T]
6471+
self.assertEqual(a2.__args__, (T_default, T))
6472+
6473+
a3 = typing.Dict[T_default, T]
6474+
self.assertEqual(a3.__args__, (T_default, T))
6475+
6476+
a4 = Callable[[Unpack[Ts]], T]
6477+
self.assertEqual(a4.__args__, (Unpack[Ts], T))
6478+
64606479

64616480
class NoDefaultTests(BaseTestCase):
64626481
@skip_if_py313_beta_1

src/typing_extensions.py

+48-22
Original file line numberDiff line numberDiff line change
@@ -2857,6 +2857,18 @@ def _check_generic(cls, parameters, elen):
28572857
typing._check_generic = _check_generic
28582858

28592859

2860+
def _has_generic_or_protocol_as_origin() -> bool:
2861+
try:
2862+
frame = sys._getframe(2)
2863+
# not all platforms have sys._getframe()
2864+
except AttributeError:
2865+
return False # err on the side of leniency
2866+
else:
2867+
return frame.f_locals.get("origin") in {
2868+
typing.Generic, Protocol, typing.Protocol
2869+
}
2870+
2871+
28602872
_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)}
28612873

28622874

@@ -2882,23 +2894,29 @@ def _collect_type_vars(types, typevar_types=None):
28822894
if typevar_types is None:
28832895
typevar_types = typing.TypeVar
28842896
tvars = []
2885-
# required TypeVarLike cannot appear after TypeVarLike with default
2897+
2898+
# A required TypeVarLike cannot appear after a TypeVarLike with a default
2899+
# if it was a direct call to `Generic[]` or `Protocol[]`
2900+
enforce_default_ordering = _has_generic_or_protocol_as_origin()
28862901
default_encountered = False
2887-
# or after TypeVarTuple
2902+
2903+
# Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
28882904
type_var_tuple_encountered = False
2905+
28892906
for t in types:
28902907
if _is_unpacked_typevartuple(t):
28912908
type_var_tuple_encountered = True
28922909
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')
2898-
default_encountered = True
2899-
elif default_encountered:
2900-
raise TypeError(f'Type parameter {t!r} without a default'
2901-
' follows type parameter with a default')
2910+
if enforce_default_ordering:
2911+
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
2912+
if has_default:
2913+
if type_var_tuple_encountered:
2914+
raise TypeError('Type parameter with a default'
2915+
' follows TypeVarTuple')
2916+
default_encountered = True
2917+
elif default_encountered:
2918+
raise TypeError(f'Type parameter {t!r} without a default'
2919+
' follows type parameter with a default')
29022920

29032921
tvars.append(t)
29042922
if _should_collect_from_parameters(t):
@@ -2916,10 +2934,15 @@ def _collect_parameters(args):
29162934
assert _collect_parameters((T, Callable[P, T])) == (T, P)
29172935
"""
29182936
parameters = []
2919-
# required TypeVarLike cannot appear after TypeVarLike with default
2937+
2938+
# A required TypeVarLike cannot appear after a TypeVarLike with default
2939+
# if it was a direct call to `Generic[]` or `Protocol[]`
2940+
enforce_default_ordering = _has_generic_or_protocol_as_origin()
29202941
default_encountered = False
2921-
# or after TypeVarTuple
2942+
2943+
# Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
29222944
type_var_tuple_encountered = False
2945+
29232946
for t in args:
29242947
if isinstance(t, type):
29252948
# We don't want __parameters__ descriptor of a bare Python class.
@@ -2933,17 +2956,20 @@ def _collect_parameters(args):
29332956
parameters.append(collected)
29342957
elif hasattr(t, '__typing_subst__'):
29352958
if t not in parameters:
2936-
has_default = getattr(t, '__default__', NoDefault) is not NoDefault
2959+
if enforce_default_ordering:
2960+
has_default = (
2961+
getattr(t, '__default__', NoDefault) is not NoDefault
2962+
)
29372963

2938-
if type_var_tuple_encountered and has_default:
2939-
raise TypeError('Type parameter with a default'
2940-
' follows TypeVarTuple')
2964+
if type_var_tuple_encountered and has_default:
2965+
raise TypeError('Type parameter with a default'
2966+
' follows TypeVarTuple')
29412967

2942-
if has_default:
2943-
default_encountered = True
2944-
elif default_encountered:
2945-
raise TypeError(f'Type parameter {t!r} without a default'
2946-
' follows type parameter with a default')
2968+
if has_default:
2969+
default_encountered = True
2970+
elif default_encountered:
2971+
raise TypeError(f'Type parameter {t!r} without a default'
2972+
' follows type parameter with a default')
29472973

29482974
parameters.append(t)
29492975
else:

0 commit comments

Comments
 (0)