From 7ee8448b02a93ecfdadbf33b610811cc49e7dcc4 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 31 May 2022 15:18:14 +0100 Subject: [PATCH 1/6] `builtins.sum`: Iterable must support addition with `int` if no `start` value is given --- stdlib/_typeshed/__init__.pyi | 5 ++++- stdlib/builtins.pyi | 18 +++++++++++------ test_cases/stdlib/builtins/test_sum.py | 27 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 test_cases/stdlib/builtins/test_sum.py diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index 3e5601e9b8bd..06d25b14780a 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -69,9 +69,12 @@ SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichC # Dunder protocols -class SupportsAdd(Protocol): +class SupportsAddition(Protocol): def __add__(self, __x: Any) -> Any: ... +class SupportsAdditionWithInt(Protocol): + def __add__(self, __x: int) -> Any: ... + class SupportsDivMod(Protocol[_T_contra, _T_co]): def __divmod__(self, __other: _T_contra) -> _T_co: ... diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index f91cb674c3ca..d868655f3faf 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -12,7 +12,8 @@ from _typeshed import ( ReadableBuffer, Self, StrOrBytesPath, - SupportsAdd, + SupportsAddition, + SupportsAdditionWithInt, SupportsAiter, SupportsAnext, SupportsDivMod, @@ -1545,8 +1546,9 @@ def sorted( @overload def sorted(__iterable: Iterable[_T], *, key: Callable[[_T], SupportsRichComparison], reverse: bool = ...) -> list[_T]: ... -_SumT = TypeVar("_SumT", bound=SupportsAdd) -_SumS = TypeVar("_SumS", bound=SupportsAdd) +_SupportsAdditionT_1 = TypeVar("_SupportsAdditionT_1", bound=SupportsAddition) +_SupportsAdditionT_2 = TypeVar("_SupportsAdditionT_2", bound=SupportsAddition) +_SupportsAdditionWithIntT = TypeVar("_SupportsAdditionWithIntT", bound=SupportsAdditionWithInt) # In general, the return type of `x + x` is *not* guaranteed to be the same type as x. # However, we can't express that in the stub for `sum()` @@ -1561,15 +1563,19 @@ else: def sum(__iterable: Iterable[bool], __start: int = ...) -> int: ... # type: ignore[misc] @overload -def sum(__iterable: Iterable[_SumT]) -> _SumT | Literal[0]: ... +def sum(__iterable: Iterable[_SupportsAdditionWithIntT]) -> _SupportsAdditionWithIntT | Literal[0]: ... if sys.version_info >= (3, 8): @overload - def sum(__iterable: Iterable[_SumT], start: _SumS) -> _SumT | _SumS: ... + def sum( + __iterable: Iterable[_SupportsAdditionT_1], start: _SupportsAdditionT_2 + ) -> _SupportsAdditionT_1 | _SupportsAdditionT_2: ... else: @overload - def sum(__iterable: Iterable[_SumT], __start: _SumS) -> _SumT | _SumS: ... + def sum( + __iterable: Iterable[_SupportsAdditionT_1], __start: _SupportsAdditionT_2 + ) -> _SupportsAdditionT_1 | _SupportsAdditionT_2: ... # The argument to `vars()` has to have a `__dict__` attribute, so can't be annotated with `object` # (A "SupportsDunderDict" protocol doesn't work) diff --git a/test_cases/stdlib/builtins/test_sum.py b/test_cases/stdlib/builtins/test_sum.py new file mode 100644 index 000000000000..5a9c7c92e15b --- /dev/null +++ b/test_cases/stdlib/builtins/test_sum.py @@ -0,0 +1,27 @@ +# pyright: reportUnnecessaryTypeIgnoreComment=true + +from typing import List +from typing_extensions import assert_type + +assert_type(sum([2, 4]), int) +assert_type(sum([3, 5], 4), int) + +assert_type(sum([True, False]), int) +assert_type(sum([True, False], True), int) + +assert_type(sum([['foo'], ['bar']], ['baz']), List[str]) + +# mypy and pyright infer the types differently for these, so we can't use assert_type +# Just test that no error is emitted for any of these +sum([('foo',), ('bar', 'baz')], ()) # mypy: `tuple[str, ...]`; pyright: `tuple[()] | tuple[str] | tuple[str, str]` +sum([5.6, 3.2]) # mypy: `float`; pyright: `float | Literal[0]` +sum([2.5, 5.8], 5) # mypy: `float`; pyright: `float | int` + +# These all fail at runtime +sum('abcde') # type: ignore[arg-type] +sum([['foo'], ['bar']]) # type: ignore[list-item] +sum([('foo',), ('bar', 'baz')]) # type: ignore[list-item] + +# TODO: these pass pyright with the current stubs, but mypy erroneously emits an error: +# sum([3, Fraction(7, 22), complex(8, 0), 9.83]) +# sum([3, Decimal('0.98')]) From a15b2ad25a15120af8e53fd34a5c8e1f9479965b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 14:39:36 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks --- test_cases/stdlib/builtins/test_sum.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test_cases/stdlib/builtins/test_sum.py b/test_cases/stdlib/builtins/test_sum.py index 5a9c7c92e15b..707b429979c3 100644 --- a/test_cases/stdlib/builtins/test_sum.py +++ b/test_cases/stdlib/builtins/test_sum.py @@ -9,18 +9,18 @@ assert_type(sum([True, False]), int) assert_type(sum([True, False], True), int) -assert_type(sum([['foo'], ['bar']], ['baz']), List[str]) +assert_type(sum([["foo"], ["bar"]], ["baz"]), List[str]) # mypy and pyright infer the types differently for these, so we can't use assert_type # Just test that no error is emitted for any of these -sum([('foo',), ('bar', 'baz')], ()) # mypy: `tuple[str, ...]`; pyright: `tuple[()] | tuple[str] | tuple[str, str]` +sum([("foo",), ("bar", "baz")], ()) # mypy: `tuple[str, ...]`; pyright: `tuple[()] | tuple[str] | tuple[str, str]` sum([5.6, 3.2]) # mypy: `float`; pyright: `float | Literal[0]` sum([2.5, 5.8], 5) # mypy: `float`; pyright: `float | int` # These all fail at runtime -sum('abcde') # type: ignore[arg-type] -sum([['foo'], ['bar']]) # type: ignore[list-item] -sum([('foo',), ('bar', 'baz')]) # type: ignore[list-item] +sum("abcde") # type: ignore[arg-type] +sum([["foo"], ["bar"]]) # type: ignore[list-item] +sum([("foo",), ("bar", "baz")]) # type: ignore[list-item] # TODO: these pass pyright with the current stubs, but mypy erroneously emits an error: # sum([3, Fraction(7, 22), complex(8, 0), 9.83]) From 30d5339cf02a7bd0c5b2479994967506e7c76e34 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Tue, 31 May 2022 19:43:27 +0100 Subject: [PATCH 3/6] Improve names --- stdlib/_typeshed/__init__.pyi | 4 ++-- stdlib/builtins.pyi | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index 06d25b14780a..f2c63bea7b8c 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -69,10 +69,10 @@ SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichC # Dunder protocols -class SupportsAddition(Protocol): +class SupportsAdd(Protocol): def __add__(self, __x: Any) -> Any: ... -class SupportsAdditionWithInt(Protocol): +class SupportsAddWithInt(Protocol): def __add__(self, __x: int) -> Any: ... class SupportsDivMod(Protocol[_T_contra, _T_co]): diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index d868655f3faf..f961a710938c 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -12,8 +12,8 @@ from _typeshed import ( ReadableBuffer, Self, StrOrBytesPath, - SupportsAddition, - SupportsAdditionWithInt, + SupportsAdd, + SupportsAddWithInt, SupportsAiter, SupportsAnext, SupportsDivMod, @@ -1546,9 +1546,9 @@ def sorted( @overload def sorted(__iterable: Iterable[_T], *, key: Callable[[_T], SupportsRichComparison], reverse: bool = ...) -> list[_T]: ... -_SupportsAdditionT_1 = TypeVar("_SupportsAdditionT_1", bound=SupportsAddition) -_SupportsAdditionT_2 = TypeVar("_SupportsAdditionT_2", bound=SupportsAddition) -_SupportsAdditionWithIntT = TypeVar("_SupportsAdditionWithIntT", bound=SupportsAdditionWithInt) +_AddableT1 = TypeVar("_AddableT1", bound=SupportsAdd) +_AddableT2 = TypeVar("_AddableT2", bound=SupportsAdd) +_AddableWithIntT = TypeVar("_AddableWithIntT", bound=SupportsAddWithInt) # In general, the return type of `x + x` is *not* guaranteed to be the same type as x. # However, we can't express that in the stub for `sum()` @@ -1563,19 +1563,15 @@ else: def sum(__iterable: Iterable[bool], __start: int = ...) -> int: ... # type: ignore[misc] @overload -def sum(__iterable: Iterable[_SupportsAdditionWithIntT]) -> _SupportsAdditionWithIntT | Literal[0]: ... +def sum(__iterable: Iterable[_AddableWithIntT]) -> _AddableWithIntT | Literal[0]: ... if sys.version_info >= (3, 8): @overload - def sum( - __iterable: Iterable[_SupportsAdditionT_1], start: _SupportsAdditionT_2 - ) -> _SupportsAdditionT_1 | _SupportsAdditionT_2: ... + def sum(__iterable: Iterable[_AddableT1], start: _AddableT2) -> _AddableT1 | _AddableT2: ... else: @overload - def sum( - __iterable: Iterable[_SupportsAdditionT_1], __start: _SupportsAdditionT_2 - ) -> _SupportsAdditionT_1 | _SupportsAdditionT_2: ... + def sum(__iterable: Iterable[_AddableT1], __start: _AddableT2) -> _AddableT1 | _AddableT2: ... # The argument to `vars()` has to have a `__dict__` attribute, so can't be annotated with `object` # (A "SupportsDunderDict" protocol doesn't work) From 4d4ce35199bc97a67ec5b22de401539e1dd30956 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 3 Jun 2022 18:47:48 +0100 Subject: [PATCH 4/6] Simplify: Just make `SupportsAdd` generic --- stdlib/_typeshed/__init__.pyi | 7 ++----- stdlib/builtins.pyi | 7 +++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index f2c63bea7b8c..7bfb886e1c14 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -69,11 +69,8 @@ SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichC # Dunder protocols -class SupportsAdd(Protocol): - def __add__(self, __x: Any) -> Any: ... - -class SupportsAddWithInt(Protocol): - def __add__(self, __x: int) -> Any: ... +class SupportsAdd(Protocol[_T_contra, _T_co]): + def __add__(self, __x: _T_contra) -> _T_co: ... class SupportsDivMod(Protocol[_T_contra, _T_co]): def __divmod__(self, __other: _T_contra) -> _T_co: ... diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index f961a710938c..1badd3f031b3 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -13,7 +13,6 @@ from _typeshed import ( Self, StrOrBytesPath, SupportsAdd, - SupportsAddWithInt, SupportsAiter, SupportsAnext, SupportsDivMod, @@ -1546,9 +1545,9 @@ def sorted( @overload def sorted(__iterable: Iterable[_T], *, key: Callable[[_T], SupportsRichComparison], reverse: bool = ...) -> list[_T]: ... -_AddableT1 = TypeVar("_AddableT1", bound=SupportsAdd) -_AddableT2 = TypeVar("_AddableT2", bound=SupportsAdd) -_AddableWithIntT = TypeVar("_AddableWithIntT", bound=SupportsAddWithInt) +_AddableT1 = TypeVar("_AddableT1", bound=SupportsAdd[Any, Any]) +_AddableT2 = TypeVar("_AddableT2", bound=SupportsAdd[Any, Any]) +_AddableWithIntT = TypeVar("_AddableWithIntT", bound=SupportsAdd[int, Any]) # In general, the return type of `x + x` is *not* guaranteed to be the same type as x. # However, we can't express that in the stub for `sum()` From 90e6fa7287a8122b77cf6ed07852329dcb7c7bb6 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 8 Jun 2022 19:54:00 +0100 Subject: [PATCH 5/6] Address review --- stdlib/_typeshed/__init__.pyi | 3 +++ stdlib/builtins.pyi | 8 +++++-- test_cases/stdlib/builtins/test_sum.py | 29 ++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index 7bfb886e1c14..aef6b553ea92 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -72,6 +72,9 @@ SupportsRichComparisonT = TypeVar("SupportsRichComparisonT", bound=SupportsRichC class SupportsAdd(Protocol[_T_contra, _T_co]): def __add__(self, __x: _T_contra) -> _T_co: ... +class SupportsRAdd(Protocol[_T_contra, _T_co]): + def __radd__(self, __x: _T_contra) -> _T_co: ... + class SupportsDivMod(Protocol[_T_contra, _T_co]): def __divmod__(self, __other: _T_contra) -> _T_co: ... diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index 1badd3f031b3..1ab87d1ae869 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -20,6 +20,7 @@ from _typeshed import ( SupportsKeysAndGetItem, SupportsLenAndGetItem, SupportsNext, + SupportsRAdd, SupportsRDivMod, SupportsRichComparison, SupportsRichComparisonT, @@ -1547,7 +1548,10 @@ def sorted(__iterable: Iterable[_T], *, key: Callable[[_T], SupportsRichComparis _AddableT1 = TypeVar("_AddableT1", bound=SupportsAdd[Any, Any]) _AddableT2 = TypeVar("_AddableT2", bound=SupportsAdd[Any, Any]) -_AddableWithIntT = TypeVar("_AddableWithIntT", bound=SupportsAdd[int, Any]) + +class _SupportsSumWithNoDefaultGiven(SupportsAdd[Any, Any], SupportsRAdd[int, Any], Protocol): ... + +_SupportsSumNoDefaultT = TypeVar("_SupportsSumNoDefaultT", bound=_SupportsSumWithNoDefaultGiven) # In general, the return type of `x + x` is *not* guaranteed to be the same type as x. # However, we can't express that in the stub for `sum()` @@ -1562,7 +1566,7 @@ else: def sum(__iterable: Iterable[bool], __start: int = ...) -> int: ... # type: ignore[misc] @overload -def sum(__iterable: Iterable[_AddableWithIntT]) -> _AddableWithIntT | Literal[0]: ... +def sum(__iterable: Iterable[_SupportsSumNoDefaultT]) -> _SupportsSumNoDefaultT | Literal[0]: ... if sys.version_info >= (3, 8): @overload diff --git a/test_cases/stdlib/builtins/test_sum.py b/test_cases/stdlib/builtins/test_sum.py index 707b429979c3..a92c41df7390 100644 --- a/test_cases/stdlib/builtins/test_sum.py +++ b/test_cases/stdlib/builtins/test_sum.py @@ -1,7 +1,26 @@ # pyright: reportUnnecessaryTypeIgnoreComment=true -from typing import List -from typing_extensions import assert_type +from typing import Any, List, Union +from typing_extensions import Literal, assert_type + + +class Foo: + def __add__(self, other: Any) -> Foo: + return Foo() + + +class Bar: + def __radd__(self, other: Any) -> Bar: + return Bar() + + +class Baz: + def __add__(self, other: Any) -> Baz: + return Baz() + + def __radd__(self, other: Any) -> Baz: + return Baz() + assert_type(sum([2, 4]), int) assert_type(sum([3, 5], 4), int) @@ -11,6 +30,9 @@ assert_type(sum([["foo"], ["bar"]], ["baz"]), List[str]) +assert_type(sum([Foo(), Foo()], start=Foo()), Foo) +assert_type(sum([Baz(), Baz()]), Union[Baz, Literal[0]]) + # mypy and pyright infer the types differently for these, so we can't use assert_type # Just test that no error is emitted for any of these sum([("foo",), ("bar", "baz")], ()) # mypy: `tuple[str, ...]`; pyright: `tuple[()] | tuple[str] | tuple[str, str]` @@ -21,6 +43,9 @@ sum("abcde") # type: ignore[arg-type] sum([["foo"], ["bar"]]) # type: ignore[list-item] sum([("foo",), ("bar", "baz")]) # type: ignore[list-item] +sum([Foo(), Foo()]) # type: ignore[list-item] +sum([Bar(), Bar()], start=Bar()) # type: ignore[call-overload] +sum([Bar(), Bar()]) # type: ignore[list-item] # TODO: these pass pyright with the current stubs, but mypy erroneously emits an error: # sum([3, Fraction(7, 22), complex(8, 0), 9.83]) From b00b2d6312c8b780e5648f69c8b70f161b7f3cd4 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 8 Jun 2022 19:58:22 +0100 Subject: [PATCH 6/6] Fix --- test_cases/stdlib/builtins/test_sum.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test_cases/stdlib/builtins/test_sum.py b/test_cases/stdlib/builtins/test_sum.py index a92c41df7390..c41f895fef34 100644 --- a/test_cases/stdlib/builtins/test_sum.py +++ b/test_cases/stdlib/builtins/test_sum.py @@ -5,20 +5,20 @@ class Foo: - def __add__(self, other: Any) -> Foo: + def __add__(self, other: Any) -> "Foo": return Foo() class Bar: - def __radd__(self, other: Any) -> Bar: + def __radd__(self, other: Any) -> "Bar": return Bar() class Baz: - def __add__(self, other: Any) -> Baz: + def __add__(self, other: Any) -> "Baz": return Baz() - def __radd__(self, other: Any) -> Baz: + def __radd__(self, other: Any) -> "Baz": return Baz() @@ -30,7 +30,7 @@ def __radd__(self, other: Any) -> Baz: assert_type(sum([["foo"], ["bar"]], ["baz"]), List[str]) -assert_type(sum([Foo(), Foo()], start=Foo()), Foo) +assert_type(sum([Foo(), Foo()], Foo()), Foo) assert_type(sum([Baz(), Baz()]), Union[Baz, Literal[0]]) # mypy and pyright infer the types differently for these, so we can't use assert_type @@ -44,7 +44,7 @@ def __radd__(self, other: Any) -> Baz: sum([["foo"], ["bar"]]) # type: ignore[list-item] sum([("foo",), ("bar", "baz")]) # type: ignore[list-item] sum([Foo(), Foo()]) # type: ignore[list-item] -sum([Bar(), Bar()], start=Bar()) # type: ignore[call-overload] +sum([Bar(), Bar()], Bar()) # type: ignore[call-overload] sum([Bar(), Bar()]) # type: ignore[list-item] # TODO: these pass pyright with the current stubs, but mypy erroneously emits an error: