Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix runtime behaviour of PEP 696 #293

Merged
merged 16 commits into from
Mar 12, 2024
24 changes: 21 additions & 3 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3262,6 +3262,9 @@ def __call__(self, *args: Unpack[Ts]) -> T: ...
self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2))
self.assertTrue(MemoizedFunc._is_protocol)

# 3.11+ moves where this exception is thrown
# 3.11: TypeVarTuple.__typing_prepare_subst__
# 3.12+: typing._typevartuple_prepare_subst
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
things = "arguments" if sys.version_info >= (3, 11) else "parameters"
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved

# A bug was fixed in 3.11.1
Expand Down Expand Up @@ -5711,9 +5714,7 @@ class Y(Generic[T], NamedTuple):
self.assertIsInstance(a, G)
self.assertEqual(a.x, 3)

things = "arguments" if sys.version_info >= (3, 11) else "parameters"

with self.assertRaisesRegex(TypeError, f'Too many {things}'):
with self.assertRaisesRegex(TypeError, f'Too many parameters'):
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
G[int, str]

@skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9")
Expand Down Expand Up @@ -6215,6 +6216,23 @@ def test_typevartuple(self):
class A(Generic[Unpack[Ts]]): ...
Alias = Optional[Unpack[Ts]]

def test_erroneous_generic(self):
DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
T = TypeVar('T')

with self.assertRaises(TypeError):
Test = Generic[DefaultStrT, T]

def test_need_more_params(self):
DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
T = typing_extensions.TypeVar('T')
U = typing_extensions.TypeVar('U')

class A(Generic[T, U, DefaultStrT]): ...
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved

with self.assertRaises(TypeError):
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
Test = A[int]

def test_pickle(self):
global U, U_co, U_contra, U_default # pickle wants to reference the class by name
U = typing_extensions.TypeVar('U')
Expand Down
189 changes: 146 additions & 43 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,27 +147,6 @@ def __repr__(self):
_marker = _Sentinel()


def _check_generic(cls, parameters, elen=_marker):
"""Check correct count for parameters of a generic cls (internal helper).
This gives a nice error message in case of count mismatch.
"""
if not elen:
raise TypeError(f"{cls} is not a generic class")
if elen is _marker:
if not hasattr(cls, "__parameters__") or not cls.__parameters__:
raise TypeError(f"{cls} is not a generic class")
elen = len(cls.__parameters__)
alen = len(parameters)
if alen != elen:
if hasattr(cls, "__parameters__"):
parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
return
raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
f" actual {alen}, expected {elen}")


if sys.version_info >= (3, 10):
def _should_collect_from_parameters(t):
return isinstance(
Expand All @@ -181,27 +160,6 @@ def _should_collect_from_parameters(t):
return isinstance(t, typing._GenericAlias) and not t._special


def _collect_type_vars(types, typevar_types=None):
"""Collect all type variable contained in types in order of
first appearance (lexicographic order). For example::

_collect_type_vars((T, List[S, T])) == (T, S)
"""
if typevar_types is None:
typevar_types = typing.TypeVar
tvars = []
for t in types:
if (
isinstance(t, typevar_types) and
t not in tvars and
not _is_unpack(t)
):
tvars.append(t)
if _should_collect_from_parameters(t):
tvars.extend([t for t in t.__parameters__ if t not in tvars])
return tuple(tvars)


NoReturn = typing.NoReturn

# Some unconstrained type variables. These are used by the container types.
Expand Down Expand Up @@ -2689,9 +2647,154 @@ def wrapper(*args, **kwargs):
# counting generic parameters, so that when we subscript a generic,
# the runtime doesn't try to substitute the Unpack with the subscripted type.
if not hasattr(typing, "TypeVarTuple"):
def _check_generic(cls, parameters, elen=_marker):
"""Check correct count for parameters of a generic cls (internal helper).
This gives a nice error message in case of count mismatch.
"""
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
if not elen:
raise TypeError(f"{cls} is not a generic class")
if elen is _marker:
if not hasattr(cls, "__parameters__") or not cls.__parameters__:
raise TypeError(f"{cls} is not a generic class")
elen = len(cls.__parameters__)
alen = len(parameters)
if alen != elen:
expect_val = elen
if hasattr(cls, "__parameters__"):
parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
return

# deal with TypeVarLike defaults
# required TypeVarLikes cannot appear after a defaulted one.
if alen < elen:
# since we validate TypeVarLike default in _collect_type_vars
# or _collect_parameters we can safely check parameters[alen]
if getattr(parameters[alen], '__default__', None) is not None:
return

num_default_tv = sum(getattr(p, '__default__', None)
is not None for p in parameters)

elen -= num_default_tv

expect_val = f"at least {elen}"

raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters"
f" for {cls}; actual {alen}, expected {expect_val}")
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
else:
# Python 3.11+

def _check_generic(cls, parameters, elen):
"""Check correct count for parameters of a generic cls (internal helper).

This gives a nice error message in case of count mismatch.
"""
if not elen:
raise TypeError(f"{cls} is not a generic class")
alen = len(parameters)
if alen != elen:
expect_val = elen
if hasattr(cls, "__parameters__"):
parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
return

# deal with TypeVarLike defaults
# required TypeVarLikes cannot appear after a defaulted one.
if alen < elen:
# since we validate TypeVarLike default in _collect_type_vars
# or _collect_parameters we can safely check parameters[alen]
if getattr(parameters[alen], '__default__', None) is not None:
return

num_default_tv = sum(getattr(p, '__default__', None)
is not None for p in parameters)

elen -= num_default_tv

expect_val = f"at least {elen}"

raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters"
f" for {cls}; actual {alen}, expected {expect_val}")
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved

typing._check_generic = _check_generic

# 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):
"""Collect all type variable contained in types in order of
first appearance (lexicographic order). For example::

_collect_type_vars((T, List[S, T])) == (T, S)
"""
if typevar_types is None:
typevar_types = typing.TypeVar
tvars = []
# required TypeVarLike cannot appear after TypeVarLike with default
default_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__', None) is not None:
if not default_encountered:
default_encountered = True
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
elif default_encountered:
raise TypeError(f'type parameter {t!r} without a default'
' follows type parameter with a default')
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved

tvars.append(t)
if _should_collect_from_parameters(t):
tvars.extend([t for t in t.__parameters__ if t not in tvars])
return tuple(tvars)

typing._collect_type_vars = _collect_type_vars
typing._check_generic = _check_generic
else:
def _collect_parameters(args):
"""Collect all type variables and parameter specifications in args
in order of first appearance (lexicographic order).

For example::

assert _collect_parameters((T, Callable[P, T])) == (T, P)
"""
parameters = []
# required TypeVarLike cannot appear after TypeVarLike with default
default_encountered = False
for t in args:
if isinstance(t, type):
# We don't want __parameters__ descriptor of a bare Python class.
pass
elif isinstance(t, tuple):
# `t` might be a tuple, when `ParamSpec` is substituted with
# `[T, int]`, or `[int, *Ts]`, etc.
for x in t:
for collected in _collect_parameters([x]):
if collected not in parameters:
parameters.append(collected)
elif hasattr(t, '__typing_subst__'):
if t not in parameters:
if getattr(t, '__default__', None) is not None:
if not default_encountered:
default_encountered = True
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
elif default_encountered:
raise TypeError(f'type parameter {t!r} without a default'
' follows type parameter with a default')
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved

parameters.append(t)
else:
if _should_collect_from_parameters(t):
NCPlayz marked this conversation as resolved.
Show resolved Hide resolved
parameters.extend(
[t for t in t.__parameters__ if t not in parameters])

return tuple(parameters)

typing._collect_parameters = _collect_parameters

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