- The 3-tuple of iterables returned by attributes.get_history()
may now be a mix of lists and tuples. (Previously members
were always lists.)
+
+ - Fixed bug whereby changing a primary key attribute on an
+ entity where the attribute's previous value had been expired
+ would produce an error upon flush(). [ticket:1151]
- Session.delete() adds the given object to the session if
not already present. This was a regression bug from 0.4
to it via this attribute.
extension
- an AttributeExtension object which will receive
- set/delete/append/remove/etc. events.
+ a single or list of AttributeExtension object(s) which will
+ receive set/delete/append/remove/etc. events.
compare_function
a function that compares two values which are normally
assignable to this attribute.
+
+ active_history
+ indicates that get_history() should always return the "old" value,
+ even if it means executing a lazy callable upon attribute change.
+ This flag is set to True if any extensions are present.
"""
self.callable_ = callable_
self.class_manager = class_manager
self.trackparent = trackparent
- self.active_history = active_history
if compare_function is None:
self.is_equal = operator.eq
else:
self.is_equal = compare_function
self.extensions = util.to_list(extension or [])
+ self.active_history = active_history or bool(self.extensions)
def hasparent(self, state, optimistic=False):
"""Return the boolean value of a `hasparent` flag attached to the given item.
def delete(self, state):
# TODO: catch key errors, convert to attributeerror?
- if self.active_history or self.extensions:
+ if self.active_history:
old = self.get(state)
else:
old = state.dict.get(self.key, NO_VALUE)
if initiator is self:
return
- if self.active_history or self.extensions:
+ if self.active_history:
old = self.get(state)
else:
old = state.dict.get(self.key, NO_VALUE)
merged_state._run_on_load(merged)
return merged
+ @classmethod
def identity_key(cls, *args, **kwargs):
return mapperutil.identity_key(*args, **kwargs)
- identity_key = classmethod(identity_key)
+ @classmethod
def object_session(cls, instance):
"""Return the ``Session`` to which an object belongs."""
return object_session(instance)
- object_session = classmethod(object_session)
def _validate_persistent(self, state):
if not self.identity_map.contains_state(state):
attributes.register_attribute(Foo, 'bar', uselist=False, callable_=lazyload, useobject=False)
lazy_load = "hi"
- # with scalar non-object, the lazy callable is only executed on gets, not history
+ # with scalar non-object and active_history=False, the lazy callable is only executed on gets, not history
# operations
f = Foo()
assert f.bar is None
eq_(attributes.get_history(attributes.instance_state(f), 'bar'), ([None], (), ["hi"]))
+ def test_scalar_via_lazyload_with_active(self):
+ class Foo(_base.BasicEntity):
+ pass
+
+ lazy_load = None
+ def lazyload(instance):
+ def load():
+ return lazy_load
+ return load
+
+ attributes.register_class(Foo)
+ attributes.register_attribute(Foo, 'bar', uselist=False, callable_=lazyload, useobject=False, active_history=True)
+ lazy_load = "hi"
+
+ # active_history=True means the lazy callable is executed on set as well as get,
+ # causing the old value to appear in the history
+
+ f = Foo()
+ eq_(f.bar, "hi")
+ eq_(attributes.get_history(attributes.instance_state(f), 'bar'), ((), ["hi"], ()))
+
+ f = Foo()
+ f.bar = None
+ eq_(attributes.get_history(attributes.instance_state(f), 'bar'), ([None], (), ['hi']))
+
+ f = Foo()
+ f.bar = "there"
+ eq_(attributes.get_history(attributes.instance_state(f), 'bar'), (["there"], (), ['hi']))
+ f.bar = "hi"
+ eq_(attributes.get_history(attributes.instance_state(f), 'bar'), ((), ["hi"], ()))
+
+ f = Foo()
+ eq_(f.bar, "hi")
+ del f.bar
+ eq_(attributes.get_history(attributes.instance_state(f), 'bar'), ((), (), ["hi"]))
+ assert f.bar is None
+ eq_(attributes.get_history(attributes.instance_state(f), 'bar'), ([None], (), ["hi"]))
+
def test_scalar_object_via_lazyload(self):
class Foo(_base.BasicEntity):
pass
self.assertEquals(User(username='ed', fullname='jack'), u1)
@testing.resolve_artifact_names
- def test_expiry(self):
+ def test_load_after_expire(self):
mapper(User, users)
sess = create_session()
assert sess.query(User).get('jack') is None
assert sess.query(User).get('ed').fullname == 'jack'
+ @testing.resolve_artifact_names
+ def test_flush_new_pk_after_expire(self):
+ mapper(User, users)
+ sess = create_session()
+ u1 = User(username='jack', fullname='jack')
+
+ sess.add(u1)
+ sess.flush()
+ assert sess.query(User).get('jack') is u1
+
+ sess.expire(u1)
+ u1.username = 'ed'
+ sess.flush()
+ sess.clear()
+ assert sess.query(User).get('ed').fullname == 'jack'
+
+
@testing.fails_on('mysql')
@testing.fails_on('sqlite')
def test_onetomany_passive(self):