Skip to content
Open
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
103 changes: 96 additions & 7 deletions stdlib/@tests/test_cases/builtins/check_set.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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])
9 changes: 9 additions & 0 deletions stdlib/_typeshed/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
4 changes: 2 additions & 2 deletions stdlib/_weakrefset.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down
49 changes: 36 additions & 13 deletions stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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]: ...
Expand Down Expand Up @@ -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]: ...
Expand Down
8 changes: 4 additions & 4 deletions stdlib/multiprocessing/managers.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -180,21 +180,21 @@ 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: ...
def __lt__(self, value: AbstractSet[object], /) -> bool: ...
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: ...

Expand Down
55 changes: 49 additions & 6 deletions stdlib/typing.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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: ...

Expand All @@ -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",)
Expand Down
4 changes: 2 additions & 2 deletions stubs/boltons/boltons/setutils.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down
Loading