]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
The :class:`.MutableComposite` type did not allow for the
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 4 Dec 2012 00:49:42 +0000 (19:49 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 4 Dec 2012 00:49:42 +0000 (19:49 -0500)
:meth:`.MutableBase.coerce` method to be used, even though
the code seemed to indicate this intent, so this now works
and a brief example is added.  As a side-effect,
the mechanics of this event handler have been changed so that
new :class:`.MutableComposite` types no longer add per-type
global event handlers.  Also in 0.7.10

[ticket:2624]

doc/build/changelog/changelog_07.rst
doc/build/changelog/changelog_08.rst
doc/build/orm/extensions/mutable.rst
lib/sqlalchemy/event.py
lib/sqlalchemy/ext/mutable.py
test/ext/test_mutable.py

index a02250635fd73abcf81c9dc4540a8360ed99c945..314a6b9a0afee82843c5f4be200c34798ab8486a 100644 (file)
@@ -8,6 +8,18 @@
     :version: 0.7.10
     :released:
 
+    .. change::
+        :tags: orm, bug
+        :tickets: 2624
+
+      The :class:`.MutableComposite` type did not allow for the
+      :meth:`.MutableBase.coerce` method to be used, even though
+      the code seemed to indicate this intent, so this now works
+      and a brief example is added.  As a side-effect,
+      the mechanics of this event handler have been changed so that
+      new :class:`.MutableComposite` types no longer add per-type
+      global event handlers.  Also in 0.8.0b2.
+
     .. change::
         :tags: orm, bug
         :tickets: 2583
index a27e076f8111492218976802e6e8afae62df1dad..2233e3be59bfe3f979060aa0270695e00b64c29e 100644 (file)
@@ -6,6 +6,18 @@
 .. changelog::
     :version: 0.8.0b2
 
+    .. change::
+        :tags: orm, bug
+        :tickets: 2624
+
+      The :class:`.MutableComposite` type did not allow for the
+      :meth:`.MutableBase.coerce` method to be used, even though
+      the code seemed to indicate this intent, so this now works
+      and a brief example is added.  As a side-effect,
+      the mechanics of this event handler have been changed so that
+      new :class:`.MutableComposite` types no longer add per-type
+      global event handlers.  Also in 0.7.10.
+
     .. change::
         :tags: sql, bug
         :tickets: 2621
index 259055980df0652e9b487d6afaf83789c7abe3e0..ba3e10542d22a4b3c638339d8307d1e851aa4c24 100644 (file)
@@ -9,7 +9,7 @@ API Reference
 -------------
 
 .. autoclass:: MutableBase
-    :members: _parents
+    :members: _parents, coerce
 
 .. autoclass:: Mutable
     :show-inheritance:
index bf996ae3c5865b0070b258014ca2405250bb2b4a..6453a3987bca02748587900f08b1dac9f462c3c6 100644 (file)
@@ -245,6 +245,9 @@ class _DispatchDescriptor(object):
         self._clslevel = util.defaultdict(list)
         self._empty_listeners = {}
 
+    def _contains(self, cls, evt):
+        return evt in self._clslevel[cls]
+
     def insert(self, obj, target, propagate):
         assert isinstance(target, type), \
                 "Class-level Event targets must be classes."
index 36d60d6d51e430ada363fd9566c1a64a04012371..e290a93e285e6723f754b966cadb1c233dbce867 100644 (file)
@@ -302,6 +302,31 @@ will flag the attribute as "dirty" on the parent object::
     >>> assert v1 in sess.dirty
     True
 
+Coercing Mutable Composites
+---------------------------
+
+The :meth:`.MutableBase.coerce` method is also supported on composite types.
+In the case of :class:`.MutableComposite`, the :meth:`.MutableBase.coerce`
+method is only called for attribute set operations, not load operations.
+Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent
+to using a :func:`.validates` validation routine for all attributes which
+make use of the custom composite type::
+
+    class Point(MutableComposite):
+        # other Point methods
+        # ...
+
+        def coerce(cls, key, value):
+            if isinstance(value, tuple):
+                value = Point(*value)
+            elif not isinstance(value, Point):
+                raise ValueError("tuple or Point expected")
+            return value
+
+.. versionadded:: 0.7.10,0.8.0b2
+    Support for the :meth:`.MutableBase.coerce` method in conjunction with
+    objects of type :class:`.MutableComposite`.
+
 Supporting Pickling
 --------------------
 
@@ -329,7 +354,7 @@ pickling process of the parent's object-relational state so that the
 """
 from ..orm.attributes import flag_modified
 from .. import event, types
-from ..orm import mapper, object_mapper
+from ..orm import mapper, object_mapper, Mapper
 from ..util import memoized_property
 import weakref
 
@@ -354,9 +379,27 @@ class MutableBase(object):
 
     @classmethod
     def coerce(cls, key, value):
-        """Given a value, coerce it into this type.
+        """Given a value, coerce it into the target type.
+
+        Can be overridden by custom subclasses to coerce incoming
+        data into a particular type.
+
+        By default, raises ``ValueError``.
+
+        This method is called in different scenarios depending on if
+        the parent class is of type :class:`.Mutable` or of type
+        :class:`.MutableComposite`.  In the case of the former, it is called
+        for both attribute-set operations as well as during ORM loading
+        operations.  For the latter, it is only called during attribute-set
+        operations; the mechanics of the :func:`.composite` construct
+        handle coercion during load operations.
+
+
+        :param key: string name of the ORM-mapped attribute being set.
+        :param value: the incoming value.
+        :return: the method should return the coerced value, or raise
+         ``ValueError`` if the coercion cannot be completed.
 
-        By default raises ValueError.
         """
         if value is None:
             return None
@@ -523,11 +566,6 @@ class Mutable(MutableBase):
         return sqltype
 
 
-class _MutableCompositeMeta(type):
-    def __init__(cls, classname, bases, dict_):
-        cls._setup_listeners()
-        return type.__init__(cls, classname, bases, dict_)
-
 
 class MutableComposite(MutableBase):
     """Mixin that defines transparent propagation of change
@@ -536,16 +574,7 @@ class MutableComposite(MutableBase):
 
     See the example in :ref:`mutable_composites` for usage information.
 
-    .. warning::
-
-       The listeners established by the :class:`.MutableComposite`
-       class are *global* to all mappers, and are *not* garbage
-       collected.   Only use :class:`.MutableComposite` for types that are
-       permanent to an application, not with ad-hoc types else this will
-       cause unbounded growth in memory usage.
-
     """
-    __metaclass__ = _MutableCompositeMeta
 
     def changed(self):
         """Subclasses should call this method whenever change events occur."""
@@ -558,24 +587,16 @@ class MutableComposite(MutableBase):
                                     prop._attribute_keys):
                 setattr(parent, attr_name, value)
 
