From 586e5be1bb2bedb69802312925ca84f81199f2ff Mon Sep 17 00:00:00 2001 From: Jason Kirtland Date: Mon, 5 May 2008 21:33:29 +0000 Subject: [PATCH] Adjusted inplace-binops on set-based collections and association proxies to more closely follow builtin (2.4+) set semantics. Formerly any set duck-type was accepted, now only types or subtypes of set, frozenset or the collection type itself are accepted. --- CHANGES | 11 ++++++++++ lib/sqlalchemy/ext/associationproxy.py | 18 +++++++++------- lib/sqlalchemy/orm/collections.py | 30 +++++++++++++++++++++----- test/ext/associationproxy.py | 1 + test/orm/collection.py | 10 +++++++++ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/CHANGES b/CHANGES index 9fe24ca815..f69cecf5a9 100644 --- a/CHANGES +++ b/CHANGES @@ -60,6 +60,11 @@ CHANGES - Fixed duplicate append event emission on repeated instrumented set.add() operations. + - set-based collections |=, -=, ^= and &= are stricter about + their operands and only operate on sets, frozensets or + subclasses of the collection type. Previously, they would + accept any duck-typed set. + - declarative extension - Joined table inheritance mappers use a slightly relaxed function to create the "inherit condition" to the parent @@ -94,6 +99,12 @@ CHANGES the databsae state cleanup (eg. issuing a rollback()) when connections are returned to the pool. +-ext + - set-based association proxies |=, -=, ^= and &= are + stricter about their operands and only operate on sets, + frozensets or other association proxies. Previously, they + would accept any duck-typed set. + - mssql - Added "odbc_autotranslate" parameter to engine / dburi parameters. Any given string will be passed through to the diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index bcbfb47800..d878f7b9b0 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -6,10 +6,12 @@ transparent proxied access to the endpoint of an association object. See the example ``examples/association/proxied_association.py``. """ -import weakref, itertools -import sqlalchemy.exceptions as exceptions -import sqlalchemy.orm as orm -import sqlalchemy.util as util +import itertools +import weakref +from sqlalchemy import exceptions +from sqlalchemy import orm +from sqlalchemy import util +from sqlalchemy.orm import collections def association_proxy(targetcollection, attr, **kw): @@ -702,7 +704,7 @@ class _AssociationSet(object): self.add(value) def __ior__(self, other): - if util.duck_type_collection(other) is not util.Set: + if not collections._set_binops_check_strict(self, other): return NotImplemented for value in other: self.add(value) @@ -726,7 +728,7 @@ class _AssociationSet(object): self.discard(value) def __isub__(self, other): - if util.duck_type_collection(other) is not util.Set: + if not collections._set_binops_check_strict(self, other): return NotImplemented for value in other: self.discard(value) @@ -748,7 +750,7 @@ class _AssociationSet(object): self.add(value) def __iand__(self, other): - if util.duck_type_collection(other) is not util.Set: + if not collections._set_binops_check_strict(self, other): return NotImplemented want, have = self.intersection(other), util.Set(self) @@ -776,7 +778,7 @@ class _AssociationSet(object): self.add(value) def __ixor__(self, other): - if util.duck_type_collection(other) is not util.Set: + if not collections._set_binops_check_strict(self, other): return NotImplemented want, have = self.symmetric_difference(other), util.Set(self) diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index 8067a030f7..c8fc2f189a 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -95,7 +95,11 @@ The owning object and InstrumentedCollectionAttribute are also reachable through the adapter, allowing for some very sophisticated behavior. """ -import copy, inspect, sys, weakref +import copy +import inspect +import sets +import sys +import weakref from sqlalchemy import exceptions, schema, util as sautil from sqlalchemy.util import attrgetter, Set @@ -1136,6 +1140,22 @@ def _dict_decorators(): l.pop('Unspecified') return l + +try: + _set_binop_bases = (set, frozenset, sets.BaseSet) +except NameError: + _set_binop_bases = (sets.BaseSet,) + +def _set_binops_check_strict(self, obj): + """Allow only set, frozenset and self.__class__-derived objects in binops.""" + return isinstance(obj, _set_binop_bases + (self.__class__,)) + +def _set_binops_check_loose(self, obj): + """Allow anything set-like to participate in set binops.""" + return (isinstance(obj, _set_binop_bases + (self.__class__,)) or + sautil.duck_type_collection(obj) == sautil.Set) + + def _set_decorators(): """Hand-turned instrumentation wrappers that can decorate any set-like sequence class.""" @@ -1208,7 +1228,7 @@ def _set_decorators(): def __ior__(fn): def __ior__(self, value): - if sautil.duck_type_collection(value) is not Set: + if not _set_binops_check_strict(self, value): return NotImplemented for item in value: self.add(item) @@ -1225,7 +1245,7 @@ def _set_decorators(): def __isub__(fn): def __isub__(self, value): - if sautil.duck_type_collection(value) is not Set: + if not _set_binops_check_strict(self, value): return NotImplemented for item in value: self.discard(item) @@ -1247,7 +1267,7 @@ def _set_decorators(): def __iand__(fn): def __iand__(self, other): - if sautil.duck_type_collection(other) is not Set: + if not _set_binops_check_strict(self, other): return NotImplemented want, have = self.intersection(other), Set(self) remove, add = have - want, want - have @@ -1274,7 +1294,7 @@ def _set_decorators(): def __ixor__(fn): def __ixor__(self, other): - if sautil.duck_type_collection(other) is not Set: + if not _set_binops_check_strict(self, other): return NotImplemented want, have = self.symmetric_difference(other), Set(self) remove, add = have - want, want - have diff --git a/test/ext/associationproxy.py b/test/ext/associationproxy.py index e9540fa2db..8837b4d04c 100644 --- a/test/ext/associationproxy.py +++ b/test/ext/associationproxy.py @@ -534,6 +534,7 @@ class SetTest(_CollectionOperations): for other in (set(['a','b','c']), set(['a','b','c','d']), set(['a']), set(['a','b']), set(['c','d']), set(['e', 'f', 'g']), + frozenset(['e', 'f', 'g']), set()): p = Parent('p') p.children = base[:] diff --git a/test/orm/collection.py b/test/orm/collection.py index 708ba9c744..711dc730ba 100644 --- a/test/orm/collection.py +++ b/test/orm/collection.py @@ -484,6 +484,11 @@ class CollectionsTest(TestBase): control |= values assert_eq() + values = frozenset([e, creator()]) + obj.attr |= values + control |= values + assert_eq() + try: direct |= [e, creator()] assert False @@ -529,6 +534,11 @@ class CollectionsTest(TestBase): control -= values assert_eq() + values = frozenset([creator()]) + obj.attr -= values + control -= values + assert_eq() + try: direct -= [e, creator()] assert False -- 2.47.3