1# ext/mutable.py
2# Copyright (C) 2005-2024 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
7
8r"""Provide support for tracking of in-place changes to scalar values,
9which are propagated into ORM change events on owning parent objects.
10
11.. _mutable_scalars:
12
13Establishing Mutability on Scalar Column Values
14===============================================
15
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::
20
21 from sqlalchemy.types import TypeDecorator, VARCHAR
22 import json
23
24 class JSONEncodedDict(TypeDecorator):
25 "Represents an immutable structure as a json-encoded string."
26
27 impl = VARCHAR
28
29 def process_bind_param(self, value, dialect):
30 if value is not None:
31 value = json.dumps(value)
32 return value
33
34 def process_result_value(self, value, dialect):
35 if value is not None:
36 value = json.loads(value)
37 return value
38
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.
43
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::
48
49 from sqlalchemy.ext.mutable import Mutable
50
51 class MutableDict(Mutable, dict):
52 @classmethod
53 def coerce(cls, key, value):
54 "Convert plain dictionaries to MutableDict."
55
56 if not isinstance(value, MutableDict):
57 if isinstance(value, dict):
58 return MutableDict(value)
59
60 # this call will raise ValueError
61 return Mutable.coerce(key, value)
62 else:
63 return value
64
65 def __setitem__(self, key, value):
66 "Detect dictionary set events and emit change events."
67
68 dict.__setitem__(self, key, value)
69 self.changed()
70
71 def __delitem__(self, key):
72 "Detect dictionary del events and emit change events."
73
74 dict.__delitem__(self, key)
75 self.changed()
76
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.
84
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``.
94
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::
101
102 from sqlalchemy import Table, Column, Integer
103
104 my_data = Table('my_data', metadata,
105 Column('id', Integer, primary_key=True),
106 Column('data', MutableDict.as_mutable(JSONEncodedDict))
107 )
108
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::
113
114 from sqlalchemy import mapper
115
116 class MyDataClass(object):
117 pass
118
119 # associates mutation listeners with MyDataClass.data
120 mapper(MyDataClass, my_data)
121
122The ``MyDataClass.data`` member will now be notified of in place changes
123to its value.
124
125There's no difference in usage when using declarative::
126
127 from sqlalchemy.ext.declarative import declarative_base
128
129 Base = declarative_base()
130
131 class MyDataClass(Base):
132 __tablename__ = 'my_data'
133 id = Column(Integer, primary_key=True)
134 data = Column(MutableDict.as_mutable(JSONEncodedDict))
135
136Any in-place changes to the ``MyDataClass.data`` member
137will flag the attribute as "dirty" on the parent object::
138
139 >>> from sqlalchemy.orm import Session
140
141 >>> sess = Session()
142 >>> m1 = MyDataClass(data={'value1':'foo'})
143 >>> sess.add(m1)
144 >>> sess.commit()
145
146 >>> m1.data['value1'] = 'bar'
147 >>> assert m1 in sess.dirty
148 True
149
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::
156
157 MutableDict.associate_with(JSONEncodedDict)
158
159 class MyDataClass(Base):
160 __tablename__ = 'my_data'
161 id = Column(Integer, primary_key=True)
162 data = Column(JSONEncodedDict)
163
164
165Supporting Pickling
166--------------------
167
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::
179
180 class MyMutableType(Mutable):
181 def __getstate__(self):
182 d = self.__dict__.copy()
183 d.pop('_parents', None)
184 return d
185
186With our dictionary example, we need to return the contents of the dict itself
187(and also restore them on __setstate__)::
188
189 class MutableDict(Mutable, dict):
190 # ....
191
192 def __getstate__(self):
193 return dict(self)
194
195 def __setstate__(self, state):
196 self.update(state)
197
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.
202
203Receiving Events
204----------------
205
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::
210
211 from sqlalchemy.ext.declarative import declarative_base
212 from sqlalchemy import event
213
214 Base = declarative_base()
215
216 class MyDataClass(Base):
217 __tablename__ = 'my_data'
218 id = Column(Integer, primary_key=True)
219 data = Column(MutableDict.as_mutable(JSONEncodedDict))
220
221 @event.listens_for(MyDataClass.data, "modified")
222 def modified_json(instance):
223 print("json value modified:", instance.data)
224
225.. _mutable_composites:
226
227Establishing Mutability on Composites
228=====================================
229
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`.
234
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::
244
245 from sqlalchemy.ext.mutable import MutableComposite
246
247 class Point(MutableComposite):
248 def __init__(self, x, y):
249 self.x = x
250 self.y = y
251
252 def __setattr__(self, key, value):
253 "Intercept set events"
254
255 # set the attribute
256 object.__setattr__(self, key, value)
257
258 # alert all parents to the change
259 self.changed()
260
261 def __composite_values__(self):
262 return self.x, self.y
263
264 def __eq__(self, other):
265 return isinstance(other, Point) and \
266 other.x == self.x and \
267 other.y == self.y
268
269 def __ne__(self, other):
270 return not self.__eq__(other)
271
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::
277
278 from sqlalchemy.orm import composite, mapper
279 from sqlalchemy import Table, Column
280
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 )
288
289 class Vertex(object):
290 pass
291
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 })
296
297Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
298will flag the attribute as "dirty" on the parent object::
299
300 >>> from sqlalchemy.orm import Session
301
302 >>> sess = Session()
303 >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
304 >>> sess.add(v1)
305 >>> sess.commit()
306
307 >>> v1.end.x = 8
308 >>> assert v1 in sess.dirty
309 True
310
311Coercing Mutable Composites
312---------------------------
313
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::
320
321 class Point(MutableComposite):
322 # other Point methods
323 # ...
324
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
331
332Supporting Pickling
333--------------------
334
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::
342
343 class Point(MutableComposite):
344 # ...
345
346 def __getstate__(self):
347 return self.x, self.y
348
349 def __setstate__(self, state):
350 self.x, self.y = state
351
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.
355
356"""
357from collections import defaultdict
358import weakref
359
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
368
369
370class MutableBase(object):
371 """Common base class to :class:`.Mutable`
372 and :class:`.MutableComposite`.
373
374 """
375
376 @memoized_property
377 def _parents(self):
378 """Dictionary of parent object's :class:`.InstanceState`->attribute
379 name on the parent.
380
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.
384
385 .. versionchanged:: 1.4 the :class:`.InstanceState` is now used
386 as the key in the weak dictionary rather than the instance
387 itself.
388
389 """
390
391 return weakref.WeakKeyDictionary()
392
393 @classmethod
394 def coerce(cls, key, value):
395 """Given a value, coerce it into the target type.
396
397 Can be overridden by custom subclasses to coerce incoming
398 data into a particular type.
399
400 By default, raises ``ValueError``.
401
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.
409
410
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.
415
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)))
421
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.
426
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.
431
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.
437
438 .. versionadded:: 1.0.5
439
440 """
441 return {attribute.key}
442
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.
447
448 """
449 key = attribute.key
450 if parent_cls is not attribute.class_:
451 return
452
453 # rely on "propagate" here
454 parent_cls = attribute.class_
455
456 listen_keys = cls._get_listen_keys(attribute)
457
458 def load(state, *args):
459 """Listen for objects loaded or refreshed.
460
461 Wrap the target data member's value with
462 ``Mutable``.
463
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
471
472 def load_attrs(state, ctx, attrs):
473 if not attrs or listen_keys.intersection(attrs):
474 load(state)
475
476 def set_(target, value, oldvalue, initiator):
477 """Listen for set/replace events on the target
478 data member.
479
480 Establish a weak reference to the parent object
481 on the incoming value, remove it for the one
482 outgoing.
483
484 """
485 if value is oldvalue:
486 return value
487
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
495
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)
502
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
513
514 event.listen(
515 parent_cls,
516 "_sa_event_merge_wo_load",
517 load,
518 raw=True,
519 propagate=True,
520 )
521
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 )
536
537
538class Mutable(MutableBase):
539 """Mixin that defines transparent propagation of change
540 events to a parent object.
541
542 See the example in :ref:`mutable_scalars` for usage information.
543
544 """
545
546 def changed(self):
547 """Subclasses should call this method whenever change events occur."""
548
549 for parent, key in self._parents.items():
550 flag_modified(parent.obj(), key)
551
552 @classmethod
553 def associate_with_attribute(cls, attribute):
554 """Establish this type as a mutation listener for the given
555 mapped descriptor.
556
557 """
558 cls._listen_on_attribute(attribute, True, attribute.class_)
559
560 @classmethod
561 def associate_with(cls, sqltype):
562 """Associate this wrapper with all future mapped columns
563 of the given type.
564
565 This is a convenience method that calls
566 ``associate_with_attribute`` automatically.
567
568 .. warning::
569
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.
575
576 """
577
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))
584
585 event.listen(mapper, "mapper_configured", listen_for_type)
586
587 @classmethod
588 def as_mutable(cls, sqltype):
589 """Associate a SQL type with this mutable Python type.
590
591 This establishes listeners that will detect ORM mappings against
592 the given type, adding mutation event trackers to those mappings.
593
594 The type is returned, unconditionally as an instance, so that
595 :meth:`.as_mutable` can be used inline::
596
597 Table('mytable', metadata,
598 Column('id', Integer, primary_key=True),
599 Column('data', MyMutableType.as_mutable(PickleType))
600 )
601
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.
605
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.
610
611 .. warning::
612
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.
618
619 """
620 sqltype = types.to_instance(sqltype)
621
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):
626
627 @event.listens_for(sqltype, "before_parent_attach")
628 def _add_column_memo(sqltyp, parent):
629 parent.info["_ext_mutable_orig_type"] = sqltyp
630
631 schema_event_check = True
632 else:
633 schema_event_check = False
634
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))
646
647 event.listen(mapper, "mapper_configured", listen_for_type)
648
649 return sqltype
650
651
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.
656
657 See the example in :ref:`mutable_composites` for usage information.
658
659 """
660
661 @classmethod
662 def _get_listen_keys(cls, attribute):
663 return {attribute.key}.union(attribute.property._attribute_keys)
664
665 def changed(self):
666 """Subclasses should call this method whenever change events occur."""
667
668 for parent, key in self._parents.items():
669
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)
675
676
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 )
688
689 if not event.contains(Mapper, "mapper_configured", _listen_for_type):
690 event.listen(Mapper, "mapper_configured", _listen_for_type)
691
692
693_setup_composite_listener()
694
695
696class MutableDict(Mutable, dict):
697 """A dictionary type that implements :class:`.Mutable`.
698
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.
702
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.
710
711 .. seealso::
712
713 :class:`.MutableList`
714
715 :class:`.MutableSet`
716
717 """
718
719 def __setitem__(self, key, value):
720 """Detect dictionary set events and emit change events."""
721 dict.__setitem__(self, key, value)
722 self.changed()
723
724 def setdefault(self, key, value):
725 result = dict.setdefault(self, key, value)
726 self.changed()
727 return result
728
729 def __delitem__(self, key):
730 """Detect dictionary del events and emit change events."""
731 dict.__delitem__(self, key)
732 self.changed()
733
734 def update(self, *a, **kw):
735 dict.update(self, *a, **kw)
736 self.changed()
737
738 def pop(self, *arg):
739 result = dict.pop(self, *arg)
740 self.changed()
741 return result
742
743 def popitem(self):
744 result = dict.popitem(self)
745 self.changed()
746 return result
747
748 def clear(self):
749 dict.clear(self)
750 self.changed()
751
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
761
762 def __getstate__(self):
763 return dict(self)
764
765 def __setstate__(self, state):
766 self.update(state)
767
768
769class MutableList(Mutable, list):
770 """A list type that implements :class:`.Mutable`.
771
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.
775
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.
783
784 .. versionadded:: 1.1
785
786 .. seealso::
787
788 :class:`.MutableDict`
789
790 :class:`.MutableSet`
791
792 """
793
794 def __reduce_ex__(self, proto):
795 return (self.__class__, (list(self),))
796
797 # needed for backwards compatibility with
798 # older pickles
799 def __setstate__(self, state):
800 self[:] = state
801
802 def __setitem__(self, index, value):
803 """Detect list set events and emit change events."""
804 list.__setitem__(self, index, value)
805 self.changed()
806
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()
811
812 def __delitem__(self, index):
813 """Detect list del events and emit change events."""
814 list.__delitem__(self, index)
815 self.changed()
816
817 def __delslice__(self, start, end):
818 """Detect list del events and emit change events."""
819 list.__delslice__(self, start, end)
820 self.changed()
821
822 def pop(self, *arg):
823 result = list.pop(self, *arg)
824 self.changed()
825 return result
826
827 def append(self, x):
828 list.append(self, x)
829 self.changed()
830
831 def extend(self, x):
832 list.extend(self, x)
833 self.changed()
834
835 def __iadd__(self, x):
836 self.extend(x)
837 return self
838
839 def insert(self, i, x):
840 list.insert(self, i, x)
841 self.changed()
842
843 def remove(self, i):
844 list.remove(self, i)
845 self.changed()
846
847 def clear(self):
848 list.clear(self)
849 self.changed()
850
851 def sort(self, **kw):
852 list.sort(self, **kw)
853 self.changed()
854
855 def reverse(self):
856 list.reverse(self)
857 self.changed()
858
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
868
869
870class MutableSet(Mutable, set):
871 """A set type that implements :class:`.Mutable`.
872
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.
876
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.
884
885 .. versionadded:: 1.1
886
887 .. seealso::
888
889 :class:`.MutableDict`
890
891 :class:`.MutableList`
892
893
894 """
895
896 def update(self, *arg):
897 set.update(self, *arg)
898 self.changed()
899
900 def intersection_update(self, *arg):
901 set.intersection_update(self, *arg)
902 self.changed()
903
904 def difference_update(self, *arg):
905 set.difference_update(self, *arg)
906 self.changed()
907
908 def symmetric_difference_update(self, *arg):
909 set.symmetric_difference_update(self, *arg)
910 self.changed()
911
912 def __ior__(self, other):
913 self.update(other)
914 return self
915
916 def __iand__(self, other):
917 self.intersection_update(other)
918 return self
919
920 def __ixor__(self, other):
921 self.symmetric_difference_update(other)
922 return self
923
924 def __isub__(self, other):
925 self.difference_update(other)
926 return self
927
928 def add(self, elem):
929 set.add(self, elem)
930 self.changed()
931
932 def remove(self, elem):
933 set.remove(self, elem)
934 self.changed()
935
936 def discard(self, elem):
937 set.discard(self, elem)
938 self.changed()
939
940 def pop(self, *arg):
941 result = set.pop(self, *arg)
942 self.changed()
943 return result
944
945 def clear(self):
946 set.clear(self)
947 self.changed()
948
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
958
959 def __getstate__(self):
960 return set(self)
961
962 def __setstate__(self, state):
963 self.update(state)
964
965 def __reduce_ex__(self, proto):
966 return (self.__class__, (list(self),))