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

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

300 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 SupportsIndex 

391from typing import Tuple 

392from typing import TYPE_CHECKING 

393from typing import TypeVar 

394from typing import Union 

395import weakref 

396from weakref import WeakKeyDictionary 

397 

398from .. import event 

399from .. import inspect 

400from .. import types 

401from ..orm import Mapper 

402from ..orm._typing import _ExternalEntityType 

403from ..orm._typing import _O 

404from ..orm._typing import _T 

405from ..orm.attributes import AttributeEventToken 

406from ..orm.attributes import flag_modified 

407from ..orm.attributes import InstrumentedAttribute 

408from ..orm.attributes import QueryableAttribute 

409from ..orm.context import QueryContext 

410from ..orm.decl_api import DeclarativeAttributeIntercept 

411from ..orm.state import InstanceState 

412from ..orm.unitofwork import UOWTransaction 

413from ..sql._typing import _TypeEngineArgument 

414from ..sql.base import SchemaEventTarget 

415from ..sql.schema import Column 

416from ..sql.type_api import TypeEngine 

417from ..util import memoized_property 

418 

419_KT = TypeVar("_KT") # Key type. 

420_VT = TypeVar("_VT") # Value type. 

421 

422 

423class MutableBase: 

424 """Common base class to :class:`.Mutable` 

425 and :class:`.MutableComposite`. 

426 

427 """ 

428 

429 @memoized_property 

430 def _parents(self) -> WeakKeyDictionary[Any, Any]: 

431 """Dictionary of parent object's :class:`.InstanceState`->attribute 

432 name on the parent. 

433 

434 This attribute is a so-called "memoized" property. It initializes 

435 itself with a new ``weakref.WeakKeyDictionary`` the first time 

436 it is accessed, returning the same object upon subsequent access. 

437 

438 .. versionchanged:: 1.4 the :class:`.InstanceState` is now used 

439 as the key in the weak dictionary rather than the instance 

440 itself. 

441 

442 """ 

443 

444 return weakref.WeakKeyDictionary() 

445 

446 @classmethod 

447 def coerce(cls, key: str, value: Any) -> Optional[Any]: 

448 """Given a value, coerce it into the target type. 

449 

450 Can be overridden by custom subclasses to coerce incoming 

451 data into a particular type. 

452 

453 By default, raises ``ValueError``. 

454 

455 This method is called in different scenarios depending on if 

456 the parent class is of type :class:`.Mutable` or of type 

457 :class:`.MutableComposite`. In the case of the former, it is called 

458 for both attribute-set operations as well as during ORM loading 

459 operations. For the latter, it is only called during attribute-set 

460 operations; the mechanics of the :func:`.composite` construct 

461 handle coercion during load operations. 

462 

463 

464 :param key: string name of the ORM-mapped attribute being set. 

465 :param value: the incoming value. 

466 :return: the method should return the coerced value, or raise 

467 ``ValueError`` if the coercion cannot be completed. 

468 

469 """ 

470 if value is None: 

471 return None 

472 msg = "Attribute '%s' does not accept objects of type %s" 

473 raise ValueError(msg % (key, type(value))) 

474 

475 @classmethod 

476 def _get_listen_keys(cls, attribute: QueryableAttribute[Any]) -> Set[str]: 

477 """Given a descriptor attribute, return a ``set()`` of the attribute 

478 keys which indicate a change in the state of this attribute. 

479 

480 This is normally just ``set([attribute.key])``, but can be overridden 

481 to provide for additional keys. E.g. a :class:`.MutableComposite` 

482 augments this set with the attribute keys associated with the columns 

483 that comprise the composite value. 

484 

485 This collection is consulted in the case of intercepting the 

486 :meth:`.InstanceEvents.refresh` and 

487 :meth:`.InstanceEvents.refresh_flush` events, which pass along a list 

488 of attribute names that have been refreshed; the list is compared 

489 against this set to determine if action needs to be taken. 

490 

491 """ 

492 return {attribute.key} 

493 

494 @classmethod 