-    @classmethod
-    def _setup_listeners(cls):
-        """Associate this wrapper with all future mapped composites
-        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), False, class_)
-
-        event.listen(mapper, 'mapper_configured', listen_for_type)
+def _setup_composite_listener():
+    def _listen_for_type(mapper, class_):
+        for prop in mapper.iterate_properties:
+            if (hasattr(prop, 'composite_class') and
+                issubclass(prop.composite_class, MutableComposite)):
+                prop.composite_class._listen_on_attribute(
+                    getattr(class_, prop.key), False, class_)
+    if not Mapper.dispatch.mapper_configured._contains(Mapper, _listen_for_type):
+        event.listen(Mapper, 'mapper_configured', _listen_for_type)
+_setup_composite_listener()
 
 
 class MutableDict(Mutable, dict):
index 57b87dcaa2a1f7d752b9bdf808072570ba7d7a65..4516e3ac2f5e26c0f84a0c4614cc59d76eca379a 100644 (file)
@@ -276,6 +276,12 @@ class _CompositeTestBase(object):
             Column('unrelated_data', String(50))
         )
 
+    def setup(self):
+        from sqlalchemy.ext import mutable
+        mutable._setup_composite_listener()
+        super(_CompositeTestBase, self).setup()
+
+
     def teardown(self):
         # clear out mapper events
         Mapper.dispatch._clear()
@@ -403,6 +409,71 @@ class MutableCompositesTest(_CompositeTestBase, fixtures.MappedTest):
 
         eq_(f1.data.x, 5)
 
+class MutableCompositeCustomCoerceTest(_CompositeTestBase, fixtures.MappedTest):
+    @classmethod
+    def _type_fixture(cls):
+
+        from sqlalchemy.ext.mutable import MutableComposite
+
+        global Point
+
+        class Point(MutableComposite):
+            def __init__(self, x, y):
+                self.x = x
+                self.y = y
+
+            @classmethod
+            def coerce(cls, key, value):
+                if isinstance(value, tuple):
+                    value = Point(*value)
+                return value
+
+            def __setattr__(self, key, value):
+                object.__setattr__(self, key, value)
+                self.changed()
+
+            def __composite_values__(self):
+                return self.x, self.y
+
+            def __getstate__(self):
+                return self.x, self.y
+
+            def __setstate__(self, state):
+                self.x, self.y = state
+
+            def __eq__(self, other):
+                return isinstance(other, Point) and \
+                    other.x == self.x and \
+                    other.y == self.y
+        return Point
+
+
+    @classmethod
+    def setup_mappers(cls):
+        foo = cls.tables.foo
+
+        Point = cls._type_fixture()
+
+        mapper(Foo, foo, properties={
+            'data': composite(Point, foo.c.x, foo.c.y)
+        })
+
+    def test_custom_coerce(self):
+        f = Foo()
+        f.data = (3, 4)
+        eq_(f.data, Point(3, 4))
+
+    def test_round_trip_ok(self):
+        sess = Session()
+        f = Foo()
+        f.data = (3, 4)
+
+        sess.add(f)
+        sess.commit()
+
+        eq_(f.data, Point(3, 4))
+
+
 class MutableInheritedCompositesTest(_CompositeTestBase, fixtures.MappedTest):
     @classmethod
     def define_tables(cls, metadata):