Fixed an import of "logging" in test_execute which was not
working on some linux platforms.
+ .. change::
+ :tags: bug, orm
+ :tickets: 2674
+
+ Improved the error message emitted when a "backref loop" is detected,
+ that is when an attribute event triggers a bidirectional
+ assignment between two other attributes with no end.
+ This condition can occur not just when an object of the wrong
+ type is assigned, but also when an attribute is mis-configured
+ to backref into an existing backref pair.
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 2674
+
+ A warning is emitted when a MapperProperty is assigned to a mapper
+ that replaces an existing property, if the properties in question
+ aren't plain column-based properties. Replacement of relationship
+ properties is rarely (ever?) what is intended and usually refers to a
+ mapper mis-configuration. This will also warn if a backref configures
+ itself on top of an existing one in an inheritance relationship
+ (which is an error in 0.8).
+
.. changelog::
:version: 0.7.10
:released: Thu Feb 7 2013
* :ref:`metadata_create_drop_tables`
+ .. change::
+ :tags: bug, orm
+ :tickets: 2674
+
+ Improved checking for an existing backref name conflict during
+ mapper configuration; will now test for name conflicts on
+ superclasses and subclasses, in addition to the current mapper,
+ as these conflicts break things just as much. This is new for
+ 0.8, but see below for a warning that will also be triggered
+ in 0.7.11.
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 2674
+
+ Improved the error message emitted when a "backref loop" is detected,
+ that is when an attribute event triggers a bidirectional
+ assignment between two other attributes with no end.
+ This condition can occur not just when an object of the wrong
+ type is assigned, but also when an attribute is mis-configured
+ to backref into an existing backref pair. Also in 0.7.11.
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 2674
+
+ A warning is emitted when a MapperProperty is assigned to a mapper
+ that replaces an existing property, if the properties in question
+ aren't plain column-based properties. Replacement of relationship
+ properties is rarely (ever?) what is intended and usually refers to a
+ mapper mis-configuration. Also in 0.7.11.
+
.. change::
:tags: feature, orm
self.expire_missing = expire_missing
+ def __str__(self):
+ return "%s.%s" % (self.class_.__name__, self.key)
+
def _get_active_history(self):
"""Backwards compat for impl.active_history"""
parent_token = attribute.impl.parent_token
- def _acceptable_key_err(child_state, initiator):
+ def _acceptable_key_err(child_state, initiator, child_impl):
raise ValueError(
- "Object %s not associated with attribute of "
- "type %s" % (orm_util.state_str(child_state),
- manager_of_class(initiator.class_)[initiator.key]))
+ "Bidirectional attribute conflict detected: "
+ 'Passing object %s to attribute "%s" '
+ 'triggers a modify event on attribute "%s" '
+ 'via the backref "%s".' % (
+ orm_util.state_str(child_state),
+ initiator.parent_token,
+ child_impl.parent_token,
+ attribute.impl.parent_token
+ )
+ )
def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
if oldchild is child:
child_impl = child_state.manager[key].impl
if initiator.parent_token is not parent_token and \
initiator.parent_token is not child_impl.parent_token:
- _acceptable_key_err(state, initiator)
+ _acceptable_key_err(state, initiator, child_impl)
child_impl.append(
child_state,
child_dict,
child_state, child_dict = instance_state(child), \
instance_dict(child)
child_impl = child_state.manager[key].impl
+
+ print initiator.parent_token, parent_token, child_impl.parent_token
if initiator.parent_token is not parent_token and \
initiator.parent_token is not child_impl.parent_token:
- _acceptable_key_err(state, initiator)
+ _acceptable_key_err(state, initiator, child_impl)
child_impl.append(
child_state,
child_dict,
# initialized; check for 'readonly'
if hasattr(self, '_readonly_props') and \
(not hasattr(col, 'table') or
- col.table not in self._cols_by_table):
+ col.table not in self._cols_by_table):
self._readonly_props.add(prop)
else:
"%r for column %r" % (syn, key, key, syn)
)
+ if key in self._props and \
+ not isinstance(prop, properties.ColumnProperty) and \
+ not isinstance(self._props[key], properties.ColumnProperty):
+ util.warn("Property %s on %s being replaced with new "
+ "property %s; the old property will be discarded" % (
+ self._props[key],
+ self,
+ prop,
+ ))
+
self._props[key] = prop
if not self.non_primary:
adapt_source=adapt_source)
def __str__(self):
- return str(self.parent.class_.__name__) + "." + self.key
+ if self.parent:
+ return str(self.parent.class_.__name__) + "." + self.key
+ else:
+ return "." + self.key
def merge(self,
session,
else:
backref_key, kwargs = self.backref
mapper = self.mapper.primary_mapper()
- if mapper.has_property(backref_key):
- raise sa_exc.ArgumentError("Error creating backref "
- "'%s' on relationship '%s': property of that "
- "name exists on mapper '%s'" % (backref_key,
- self, mapper))
+
+ check = set(mapper.iterate_to_root()).\
+ union(mapper.self_and_descendants)
+ for m in check:
+ if m.has_property(backref_key):
+ raise sa_exc.ArgumentError("Error creating backref "
+ "'%s' on relationship '%s': property of that "
+ "name exists on mapper '%s'" % (backref_key,
+ self, m))
# determine primaryjoin/secondaryjoin for the
# backref. Use the one we had, so that
b1 = B()
assert_raises_message(
ValueError,
- "Object <B at .*> not associated with attribute of type C.a",
+ 'Bidirectional attribute conflict detected: '
+ 'Passing object <B at .*> to attribute "C.a" '
+ 'triggers a modify event on attribute "C.b" '
+ 'via the backref "B.c".',
setattr, c1, 'a', b1
)
b1 = B()
assert_raises_message(
ValueError,
- "Object <B at .*> not associated with attribute of type C.a",
+ 'Bidirectional attribute conflict detected: '
+ 'Passing object <B at .*> to attribute "C.a" '
+ 'triggers a modify event on attribute "C.b" '
+ 'via the backref "B.c".',
c1.a.append, b1
)
+
def _scalar_fixture(self):
class A(object):
pass
return A, B, C
+ def _broken_collection_fixture(self):
+ class A(object):
+ pass
+ class B(object):
+ pass
+ instrumentation.register_class(A)
+ instrumentation.register_class(B)
+
+ attributes.register_attribute(A, 'b', backref='a1', useobject=True)
+ attributes.register_attribute(B, 'a1', backref='b', useobject=True,
+ uselist=True)
+
+ attributes.register_attribute(B, 'a2', backref='b', useobject=True,
+ uselist=True)
+
+ return A, B
+
+ def test_broken_collection_assertion(self):
+ A, B = self._broken_collection_fixture()
+ b1 = B()
+ a1 = A()
+ assert_raises_message(
+ ValueError,
+ 'Bidirectional attribute conflict detected: '
+ 'Passing object <A at .*> to attribute "B.a2" '
+ 'triggers a modify event on attribute "B.a1" '
+ 'via the backref "A.b".',
+ b1.a2.append, a1
+ )
+
class PendingBackrefTest(fixtures.ORMTest):
def setup(self):
global Post, Blog, called, lazy_load
b = Table('b', meta, Column('id', Integer, primary_key=True),
Column('a_id', Integer, ForeignKey('a.id')))
- class A(object):pass
- class B(object):pass
+ class A(object):
+ pass
+ class B(object):
+ pass
mapper(A, a, properties={
'b':relationship(B, backref='a')
configure_mappers
)
+ def test_conflicting_backref_subclass(self):
+ meta = MetaData()
+
+ a = Table('a', meta, Column('id', Integer, primary_key=True))
+ b = Table('b', meta, Column('id', Integer, primary_key=True),
+ Column('a_id', Integer, ForeignKey('a.id')))
+
+ class A(object):
+ pass
+ class B(object):
+ pass
+ class C(B):
+ pass
+
+ mapper(A, a, properties={
+ 'b': relationship(B, backref='a'),
+ 'c': relationship(C, backref='a')
+ })
+ mapper(B, b)
+ mapper(C, None, inherits=B)
+
+ assert_raises_message(
+ sa_exc.ArgumentError,
+ "Error creating backref",
+ configure_mappers
+ )
assert hasattr(User, 'addresses')
assert "addresses" in [p.key for p in m1._polymorphic_properties]
- def test_replace_property(self):
+ def test_replace_col_prop_w_syn(self):
users, User = self.tables.users, self.classes.User
m = mapper(User, users)
u.name = 'jacko'
assert m._columntoproperty[users.c.name] is m.get_property('_name')
+ def test_replace_rel_prop_with_rel_warns(self):
+ users, User = self.tables.users, self.classes.User
+ addresses, Address = self.tables.addresses, self.classes.Address
+
+ m = mapper(User, users, properties={
+ "addresses": relationship(Address)
+ })
+ mapper(Address, addresses)
+
+ assert_raises_message(
+ sa.exc.SAWarning,
+ "Property User.addresses on Mapper|User|users being replaced "
+ "with new property User.addresses; the old property will "
+ "be discarded",
+ m.add_property,
+ "addresses", relationship(Address)
+ )
+
def test_add_column_prop_deannotate(self):
User, users = self.classes.User, self.tables.users
Address, addresses = self.classes.Address, self.tables.addresses