From 48e5c71048d9b3bc335fda5df2ef0413f5959685 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Mon, 7 Jul 2025 17:29:07 +0200 Subject: [PATCH 1/2] use SupportsBool as return type + added tests for `min` --- .../@tests/test_cases/builtins/check_min.py | 94 +++++++++++++++++++ stdlib/_typeshed/__init__.pyi | 10 +- 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 stdlib/@tests/test_cases/builtins/check_min.py diff --git a/stdlib/@tests/test_cases/builtins/check_min.py b/stdlib/@tests/test_cases/builtins/check_min.py new file mode 100644 index 000000000000..a300ad45c482 --- /dev/null +++ b/stdlib/@tests/test_cases/builtins/check_min.py @@ -0,0 +1,94 @@ +from typing_extensions import assert_type + + +def test_min_builtin() -> None: + # legal comparisons that succeed at runtime + b1, b2 = bool(True), bool(False) + i1, i2 = int(1), int(2) + s1, s2 = str("a"), str("b") + f1, f2 = float(0.5), float(2.3) + l1, l2 = list[int]([1, 2]), list[int]([3, 4]) + t1, t2 = tuple[str, str](("A", "B")), tuple[str, str](("C", "D")) + tN = tuple[str, ...](["A", "B", "C"]) + + assert_type(min(b1, b2), bool) + assert_type(min(i1, i2), int) + assert_type(min(s1, s2), str) + assert_type(min(f1, f2), float) + + # mixed numerical types (note: float = int or float) + assert_type(min(b1, i1), int) + assert_type(min(i1, b1), int) + + assert_type(min(b1, f1), float) + assert_type(min(f1, b1), float) + + assert_type(min(i1, f1), float) + assert_type(min(f1, i1), float) + + # comparisons with lists and tuples + assert_type(min(l1, l2), list[int]) + assert_type(min(t1, t2), tuple[str, str]) + assert_type(min(tN, t2), tuple[str, ...]) + + +def test_min_bad_builtin() -> None: + # illegal comparisons that fail at runtime + i1 = int(1) + s1 = str("a") + f1 = float(1.0) + c1, c2 = complex(1.0, 2.0), complex(3.0, 4.0) + list_str = list[str](["A", "B"]) + list_int = list[int]([2, 3]) + tup_str = tuple[str, str](("A", "B")) + tup_int = tuple[int, int]((2, 3)) + + # True negatives. + min(c1, c2) # type: ignore + + # FIXME: False negatives. + min(i1, s1) + min(s1, f1) + min(f1, list_str) + min(list_str, list_int) + min(tup_str, tup_int) + + +def test_min_custom_comparison() -> None: + class BoolScalar: + def __bool__(self) -> bool: ... + + class FloatScalar: + def __float__(self) -> float: ... + def __ge__(self, other: "FloatScalar") -> BoolScalar: ... + def __gt__(self, other: "FloatScalar") -> BoolScalar: ... + def __lt__(self, other: "FloatScalar") -> BoolScalar: ... + def __le__(self, other: "FloatScalar") -> BoolScalar: ... + + f1 = FloatScalar() + f2 = FloatScalar() + + assert_type(min(f1, f2), FloatScalar) + + +def test_min_bad_custom_type() -> None: + class FloatScalar: + def __float__(self) -> float: ... + def __ge__(self, other: "FloatScalar") -> object: + return object() + + def __gt__(self, other: "FloatScalar") -> object: + return object() + + def __lt__(self, other: "FloatScalar") -> object: + return object() + + def __le__(self, other: "FloatScalar") -> object: + return object() + + f1 = FloatScalar() + f2 = FloatScalar() + + # Note: min(f1, f2) works at runtime, but always returns the second argument. + # therefore, we require returning a boolean-like type for comparisons. + min(f1, f2) # type: ignore diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index f322244016dd..1c426ba3980f 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -83,18 +83,20 @@ class SupportsAnext(Protocol[_T_co]): def __anext__(self) -> Awaitable[_T_co]: ... # Comparison protocols +class SupportsBool(Protocol): + def __bool__(self) -> bool: ... class SupportsDunderLT(Protocol[_T_contra]): - def __lt__(self, other: _T_contra, /) -> bool: ... + def __lt__(self, other: _T_contra, /) -> SupportsBool: ... class SupportsDunderGT(Protocol[_T_contra]): - def __gt__(self, other: _T_contra, /) -> bool: ... + def __gt__(self, other: _T_contra, /) -> SupportsBool: ... class SupportsDunderLE(Protocol[_T_contra]): - def __le__(self, other: _T_contra, /) -> bool: ... + def __le__(self, other: _T_contra, /) -> SupportsBool: ... class SupportsDunderGE(Protocol[_T_contra]): - def __ge__(self, other: _T_contra, /) -> bool: ... + def __ge__(self, other: _T_contra, /) -> SupportsBool: ... class SupportsAllComparisons( SupportsDunderLT[Any], SupportsDunderGT[Any], SupportsDunderLE[Any], SupportsDunderGE[Any], Protocol From 2f3e9d6cde36bec6025372e148cdf765a7805dd2 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Mon, 7 Jul 2025 19:52:49 +0200 Subject: [PATCH 2/2] formatting --- stdlib/_typeshed/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index 1c426ba3980f..b5a4941e6965 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -82,10 +82,10 @@ class SupportsNext(Protocol[_T_co]): class SupportsAnext(Protocol[_T_co]): def __anext__(self) -> Awaitable[_T_co]: ... -# Comparison protocols class SupportsBool(Protocol): def __bool__(self) -> bool: ... +# Comparison protocols class SupportsDunderLT(Protocol[_T_contra]): def __lt__(self, other: _T_contra, /) -> SupportsBool: ...