]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- restore mapper.get_property() to use the _props dict. at the moment
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 29 Dec 2010 03:23:13 +0000 (22:23 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 29 Dec 2010 03:23:13 +0000 (22:23 -0500)
synonyms for relationships might just be taken out altogether, since they aren't
documented and are of little use.   a plain proxying descriptor, combined with
attribute-based usage with Query (as opposted to naming it by string)
 can do the same thing more simply.
- add event support to composites, change the model around so that the composite
is generated at the point of load.
- add a recipe for tracking mutations on composites.  will probably make both
of these mutations examples into extensions since they're intricate, should
have a lot of test coverage, and what they need to do is fairly straightforward.
Will use metaclasses so that no extra userland step is needed beyond usage
of the type.

examples/mutable_events/composite.py [new file with mode: 0644]
examples/mutable_events/scalars.py
lib/sqlalchemy/orm/descriptor_props.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/mapper.py
test/orm/test_composites.py
test/orm/test_mapper.py
test/orm/test_query.py

diff --git a/examples/mutable_events/composite.py b/examples/mutable_events/composite.py
new file mode 100644 (file)
index 0000000..f46f28e
--- /dev/null
@@ -0,0 +1,139 @@
+# this example is probably moving to be an extension.
+
+from sqlalchemy import event
+from sqlalchemy.orm import mapper, composite, object_mapper
+
+from sqlalchemy.util import memoized_property
+import weakref
+
+class _CompositeMutationsMixinMeta(type):
+    def __init__(cls, classname, bases, dict_):
+        cls._setup_listeners()
+        return type.__init__(cls, classname, bases, dict_)
+
+class CompositeMutationsMixin(object):
+    """Mixin that defines transparent propagation of change
+    events to a parent object.
+
+    This class might be moved to be a SQLA extension
+    due to its complexity and potential for widespread use.
+    
+    """
+    __metaclass__ = _CompositeMutationsMixinMeta
+
+    @memoized_property
+    def _parents(self):
+        """Dictionary of parent object->attribute name on the parent."""
+        
+        return weakref.WeakKeyDictionary()
+
+    def __setattr__(self, key, value):
+        object.__setattr__(self, key, value)
+        self.on_change()
+    
+    def on_change(self):
+        """Subclasses should call this method whenever change events occur."""
+        
+        for parent, key in self._parents.items():
+            
+            prop = object_mapper(parent).get_property(key)
+            for value, attr_name in zip(self.__composite_values__(), prop._attribute_keys):
+                setattr(parent, attr_name, value)
+    
+    @classmethod
+    def _listen_on_attribute(cls, attribute):
+        """Establish this type as a mutation listener for the given 
+        mapped descriptor.
+        
+        """
+        key = attribute.key
+        parent_cls = attribute.class_
+        
+        def on_load(state):
+            """Listen for objects loaded or refreshed.   
+            
+            Wrap the target data member's value with 
+            ``TrackMutationsMixin``.
+            
+            """
+            
+            val = state.dict.get(key, None)
+            if val is not None:
+                val._parents[state.obj()] = key
+
+        def on_set(target, value, oldvalue, initiator):
+            """Listen for set/replace events on the target
+            data member.
+            
+            Establish a weak reference to the parent object
+            on the incoming value, remove it for the one 
+            outgoing.
+            
+            """
+            
+            value._parents[target.obj()] = key
+            if isinstance(oldvalue, cls):
+                oldvalue._parents.pop(state.obj(), None)
+            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)
+    
+    @classmethod
+    def _setup_listeners(cls):
+        """Associate this wrapper with all future mapped compoistes
+        of the given type.
+        
+        This is a convenience method that calls ``associate_with_attribute`` automatically.
+        
+        """
+        
+        def listen_for_type(mapper, class_):
+            for prop in mapper.iterate_properties:
+                if hasattr(prop, 'composite_class') and issubclass(prop.composite_class, cls):
+                    cls._listen_on_attribute(getattr(class_, prop.key))
+                    
+        event.listen(mapper, 'on_mapper_configured', listen_for_type)
+
+        
+if __name__ == '__main__':
+    from sqlalchemy import Column, Integer, create_engine
+    from sqlalchemy.orm import Session
+    from sqlalchemy.ext.declarative import declarative_base
+
+    class Point(CompositeMutationsMixin):
+        def __init__(self, x, y):
+            self.x = x
+            self.y = y
+        
+        def __composite_values__(self):
+            return self.x, self.y
+            
+        def __eq__(self, other):
+            return isinstance(other, Point) and \
+                other.x == self.x and \
+                other.y == self.y
+    
+    Base = declarative_base()
+    class Foo(Base):
+        __tablename__ = 'foo'
+        id = Column(Integer, primary_key=True)
+        data = composite(Point, Column('x', Integer), Column('y', Integer))
+    
+    e = create_engine('sqlite://', echo=True)
+
+    Base.metadata.create_all(e)
+
+    sess = Session(e)
+    d = Point(3, 4)
+    f1 = Foo(data=d)
+    sess.add(f1)
+    sess.commit()
+
+    f1.data.y = 5
+    sess.commit()
+
+    assert f1.data == Point(3, 5)
+
+        
\ No newline at end of file
index b4d6b350d73d8bc5593c00c5a99a556cb0a8fd02..1c135a95763916a00d54b18a5e582924eb8e8d1e 100644 (file)
@@ -1,3 +1,5 @@
+# this example is probably moving to be an extension.
+
 from sqlalchemy.orm.attributes import flag_modified
 from sqlalchemy import event
 from sqlalchemy.orm import mapper
