From 4891252d1bc529783ff6583f880e6e260c2eac34 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sun, 8 May 2022 17:26:49 +0100 Subject: [PATCH 1/4] Implement splitting of *tuple[int, ...] in typing.py --- Lib/test/test_typing.py | 13 +++- Lib/typing.py | 130 +++++++++++++++++++++++++++++++++++----- 2 files changed, 127 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2afac235391552..9f41eba99f2ba0 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -769,7 +769,10 @@ 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[*Ts, T]', '[int]', 'generic[int]'), ('generic[*Ts, T]', '[int, str]', 'generic[int, str]'), @@ -4901,6 +4904,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..6ff943604a9573 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,19 @@ 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:])) + new_arg_by_param = self._compute_new_args_by_param_variadic( + args, typevartuple_index, + ) else: 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)) + new_arg_by_param = dict(zip(params, args)) new_args = [] for old_arg in self.__args__: @@ -1482,6 +1492,92 @@ def _determine_new_args(self, args): return tuple(new_args) + def _compute_new_args_by_param_variadic( + self, args: list[Any], typevartuple_idx: int + ) -> dict[Any, Any]: + """Computes map from parameters to `args` when `self` is variadic. + + `self` is variadic if it was created using a `TypeVarTuple`. For + example: + + Ts = TypeVarTuple('Ts') + class C(Generic[*Ts]): ... + Alias = C[int, *Ts] + # Alias is a variadic _GenericAlias + + This function computes which type arguments map to which type parameters + in such cases. For example, consider the following. + + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + Alias = C[T, *Ts] + Alias[int, str, float] + + Here, we need to figure out that: + * `T` binds to `int` + * `*Ts` binds to `str` and `float` + We encode this by a return value of {T: int, Ts: [str, float]}. + """ + + # A difficult edge case this function must deal with is unpacked + # arbitrary-length tuples such as `*tuple[int, ...]`. Consider the + # following case. + # + # Alias = C[T, *Ts] + # Alias[*tuple[int, ...]] + # + # We need to split the *tuple[int, ...] over `T, *Ts`, resulting in a + # map of {T: int, Ts: *tuple[int, ...]}. + # + # Here's the algorithm we use to deal with this: + # 1. Detect the edge case by checking whether a) the number of type + # arguments is less than the number of type parameters, and b) + # an unpacked arbitrary-length tuple is present in the type + # arguments. + # 2. If so, duplicate the unpacked arbitrary-length tuple until we have + # the same number of type arguments as type parameters. For example, + # in the case above, we would grow from `args` from [*tuple[int, ...] + # to [*tuple[int, ...], *tuple[int, ...]]. + # 3. Bind type arguments to type variables as normal. + # 4. Go through the bindings and change it so that the normal `TypeVars` + # are bound to the inner type `int` rather than `*tuple[int, ...]`. + + params = self.__parameters__ + alen = len(args) + plen = len(params) + + # Detect the edge case mentioned above. + if alen < plen: + for i, arg in enumerate(args): + if _is_unpacked_arbitrary_length_tuple(arg): + # Step 2 from the edge case algorithm above. + args = [ + *args[:i], + *((plen - alen) * [arg]), + *args[i:] + ] + alen = len(args) + break + + i = typevartuple_idx + j = alen - (plen - i - 1) + if j < i: + raise TypeError(f"Too few arguments for {self};" + f" actual {len(args)}, 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:])) + + # Step 4 from the edge case algorithm above. + for param, new_arg in new_arg_by_param.items(): + if (isinstance(param, TypeVar) + and _is_unpacked_arbitrary_length_tuple(new_arg)): + inner_type = get_args(new_arg)[0] # *tuple[int, ...] -> int + new_arg_by_param[param] = inner_type + + return new_arg_by_param + def copy_with(self, args): return self.__class__(self.__origin__, args, name=self._name, inst=self._inst, _paramspec_tvars=self._paramspec_tvars) @@ -2423,9 +2519,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): From fce35f1e13ec01aa6845782127a0acfbbea94a98 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 28 May 2022 13:03:29 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2022-05-28-13-03-28.gh-issue-91162.0kxjpV.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-05-28-13-03-28.gh-issue-91162.0kxjpV.rst 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``. From 75525fd96c8cc11d4fc1280fc1392babc977837c Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 28 May 2022 15:34:18 +0100 Subject: [PATCH 3/4] Switch to simpler implementation --- Lib/typing.py | 120 ++++++++++++-------------------------------------- 1 file changed, 27 insertions(+), 93 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 6ff943604a9573..47b4a62e8b0ae3 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1435,15 +1435,35 @@ def _determine_new_args(self, args): # In the example above, `new_arg_by_param` would be {T3: str}. alen = len(args) plen = len(params) - if typevartuple_index is not None: - new_arg_by_param = self._compute_new_args_by_param_variadic( - args, typevartuple_index, - ) - 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}") + 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__: @@ -1492,92 +1512,6 @@ def _determine_new_args(self, args): return tuple(new_args) - def _compute_new_args_by_param_variadic( - self, args: list[Any], typevartuple_idx: int - ) -> dict[Any, Any]: - """Computes map from parameters to `args` when `self` is variadic. - - `self` is variadic if it was created using a `TypeVarTuple`. For - example: - - Ts = TypeVarTuple('Ts') - class C(Generic[*Ts]): ... - Alias = C[int, *Ts] - # Alias is a variadic _GenericAlias - - This function computes which type arguments map to which type parameters - in such cases. For example, consider the following. - - T = TypeVar('T') - Ts = TypeVarTuple('Ts') - Alias = C[T, *Ts] - Alias[int, str, float] - - Here, we need to figure out that: - * `T` binds to `int` - * `*Ts` binds to `str` and `float` - We encode this by a return value of {T: int, Ts: [str, float]}. - """ - - # A difficult edge case this function must deal with is unpacked - # arbitrary-length tuples such as `*tuple[int, ...]`. Consider the - # following case. - # - # Alias = C[T, *Ts] - # Alias[*tuple[int, ...]] - # - # We need to split the *tuple[int, ...] over `T, *Ts`, resulting in a - # map of {T: int, Ts: *tuple[int, ...]}. - # - # Here's the algorithm we use to deal with this: - # 1. Detect the edge case by checking whether a) the number of type - # arguments is less than the number of type parameters, and b) - # an unpacked arbitrary-length tuple is present in the type - # arguments. - # 2. If so, duplicate the unpacked arbitrary-length tuple until we have - # the same number of type arguments as type parameters. For example, - # in the case above, we would grow from `args` from [*tuple[int, ...] - # to [*tuple[int, ...], *tuple[int, ...]]. - # 3. Bind type arguments to type variables as normal. - # 4. Go through the bindings and change it so that the normal `TypeVars` - # are bound to the inner type `int` rather than `*tuple[int, ...]`. - - params = self.__parameters__ - alen = len(args) - plen = len(params) - - # Detect the edge case mentioned above. - if alen < plen: - for i, arg in enumerate(args): - if _is_unpacked_arbitrary_length_tuple(arg): - # Step 2 from the edge case algorithm above. - args = [ - *args[:i], - *((plen - alen) * [arg]), - *args[i:] - ] - alen = len(args) - break - - i = typevartuple_idx - j = alen - (plen - i - 1) - if j < i: - raise TypeError(f"Too few arguments for {self};" - f" actual {len(args)}, 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:])) - - # Step 4 from the edge case algorithm above. - for param, new_arg in new_arg_by_param.items(): - if (isinstance(param, TypeVar) - and _is_unpacked_arbitrary_length_tuple(new_arg)): - inner_type = get_args(new_arg)[0] # *tuple[int, ...] -> int - new_arg_by_param[param] = inner_type - - return new_arg_by_param - def copy_with(self, args): return self.__class__(self.__origin__, args, name=self._name, inst=self._inst, _paramspec_tvars=self._paramspec_tvars) From 6b0780b5e1f14d4c00f47126e3877f2c7b5e5ccc Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sat, 28 May 2022 15:54:07 +0100 Subject: [PATCH 4/4] Add test for multiple arbitrary-length unpacked tuples in args list --- Lib/test/test_typing.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 9f41eba99f2ba0..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!"], @@ -774,9 +796,12 @@ class C(Generic[*Ts]): pass # 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, ...]]'), pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy