diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 55e18c08537df7..5867cbec4b40ca 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -581,11 +581,25 @@ def test_no_duplicates_if_replacement_not_in_templates(self): class GenericAliasSubstitutionTests(BaseTestCase): """Tests for type variable substitution in generic aliases. - Note that the expected results here are tentative, based on a - still-being-worked-out spec for what we allow at runtime (given that - implementation of *full* substitution logic at runtime would add too much - complexity to typing.py). This spec is currently being discussed at - https://github.com/python/cpython/issues/91162. + Note that not all the rules governing substitution behavior at runtime + is codified in PEPs; the source of truth for these tests is the tests + themselves. + + Informally, the specification is as follows: + * In general, we aim to support as many valid substitutions (as specified + by the PEPs themselves) as possible at runtime. + * Leniency: In some cases, we also choose to allow some substitutions that + the PEPs themselves might forbid. This is partly for simplicity - we want + to minimise complexity in the runtime - and partly to enable users to + experiment with new ways to use types. + * Exceptions: + * PEP 646 Exception 1: Unpacked types (e.g. *tuple[int], *tuple[int, ...], + *Ts where Ts is a TypeVarTuple) cannot be used as arguments to generic + aliases which expect a fixed number of arguments. See the tests + themselves for examples. + * PEP 646 Exception 2: Unpacked TypeVarTuples can only be used as + arguments to generic aliases whose sole parameter is also an unpacked + TypeVarTuple. """ def test_one_parameter(self): @@ -603,21 +617,14 @@ class C(Generic[T]): pass ('generic[T]', '[int]', 'generic[int]'), ('generic[T]', '[int, str]', 'TypeError'), ('generic[T]', '[tuple_type[int, ...]]', 'generic[tuple_type[int, ...]]'), - # Should raise TypeError: a) according to the tentative spec, - # unpacked types cannot be used as arguments to aliases that expect - # a fixed number of arguments; b) it's equivalent to generic[()]. - ('generic[T]', '[*tuple[()]]', 'generic[*tuple[()]]'), + # These are disallowed by PEP 646 Exception 1 in the docstring. + ('generic[T]', '[*tuple[()]]', 'TypeError'), ('generic[T]', '[*Tuple[()]]', 'TypeError'), - # Should raise TypeError according to the tentative spec: unpacked - # types cannot be used as arguments to aliases that expect a fixed - # number of arguments. - ('generic[T]', '[*tuple[int]]', 'generic[*tuple[int]]'), + ('generic[T]', '[*tuple[int]]', 'TypeError'), ('generic[T]', '[*Tuple[int]]', 'TypeError'), - # Ditto. - ('generic[T]', '[*tuple[int, str]]', 'generic[*tuple[int, str]]'), + ('generic[T]', '[*tuple[int, str]]', 'TypeError'), ('generic[T]', '[*Tuple[int, str]]', 'TypeError'), - # Ditto. - ('generic[T]', '[*tuple[int, ...]]', 'generic[*tuple[int, ...]]'), + ('generic[T]', '[*tuple[int, ...]]', 'TypeError'), ('generic[T]', '[*Tuple[int, ...]]', 'TypeError'), ('generic[T]', '[*Ts]', 'TypeError'), ('generic[T]', '[T, *Ts]', 'TypeError'), @@ -663,31 +670,20 @@ class C(Generic[T1, T2]): pass ('generic[T1, T2]', '[int]', 'TypeError'), ('generic[T1, T2]', '[int, str]', 'generic[int, str]'), ('generic[T1, T2]', '[int, str, bool]', 'TypeError'), + ('generic[T1, T2]', '[tuple_type[int, ...], tuple_type[str, ...]]', 'generic[tuple_type[int, ...], tuple_type[str, ...]]'), + # These are disallowed by PEP 646 Exception 1 in the docstring. ('generic[T1, T2]', '[*tuple_type[int]]', 'TypeError'), ('generic[T1, T2]', '[*tuple_type[int, str]]', 'TypeError'), ('generic[T1, T2]', '[*tuple_type[int, str, bool]]', 'TypeError'), - - # Should raise TypeError according to the tentative spec: unpacked - # types cannot be used as arguments to aliases that expect a fixed - # number of arguments. - ('generic[T1, T2]', '[*tuple[int, str], *tuple[float, bool]]', 'generic[*tuple[int, str], *tuple[float, bool]]'), - ('generic[T1, T2]', '[*Tuple[int, str], *Tuple[float, bool]]', 'TypeError'), - + ('generic[T1, T2]', '[*tuple_type[int, str], *tuple_type[float, bool]]', 'TypeError'), ('generic[T1, T2]', '[tuple_type[int, ...]]', 'TypeError'), - ('generic[T1, T2]', '[tuple_type[int, ...], tuple_type[str, ...]]', 'generic[tuple_type[int, ...], tuple_type[str, ...]]'), ('generic[T1, T2]', '[*tuple_type[int, ...]]', 'TypeError'), - - # Ditto. - ('generic[T1, T2]', '[*tuple[int, ...], *tuple[str, ...]]', 'generic[*tuple[int, ...], *tuple[str, ...]]'), - ('generic[T1, T2]', '[*Tuple[int, ...], *Tuple[str, ...]]', 'TypeError'), - + ('generic[T1, T2]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'TypeError'), ('generic[T1, T2]', '[*Ts]', 'TypeError'), ('generic[T1, T2]', '[T, *Ts]', 'TypeError'), ('generic[T1, T2]', '[*Ts, T]', 'TypeError'), - # Should raise TypeError according to the tentative spec: unpacked - # types cannot be used as arguments to generics that expect a fixed - # number of arguments. - # (None of the things in `generics` were defined using *Ts.) + # Should raise TypeError by PEP 646 Exception 1 - + # none of the things in `generics` were defined using *Ts. ('generic[T1, *tuple_type[int, ...]]', '[str]', 'generic[str, *tuple_type[int, ...]]'), ] @@ -749,96 +745,45 @@ class C(Generic[*Ts]): pass generics = ['C', 'tuple', 'Tuple'] tuple_types = ['tuple', 'Tuple'] - # The majority of these have three separate cases for C, tuple and + # Some of these have three separate cases for C, tuple and # Tuple because tuple currently behaves differently. tests = [ # Alias # Args # Expected result - ('C[*Ts]', '[()]', 'C[()]'), - ('tuple[*Ts]', '[()]', 'tuple[()]'), - ('Tuple[*Ts]', '[()]', 'Tuple[()]'), + ('generic[*Ts]', '[()]', 'generic[()]'), + ('generic[*Ts]', '[int]', 'generic[int]'), + ('generic[*Ts]', '[int, str]', 'generic[int, str]'), - ('C[*Ts]', '[int]', 'C[int]'), - ('tuple[*Ts]', '[int]', 'tuple[int]'), - ('Tuple[*Ts]', '[int]', 'Tuple[int]'), - - ('C[*Ts]', '[int, str]', 'C[int, str]'), - ('tuple[*Ts]', '[int, str]', 'tuple[int, str]'), - ('Tuple[*Ts]', '[int, str]', 'Tuple[int, str]'), - - ('C[*Ts]', '[*tuple_type[int]]', 'C[*tuple_type[int]]'), # Should be C[int] + ('C[*Ts]', '[*tuple_type[int]]', 'C[int]'), + ('Tuple[*Ts]', '[*tuple_type[int]]', 'Tuple[int]'), ('tuple[*Ts]', '[*tuple_type[int]]', 'tuple[*tuple_type[int]]'), # Should be tuple[int] - ('Tuple[*Ts]', '[*tuple_type[int]]', 'Tuple[*tuple_type[int]]'), # Should be Tuple[int] - ('C[*Ts]', '[*tuple_type[*Ts]]', 'C[*tuple_type[*Ts]]'), # Should be C[*Ts] + ('C[*Ts]', '[*tuple_type[*Ts]]', 'C[*Ts]'), + ('Tuple[*Ts]', '[*tuple_type[*Ts]]', 'Tuple[*Ts]'), ('tuple[*Ts]', '[*tuple_type[*Ts]]', 'tuple[*tuple_type[*Ts]]'), # Should be tuple[*Ts] - ('Tuple[*Ts]', '[*tuple_type[*Ts]]', 'Tuple[*tuple_type[*Ts]]'), # Should be Tuple[*Ts] - ('C[*Ts]', '[*tuple_type[int, str]]', 'C[*tuple_type[int, str]]'), # Should be C[int, str] + ('C[*Ts]', '[*tuple_type[int, str]]', 'C[int, str]'), + ('Tuple[*Ts]', '[*tuple_type[int, str]]', 'Tuple[int, str]'), ('tuple[*Ts]', '[*tuple_type[int, str]]', 'tuple[*tuple_type[int, str]]'), # Should be tuple[int, str] - ('Tuple[*Ts]', '[*tuple_type[int, str]]', 'Tuple[*tuple_type[int, str]]'), # Should be Tuple[int, str] - - ('C[*Ts]', '[tuple_type[int, ...]]', 'C[tuple_type[int, ...]]'), - ('tuple[*Ts]', '[tuple_type[int, ...]]', 'tuple[tuple_type[int, ...]]'), - ('Tuple[*Ts]', '[tuple_type[int, ...]]', 'Tuple[tuple_type[int, ...]]'), - ('C[*Ts]', '[tuple_type[int, ...], tuple_type[str, ...]]', 'C[tuple_type[int, ...], tuple_type[str, ...]]'), - ('tuple[*Ts]', '[tuple_type[int, ...], tuple_type[str, ...]]', 'tuple[tuple_type[int, ...], tuple_type[str, ...]]'), - ('Tuple[*Ts]', '[tuple_type[int, ...], tuple_type[str, ...]]', 'Tuple[tuple_type[int, ...], tuple_type[str, ...]]'), - - ('C[*Ts]', '[*tuple_type[int, ...]]', 'C[*tuple_type[int, ...]]'), - ('tuple[*Ts]', '[*tuple_type[int, ...]]', 'tuple[*tuple_type[int, ...]]'), - ('Tuple[*Ts]', '[*tuple_type[int, ...]]', 'Tuple[*tuple_type[int, ...]]'), + ('generic[*Ts]', '[tuple_type[int, ...]]', 'generic[tuple_type[int, ...]]'), + ('generic[*Ts]', '[tuple_type[int, ...], tuple_type[str, ...]]', 'generic[tuple_type[int, ...], tuple_type[str, ...]]'), + ('generic[*Ts]', '[*tuple_type[int, ...]]', 'generic[*tuple_type[int, ...]]'), # Technically, multiple unpackings are forbidden by PEP 646, but we # choose to be less restrictive at runtime, to allow folks room # to experiment. So all three of these should be valid. - ('C[*Ts]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'C[*tuple_type[int, ...], *tuple_type[str, ...]]'), - ('tuple[*Ts]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'tuple[*tuple_type[int, ...], *tuple_type[str, ...]]'), - ('Tuple[*Ts]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'Tuple[*tuple_type[int, ...], *tuple_type[str, ...]]'), - - ('C[*Ts]', '[*Ts]', 'C[*Ts]'), - ('tuple[*Ts]', '[*Ts]', 'tuple[*Ts]'), - ('Tuple[*Ts]', '[*Ts]', 'Tuple[*Ts]'), - - ('C[*Ts]', '[T, *Ts]', 'C[T, *Ts]'), - ('tuple[*Ts]', '[T, *Ts]', 'tuple[T, *Ts]'), - ('Tuple[*Ts]', '[T, *Ts]', 'Tuple[T, *Ts]'), - - ('C[*Ts]', '[*Ts, T]', 'C[*Ts, T]'), - ('tuple[*Ts]', '[*Ts, T]', 'tuple[*Ts, T]'), - ('Tuple[*Ts]', '[*Ts, T]', 'Tuple[*Ts, T]'), - - ('C[T, *Ts]', '[int]', 'C[int]'), - ('tuple[T, *Ts]', '[int]', 'tuple[int]'), - ('Tuple[T, *Ts]', '[int]', 'Tuple[int]'), - - ('C[T, *Ts]', '[int, str]', 'C[int, str]'), - ('tuple[T, *Ts]', '[int, str]', 'tuple[int, str]'), - ('Tuple[T, *Ts]', '[int, str]', 'Tuple[int, str]'), - - ('C[T, *Ts]', '[int, str, bool]', 'C[int, str, bool]'), - ('tuple[T, *Ts]', '[int, str, bool]', 'tuple[int, str, bool]'), - ('Tuple[T, *Ts]', '[int, str, bool]', 'Tuple[int, str, bool]'), - - ('C[T, *Ts]', '[*tuple[int, ...]]', 'C[*tuple[int, ...]]'), # Should be C[int, *tuple[int, ...]] - ('C[T, *Ts]', '[*Tuple[int, ...]]', 'TypeError'), # Ditto - ('tuple[T, *Ts]', '[*tuple[int, ...]]', 'tuple[*tuple[int, ...]]'), # Should be tuple[int, *tuple[int, ...]] - ('tuple[T, *Ts]', '[*Tuple[int, ...]]', 'TypeError'), # Should be tuple[int, *Tuple[int, ...]] - ('Tuple[T, *Ts]', '[*tuple[int, ...]]', 'Tuple[*tuple[int, ...]]'), # Should be Tuple[int, *tuple[int, ...]] - ('Tuple[T, *Ts]', '[*Tuple[int, ...]]', 'TypeError'), # Should be Tuple[int, *Tuple[int, ...]] - - ('C[*Ts, T]', '[int]', 'C[int]'), - ('tuple[*Ts, T]', '[int]', 'tuple[int]'), - ('Tuple[*Ts, T]', '[int]', 'Tuple[int]'), - - ('C[*Ts, T]', '[int, str]', 'C[int, str]'), - ('tuple[*Ts, T]', '[int, str]', 'tuple[int, str]'), - ('Tuple[*Ts, T]', '[int, str]', 'Tuple[int, str]'), - - ('C[*Ts, T]', '[int, str, bool]', 'C[int, str, bool]'), - ('tuple[*Ts, T]', '[int, str, bool]', 'tuple[int, str, bool]'), - ('Tuple[*Ts, T]', '[int, str, bool]', 'Tuple[int, str, bool]'), - + ('generic[*Ts]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'generic[*tuple_type[int, ...], *tuple_type[str, ...]]'), + + ('generic[*Ts]', '[*Ts]', 'generic[*Ts]'), + ('generic[*Ts]', '[T, *Ts]', 'generic[T, *Ts]'), + ('generic[*Ts]', '[*Ts, T]', 'generic[*Ts, T]'), + ('generic[T, *Ts]', '[int]', 'generic[int]'), + ('generic[T, *Ts]', '[int, str]', 'generic[int, str]'), + ('generic[T, *Ts]', '[int, str, bool]', 'generic[int, str, bool]'), + ('generic[T, *Ts]', '[*tuple_type[int, ...]]', 'TypeError'), # Should be generic[int, *tuple_type[int, ...]] + ('generic[*Ts, T]', '[int]', 'generic[int]'), + ('generic[*Ts, T]', '[int, str]', 'generic[int, str]'), + ('generic[*Ts, T]', '[int, str, bool]', 'generic[int, str, bool]'), ('generic[T, *tuple_type[int, ...]]', '[str]', 'generic[str, *tuple_type[int, ...]]'), ('generic[T1, T2, *tuple_type[int, ...]]', '[str, bool]', 'generic[str, bool, *tuple_type[int, ...]]'), ('generic[T1, *tuple_type[int, ...], T2]', '[str, bool]', 'generic[str, *tuple_type[int, ...], bool]'), diff --git a/Lib/typing.py b/Lib/typing.py index bdc14e39033dcf..21820acd1ba136 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -876,6 +876,15 @@ def __repr__(self): return f'ForwardRef({self.__forward_arg__!r}{module_repr})' +def _is_unpacked_type(x: Any) -> bool: + return ( + # E.g. *tuple[int] + (isinstance(x, GenericAlias) and x.__unpacked__) + or + # E.g. Unpack[tuple[int]] + (isinstance(x, _GenericAlias) and x.__origin__ is Unpack) + ) + def _is_unpacked_typevartuple(x: Any) -> bool: return ( isinstance(x, _UnpackGenericAlias) @@ -995,7 +1004,7 @@ def __init__(self, name, *constraints, bound=None, def __typing_subst__(self, arg): msg = "Parameters to generic types must be types." arg = _type_check(arg, msg, is_argument=True) - if (isinstance(arg, _GenericAlias) and arg.__origin__ is Unpack): + if _is_unpacked_type(arg): raise TypeError(f"{arg} is not valid as type argument") return arg @@ -1232,42 +1241,92 @@ def __dir__(self): + [attr for attr in dir(self.__origin__) if not _is_dunder(attr)])) -def _is_unpacked_tuple(x: Any) -> bool: - # Is `x` something like `*tuple[int]` or `*tuple[int, ...]`? +def _is_unpacked_native_tuple(x: Any) -> bool: + """Checks whether x is an unpacked tuple - e.g. *tuple[int]. + + Specifically, this functions checks for an unpacked *native* tuple - *not* + an unpacked typing.Tuple like *Tuple[int]. + + This function works whether the tuple is unpacked using * or Unpack[]. + """ + return ( + isinstance(x, types.GenericAlias) + and x.__origin__ is tuple + and x.__unpacked__ + ) + + +def _is_unpacked_typing_tuple(x: Any) -> bool: + """Checks whether x is an unpacked Tuple - e.g. *Tuple[int]. + + Specifically, this functions checks for an unpacked typing.Tuple - *not* an + unpacked native tuple like *tuple[int]. + + This function works whether the Tuple is unpacked using * or Unpack[]. + """ + # Does x appear to be an unpacked type from typing.py? if not isinstance(x, _UnpackGenericAlias): return False - # Alright, `x` is `Unpack[something]`. - - # `x` will always have `__args__`, because Unpack[] and Unpack[()] - # aren't legal. + # Ok, so x is an Unpack[something]. + # (Even if we did *Tuple[int] instead of Unpack[tuple[int]], + # the result is still represented as Unpack[tuple[int]].) + # Let's get the 'something'. + # Unpack[] always takes exactly one argument, so we don't need to worry + # about x.__args__ being empty. unpacked_type = x.__args__[0] - + # Finally, is that 'something' tuple? + # (Yes, Tuple[int].__origin__ is still tuple rather than Tuple.) return getattr(unpacked_type, '__origin__', None) is tuple -def _is_unpacked_arbitrary_length_tuple(x: Any) -> bool: - if not _is_unpacked_tuple(x): - return False - unpacked_tuple = x.__args__[0] +def _is_unpacked_tuple(x: Any) -> bool: + return _is_unpacked_native_tuple(x) or _is_unpacked_typing_tuple(x) + + +def _get_unpacked_tuple_args( + # types.GenericAlias for unpacked native tuple, e.g. *tuple[int]; + # _UnpackGenericAlias for unpacked typing tuple, e.g. *Tuple[int]. + x: 'types.GenericAlias | _UnpackGenericAlias', +) -> tuple[Any, ...]: + if _is_unpacked_native_tuple(x): + return x.__args__ + elif _is_unpacked_typing_tuple(x): + # Unpack[] always takes exactly one argument, + # so x.__args__ will always have length 1. + unpacked_type = x.__args__[0] # E.g. Tuple[int] + return unpacked_type.__args__ + else: + raise TypeError(f"'{x}' is not an unpacked tuple") - if not hasattr(unpacked_tuple, '__args__'): - # It's `Unpack[tuple]`. We can't make any assumptions about the length - # of the tuple, so it's effectively an arbitrary-length tuple. - return True - tuple_args = unpacked_tuple.__args__ - if not tuple_args: - # It's `Unpack[tuple[()]]`. +def _is_unpacked_arbitrary_length_tuple(x: Any) -> bool: + """Checks whether x is something like *tuple[int, ...].""" + try: + tuple_args = _get_unpacked_tuple_args(x) + except TypeError: return False - last_arg = tuple_args[-1] - if last_arg is Ellipsis: - # It's `Unpack[tuple[something, ...]]`, which is arbitrary-length. - return True + return last_arg is Ellipsis + - # If the arguments didn't end with an ellipsis, then it's not an - # arbitrary-length tuple. - return False +def _extract_types_from_unpacked_tuples(type_list: tuple[Any]) -> tuple[Any]: + """Extracts inner types from unpacked tuples like *tuple[int, str]. + + For example, if called with: + (int, *tuple[bool, float], str) + then we would return: + (int, bool, float, str) + """ + new_type_list = [] + for t in type_list: + # If t is an unpacked arbitrary-length tuple like *tuple[int, ...], + # there'd no way to represent the result, so we leave it as it is. + if _is_unpacked_tuple(t) and not _is_unpacked_arbitrary_length_tuple(t): + args = _get_unpacked_tuple_args(t) + new_type_list.extend(args) + else: + new_type_list.append(t) + return tuple(new_type_list) # Special typing constructs Union, Optional, Generic, Callable and Tuple @@ -1355,10 +1414,24 @@ def __getitem__(self, args): # Can't subscript Generic[...] or Protocol[...]. raise TypeError(f"Cannot subscript already-subscripted {self}") - # Preprocess `args`. if not isinstance(args, tuple): args = (args,) + + accepts_arbitrary_num_type_arguments = any( + isinstance(param, TypeVarTuple) for param in self.__parameters__ + ) + if not accepts_arbitrary_num_type_arguments: + for arg in args: + if _is_unpacked_type(arg): + raise TypeError( + f"Generic alias '{self}' cannot accept an arbitrary " + "number of type arguments, so cannot accept unpacked " + f"type argument '{arg}'." + ) + + # Preprocess `args`. args = tuple(_type_convert(p) for p in args) + args = _extract_types_from_unpacked_tuples(args) if (self._paramspec_tvars and any(isinstance(t, ParamSpec) for t in self.__parameters__)): args = _prepare_paramspec_params(self, args)