@@ -8,6 +10,9 @@ class TrackMutationsMixin(object):
     """Mixin that defines transparent propagation of change
     events to a parent object.
     
+    This class might be moved to be a SQLA extension
+    due to its complexity and potential for widespread use.
+    
     """
     @memoized_property
     def _parents(self):
@@ -76,7 +81,7 @@ class TrackMutationsMixin(object):
         def listen_for_type(mapper, class_):
             for prop in mapper.iterate_properties:
                 if hasattr(prop, 'columns') and isinstance(prop.columns[0].type, type_):
-                    cls.listen(getattr(class_, prop.key))
+                    cls.associate_with_attribute(getattr(class_, prop.key))
                     
         event.listen(mapper, 'on_mapper_configured', listen_for_type)
         
@@ -121,7 +126,8 @@ if __name__ == '__main__':
         def __delitem__(self, key):
             dict.__delitem__(self, key)
             self.on_change()
-            
+    
+    # TODO: do the metaclass approach the same as composite
     MutationDict.associate_with_type(JSONEncodedDict)
     
     Base = declarative_base()
index 347f9bce9a9e1b762cc0eca8020c4b98ce239804..5f974e2607ec33b00aa7b2da354d54d5a91dff08 100644 (file)
@@ -66,7 +66,7 @@ class DescriptorProperty(MapperProperty):
                         lambda: self._comparator_factory(mapper),
                         doc=self.doc
                     )
-
+        proxy_attr.property = self
         proxy_attr.impl = _ProxyImpl(self.key)
         mapper.class_manager.instrument_attribute(self.key, proxy_attr)
     
@@ -81,6 +81,10 @@ class CompositeProperty(DescriptorProperty):
         self.group = kwargs.get('group', None)
         util.set_creation_order(self)
         self._create_descriptor()
+
+    def instrument_class(self, mapper):
+        super(CompositeProperty, self).instrument_class(mapper)
+        self._setup_event_handlers()
         
     def do_init(self):
         """Initialization which occurs after the :class:`.CompositeProperty` 
@@ -88,43 +92,55 @@ class CompositeProperty(DescriptorProperty):
         
         """
         self._setup_arguments_on_columns()
-        self._setup_event_handlers()
     
     def _create_descriptor(self):
