diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-12-29 15:04:35 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-12-29 15:04:35 -0500 |
| commit | b1c90de4494369c7c901f9f3e5e21271656024c5 (patch) | |
| tree | 91dade0d060c5f49aedd68271f64a6eb56cf214b /lib/sqlalchemy/ext/mutable.py | |
| parent | 3b41b66981d8665282c645178643d273361eb6ad (diff) | |
| download | sqlalchemy-b1c90de4494369c7c901f9f3e5e21271656024c5.tar.gz | |
- mutable examples now move into sqlalchemy.ext.mutable
- streamline interfaces, get Mutable/MutableComposite to be as minimal
in usage as possible
- docs for mutable, warnings regrarding mapper events being global
- move MutableType/mutable=True outwards, move orm tests to its
own module, note in all documentation
- still need more events/tests for correct pickling support of
composites, mutables. in the case of composites its needed
even without mutation. see [ticket:2009]
Diffstat (limited to 'lib/sqlalchemy/ext/mutable.py')
| -rw-r--r-- | lib/sqlalchemy/ext/mutable.py | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py new file mode 100644 index 000000000..7dcbfd996 --- /dev/null +++ b/lib/sqlalchemy/ext/mutable.py @@ -0,0 +1,281 @@ +"""Provide support for tracking of in-place changes to scalar values, +which are propagated to 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. + + +""" +from sqlalchemy.orm.attributes import flag_modified +from sqlalchemy import event, types +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. + + """ + + @memoized_property + def _parents(self): + """Dictionary of parent object->attribute name on the parent.""" + + return weakref.WeakKeyDictionary() + + def on_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. + """ + 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. + + """ + 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 + ``Mutable``. + + """ + val = state.dict.get(key, None) + if val is not None: + val = cls.coerce(key, val) + state.dict[key] = val + 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. + + """ + + if not isinstance(value, cls): + value = cls.coerce(key, value) + 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) + + # TODO: need a deserialize hook here + + @classmethod + def associate_with(cls, sqltype): + """Associate this wrapper with all future mapped columns + of the given type. + + This is a convenience method that calls ``associate_with_attribute`` automatically. + + .. warning:: The listeners established by this method are *global* + to all mappers, and are *not* garbage collected. Only use + :meth:`.associate_with` for types that are permanent to an application, + not with ad-hoc types else this will cause unbounded growth + in memory usage. + + """ + + def listen_for_type(mapper, class_): + for prop in mapper.iterate_properties: + if hasattr(prop, 'columns'): + if isinstance(prop.columns[0].type, sqltype): + cls.associate_with_attribute(getattr(class_, prop.key)) + break + + event.listen(mapper, 'on_mapper_configured', listen_for_type) + + @classmethod + def as_mutable(cls, sqltype): + """Associate a SQL type with this mutable Python type. + + This establishes listeners that will detect ORM mappings against + the given type, adding mutation event trackers to those mappings. + + The type is returned, unconditionally as an instance, so that + :meth:`.as_mutable` can be used inline:: + + Table('mytable', metadata, + Column('id', Integer, primary_key=True), + Column('data', MyMutableType.as_mutable(PickleType)) + ) + + Note that the returned type is always an instance, even if a class + is given, and that only columns which are declared specifically with that + type instance receive additional instrumentation. + + To associate a particular mutable type with all occurences of a + particular type, use the :meth:`.Mutable.associate_with` classmethod + of the particular :meth:`.Mutable` subclass to establish a global + assoiation. + + .. warning:: The listeners established by this method are *global* + to all mappers, and are *not* garbage collected. Only use + :meth:`.as_mutable` for types that are permanent to an application, + not with ad-hoc types else this will cause unbounded growth + in memory usage. + + """ + sqltype = types.to_instance(sqltype) + + def listen_for_type(mapper, class_): + for prop in mapper.iterate_properties: + if hasattr(prop, 'columns'): + if prop.columns[0].type is sqltype: + cls.associate_with_attribute(getattr(class_, prop.key)) + break + + event.listen(mapper, 'on_mapper_configured', listen_for_type) + + return sqltype + + +class _MutableCompositeMeta(type): + def __init__(cls, classname, bases, dict_): + cls._setup_listeners() + return type.__init__(cls, classname, bases, dict_) + +class MutableComposite(object): + """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:`.on_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.on_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. + + .. 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 + + @memoized_property + def _parents(self): + """Dictionary of parent object->attribute name on the parent.""" + + return weakref.WeakKeyDictionary() + + 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 + ``Mutable``. + + """ + + 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) + + # TODO: need a deserialize hook here + + @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) + |
