Skip to content

gh-91162: Implement splitting of *tuple[int, ...] over T, *Ts in typing.py #93318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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!"],
Expand Down Expand Up @@ -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, ...]]'),
Expand Down Expand Up @@ -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):
Expand Down
72 changes: 53 additions & 19 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1409,31 +1424,46 @@ 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):
if typevartuple_index is not None:
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__:
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix substitution of e.g. ``tuple[int, ...]`` into a generic type alias with parameters e.g. ``T, *Ts``.