From fc3926633064ca444ae76e572bd71116f452c158 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 09:45:31 -0400 Subject: [PATCH 1/6] Add failing tests --- src/test_typing_extensions.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fa04e59e..0ab1e7fb 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6402,6 +6402,26 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] + def test_typevartuple_specialization(self): + T = TypeVar("T") + Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) + self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + class A(Generic[T, Unpack[Ts]]): ... + self.assertEqual(A[float].__args__, (float, str, int)) + self.assertEqual(A[float, range].__args__, (float, range)) + self.assertEqual(A[float, *tuple[int, ...]].__args__, (float, *tuple[int, ...])) + + def test_typevar_and_typevartuple_specialization(self): + T = TypeVar("T") + U = TypeVar("U", default=float) + Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) + self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + class A(Generic[T, U, Unpack[Ts]]): ... + self.assertEqual(A[int].__args__, (int, float, str, int)) + self.assertEqual(A[int, str].__args__, (int, str, str, int)) + self.assertEqual(A[int, str, range].__args__, (int, str, range)) + self.assertEqual(A[int, str, *tuple[int, ...]].__args__, (int, str, *tuple[int, ...])) + def test_no_default_after_typevar_tuple(self): T = TypeVar("T", default=int) Ts = TypeVarTuple("Ts") @@ -6487,6 +6507,34 @@ def test_allow_default_after_non_default_in_alias(self): a4 = Callable[[Unpack[Ts]], T] self.assertEqual(a4.__args__, (Unpack[Ts], T)) + def test_paramspec_specialization(self): + T = TypeVar("T") + P = ParamSpec('P', default=[str, int]) + self.assertEqual(P.__default__, [str, int]) + class A(Generic[T, P]): ... + self.assertEqual(A[float].__args__, (float, (str, int))) + self.assertEqual(A[float, [range]].__args__, (float, (range,))) + + def test_typevar_and_paramspec_specialization(self): + T = TypeVar("T") + U = TypeVar("U", default=float) + P = ParamSpec('P', default=[str, int]) + self.assertEqual(P.__default__, [str, int]) + class A(Generic[T, U, P]): ... + self.assertEqual(A[float].__args__, (float, float, (str, int))) + self.assertEqual(A[float, int].__args__, (float, int, (str, int))) + self.assertEqual(A[float, int, [range]].__args__, (float, int, (range,))) + + def test_paramspec_and_typevar_specialization(self): + T = TypeVar("T") + P = ParamSpec('P', default=[str, int]) + U = TypeVar("U", default=float) + self.assertEqual(P.__default__, [str, int]) + class A(Generic[T, P, U]): ... + self.assertEqual(A[float].__args__, (float, (str, int), float)) + self.assertEqual(A[float, [range]].__args__, (float, (range,), float)) + self.assertEqual(A[float, [range], int].__args__, (float, (range,), int)) + class NoDefaultTests(BaseTestCase): @skip_if_py313_beta_1 From 61337c4e7449ee53cdfd2b1484a1f6cb1968803e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 10:21:21 -0400 Subject: [PATCH 2/6] Make it work on py311-12 --- src/typing_extensions.py | 88 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 07def90e..09f60bcc 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1515,6 +1515,13 @@ def __new__(cls, name, *constraints, bound=None, typevar.__infer_variance__ = infer_variance _set_default(typevar, default) _set_module(typevar) + + def _tvar_prepare_subst(alias, args): + if alias.__parameters__.index(typevar) == len(args) and typevar.has_default(): + args += (typevar.__default__,) + return args + + typevar.__typing_prepare_subst__ = _tvar_prepare_subst return typevar def __init_subclass__(cls) -> None: @@ -1589,7 +1596,6 @@ def __eq__(self, other): # 3.10+ elif hasattr(typing, 'ParamSpec'): - # Add default parameter - PEP 696 class ParamSpec(metaclass=_TypeVarLikeMeta): """Parameter specification.""" @@ -1613,6 +1619,24 @@ def __new__(cls, name, *, bound=None, _set_default(paramspec, default) _set_module(paramspec) + + def _paramspec_prepare_subst(alias, args): + params = alias.__parameters__ + i = params.index(paramspec) + if i == len(args) and paramspec.has_default(): + args = [*args, paramspec.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {alias}") + # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. + if len(params) == 1 and not _is_param_expr(args[0]): + assert i == 0 + args = (args,) + # Convert lists to tuples to help other libraries cache the results. + elif isinstance(args[i], list): + args = (*args[:i], tuple(args[i]), *args[i+1:]) + return args + + paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst return paramspec def __init_subclass__(cls) -> None: @@ -2311,6 +2335,17 @@ def __init__(self, getitem): class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar + @property + def __typing_unpacked_tuple_args__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + arg, = self.__args__ + if isinstance(arg, (typing._GenericAlias, _types.GenericAlias)): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + return arg.__args__ + return None + @_UnpackSpecialForm def Unpack(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2339,6 +2374,15 @@ def _is_unpack(obj): from typing import TypeVarTuple elif hasattr(typing, "TypeVarTuple"): # 3.11+ + def _unpack_args(*args): + newargs = [] + for arg in args: + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs is not None and not (subargs and subargs[-1] is ...): + newargs.extend(subargs) + else: + newargs.append(arg) + return newargs # Add default parameter - PEP 696 class TypeVarTuple(metaclass=_TypeVarLikeMeta): @@ -2350,6 +2394,48 @@ def __new__(cls, name, *, default=NoDefault): tvt = typing.TypeVarTuple(name) _set_default(tvt, default) _set_module(tvt) + + def _typevartuple_prepare_subst(alias, args): + params = alias.__parameters__ + typevartuple_index = params.index(tvt) + for param in params[typevartuple_index + 1:]: + if isinstance(param, TypeVarTuple): + raise TypeError(f"More than one TypeVarTuple parameter in {alias}") + + alen = len(args) + plen = len(params) + left = typevartuple_index + right = plen - typevartuple_index - 1 + var_tuple_index = None + fillarg = None + for k, arg in enumerate(args): + if not isinstance(arg, type): + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs and len(subargs) == 2 and subargs[-1] is ...: + if var_tuple_index is not None: + raise TypeError("More than one unpacked arbitrary-length tuple argument") + var_tuple_index = k + fillarg = subargs[0] + if var_tuple_index is not None: + left = min(left, var_tuple_index) + right = min(right, alen - var_tuple_index - 1) + elif left + right > alen: + raise TypeError(f"Too few arguments for {alias};" + f" actual {alen}, expected at least {plen-1}") + if left == alen - right and tvt.has_default(): + replacement = _unpack_args(tvt.__default__) + else: + replacement = args[left: alen - right] + + return ( + *args[:left], + *([fillarg]*(typevartuple_index - left)), + replacement, + *([fillarg]*(plen - right - left - typevartuple_index - 1)), + *args[alen - right:], + ) + + tvt.__typing_prepare_subst__ = _typevartuple_prepare_subst return tvt def __init_subclass__(self, *args, **kwds): From cfd02155e24a2c4fa7dda6c9a3b6eff78067bd56 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 10:35:32 -0400 Subject: [PATCH 3/6] Remove syntax errors on older Python --- src/test_typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 0ab1e7fb..8176a8a4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6409,7 +6409,7 @@ def test_typevartuple_specialization(self): class A(Generic[T, Unpack[Ts]]): ... self.assertEqual(A[float].__args__, (float, str, int)) self.assertEqual(A[float, range].__args__, (float, range)) - self.assertEqual(A[float, *tuple[int, ...]].__args__, (float, *tuple[int, ...])) + self.assertEqual(A[float, Unpack[tuple[int, ...]]].__args__, (float, Unpack[tuple[int, ...]])) def test_typevar_and_typevartuple_specialization(self): T = TypeVar("T") @@ -6420,7 +6420,7 @@ class A(Generic[T, U, Unpack[Ts]]): ... self.assertEqual(A[int].__args__, (int, float, str, int)) self.assertEqual(A[int, str].__args__, (int, str, str, int)) self.assertEqual(A[int, str, range].__args__, (int, str, range)) - self.assertEqual(A[int, str, *tuple[int, ...]].__args__, (int, str, *tuple[int, ...])) + self.assertEqual(A[int, str, Unpack[tuple[int, ...]]].__args__, (int, str, Unpack[tuple[int, ...]])) def test_no_default_after_typevar_tuple(self): T = TypeVar("T", default=int) From 6c27f6eda8f494083d5c4685b4227bc8a92c7ad2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 10:37:06 -0400 Subject: [PATCH 4/6] Add skips for older versions --- src/test_typing_extensions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8176a8a4..eabcf00a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6402,6 +6402,10 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] + @skipIf( + sys.version_info < (3, 11), + "Not yet backported for older versions of Python" + ) def test_typevartuple_specialization(self): T = TypeVar("T") Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) @@ -6411,6 +6415,10 @@ class A(Generic[T, Unpack[Ts]]): ... self.assertEqual(A[float, range].__args__, (float, range)) self.assertEqual(A[float, Unpack[tuple[int, ...]]].__args__, (float, Unpack[tuple[int, ...]])) + @skipIf( + sys.version_info < (3, 11), + "Not yet backported for older versions of Python" + ) def test_typevar_and_typevartuple_specialization(self): T = TypeVar("T") U = TypeVar("U", default=float) @@ -6507,6 +6515,10 @@ def test_allow_default_after_non_default_in_alias(self): a4 = Callable[[Unpack[Ts]], T] self.assertEqual(a4.__args__, (Unpack[Ts], T)) + @skipIf( + sys.version_info < (3, 11), + "Not yet backported for older versions of Python" + ) def test_paramspec_specialization(self): T = TypeVar("T") P = ParamSpec('P', default=[str, int]) @@ -6515,6 +6527,10 @@ class A(Generic[T, P]): ... self.assertEqual(A[float].__args__, (float, (str, int))) self.assertEqual(A[float, [range]].__args__, (float, (range,))) + @skipIf( + sys.version_info < (3, 11), + "Not yet backported for older versions of Python" + ) def test_typevar_and_paramspec_specialization(self): T = TypeVar("T") U = TypeVar("U", default=float) @@ -6525,6 +6541,10 @@ class A(Generic[T, U, P]): ... self.assertEqual(A[float, int].__args__, (float, int, (str, int))) self.assertEqual(A[float, int, [range]].__args__, (float, int, (range,))) + @skipIf( + sys.version_info < (3, 11), + "Not yet backported for older versions of Python" + ) def test_paramspec_and_typevar_specialization(self): T = TypeVar("T") P = ParamSpec('P', default=[str, int]) From 3173faa1486204c5be43a90150732aba14499ecb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 10:53:48 -0400 Subject: [PATCH 5/6] linting complaints --- src/typing_extensions.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 09f60bcc..d549fb69 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1513,11 +1513,15 @@ def __new__(cls, name, *constraints, bound=None, if infer_variance and (covariant or contravariant): raise ValueError("Variance cannot be specified with infer_variance.") typevar.__infer_variance__ = infer_variance + _set_default(typevar, default) _set_module(typevar) def _tvar_prepare_subst(alias, args): - if alias.__parameters__.index(typevar) == len(args) and typevar.has_default(): + if ( + typevar.has_default() + and alias.__parameters__.index(typevar) == len(args) + ): args += (typevar.__default__,) return args @@ -1596,6 +1600,7 @@ def __eq__(self, other): # 3.10+ elif hasattr(typing, 'ParamSpec'): + # Add default parameter - PEP 696 class ParamSpec(metaclass=_TypeVarLikeMeta): """Parameter specification.""" @@ -1628,12 +1633,12 @@ def _paramspec_prepare_subst(alias, args): if i >= len(args): raise TypeError(f"Too few arguments for {alias}") # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. - if len(params) == 1 and not _is_param_expr(args[0]): + if len(params) == 1 and not typing._is_param_expr(args[0]): assert i == 0 args = (args,) # Convert lists to tuples to help other libraries cache the results. elif isinstance(args[i], list): - args = (*args[:i], tuple(args[i]), *args[i+1:]) + args = (*args[:i], tuple(args[i]), *args[i + 1:]) return args paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst @@ -2374,6 +2379,7 @@ def _is_unpack(obj): from typing import TypeVarTuple elif hasattr(typing, "TypeVarTuple"): # 3.11+ + def _unpack_args(*args): newargs = [] for arg in args: @@ -2400,7 +2406,9 @@ def _typevartuple_prepare_subst(alias, args): typevartuple_index = params.index(tvt) for param in params[typevartuple_index + 1:]: if isinstance(param, TypeVarTuple): - raise TypeError(f"More than one TypeVarTuple parameter in {alias}") + raise TypeError( + f"More than one TypeVarTuple parameter in {alias}" + ) alen = len(args) plen = len(params) @@ -2413,7 +2421,10 @@ def _typevartuple_prepare_subst(alias, args): subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) if subargs and len(subargs) == 2 and subargs[-1] is ...: if var_tuple_index is not None: - raise TypeError("More than one unpacked arbitrary-length tuple argument") + raise TypeError( + "More than one unpacked " + "arbitrary-length tuple argument" + ) var_tuple_index = k fillarg = subargs[0] if var_tuple_index is not None: @@ -2421,7 +2432,7 @@ def _typevartuple_prepare_subst(alias, args): right = min(right, alen - var_tuple_index - 1) elif left + right > alen: raise TypeError(f"Too few arguments for {alias};" - f" actual {alen}, expected at least {plen-1}") + f" actual {alen}, expected at least {plen - 1}") if left == alen - right and tvt.has_default(): replacement = _unpack_args(tvt.__default__) else: @@ -2429,9 +2440,9 @@ def _typevartuple_prepare_subst(alias, args): return ( *args[:left], - *([fillarg]*(typevartuple_index - left)), + *([fillarg] * (typevartuple_index - left)), replacement, - *([fillarg]*(plen - right - left - typevartuple_index - 1)), + *([fillarg] * (plen - right - left - typevartuple_index - 1)), *args[alen - right:], ) From 6053e461c50c9d46daeb79bd34b00f23744ed631 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 11:14:23 -0400 Subject: [PATCH 6/6] needs 3.11.1 --- src/test_typing_extensions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index eabcf00a..ae6ad076 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6403,7 +6403,7 @@ class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] @skipIf( - sys.version_info < (3, 11), + sys.version_info < (3, 11, 1), "Not yet backported for older versions of Python" ) def test_typevartuple_specialization(self): @@ -6416,7 +6416,7 @@ class A(Generic[T, Unpack[Ts]]): ... self.assertEqual(A[float, Unpack[tuple[int, ...]]].__args__, (float, Unpack[tuple[int, ...]])) @skipIf( - sys.version_info < (3, 11), + sys.version_info < (3, 11, 1), "Not yet backported for older versions of Python" ) def test_typevar_and_typevartuple_specialization(self): @@ -6516,7 +6516,7 @@ def test_allow_default_after_non_default_in_alias(self): self.assertEqual(a4.__args__, (Unpack[Ts], T)) @skipIf( - sys.version_info < (3, 11), + sys.version_info < (3, 11, 1), "Not yet backported for older versions of Python" ) def test_paramspec_specialization(self): @@ -6528,7 +6528,7 @@ class A(Generic[T, P]): ... self.assertEqual(A[float, [range]].__args__, (float, (range,))) @skipIf( - sys.version_info < (3, 11), + sys.version_info < (3, 11, 1), "Not yet backported for older versions of Python" ) def test_typevar_and_paramspec_specialization(self): @@ -6542,7 +6542,7 @@ class A(Generic[T, U, P]): ... self.assertEqual(A[float, int, [range]].__args__, (float, int, (range,))) @skipIf( - sys.version_info < (3, 11), + sys.version_info < (3, 11, 1), "Not yet backported for older versions of Python" ) def test_paramspec_and_typevar_specialization(self):