495 def _listen_on_attribute( 

496 cls, 

497 attribute: QueryableAttribute[Any], 

498 coerce: bool, 

499 parent_cls: _ExternalEntityType[Any], 

500 ) -> None: 

501 """Establish this type as a mutation listener for the given 

502 mapped descriptor. 

503 

504 """ 

505 key = attribute.key 

506 if parent_cls is not attribute.class_: 

507 return 

508 

509 # rely on "propagate" here 

510 parent_cls = attribute.class_ 

511 

512 listen_keys = cls._get_listen_keys(attribute) 

513 

514 def load(state: InstanceState[_O], *args: Any) -> None: 

515 """Listen for objects loaded or refreshed. 

516 

517 Wrap the target data member's value with 

518 ``Mutable``. 

519 

520 """ 

521 val = state.dict.get(key, None) 

522 if val is not None: 

523 if coerce: 

524 val = cls.coerce(key, val) 

525 assert val is not None 

526 state.dict[key] = val 

527 val._parents[state] = key 

528 

529 def load_attrs( 

530 state: InstanceState[_O], 

531 ctx: Union[object, QueryContext, UOWTransaction], 

532 attrs: Iterable[Any], 

533 ) -> None: 

534 if not attrs or listen_keys.intersection(attrs): 

535 load(state) 

536 

537 def set_( 

538 target: InstanceState[_O], 

539 value: MutableBase | None, 

540 oldvalue: MutableBase | None, 

541 initiator: AttributeEventToken, 

542 ) -> MutableBase | None: 

543 """Listen for set/replace events on the target 

544 data member. 

545 

546 Establish a weak reference to the parent object 

547 on the incoming value, remove it for the one 

548 outgoing. 

549 

550 """ 

551 if value is oldvalue: 

552 return value 

553 

554 if not isinstance(value, cls): 

555 value = cls.coerce(key, value) 

556 if value is not None: 

557 value._parents[target] = key 

558 if isinstance(oldvalue, cls): 

559 oldvalue._parents.pop(inspect(target), None) 

560 return value 

561 

562 def pickle( 

563 state: InstanceState[_O], state_dict: Dict[str, Any] 

564 ) -> None: 

565 val = state.dict.get(key, None) 

566 if val is not None: 

567 if "ext.mutable.values" not in state_dict: 

568 state_dict["ext.mutable.values"] = defaultdict(list) 

569 state_dict["ext.mutable.values"][key].append(val) 

570 

571 def unpickle( 

572 state: InstanceState[_O], state_dict: Dict[str, Any] 

573 ) -> None: 

574 if "ext.mutable.values" in state_dict: 

575 collection = state_dict["ext.mutable.values"] 

576 if isinstance(collection, list): 

577 # legacy format 

578 for val in collection: 

579 val._parents[state] = key 

580 else: 

581 for val in state_dict["ext.mutable.values"][key]: 

582 val._parents[state] = key 

583 

584 event.listen( 

585 parent_cls, 

586 "_sa_event_merge_wo_load", 

587 load, 

588 raw=True, 

589 propagate=True, 

590 ) 

591 

592 event.listen(parent_cls, "load", load, raw=True, propagate=True) 

593 event.listen( 

594 parent_cls, "refresh", load_attrs, raw=True, propagate=True 

595 ) 

596 event.listen( 

597 parent_cls, "refresh_flush", load_attrs, raw=True, propagate=True 

598 ) 

599 event.listen( 

600 attribute, "set", set_, raw=True, retval=True, propagate=True 

601 ) 

602 event.listen(parent_cls, "pickle", pickle, raw=True, propagate=True) 

603 event.listen( 

604 parent_cls, "unpickle", unpickle, raw=True, propagate=True 

605 ) 

606 

607 

608class Mutable(MutableBase): 

609 """Mixin that defines transparent propagation of change 

610 events to a parent object. 

611 

612 See the example in :ref:`mutable_scalars` for usage information. 

613 

614 """ 

615 

616 def changed(self) -> None: 

617 """Subclasses should call this method whenever change events occur.""" 

618 

619 for parent, key in self._parents.items(): 

620 flag_modified(parent.obj(), key) 

621 

622 @classmethod 

