]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- add support for pickling with mutable scalars, mutable composites
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 3 Jan 2011 00:54:31 +0000 (19:54 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 3 Jan 2011 00:54:31 +0000 (19:54 -0500)
- add pickle/unpickle events to ORM events.  these are needed
for the mutable extension.
- finish mutable extension documentation, consolidate examples,
add full descriptions

doc/build/conf.py
doc/build/core/types.rst
doc/build/orm/extensions/mutable.rst
doc/build/orm/mapper_config.rst
lib/sqlalchemy/ext/mutable.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/events.py
lib/sqlalchemy/orm/state.py
test/ext/test_mutable.py
test/orm/test_pickled.py

index 81120fed00145f2599e95c4a6c89cef637046f5e..43c787824a1a9ba46d03d251a03ea942e6340815 100644 (file)
@@ -53,7 +53,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'SQLAlchemy'
-copyright = u'2007, 2008, 2009, 2010, the SQLAlchemy authors and contributors'
+copyright = u'2007-2010, the SQLAlchemy authors and contributors'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
index d2a56e8e9f28531b4fdfaf50804f6c84035a8125..fe4cca9500305ebabd201636c232624f1897df1c 100644 (file)
@@ -342,9 +342,8 @@ Marshal JSON Strings
 This type uses ``simplejson`` to marshal Python data structures
 to/from JSON.   Can be modified to use Python's builtin json encoder::
 
-
-    from sqlalchemy.types import TypeDecorator, MutableType, VARCHAR
-    import simplejson
+    from sqlalchemy.types import TypeDecorator, VARCHAR
+    import json
 
     class JSONEncodedDict(TypeDecorator):
         """Represents an immutable structure as a json-encoded string.
@@ -359,65 +358,23 @@ to/from JSON.   Can be modified to use Python's builtin json encoder::
 
         def process_bind_param(self, value, dialect):
             if value is not None:
-                value = simplejson.dumps(value, use_decimal=True)
+                value = json.dumps(value)
 
             return value
 
         def process_result_value(self, value, dialect):
             if value is not None:
-                value = simplejson.loads(value, use_decimal=True)
+                value = json.loads(value)
             return value
 
-
-Note that the base type is not "mutable", meaning in-place changes to 
-the value will not be detected by the ORM - you instead would need to 
-replace the existing value with a new one to detect changes.  To add
-support for mutability, we need to build a dictionary that detects
-changes, and combine this using the ``sqlalchemy.ext.mutable`` extension
-described in :ref:`mutable_toplevel`::
-
-    from sqlalchemy.ext.mutable import Mutable
-
-    class MutationDict(Mutable, dict):
-        @classmethod
-        def coerce(cls, key, value):
-            """Convert plain dictionaries to MutationDict."""
-            if not isinstance(value, MutationDict):
-                if isinstance(value, dict):
-                    return MutationDict(value)
-
-                # this call will raise ValueError
-                return Mutable.coerce(key, value)
-            else:
-                return value
-
-        def __setitem__(self, key, value):
-            """Detect dictionary set events and emit change events."""
-
-            dict.__setitem__(self, key, value)
-            self.change()
-
-        def __delitem__(self, key):
-            """Detect dictionary del events and emit change events."""
-
-            dict.__delitem__(self, key)
-            self.change()
-
-        # additional dict methods would be overridden here
-
-The new dictionary type can be associated with JSONEncodedDict using
-an event listener established by the :meth:`.Mutable.associate_with`
-method::
-
-    MutationDict.associate_with(JSONEncodedDict)
-
-Alternatively, specific usages of ``JSONEncodedDict`` can be associated
-with ``MutationDict`` via :meth:`.Mutable.as_mutable`::
-
-    Table('mytable', metadata,
-        Column('id', Integer, primary_key=True),
-        Column('data', MutationDict.as_mutable(JSONEncodedDict))
-    )
+Note that the ORM by default will not detect "mutability" on such a type -
+meaning, in-place changes to values will not be detected and will not be
+flushed. Without further steps, you instead would need to replace the existing
+value with a new one on each parent object to detect changes. Note that
+there's nothing wrong with this, as many applications may not require that the
+values are ever mutated once created.  For those which do have this requirment,
+support for mutability is best applied using the ``sqlalchemy.ext.mutable``
+extension - see the example in :ref:`mutable_toplevel`.
 
 Creating New Types
 ~~~~~~~~~~~~~~~~~~
index 0b15e7a60f3c69177f68d956bf55509dd8a2e528..b0428f951959989e28ad2a66579e106a3cd7e8a4 100644 (file)
@@ -8,11 +8,18 @@ Mutation Tracking
 API Reference
 -------------
 
+.. autoclass:: MutableBase
+    :members: _parents
+
 .. autoclass:: Mutable
+    :show-inheritance:
     :members:
 
 .. autoclass:: MutableComposite
+    :show-inheritance:
     :members:
 
 
 
+
+
index 7c0641147c784a0902387a0c3842af5cea5094d3..75ce82c25ac1a621594fa919f73cb7c1ebf5b25e 100644 (file)
@@ -493,10 +493,10 @@ provides a single attribute which represents the group of columns using the
 class you provide.
 
 .. note::
-    As of SQLAlchemy 0.7, composites are implemented as a simple wrapper using
-    the :ref:`hybrids_toplevel` feature.   Note that composites no longer
-    "conceal" the underlying colunm based attributes, or support in-place 
-    mutation.
+    As of SQLAlchemy 0.7, composites have been simplified such that 
+    they no longer "conceal" the underlying column based attributes.  Additionally,
+    in-place mutation is no longer automatic; see the section below on
+    enabling mutability to support tracking of in-place changes.
 
 A simple example represents pairs of columns as a "Point" object.
 Starting with a table that represents two points as x1/y1 and x2/y2::
@@ -518,12 +518,15 @@ pair::
         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 other is not None and \
-                    other.x == self.x and \
-                    other.y == self.y
+            return isinstance(other, Point) and \
+                other.x == self.x and \
+                other.y == self.y
+
         def __ne__(self, other):
             return not self.__eq__(other)
 
@@ -546,8 +549,22 @@ The :func:`.composite` function is then used in the mapping::
         'end': composite(Point, vertices.c.x2, vertices.c.y2)
     })
 
-We can now use the ``Vertex`` instances as well as querying as though the
-``start`` and ``end`` attributes are regular scalar attributes::
+When using :mod:`sqlalchemy.ext.declarative`, the individual 
+:class:`.Column` objects may optionally be bundled into the 
+:func:`.composite` call, ensuring that they are named::
+
+    from sqlalchemy.ext.declarative import declarative_base
+    Base = declarative_base()
+
+    class Vertex(Base):
+        __tablename__ = 'vertices'
+        id = Column(Integer, primary_key=True)
+        start = composite(Point, Column('x1', Integer), Column('y1', Integer))
+        end = composite(Point, Column('x2', Integer), Column('y2', Integer))
+
+Using either configurational approach, we can now use the ``Vertex`` instances
+as well as querying as though the ``start`` and ``end`` attributes are regular
+scalar attributes::
 
     session = Session()
     v = Vertex(Point(3, 4), Point(5, 6))
@@ -555,6 +572,21 @@ We can now use the ``Vertex`` instances as well as querying as though the
 
     v2 = session.query(Vertex).filter(Vertex.start == Point(3, 4))
 
+.. autofunction:: composite
+
+Tracking In-Place Mutations on Composites
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As of SQLAlchemy 0.7, in-place changes to an existing composite value are 
+not tracked automatically.  Instead, the composite class needs to provide
+events to its parent object explicitly.   This task is largely automated 
+via the usage of the :class:`.MutableComposite` mixin, which uses events
+to associate each user-defined composite object with all parent associations.
+Please see the example in :ref:`mutable_composites`.
+
+Redefining Comparison Operations for Composites
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 The "equals" comparison operation by default produces an AND of all
 corresponding columns equated to one another. This can be changed using
 the ``comparator_factory``, described in :ref:`custom_comparators`.
@@ -579,9 +611,6 @@ the same expression that the base "greater than" does::
                                     comparator_factory=PointComparator)
     })
 
-.. autofunction:: composite
-
-
 .. _maptojoin:
 
 Mapping a Class against Multiple Tables
index 11a7977f67e5c9a78a5e663d2177fa13b164a87c..594e664fbd46cf5ee15f8f599897d16c066640cb 100644 (file)
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
 """Provide support for tracking of in-place changes to scalar values,
-which are propagated to owning parent objects.
+which are propagated into ORM change events on owning parent objects.
 
-The ``mutable`` extension is a replacement for the :class:`.types.MutableType`
-class as well as the ``mutable=True`` flag available on types which subclass
-it.
+The :mod:`sqlalchemy.ext.mutable` extension replaces SQLAlchemy's legacy approach to in-place
+mutations of scalar values, established by the :class:`.types.MutableType`
+class as well as the ``mutable=True`` type flag, with a system that allows
+change events to be propagated from the value to the owning parent, thereby
+removing the need for the ORM to maintain copies of values as well as the very
+expensive requirement of scanning through all "mutable" values on each flush
+call, looking for changes.
 
+.. _mutable_scalars:
+
+Establishing Mutability on Scalar Column Values
+===============================================
+
+A typical example of a "mutable" structure is a Python dictionary.
+Following the example introduced in :ref:`types_toplevel`, we 
+begin with a custom type that marshals Python dictionaries into 
+JSON strings before being persisted::
+
+    from sqlalchemy.types import TypeDecorator, VARCHAR
+    import json
+
+    class JSONEncodedDict(TypeDecorator):
+        "Represents an immutable structure as a json-encoded string."
+
+        impl = VARCHAR
+
+        def process_bind_param(self, value, dialect):
+            if value is not None:
+                value = json.dumps(value)
+            return value
+
+        def process_result_value(self, value, dialect):
+            if value is not None:
+                value = json.loads(value)
+            return value
+
+The usage of ``json`` is only for the purposes of example. The :mod:`sqlalchemy.ext.mutable` 
+extension can be used
+with any type whose target Python type may be mutable, including
+:class:`.PickleType`, :class:`.postgresql.ARRAY`, etc.
+
+When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself
+tracks all parents which reference it.  Here we will replace the usage
+of plain Python dictionaries with a dict subclass that implements
+the :class:`.Mutable` mixin::
+
+    import collections
+    from sqlalchemy.ext.mutable import Mutable
+
+    class MutationDict(Mutable, collections.MutableMapping, dict):
+        @classmethod
+        def coerce(cls, key, value):
+            "Convert plain dictionaries to MutationDict."
+
+            if not isinstance(value, MutationDict):
+                if isinstance(value, dict):
+                    return MutationDict(value)
+
+                # this call will raise ValueError
+                return Mutable.coerce(key, value)
+            else:
+                return value
+
+        def __setitem__(self, key, value):
+            "Detect dictionary set events and emit change events."
+
+            dict.__setitem__(self, key, value)
+            self.changed()
+
+        def __delitem__(self, key):
+            "Detect dictionary del events and emit change events."
+
+            dict.__delitem__(self, key)
+            self.changed()
+
+The above dictionary class takes the approach of subclassing the Python
+built-ins ``collections.MutableMapping`` and ``dict`` to produce a dict
+subclass which routes all mutation events through ``__setitem__``. There are
+many variants on this approach, such as subclassing ``UserDict.UserDict``,
+etc. The part that's important to this example is that the
+:meth:`.Mutable.changed` method is called whenever an in-place change to the
+datastructure takes place.
+
+We also redefine the :meth:`.Mutable.coerce` method which will be used to
+convert any values that are not instances of ``MutationDict``, such
+as the plain dictionaries returned by the ``json`` module, into the
+appropriate type.  Defining this method is optional; we could just as well created our
+``JSONEncodedDict`` such that it always returns an instance of ``MutationDict``,
+and additionally ensured that all calling code uses ``MutationDict`` 
+explicitly.  When :meth:`.Mutable.coerce` is not overridden, any values
+applied to a parent object which are not instances of the mutable type
+will raise a ``ValueError``.
+
+Our new ``MutationDict`` type offers a class method
+:meth:`~.Mutable.associate_with` which we can use within column metadata
+to associate with types. This method grabs the given type object and
+associates a listener that will detect all future mappings of this type,
+applying event listening instrumentation to the mapped attribute. Such as,
+with classical table metadata::
+
+    from sqlalchemy import Table, Column, Integer
+
+    my_data = Table('my_data', metadata,
+        Column('id', Integer, primary_key=True),
+        Column('data', MutationDict.associate_with(JSONEncodedDict))
+    )
+
+Above, :meth:`~.Mutable.associate_with` returns an instance of ``JSONEncodedDict``
+(if the type object was not an instance already), which will intercept any 
+attributes which are mapped against this type.  Below we establish a simple
+mapping against the ``my_data`` table::
+
+    from sqlalchemy import mapper
+
+    class MyDataClass(object):
+        pass
+
+    # associates mutation listeners with MyDataClass.data
+    mapper(MyDataClass, my_data)
+
+The ``MyDataClass.data`` member will now be notified of in place changes
+to its value.
+
+There's no difference in usage when using declarative::
+
+    from sqlalchemy.ext.declarative import declarative_base
+
+    Base = declarative_base()
+
+    class MyDataClass(Base):
+        __tablename__ = 'my_data'
+        id = Column(Integer, primary_key=True)
+        data = Column(MutationDict.associate_with(JSONEncodedDict))
+
+Any in-place changes to the ``MyDataClass.data`` member
+will flag the attribute as "dirty" on the parent object::
+
+    >>> from sqlalchemy.orm import Session
+
+    >>> sess = Session()
+    >>> m1 = MyDataClass(data={'value1':'foo'})
+    >>> sess.add(m1)
+    >>> sess.commit()
+
+    >>> m1.data['value1'] = 'bar'
+    >>> assert m1 in sess.dirty
+    True
+
+Supporting Pickling
+--------------------
+
+The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the
+placement of a ``weakref.WeakKeyDictionary`` upon the value object, which
+stores a mapping of parent mapped objects keyed to the attribute name under
+which they are associated with this value. ``WeakKeyDictionary`` objects are
+not picklable, due to the fact that they contain weakrefs and function
+callbacks. In our case, this is a good thing, since if this dictionary were
+picklable, it could lead to an excessively large pickle size for our value
+objects that are pickled by themselves outside of the context of the parent.
+The developer responsiblity here is only to provide a ``__getstate__`` method
+that excludes the :meth:`~.MutableBase._parents` collection from the pickle
+stream::
+
+    class MyMutableType(Mutable):
+        def __getstate__(self):
+            d = self.__dict__.copy()
+            d.pop('_parents', None)
+            return d
+
+With our dictionary example, we need to return the contents of the dict itself
+(and also restore them on __setstate__)::
+
+    class MutationDict(Mutable, collections.MutableMapping, dict):
+        # ....
+
+        def __getstate__(self):
+            return dict(self)
+
+        def __setstate__(self, state):
+            self.update(state)
+
+In the case that our mutable value object is pickled as it is attached to one
+or more parent objects that are also part of the pickle, the :class:`.Mutable`
+mixin will re-establish the :attr:`.Mutable._parents` collection on each value
+object as the owning parents themselves are unpickled.
+
+.. _mutable_composites:
+
+Establishing Mutability on Composites
+=====================================
+
+Composites are a special ORM feature which allow a single scalar attribute to
+be assigned an object value which represents information "composed" from one
+or more columns from the underlying mapped table. The usual example is that of
+a geometric "point", and is introduced in :ref:`mapper_composite`.
+
+As of SQLAlchemy 0.7, the internals of :func:`.orm.composite` have been
+greatly simplified and in-place mutation detection is no longer enabled by
+default; instead, the user-defined value must detect changes on its own and
+propagate them to all owning parents. The :mod:`sqlalchemy.ext.mutable`
+extension provides the helper class :class:`.MutableComposite`, which is a
+slight variant on the :class:`.Mutable` class.
+
+As is the case with :class:`.Mutable`, the user-defined composite class
+subclasses :class:`.MutableComposite` as a mixin, and detects and delivers
+change events to its parents via the :meth:`.MutableComposite.changed` method.
+In the case of a composite class, the detection is usually via the usage of
+Python descriptors (i.e. ``@property``), or alternatively via the special
+Python method ``__setattr__()``. Below we expand upon the ``Point`` class
+introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite`
+and to also route attribute set events via ``__setattr__`` to the
+:meth:`.MutableComposite.changed` method::
+
+    from sqlalchemy.ext.mutable import MutableComposite
+
+    class Point(MutableComposite):
+        def __init__(self, x, y):
+            self.x = x
+            self.y = y
+
+        def __setattr__(self, key, value):
+            "Intercept set events"
+
+            # set the attribute
+            object.__setattr__(self, key, value)
+
+            # alert all parents to the change
+            self.changed()
+
+        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
+
+        def __ne__(self, other):
+            return not self.__eq__(other)
+
+The :class:`.MutableComposite` class uses a Python metaclass to automatically
+establish listeners for any usage of :func:`.orm.composite` that specifies our
+``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class,
+listeners are established which will route change events from ``Point``
+objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
+
+    from sqlalchemy.orm import composite, mapper
+    from sqlalchemy import Table, Column
+
+    vertices = Table('vertices', metadata,
+        Column('id', Integer, primary_key=True),
+        Column('x1', Integer),
+        Column('y1', Integer),
+        Column('x2', Integer),
+        Column('y2', Integer),
+        )
+
+    class Vertex(object):
+        pass
+
+    mapper(Vertex, vertices, properties={
+        'start': composite(Point, vertices.c.x1, vertices.c.y1),
+        'end': composite(Point, vertices.c.x2, vertices.c.y2)
+    })
+
+Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
+will flag the attribute as "dirty" on the parent object::
+
+    >>> from sqlalchemy.orm import Session
+
+    >>> sess = Session()
+    >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
+    >>> sess.add(v1)
+    >>> sess.commit()
+
+    >>> v1.end.x = 8
+    >>> assert v1 in sess.dirty
+    True
+
+Supporting Pickling
+--------------------
+
+As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper
+class uses a ``weakref.WeakKeyDictionary`` available via the
+:meth:`.MutableBase._parents` attribute which isn't picklable. If we need to
+pickle instances of ``Point`` or its owning class ``Vertex``, we at least need
+to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
+Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
+the minimal form of our ``Point`` class::
+
+    class Point(MutableComposite):
+        # ...
+        
+        def __getstate__(self):
+            return self.x, self.y
+        
+        def __setstate__(self, state):
+            self.x, self.y = state
+
+As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
+pickling process of the parent's object-relational state so that the
+:meth:`.MutableBase._parents` collection is restored to all ``Point`` objects.
 
 """
 from sqlalchemy.orm.attributes import flag_modified
@@ -19,37 +317,23 @@ from sqlalchemy.orm import mapper, object_mapper
 from sqlalchemy.util import memoized_property
 import weakref
 
-class Mutable(object):
-    """Mixin that defines transparent propagation of change
-    events to a parent object.
-
-    """
+class MutableBase(object):
+    """Common base class to :class:`.Mutable` and :class:`.MutableComposite`."""
 
     @memoized_property
     def _parents(self):
-        """Dictionary of parent object->attribute name on the parent."""
-
-        return weakref.WeakKeyDictionary()
-
-    def change(self):
-        """Subclasses should call this method whenever change events occur."""
-
-        for parent, key in self._parents.items():
-            flag_modified(parent, key)
-
-    @classmethod
-    def coerce(cls, key, value):
-        """Given a value, coerce it into this type.
-
-        By default raises ValueError.
+        """Dictionary of parent object->attribute name on the parent.
+        
+        This attribute is a so-called "memoized" property.  It initializes
+        itself with a new ``weakref.WeakKeyDictionary`` the first time
+        it is accessed, returning the same object upon subsequent access.
+        
         """
-        if value is None:
-            return None
-        raise ValueError("Attribute '%s' accepts objects of type %s" % (key, cls))
 
+        return weakref.WeakKeyDictionary()
 
     @classmethod
-    def associate_with_attribute(cls, attribute):
+    def _listen_on_attribute(cls, attribute, coerce):
         """Establish this type as a mutation listener for the given 
         mapped descriptor.
 
@@ -66,8 +350,9 @@ class Mutable(object):
             """
             val = state.dict.get(key, None)
             if val is not None:
-                val = cls.coerce(key, val)
-                state.dict[key] = val
+                if coerce:
+                    val = cls.coerce(key, val)
+                    state.dict[key] = val
                 val._parents[state.obj()] = key
 
         def set(target, value, oldvalue, initiator):
@@ -87,11 +372,55 @@ class Mutable(object):
                 oldvalue._parents.pop(state.obj(), None)
             return value
 
+        def pickle(state, state_dict):
+            val = state.dict.get(key, None)
+            if val is not None:
+                if 'ext.mutable.values' not in state_dict:
+                    state_dict['ext.mutable.values'] = []
+                state_dict['ext.mutable.values'].append(val)
+
+        def unpickle(state, state_dict):
+            if 'ext.mutable.values' in state_dict:
+                for val in state_dict['ext.mutable.values']:
+                    val._parents[state.obj()] = key
+
         event.listen(parent_cls, 'load', load, raw=True)
         event.listen(parent_cls, 'refresh', load, raw=True)
         event.listen(attribute, 'set', set, raw=True, retval=True)
+        event.listen(parent_cls, 'pickle', pickle, raw=True)
+        event.listen(parent_cls, 'unpickle', unpickle, raw=True)
+
+class Mutable(MutableBase):
+    """Mixin that defines transparent propagation of change
+    events to a parent object.
+
+    See the example in :ref:`mutable_scalars` for usage information.
+
+    """
+
+    def changed(self):
+        """Subclasses should call this method whenever change events occur."""
+
+        for parent, key in self._parents.items():
+            flag_modified(parent, key)
+
+    @classmethod
+    def coerce(cls, key, value):
+        """Given a value, coerce it into this type.
+
+        By default raises ValueError.
+        """
+        if value is None:
+            return None
+        raise ValueError("Attribute '%s' accepts objects of type %s" % (key, cls))
+
+    @classmethod
+    def associate_with_attribute(cls, attribute):
+        """Establish this type as a mutation listener for the given 
+        mapped descriptor.
 
-        # TODO: need a deserialize hook here
+        """
+        cls._listen_on_attribute(attribute, True)
 
     @classmethod
     def associate_with(cls, sqltype):
@@ -161,46 +490,18 @@ class Mutable(object):
 
         return sqltype
 
-
 class _MutableCompositeMeta(type):
     def __init__(cls, classname, bases, dict_):
         cls._setup_listeners()
         return type.__init__(cls, classname, bases, dict_)
 
-class MutableComposite(object):
+class MutableComposite(MutableBase):
     """Mixin that defines transparent propagation of change
     events on a SQLAlchemy "composite" object to its
     owning parent or parents.
-
-    Composite classes, in addition to meeting the usage contract
-    defined in :ref:`mapper_composite`, also define some system
-    of relaying change events to the given :meth:`.change` 
-    method, which will notify all parents of the change.  Below
-    the special Python method ``__setattr__`` is used to intercept
-    all changes::
-
-        class Point(MutableComposite):
-            def __init__(self, x, y):
-                self.x = x
-                self.y = y
-
-            def __setattr__(self, key, value):
-                object.__setattr__(self, key, value)
-                self.change()
-
-            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
-
-    :class:`.MutableComposite` defines a metaclass which augments
-    the creation of :class:`.MutableComposite` subclasses with an event
-    that will listen for any :func:`~.orm.composite` mappings against the 
-    new type, establishing listeners that will track parent associations.
-
+    
+    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,
@@ -210,13 +511,7 @@ class MutableComposite(object):
     """
     __metaclass__ = _MutableCompositeMeta
 
-    @memoized_property
-    def _parents(self):
-        """Dictionary of parent object->attribute name on the parent."""
-
-        return weakref.WeakKeyDictionary()
-
-    def change(self):
+    def changed(self):
         """Subclasses should call this method whenever change events occur."""
 
         for parent, key in self._parents.items():
@@ -227,48 +522,6 @@ class MutableComposite(object):
                                     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 load(state, *args):
-            """Listen for objects loaded or refreshed.
-
-            Wrap the target data member's value with 
-            ``Mutable``.
-
-            """
-
-            val = state.dict.get(key, None)
-            if val is not None:
-                val._parents[state.obj()] = key
-
-        def 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, 'load', load, raw=True)
-        event.listen(parent_cls, 'refresh', load, raw=True)
-        event.listen(attribute, 'set', set, raw=True, retval=True)
-
-        # TODO: need a deserialize hook here
-
     @classmethod
     def _setup_listeners(cls):
         """Associate this wrapper with all future mapped compoistes
@@ -281,7 +534,7 @@ class MutableComposite(object):
         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))
+                    cls._listen_on_attribute(getattr(class_, prop.key), False)
 
         event.listen(mapper, 'mapper_configured', listen_for_type)
 
index 0b77b0239d8f8cd8135262865bb0165a9d0c9e97..73b079e0320a226ec8e78d602aafa44cca1f5a6c 100644 (file)
@@ -671,6 +671,7 @@ def composite(class_, *cols, **kwargs):
       an :class:`.AttributeExtension` instance,
       or list of extensions, which will be prepended to the list of
       attribute listeners for the resulting descriptor placed on the class.
+      **Deprecated.**  Please see :class:`.AttributeEvents`.
 
     """
     return CompositeProperty(class_, *cols, **kwargs)
index 761ba315dda017b6a78b14ce83f81386a56cfba9..8e4d4a1470445c2180615b736dda8f4aef58f6bd 100644 (file)
@@ -195,6 +195,33 @@ class InstanceEvents(event.Events):
 
         """
 
+    def pickle(self, target, state_dict):
+        """Receive an object instance when its associated state is
+        being pickled.
+
+        :param target: the mapped instance.  If 
+         the event is configured with ``raw=True``, this will 
+         instead be the :class:`.InstanceState` state-management
+         object associated with the instance.
+        :param state_dict: the dictionary returned by 
+         :class:`.InstanceState.__getstate__`, containing the state
+         to be pickled.
+         
+        """
+
+    def unpickle(self, target, state_dict):
+        """Receive an object instance after it's associated state has
+        been unpickled.
+
+        :param target: the mapped instance.  If 
+         the event is configured with ``raw=True``, this will 
+         instead be the :class:`.InstanceState` state-management
+         object associated with the instance.
+        :param state_dict: the dictionary sent to
+         :class:`.InstanceState.__setstate__`, containing the state
+         dictionary which was pickled.
+        
+        """
 
 class MapperEvents(event.Events):
     """Define events specific to mappings.
index da91a353eb1c9cd334fcfbd4e6506a2fbbede6bc..7a93b5cb0be5bc324e66b9c38d427449dde77633 100644 (file)
@@ -147,6 +147,9 @@ class InstanceState(object):
         )
         if self.load_path:
             d['load_path'] = interfaces.serialize_path(self.load_path)
+
+        self.manager.dispatch.pickle(self, d)
+
         return d
 
     def __setstate__(self, state):
@@ -182,7 +185,7 @@ class InstanceState(object):
         if 'load_path' in state:
             self.load_path = interfaces.deserialize_path(state['load_path'])
 
-        # TODO: need an event here, link to composite, mutable
+        manager.dispatch.unpickle(self, state)
 
     def initialize(self, key):
         """Set this attribute to an empty value or collection, 
index 9ae28a7fc6883e0cd26eca02e6502405113d9bc4..56da4120e1dd03c834f524697fee02dad47a59e2 100644 (file)
@@ -5,11 +5,18 @@ from sqlalchemy.orm.mapper import Mapper
 from sqlalchemy.orm.instrumentation import ClassManager
 from test.lib.schema import Table, Column
 from test.lib.testing import eq_
+from test.lib.util import picklers
 from test.lib import testing
 from test.orm import _base
 import sys
+import pickle
+
+class Foo(_base.BasicEntity):
+    pass
 
 class _MutableDictTestBase(object):
+    run_define_tables = 'each'
+
     @classmethod
     def _type_fixture(cls):
         from sqlalchemy.ext.mutable import Mutable
@@ -30,23 +37,20 @@ class _MutableDictTestBase(object):
             def __getstate__(self):
                 return dict(self)
 
-            def __setstate__(self, dict):
-                self.update(dict)
+            def __setstate__(self, state):
+                self.update(state)
 
             def __setitem__(self, key, value):
                 dict.__setitem__(self, key, value)
-                self.change()
+                self.changed()
 
             def __delitem__(self, key):
                 dict.__delitem__(self, key)
-                self.change()
+                self.changed()
         return MutationDict
 
     @testing.resolve_artifact_names
     def setup_mappers(cls):
-        class Foo(_base.BasicEntity):
-            pass
-
         mapper(Foo, foo)
 
     def teardown(self):
@@ -68,6 +72,23 @@ class _MutableDictTestBase(object):
 
         eq_(f1.data, {'a':'c'})
 
+    @testing.resolve_artifact_names
+    def test_pickle_parent(self):
+        sess = Session()
+
+        f1 = Foo(data={'a':'b'})
+        sess.add(f1)
+        sess.commit()
+        f1.data
+        sess.close()
+
+        for loads, dumps in picklers():
+            sess = Session()
+            f2 = loads(dumps(f1))
+            sess.add(f2)
+            f2.data['a'] = 'c'
+            assert f2 in sess.dirty
+
     @testing.resolve_artifact_names
     def _test_non_mutable(self):
         sess = Session()
@@ -169,7 +190,8 @@ class MutableAssociationScalarJSONTest(_MutableDictTestBase, _base.MappedTest):
             Column('data', JSONEncodedDict)
         )
 
-class MutableCompositesTest(_base.MappedTest):
+
+class _CompositeTestBase(object):
     @classmethod
     def define_tables(cls, metadata):
         Table('foo', metadata,
@@ -182,7 +204,9 @@ class MutableCompositesTest(_base.MappedTest):
         # clear out mapper events
         Mapper.dispatch._clear()
         ClassManager.dispatch._clear()
-        super(MutableCompositesTest, self).teardown()
+        super(_CompositeTestBase, self).teardown()
+
+class MutableCompositesTest(_CompositeTestBase, _base.MappedTest):
 
     @classmethod
     def _type_fixture(cls):
@@ -199,11 +223,19 @@ class MutableCompositesTest(_base.MappedTest):
 
             def __setattr__(self, key, value):
                 object.__setattr__(self, key, value)
-                self.change()
+                self.changed()
 
             def __composite_values__(self):
                 return self.x, self.y
 
+            def __getstate__(self):
+                d = dict(self.__dict__)
+                d.pop('_parents', None)
+                return d
+
+            #def __setstate__(self, state):
+            #    self.x, self.y = state
+
             def __eq__(self, other):
                 return isinstance(other, Point) and \
                     other.x == self.x and \
@@ -215,9 +247,6 @@ class MutableCompositesTest(_base.MappedTest):
     def setup_mappers(cls):
         Point = cls._type_fixture()
 
-        class Foo(_base.BasicEntity):
-            pass
-
         mapper(Foo, foo, properties={
             'data':composite(Point, foo.c.x, foo.c.y)
         })
@@ -235,3 +264,21 @@ class MutableCompositesTest(_base.MappedTest):
 
         eq_(f1.data, Point(3, 5))
 
+    @testing.resolve_artifact_names
+    def test_pickle_of_parent(self):
+        sess = Session()
+        d = Point(3, 4)
+        f1 = Foo(data=d)
+        sess.add(f1)
+        sess.commit()
+
+        f1.data
+        assert 'data' in f1.__dict__
+        sess.close()
+
+        for loads, dumps in picklers():
+            sess = Session()
+            f2 = loads(dumps(f1))
+            sess.add(f2)
+            f2.data.y = 12
+            assert f2 in sess.dirty
index 3383c48b985e49c93bd8265585a771c57430e876..fd75077335a8b4f14f8ba1843c3a6e3257c8dddc 100644 (file)
@@ -2,6 +2,7 @@ from test.lib.testing import eq_
 import pickle
 import sqlalchemy as sa
 from test.lib import testing
+from test.lib.util import picklers
 from test.lib.testing import assert_raises_message
 from sqlalchemy import Integer, String, ForeignKey, exc, MetaData
 from test.lib.schema import Table, Column
@@ -215,8 +216,9 @@ class PickleTest(_fixtures.FixtureTest):
 
         u1 = sess.query(User).first()
         u1.addresses
-        for protocol in -1, 0, 1, 2:
-            u2 = pickle.loads(pickle.dumps(u1, protocol))
+
+        for loads, dumps in picklers():
+            u2 = loads(dumps(u1))
             eq_(u1, u2)
 
     @testing.resolve_artifact_names