From: Mike Bayer Date: Tue, 4 Dec 2012 00:56:55 +0000 (-0500) Subject: The :class:`.MutableComposite` type did not allow for the X-Git-Tag: rel_0_7_10~23 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=84e000f0766b8e4f9e2cd48615720c32c234ffa0;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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. [ticket:2624] --- diff --git a/doc/build/changelog/changelog_07.rst b/doc/build/changelog/changelog_07.rst index 4623d08efb..8d61cb4124 100644 --- a/doc/build/changelog/changelog_07.rst +++ b/doc/build/changelog/changelog_07.rst @@ -27,6 +27,18 @@ to the MSSQL dialect's "schema rendering" logic's failure to take .key into account. + .. 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 diff --git a/doc/build/orm/extensions/mutable.rst b/doc/build/orm/extensions/mutable.rst index b0428f9519..88976f056b 100644 --- a/doc/build/orm/extensions/mutable.rst +++ b/doc/build/orm/extensions/mutable.rst @@ -9,7 +9,7 @@ API Reference ------------- .. autoclass:: MutableBase - :members: _parents + :members: _parents, coerce .. autoclass:: Mutable :show-inheritance: diff --git a/lib/sqlalchemy/event.py b/lib/sqlalchemy/event.py index 5c7eacebf8..2151a218fc 100644 --- a/lib/sqlalchemy/event.py +++ b/lib/sqlalchemy/event.py @@ -206,6 +206,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." diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index ab4aff806b..5327ed59c3 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -301,6 +301,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 -------------------- @@ -328,7 +353,7 @@ pickling process of the parent's object-relational state so that the """ from sqlalchemy.orm.attributes import flag_modified from sqlalchemy import event, types -from sqlalchemy.orm import mapper, object_mapper +from sqlalchemy.orm import mapper, object_mapper, Mapper from sqlalchemy.util import memoized_property import weakref @@ -349,9 +374,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 @@ -512,11 +555,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 events on a SQLAlchemy "composite" object to its @@ -533,7 +571,6 @@ class MutableComposite(MutableBase): in memory usage. """ - __metaclass__ = _MutableCompositeMeta def changed(self): """Subclasses should call this method whenever change events occur.""" @@ -546,19 +583,14 @@ 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() diff --git a/test/ext/test_mutable.py b/test/ext/test_mutable.py index c345d8fe38..a923b84189 100644 --- a/test/ext/test_mutable.py +++ b/test/ext/test_mutable.py @@ -301,6 +301,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() @@ -429,6 +435,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):