623 def associate_with_attribute( 

624 cls, attribute: InstrumentedAttribute[_O] 

625 ) -> None: 

626 """Establish this type as a mutation listener for the given 

627 mapped descriptor. 

628 

629 """ 

630 cls._listen_on_attribute(attribute, True, attribute.class_) 

631 

632 @classmethod 

633 def associate_with(cls, sqltype: type) -> None: 

634 """Associate this wrapper with all future mapped columns 

635 of the given type. 

636 

637 This is a convenience method that calls 

638 ``associate_with_attribute`` automatically. 

639 

640 .. warning:: 

641 

642 The listeners established by this method are *global* 

643 to all mappers, and are *not* garbage collected. Only use 

644 :meth:`.associate_with` for types that are permanent to an 

645 application, not with ad-hoc types else this will cause unbounded 

646 growth in memory usage. 

647 

648 """ 

649 

650 def listen_for_type(mapper: Mapper[_O], class_: type) -> None: 

651 for prop in mapper.column_attrs: 

652 if isinstance(prop.columns[0].type, sqltype): 

653 cls.associate_with_attribute(getattr(class_, prop.key)) 

654 

655 event.listen(Mapper, "mapper_configured", listen_for_type) 

656 

657 @classmethod 

658 def as_mutable(cls, sqltype: _TypeEngineArgument[_T]) -> TypeEngine[_T]: 

659 """Associate a SQL type with this mutable Python type. 

660 

661 This establishes listeners that will detect ORM mappings against 

662 the given type, adding mutation event trackers to those mappings. 

663 

664 The type is returned, unconditionally as an instance, so that 

665 :meth:`.as_mutable` can be used inline:: 

666 

667 Table( 

668 "mytable", 

669 metadata, 

670 Column("id", Integer, primary_key=True), 

671 Column("data", MyMutableType.as_mutable(PickleType)), 

672 ) 

673 

674 Note that the returned type is always an instance, even if a class 

675 is given, and that only columns which are declared specifically with 

676 that type instance receive additional instrumentation. 

677 

678 To associate a particular mutable type with all occurrences of a 

679 particular type, use the :meth:`.Mutable.associate_with` classmethod 

680 of the particular :class:`.Mutable` subclass to establish a global 

681 association. 

682 

683 .. warning:: 

684 

685 The listeners established by this method are *global* 

686 to all mappers, and are *not* garbage collected. Only use 

687 :meth:`.as_mutable` for types that are permanent to an application, 

688 not with ad-hoc types else this will cause unbounded growth 

689 in memory usage. 

690 

691 """ 

692 sqltype = types.to_instance(sqltype) 

693 

694 # a SchemaType will be copied when the Column is copied, 

695 # and we'll lose our ability to link that type back to the original. 

696 # so track our original type w/ columns 

697 if isinstance(sqltype, SchemaEventTarget): 

698 

699 @event.listens_for(sqltype, "before_parent_attach") 

700 def _add_column_memo( 

701 sqltyp: TypeEngine[Any], 

702 parent: Column[_T], 

703 ) -> None: 

704 parent.info["_ext_mutable_orig_type"] = sqltyp 

705 

706 schema_event_check = True 

707 else: 

708 schema_event_check = False 

709 

710 def listen_for_type( 

711 mapper: Mapper[_T], 

712 class_: Union[DeclarativeAttributeIntercept, type], 

713 ) -> None: 

714 _APPLIED_KEY = "_ext_mutable_listener_applied" 

715 

716 for prop in mapper.column_attrs: 

717 if ( 

718 # all Mutable types refer to a Column that's mapped, 

719 # since this is the only kind of Core target the ORM can 

720 # "mutate" 

721 isinstance(prop.expression, Column) 

722 and ( 

723 ( 

724 schema_event_check 

725 and prop.expression.info.get( 

726 "_ext_mutable_orig_type" 

727 ) 

728 is sqltype 

729 ) 

730 or prop.expression.type is sqltype 

731 ) 

732 ): 

733 if not prop.expression.info.get(_APPLIED_KEY, False): 