-        """Create the actual Python descriptor that will serve as 
-        the access point on the mapped class.
+        """Create the Python descriptor that will serve as 
+        the access point on instances of the mapped class.
         
         """
 
         def fget(instance):
             dict_ = attributes.instance_dict(instance)
-            if self.key in dict_:
-                return dict_[self.key]
-            else:
-                dict_[self.key] = composite = self.composite_class(
-                    *[getattr(instance, key) for key in self._attribute_keys]
-            )
-                return composite
+            
+            # key not present, assume the columns aren't
+            # loaded.  The load events will establish
+            # the item.
+            if self.key not in dict_:
+                for key in self._attribute_keys:
+                    getattr(instance, key)
+                    
+            return dict_.get(self.key, None)
                 
         def fset(instance, value):
+            dict_ = attributes.instance_dict(instance)
+            state = attributes.instance_state(instance)
+            attr = state.manager[self.key]
+            previous = dict_.get(self.key, attributes.NO_VALUE)
+            for fn in attr.dispatch.on_set:
+                value = fn(state, value, previous, attr.impl)
+            dict_[self.key] = value
             if value is None:
-                fdel(instance)
+                for key in self._attribute_keys:
+                    setattr(instance, key, None)
             else:
-                dict_ = attributes.instance_dict(instance)
-                dict_[self.key] = value
                 for key, value in zip(
                         self._attribute_keys, 
                         value.__composite_values__()):
                     setattr(instance, key, value)
         
         def fdel(instance):
+            state = attributes.instance_state(instance)
+            dict_ = attributes.instance_dict(instance)
+            previous = dict_.pop(self.key, attributes.NO_VALUE)
+            attr = state.manager[self.key]
+            attr.dispatch.on_remove(state, previous, attr.impl)
             for key in self._attribute_keys:
                 setattr(instance, key, None)
         
         self.descriptor = property(fget, fset, fdel)
         
     def _setup_arguments_on_columns(self):
-        """Propigate configuration arguments made on this composite
+        """Propagate configuration arguments made on this composite
         to the target columns, for those that apply.
         
         """
@@ -137,19 +153,35 @@ class CompositeProperty(DescriptorProperty):
             prop.group = self.group
 
     def _setup_event_handlers(self):
-        """Establish events that will clear out the composite value
-        whenever changes in state occur on the target columns.
+        """Establish events that populate/expire the composite attribute."""
         
-        """
         def load_handler(state):
-            state.dict.pop(self.key, None)
+            dict_ = state.dict
+            
+            if self.key in dict_:
+                return
+                
+            # if column elements aren't loaded, skip.
+            # __get__() will initiate a load for those 
+            # columns
+            for k in self._attribute_keys:
+                if k not in dict_:
+                    return
+                    
+            dict_[self.key] = self.composite_class(
+                    *[state.dict[key] for key in 
+                    self._attribute_keys]
+                )
             
         def expire_handler(state, keys):
             if keys is None or set(self._attribute_keys).intersection(keys):
                 state.dict.pop(self.key, None)
         
         def insert_update_handler(mapper, connection, state):
-            state.dict.pop(self.key, None)
+            state.dict[self.key] = self.composite_class(
+                    *[state.dict.get(key, None) for key in 
+                    self._attribute_keys]
+                )
             
         event.listen(self.parent, 'on_after_insert', 
                                     insert_update_handler, raw=True)
@@ -159,14 +191,6 @@ class CompositeProperty(DescriptorProperty):
         event.listen(self.parent, 'on_refresh', load_handler, raw=True)
         event.listen(self.parent, "on_expire", expire_handler, raw=True)
         
-        # TODO:  add listeners to the column attributes, which 
-        # refresh the composite based on userland settings.
-        
-        # TODO: add a callable to the composite of the form
-        # _on_change(self, attrname) which will send up a corresponding
-        # refresh to the column attribute on all parents.  Basically
-        # a specialization of the scalars.py example.
-        
         
     @util.memoized_property
     def _attribute_keys(self):
@@ -293,10 +317,18 @@ class SynonymProperty(DescriptorProperty):
         self.descriptor = descriptor
         self.comparator_factory = comparator_factory
         self.doc = doc or (descriptor and descriptor.__doc__) or None
+        
         util.set_creation_order(self)
-
+    
+    # TODO: when initialized, check _proxied_property,
+    # emit a warning if its not a column-based property
+    
+    @util.memoized_property
+    def _proxied_property(self):
+        return getattr(self.parent.class_, self.name).property
+        
     def _comparator_factory(self, mapper):
-        prop = getattr(mapper.class_, self.name).property
+        prop = self._proxied_property
 
         if self.comparator_factory:
             comp = self.comparator_factory(prop, mapper)
index 47f63a7d6b147a8d3de71a22d1469509bb59cfbd..6c512100f49d0e3dad03455a888a995a1f7fb68b 100644 (file)
@@ -168,7 +168,7 @@ class MapperProperty(object):
 
         pass
 
