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``.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: