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 Tuple
391from typing import TYPE_CHECKING
392from typing import TypeVar
393from typing import Union
394import weakref
395from weakref import WeakKeyDictionary
396
397from .. import event
398from .. import inspect
399from .. import types
400from ..orm import Mapper
401from ..orm._typing import _ExternalEntityType
402from ..orm._typing import _O
403from ..orm._typing import _T
404from ..orm.attributes import AttributeEventToken
405from ..orm.attributes import flag_modified
406from ..orm.attributes import InstrumentedAttribute
407from ..orm.attributes import QueryableAttribute
408from ..orm.context import QueryContext
409from ..orm.decl_api import DeclarativeAttributeIntercept
410from ..orm.state import InstanceState
411from ..orm.unitofwork import UOWTransaction
412from ..sql._typing import _TypeEngineArgument
413from ..sql.base import SchemaEventTarget
414from ..sql.schema import Column
415from ..sql.type_api import TypeEngine
416from ..util import memoized_property
417from ..util.typing import SupportsIndex
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 if mapper.non_primary:
652 return
653 for prop in mapper.column_attrs:
654 if isinstance(prop.columns[0].type, sqltype):
655 cls.associate_with_attribute(getattr(class_, prop.key))
656
657 event.listen(Mapper, "mapper_configured", listen_for_type)
658
659 @classmethod
660 def as_mutable(cls, sqltype: _TypeEngineArgument[_T]) -> TypeEngine[_T]:
661 """Associate a SQL type with this mutable Python type.
662
663 This establishes listeners that will detect ORM mappings against
664 the given type, adding mutation event trackers to those mappings.
665
666 The type is returned, unconditionally as an instance, so that
667 :meth:`.as_mutable` can be used inline::
668
669 Table(
670 "mytable",
671 metadata,
672 Column("id", Integer, primary_key=True),
673 Column("data", MyMutableType.as_mutable(PickleType)),
674 )
675
676 Note that the returned type is always an instance, even if a class
677 is given, and that only columns which are declared specifically with
678 that type instance receive additional instrumentation.
679
680 To associate a particular mutable type with all occurrences of a
681 particular type, use the :meth:`.Mutable.associate_with` classmethod
682 of the particular :class:`.Mutable` subclass to establish a global
683 association.
684
685 .. warning::
686
687 The listeners established by this method are *global*
688 to all mappers, and are *not* garbage collected. Only use
689 :meth:`.as_mutable` for types that are permanent to an application,
690 not with ad-hoc types else this will cause unbounded growth
691 in memory usage.
692
693 """
694 sqltype = types.to_instance(sqltype)
695
696 # a SchemaType will be copied when the Column is copied,
697 # and we'll lose our ability to link that type back to the original.
698 # so track our original type w/ columns
699 if isinstance(sqltype, SchemaEventTarget):
700
701 @event.listens_for(sqltype, "before_parent_attach")
702 def _add_column_memo(
703 sqltyp: TypeEngine[Any],
704 parent: Column[_T],
705 ) -> None:
706 parent.info["_ext_mutable_orig_type"] = sqltyp
707
708 schema_event_check = True
709 else:
710 schema_event_check = False
711
712 def listen_for_type(
713 mapper: Mapper[_T],
714 class_: Union[DeclarativeAttributeIntercept, type],
715 ) -> None:
716 if mapper.non_primary:
717 return
718 _APPLIED_KEY = "_ext_mutable_listener_applied"
719
720 for prop in mapper.column_attrs:
721 if (
722 # all Mutable types refer to a Column that's mapped,
723 # since this is the only kind of Core target the ORM can
724 # "mutate"
725 isinstance(prop.expression, Column)
726 and (
727 (
728 schema_event_check
729 and prop.expression.info.get(
730 "_ext_mutable_orig_type"
731 )
732 is sqltype
733 )
734 or prop.expression.type is sqltype
735 )
736 ):
737 if not prop.expression.info.get(_APPLIED_KEY, False):
738 prop.expression.info[_APPLIED_KEY] = True
739 cls.associate_with_attribute(getattr(class_, prop.key))
740
741 event.listen(Mapper, "mapper_configured", listen_for_type)
742
743 return sqltype
744
745
746class MutableComposite(MutableBase):
747 """Mixin that defines transparent propagation of change
748 events on a SQLAlchemy "composite" object to its
749 owning parent or parents.
750
751 See the example in :ref:`mutable_composites` for usage information.
752
753 """
754
755 @classmethod
756 def _get_listen_keys(cls, attribute: QueryableAttribute[_O]) -> Set[str]:
757 return {attribute.key}.union(attribute.property._attribute_keys)
758
759 def changed(self) -> None:
760 """Subclasses should call this method whenever change events occur."""
761
762 for parent, key in self._parents.items():
763 prop = parent.mapper.get_property(key)
764 for value, attr_name in zip(
765 prop._composite_values_from_instance(self),
766 prop._attribute_keys,
767 ):
768 setattr(parent.obj(), attr_name, value)
769
770
771def _setup_composite_listener() -> None:
772 def _listen_for_type(mapper: Mapper[_T], class_: type) -> None:
773 for prop in mapper.iterate_properties:
774 if (
775 hasattr(prop, "composite_class")
776 and isinstance(prop.composite_class, type)
777 and issubclass(prop.composite_class, MutableComposite)
778 ):
779 prop.composite_class._listen_on_attribute(
780 getattr(class_, prop.key), False, class_
781 )
782
783 if not event.contains(Mapper, "mapper_configured", _listen_for_type):
784 event.listen(Mapper, "mapper_configured", _listen_for_type)
785
786
787_setup_composite_listener()
788
789
790class MutableDict(Mutable, Dict[_KT, _VT]):
791 """A dictionary type that implements :class:`.Mutable`.
792
793 The :class:`.MutableDict` object implements a dictionary that will
794 emit change events to the underlying mapping when the contents of
795 the dictionary are altered, including when values are added or removed.
796
797 Note that :class:`.MutableDict` does **not** apply mutable tracking to the
798 *values themselves* inside the dictionary. Therefore it is not a sufficient
799 solution for the use case of tracking deep changes to a *recursive*
800 dictionary structure, such as a JSON structure. To support this use case,
801 build a subclass of :class:`.MutableDict` that provides appropriate
802 coercion to the values placed in the dictionary so that they too are
803 "mutable", and emit events up to their parent structure.
804
805 .. seealso::
806
807 :class:`.MutableList`
808
809 :class:`.MutableSet`
810
811 """
812
813 def __setitem__(self, key: _KT, value: _VT) -> None:
814 """Detect dictionary set events and emit change events."""
815 dict.__setitem__(self, key, value)
816 self.changed()
817
818 if TYPE_CHECKING:
819 # from https://github.com/python/mypy/issues/14858
820
821 @overload
822 def setdefault(
823 self: MutableDict[_KT, Optional[_T]], key: _KT, value: None = None
824 ) -> Optional[_T]: ...
825
826 @overload
827 def setdefault(self, key: _KT, value: _VT) -> _VT: ...
828
829 def setdefault(self, key: _KT, value: object = None) -> object: ...
830
831 else:
832
833 def setdefault(self, *arg): # noqa: F811
834 result = dict.setdefault(self, *arg)
835 self.changed()
836 return result
837
838 def __delitem__(self, key: _KT) -> None:
839 """Detect dictionary del events and emit change events."""
840 dict.__delitem__(self, key)
841 self.changed()
842
843 def update(self, *a: Any, **kw: _VT) -> None:
844 dict.update(self, *a, **kw)
845 self.changed()
846
847 if TYPE_CHECKING:
848
849 @overload
850 def pop(self, __key: _KT) -> _VT: ...
851
852 @overload
853 def pop(self, __key: _KT, __default: _VT | _T) -> _VT | _T: ...
854
855 def pop(
856 self, __key: _KT, __default: _VT | _T | None = None
857 ) -> _VT | _T: ...
858
859 else:
860
861 def pop(self, *arg): # noqa: F811
862 result = dict.pop(self, *arg)
863 self.changed()
864 return result
865
866 def popitem(self) -> Tuple[_KT, _VT]:
867 result = dict.popitem(self)
868 self.changed()
869 return result
870
871 def clear(self) -> None:
872 dict.clear(self)
873 self.changed()
874
875 @classmethod
876 def coerce(cls, key: str, value: Any) -> MutableDict[_KT, _VT] | None:
877 """Convert plain dictionary to instance of this class."""
878 if not isinstance(value, cls):
879 if isinstance(value, dict):
880 return cls(value)
881 return Mutable.coerce(key, value)
882 else:
883 return value
884
885 def __getstate__(self) -> Dict[_KT, _VT]:
886 return dict(self)
887
888 def __setstate__(
889 self, state: Union[Dict[str, int], Dict[str, str]]
890 ) -> None:
891 self.update(state)
892
893
894class MutableList(Mutable, List[_T]):
895 """A list type that implements :class:`.Mutable`.
896
897 The :class:`.MutableList` object implements a list that will
898 emit change events to the underlying mapping when the contents of
899 the list are altered, including when values are added or removed.
900
901 Note that :class:`.MutableList` does **not** apply mutable tracking to the
902 *values themselves* inside the list. Therefore it is not a sufficient
903 solution for the use case of tracking deep changes to a *recursive*
904 mutable structure, such as a JSON structure. To support this use case,
905 build a subclass of :class:`.MutableList` that provides appropriate
906 coercion to the values placed in the dictionary so that they too are
907 "mutable", and emit events up to their parent structure.
908
909 .. seealso::
910
911 :class:`.MutableDict`
912
913 :class:`.MutableSet`
914
915 """
916
917 def __reduce_ex__(
918 self, proto: SupportsIndex
919 ) -> Tuple[type, Tuple[List[int]]]:
920 return (self.__class__, (list(self),))
921
922 # needed for backwards compatibility with
923 # older pickles
924 def __setstate__(self, state: Iterable[_T]) -> None:
925 self[:] = state
926
927 def __setitem__(
928 self, index: SupportsIndex | slice, value: _T | Iterable[_T]
929 ) -> None:
930 """Detect list set events and emit change events."""
931 list.__setitem__(self, index, value)
932 self.changed()
933
934 def __delitem__(self, index: SupportsIndex | slice) -> None:
935 """Detect list del events and emit change events."""
936 list.__delitem__(self, index)
937 self.changed()
938
939 def pop(self, *arg: SupportsIndex) -> _T:
940 result = list.pop(self, *arg)
941 self.changed()
942 return result
943
944 def append(self, x: _T) -> None:
945 list.append(self, x)
946 self.changed()
947
948 def extend(self, x: Iterable[_T]) -> None:
949 list.extend(self, x)
950 self.changed()
951
952 def __iadd__(self, x: Iterable[_T]) -> MutableList[_T]: # type: ignore[override,misc] # noqa: E501
953 self.extend(x)
954 return self
955
956 def insert(self, i: SupportsIndex, x: _T) -> None:
957 list.insert(self, i, x)
958 self.changed()
959
960 def remove(self, i: _T) -> None:
961 list.remove(self, i)
962 self.changed()
963
964 def clear(self) -> None:
965 list.clear(self)
966 self.changed()
967
968 def sort(self, **kw: Any) -> None:
969 list.sort(self, **kw)
970 self.changed()
971
972 def reverse(self) -> None:
973 list.reverse(self)
974 self.changed()
975
976 @classmethod
977 def coerce(
978 cls, key: str, value: MutableList[_T] | _T
979 ) -> Optional[MutableList[_T]]:
980 """Convert plain list to instance of this class."""
981 if not isinstance(value, cls):
982 if isinstance(value, list):
983 return cls(value)
984 return Mutable.coerce(key, value)
985 else:
986 return value
987
988
989class MutableSet(Mutable, Set[_T]):
990 """A set type that implements :class:`.Mutable`.
991
992 The :class:`.MutableSet` object implements a set that will
993 emit change events to the underlying mapping when the contents of
994 the set are altered, including when values are added or removed.
995
996 Note that :class:`.MutableSet` does **not** apply mutable tracking to the
997 *values themselves* inside the set. Therefore it is not a sufficient
998 solution for the use case of tracking deep changes to a *recursive*
999 mutable structure. To support this use case,
1000 build a subclass of :class:`.MutableSet` that provides appropriate
1001 coercion to the values placed in the dictionary so that they too are
1002 "mutable", and emit events up to their parent structure.
1003
1004 .. seealso::
1005
1006 :class:`.MutableDict`
1007
1008 :class:`.MutableList`
1009
1010
1011 """
1012
1013 def update(self, *arg: Iterable[_T]) -> None:
1014 set.update(self, *arg)
1015 self.changed()
1016
1017 def intersection_update(self, *arg: Iterable[Any]) -> None:
1018 set.intersection_update(self, *arg)
1019 self.changed()
1020
1021 def difference_update(self, *arg: Iterable[Any]) -> None:
1022 set.difference_update(self, *arg)
1023 self.changed()
1024
1025 def symmetric_difference_update(self, *arg: Iterable[_T]) -> None:
1026 set.symmetric_difference_update(self, *arg)
1027 self.changed()
1028
1029 def __ior__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501
1030 self.update(other)
1031 return self
1032
1033 def __iand__(self, other: AbstractSet[object]) -> MutableSet[_T]:
1034 self.intersection_update(other)
1035 return self
1036
1037 def __ixor__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501
1038 self.symmetric_difference_update(other)
1039 return self
1040
1041 def __isub__(self, other: AbstractSet[object]) -> MutableSet[_T]: # type: ignore[misc] # noqa: E501
1042 self.difference_update(other)
1043 return self
1044
1045 def add(self, elem: _T) -> None:
1046 set.add(self, elem)
1047 self.changed()
1048
1049 def remove(self, elem: _T) -> None:
1050 set.remove(self, elem)
1051 self.changed()
1052
1053 def discard(self, elem: _T) -> None:
1054 set.discard(self, elem)
1055 self.changed()
1056
1057 def pop(self, *arg: Any) -> _T:
1058 result = set.pop(self, *arg)
1059 self.changed()
1060 return result
1061
1062 def clear(self) -> None:
1063 set.clear(self)
1064 self.changed()
1065
1066 @classmethod
1067 def coerce(cls, index: str, value: Any) -> Optional[MutableSet[_T]]:
1068 """Convert plain set to instance of this class."""
1069 if not isinstance(value, cls):
1070 if isinstance(value, set):
1071 return cls(value)
1072 return Mutable.coerce(index, value)
1073 else:
1074 return value
1075
1076 def __getstate__(self) -> Set[_T]:
1077 return set(self)
1078
1079 def __setstate__(self, state: Iterable[_T]) -> None:
1080 self.update(state)
1081
1082 def __reduce_ex__(
1083 self, proto: SupportsIndex
1084 ) -> Tuple[type, Tuple[List[int]]]:
1085 return (self.__class__, (list(self),))