-    def compare(self, operator, value):
+    def compare(self, operator, value, **kw):
         """Return a compare operation for the columns represented by
         this ``MapperProperty`` to the given value, which may be a
         column value or an instance.  'operator' is an operator from
index 346d7d4bf6d39cfc4cab6d028fe19774044289ec..cfd17500866fd18c289c4325fe983ff5ed96e0cc 100644 (file)
@@ -894,18 +894,14 @@ class Mapper(object):
 
     def get_property(self, key, _compile_mappers=True):
         """return a MapperProperty associated with the given key.
-        
-        Calls getattr() against the mapped class itself, so that class-level 
-        proxies will be resolved to the underlying property, if any.
-        
         """
 
         if _compile_mappers and _new_mappers:
             configure_mappers()
 
         try:
-            return getattr(self.class_, key).property
-        except AttributeError:
+            return self._props[key]
+        except KeyError:
             raise sa_exc.InvalidRequestError(
                     "Mapper '%s' has no property '%s'" % (self, key))
             
index 558a80b15ff19b2db6124e5434f884bd6cc06d5c..768d8636e691211bdb612ccaf72f0a0f9e1ecf60 100644 (file)
@@ -50,9 +50,9 @@ class PointTest(_base.MappedTest):
         class Graph(_base.BasicEntity):
             pass
         class Edge(_base.BasicEntity):
-            def __init__(self, start, end):
-                self.start = start
-                self.end = end
+            def __init__(self, *args):
+                if args:
+                    self.start, self.end = args
 
         mapper(Graph, graphs, properties={
             'edges':relationship(Edge)
@@ -183,6 +183,20 @@ class PointTest(_base.MappedTest):
         assert g2.edges[-1].start.x is None
         assert g2.edges[-1].start.y is None
 
+    @testing.resolve_artifact_names
+    def test_expire(self):
+        sess = self._fixture()
+        g = sess.query(Graph).first()
+        e = g.edges[0]
+        sess.expire(e)
+        assert 'start' not in e.__dict__
+        assert e.start == Point(3, 4)
+    
+    @testing.resolve_artifact_names
+    def test_default_value(self):
+        e = Edge()
+        eq_(e.start, None)
+        
 class PrimaryKeyTest(_base.MappedTest):
     @classmethod
     def define_tables(cls, metadata):
index 8f30325554e40d9b60da99a2312cf38a3be61d65..ba7a82c62adc4f0bf53d07c9d6b79b7c74837f58 100644 (file)
@@ -1212,7 +1212,9 @@ class DocumentTest(testing.TestBase):
         
         
 class OptionsTest(_fixtures.FixtureTest):
-
+    
+    @testing.fails_if(lambda: True, "0.7 regression, may not support "
+                                "synonyms for relationship")
     @testing.fails_on('maxdb', 'FIXME: unknown')
     @testing.resolve_artifact_names
     def test_synonym_options(self):
@@ -1220,8 +1222,7 @@ class OptionsTest(_fixtures.FixtureTest):
             addresses = relationship(mapper(Address, addresses), lazy='select',
                                  order_by=addresses.c.id),
             adlist = synonym('addresses')))
-
-
+        
         def go():
             sess = create_session()
             u = (sess.query(User).
index cd0deee2ecb7cd9c46bf6cba063c239086f656ad..15ffaff2f034ea16e5dd44eb11b3993781b7c474 100644 (file)
@@ -1401,6 +1401,8 @@ class SynonymTest(QueryTest):
         })
         mapper(Keyword, keywords)
 
+    @testing.fails_if(lambda: True, "0.7 regression, may not support "
+                                "synonyms for relationship")
     def test_joins(self):
         for j in (
             ['orders', 'items'],
@@ -1411,6 +1413,8 @@ class SynonymTest(QueryTest):
             result = create_session().query(User).join(*j).filter_by(id=3).all()
             assert [User(id=7, name='jack'), User(id=9, name='fred')] == result
 
+    @testing.fails_if(lambda: True, "0.7 regression, may not support "
+                                "synonyms for relationship")
     def test_with_parent(self):
         for nameprop, orderprop in (
             ('name', 'orders'),