From 7dc5cfee89b800d1b8f1b71ee8061739a04cbfaf Mon Sep 17 00:00:00 2001 From: JSup Date: Thu, 16 Jul 2026 04:08:47 +0900 Subject: [PATCH] Fix groupby inference for union iterables --- .../test_cases/itertools/check_groupby.py | 12 ++++++++ stdlib/itertools.pyi | 30 +++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 stdlib/@tests/test_cases/itertools/check_groupby.py diff --git a/stdlib/@tests/test_cases/itertools/check_groupby.py b/stdlib/@tests/test_cases/itertools/check_groupby.py new file mode 100644 index 000000000000..cf552ebf4538 --- /dev/null +++ b/stdlib/@tests/test_cases/itertools/check_groupby.py @@ -0,0 +1,12 @@ +# mypy: disable-error-code=assert-type + +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from itertools import groupby +from typing_extensions import assert_type + + +def check_union_input(items: Sequence[str] | Sequence[int]) -> None: + for _, group in groupby(items): + assert_type(group, Iterator[str] | Iterator[int]) diff --git a/stdlib/itertools.pyi b/stdlib/itertools.pyi index d26a4e1da21e..e8c470c7010c 100644 --- a/stdlib/itertools.pyi +++ b/stdlib/itertools.pyi @@ -2,7 +2,20 @@ import sys from _typeshed import MaybeNone from collections.abc import Callable, Iterable, Iterator from types import GenericAlias -from typing import Any, Generic, Literal, SupportsComplex, SupportsFloat, SupportsIndex, SupportsInt, TypeAlias, TypeVar, overload +from typing import ( + Any, + Generic, + Literal, + Protocol, + SupportsComplex, + SupportsFloat, + SupportsIndex, + SupportsInt, + TypeAlias, + TypeVar, + overload, + type_check_only, +) from typing_extensions import Self, disjoint_base _T = TypeVar("_T") @@ -10,6 +23,7 @@ _S = TypeVar("_S") _N = TypeVar("_N", int, float, SupportsFloat, SupportsInt, SupportsIndex, SupportsComplex) _T_co = TypeVar("_T_co", covariant=True) _S_co = TypeVar("_S_co", covariant=True) +_IteratorT_co = TypeVar("_IteratorT_co", bound=Iterator[Any], covariant=True) _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") _T3 = TypeVar("_T3") @@ -94,16 +108,28 @@ class filterfalse(Generic[_T]): def __iter__(self) -> Self: ... def __next__(self) -> _T: ... +@type_check_only +class _GroupByIterable(Protocol[_T_co, _IteratorT_co]): + @overload + def __iter__(self) -> Iterator[_T_co]: ... + @overload + def __iter__(self) -> _IteratorT_co: ... # type: ignore[overload-cannot-match] + @disjoint_base class groupby(Generic[_T_co, _S_co]): @overload - def __new__(cls, iterable: Iterable[_T1], key: None = None) -> groupby[_T1, _T1]: ... + def __new__(cls, iterable: _GroupByIterable[_T1, _IteratorT_co], key: None = None) -> _ExactGroupBy[_T1, _IteratorT_co]: ... @overload def __new__(cls, iterable: Iterable[_T1], key: Callable[[_T1], _T2]) -> groupby[_T2, _T1]: ... def __iter__(self) -> Self: ... def __next__(self) -> tuple[_T_co, Iterator[_S_co]]: ... +@type_check_only +class _ExactGroupBy(groupby[_T_co, Any], Generic[_T_co, _IteratorT_co]): + def __iter__(self) -> Self: ... + def __next__(self) -> tuple[_T_co, _IteratorT_co]: ... + @disjoint_base class islice(Generic[_T]): @overload