From: Mike Bayer Date: Thu, 23 Dec 2010 20:27:47 +0000 (-0500) Subject: - restore declarative support for "composite" X-Git-Tag: rel_0_7b1~119 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6cd730541f4e61fb8262ac50752c21cf1e7262ac;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - restore declarative support for "composite" - add an example of mutable scalars with events --- diff --git a/doc/build/orm/session.rst b/doc/build/orm/session.rst index 061f16f7ed..d08ce5f5a9 100644 --- a/doc/build/orm/session.rst +++ b/doc/build/orm/session.rst @@ -1472,6 +1472,8 @@ those described in :ref:`events_orm_toplevel`. .. autofunction:: init_collection +.. autofunction:: flag_modified + .. function:: instance_state Return the :class:`InstanceState` for a given object. diff --git a/examples/mutable_events/__init__.py b/examples/mutable_events/__init__.py new file mode 100644 index 0000000000..9802a109b5 --- /dev/null +++ b/examples/mutable_events/__init__.py @@ -0,0 +1,31 @@ +""" +Illustrates how to build and use "mutable" types, such as dictionaries and +user-defined classes, as scalar attributes which detect in-place changes. + +The example is based around the usage of the event model introduced in +:ref:`event_toplevel`, along with the :func:`attributes.flag_modified` function +which establishes the "dirty" flag on a particular mapped attribute. These +functions are encapsulated in a mixin called ``TrackMutationsMixin``. +Subclassing ``dict`` to provide "mutation tracking" looks like:: + + class MutationDict(TrackMutationsMixin, dict): + def __init__(self, other): + self.update(other) + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + self.on_change() + + def __delitem__(self, key): + dict.__delitem__(self, key) + self.on_change() + + Base = declarative_base() + class Foo(Base): + __tablename__ = 'foo' + id = Column(Integer, primary_key=True) + data = Column(JSONEncodedDict) + + MutationDict.listen(Foo.data) + +""" \ No newline at end of file diff --git a/examples/mutable_events/scalars.py b/examples/mutable_events/scalars.py new file mode 100644 index 0000000000..4d434fd54a --- /dev/null +++ b/examples/mutable_events/scalars.py @@ -0,0 +1,121 @@ +from sqlalchemy.orm.attributes import flag_modified +from sqlalchemy import event +import weakref + +class TrackMutationsMixin(object): + """Mixin that defines transparent propagation of change + events to a parent object. + + """ + _key = None + _parent = None + + def _set_parent(self, parent, key): + self._parent = weakref.ref(parent) + self._key = key + + def _remove_parent(self): + del self._parent + + def on_change(self, key=None): + """Subclasses should call this method whenever change events occur.""" + + if key is None: + key = self._key + if self._parent: + p = self._parent() + if p: + flag_modified(p, self._key) + + @classmethod + def listen(cls, attribute): + """Establish this type as a mutation listener for the given class and + attribute name. + + """ + key = attribute.key + parent_cls = attribute.class_ + + def on_load(state): + val = state.dict.get(key, None) + if val is not None: + val = cls(val) + state.dict[key] = val + val._set_parent(state.obj(), key) + + def on_set(target, value, oldvalue, initiator): + if not isinstance(value, cls): + value = cls(value) + value._set_parent(target.obj(), key) + if isinstance(oldvalue, cls): + oldvalue._remove_parent() + return value + + event.listen(parent_cls, 'on_load', on_load, raw=True) + event.listen(parent_cls, 'on_refresh', on_load, raw=True) + event.listen(attribute, 'on_set', on_set, raw=True, retval=True) + +if __name__ == '__main__': + from sqlalchemy import Column, Integer, VARCHAR, create_engine + from sqlalchemy.orm import Session + from sqlalchemy.types import TypeDecorator + from sqlalchemy.ext.declarative import declarative_base + import simplejson + + class JSONEncodedDict(TypeDecorator): + """Represents an immutable structure as a json-encoded string. + + Usage:: + + JSONEncodedDict(255) + + """ + + impl = VARCHAR + + def process_bind_param(self, value, dialect): + if value is not None: + value = simplejson.dumps(value, use_decimal=True) + + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = simplejson.loads(value, use_decimal=True) + return value + + class MutationDict(TrackMutationsMixin, dict): + def __init__(self, other): + self.update(other) + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + self.on_change() + + def __delitem__(self, key): + dict.__delitem__(self, key) + self.on_change() + + Base = declarative_base() + class Foo(Base): + __tablename__ = 'foo' + id = Column(Integer, primary_key=True) + data = Column(JSONEncodedDict) + + MutationDict.listen(Foo.data) + + e = create_engine('sqlite://', echo=True) + + Base.metadata.create_all(e) + + sess = Session(e) + f1 = Foo(data={'a':'b'}) + sess.add(f1) + sess.commit() + + f1.data['a'] = 'c' + sess.commit() + + assert f1.data == {'a':'c'} + + \ No newline at end of file diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 40abfbb292..5d3c7d29d0 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -928,7 +928,7 @@ from sqlalchemy.schema import Table, Column, MetaData, _get_table_key from sqlalchemy.orm import synonym as _orm_synonym, mapper,\ comparable_property, class_mapper from sqlalchemy.orm.interfaces import MapperProperty -from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty +from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty, CompositeProperty from sqlalchemy.orm.util import _is_mapped_class from sqlalchemy import util, exceptions from sqlalchemy.sql import util as sql_util, expression @@ -1083,7 +1083,7 @@ def _as_declarative(cls, classname, dict_): # extract columns from the class dict cols = [] for key, c in our_stuff.iteritems(): - if isinstance(c, ColumnProperty): + if isinstance(c, (ColumnProperty, CompositeProperty)): for col in c.columns: if isinstance(col, Column) and col.table is None: _undefer_column_name(key, col) diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 40a69d96ab..ac6a498c72 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -149,7 +149,8 @@ def create_proxied_attribute(descriptor): class Proxy(QueryableAttribute): """A combination of InsturmentedAttribute and a regular descriptor.""" - def __init__(self, key, descriptor, comparator, adapter=None): + def __init__(self, class_, key, descriptor, comparator, adapter=None): + self.class_ = class_ self.key = key self.descriptor = descriptor self._comparator = comparator diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 3344ed888d..a1498bfd49 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -75,6 +75,7 @@ class DescriptorProperty(MapperProperty): proxy_attr = attributes.\ create_proxied_attribute(self.descriptor or descriptor)\ ( + self.parent.class_, self.key, self.descriptor or descriptor, lambda: self._comparator_factory(mapper) @@ -94,6 +95,7 @@ class CompositeProperty(DescriptorProperty): self.active_history = kwargs.get('active_history', False) self.deferred = kwargs.get('deferred', False) self.group = kwargs.get('group', None) + util.set_creation_order(self) def fget(instance): # this could be optimized to store the value in __dict__, diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 6d1e8f7137..f511b0e403 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -136,7 +136,15 @@ class InstanceEvents(event.Events): initialized, depending on what's present in the result rows. """ - + + def on_refresh(self, target): + """Receive an object instance after one or more attributes have + been refreshed. + + This hook is called after expired attributes have been reloaded. + + """ + def on_resurrect(self, target): """Receive an object instance as it is 'resurrected' from garbage collection, which occurs when a "dirty" state falls diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 91d512ad0d..f2bc045700 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2291,6 +2291,8 @@ class Mapper(object): if loaded_instance: state.manager.dispatch.on_load(state) + elif isnew: + state.manager.dispatch.on_refresh(state) if result is not None: if append_result: diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index f06173d322..d400368f1c 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -2322,6 +2322,7 @@ class MapperEventsTest(_fixtures.FixtureTest): 'on_append_result', 'on_populate_instance', 'on_load', + 'on_refresh', 'on_before_insert', 'on_after_insert', 'on_before_update', @@ -2352,6 +2353,7 @@ class MapperEventsTest(_fixtures.FixtureTest): eq_(canary, ['on_init', 'on_before_insert', 'on_after_insert', 'on_translate_row', 'on_populate_instance', + 'on_refresh', 'on_append_result', 'on_translate_row', 'on_create_instance', 'on_populate_instance', 'on_load', 'on_append_result', 'on_before_update', 'on_after_update', 'on_before_delete', 'on_after_delete']) @@ -2380,14 +2382,14 @@ class MapperEventsTest(_fixtures.FixtureTest): sess.delete(am) sess.flush() eq_(canary1, ['on_init', 'on_before_insert', 'on_after_insert', - 'on_translate_row', 'on_populate_instance', + 'on_translate_row', 'on_populate_instance','on_refresh', 'on_append_result', 'on_translate_row', 'on_create_instance' , 'on_populate_instance', 'on_load', 'on_append_result', 'on_before_update', 'on_after_update', 'on_before_delete', 'on_after_delete']) eq_(canary2, []) eq_(canary3, ['on_init', 'on_before_insert', 'on_after_insert', - 'on_translate_row', 'on_populate_instance', + 'on_translate_row', 'on_populate_instance','on_refresh', 'on_append_result', 'on_translate_row', 'on_create_instance' , 'on_populate_instance', 'on_load', 'on_append_result', 'on_before_update', 'on_after_update', 'on_before_delete',