734 prop.expression.info[_APPLIED_KEY] = True 

735 cls.associate_with_attribute(getattr(class_, prop.key)) 

736 

737 event.listen(Mapper, "mapper_configured", listen_for_type) 

738 

739 return sqltype 

740 

741 

742class MutableComposite(MutableBase): 

743 """Mixin that defines transparent propagation of change 

744 events on a SQLAlchemy "composite" object to its 

745 owning parent or parents. 

746 

747 See the example in :ref:`mutable_composites` for usage information. 

748 

749 """ 

750 

751 @classmethod 

752 def _get_listen_keys(cls, attribute: QueryableAttribute[_O]) -> Set[str]: 

753 return {attribute.key}.union(attribute.property._attribute_keys) 

754 

755 def changed(self) -> None: 

756 """Subclasses should call this method whenever change events occur.""" 

757 

758 for parent, key in self._parents.items(): 

759 prop = parent.mapper.get_property(key) 

760 for value, attr_name in zip( 

761 prop._composite_values_from_instance(self), 

762 prop._attribute_keys, 

763 ): 

764 setattr(parent.obj(), attr_name, value) 

765 

766 

767def _setup_composite_listener() -> None: 

768 def _listen_for_type(mapper: Mapper[_T], class_: type) -> None: 

769 for prop in mapper.iterate_properties: 

770 if ( 

771 hasattr(prop, "composite_class") 

772 and isinstance(prop.composite_class, type) 

773 and issubclass(prop.composite_class, MutableComposite) 

774 ): 

775 prop.composite_class._listen_on_attribute( 

776 getattr(class_, prop.key), False, class_ 

777 ) 

778 

779 if not event.contains(Mapper, "mapper_configured", _listen_for_type): 

780 event.listen(Mapper, "mapper_configured", _listen_for_type) 

781 

782 

783_setup_composite_listener() 

784 

785 

786class MutableDict(Mutable, Dict[_KT, _VT]): 

787 """A dictionary type that implements :class:`.Mutable`. 

788 

789 The :class:`.MutableDict` object implements a dictionary that will 

790 emit change events to the underlying mapping when the contents of 

791 the dictionary are altered, including when values are added or removed. 

792 

793 Note that :class:`.MutableDict` does **not** apply mutable tracking to the 

794 *values themselves* inside the dictionary. Therefore it is not a sufficient 

795 solution for the use case of tracking deep changes to a *recursive* 

796 dictionary structure, such as a JSON structure. To support this use case, 

797 build a subclass of :class:`.MutableDict` that provides appropriate 

798 coercion to the values placed in the dictionary so that they too are 

799 "mutable", and emit events up to their parent structure. 

800 

801 .. seealso:: 

802 

803 :class:`.MutableList` 

804 

805 :class:`.MutableSet` 

806 

807 """ 

808 

809 def __setitem__(self, key: _KT, value: _VT) -> None: 

810 """Detect dictionary set events and emit change events.""" 

811 dict.__setitem__(self, key, value) 

812 self.changed() 

813 

814 if TYPE_CHECKING: 

815 # from https://github.com/python/mypy/issues/14858 

816 

817 @overload 

818 def setdefault( 

819 self: MutableDict[_KT, Optional[_T]], key: _KT, value: None = None 

820 ) -> Optional[_T]: ... 

821 

822 @overload 

823 def setdefault(self, key: _KT, value: _VT) -> _VT: ... 

824 

825 def setdefault(self, key: _KT, value: object = None) -> object: ... 

826 

827 else: 

828 

829 def setdefault(self, *arg): # noqa: F811 

830 result = dict.setdefault(self, *arg) 

831 self.changed() 

832 return result 

833 

834 def __delitem__(self, key: _KT) -> None: 

835 """Detect dictionary del events and emit change events.""" 

836 dict.__delitem__(self, key) 

837 self.changed() 

838 

839 def update(self, *a: Any, **kw: _VT) -> None: 

840 dict.update(self, *a, **kw) 

841 self.changed() 

842 

843 if TYPE_CHECKING: 

844 

845 @overload 

846 def pop(self, __key: _KT, /) -> _VT: ... 

