Skip to content

Commit 8170fc7

Browse files
NCPlayzJelleZijlstraGobot1234cdce8p
authored
Fix runtime behaviour of PEP 696 (#293)
Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: James Hilton-Balfe <[email protected]> Co-authored-by: Marc Mueller <[email protected]>
1 parent d34c389 commit 8170fc7

File tree

3 files changed

+166
-45
lines changed

3 files changed

+166
-45
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- Fix the runtime behavior of type parameters with defaults (PEP 696).
4+
Patch by Nadir Chowdhury.
35
- Fix minor discrepancy between error messages produced by `typing`
46
and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra.
57
- When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation.

Diff for: src/test_typing_extensions.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -5712,7 +5712,6 @@ class Y(Generic[T], NamedTuple):
57125712
self.assertEqual(a.x, 3)
57135713

57145714
things = "arguments" if sys.version_info >= (3, 10) else "parameters"
5715-
57165715
with self.assertRaisesRegex(TypeError, f'Too many {things}'):
57175716
G[int, str]
57185717

@@ -6215,6 +6214,27 @@ def test_typevartuple(self):
62156214
class A(Generic[Unpack[Ts]]): ...
62166215
Alias = Optional[Unpack[Ts]]
62176216

6217+
def test_erroneous_generic(self):
6218+
DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
6219+
T = TypeVar('T')
6220+
6221+
with self.assertRaises(TypeError):
6222+
Test = Generic[DefaultStrT, T]
6223+
6224+
def test_need_more_params(self):
6225+
DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
6226+
T = typing_extensions.TypeVar('T')
6227+
U = typing_extensions.TypeVar('U')
6228+
6229+
class A(Generic[T, U, DefaultStrT]): ...
6230+
A[int, bool]
6231+
A[int, bool, str]
6232+
6233+
with self.assertRaises(
6234+
TypeError, msg="Too few arguments for .+; actual 1, expected at least 2"
6235+
):
6236+
Test = A[int]
6237+
62186238
def test_pickle(self):
62196239
global U, U_co, U_contra, U_default # pickle wants to reference the class by name
62206240
U = typing_extensions.TypeVar('U')

Diff for: src/typing_extensions.py

+143-44
Original file line numberDiff line numberDiff line change
@@ -147,28 +147,6 @@ def __repr__(self):
147147
_marker = _Sentinel()
148148

149149

150-
def _check_generic(cls, parameters, elen=_marker):
151-
"""Check correct count for parameters of a generic cls (internal helper).
152-
This gives a nice error message in case of count mismatch.
153-
"""
154-
if not elen:
155-
raise TypeError(f"{cls} is not a generic class")
156-
if elen is _marker:
157-
if not hasattr(cls, "__parameters__") or not cls.__parameters__:
158-
raise TypeError(f"{cls} is not a generic class")
159-
elen = len(cls.__parameters__)
160-
alen = len(parameters)
161-
if alen != elen:
162-
if hasattr(cls, "__parameters__"):
163-
parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
164-
num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
165-
if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
166-
return
167-
things = "arguments" if sys.version_info >= (3, 10) else "parameters"
168-
raise TypeError(f"Too {'many' if alen > elen else 'few'} {things} for {cls};"
169-
f" actual {alen}, expected {elen}")
170-
171-
172150
if sys.version_info >= (3, 10):
173151
def _should_collect_from_parameters(t):
174152
return isinstance(
@@ -182,27 +160,6 @@ def _should_collect_from_parameters(t):
182160
return isinstance(t, typing._GenericAlias) and not t._special
183161

184162

185-
def _collect_type_vars(types, typevar_types=None):
186-
"""Collect all type variable contained in types in order of
187-
first appearance (lexicographic order). For example::
188-
189-
_collect_type_vars((T, List[S, T])) == (T, S)
190-
"""
191-
if typevar_types is None:
192-
typevar_types = typing.TypeVar
193-
tvars = []
194-
for t in types:
195-
if (
196-
isinstance(t, typevar_types) and
197-
t not in tvars and
198-
not _is_unpack(t)
199-
):
200-
tvars.append(t)
201-
if _should_collect_from_parameters(t):
202-
tvars.extend([t for t in t.__parameters__ if t not in tvars])
203-
return tuple(tvars)
204-
205-
206163
NoReturn = typing.NoReturn
207164

208165
# Some unconstrained type variables. These are used by the container types.
@@ -2690,9 +2647,151 @@ def wrapper(*args, **kwargs):
26902647
# counting generic parameters, so that when we subscript a generic,
26912648
# the runtime doesn't try to substitute the Unpack with the subscripted type.
26922649
if not hasattr(typing, "TypeVarTuple"):
2650+
def _check_generic(cls, parameters, elen=_marker):
2651+
"""Check correct count for parameters of a generic cls (internal helper).
2652+
2653+
This gives a nice error message in case of count mismatch.
2654+
"""
2655+
if not elen:
2656+
raise TypeError(f"{cls} is not a generic class")
2657+
if elen is _marker:
2658+
if not hasattr(cls, "__parameters__") or not cls.__parameters__:
2659+
raise TypeError(f"{cls} is not a generic class")
2660+
elen = len(cls.__parameters__)
2661+
alen = len(parameters)
2662+
if alen != elen:
2663+
expect_val = elen
2664+
if hasattr(cls, "__parameters__"):
2665+
parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
2666+
num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
2667+
if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
2668+
return
2669+
2670+
# deal with TypeVarLike defaults
2671+
# required TypeVarLikes cannot appear after a defaulted one.
2672+
if alen < elen:
2673+
# since we validate TypeVarLike default in _collect_type_vars
2674+
# or _collect_parameters we can safely check parameters[alen]
2675+
if getattr(parameters[alen], '__default__', None) is not None:
2676+
return
2677+
2678+
num_default_tv = sum(getattr(p, '__default__', None)
2679+
is not None for p in parameters)
2680+
2681+
elen -= num_default_tv
2682+
2683+
expect_val = f"at least {elen}"
2684+
2685+
things = "arguments" if sys.version_info >= (3, 10) else "parameters"
2686+
raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}"
2687+
f" for {cls}; actual {alen}, expected {expect_val}")
2688+
else:
2689+
# Python 3.11+
2690+
2691+
def _check_generic(cls, parameters, elen):
2692+
"""Check correct count for parameters of a generic cls (internal helper).
2693+
2694+
This gives a nice error message in case of count mismatch.
2695+
"""
2696+
if not elen:
2697+
raise TypeError(f"{cls} is not a generic class")
2698+
alen = len(parameters)
2699+
if alen != elen:
2700+
expect_val = elen
2701+
if hasattr(cls, "__parameters__"):
2702+
parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
2703+
2704+
# deal with TypeVarLike defaults
2705+
# required TypeVarLikes cannot appear after a defaulted one.
2706+
if alen < elen:
2707+
# since we validate TypeVarLike default in _collect_type_vars
2708+
# or _collect_parameters we can safely check parameters[alen]
2709+
if getattr(parameters[alen], '__default__', None) is not None:
2710+
return
2711+
2712+
num_default_tv = sum(getattr(p, '__default__', None)
2713+
is not None for p in parameters)
2714+
2715+
elen -= num_default_tv
2716+
2717+
expect_val = f"at least {elen}"
2718+
2719+
raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments"
2720+
f" for {cls}; actual {alen}, expected {expect_val}")
2721+
2722+
typing._check_generic = _check_generic
2723+
2724+
# Python 3.11+ _collect_type_vars was renamed to _collect_parameters
2725+
if hasattr(typing, '_collect_type_vars'):
2726+
def _collect_type_vars(types, typevar_types=None):
2727+
"""Collect all type variable contained in types in order of
2728+
first appearance (lexicographic order). For example::
2729+
2730+
_collect_type_vars((T, List[S, T])) == (T, S)
2731+
"""
2732+
if typevar_types is None:
2733+
typevar_types = typing.TypeVar
2734+
tvars = []
2735+
# required TypeVarLike cannot appear after TypeVarLike with default
2736+
default_encountered = False
2737+
for t in types:
2738+
if (
2739+
isinstance(t, typevar_types) and
2740+
t not in tvars and
2741+
not _is_unpack(t)
2742+
):
2743+
if getattr(t, '__default__', None) is not None:
2744+
default_encountered = True
2745+
elif default_encountered:
2746+
raise TypeError(f'Type parameter {t!r} without a default'
2747+
' follows type parameter with a default')
2748+
2749+
tvars.append(t)
2750+
if _should_collect_from_parameters(t):
2751+
tvars.extend([t for t in t.__parameters__ if t not in tvars])
2752+
return tuple(tvars)
2753+
26932754
typing._collect_type_vars = _collect_type_vars
2694-
typing._check_generic = _check_generic
2755+
else:
2756+
def _collect_parameters(args):
2757+
"""Collect all type variables and parameter specifications in args
2758+
in order of first appearance (lexicographic order).
2759+
2760+
For example::
2761+
2762+
assert _collect_parameters((T, Callable[P, T])) == (T, P)
2763+
"""
2764+
parameters = []
2765+
# required TypeVarLike cannot appear after TypeVarLike with default
2766+
default_encountered = False
2767+
for t in args:
2768+
if isinstance(t, type):
2769+
# We don't want __parameters__ descriptor of a bare Python class.
2770+
pass
2771+
elif isinstance(t, tuple):
2772+
# `t` might be a tuple, when `ParamSpec` is substituted with
2773+
# `[T, int]`, or `[int, *Ts]`, etc.
2774+
for x in t:
2775+
for collected in _collect_parameters([x]):
2776+
if collected not in parameters:
2777+
parameters.append(collected)
2778+
elif hasattr(t, '__typing_subst__'):
2779+
if t not in parameters:
2780+
if getattr(t, '__default__', None) is not None:
2781+
default_encountered = True
2782+
elif default_encountered:
2783+
raise TypeError(f'Type parameter {t!r} without a default'
2784+
' follows type parameter with a default')
2785+
2786+
parameters.append(t)
2787+
else:
2788+
for x in getattr(t, '__parameters__', ()):
2789+
if x not in parameters:
2790+
parameters.append(x)
2791+
2792+
return tuple(parameters)
26952793

2794+
typing._collect_parameters = _collect_parameters
26962795

26972796
# Backport typing.NamedTuple as it exists in Python 3.13.
26982797
# In 3.11, the ability to define generic `NamedTuple`s was supported.

0 commit comments

Comments
 (0)