>>> 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
--------------------
"""
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
@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
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
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."""
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):
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()
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):