Skip to content

Commit b48e537

Browse files
committed
Return NotImplemented from SortedSet set-algebra operators
`SortedSet.__and__`, `__or__`, `__sub__` and `__xor__` were aliased directly to `intersection`/`union`/`difference`/`symmetric_difference`, so an unsupported (non-iterable) right-hand operand raised `TypeError` from deep inside the set operation instead of returning `NotImplemented`. That prevented Python from falling back to the other operand's reflected operator (e.g. an `Interval` type defining `__rand__`). Give each operator an explicit implementation that returns `NotImplemented` when the operand is not iterable and otherwise delegates to the existing method, mirroring how the comparison operators already use `__make_cmp`. Iterable operands (the existing, documented behavior) are unchanged, and a plain non-iterable operand with no reflected operator still ends up raising `TypeError` as before. Closes #219
1 parent 3ac3586 commit b48e537

2 files changed

Lines changed: 97 additions & 5 deletions

File tree

src/sortedcontainers/sortedset.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
1414
"""
1515

16-
from collections.abc import MutableSet, Sequence, Set
16+
from collections.abc import Iterable, MutableSet, Sequence, Set
1717
from itertools import chain
1818
from operator import eq, ge, gt, le, lt, ne
1919
from textwrap import dedent
@@ -470,7 +470,21 @@ def difference(self, *iterables):
470470
diff = self._set.difference(*iterables)
471471
return self._fromset(diff, key=self._key)
472472

473-
__sub__ = difference
473+
def __sub__(self, other):
474+
"""Return the difference of two sets as a new sorted set.
475+
476+
``ss.__sub__(other)`` <==> ``ss - other``
477+
478+
Return :data:`NotImplemented` when `other` is not iterable so that
479+
Python can fall back to `other`'s reflected operator instead of raising.
480+
481+
:param other: `other` iterable
482+
:return: new sorted set
483+
484+
"""
485+
if isinstance(other, Iterable):
486+
return self.difference(other)
487+
return NotImplemented
474488

475489
def difference_update(self, *iterables):
476490
"""Remove all values of `iterables` from this sorted set.
@@ -524,7 +538,22 @@ def intersection(self, *iterables):
524538
intersect = self._set.intersection(*iterables)
525539
return self._fromset(intersect, key=self._key)
526540

527-
__and__ = intersection
541+
def __and__(self, other):
542+
"""Return the intersection of two sets as a new sorted set.
543+
544+
``ss.__and__(other)`` <==> ``ss & other``
545+
546+
Return :data:`NotImplemented` when `other` is not iterable so that
547+
Python can fall back to `other`'s reflected operator instead of raising.
548+
549+
:param other: `other` iterable
550+
:return: new sorted set
551+
552+
"""
553+
if isinstance(other, Iterable):
554+
return self.intersection(other)
555+
return NotImplemented
556+
528557
__rand__ = __and__
529558

530559
def intersection_update(self, *iterables):
@@ -575,7 +604,22 @@ def symmetric_difference(self, other):
575604
diff = self._set.symmetric_difference(other)
576605
return self._fromset(diff, key=self._key)
577606

578-
__xor__ = symmetric_difference
607+
def __xor__(self, other):
608+
"""Return the symmetric difference with `other` as a new sorted set.
609+
610+
``ss.__xor__(other)`` <==> ``ss ^ other``
611+
612+
Return :data:`NotImplemented` when `other` is not iterable so that
613+
Python can fall back to `other`'s reflected operator instead of raising.
614+
615+
:param other: `other` iterable
616+
:return: new sorted set
617+
618+
"""
619+
if isinstance(other, Iterable):
620+
return self.symmetric_difference(other)
621+
return NotImplemented
622+
579623
__rxor__ = __xor__
580624

581625
def symmetric_difference_update(self, other):
@@ -623,7 +667,22 @@ def union(self, *iterables):
623667
"""
624668
return self.__class__(chain(iter(self), *iterables), key=self._key)
625669

626-
__or__ = union
670+
def __or__(self, other):
671+
"""Return new sorted set with values from itself and `other`.
672+
673+
``ss.__or__(other)`` <==> ``ss | other``
674+
675+
Return :data:`NotImplemented` when `other` is not iterable so that
676+
Python can fall back to `other`'s reflected operator instead of raising.
677+
678+
:param other: `other` iterable
679+
:return: new sorted set
680+
681+
"""
682+
if isinstance(other, Iterable):
683+
return self.union(other)
684+
return NotImplemented
685+
627686
__ror__ = __or__
628687

629688
def update(self, *iterables):

tests/test_coverage_sortedset.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,36 @@ def test_pickle():
535535
beta = pickle.loads(data)
536536
assert alpha == beta
537537
assert alpha._key == beta._key
538+
539+
540+
def test_set_op_returns_notimplemented():
541+
# Set-algebra operators must return NotImplemented for an unsupported
542+
# (non-iterable) operand instead of raising, so the other operand's
543+
# reflected operator can run. See issue #219.
544+
temp = SortedSet(range(10))
545+
546+
assert temp.__and__(5) is NotImplemented
547+
assert temp.__or__(5) is NotImplemented
548+
assert temp.__sub__(5) is NotImplemented
549+
assert temp.__xor__(5) is NotImplemented
550+
551+
# Iterable operands keep working as before.
552+
assert temp & [1, 2, 20] == SortedSet([1, 2])
553+
assert temp - [0, 1] == SortedSet(range(2, 10))
554+
assert temp ^ [0, 10] == SortedSet(list(range(1, 10)) + [10])
555+
assert temp | [10] == SortedSet(range(11))
556+
557+
# A non-iterable operand that defines a reflected operator now wins.
558+
class OnlyReflected:
559+
def __rand__(self, other):
560+
return 'reflected'
561+
562+
assert (temp & OnlyReflected()) == 'reflected'
563+
564+
# A plain non-iterable with no reflected operator still raises TypeError.
565+
try:
566+
temp & 5
567+
except TypeError:
568+
pass
569+
else:
570+
assert False, 'expected TypeError'

0 commit comments

Comments
 (0)