1# ext/mutable.py
2# Copyright (C) 2005-2025 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
25 class JSONEncodedDict(TypeDecorator):
26 "Represents an immutable structure as a json-encoded string."
27
28 impl = VARCHAR
29
30 def process_bind_param(self, value, dialect):
31 if value is not None:
32 value = json.dumps(value)
33 return value
34
35 def process_result_value(self, value, dialect):
36 if value is not None:
37 value = json.loads(value)
38 return value
39
40The usage of ``json`` is only for the purposes of example. The
41:mod:`sqlalchemy.ext.mutable` extension can be used
42with any type whose target Python type may be mutable, including
43:class:`.PickleType`, :class:`_postgresql.ARRAY`, etc.
44
45When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself
46tracks all parents which reference it. Below, we illustrate a simple
47version of the :class:`.MutableDict` dictionary object, which applies
48the :class:`.Mutable` mixin to a plain Python dictionary::
49
50 from sqlalchemy.ext.mutable import Mutable
51
52
53 class MutableDict(Mutable, dict):
54 @classmethod
55 def coerce(cls, key, value):
56 "Convert plain dictionaries to MutableDict."
57
58 if not isinstance(value, MutableDict):
59 if isinstance(value, dict):
60 return MutableDict(value)
61
62 # this call will raise ValueError
63 return Mutable.coerce(key, value)
64 else:
65 return value
66
67 def __setitem__(self, key, value):
68 "Detect dictionary set events and emit change events."
69
70 dict.__setitem__(self, key, value)
71 self.changed()
72
73 def __delitem__(self, key):
74 "Detect dictionary del events and emit change events."
75
76 dict.__delitem__(self, key)
77 self.changed()
78
79The above dictionary class takes the approach of subclassing the Python
80built-in ``dict`` to produce a dict
81subclass which routes all mutation events through ``__setitem__``. There are
82variants on this approach, such as subclassing ``UserDict.UserDict`` or
83``collections.MutableMapping``; the part that's important to this example is
84that the :meth:`.Mutable.changed` method is called whenever an in-place
85change to the datastructure takes place.
86
87We also redefine the :meth:`.Mutable.coerce` method which will be used to
88convert any values that are not instances of ``MutableDict``, such
89as the plain dictionaries returned by the ``json`` module, into the
90appropriate type. Defining this method is optional; we could just as well
91created our ``JSONEncodedDict`` such that it always returns an instance
92of ``MutableDict``, and additionally ensured that all calling code
93uses ``MutableDict`` explicitly. When :meth:`.Mutable.coerce` is not
94overridden, any values applied to a parent object which are not instances
95of the mutable type will raise a ``ValueError``.
96
97Our new ``MutableDict`` type offers a class method
98:meth:`~.Mutable.as_mutable` which we can use within column metadata
99to associate with types. This method grabs the given type object or
100class and associates a listener that will detect all future mappings
101of this type, applying event listening instrumentation to the mapped
102attribute. Such as, with classical table metadata::
103
104 from sqlalchemy import Table, Column, Integer
105
106 my_data = Table(
107 "my_data",
108 metadata,
109 Column("id", Integer, primary_key=True),
110 Column("data", MutableDict.as_mutable(JSONEncodedDict)),
111 )
112
113Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict``
114(if the type object was not an instance already), which will intercept any
115attributes which are mapped against this type. Below we establish a simple
116mapping against the ``my_data`` table::
117
118 from sqlalchemy.orm import DeclarativeBase
119 from sqlalchemy.orm import Mapped
120 from sqlalchemy.orm import mapped_column
121
122
123 class Base(DeclarativeBase):
124 pass
125
126
127 class MyDataClass(Base):
128 __tablename__ = "my_data"
129 id: Mapped[int] = mapped_column(primary_key=True)
130 data: Mapped[dict[str, str]] = mapped_column(
131 MutableDict.as_mutable(JSONEncodedDict)
132 )
133
134The ``MyDataClass.data`` member will now be notified of in place changes
135to its value.
136
137Any in-place changes to the ``MyDataClass.data`` member
138will flag the attribute as "dirty" on the parent object::
139
140 >>> from sqlalchemy.orm import Session
141
142 >>> sess = Session(some_engine)
143 >>> m1 = MyDataClass(data={"value1": "foo"})
144 >>> sess.add(m1)
145 >>> sess.commit()
146
147 >>> m1.data["value1"] = "bar"
148 >>> assert m1 in sess.dirty
149 True
150
151The ``MutableDict`` can be associated with all future instances
152of ``JSONEncodedDict`` in one step, using
153:meth:`~.Mutable.associate_with`. This is similar to
154:meth:`~.Mutable.as_mutable` except it will intercept all occurrences
155of ``MutableDict`` in all mappings unconditionally, without
156the need to declare it individually::
157
158 from sqlalchemy.orm import DeclarativeBase
159 from sqlalchemy.orm import Mapped
160 from sqlalchemy.orm import mapped_column
161
162 MutableDict.associate_with(JSONEncodedDict)
163
164
165 class Base(DeclarativeBase):
166 pass
167
168
169 class MyDataClass(Base):
170 __tablename__ = "my_data"
171 id: Mapped[int] = mapped_column(primary_key=True)
172 data: Mapped[dict[str, str]] = mapped_column(JSONEncodedDict)
173
174Supporting Pickling
175--------------------
176
177The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the
178placement of a ``weakref.WeakKeyDictionary`` upon the value object, which
179stores a mapping of parent mapped objects keyed to the attribute name under
180which they are associated with this value. ``WeakKeyDictionary`` objects are
181not picklable, due to the fact that they contain weakrefs and function
182callbacks. In our case, this is a good thing, since if this dictionary were
183picklable, it could lead to an excessively large pickle size for our value
184objects that are pickled by themselves outside of the context of the parent.
185The developer responsibility here is only to provide a ``__getstate__`` method
186that excludes the :meth:`~MutableBase._parents` collection from the pickle
187stream::
188
189 class MyMutableType(Mutable):
190 def __getstate__(self):
191 d = self.__dict__.copy()
192 d.pop("_parents", None)
193 return d
194
195With our dictionary example, we need to return the contents of the dict itself
196(and also restore them on __setstate__)::
197
198 class MutableDict(Mutable, dict):
199 # ....
200
201 def __getstate__(self):
202 return dict(self)
203
204 def __setstate__(self, state):
205 self.update(state)
206
207In the case that our mutable value object is pickled as it is attached to one
208or more parent objects that are also part of the pickle, the :class:`.Mutable`
209mixin will re-establish the :attr:`.Mutable._parents` collection on each value
210object as the owning parents themselves are unpickled.
211
212Receiving Events
213----------------
214
215The :meth:`.AttributeEvents.modified` event handler may be used to receive
216an event when a mutable scalar emits a change event. This event handler
217is called when the :func:`.attributes.flag_modified` function is called
218from within the mutable extension::
219
220 from sqlalchemy.orm import DeclarativeBase
221 from sqlalchemy.orm import Mapped
222 from sqlalchemy.orm import mapped_column
223 from sqlalchemy import event
224
225
226 class Base(DeclarativeBase):
227 pass
228
229
230 class MyDataClass(Base):
231 __tablename__ = "my_data"
232 id: Mapped[int] = mapped_column(primary_key=True)
233 data: Mapped[dict[str, str]] = mapped_column(
234 MutableDict.as_mutable(JSONEncodedDict)
235 )
236
237
238 @event.listens_for(MyDataClass.data, "modified")
239 def modified_json(instance, initiator):
240 print("json value modified:", instance.data)
241
242.. _mutable_composites:
243
244Establishing Mutability on Composites
245=====================================
246
247Composites are a special ORM feature which allow a single scalar attribute to
248be assigned an object value which represents information "composed" from one
249or more columns from the underlying mapped table. The usual example is that of
250a geometric "point", and is introduced in :ref:`mapper_composite`.
251
252As is the case with :class:`.Mutable`, the user-defined composite class
253subclasses :class:`.MutableComposite` as a mixin, and detects and delivers
254change events to its parents via the :meth:`.MutableComposite.changed` method.
255In the case of a composite class, the detection is usually via the usage of the
256special Python method ``__setattr__()``. In the example below, we expand upon the ``Point``
257class introduced in :ref:`mapper_composite` to include
258:class:`.MutableComposite` in its bases and to route attribute set events via
259``__setattr__`` to the :meth:`.MutableComposite.changed` method::
260
261 import dataclasses
262 from sqlalchemy.ext.mutable import MutableComposite
263
264
265 @dataclasses.dataclass
266 class Point(MutableComposite):
267 x: int
268 y: int
269
270 def __setattr__(self, key, value):
271 "Intercept set events"
272
273 # set the attribute
274 object.__setattr__(self, key, value)
275
276 # alert all parents to the change
277 self.changed()
278
279The :class:`.MutableComposite` class makes use of class mapping events to
280automatically establish listeners for any usage of :func:`_orm.composite` that
281specifies our ``Point`` type. Below, when ``Point`` is mapped to the ``Vertex``
282class, listeners are established which will route change events from ``Point``
283objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes::
284
285 from sqlalchemy.orm import DeclarativeBase, Mapped
286 from sqlalchemy.orm import composite, mapped_column
287
288
289 class Base(DeclarativeBase):
290 pass
291
292
293 class Vertex(Base):
294 __tablename__ = "vertices"
295
296 id: Mapped[int] = mapped_column(primary_key=True)
297
298 start: Mapped[Point] = composite(
299 mapped_column("x1"), mapped_column("y1")
300 )
301 end: Mapped[Point] = composite(
302 mapped_column("x2"), mapped_column("y2")
303 )
304
305 def __repr__(self):
306 return f"Vertex(start={self.start}, end={self.end})"
307
308Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members
309will flag the attribute as "dirty" on the parent object:
310
311.. sourcecode:: python+sql
312
313 >>> from sqlalchemy.orm import Session
314 >>> sess = Session(engine)
315 >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15))
316 >>> sess.add(v1)
317 {sql}>>> sess.flush()
318 BEGIN (implicit)
319 INSERT INTO vertices (x1, y1, x2, y2) VALUES (?, ?, ?, ?)
320 [...] (3, 4, 12, 15)
321
322 {stop}>>> v1.end.x = 8
323 >>> assert v1 in sess.dirty
324 True
325 {sql}>>> sess.commit()
326 UPDATE vertices SET x2=? WHERE vertices.id = ?
327 [...] (8, 1)
328 COMMIT
329
330Coercing Mutable Composites
331---------------------------
332
333The :meth:`.MutableBase.coerce` method is also supported on composite types.
334In the case of :class:`.MutableComposite`, the :meth:`.MutableBase.coerce`
335method is only called for attribute set operations, not load operations.
336Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent
337to using a :func:`.validates` validation routine for all attributes which
338make use of the custom composite type::
339
340 @dataclasses.dataclass
341 class Point(MutableComposite):
342 # other Point methods
343 # ...
344
345 def coerce(cls, key, value):
346 if isinstance(value, tuple):
347 value = Point(*value)
348 elif not isinstance(value, Point):
349 raise ValueError("tuple or Point expected")
350 return value
351
352Supporting Pickling
353--------------------
354
355As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper
356class uses a ``weakref.WeakKeyDictionary`` available via the
357:meth:`MutableBase._parents` attribute which isn't picklable. If we need to
358pickle instances of ``Point`` or its owning class ``Vertex``, we at least need
359to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary.
360Below we define both a ``__getstate__`` and a ``__setstate__`` that package up
361the minimal form of our ``Point`` class::
362
363 @dataclasses.dataclass
364 class Point(MutableComposite):
365 # ...
366
367 def __getstate__(self):
368 return self.x, self.y
369
370 def __setstate__(self, state):
371 self.x, self.y = state
372
373As with :class:`.Mutable`, the :class:`.MutableComposite` augments the
374pickling process of the parent's object-relational state so that the
375:meth:`MutableBase._parents` collection is restored to all ``Point`` objects.
376
377""" # noqa: E501
378
379from __future__ import annotations
380
381from collections import defaultdict
382from typing import AbstractSet
383from typing import Any
384from typing import Dict
385from typing import Iterable
386from typing import List
387from typing import Optional
388from typing import overload
389from typing import Set
390from typing import SupportsIndex
391from typing import Tuple
392from typing import TYPE_CHECKING
393from typing import TypeVar
394from typing import Union
395import weakref
396from weakref import WeakKeyDictionary
397
398from .. import event
399from .. import inspect
400from .. import types
401from ..orm import Mapper
402from ..orm._typing import _ExternalEntityType
403from ..orm._typing import _O
404from ..orm._typing import _T
405from ..orm.attributes import AttributeEventToken
406from ..orm.attributes import flag_modified
407from ..orm.attributes import InstrumentedAttribute
408from ..orm.attributes import QueryableAttribute
409from ..orm.context import QueryContext
410from ..orm.decl_api import DeclarativeAttributeIntercept
411from ..orm.state import InstanceState
412from ..orm.unitofwork import UOWTransaction
413from ..sql._typing import _TypeEngineArgument
414from ..sql.base import SchemaEventTarget
415from ..sql.schema import Column
416from ..sql.type_api import TypeEngine
417from ..util import memoized_property
418
419_KT = TypeVar("_KT") # Key type.
420_VT = TypeVar("_VT") # Value type.
421
422
423class MutableBase:
424 """Common base class to :class:`.Mutable`
425 and :class:`.MutableComposite`.
426
427 """
428
429 @memoized_property
430 def _parents(self) -> WeakKeyDictionary[Any, Any]:
431 """Dictionary of parent object's :class:`.InstanceState`->attribute
432 name on the parent.
433
434 This attribute is a so-called "memoized" property. It initializes
435 itself with a new ``weakref.WeakKeyDictionary`` the first time
436 it is accessed, returning the same object upon subsequent access.
437
438 .. versionchanged:: 1.4 the :class:`.InstanceState` is now used
439 as the key in the weak dictionary rather than the instance
440 itself.
441
442 """
443
444 return weakref.WeakKeyDictionary()
445
446 @classmethod
447 def coerce(cls, key: str, value: Any) -> Optional[Any]:
448 """Given a value, coerce it into the target type.
449
450 Can be overridden by custom subclasses to coerce incoming
451 data into a particular type.
452
453 By default, raises ``ValueError``.
454
455 This method is called in different scenarios depending on if
456 the parent class is of type :class:`.Mutable` or of type
457 :class:`.MutableComposite`. In the case of the former, it is called
458 for both attribute-set operations as well as during ORM loading
459 operations. For the latter, it is only called during attribute-set
460 operations; the mechanics of the :func:`.composite` construct
461 handle coercion during load operations.
462
463
464 :param key: string name of the ORM-mapped attribute being set.
465 :param value: the incoming value.
466 :return: the method should return the coerced value, or raise
467 ``ValueError`` if the coercion cannot be completed.
468
469 """
470 if value is None:
471 return None
472 msg = "Attribute '%s' does not accept objects of type %s"
473 raise ValueError(msg % (key, type(value)))
474
475 @classmethod
476 def _get_listen_keys(cls, attribute: QueryableAttribute[Any]) -> Set[str]:
477 """Given a descriptor attribute, return a ``set()`` of the attribute
478 keys which indicate a change in the state of this attribute.
479
480 This is normally just ``set([attribute.key])``, but can be overridden
481 to provide for additional keys. E.g. a :class:`.MutableComposite`
482 augments this set with the attribute keys associated with the columns
483 that comprise the composite value.
484
485 This collection is consulted in the case of intercepting the
486 :meth:`.InstanceEvents.refresh` and
487 :meth:`.InstanceEvents.refresh_flush` events, which pass along a list
488 of attribute names that have been refreshed; the list is compared
489 against this set to determine if action needs to be taken.
490
491 """
492 return {attribute.key}
493
494 @classmethod
495 def _listen_on_attribute(
496 cls,
497 attribute: QueryableAttribute[Any],
498 coerce: bool,
499 parent_cls: _ExternalEntityType[Any],
500 ) -> None:
501 """Establish this type as a mutation listener for the given
502 mapped descriptor.
503
504 """
505 key = attribute.key
506 if parent_cls is not attribute.class_:
507 return
508
509 # rely on "propagate" here
510 parent_cls = attribute.class_
511
512 listen_keys = cls._get_listen_keys(attribute)
513
514 def load(state: InstanceState[_O], *args: Any) -> None:
515 """Listen for objects loaded or refreshed.
516
517 Wrap the target data member's value with
518 ``Mutable``.
519
520 """
521 val = state.dict.get(key, None)
522 if val is not None:
523 if coerce:
524 val = cls.coerce(key, val)
525 assert val is not None
526 state.dict[key] = val
527 val._parents[state] = key
528
529 def load_attrs(
530 state: InstanceState[_O],
531 ctx: Union[object, QueryContext, UOWTransaction],
532 attrs: Iterable[Any],
533 ) -> None:
534 if not attrs or listen_keys.intersection(attrs):
535 load(state)
536
537 def set_(
538 target: InstanceState[_O],
539 value: MutableBase | None,
540 oldvalue: MutableBase | None,
541 initiator: AttributeEventToken,
542 ) -> MutableBase | None:
543 """Listen for set/replace events on the target
544 data member.
545
546 Establish a weak reference to the parent object
547 on the incoming value, remove it for the one
548 outgoing.
549
550 """
551 if value is oldvalue:
552 return value
553
554 if not isinstance(value, cls):
555 value = cls.coerce(key, value)
556 if value is not None:
557 value._parents[target] = key
558 if isinstance(oldvalue, cls):
559 oldvalue._parents.pop(inspect(target), None)
560 return value
561
562 def pickle(
563 state: InstanceState[_O], state_dict: Dict[str, Any]
564 ) -> None:
565 val = state.dict.get(key, None)
566 if val is not None:
567 if "ext.mutable.values" not in state_dict:
568 state_dict["ext.mutable.values"] = defaultdict(list)
569 state_dict["ext.mutable.values"][key].append(val)
570
571 def unpickle(
572 state: InstanceState[_O], state_dict: Dict[str, Any]
573 ) -> None:
574 if "ext.mutable.values" in state_dict:
575 collection = state_dict["ext.mutable.values"]
576 if isinstance(collection, list):
577 # legacy format
578 for val in collection:
579 val._parents[state] = key
580 else:
581 for val in state_dict["ext.mutable.values"][key]:
582 val._parents[state] = key
583
584 event.listen(
585 parent_cls,
586 "_sa_event_merge_wo_load",
587 load,
588 raw=True,
589 propagate=True,
590 )
591
592 event.listen(parent_cls, "load", load, raw=True, propagate=True)
593 event.listen(
594 parent_cls, "refresh", load_attrs, raw=True, propagate=True
595 )
596 event.listen(
597 parent_cls, "refresh_flush", load_attrs, raw=True, propagate=True
598 )
599 event.listen(
600 attribute, "set", set_, raw=True, retval=True, propagate=True
601 )
602 event.listen(parent_cls, "pickle", pickle, raw=True, propagate=True)
603 event.listen(
604 parent_cls, "unpickle", unpickle, raw=True, propagate=True
605 )
606
607
608class Mutable(MutableBase):
609 """Mixin that defines transparent propagation of change
610 events to a parent object.
611
612 See the example in :ref:`mutable_scalars` for usage information.
613
614 """
615
616 def changed(self) -> None:
617 """Subclasses should call this method whenever change events occur."""
618
619 for parent, key in self._parents.items():
620 flag_modified(parent.obj(), key)
621
622 @classmethod
623 def associate_with_attribute(
624 cls, attribute: InstrumentedAttribute[_O]
625 ) -> None:
626 """Establish this type as a mutation listener for the given
627 mapped descriptor.
628
629 """
630 cls._listen_on_attribute(attribute, True, attribute.class_)
631
632 @classmethod
633 def associate_with(cls, sqltype: type) -> None:
634 """Associate this wrapper with all future mapped columns
635 of the given type.
636
637 This is a convenience method that calls
638 ``associate_with_attribute`` automatically.
639
640 .. warning::
641
642 The listeners established by this method are *global*
643 to all mappers, and are *not* garbage collected. Only use
644 :meth:`.associate_with` for types that are permanent to an
645 application, not with ad-hoc types else this will cause unbounded
646 growth in memory usage.
647
648 """
649
650 def listen_for_type(mapper: Mapper[_O], class_: type) -> None:
651 for prop in mapper.column_attrs:
652 if isinstance(prop.columns[0].type, sqltype):
653 cls.associate_with_attribute(getattr(class_, prop.key))
654
655 event.listen(Mapper, "mapper_configured", listen_for_type)
656
657 @classmethod
658 def as_mutable(cls, sqltype: _TypeEngineArgument[_T]) -> TypeEngine[_T]:
659 """Associate a SQL type with this mutable Python type.
660
661 This establishes listeners that will detect ORM mappings against
662 the given type, adding mutation event trackers to those mappings.
663
664 The type is returned, unconditionally as an instance, so that
665 :meth:`.as_mutable` can be used inline::
666
667 Table(
668 "mytable",
669 metadata,
670 Column("id", Integer, primary_key=True),
671 Column("data", MyMutableType.as_mutable(PickleType)),
672 )
673
674 Note that the returned type is always an instance, even if a class
675 is given, and that only columns which are declared specifically with
676 that type instance receive additional instrumentation.
677
678 To associate a particular mutable type with all occurrences of a
679 particular type, use the :meth:`.Mutable.associate_with` classmethod
680 of the particular :class:`.Mutable` subclass to establish a global
681 association.
682
683 .. warning::
684
685 The listeners established by this method are *global*
686 to all mappers, and are *not* garbage collected. Only use
687 :meth:`.as_mutable` for types that are permanent to an application,
688 not with ad-hoc types else this will cause unbounded growth
689 in memory usage.
690
691 """
692 sqltype = types.to_instance(sqltype)
693
694 # a SchemaType will be copied when the Column is copied,
695 # and we'll lose our ability to link that type back to the original.
696 # so track our original type w/ columns
697 if isinstance(sqltype, SchemaEventTarget):
698
699 @event.listens_for(sqltype, "before_parent_attach")
700 def _add_column_memo(
701 sqltyp: TypeEngine[Any],
702 parent: Column[_T],
703 ) -> None:
704 parent.info["_ext_mutable_orig_type"] = sqltyp
705
706 schema_event_check = True
707 else:
708 schema_event_check = False
709
710 def listen_for_type(
711 mapper: Mapper[_T],
712 class_: Union[DeclarativeAttributeIntercept, type],
713 ) -> None:
714 _APPLIED_KEY = "_ext_mutable_listener_applied"
715
716 for prop in mapper.column_attrs:
717 if (
718 # all Mutable types refer to a Column that's mapped,
719 # since this is the only kind of Core target the ORM can
720 # "mutate"
721 isinstance(prop.expression, Column)
722 and (
723 (
724 schema_event_check
725 and prop.expression.info.get(
726 "_ext_mutable_orig_type"
727 )
728 is sqltype
729 )
730 or prop.expression.type is sqltype
731 )
732 ):
733 if not prop.expression.info.get(_APPLIED_KEY, False):
734 prop.expression.info[_APPLIED_KEY] = True
735 cls.associate_with_attribute(getattr(class_, prop.key))
736
737 event.listen(Mapper, "mapper_configured", listen_for_type)
738
739 return sqltype
740
741
742class MutableComposite(MutableBase):
743 """Mixin that defines transparent propagation of change
744 events on a SQLAlchemy "composite" object to its
745 owning parent or parents.
746
747 See the example in :ref:`mutable_composites` for usage information.
748
749 """
750
751 @classmethod
752 def _get_listen_keys(cls, attribute: QueryableAttribute[_O]) -> Set[str]:
753 return {attribute.key}.union(attribute.property._attribute_keys)
754
755 def changed(self) -> None:
756 """Subclasses should call this method whenever change events occur."""
757
758 for parent, key in self._parents.items():
759 prop = parent.mapper.get_property(key)
760 for value, attr_name in zip(
761 prop._composite_values_from_instance(self),
762 prop._attribute_keys,
763 ):
764 setattr(parent.obj(), attr_name, value)
765
766
767def _setup_composite_listener() -> None:
768 def _listen_for_type(mapper: Mapper[_T], class_: type) -> None:
769 for prop in mapper.iterate_properties:
770 if (
771 hasattr(prop, "composite_class")
772 and isinstance(prop.composite_class, type)
773 and issubclass(prop.composite_class, MutableComposite)
774 ):
775 prop.composite_class._listen_on_attribute(
776 getattr(class_, prop.key), False, class_
777 )
778
779 if not event.contains(Mapper, "mapper_configured", _listen_for_type):
780 event.listen(Mapper, "mapper_configured", _listen_for_type)
781
782
783_setup_composite_listener()
784
785
786class MutableDict(Mutable, Dict[_KT, _VT]):
787 """A dictionary type that implements :class:`.Mutable`.
788
789 The :class:`.MutableDict` object implements a dictionary that will
790 emit change events to the underlying mapping when the contents of
791 the dictionary are altered, including when values are added or removed.
792
793 Note that :class:`.MutableDict` does **not** apply mutable tracking to the
794 *values themselves* inside the dictionary. Therefore it is not a sufficient
795 solution for the use case of tracking deep changes to a *recursive*
796 dictionary structure, such as a JSON structure. To support this use case,
797 build a subclass of :class:`.MutableDict` that provides appropriate
798 coercion to the values placed in the dictionary so that they too are
799 "mutable", and emit events up to their parent structure.
800
801 .. seealso::
802
803 :class:`.MutableList`
804
805 :class:`.MutableSet`
806
807 """
808
809 def __setitem__(self, key: _KT, value: _VT) -> None:
810 """Detect dictionary set events and emit change events."""
811 dict.__setitem__(self, key, value)
812 self.changed()
813
814 if TYPE_CHECKING:
815 # from https://github.com/python/mypy/issues/14858
816
817 @overload
818 def setdefault(
819 self: MutableDict[_KT, Optional[_T]], key: _KT, value: None = None
820 ) -> Optional[_T]: ...
821
822 @overload
823 def setdefault(self, key: _KT, value: _VT) -> _VT: ...
824
825 def setdefault(self, key: _KT, value: object = None) -> object: ...
826
827 else:
828
829 def setdefault(self, *arg): # noqa: F811
830 result = dict.setdefault(self, *arg)
831 self.changed()
832 return result
833
834 def __delitem__(self, key: _KT) -> None:
835 """Detect dictionary del events and emit change events."""
836 dict.__delitem__(self, key)
837 self.changed()
838
839 def update(self, *a: Any, **kw: _VT) -> None:
840 dict.update(self, *a, **kw)
841 self.changed()
842
843 if TYPE_CHECKING:
844
845 @overload
846 def pop(self, __key: _KT, /) -> _VT: ...
847
848 @overload
849 def pop(self, __key: _KT, default: _VT | _T, /) -> _VT | _T: ...
850
851 def pop(
852 self, __key: _KT, __default: _VT | _T | None = None, /
853 ) -> _VT | _T: ...
854
855 else:
856
857 def pop(self, *arg): # noqa: F811
858 result = dict.pop(self, *arg)
859 self.changed()
860 return result
861
862 def popitem(self) -> Tuple[_KT, _VT]:
863 result = dict.popitem(self)
864 self.changed()
865 return result
866
867 def clear(self) -> None:
868 dict.clear(self)
869 self.changed()
870
871 @classmethod
872 def coerce(cls, key: str, value: Any) -> MutableDict[_KT, _VT] | None:
873 """Convert plain dictionary to instance of this class."""
874 if not isinstance(value, cls):
875 if isinstance(value, dict):
876 return cls(value)
877 return Mutable.coerce(key, value)
878 else:
879 return value
880
881 def __getstate__(self) -> Dict[_KT, _VT]:
882 return dict(self)
883
884 def __setstate__(
885 self, state: Union[Dict[str, int], Dict[str, str]]
886 ) -> None:
887 self.update(state)
888
889
890class MutableList(Mutable, List[_T]):
891 """A list type that implements :class:`.Mutable`.
892
893 The :class:`.MutableList` object implements a list that will
894 emit change events to the underlying mapping when the contents of
895 the list are altered, including when values are added or removed.
896
897 Note that :class:`.MutableList` does **not** apply mutable tracking to the
898 *values themselves* inside the list. Therefore it is not a sufficient
899 solution for the use case of tracking deep changes to a *recursive*
900 mutable structure, such as a JSON structure. To support this use case,
901 build a subclass of :class:`.MutableList` that provides appropriate
902 coercion to the values placed in the dictionary so that they too are
903 "mutable", and emit events up to their parent structure.
904
905 .. seealso::
906
907 :class:`.MutableDict`
908
909 :class:`.MutableSet`
910
911 """
912
913 def __reduce_ex__(
914 self, proto: SupportsIndex
915 ) -> Tuple[type, Tuple[List[int]]]:
916 return (self.__class__, (list(self),))
917
918 # needed for backwards compatibility with
919 # older pickles
920 def __setstate__(self, state: Iterable[_T]) -> None:
921 self[:] = state
922
923 def __setitem__(
924 self, index: SupportsIndex | slice, value: _T | Iterable[_T]
925 ) -> None:
926 """Detect list set events and emit change events."""
927 list.__setitem__(self, index, value)
928 self.changed()
929
930 def __delitem__(self, index: SupportsIndex | slice) -> None:
931 """Detect list del events and emit change events."""
932 list.__delitem__(self, index)
933 self.changed()
934
935 def pop(self, *arg: SupportsIndex) -> _T:
936 result = list.pop(self, *arg)
937 self.changed()
938 return result
939
940 def append(self, x: _T) -> None:
941 list.append(self, x)
942 self.changed()
943
944 def extend(self, x: Iterable[_T]) -> None:
945 list.extend(self, x)
946 self.changed()
947
948 def __iadd__(self, x: Iterable[_T]) -> MutableList[_T]: # type: ignore[override,misc] # noqa: E501
949 self.extend(x)
950 return self
951
952 def insert(self, i: SupportsIndex, x: _T) -> None:
953 list.insert(self, i, x)
954 self.changed()
955
956 def remove(self, i: _T) -> None:
957 list.remove(self, i)
958 self.changed()
959
960 def clear(self) -> None:
961 list.clear(self)
962 self.changed()
963
964 def sort(self, **kw: Any) -> None:
965 list.sort(self, **kw)
966 self.changed()
967
968 def reverse(self) -> None:
969 list.reverse(self)
970 self.changed()
971
972 @classmethod
973 def coerce(
974 cls, key: str, value: MutableList[_T] | _T
975 ) -> Optional[MutableList[_T]]:
976 """Convert plain list to instance of this class."""
977 if not isinstance(value, cls):
978 if isinstance(value, list):
979 return cls(value)
980 return Mutable.coerce(key, value)
981 else:
982 return value
983
984
985class MutableSet(Mutable, Set[_T]):
986 """A set type that implements :class:`.Mutable`.
987
988 The :class:`.MutableSet` object implements a set that will
989 emit change events to the underlying mapping when the contents of
990 the set are altered, including when values are added or removed.
991
992 Note that :class:`.MutableSet` does **not** apply mutable tracking to the
993 *values themselves* inside the set. Therefore it is not a sufficient
994 solution for the use case of tracking deep changes to a *recursive*
995 mutable structure. To support this use case,
996 build a subclass of :class:`.MutableSet` that provides appropriate
997 coercion to the values placed in the dictionary so that they too are
998 "mutable", and emit events up to their parent structure.
999
1000 .. seealso::
1001
1002 :class:`.MutableDict`
1003
1004 :class:`.MutableList`
1005
1006
1007 """
1008
1009 def update(self, *arg: Iterable[_T]) -> None:
1010 set.update(self, *arg)
1011 self.changed()
1012
1013 def intersection_update(self, *arg: Iterable[Any]) -> None:
1014 set.intersection_update(self, *arg)
1015 self.changed()
1016
1017 def difference_update(self, *arg: Iterable[Any]) -> None:
1018 set.difference_update(self, *arg)
1019 self.changed()
1020
1021 def symmetric_difference_update(self, *arg: Iterable[_T]) -> None:
1022 set.symmetric_difference_update(self, *arg)
1023 self.changed()
1024
1025 def __ior__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501
1026 self.update(other)
1027 return self
1028
1029 def __iand__(self, other: AbstractSet[object]) -> MutableSet[_T]:
1030 self.intersection_update(other)
1031 return self
1032
1033 def __ixor__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501
1034 self.symmetric_difference_update(other)
1035 return self
1036
1037 def __isub__(self, other: AbstractSet[object]) -> MutableSet[_T]: # type: ignore[misc] # noqa: E501
1038 self.difference_update(other)
1039 return self
1040
1041 def add(self, elem: _T) -> None:
1042 set.add(self, elem)
1043 self.changed()
1044
1045 def remove(self, elem: _T) -> None:
1046 set.remove(self, elem)
1047 self.changed()
1048
1049 def discard(self, elem: _T) -> None:
1050 set.discard(self, elem)
1051 self.changed()
1052
1053 def pop(self, *arg: Any) -> _T:
1054 result = set.pop(self, *arg)
1055 self.changed()
1056 return result
1057
1058 def clear(self) -> None:
1059 set.clear(self)
1060 self.changed()
1061
1062 @classmethod
1063 def coerce(cls, index: str, value: Any) -> Optional[MutableSet[_T]]:
1064 """Convert plain set to instance of this class."""
1065 if not isinstance(value, cls):
1066 if isinstance(value, set):
1067 return cls(value)
1068 return Mutable.coerce(index, value)
1069 else:
1070 return value
1071
1072 def __getstate__(self) -> Set[_T]:
1073 return set(self)
1074
1075 def __setstate__(self, state: Iterable[_T]) -> None:
1076 self.update(state)
1077
1078 def __reduce_ex__(
1079 self, proto: SupportsIndex
1080 ) -> Tuple[type, Tuple[List[int]]]:
1081 return (self.__class__, (list(self),))