Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/ext/mutable.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

312 statements  

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),))