847 

848 @overload 

849 def pop(self, __key: _KT, default: _VT | _T, /) -> _VT | _T: ... 

850 

851 def pop( 

852 self, __key: _KT, __default: _VT | _T | None = None, / 

853 ) -> _VT | _T: ... 

854 

855 else: 

856 

857 def pop(self, *arg): # noqa: F811 

858 result = dict.pop(self, *arg) 

859 self.changed() 

860 return result 

861 

862 def popitem(self) -> Tuple[_KT, _VT]: 

863 result = dict.popitem(self) 

864 self.changed() 

865 return result 

866 

867 def clear(self) -> None: 

868 dict.clear(self) 

869 self.changed() 

870 

871 @classmethod 

872 def coerce(cls, key: str, value: Any) -> MutableDict[_KT, _VT] | None: 

873 """Convert plain dictionary to instance of this class.""" 

874 if not isinstance(value, cls): 

875 if isinstance(value, dict): 

876 return cls(value) 

877 return Mutable.coerce(key, value) 

878 else: 

879 return value 

880 

881 def __getstate__(self) -> Dict[_KT, _VT]: 

882 return dict(self) 

883 

884 def __setstate__( 

885 self, state: Union[Dict[str, int], Dict[str, str]] 

886 ) -> None: 

887 self.update(state) 

888 

889 

890class MutableList(Mutable, List[_T]): 

891 """A list type that implements :class:`.Mutable`. 

892 

893 The :class:`.MutableList` object implements a list that will 

894 emit change events to the underlying mapping when the contents of 

895 the list are altered, including when values are added or removed. 

896 

897 Note that :class:`.MutableList` does **not** apply mutable tracking to the 

898 *values themselves* inside the list. Therefore it is not a sufficient 

899 solution for the use case of tracking deep changes to a *recursive* 

900 mutable structure, such as a JSON structure. To support this use case, 

901 build a subclass of :class:`.MutableList` that provides appropriate 

902 coercion to the values placed in the dictionary so that they too are 

903 "mutable", and emit events up to their parent structure. 

904 

905 .. seealso:: 

906 

907 :class:`.MutableDict` 

908 

909 :class:`.MutableSet` 

910 

911 """ 

912 

913 def __reduce_ex__( 

914 self, proto: SupportsIndex 

915 ) -> Tuple[type, Tuple[List[int]]]: 

916 return (self.__class__, (list(self),)) 

917 

918 # needed for backwards compatibility with 

919 # older pickles 

920 def __setstate__(self, state: Iterable[_T]) -> None: 

921 self[:] = state 

922 

923 def __setitem__( 

924 self, index: SupportsIndex | slice, value: _T | Iterable[_T] 

925 ) -> None: 

926 """Detect list set events and emit change events.""" 

927 list.__setitem__(self, index, value) 

928 self.changed() 

929 

930 def __delitem__(self, index: SupportsIndex | slice) -> None: 

931 """Detect list del events and emit change events.""" 

932 list.__delitem__(self, index) 

933 self.changed() 

934 

935 def pop(self, *arg: SupportsIndex) -> _T: 

936 result = list.pop(self, *arg) 

937 self.changed() 

938 return result 

939 

940 def append(self, x: _T) -> None: 

941 list.append(self, x) 

942 self.changed() 

943 

944 def extend(self, x: Iterable[_T]) -> None: 

945 list.extend(self, x) 

946 self.changed() 

947 

948 def __iadd__(self, x: Iterable[_T]) -> MutableList[_T]: # type: ignore[override,misc] # noqa: E501 

949 self.extend(x) 

950 return self 

951 

952 def insert(self, i: SupportsIndex, x: _T) -> None: 

953 list.insert(self, i, x) 

954 self.changed() 

955 

956 def remove(self, i: _T) -> None: 

957 list.remove(self, i) 

958 self.changed() 

959 

960 def clear(self) -> None: 

961 list.clear(self) 

962 self.changed() 

963 

964 def sort(self, **kw: Any) -> None: 

965 list.sort(self, **kw) 

966 self.changed() 

967 

968 def reverse(self) -> None: 

