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 .. import util
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
418from ..util.typing import SupportsIndex
419from ..util.typing import TypeGuard
420
421_KT = TypeVar("_KT") # Key type.
422_VT = TypeVar("_VT") # Value type.
423
424
425class MutableBase:
426 """Common base class to :class:`.Mutable`
427 and :class:`.MutableComposite`.
428
429 """
430
431 @memoized_property
432 def _parents(self) -> WeakKeyDictionary[Any, Any]:
433 """Dictionary of parent object's :class:`.InstanceState`->attribute
434 name on the parent.
435
436 This attribute is a so-called "memoized" property. It initializes
437 itself with a new ``weakref.WeakKeyDictionary`` the first time
438 it is accessed, returning the same object upon subsequent access.
439
440 .. versionchanged:: 1.4 the :class:`.InstanceState` is now used
441 as the key in the weak dictionary rather than the instance
442 itself.
443
444 """
445
446 return weakref.WeakKeyDictionary()
447
448 @classmethod
449 def coerce(cls, key: str, value: Any) -> Optional[Any]:
450 """Given a value, coerce it into the target type.
451
452 Can be overridden by custom subclasses to coerce incoming
453 data into a particular type.
454
455 By default, raises ``ValueError``.
456
457 This method is called in different scenarios depending on if
458 the parent class is of type :class:`.Mutable` or of type
459 :class:`.MutableComposite`. In the case of the former, it is called
460 for both attribute-set operations as well as during ORM loading
461 operations. For the latter, it is only called during attribute-set
462 operations; the mechanics of the :func:`.composite` construct
463 handle coercion during load operations.
464
465
466 :param key: string name of the ORM-mapped attribute being set.
467 :param value: the incoming value.
468 :return: the method should return the coerced value, or raise
469 ``ValueError`` if the coercion cannot be completed.
470
471 """
472 if value is None:
473 return None
474 msg = "Attribute '%s' does not accept objects of type %s"
475 raise ValueError(msg % (key, type(value)))
476
477 @classmethod
478 def _get_listen_keys(cls, attribute: QueryableAttribute[Any]) -> Set[str]:
479 """Given a descriptor attribute, return a ``set()`` of the attribute
480 keys which indicate a change in the state of this attribute.
481
482 This is normally just ``set([attribute.key])``, but can be overridden
483 to provide for additional keys. E.g. a :class:`.MutableComposite`
484 augments this set with the attribute keys associated with the columns
485 that comprise the composite value.
486
487 This collection is consulted in the case of intercepting the
488 :meth:`.InstanceEvents.refresh` and
489 :meth:`.InstanceEvents.refresh_flush` events, which pass along a list
490 of attribute names that have been refreshed; the list is compared
491 against this set to determine if action needs to be taken.
492
493 """
494 return {attribute.key}
495
496 @classmethod
497 def _listen_on_attribute(
498 cls,
499 attribute: QueryableAttribute[Any],
500 coerce: bool,
501 parent_cls: _ExternalEntityType[Any],
502 ) -> None:
503 """Establish this type as a mutation listener for the given
504 mapped descriptor.
505
506 """
507 key = attribute.key
508 if parent_cls is not attribute.class_:
509 return
510
511 # rely on "propagate" here
512 parent_cls = attribute.class_
513
514 listen_keys = cls._get_listen_keys(attribute)
515
516 def load(state: InstanceState[_O], *args: Any) -> None:
517 """Listen for objects loaded or refreshed.
518
519 Wrap the target data member's value with
520 ``Mutable``.
521
522 """
523 val = state.dict.get(key, None)
524 if val is not None:
525 if coerce:
526 val = cls.coerce(key, val)
527 state.dict[key] = val
528 val._parents[state] = key
529
530 def load_attrs(
531 state: InstanceState[_O],
532 ctx: Union[object, QueryContext, UOWTransaction],
533 attrs: Iterable[Any],
534 ) -> None:
535 if not attrs or listen_keys.intersection(attrs):
536 load(state)
537
538 def set_(
539 target: InstanceState[_O],
540 value: MutableBase | None,
541 oldvalue: MutableBase | None,
542 initiator: AttributeEventToken,
543 ) -> MutableBase | None:
544 """Listen for set/replace events on the target
545 data member.
546
547 Establish a weak reference to the parent object
548 on the incoming value, remove it for the one
549 outgoing.
550
551 """
552 if value is oldvalue:
553 return value
554
555 if not isinstance(value, cls):
556 value = cls.coerce(key, value)
557 if value is not None:
558 value._parents[target] = key
559 if isinstance(oldvalue, cls):
560 oldvalue._parents.pop(inspect(target), None)
561 return value
562
563 def pickle(
564 state: InstanceState[_O], state_dict: Dict[str, Any]
565 ) -> None:
566 val = state.dict.get(key, None)
567 if val is not None:
568 if "ext.mutable.values" not in state_dict:
569 state_dict["ext.mutable.values"] = defaultdict(list)
570 state_dict["ext.mutable.values"][key].append(val)
571
572 def unpickle(
573 state: InstanceState[_O], state_dict: Dict[str, Any]
574 ) -> None:
575 if "ext.mutable.values" in state_dict:
576 collection = state_dict["ext.mutable.values"]
577 if isinstance(collection, list):
578 # legacy format
579 for val in collection:
580 val._parents[state] = key
581 else:
582 for val in state_dict["ext.mutable.values"][key]:
583 val._parents[state] = key
584
585 event.listen(
586 parent_cls,
587 "_sa_event_merge_wo_load",
588 load,
589 raw=True,
590 propagate=True,
591 )
592
593 event.listen(parent_cls, "load", load, raw=True, propagate=True)
594 event.listen(
595 parent_cls, "refresh", load_attrs, raw=True, propagate=True
596 )
597 event.listen(
598 parent_cls, "refresh_flush", load_attrs, raw=True, propagate=True
599 )
600 event.listen(
601 attribute, "set", set_, raw=True, retval=True, propagate=True
602 )
603 event.listen(parent_cls, "pickle", pickle, raw=True, propagate=True)
604 event.listen(
605 parent_cls, "unpickle", unpickle, raw=True, propagate=True
606 )
607
608
609class Mutable(MutableBase):
610 """Mixin that defines transparent propagation of change
611 events to a parent object.
612
613 See the example in :ref:`mutable_scalars` for usage information.
614
615 """
616
617 def changed(self) -> None:
618 """Subclasses should call this method whenever change events occur."""
619
620 for parent, key in self._parents.items():
621 flag_modified(parent.obj(), key)
622
623 @classmethod
624 def associate_with_attribute(
625 cls, attribute: InstrumentedAttribute[_O]
626 ) -> None:
627 """Establish this type as a mutation listener for the given
628 mapped descriptor.
629
630 """
631 cls._listen_on_attribute(attribute, True, attribute.class_)
632
633 @classmethod
634 def associate_with(cls, sqltype: type) -> None:
635 """Associate this wrapper with all future mapped columns
636 of the given type.
637
638 This is a convenience method that calls
639 ``associate_with_attribute`` automatically.
640
641 .. warning::
642
643 The listeners established by this method are *global*
644 to all mappers, and are *not* garbage collected. Only use
645 :meth:`.associate_with` for types that are permanent to an
646 application, not with ad-hoc types else this will cause unbounded
647 growth in memory usage.
648
649 """
650
651 def listen_for_type(mapper: Mapper[_O], class_: type) -> None:
652 if mapper.non_primary:
653 return
654 for prop in mapper.column_attrs:
655 if isinstance(prop.columns[0].type, sqltype):
656 cls.associate_with_attribute(getattr(class_, prop.key))
657
658 event.listen(Mapper, "mapper_configured", listen_for_type)
659
660 @classmethod
661 def as_mutable(cls, sqltype: _TypeEngineArgument[_T]) -> TypeEngine[_T]:
662 """Associate a SQL type with this mutable Python type.
663
664 This establishes listeners that will detect ORM mappings against
665 the given type, adding mutation event trackers to those mappings.
666
667 The type is returned, unconditionally as an instance, so that
668 :meth:`.as_mutable` can be used inline::
669
670 Table(
671 "mytable",
672 metadata,
673 Column("id", Integer, primary_key=True),
674 Column("data", MyMutableType.as_mutable(PickleType)),
675 )
676
677 Note that the returned type is always an instance, even if a class
678 is given, and that only columns which are declared specifically with
679 that type instance receive additional instrumentation.
680
681 To associate a particular mutable type with all occurrences of a
682 particular type, use the :meth:`.Mutable.associate_with` classmethod
683 of the particular :class:`.Mutable` subclass to establish a global
684 association.
685
686 .. warning::
687
688 The listeners established by this method are *global*
689 to all mappers, and are *not* garbage collected. Only use
690 :meth:`.as_mutable` for types that are permanent to an application,
691 not with ad-hoc types else this will cause unbounded growth
692 in memory usage.
693
694 """
695 sqltype = types.to_instance(sqltype)
696
697 # a SchemaType will be copied when the Column is copied,
698 # and we'll lose our ability to link that type back to the original.
699 # so track our original type w/ columns
700 if isinstance(sqltype, SchemaEventTarget):
701
702 @event.listens_for(sqltype, "before_parent_attach")
703 def _add_column_memo(
704 sqltyp: TypeEngine[Any],
705 parent: Column[_T],
706 ) -> None:
707 parent.info["_ext_mutable_orig_type"] = sqltyp
708
709 schema_event_check = True
710 else:
711 schema_event_check = False
712
713 def listen_for_type(
714 mapper: Mapper[_T],
715 class_: Union[DeclarativeAttributeIntercept, type],
716 ) -> None:
717 if mapper.non_primary:
718 return
719 _APPLIED_KEY = "_ext_mutable_listener_applied"
720
721 for prop in mapper.column_attrs:
722 if (
723 # all Mutable types refer to a Column that's mapped,
724 # since this is the only kind of Core target the ORM can
725 # "mutate"
726 isinstance(prop.expression, Column)
727 and (
728 (
729 schema_event_check
730 and prop.expression.info.get(
731 "_ext_mutable_orig_type"
732 )
733 is sqltype
734 )
735 or prop.expression.type is sqltype
736 )
737 ):
738 if not prop.expression.info.get(_APPLIED_KEY, False):
739 prop.expression.info[_APPLIED_KEY] = True
740 cls.associate_with_attribute(getattr(class_, prop.key))
741
742 event.listen(Mapper, "mapper_configured", listen_for_type)
743
744 return sqltype
745
746
747class MutableComposite(MutableBase):
748 """Mixin that defines transparent propagation of change
749 events on a SQLAlchemy "composite" object to its
750 owning parent or parents.
751
752 See the example in :ref:`mutable_composites` for usage information.
753
754 """
755
756 @classmethod
757 def _get_listen_keys(cls, attribute: QueryableAttribute[_O]) -> Set[str]:
758 return {attribute.key}.union(attribute.property._attribute_keys)
759
760 def changed(self) -> None:
761 """Subclasses should call this method whenever change events occur."""
762
763 for parent, key in self._parents.items():
764 prop = parent.mapper.get_property(key)
765 for value, attr_name in zip(
766 prop._composite_values_from_instance(self),
767 prop._attribute_keys,
768 ):
769 setattr(parent.obj(), attr_name, value)
770
771
772def _setup_composite_listener() -> None:
773 def _listen_for_type(mapper: Mapper[_T], class_: type) -> None:
774 for prop in mapper.iterate_properties:
775 if (
776 hasattr(prop, "composite_class")
777 and isinstance(prop.composite_class, type)
778 and issubclass(prop.composite_class, MutableComposite)
779 ):
780 prop.composite_class._listen_on_attribute(
781 getattr(class_, prop.key), False, class_
782 )
783
784 if not event.contains(Mapper, "mapper_configured", _listen_for_type):
785 event.listen(Mapper, "mapper_configured", _listen_for_type)
786
787
788_setup_composite_listener()
789
790
791class MutableDict(Mutable, Dict[_KT, _VT]):
792 """A dictionary type that implements :class:`.Mutable`.
793
794 The :class:`.MutableDict` object implements a dictionary that will
795 emit change events to the underlying mapping when the contents of
796 the dictionary are altered, including when values are added or removed.
797
798 Note that :class:`.MutableDict` does **not** apply mutable tracking to the
799 *values themselves* inside the dictionary. Therefore it is not a sufficient
800 solution for the use case of tracking deep changes to a *recursive*
801 dictionary structure, such as a JSON structure. To support this use case,
802 build a subclass of :class:`.MutableDict` that provides appropriate
803 coercion to the values placed in the dictionary so that they too are
804 "mutable", and emit events up to their parent structure.
805
806 .. seealso::
807
808 :class:`.MutableList`
809
810 :class:`.MutableSet`
811
812 """
813
814 def __setitem__(self, key: _KT, value: _VT) -> None:
815 """Detect dictionary set events and emit change events."""
816 super().__setitem__(key, value)
817 self.changed()
818
819 if TYPE_CHECKING:
820 # from https://github.com/python/mypy/issues/14858
821
822 @overload
823 def setdefault(
824 self: MutableDict[_KT, Optional[_T]], key: _KT, value: None = None
825 ) -> Optional[_T]: ...
826
827 @overload
828 def setdefault(self, key: _KT, value: _VT) -> _VT: ...
829
830 def setdefault(self, key: _KT, value: object = None) -> object: ...
831
832 else:
833
834 def setdefault(self, *arg): # noqa: F811
835 result = super().setdefault(*arg)
836 self.changed()
837 return result
838
839 def __delitem__(self, key: _KT) -> None:
840 """Detect dictionary del events and emit change events."""
841 super().__delitem__(key)
842 self.changed()
843
844 def update(self, *a: Any, **kw: _VT) -> None:
845 super().update(*a, **kw)
846 self.changed()
847
848 if TYPE_CHECKING:
849
850 @overload
851 def pop(self, __key: _KT) -> _VT: ...
852
853 @overload
854 def pop(self, __key: _KT, __default: _VT | _T) -> _VT | _T: ...
855
856 def pop(
857 self, __key: _KT, __default: _VT | _T | None = None
858 ) -> _VT | _T: ...
859
860 else:
861
862 def pop(self, *arg): # noqa: F811
863 result = super().pop(*arg)
864 self.changed()
865 return result
866
867 def popitem(self) -> Tuple[_KT, _VT]:
868 result = super().popitem()
869 self.changed()
870 return result
871
872 def clear(self) -> None:
873 super().clear()
874 self.changed()
875
876 @classmethod
877 def coerce(cls, key: str, value: Any) -> MutableDict[_KT, _VT] | None:
878 """Convert plain dictionary to instance of this class."""
879 if not isinstance(value, cls):
880 if isinstance(value, dict):
881 return cls(value)
882 return Mutable.coerce(key, value)
883 else:
884 return value
885
886 def __getstate__(self) -> Dict[_KT, _VT]:
887 return dict(self)
888
889 def __setstate__(
890 self, state: Union[Dict[str, int], Dict[str, str]]
891 ) -> None:
892 self.update(state)
893
894
895class MutableList(Mutable, List[_T]):
896 """A list type that implements :class:`.Mutable`.
897
898 The :class:`.MutableList` object implements a list that will
899 emit change events to the underlying mapping when the contents of
900 the list are altered, including when values are added or removed.
901
902 Note that :class:`.MutableList` does **not** apply mutable tracking to the
903 *values themselves* inside the list. Therefore it is not a sufficient
904 solution for the use case of tracking deep changes to a *recursive*
905 mutable structure, such as a JSON structure. To support this use case,
906 build a subclass of :class:`.MutableList` that provides appropriate
907 coercion to the values placed in the dictionary so that they too are
908 "mutable", and emit events up to their parent structure.
909
910 .. seealso::
911
912 :class:`.MutableDict`
913
914 :class:`.MutableSet`
915
916 """
917
918 def __reduce_ex__(
919 self, proto: SupportsIndex
920 ) -> Tuple[type, Tuple[List[int]]]:
921 return (self.__class__, (list(self),))
922
923 # needed for backwards compatibility with
924 # older pickles
925 def __setstate__(self, state: Iterable[_T]) -> None:
926 self[:] = state
927
928 def is_scalar(self, value: _T | Iterable[_T]) -> TypeGuard[_T]:
929 return not util.is_non_string_iterable(value)
930
931 def is_iterable(self, value: _T | Iterable[_T]) -> TypeGuard[Iterable[_T]]:
932 return util.is_non_string_iterable(value)
933
934 def __setitem__(
935 self, index: SupportsIndex | slice, value: _T | Iterable[_T]
936 ) -> None:
937 """Detect list set events and emit change events."""
938 if isinstance(index, SupportsIndex) and self.is_scalar(value):
939 super().__setitem__(index, value)
940 elif isinstance(index, slice) and self.is_iterable(value):
941 super().__setitem__(index, value)
942 self.changed()
943
944 def __delitem__(self, index: SupportsIndex | slice) -> None:
945 """Detect list del events and emit change events."""
946 super().__delitem__(index)
947 self.changed()
948
949 def pop(self, *arg: SupportsIndex) -> _T:
950 result = super().pop(*arg)
951 self.changed()
952 return result
953
954 def append(self, x: _T) -> None:
955 super().append(x)
956 self.changed()
957
958 def extend(self, x: Iterable[_T]) -> None:
959 super().extend(x)
960 self.changed()
961
962 def __iadd__(self, x: Iterable[_T]) -> MutableList[_T]: # type: ignore[override,misc] # noqa: E501
963 self.extend(x)
964 return self
965
966 def insert(self, i: SupportsIndex, x: _T) -> None:
967 super().insert(i, x)
968 self.changed()
969
970 def remove(self, i: _T) -> None:
971 super().remove(i)
972 self.changed()
973
974 def clear(self) -> None:
975 super().clear()
976 self.changed()
977
978 def sort(self, **kw: Any) -> None:
979 super().sort(**kw)
980 self.changed()
981
982 def reverse(self) -> None:
983 super().reverse()
984 self.changed()
985
986 @classmethod
987 def coerce(
988 cls, key: str, value: MutableList[_T] | _T
989 ) -> Optional[MutableList[_T]]:
990 """Convert plain list to instance of this class."""
991 if not isinstance(value, cls):
992 if isinstance(value, list):
993 return cls(value)
994 return Mutable.coerce(key, value)
995 else:
996 return value
997
998
999class MutableSet(Mutable, Set[_T]):
1000 """A set type that implements :class:`.Mutable`.
1001
1002 The :class:`.MutableSet` object implements a set that will
1003 emit change events to the underlying mapping when the contents of
1004 the set are altered, including when values are added or removed.
1005
1006 Note that :class:`.MutableSet` does **not** apply mutable tracking to the
1007 *values themselves* inside the set. Therefore it is not a sufficient
1008 solution for the use case of tracking deep changes to a *recursive*
1009 mutable structure. To support this use case,
1010 build a subclass of :class:`.MutableSet` that provides appropriate
1011 coercion to the values placed in the dictionary so that they too are
1012 "mutable", and emit events up to their parent structure.
1013
1014 .. seealso::
1015
1016 :class:`.MutableDict`
1017
1018 :class:`.MutableList`
1019
1020
1021 """
1022
1023 def update(self, *arg: Iterable[_T]) -> None:
1024 super().update(*arg)
1025 self.changed()
1026
1027 def intersection_update(self, *arg: Iterable[Any]) -> None:
1028 super().intersection_update(*arg)
1029 self.changed()
1030
1031 def difference_update(self, *arg: Iterable[Any]) -> None:
1032 super().difference_update(*arg)
1033 self.changed()
1034
1035 def symmetric_difference_update(self, *arg: Iterable[_T]) -> None:
1036 super().symmetric_difference_update(*arg)
1037 self.changed()
1038
1039 def __ior__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501
1040 self.update(other)
1041 return self
1042
1043 def __iand__(self, other: AbstractSet[object]) -> MutableSet[_T]:
1044 self.intersection_update(other)
1045 return self
1046
1047 def __ixor__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501
1048 self.symmetric_difference_update(other)
1049 return self
1050
1051 def __isub__(self, other: AbstractSet[object]) -> MutableSet[_T]: # type: ignore[misc] # noqa: E501
1052 self.difference_update(other)
1053 return self
1054
1055 def add(self, elem: _T) -> None:
1056 super().add(elem)
1057 self.changed()
1058
1059 def remove(self, elem: _T) -> None:
1060 super().remove(elem)
1061 self.changed()
1062
1063 def discard(self, elem: _T) -> None:
1064 super().discard(elem)
1065 self.changed()
1066
1067 def pop(self, *arg: Any) -> _T:
1068 result = super().pop(*arg)
1069 self.changed()
1070 return result
1071
1072 def clear(self) -> None:
1073 super().clear()
1074 self.changed()
1075
1076 @classmethod
1077 def coerce(cls, index: str, value: Any) -> Optional[MutableSet[_T]]:
1078 """Convert plain set to instance of this class."""
1079 if not isinstance(value, cls):
1080 if isinstance(value, set):
1081 return cls(value)
1082 return Mutable.coerce(index, value)
1083 else:
1084 return value
1085
1086 def __getstate__(self) -> Set[_T]:
1087 return set(self)
1088
1089 def __setstate__(self, state: Iterable[_T]) -> None:
1090 self.update(state)
1091
1092 def __reduce_ex__(
1093 self, proto: SupportsIndex
1094 ) -> Tuple[type, Tuple[List[int]]]:
1095 return (self.__class__, (list(self),))