]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- restore declarative support for "composite"
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 23 Dec 2010 20:27:47 +0000 (15:27 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 23 Dec 2010 20:27:47 +0000 (15:27 -0500)
- add an example of mutable scalars with events

doc/build/orm/session.rst
examples/mutable_events/__init__.py [new file with mode: 0644]
examples/mutable_events/scalars.py [new file with mode: 0644]
lib/sqlalchemy/ext/declarative.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/descriptor_props.py
lib/sqlalchemy/orm/events.py
lib/sqlalchemy/orm/mapper.py
test/orm/test_mapper.py

index 061f16f7ed3df78dbf66098ac2b686ecc3180271..d08ce5f5a970bd852663e9cb50da7b84ace5068c 100644 (file)
@@ -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 (file)
index 0000000..9802a10
--- /dev/null
@@ -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 (file)
index 0000000..4d434fd
--- /dev/null
@@ -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
index 40abfbb29235ada7a3e3c4c05e6dd9579554490b..5d3c7d29d0d1d7f14358fc2a9f972156f28b5868 100755 (executable)
@@ -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)
index 40a69d96ab1d2a117bfb194b492ac40676c04e51..ac6a498c7284a889dab730b799ad6e1134aa7f48 100644 (file)
@@ -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
index 3344ed888d735d93188d6abebd47ed6bfddf303a..a1498bfd493a7dc61f8d97e6e98e8d66af5dc2cc 100644 (file)
@@ -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__,
index 6d1e8f713742030dabd21e48019937a585d84151..f511b0e4031f2c7f056f801dfec298efb4787915 100644 (file)
@@ -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
index 91d512ad0d0ae9aca13c4fb8388c08bec5a35920..f2bc045700722e6bd0457e3c517f5caa4c3e8005 100644 (file)
@@ -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:
index f06173d32223117bcae3135324de1fc0d331ced6..d400368f1c692d10385852b4fb38525919aa2707 100644 (file)
@@ -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',