diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2afac235391552..ede8c062abadab 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -501,6 +501,15 @@ def template_replace(templates: list[str], replacements: dict[str, list[str]]) - ("Huskies are cute but also tiring") ] + Example 3: Suppose that: + templates = ["Huskies are word1. I said word!"] + replacements = {"word1": ["cool", "awesome"]} + Then we would return: + [ + ("Huskies are cool. I said cool!"), + ("Huskies are awesome. I said awesome!"), + ] + Note that if any of the replacements do not occur in any template: templates = ["Huskies are word1", "Beagles!"] replacements = {"word1": ["playful", "cute"], @@ -563,6 +572,19 @@ def test_two_templates_two_replacements_yields_correct_renders(self): ] self.assertEqual(actual, expected) + def test_two_instances_of_replacement_word_yields_correct_renders(self): + actual = template_replace( + templates=["Cats are word1. That's word1!"], + replacements={ + "word1": ["smol", "cute"], + }, + ) + expected = [ + ("Cats are smol. That's smol!",), + ("Cats are cute. That's cute!",), + ] + self.assertEqual(actual, expected) + def test_no_duplicates_if_replacement_not_in_templates(self): actual = template_replace( templates=["Cats are word1", "Dogs!"], @@ -769,11 +791,17 @@ class C(Generic[*Ts]): pass ('generic[T, *Ts]', '[int, str]', 'generic[int, str]'), ('generic[T, *Ts]', '[int, str, bool]', 'generic[int, str, bool]'), - ('generic[T, *Ts]', '[*tuple[int, ...]]', 'TypeError'), # Should be generic[int, *tuple[int, ...]] + ('C[T, *Ts]', '[*tuple_type[int, ...]]', 'C[int, *tuple_type[int, ...]]'), + ('Tuple[T, *Ts]', '[*tuple_type[int, ...]]', 'Tuple[int, *tuple_type[int, ...]]'), + # Should be tuple[int, *tuple[int, ...]] + ('tuple[T, *Ts]', '[*tuple_type[int, ...]]', 'TypeError'), + + ('generic[T, *Ts]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'TypeError'), + ('generic[*Ts, T]', '[*tuple_type[int, ...], *tuple_type[str, ...]]', 'TypeError'), ('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[*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, ...]]'), @@ -4901,6 +4929,14 @@ class C(Generic[T]): pass self.assertEqual(get_args(list | str), (list, str)) self.assertEqual(get_args(Required[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,)) + self.assertEqual(get_args(Unpack[Tuple[int]]), (int,)) + self.assertEqual(get_args(Unpack[tuple[int]]), (int,)) + self.assertEqual(get_args(Unpack[Tuple[int, ...]]), (int, ...)) + self.assertEqual(get_args(Unpack[tuple[int, ...]]), (int, ...)) + self.assertEqual(get_args((*Tuple[int],)[0]), (int,)) + self.assertEqual(get_args((*tuple[int],)[0]), (int,)) + self.assertEqual(get_args((*Tuple[int, ...],)[0]), (int, ...)) + self.assertEqual(get_args((*tuple[int, ...],)[0]), (int, ...)) class CollectionsAbcTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 40ab516f7c8ff7..47b4a62e8b0ae3 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1255,8 +1255,17 @@ 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 e.g. `*tuple[int]` or `*tuple[int, ...]`.""" + 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 e.g. `*Tuple[int]` or `*Tuple[int, ...]`.""" if not isinstance(x, _UnpackGenericAlias): return False # Alright, `x` is `Unpack[something]`. @@ -1268,6 +1277,10 @@ def _is_unpacked_tuple(x: Any) -> bool: return getattr(unpacked_type, '__origin__', None) is tuple +def _is_unpacked_tuple(x: Any) -> bool: + return _is_unpacked_typing_tuple(x) or _is_unpacked_native_tuple(x) + + def _is_unpacked_arbitrary_length_tuple(x: Any) -> bool: if not _is_unpacked_tuple(x): return False @@ -1280,12 +1293,14 @@ def _is_unpacked_arbitrary_length_tuple(x: Any) -> bool: tuple_args = unpacked_tuple.__args__ if not tuple_args: - # It's `Unpack[tuple[()]]`. + # It's `Unpack[tuple[()]]` or `*tuple[()]`. return False last_arg = tuple_args[-1] + if last_arg is Ellipsis: - # It's `Unpack[tuple[something, ...]]`, which is arbitrary-length. + # It's `Unpack[tuple[something, ...]]` or `*tuple[something, ...]`, + # which are arbitrary-length. return True # If the arguments didn't end with an ellipsis, then it's not an @@ -1409,8 +1424,6 @@ def _determine_new_args(self, args): # edge cases. params = self.__parameters__ - # In the example above, this would be {T3: str} - new_arg_by_param = {} typevartuple_index = None for i, param in enumerate(params): if isinstance(param, TypeVarTuple): @@ -1418,22 +1431,39 @@ def _determine_new_args(self, args): raise TypeError(f"More than one TypeVarTuple parameter in {self}") typevartuple_index = i + # Populate `new_arg_by_param` structure. + # In the example above, `new_arg_by_param` would be {T3: str}. alen = len(args) plen = len(params) - if typevartuple_index is not None: - i = typevartuple_index - j = alen - (plen - i - 1) - if j < i: - raise TypeError(f"Too few arguments for {self};" - f" actual {alen}, expected at least {plen-1}") - new_arg_by_param.update(zip(params[:i], args[:i])) - new_arg_by_param[params[i]] = tuple(args[i: j]) - new_arg_by_param.update(zip(params[i + 1:], args[j:])) - else: + if typevartuple_index is None: if alen != plen: - raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};" - f" actual {alen}, expected {plen}") - new_arg_by_param.update(zip(params, args)) + raise TypeError( + f"Too {'many' if alen > plen else 'few'} arguments for {self};" + f" actual {alen}, expected {plen}") + new_arg_by_param = dict(zip(params, args)) + else: + if alen == 1 and _is_unpacked_arbitrary_length_tuple(args[0]): + # Handle an unpacked arbitrary-length tuple being split over + # multiple parameters, e.g. Tuple[T, *Ts][*Tuple[int, ...]]. + new_arg_by_param = {} + for param in params: + if isinstance(param, TypeVarTuple): + # new_arg_by_param[param] must be a sequence + # when param is a TypeVarTuple. + new_arg_by_param[param] = (args[0],) + else: + # *tuple[int, ...] -> int + new_arg_by_param[param] = get_args(args[0])[0] + else: + i = typevartuple_index + j = alen - (plen - i - 1) + if j < i: + raise TypeError(f"Too few arguments for {self};" + f" actual {alen}, expected at least {plen - 1}") + new_arg_by_param = {} + new_arg_by_param.update(zip(params[:i], args[:i])) + new_arg_by_param[params[i]] = tuple(args[i: j]) + new_arg_by_param.update(zip(params[i + 1:], args[j:])) new_args = [] for old_arg in self.__args__: @@ -2423,9 +2453,13 @@ def get_args(tp): get_args(Union[int, Union[T, int], str][int]) == (int, str) get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) get_args(Callable[[], T][int]) == ([], int) + get_args(Unpack[Tuple[int, str]]) == (int, str) """ if isinstance(tp, _AnnotatedAlias): return (tp.__origin__,) + tp.__metadata__ + if isinstance(tp, _UnpackGenericAlias): + # Get the packed type - e.g. *tuple[int] -> tuple[int] + tp = tp.__args__[0] if isinstance(tp, (_GenericAlias, GenericAlias)): res = tp.__args__ if _should_unflatten_callable_args(tp, res): diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-28-13-03-28.gh-issue-91162.0kxjpV.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-28-13-03-28.gh-issue-91162.0kxjpV.rst new file mode 100644 index 00000000000000..f93d15e9be22ae --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-28-13-03-28.gh-issue-91162.0kxjpV.rst @@ -0,0 +1 @@ +Fix substitution of e.g. ``tuple[int, ...]`` into a generic type alias with parameters e.g. ``T, *Ts``.