--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 11349
+
+ Revised the set "binary" operators for the association proxy ``set()``
+ interface to correctly raise ``TypeError`` for invalid use of the ``|``,
+ ``&``, ``^``, and ``-`` operators, as well as the in-place mutation
+ versions of these methods, to match the behavior of standard Python
+ ``set()`` as well as SQLAlchemy ORM's "intstrumented" set implementation.
+
+
self, other: AbstractSet[_S]
) -> MutableSet[Union[_T, _S]]:
if not collections._set_binops_check_strict(self, other):
- raise NotImplementedError()
+ return NotImplemented
for value in other:
self.add(value)
return self
return set(self).union(*s)
def __or__(self, __s: AbstractSet[_S]) -> MutableSet[Union[_T, _S]]:
+ if not collections._set_binops_check_strict(self, __s):
+ return NotImplemented
return self.union(__s)
def difference(self, *s: Iterable[Any]) -> MutableSet[_T]:
return set(self).difference(*s)
def __sub__(self, s: AbstractSet[Any]) -> MutableSet[_T]:
+ if not collections._set_binops_check_strict(self, s):
+ return NotImplemented
return self.difference(s)
def difference_update(self, *s: Iterable[Any]) -> None:
def __isub__(self, s: AbstractSet[Any]) -> Self:
if not collections._set_binops_check_strict(self, s):
- raise NotImplementedError()
+ return NotImplemented
for value in s:
self.discard(value)
return self
return set(self).intersection(*s)
def __and__(self, s: AbstractSet[Any]) -> MutableSet[_T]:
+ if not collections._set_binops_check_strict(self, s):
+ return NotImplemented
return self.intersection(s)
def intersection_update(self, *s: Iterable[Any]) -> None:
def __iand__(self, s: AbstractSet[Any]) -> Self:
if not collections._set_binops_check_strict(self, s):
- raise NotImplementedError()
+ return NotImplemented
want = self.intersection(s)
have: Set[_T] = set(self)
return set(self).symmetric_difference(__s)
def __xor__(self, s: AbstractSet[_S]) -> MutableSet[Union[_T, _S]]:
+ if not collections._set_binops_check_strict(self, s):
+ return NotImplemented
return self.symmetric_difference(s)
def symmetric_difference_update(self, other: Iterable[Any]) -> None:
def __ixor__(self, other: AbstractSet[_S]) -> MutableSet[Union[_T, _S]]: # type: ignore # noqa: E501
if not collections._set_binops_check_strict(self, other):
- raise NotImplementedError()
+ return NotImplemented
self.symmetric_difference_update(other)
return self
return isinstance(obj, _set_binop_bases + (self.__class__,))
-def _set_binops_check_loose(self: Any, obj: Any) -> bool:
- """Allow anything set-like to participate in set binops."""
- return (
- isinstance(obj, _set_binop_bases + (self.__class__,))
- or util.duck_type_collection(obj) == set
- )
-
-
def _set_decorators() -> Dict[str, Callable[[_FN], _FN]]:
"""Tailored instrumentation wrappers for any set-like class."""
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import AssertsCompiledSQL
from sqlalchemy.testing import eq_
+from sqlalchemy.testing import expect_raises
from sqlalchemy.testing import expect_warnings
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
assert_raises(TypeError, set, [p1.children])
+ def test_special_binops_checks(self):
+ """test for #11349"""
+
+ Parent = self.classes.Parent
+
+ p1 = Parent("P1")
+ p1.children = ["a", "b", "c"]
+ control = {"a", "b", "c"}
+
+ with expect_raises(TypeError):
+ control | ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children | ["c", "d"]
+
+ with expect_raises(TypeError):
+ control |= ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children |= ["c", "d"]
+
+ with expect_raises(TypeError):
+ control & ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children & ["c", "d"]
+
+ with expect_raises(TypeError):
+ control &= ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children &= ["c", "d"]
+
+ with expect_raises(TypeError):
+ control ^ ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children ^ ["c", "d"]
+
+ with expect_raises(TypeError):
+ control ^= ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children ^= ["c", "d"]
+
+ with expect_raises(TypeError):
+ control - ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children - ["c", "d"]
+
+ with expect_raises(TypeError):
+ control -= ["c", "d"]
+
+ with expect_raises(TypeError):
+ p1.children -= ["c", "d"]
+
def test_set_comparisons(self):
Parent = self.classes.Parent
from sqlalchemy.testing import assert_raises
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import eq_
+from sqlalchemy.testing import expect_raises
from sqlalchemy.testing import expect_raises_message
from sqlalchemy.testing import expect_warnings
from sqlalchemy.testing import fixtures
control |= values
assert_eq()
- try:
+ with expect_raises(TypeError):
+ control |= [e, creator()]
+
+ with expect_raises(TypeError):
direct |= [e, creator()]
- assert False
- except TypeError:
- assert True
addall(creator(), creator())
direct.clear()
control -= values
assert_eq()
- try:
+ with expect_raises(TypeError):
+ control -= [e, creator()]
+
+ with expect_raises(TypeError):
direct -= [e, creator()]
- assert False
- except TypeError:
- assert True
if hasattr(direct, "intersection_update"):
zap()
control &= values
assert_eq()
- try:
+ with expect_raises(TypeError):
+ control &= [e, creator()]
+
+ with expect_raises(TypeError):
direct &= [e, creator()]
- assert False
- except TypeError:
- assert True
if hasattr(direct, "symmetric_difference_update"):
zap()
control ^= values
assert_eq()
- try:
+ with expect_raises(TypeError):
+ control ^= [e, creator()]
+
+ with expect_raises(TypeError):
direct ^= [e, creator()]
- assert False
- except TypeError:
- assert True
def _test_set_bulk(self, typecallable, creator=None):
if creator is None: