diff --git a/stdlib/@tests/test_cases/builtins/check_set.py b/stdlib/@tests/test_cases/builtins/check_set.py index 545177169889..958c1ef0550b 100644 --- a/stdlib/@tests/test_cases/builtins/check_set.py +++ b/stdlib/@tests/test_cases/builtins/check_set.py @@ -1,4 +1,4 @@ -from collections.abc import Set as AbstractSet +from collections.abc import Iterator, MutableSet, Set as AbstractSet from typing_extensions import Literal, assert_type @@ -58,15 +58,104 @@ def test_frozenset_interface(s: frozenset[Literal["foo", "bar"]], y: frozenset[s assert_type(s ^ y, frozenset[str]) -def test_xor_with_abstract_set( +# set and frozenset only support set and frozenset operands. Other abstract +# sets are handled by their reflected methods, which return abstract sets. +def test_binary_ops_with_abstract_set( s: set[Literal["foo", "bar"]], frozen: frozenset[Literal["foo", "bar"]], other: AbstractSet[str] ) -> None: + s.__and__(other) # type: ignore + s.__or__(other) # type: ignore + s.__sub__(other) # type: ignore s.__xor__(other) # type: ignore + frozen.__and__(other) # type: ignore + frozen.__or__(other) # type: ignore + frozen.__sub__(other) # type: ignore frozen.__xor__(other) # type: ignore - assert_type(other.__rxor__(s), AbstractSet[str]) - assert_type(other.__rxor__(frozen), AbstractSet[str]) - assert_type(other.__rxor__([1]), AbstractSet[str | int]) - assert_type([1] ^ other, AbstractSet[str | int]) - assert_type(("foo", "baz") ^ other, AbstractSet[str]) + assert_type(s & other, AbstractSet[Literal["foo", "bar"]]) + assert_type(s | other, AbstractSet[str]) + assert_type(s - other, AbstractSet[Literal["foo", "bar"]]) assert_type(s ^ other, AbstractSet[str]) + assert_type(frozen & other, AbstractSet[Literal["foo", "bar"]]) + assert_type(frozen | other, AbstractSet[str]) + assert_type(frozen - other, AbstractSet[Literal["foo", "bar"]]) assert_type(frozen ^ other, AbstractSet[str]) + + # The mixin methods accept any iterable, but set and frozenset don't. + [1] & other # type: ignore + [1] | other # type: ignore + [1] - other # type: ignore + [1] ^ other # type: ignore + + +# The in-place operators of set only work in place for set and frozenset +# operands. For other operands, the target is rebound to the result of the +# operand's reflected method. +def test_inplace_ops_with_other_operands(s: set[int], ms: MutableSet[int], d: dict[int, str], other: AbstractSet[int]) -> None: + assert_type(s.__ior__(d.keys()), set[int]) + assert_type(s.__iand__(d.keys()), set[int]) + assert_type(s.__isub__(d.keys()), set[int]) + assert_type(s.__ixor__(d.keys()), set[int]) + + s |= d.keys() + s &= d.keys() + s -= d.keys() + s ^= d.keys() + ms |= d.keys() + ms &= d.keys() + ms -= d.keys() + ms ^= d.keys() + + # Dict items views rebind the target to a set of tuples. + pairs: set[tuple[int, str]] = set() + pairs |= d.items() + s |= d.items() # type: ignore + + # At runtime, these rebind the target to an arbitrary abstract set, + # which is not necessarily a set or even mutable. + s |= other # type: ignore + s &= other # type: ignore + s -= other # type: ignore + s ^= other # type: ignore + ms |= other # type: ignore + ms &= other # type: ignore + ms -= other # type: ignore + ms ^= other # type: ignore + + +class _ImmutableSet(AbstractSet[int]): + def __contains__(self, x: object) -> bool: + return False + + def __iter__(self) -> Iterator[int]: + return iter(()) + + def __len__(self) -> int: + return 0 + + +def test_inplace_ops_with_custom_set(s: set[int], ms: MutableSet[int]) -> None: + # At runtime, s and ms would be rebound to an _ImmutableSet instance. + s |= _ImmutableSet() # type: ignore + s &= _ImmutableSet() # type: ignore + s -= _ImmutableSet() # type: ignore + s ^= _ImmutableSet() # type: ignore + ms |= _ImmutableSet() # type: ignore + ms &= _ImmutableSet() # type: ignore + ms -= _ImmutableSet() # type: ignore + ms ^= _ImmutableSet() # type: ignore + + # Set operands are still modified in place. + assert_type(s.__ior__({1}), set[int]) + assert_type(s.__iand__(frozenset({1})), set[int]) + s |= {1} + s &= frozenset({1}) + ms |= {1} + ms &= frozenset({1}) + + +def test_binary_ops_with_custom_set(s: set[int], frozen: frozenset[int]) -> None: + # The result is an _ImmutableSet instance at runtime, not a set. + x: set[int] = s & _ImmutableSet() # type: ignore + y: frozenset[int] = frozen | _ImmutableSet() # type: ignore + assert_type(s & _ImmutableSet(), AbstractSet[int]) + assert_type(frozen | _ImmutableSet(), AbstractSet[int]) diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi index fa6ef8150e3a..96c2d898c157 100644 --- a/stdlib/_typeshed/__init__.pyi +++ b/stdlib/_typeshed/__init__.pyi @@ -146,6 +146,15 @@ class SupportsDivMod(Protocol[_T_contra, _T_co]): class SupportsRDivMod(Protocol[_T_contra, _T_co]): def __rdivmod__(self, other: _T_contra, /) -> _T_co: ... +class SupportsRAnd(Protocol[_T_contra, _T_co]): + def __rand__(self, other: _T_contra, /) -> _T_co: ... + +class SupportsROr(Protocol[_T_contra, _T_co]): + def __ror__(self, other: _T_contra, /) -> _T_co: ... + +class SupportsRXor(Protocol[_T_contra, _T_co]): + def __rxor__(self, other: _T_contra, /) -> _T_co: ... + # This protocol is generic over the iterator type, while Iterable is # generic over the type that is iterated over. class SupportsIter(Protocol[_T_co]): diff --git a/stdlib/_weakrefset.pyi b/stdlib/_weakrefset.pyi index 82ffa4463f48..b2d0bbadbba7 100644 --- a/stdlib/_weakrefset.pyi +++ b/stdlib/_weakrefset.pyi @@ -27,11 +27,11 @@ class WeakSet(MutableSet[_T]): def difference(self, other: Iterable[_T]) -> Self: ... def __sub__(self, other: Iterable[Any]) -> Self: ... def difference_update(self, other: Iterable[Any]) -> None: ... - def __isub__(self, other: Iterable[Any]) -> Self: ... + def __isub__(self, other: Iterable[Any]) -> Self: ... # type: ignore[override] def intersection(self, other: Iterable[_T]) -> Self: ... def __and__(self, other: Iterable[Any]) -> Self: ... def intersection_update(self, other: Iterable[Any]) -> None: ... - def __iand__(self, other: Iterable[Any]) -> Self: ... + def __iand__(self, other: Iterable[Any]) -> Self: ... # type: ignore[override] def issubset(self, other: Iterable[_T]) -> bool: ... def __le__(self, other: Iterable[_T]) -> bool: ... def __lt__(self, other: Iterable[_T]) -> bool: ... diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index 0075adf561c2..a51621d10039 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -25,9 +25,13 @@ from _typeshed import ( SupportsLenAndGetItem, SupportsNext, SupportsRAdd, + SupportsRAnd, SupportsRDivMod, SupportsRichComparison, SupportsRichComparisonT, + SupportsROr, + SupportsRSub, + SupportsRXor, SupportsWrite, ) from collections.abc import Awaitable, Callable, Hashable, Iterable, Iterator, MutableSet, Set as AbstractSet, Sized @@ -79,6 +83,7 @@ _R_co = TypeVar("_R_co", covariant=True) _KT = TypeVar("_KT") _VT = TypeVar("_VT") _S = TypeVar("_S") +_R = TypeVar("_R") _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") _T3 = TypeVar("_T3") @@ -1453,32 +1458,48 @@ class set(MutableSet[_T]): def __len__(self) -> int: ... def __contains__(self, o: object, /) -> bool: ... def __iter__(self) -> Iterator[_T]: ... - def __and__(self, value: AbstractSet[object], /) -> set[_T]: ... - def __iand__(self, value: AbstractSet[object], /) -> Self: ... + def __and__(self, value: set[Any] | frozenset[Any], /) -> set[_T]: ... # type: ignore[override] + @overload # type: ignore[misc] + def __iand__(self, value: set[Any] | frozenset[Any], /) -> Self: ... @overload + def __iand__(self, value: SupportsRAnd[set[_T], _R], /) -> _R: ... + + @overload # type: ignore[override] def __rand__(self, value: set[_S], /) -> set[_S]: ... @overload def __rand__(self, value: frozenset[_S], /) -> frozenset[_S]: ... - def __or__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... - def __ior__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] + def __or__(self, value: set[_S] | frozenset[_S], /) -> set[_T | _S]: ... # type: ignore[override] + @overload # type: ignore[override,misc] + def __ior__(self, value: set[_T] | frozenset[_T], /) -> Self: ... @overload + def __ior__(self, value: SupportsROr[set[_T], _R], /) -> _R: ... + + @overload # type: ignore[override] def __ror__(self, value: set[_S], /) -> set[_T | _S]: ... @overload def __ror__(self, value: frozenset[_S], /) -> frozenset[_T | _S]: ... - def __sub__(self, value: AbstractSet[object], /) -> set[_T]: ... - def __isub__(self, value: AbstractSet[object], /) -> Self: ... + def __sub__(self, value: set[Any] | frozenset[Any], /) -> set[_T]: ... # type: ignore[override] + @overload # type: ignore[misc] + def __isub__(self, value: set[Any] | frozenset[Any], /) -> Self: ... @overload + def __isub__(self, value: SupportsRSub[set[_T], _R], /) -> _R: ... + + @overload # type: ignore[override] def __rsub__(self, value: set[_S], /) -> set[_S]: ... @overload def __rsub__(self, value: frozenset[_S], /) -> frozenset[_S]: ... def __xor__(self, value: set[_S] | frozenset[_S], /) -> set[_T | _S]: ... # type: ignore[override] - def __ixor__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] + + @overload # type: ignore[override,misc] + def __ixor__(self, value: set[_T] | frozenset[_T], /) -> Self: ... + @overload + def __ixor__(self, value: SupportsRXor[set[_T], _R], /) -> _R: ... @overload # type: ignore[override] def __rxor__(self, value: set[_S], /) -> set[_T | _S]: ... @@ -1511,23 +1532,25 @@ class frozenset(AbstractSet[_T_co]): def __len__(self) -> int: ... def __contains__(self, o: object, /) -> bool: ... def __iter__(self) -> Iterator[_T_co]: ... - def __and__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ... + # The binary operators only support set and frozenset. For other operands, + # Python falls back to the operand's reflected method. + def __and__(self, value: set[Any] | frozenset[Any], /) -> frozenset[_T_co]: ... # type: ignore[override] - @overload + @overload # type: ignore[override] def __rand__(self, value: set[_S], /) -> set[_S]: ... @overload def __rand__(self, value: frozenset[_S], /) -> frozenset[_S]: ... - def __or__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ... + def __or__(self, value: set[_S] | frozenset[_S], /) -> frozenset[_T_co | _S]: ... # type: ignore[override] - @overload + @overload # type: ignore[override] def __ror__(self, value: set[_S], /) -> set[_T_co | _S]: ... @overload def __ror__(self, value: frozenset[_S], /) -> frozenset[_T_co | _S]: ... - def __sub__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ... + def __sub__(self, value: set[Any] | frozenset[Any], /) -> frozenset[_T_co]: ... # type: ignore[override] - @overload + @overload # type: ignore[override] def __rsub__(self, value: set[_S], /) -> set[_S]: ... @overload def __rsub__(self, value: frozenset[_S], /) -> frozenset[_S]: ... diff --git a/stdlib/multiprocessing/managers.pyi b/stdlib/multiprocessing/managers.pyi index 0727f60f1080..6768f16832ed 100644 --- a/stdlib/multiprocessing/managers.pyi +++ b/stdlib/multiprocessing/managers.pyi @@ -180,11 +180,11 @@ if sys.version_info >= (3, 14): def __contains__(self, o: object, /) -> bool: ... def __iter__(self) -> Iterator[_T]: ... def __and__(self, value: AbstractSet[object], /) -> set[_T]: ... - def __iand__(self, value: AbstractSet[object], /) -> Self: ... + def __iand__(self, value: AbstractSet[object], /) -> Self: ... # type: ignore[override] def __or__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... def __ior__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] def __sub__(self, value: AbstractSet[_T | None], /) -> set[_T]: ... - def __isub__(self, value: AbstractSet[object], /) -> Self: ... + def __isub__(self, value: AbstractSet[object], /) -> Self: ... # type: ignore[override] def __xor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... def __ixor__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] def __le__(self, value: AbstractSet[object], /) -> bool: ... @@ -192,9 +192,9 @@ if sys.version_info >= (3, 14): def __ge__(self, value: AbstractSet[object], /) -> bool: ... def __gt__(self, value: AbstractSet[object], /) -> bool: ... def __eq__(self, value: object, /) -> bool: ... - def __rand__(self, value: AbstractSet[object], /) -> set[_T]: ... + def __rand__(self, value: AbstractSet[object], /) -> set[_T]: ... # type: ignore[override] def __ror__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] - def __rsub__(self, value: AbstractSet[_T], /) -> set[_T]: ... + def __rsub__(self, value: AbstractSet[_T], /) -> set[_T]: ... # type: ignore[override] def __rxor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[override,misc] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index b8b90a0934e5..0f25cf38be2f 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -5,7 +5,18 @@ import collections # ruff: ignore[unused-import] # pyright: ignore[reportUnuse import sys import typing_extensions from _collections_abc import dict_items, dict_keys, dict_values -from _typeshed import IdentityFunction, ReadableBuffer, SupportsGetItem, SupportsGetItemViewable, SupportsKeysAndGetItem, Viewable +from _typeshed import ( + IdentityFunction, + ReadableBuffer, + SupportsGetItem, + SupportsGetItemViewable, + SupportsKeysAndGetItem, + SupportsRAnd, + SupportsROr, + SupportsRSub, + SupportsRXor, + Viewable, +) from abc import ABCMeta, abstractmethod from re import Match as Match, Pattern as Pattern from types import ( @@ -411,6 +422,7 @@ _FT = TypeVar("_FT", bound=Callable[..., Any] | type) # These type variables are used by the container types. _S = TypeVar("_S") +_R = TypeVar("_R") _KT = TypeVar("_KT") # Key type. _VT = TypeVar("_VT") # Value type. _T_co = TypeVar("_T_co", covariant=True) # Any type covariant containers. @@ -740,11 +752,20 @@ class AbstractSet(Collection[_T_co]): def __lt__(self, other: AbstractSet[Any], /) -> bool: ... def __gt__(self, other: AbstractSet[Any], /) -> bool: ... def __ge__(self, other: AbstractSet[Any], /) -> bool: ... + # These annotations describe what all implementations support, including + # set and frozenset, which don't use the mixin methods. While the mixin + # methods accept any iterable, set and frozenset only accept set and + # frozenset. The forward methods still accept any AbstractSet, because + # if one operand doesn't support the other, Python falls back to the + # other operand's reflected method, which always returns an AbstractSet. def __and__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... def __or__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... def __sub__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... def __xor__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... - def __rxor__(self, other: Iterable[_T], /) -> AbstractSet[_T_co | _T]: ... + def __rand__(self, other: AbstractSet[_T], /) -> AbstractSet[_T]: ... + def __ror__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... + def __rsub__(self, other: AbstractSet[_T], /) -> AbstractSet[_T]: ... + def __rxor__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... def __eq__(self, other: object, /) -> bool: ... def isdisjoint(self, other: Iterable[Any], /) -> bool: ... @@ -757,10 +778,32 @@ class MutableSet(AbstractSet[_T]): def clear(self) -> None: ... def pop(self) -> _T: ... def remove(self, value: _T, /) -> None: ... - def __ior__(self, it: AbstractSet[_T], /) -> typing_extensions.Self: ... # type: ignore[override,misc] - def __iand__(self, it: AbstractSet[Any], /) -> typing_extensions.Self: ... - def __ixor__(self, it: AbstractSet[_T], /) -> typing_extensions.Self: ... # type: ignore[override,misc] - def __isub__(self, it: AbstractSet[Any], /) -> typing_extensions.Self: ... + + # These annotations describe what all implementations support, including + # set, which doesn't use the mixin methods. While the mixin methods accept + # any iterable and always modify the set in place, set only does so for + # set and frozenset arguments. For other arguments, Python falls back to + # the argument's reflected method and rebinds the target to its result, + # which might not even be a mutable set. + @overload # type: ignore[override,misc] + def __ior__(self, it: set[_T] | frozenset[_T], /) -> typing_extensions.Self: ... + @overload + def __ior__(self, it: SupportsROr[MutableSet[_T], _R], /) -> typing_extensions.Self | _R: ... + + @overload # type: ignore[misc] + def __iand__(self, it: set[Any] | frozenset[Any], /) -> typing_extensions.Self: ... + @overload + def __iand__(self, it: SupportsRAnd[MutableSet[_T], _R], /) -> typing_extensions.Self | _R: ... + + @overload # type: ignore[override,misc] + def __ixor__(self, it: set[_T] | frozenset[_T], /) -> typing_extensions.Self: ... + @overload + def __ixor__(self, it: SupportsRXor[MutableSet[_T], _R], /) -> typing_extensions.Self | _R: ... + + @overload # type: ignore[misc] + def __isub__(self, it: set[Any] | frozenset[Any], /) -> typing_extensions.Self: ... + @overload + def __isub__(self, it: SupportsRSub[MutableSet[_T], _R], /) -> typing_extensions.Self | _R: ... class MappingView(Sized): __slots__ = ("_mapping",) diff --git a/stubs/boltons/boltons/setutils.pyi b/stubs/boltons/boltons/setutils.pyi index 9dd1edee1a73..149051b9b5c7 100644 --- a/stubs/boltons/boltons/setutils.pyi +++ b/stubs/boltons/boltons/setutils.pyi @@ -39,8 +39,8 @@ class IndexedSet(MutableSet[Any]): __rand__ = intersection # __sub__ = difference # __xor__ = symmetric_difference - __rxor__ = symmetric_difference # type: ignore[assignment] # pyright: ignore[reportAssignmentType] - def __rsub__(self, other: _RSub[_T_co]) -> _RSub[_T_co]: ... + __rxor__ = symmetric_difference # type: ignore[assignment] + def __rsub__(self, other: _RSub[_T_co]) -> _RSub[_T_co]: ... # type: ignore[override] def update(self, *others: Iterable[Any]) -> None: ... def intersection_update(self, *others: Iterable[Any]) -> None: ... def difference_update(self, *others: Container[Any]) -> None: ...