969 list.reverse(self) 

970 self.changed() 

971 

972 @classmethod 

973 def coerce( 

974 cls, key: str, value: MutableList[_T] | _T 

975 ) -> Optional[MutableList[_T]]: 

976 """Convert plain list to instance of this class.""" 

977 if not isinstance(value, cls): 

978 if isinstance(value, list): 

979 return cls(value) 

980 return Mutable.coerce(key, value) 

981 else: 

982 return value 

983 

984 

985class MutableSet(Mutable, Set[_T]): 

986 """A set type that implements :class:`.Mutable`. 

987 

988 The :class:`.MutableSet` object implements a set that will 

989 emit change events to the underlying mapping when the contents of 

990 the set are altered, including when values are added or removed. 

991 

992 Note that :class:`.MutableSet` does **not** apply mutable tracking to the 

993 *values themselves* inside the set. Therefore it is not a sufficient 

994 solution for the use case of tracking deep changes to a *recursive* 

995 mutable structure. To support this use case, 

996 build a subclass of :class:`.MutableSet` that provides appropriate 

997 coercion to the values placed in the dictionary so that they too are 

998 "mutable", and emit events up to their parent structure. 

999 

1000 .. seealso:: 

1001 

1002 :class:`.MutableDict` 

1003 

1004 :class:`.MutableList` 

1005 

1006 

1007 """ 

1008 

1009 def update(self, *arg: Iterable[_T]) -> None: 

1010 set.update(self, *arg) 

1011 self.changed() 

1012 

1013 def intersection_update(self, *arg: Iterable[Any]) -> None: 

1014 set.intersection_update(self, *arg) 

1015 self.changed() 

1016 

1017 def difference_update(self, *arg: Iterable[Any]) -> None: 

1018 set.difference_update(self, *arg) 

1019 self.changed() 

1020 

1021 def symmetric_difference_update(self, *arg: Iterable[_T]) -> None: 

1022 set.symmetric_difference_update(self, *arg) 

1023 self.changed() 

1024 

1025 def __ior__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501 

1026 self.update(other) 

1027 return self 

1028 

1029 def __iand__(self, other: AbstractSet[object]) -> MutableSet[_T]: 

1030 self.intersection_update(other) 

1031 return self 

1032 

1033 def __ixor__(self, other: AbstractSet[_T]) -> MutableSet[_T]: # type: ignore[override,misc] # noqa: E501 

1034 self.symmetric_difference_update(other) 

1035 return self 

1036 

1037 def __isub__(self, other: AbstractSet[object]) -> MutableSet[_T]: # type: ignore[misc] # noqa: E501 

1038 self.difference_update(other) 

1039 return self 

1040 

1041 def add(self, elem: _T) -> None: 

1042 set.add(self, elem) 

1043 self.changed() 

1044 

1045 def remove(self, elem: _T) -> None: 

1046 set.remove(self, elem) 

1047 self.changed() 

1048 

1049 def discard(self, elem: _T) -> None: 

1050 set.discard(self, elem) 

1051 self.changed() 

1052 

1053 def pop(self, *arg: Any) -> _T: 

1054 result = set.pop(self, *arg) 

1055 self.changed() 

1056 return result 

1057 

1058 def clear(self) -> None: 

1059 set.clear(self) 

1060 self.changed() 

1061 

1062 @classmethod 

1063 def coerce(cls, index: str, value: Any) -> Optional[MutableSet[_T]]: 

1064 """Convert plain set to instance of this class.""" 

1065 if not isinstance(value, cls): 

1066 if isinstance(value, set): 

1067 return cls(value) 

1068 return Mutable.coerce(index, value) 

1069 else: 

1070 return value 

1071 

1072 def __getstate__(self) -> Set[_T]: 

1073 return set(self) 

1074 

1075 def __setstate__(self, state: Iterable[_T]) -> None: 

1076 self.update(state) 

1077 

1078 def __reduce_ex__( 

1079 self, proto: SupportsIndex 

1080 ) -> Tuple[type, Tuple[List[int]]]: 

1081 return (self.__class__, (list(self),))