Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/ext/mutable.py: 36%
264 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1# ext/mutable.py
2# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
8r"""Provide support for tracking of in-place changes to scalar values,
9which are propagated into ORM change events on owning parent objects.
11.. _mutable_scalars:
13Establishing Mutability on Scalar Column Values
14===============================================
16A typical example of a "mutable" structure is a Python dictionary.
17Following the example introduced in :ref:`types_toplevel`, we
18begin with a custom type that marshals Python dictionaries into
19JSON strings before being persisted::
21 from sqlalchemy.types import TypeDecorator, VARCHAR
22 import json
24 class JSONEncodedDict(TypeDecorator):
25 "Represents an immutable structure as a json-encoded string."
27 impl = VARCHAR
29 def process_bind_param(self, value, dialect):
30 if value is not None:
31 value = json.dumps(value)
32 return value
34 def process_result_value(self, value, dialect):
35 if value is not None:
36 value = json.loads(value)
37 return value
39The usage of ``json`` is only for the purposes of example. The
40:mod:`sqlalchemy.ext.mutable` extension can be used
41with any type whose target Python type may be mutable, including
42:class:`.PickleType`, :class:`_postgresql.ARRAY`, etc.
44When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself
45tracks all parents which reference it. Below, we illustrate a simple
46version of the :class:`.MutableDict` dictionary object, which applies
47the :class:`.Mutable` mixin to a plain Python dictionary::
49 from sqlalchemy.ext.mutable import Mutable
51 class MutableDict(Mutable, dict):
52 @classmethod
53 def coerce(cls, key, value):
54 "Convert plain dictionaries to MutableDict."
56 if not isinstance(value, MutableDict):
57 if isinstance(value, dict):
58 return MutableDict(value)
60 # this call will raise ValueError
61 return Mutable.coerce(key, value)
62 else:
63 return value
65 def __setitem__(self, key, value):
66 "Detect dictionary set events and emit change events."
68 dict.__setitem__(self, key, value)
69 self.changed()
71 def __delitem__(self, key):
72 "Detect dictionary del events and emit change events."
74 dict.__delitem__(self, key)
75 self.changed()
77The above dictionary class takes the approach of subclassing the Python
78built-in ``dict`` to produce a dict
79subclass which routes all mutation events through ``__setitem__``. There are
80variants on this approach, such as subclassing ``UserDict.UserDict`` or
81``collections.MutableMapping``; the part that's important to this example is
82that the :meth:`.Mutable.changed` method is called whenever an in-place
83change to the datastructure takes place.
85We also redefine the :meth:`.Mutable.coerce` method which will be used to
86convert any values that are not instances of ``MutableDict``, such
87as the plain dictionaries returned by the ``json`` module, into the
88appropriate type. Defining this method is optional; we could just as well
89created our ``JSONEncodedDict`` such that it always returns an instance
90of ``MutableDict``, and additionally ensured that all calling code
91uses ``MutableDict`` explicitly. When :meth:`.Mutable.coerce` is not
92overridden, any values applied to a parent object which are not instances
93of the mutable type will raise a ``ValueError``.
95Our new ``MutableDict`` type offers a class method
96:meth:`~.Mutable.as_mutable` which we can use within column metadata
97to associate with types. This method grabs the given type object or
98class and associates a listener that will detect all future mappings
99of this type, applying event listening instrumentation to the mapped
100attribute. Such as, with classical table metadata::
102 from sqlalchemy import Table, Column, Integer
104 my_data = Table('my_data', metadata,
105 Column('id', Integer, primary_key=True),
106 Column('data', MutableDict.as_mutable(JSONEncodedDict))
107 )
109Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
110(if the type object was not an instance already), which will intercept any
111attributes which are mapped against this type. Below we establish a simple
112mapping against the ``my_data`` table::
114 from sqlalchemy import mapper
116 class MyDataClass(object):
117 pass
119 # associates mutation listeners with MyDataClass.data
120 mapper(MyDataClass, my_data)
122The ``MyDataClass.data`` member will now be notified of in place changes
123to its value.
125There's no difference in usage when using declarative::
127 from sqlalchemy.ext.declarative import declarative_base
129 Base = declarative_base()
131 class MyDataClass(Base):
132 __tablename__ = 'my_data'
133 id = Column(Integer, primary_key=True)
134 data = Column(MutableDict.as_mutable(JSONEncodedDict))
136Any in-place changes to the ``MyDataClass.data`` member
137will flag the attribute as "dirty" on the parent object::
139 >>> from sqlalchemy.orm import Session
141 >>> sess = Session()
142 >>> m1 = MyDataClass(data={'value1':'foo'})
143 >>> sess.add(m1)
144 >>> sess.commit()
146 >>> m1.data['value1'] = 'bar'
147 >>> assert m1 in sess.dirty
148 True
150The ``MutableDict`` can be associated with all future instances
151of ``JSONEncodedDict`` in one step, using
152:meth:`~.Mutable.associate_with`. This is similar to
153:meth:`~.Mutable.as_mutable` except it will intercept all occurrences
154of ``MutableDict`` in all mappings unconditionally, without
155the need to declare it individually::
157 MutableDict.associate_with(JSONEncodedDict)
159 class MyDataClass(Base):
160 __tablename__ = 'my_data'
161 id = Column(Integer, primary_key=True)
162 data = Column(JSONEncodedDict)
165Supporting Pickling
166--------------------
168The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the
169placement of a ``weakref.WeakKeyDictionary`` upon the value object, which
170stores a mapping of parent mapped objects keyed to the attribute name under
171which they are associated with this value. ``WeakKeyDictionary`` objects are
172not picklable, due to the fact that they contain weakrefs and function
173callbacks. In our case, this is a good thing, since if this dictionary were
174picklable, it could lead to an excessively large pickle size for our value
175objects that are pickled by themselves outside of the context of the parent.
176The developer responsibility here is only to provide a ``__getstate__`` method
177that excludes the :meth:`~MutableBase._parents` collection from the pickle
178stream::
180 class MyMutableType(Mutable):
181 def __getstate__(self):
182 d = self.__dict__.copy()
183 d.pop('_parents', None)
184 return d
186With our dictionary example, we need to return the contents of the dict itself
187(and also restore them on __setstate__)::
189 class MutableDict(Mutable, dict):
190 # ....
192 def __getstate__(self):
193 return dict(self)
195 def __setstate__(self, state):
196 self.update(state)
198In the case that our mutable value object is pickled as it is attached to one
199or more parent objects that are also part of the pickle, the :class:`.Mutable`
200mixin will re-establish the :attr:`.Mutable._parents` collection on each value
201object as the owning parents themselves are unpickled.
203Receiving Events
204----------------
206The :meth:`.AttributeEvents.modified` event handler may be used to receive
207an event when a mutable scalar emits a change event. This event handler
208is called when the :func:`.attributes.flag_modified` function is called
209from within the mutable extension::
211 from sqlalchemy.ext.declarative import declarative_base
212 from sqlalchemy import event
214 Base = declarative_base()
216 class MyDataClass(Base):
217 __tablename__ = 'my_data'
218 id = Column(Integer, primary_key=True)
219 data = Column(MutableDict.as_mutable(JSONEncodedDict))
221 @event.listens_for(MyDataClass.data, "modified")
222 def modified_json(instance):
223 print("json value modified:", instance.data)
225.. _mutable_composites:
227Establishing Mutability on Composites
228=====================================
230Composites are a special ORM feature which allow a single scalar attribute to
231be assigned an object value which represents information "composed" from one
232or more columns from the underlying mapped table. The usual example is that of
233a geometric "point", and is introduced in :ref:`mapper_composite`.
235As is the case with :class:`.Mutable`, the user-defined composite class
236subclasses :class:`.MutableComposite` as a mixin, and detects and delivers
237change events to its parents via the :meth:`.MutableComposite.changed` method.
238In the case of a composite class, the detection is usually via the usage of
239Python descriptors (i.e. ``@property``), or alternatively via the special
240Python method ``__setattr__()``. Below we expand upon the ``Point`` class
241introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite`
242and to also route attribute set events via ``__setattr__`` to the
243:meth:`.MutableComposite.changed` method::
245 from sqlalchemy.ext.mutable import MutableComposite
247 class Point(MutableComposite):
248 def __init__(self, x, y):
249 self.x = x
250 self.y = y
252 def __setattr__(self, key, value):
253 "Intercept set events"
255 # set the attribute
256 object.__setattr__(self, key, value)
258 # alert all parents to the change
259 self.changed()
261 def __composite_values__(self):
262 return self.x, self.y
264 def __eq__(self, other):
265 return isinstance(other, Point) and \
266 other.x == self.x and \
267 other.y == self.y
269 def __ne__(self, other):
270 return not self.__eq__(other)
272The :class:`.MutableComposite` class uses a Python metaclass to automatically
273establish listeners for any usage of :func:`_orm.composite` that specifies our
274``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class,
275listeners are established which will route change events from ``Point``
276objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
278 from sqlalchemy.orm import composite, mapper
279 from sqlalchemy import Table, Column
281 vertices = Table('vertices', metadata,
282 Column('id', Integer, primary_key=True),
283 Column('x1', Integer),
284 Column('y1', Integer),
285 Column('x2', Integer),
286 Column('y2', Integer),
287 )
289 class Vertex(object):
290 pass
292 mapper(Vertex, vertices, properties={
293 'start': composite(Point, vertices.c.x1, vertices.c.y1),
294 'end': composite(Point, vertices.c.x2, vertices.c.y2)
295 })
297Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
298will flag the attribute as "dirty" on the parent object::
300 >>> from sqlalchemy.orm import Session
302 >>> sess = Session()
303 >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
304 >>> sess.add(v1)
305 >>> sess.commit()
307 >>> v1.end.x = 8
308 >>> assert v1 in sess.dirty
309 True
311Coercing Mutable Composites
312---------------------------
314The :meth:`.MutableBase.coerce` method is also supported on composite types.
315In the case of :class:`.MutableComposite`, the :meth:`.MutableBase.coerce`
316method is only called for attribute set operations, not load operations.
317Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent
318to using a :func:`.validates` validation routine for all attributes which
319make use of the custom composite type::
321 class Point(MutableComposite):
322 # other Point methods
323 # ...
325 def coerce(cls, key, value):
326 if isinstance(value, tuple):
327 value = Point(*value)
328 elif not isinstance(value, Point):
329 raise ValueError("tuple or Point expected")
330 return value
332Supporting Pickling
333--------------------
335As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper
336class uses a ``weakref.WeakKeyDictionary`` available via the
337:meth:`MutableBase._parents` attribute which isn't picklable. If we need to
338pickle instances of ``Point`` or its owning class ``Vertex``, we at least need
339to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
340Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
341the minimal form of our ``Point`` class::
343 class Point(MutableComposite):
344 # ...
346 def __getstate__(self):
347 return self.x, self.y
349 def __setstate__(self, state):
350 self.x, self.y = state
352As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
353pickling process of the parent's object-relational state so that the
354:meth:`MutableBase._parents` collection is restored to all ``Point`` objects.
356"""
357from collections import defaultdict
358import weakref
360from .. import event
361from .. import inspect
362from .. import types
363from ..orm import Mapper
364from ..orm import mapper
365from ..orm.attributes import flag_modified
366from ..sql.base import SchemaEventTarget
367from ..util import memoized_property
370class MutableBase(object):
371 """Common base class to :class:`.Mutable`
372 and :class:`.MutableComposite`.
374 """
376 @memoized_property
377 def _parents(self):
378 """Dictionary of parent object's :class:`.InstanceState`->attribute
379 name on the parent.
381 This attribute is a so-called "memoized" property. It initializes
382 itself with a new ``weakref.WeakKeyDictionary`` the first time
383 it is accessed, returning the same object upon subsequent access.
385 .. versionchanged:: 1.4 the :class:`.InstanceState` is now used
386 as the key in the weak dictionary rather than the instance
387 itself.
389 """
391 return weakref.WeakKeyDictionary()
393 @classmethod
394 def coerce(cls, key, value):
395 """Given a value, coerce it into the target type.
397 Can be overridden by custom subclasses to coerce incoming
398 data into a particular type.
400 By default, raises ``ValueError``.
402 This method is called in different scenarios depending on if
403 the parent class is of type :class:`.Mutable` or of type
404 :class:`.MutableComposite`. In the case of the former, it is called
405 for both attribute-set operations as well as during ORM loading
406 operations. For the latter, it is only called during attribute-set
407 operations; the mechanics of the :func:`.composite` construct
408 handle coercion during load operations.
411 :param key: string name of the ORM-mapped attribute being set.
412 :param value: the incoming value.
413 :return: the method should return the coerced value, or raise
414 ``ValueError`` if the coercion cannot be completed.
416 """
417 if value is None:
418 return None
419 msg = "Attribute '%s' does not accept objects of type %s"
420 raise ValueError(msg % (key, type(value)))
422 @classmethod
423 def _get_listen_keys(cls, attribute):
424 """Given a descriptor attribute, return a ``set()`` of the attribute
425 keys which indicate a change in the state of this attribute.
427 This is normally just ``set([attribute.key])``, but can be overridden
428 to provide for additional keys. E.g. a :class:`.MutableComposite`
429 augments this set with the attribute keys associated with the columns
430 that comprise the composite value.
432 This collection is consulted in the case of intercepting the
433 :meth:`.InstanceEvents.refresh` and
434 :meth:`.InstanceEvents.refresh_flush` events, which pass along a list
435 of attribute names that have been refreshed; the list is compared
436 against this set to determine if action needs to be taken.
438 .. versionadded:: 1.0.5
440 """
441 return {attribute.key}
443 @classmethod
444 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
445 """Establish this type as a mutation listener for the given
446 mapped descriptor.
448 """
449 key = attribute.key
450 if parent_cls is not attribute.class_:
451 return
453 # rely on "propagate" here
454 parent_cls = attribute.class_
456 listen_keys = cls._get_listen_keys(attribute)
458 def load(state, *args):
459 """Listen for objects loaded or refreshed.
461 Wrap the target data member's value with
462 ``Mutable``.
464 """
465 val = state.dict.get(key, None)
466 if val is not None:
467 if coerce:
468 val = cls.coerce(key, val)
469 state.dict[key] = val
470 val._parents[state] = key
472 def load_attrs(state, ctx, attrs):
473 if not attrs or listen_keys.intersection(attrs):
474 load(state)
476 def set_(target, value, oldvalue, initiator):
477 """Listen for set/replace events on the target
478 data member.
480 Establish a weak reference to the parent object
481 on the incoming value, remove it for the one
482 outgoing.
484 """
485 if value is oldvalue:
486 return value
488 if not isinstance(value, cls):
489 value = cls.coerce(key, value)
490 if value is not None:
491 value._parents[target] = key
492 if isinstance(oldvalue, cls):
493 oldvalue._parents.pop(inspect(target), None)
494 return value
496 def pickle(state, state_dict):
497 val = state.dict.get(key, None)
498 if val is not None:
499 if "ext.mutable.values" not in state_dict:
500 state_dict["ext.mutable.values"] = defaultdict(list)
501 state_dict["ext.mutable.values"][key].append(val)
503 def unpickle(state, state_dict):
504 if "ext.mutable.values" in state_dict:
505 collection = state_dict["ext.mutable.values"]
506 if isinstance(collection, list):
507 # legacy format
508 for val in collection:
509 val._parents[state] = key
510 else:
511 for val in state_dict["ext.mutable.values"][key]:
512 val._parents[state] = key
514 event.listen(
515 parent_cls,
516 "_sa_event_merge_wo_load",
517 load,
518 raw=True,
519 propagate=True,
520 )
522 event.listen(parent_cls, "load", load, raw=True, propagate=True)
523 event.listen(
524 parent_cls, "refresh", load_attrs, raw=True, propagate=True
525 )
526 event.listen(
527 parent_cls, "refresh_flush", load_attrs, raw=True, propagate=True
528 )
529 event.listen(
530 attribute, "set", set_, raw=True, retval=True, propagate=True
531 )
532 event.listen(parent_cls, "pickle", pickle, raw=True, propagate=True)
533 event.listen(
534 parent_cls, "unpickle", unpickle, raw=True, propagate=True
535 )
538class Mutable(MutableBase):
539 """Mixin that defines transparent propagation of change
540 events to a parent object.
542 See the example in :ref:`mutable_scalars` for usage information.
544 """
546 def changed(self):
547 """Subclasses should call this method whenever change events occur."""
549 for parent, key in self._parents.items():
550 flag_modified(parent.obj(), key)
552 @classmethod
553 def associate_with_attribute(cls, attribute):
554 """Establish this type as a mutation listener for the given
555 mapped descriptor.
557 """
558 cls._listen_on_attribute(attribute, True, attribute.class_)
560 @classmethod
561 def associate_with(cls, sqltype):
562 """Associate this wrapper with all future mapped columns
563 of the given type.
565 This is a convenience method that calls
566 ``associate_with_attribute`` automatically.
568 .. warning::
570 The listeners established by this method are *global*
571 to all mappers, and are *not* garbage collected. Only use
572 :meth:`.associate_with` for types that are permanent to an
573 application, not with ad-hoc types else this will cause unbounded
574 growth in memory usage.
576 """
578 def listen_for_type(mapper, class_):
579 if mapper.non_primary:
580 return
581 for prop in mapper.column_attrs:
582 if isinstance(prop.columns[0].type, sqltype):
583 cls.associate_with_attribute(getattr(class_, prop.key))
585 event.listen(mapper, "mapper_configured", listen_for_type)
587 @classmethod
588 def as_mutable(cls, sqltype):
589 """Associate a SQL type with this mutable Python type.
591 This establishes listeners that will detect ORM mappings against
592 the given type, adding mutation event trackers to those mappings.
594 The type is returned, unconditionally as an instance, so that
595 :meth:`.as_mutable` can be used inline::
597 Table('mytable', metadata,
598 Column('id', Integer, primary_key=True),
599 Column('data', MyMutableType.as_mutable(PickleType))
600 )
602 Note that the returned type is always an instance, even if a class
603 is given, and that only columns which are declared specifically with
604 that type instance receive additional instrumentation.
606 To associate a particular mutable type with all occurrences of a
607 particular type, use the :meth:`.Mutable.associate_with` classmethod
608 of the particular :class:`.Mutable` subclass to establish a global
609 association.
611 .. warning::
613 The listeners established by this method are *global*
614 to all mappers, and are *not* garbage collected. Only use
615 :meth:`.as_mutable` for types that are permanent to an application,
616 not with ad-hoc types else this will cause unbounded growth
617 in memory usage.
619 """
620 sqltype = types.to_instance(sqltype)
622 # a SchemaType will be copied when the Column is copied,
623 # and we'll lose our ability to link that type back to the original.
624 # so track our original type w/ columns
625 if isinstance(sqltype, SchemaEventTarget):
627 @event.listens_for(sqltype, "before_parent_attach")
628 def _add_column_memo(sqltyp, parent):
629 parent.info["_ext_mutable_orig_type"] = sqltyp
631 schema_event_check = True
632 else:
633 schema_event_check = False
635 def listen_for_type(mapper, class_):
636 if mapper.non_primary:
637 return
638 for prop in mapper.column_attrs:
639 if (
640 schema_event_check
641 and hasattr(prop.expression, "info")
642 and prop.expression.info.get("_ext_mutable_orig_type")
643 is sqltype
644 ) or (prop.columns[0].type is sqltype):
645 cls.associate_with_attribute(getattr(class_, prop.key))
647 event.listen(mapper, "mapper_configured", listen_for_type)
649 return sqltype
652class MutableComposite(MutableBase):
653 """Mixin that defines transparent propagation of change
654 events on a SQLAlchemy "composite" object to its
655 owning parent or parents.
657 See the example in :ref:`mutable_composites` for usage information.
659 """
661 @classmethod
662 def _get_listen_keys(cls, attribute):
663 return {attribute.key}.union(attribute.property._attribute_keys)
665 def changed(self):
666 """Subclasses should call this method whenever change events occur."""
668 for parent, key in self._parents.items():
670 prop = parent.mapper.get_property(key)
671 for value, attr_name in zip(
672 self.__composite_values__(), prop._attribute_keys
673 ):
674 setattr(parent.obj(), attr_name, value)
677def _setup_composite_listener():
678 def _listen_for_type(mapper, class_):
679 for prop in mapper.iterate_properties:
680 if (
681 hasattr(prop, "composite_class")
682 and isinstance(prop.composite_class, type)
683 and issubclass(prop.composite_class, MutableComposite)
684 ):
685 prop.composite_class._listen_on_attribute(
686 getattr(class_, prop.key), False, class_
687 )
689 if not event.contains(Mapper, "mapper_configured", _listen_for_type):
690 event.listen(Mapper, "mapper_configured", _listen_for_type)
693_setup_composite_listener()
696class MutableDict(Mutable, dict):
697 """A dictionary type that implements :class:`.Mutable`.
699 The :class:`.MutableDict` object implements a dictionary that will
700 emit change events to the underlying mapping when the contents of
701 the dictionary are altered, including when values are added or removed.
703 Note that :class:`.MutableDict` does **not** apply mutable tracking to the
704 *values themselves* inside the dictionary. Therefore it is not a sufficient
705 solution for the use case of tracking deep changes to a *recursive*
706 dictionary structure, such as a JSON structure. To support this use case,
707 build a subclass of :class:`.MutableDict` that provides appropriate
708 coercion to the values placed in the dictionary so that they too are
709 "mutable", and emit events up to their parent structure.
711 .. seealso::
713 :class:`.MutableList`
715 :class:`.MutableSet`
717 """
719 def __setitem__(self, key, value):
720 """Detect dictionary set events and emit change events."""
721 dict.__setitem__(self, key, value)
722 self.changed()
724 def setdefault(self, key, value):
725 result = dict.setdefault(self, key, value)
726 self.changed()
727 return result
729 def __delitem__(self, key):
730 """Detect dictionary del events and emit change events."""
731 dict.__delitem__(self, key)
732 self.changed()
734 def update(self, *a, **kw):
735 dict.update(self, *a, **kw)
736 self.changed()
738 def pop(self, *arg):
739 result = dict.pop(self, *arg)
740 self.changed()
741 return result
743 def popitem(self):
744 result = dict.popitem(self)
745 self.changed()
746 return result
748 def clear(self):
749 dict.clear(self)
750 self.changed()
752 @classmethod
753 def coerce(cls, key, value):
754 """Convert plain dictionary to instance of this class."""
755 if not isinstance(value, cls):
756 if isinstance(value, dict):
757 return cls(value)
758 return Mutable.coerce(key, value)
759 else:
760 return value
762 def __getstate__(self):
763 return dict(self)
765 def __setstate__(self, state):
766 self.update(state)
769class MutableList(Mutable, list):
770 """A list type that implements :class:`.Mutable`.
772 The :class:`.MutableList` object implements a list that will
773 emit change events to the underlying mapping when the contents of
774 the list are altered, including when values are added or removed.
776 Note that :class:`.MutableList` does **not** apply mutable tracking to the
777 *values themselves* inside the list. Therefore it is not a sufficient
778 solution for the use case of tracking deep changes to a *recursive*
779 mutable structure, such as a JSON structure. To support this use case,
780 build a subclass of :class:`.MutableList` that provides appropriate
781 coercion to the values placed in the dictionary so that they too are
782 "mutable", and emit events up to their parent structure.
784 .. versionadded:: 1.1
786 .. seealso::
788 :class:`.MutableDict`
790 :class:`.MutableSet`
792 """
794 def __reduce_ex__(self, proto):
795 return (self.__class__, (list(self),))
797 # needed for backwards compatibility with
798 # older pickles
799 def __setstate__(self, state):
800 self[:] = state
802 def __setitem__(self, index, value):
803 """Detect list set events and emit change events."""
804 list.__setitem__(self, index, value)
805 self.changed()
807 def __setslice__(self, start, end, value):
808 """Detect list set events and emit change events."""
809 list.__setslice__(self, start, end, value)
810 self.changed()
812 def __delitem__(self, index):
813 """Detect list del events and emit change events."""
814 list.__delitem__(self, index)
815 self.changed()
817 def __delslice__(self, start, end):
818 """Detect list del events and emit change events."""
819 list.__delslice__(self, start, end)
820 self.changed()
822 def pop(self, *arg):
823 result = list.pop(self, *arg)
824 self.changed()
825 return result
827 def append(self, x):
828 list.append(self, x)
829 self.changed()
831 def extend(self, x):
832 list.extend(self, x)
833 self.changed()
835 def __iadd__(self, x):
836 self.extend(x)
837 return self
839 def insert(self, i, x):
840 list.insert(self, i, x)
841 self.changed()
843 def remove(self, i):
844 list.remove(self, i)
845 self.changed()
847 def clear(self):
848 list.clear(self)
849 self.changed()
851 def sort(self, **kw):
852 list.sort(self, **kw)
853 self.changed()
855 def reverse(self):
856 list.reverse(self)
857 self.changed()
859 @classmethod
860 def coerce(cls, index, value):
861 """Convert plain list to instance of this class."""
862 if not isinstance(value, cls):
863 if isinstance(value, list):
864 return cls(value)
865 return Mutable.coerce(index, value)
866 else:
867 return value
870class MutableSet(Mutable, set):
871 """A set type that implements :class:`.Mutable`.
873 The :class:`.MutableSet` object implements a set that will
874 emit change events to the underlying mapping when the contents of
875 the set are altered, including when values are added or removed.
877 Note that :class:`.MutableSet` does **not** apply mutable tracking to the
878 *values themselves* inside the set. Therefore it is not a sufficient
879 solution for the use case of tracking deep changes to a *recursive*
880 mutable structure. To support this use case,
881 build a subclass of :class:`.MutableSet` that provides appropriate
882 coercion to the values placed in the dictionary so that they too are
883 "mutable", and emit events up to their parent structure.
885 .. versionadded:: 1.1
887 .. seealso::
889 :class:`.MutableDict`
891 :class:`.MutableList`
894 """
896 def update(self, *arg):
897 set.update(self, *arg)
898 self.changed()
900 def intersection_update(self, *arg):
901 set.intersection_update(self, *arg)
902 self.changed()
904 def difference_update(self, *arg):
905 set.difference_update(self, *arg)
906 self.changed()
908 def symmetric_difference_update(self, *arg):
909 set.symmetric_difference_update(self, *arg)
910 self.changed()
912 def __ior__(self, other):
913 self.update(other)
914 return self
916 def __iand__(self, other):
917 self.intersection_update(other)
918 return self
920 def __ixor__(self, other):
921 self.symmetric_difference_update(other)
922 return self
924 def __isub__(self, other):
925 self.difference_update(other)
926 return self
928 def add(self, elem):
929 set.add(self, elem)
930 self.changed()
932 def remove(self, elem):
933 set.remove(self, elem)
934 self.changed()
936 def discard(self, elem):
937 set.discard(self, elem)
938 self.changed()
940 def pop(self, *arg):
941 result = set.pop(self, *arg)
942 self.changed()
943 return result
945 def clear(self):
946 set.clear(self)
947 self.changed()
949 @classmethod
950 def coerce(cls, index, value):
951 """Convert plain set to instance of this class."""
952 if not isinstance(value, cls):
953 if isinstance(value, set):
954 return cls(value)
955 return Mutable.coerce(index, value)
956 else:
957 return value
959 def __getstate__(self):
960 return set(self)
962 def __setstate__(self, state):
963 self.update(state)
965 def __reduce_ex__(self, proto):
966 return (self.__class__, (list(self),))