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

264 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +0000

1# ext/mutable.py 

2# Copyright (C) 2005-2022 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 class JSONEncodedDict(TypeDecorator): 

25 "Represents an immutable structure as a json-encoded string." 

26 

27 impl = VARCHAR 

28 

29 def process_bind_param(self, value, dialect): 

30 if value is not None: 

31 value = json.dumps(value) 

32 return value 

33 

34 def process_result_value(self, value, dialect): 

35 if value is not None: 

36 value = json.loads(value) 

37 return value 

38 

39The usage of ``json`` is only for the purposes of example. The 

40:mod:`sqlalchemy.ext.mutable` extension can be used 

41with any type whose target Python type may be mutable, including 

42:class:`.PickleType`, :class:`_postgresql.ARRAY`, etc. 

43 

44When using the :mod:`sqlalchemy.ext.mutable` extension, the value itself 

45tracks all parents which reference it. Below, we illustrate a simple 

46version of the :class:`.MutableDict` dictionary object, which applies 

47the :class:`.Mutable` mixin to a plain Python dictionary:: 

48 

49 from sqlalchemy.ext.mutable import Mutable 

50 

51 class MutableDict(Mutable, dict): 

52 @classmethod 

53 def coerce(cls, key, value): 

54 "Convert plain dictionaries to MutableDict." 

55 

56 if not isinstance(value, MutableDict): 

57 if isinstance(value, dict): 

58 return MutableDict(value) 

59 

60 # this call will raise ValueError 

61 return Mutable.coerce(key, value) 

62 else: 

63 return value 

64 

65 def __setitem__(self, key, value): 

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

67 

68 dict.__setitem__(self, key, value) 

69 self.changed() 

70 

71 def __delitem__(self, key): 

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

73 

74 dict.__delitem__(self, key) 

75 self.changed() 

76 

77The above dictionary class takes the approach of subclassing the Python 

78built-in ``dict`` to produce a dict 

79subclass which routes all mutation events through ``__setitem__``. There are 

80variants on this approach, such as subclassing ``UserDict.UserDict`` or 

81``collections.MutableMapping``; the part that's important to this example is 

82that the :meth:`.Mutable.changed` method is called whenever an in-place 

83change to the datastructure takes place. 

84 

85We also redefine the :meth:`.Mutable.coerce` method which will be used to 

86convert any values that are not instances of ``MutableDict``, such 

87as the plain dictionaries returned by the ``json`` module, into the 

88appropriate type. Defining this method is optional; we could just as well 

89created our ``JSONEncodedDict`` such that it always returns an instance 

90of ``MutableDict``, and additionally ensured that all calling code 

91uses ``MutableDict`` explicitly. When :meth:`.Mutable.coerce` is not 

92overridden, any values applied to a parent object which are not instances 

93of the mutable type will raise a ``ValueError``. 

94 

95Our new ``MutableDict`` type offers a class method 

96:meth:`~.Mutable.as_mutable` which we can use within column metadata 

97to associate with types. This method grabs the given type object or 

98class and associates a listener that will detect all future mappings 

99of this type, applying event listening instrumentation to the mapped 

100attribute. Such as, with classical table metadata:: 

101 

102 from sqlalchemy import Table, Column, Integer 

103 

104 my_data = Table('my_data', metadata, 

105 Column('id', Integer, primary_key=True), 

106 Column('data', MutableDict.as_mutable(JSONEncodedDict)) 

107 ) 

108 

109Above, :meth:`~.Mutable.as_mutable` returns an instance of ``JSONEncodedDict`` 

110(if the type object was not an instance already), which will intercept any 

111attributes which are mapped against this type. Below we establish a simple 

112mapping against the ``my_data`` table:: 

113 

114 from sqlalchemy import mapper 

115 

116 class MyDataClass(object): 

117 pass 

118 

119 # associates mutation listeners with MyDataClass.data 

120 mapper(MyDataClass, my_data) 

121 

122The ``MyDataClass.data`` member will now be notified of in place changes 

123to its value. 

124 

125There's no difference in usage when using declarative:: 

126 

127 from sqlalchemy.ext.declarative import declarative_base 

128 

129 Base = declarative_base() 

130 

131 class MyDataClass(Base): 

132 __tablename__ = 'my_data' 

133 id = Column(Integer, primary_key=True) 

134 data = Column(MutableDict.as_mutable(JSONEncodedDict)) 

135 

136Any in-place changes to the ``MyDataClass.data`` member 

137will flag the attribute as "dirty" on the parent object:: 

138 

139 >>> from sqlalchemy.orm import Session 

140 

141 >>> sess = Session() 

142 >>> m1 = MyDataClass(data={'value1':'foo'}) 

143 >>> sess.add(m1) 

144 >>> sess.commit() 

145 

146 >>> m1.data['value1'] = 'bar' 

147 >>> assert m1 in sess.dirty 

148 True 

149 

150The ``MutableDict`` can be associated with all future instances 

151of ``JSONEncodedDict`` in one step, using 

152:meth:`~.Mutable.associate_with`. This is similar to 

153:meth:`~.Mutable.as_mutable` except it will intercept all occurrences 

154of ``MutableDict`` in all mappings unconditionally, without 

155the need to declare it individually:: 

156 

157 MutableDict.associate_with(JSONEncodedDict) 

158 

159 class MyDataClass(Base): 

160 __tablename__ = 'my_data' 

161 id = Column(Integer, primary_key=True) 

162 data = Column(JSONEncodedDict) 

163 

164 

165Supporting Pickling 

166-------------------- 

167 

168The key to the :mod:`sqlalchemy.ext.mutable` extension relies upon the 

169placement of a ``weakref.WeakKeyDictionary`` upon the value object, which 

170stores a mapping of parent mapped objects keyed to the attribute name under 

171which they are associated with this value. ``WeakKeyDictionary`` objects are 

172not picklable, due to the fact that they contain weakrefs and function 

173callbacks. In our case, this is a good thing, since if this dictionary were 

174picklable, it could lead to an excessively large pickle size for our value 

175objects that are pickled by themselves outside of the context of the parent. 

176The developer responsibility here is only to provide a ``__getstate__`` method 

177that excludes the :meth:`~MutableBase._parents` collection from the pickle 

178stream:: 

179 

180 class MyMutableType(Mutable): 

181 def __getstate__(self): 

182 d = self.__dict__.copy() 

183 d.pop('_parents', None) 

184 return d 

185 

186With our dictionary example, we need to return the contents of the dict itself 

187(and also restore them on __setstate__):: 

188 

189 class MutableDict(Mutable, dict): 

190 # .... 

191 

192 def __getstate__(self): 

193 return dict(self) 

194 

195 def __setstate__(self, state): 

196 self.update(state) 

197 

198In the case that our mutable value object is pickled as it is attached to one 

199or more parent objects that are also part of the pickle, the :class:`.Mutable` 

200mixin will re-establish the :attr:`.Mutable._parents` collection on each value 

201object as the owning parents themselves are unpickled. 

202 

203Receiving Events 

204---------------- 

205 

206The :meth:`.AttributeEvents.modified` event handler may be used to receive 

207an event when a mutable scalar emits a change event. This event handler 

208is called when the :func:`.attributes.flag_modified` function is called 

209from within the mutable extension:: 

210 

211 from sqlalchemy.ext.declarative import declarative_base 

212 from sqlalchemy import event 

213 

214 Base = declarative_base() 

215 

216 class MyDataClass(Base): 

217 __tablename__ = 'my_data' 

218 id = Column(Integer, primary_key=True) 

219 data = Column(MutableDict.as_mutable(JSONEncodedDict)) 

220 

221 @event.listens_for(MyDataClass.data, "modified") 

222 def modified_json(instance): 

223 print("json value modified:", instance.data) 

224 

225.. _mutable_composites: 

226 

227Establishing Mutability on Composites 

228===================================== 

229 

230Composites are a special ORM feature which allow a single scalar attribute to 

231be assigned an object value which represents information "composed" from one 

232or more columns from the underlying mapped table. The usual example is that of 

233a geometric "point", and is introduced in :ref:`mapper_composite`. 

234 

235As is the case with :class:`.Mutable`, the user-defined composite class 

236subclasses :class:`.MutableComposite` as a mixin, and detects and delivers 

237change events to its parents via the :meth:`.MutableComposite.changed` method. 

238In the case of a composite class, the detection is usually via the usage of 

239Python descriptors (i.e. ``@property``), or alternatively via the special 

240Python method ``__setattr__()``. Below we expand upon the ``Point`` class 

241introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite` 

242and to also route attribute set events via ``__setattr__`` to the 

243:meth:`.MutableComposite.changed` method:: 

244 

245 from sqlalchemy.ext.mutable import MutableComposite 

246 

247 class Point(MutableComposite): 

248 def __init__(self, x, y): 

249 self.x = x 

250 self.y = y 

251 

252 def __setattr__(self, key, value): 

253 "Intercept set events" 

254 

255 # set the attribute 

256 object.__setattr__(self, key, value) 

257 

258 # alert all parents to the change 

259 self.changed() 

260 

261 def __composite_values__(self): 

262 return self.x, self.y 

263 

264 def __eq__(self, other): 

265 return isinstance(other, Point) and \ 

266 other.x == self.x and \ 

267 other.y == self.y 

268 

269 def __ne__(self, other): 

270 return not self.__eq__(other) 

271 

272The :class:`.MutableComposite` class uses a Python metaclass to automatically 

273establish listeners for any usage of :func:`_orm.composite` that specifies our 

274``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class, 

275listeners are established which will route change events from ``Point`` 

276objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes:: 

277 

278 from sqlalchemy.orm import composite, mapper 

279 from sqlalchemy import Table, Column 

280 

281 vertices = Table('vertices', metadata, 

282 Column('id', Integer, primary_key=True), 

283 Column('x1', Integer), 

284 Column('y1', Integer), 

285 Column('x2', Integer), 

286 Column('y2', Integer), 

287 ) 

288 

289 class Vertex(object): 

290 pass 

291 

292 mapper(Vertex, vertices, properties={ 

293 'start': composite(Point, vertices.c.x1, vertices.c.y1), 

294 'end': composite(Point, vertices.c.x2, vertices.c.y2) 

295 }) 

296 

297Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members 

298will flag the attribute as "dirty" on the parent object:: 

299 

300 >>> from sqlalchemy.orm import Session 

301 

302 >>> sess = Session() 

303 >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15)) 

304 >>> sess.add(v1) 

305 >>> sess.commit() 

306 

307 >>> v1.end.x = 8 

308 >>> assert v1 in sess.dirty 

309 True 

310 

311Coercing Mutable Composites 

312--------------------------- 

313 

314The :meth:`.MutableBase.coerce` method is also supported on composite types. 

315In the case of :class:`.MutableComposite`, the :meth:`.MutableBase.coerce` 

316method is only called for attribute set operations, not load operations. 

317Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent 

318to using a :func:`.validates` validation routine for all attributes which 

319make use of the custom composite type:: 

320 

321 class Point(MutableComposite): 

322 # other Point methods 

323 # ... 

324 

325 def coerce(cls, key, value): 

326 if isinstance(value, tuple): 

327 value = Point(*value) 

328 elif not isinstance(value, Point): 

329 raise ValueError("tuple or Point expected") 

330 return value 

331 

332Supporting Pickling 

333-------------------- 

334 

335As is the case with :class:`.Mutable`, the :class:`.MutableComposite` helper 

336class uses a ``weakref.WeakKeyDictionary`` available via the 

337:meth:`MutableBase._parents` attribute which isn't picklable. If we need to 

338pickle instances of ``Point`` or its owning class ``Vertex``, we at least need 

339to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary. 

340Below we define both a ``__getstate__`` and a ``__setstate__`` that package up 

341the minimal form of our ``Point`` class:: 

342 

343 class Point(MutableComposite): 

344 # ... 

345 

346 def __getstate__(self): 

347 return self.x, self.y 

348 

349 def __setstate__(self, state): 

350 self.x, self.y = state 

351 

352As with :class:`.Mutable`, the :class:`.MutableComposite` augments the 

353pickling process of the parent's object-relational state so that the 

354:meth:`MutableBase._parents` collection is restored to all ``Point`` objects. 

355 

356""" 

357from collections import defaultdict 

358import weakref 

359 

360from .. import event 

361from .. import inspect 

362from .. import types 

363from ..orm import Mapper 

364from ..orm import mapper 

365from ..orm.attributes import flag_modified 

366from ..sql.base import SchemaEventTarget 

367from ..util import memoized_property 

368 

369 

370class MutableBase(object): 

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

372 and :class:`.MutableComposite`. 

373 

374 """ 

375 

376 @memoized_property 

377 def _parents(self): 

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

379 name on the parent. 

380 

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

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

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

384 

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

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

387 itself. 

388 

389 """ 

390 

391 return weakref.WeakKeyDictionary() 

392 

393 @classmethod 

394 def coerce(cls, key, value): 

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

396 

397 Can be overridden by custom subclasses to coerce incoming 

398 data into a particular type. 

399 

400 By default, raises ``ValueError``. 

401 

402 This method is called in different scenarios depending on if 

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

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

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

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

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

408 handle coercion during load operations. 

409 

410 

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

412 :param value: the incoming value. 

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

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

415 

416 """ 

417 if value is None: 

418 return None 

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

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

421 

422 @classmethod 

423 def _get_listen_keys(cls, attribute): 

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

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

426 

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

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

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

430 that comprise the composite value. 

431 

432 This collection is consulted in the case of intercepting the 

433 :meth:`.InstanceEvents.refresh` and 

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

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

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

437 

438 .. versionadded:: 1.0.5 

439 

440 """ 

441 return {attribute.key} 

442 

443 @classmethod 

444 def _listen_on_attribute(cls, attribute, coerce, parent_cls): 

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

446 mapped descriptor. 

447 

448 """ 

449 key = attribute.key 

450 if parent_cls is not attribute.class_: 

451 return 

452 

453 # rely on "propagate" here 

454 parent_cls = attribute.class_ 

455 

456 listen_keys = cls._get_listen_keys(attribute) 

457 

458 def load(state, *args): 

459 """Listen for objects loaded or refreshed. 

460 

461 Wrap the target data member's value with 

462 ``Mutable``. 

463 

464 """ 

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

466 if val is not None: 

467 if coerce: 

468 val = cls.coerce(key, val) 

469 state.dict[key] = val 

470 val._parents[state] = key 

471 

472 def load_attrs(state, ctx, attrs): 

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

474 load(state) 

475 

476 def set_(target, value, oldvalue, initiator): 

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

478 data member. 

479 

480 Establish a weak reference to the parent object 

481 on the incoming value, remove it for the one 

482 outgoing. 

483 

484 """ 

485 if value is oldvalue: 

486 return value 

487 

488 if not isinstance(value, cls): 

489 value = cls.coerce(key, value) 

490 if value is not None: 

491 value._parents[target] = key 

492 if isinstance(oldvalue, cls): 

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

494 return value 

495 

496 def pickle(state, state_dict): 

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

498 if val is not None: 

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

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

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

502 

503 def unpickle(state, state_dict): 

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

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

506 if isinstance(collection, list): 

507 # legacy format 

508 for val in collection: 

509 val._parents[state] = key 

510 else: 

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

512 val._parents[state] = key 

513 

514 event.listen( 

515 parent_cls, 

516 "_sa_event_merge_wo_load", 

517 load, 

518 raw=True, 

519 propagate=True, 

520 ) 

521 

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

523 event.listen( 

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

525 ) 

526 event.listen( 

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

528 ) 

529 event.listen( 

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

531 ) 

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

533 event.listen( 

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

535 ) 

536 

537 

538class Mutable(MutableBase): 

539 """Mixin that defines transparent propagation of change 

540 events to a parent object. 

541 

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

543 

544 """ 

545 

546 def changed(self): 

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

548 

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

550 flag_modified(parent.obj(), key) 

551 

552 @classmethod 

553 def associate_with_attribute(cls, attribute): 

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

555 mapped descriptor. 

556 

557 """ 

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

559 

560 @classmethod 

561 def associate_with(cls, sqltype): 

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

563 of the given type. 

564 

565 This is a convenience method that calls 

566 ``associate_with_attribute`` automatically. 

567 

568 .. warning:: 

569 

570 The listeners established by this method are *global* 

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

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

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

574 growth in memory usage. 

575 

576 """ 

577 

578 def listen_for_type(mapper, class_): 

579 if mapper.non_primary: 

580 return 

581 for prop in mapper.column_attrs: 

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

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

584 

585 event.listen(mapper, "mapper_configured", listen_for_type) 

586 

587 @classmethod 

588 def as_mutable(cls, sqltype): 

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

590 

591 This establishes listeners that will detect ORM mappings against 

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

593 

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

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

596 

597 Table('mytable', metadata, 

598 Column('id', Integer, primary_key=True), 

599 Column('data', MyMutableType.as_mutable(PickleType)) 

600 ) 

601 

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

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

604 that type instance receive additional instrumentation. 

605 

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

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

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

609 association. 

610 

611 .. warning:: 

612 

613 The listeners established by this method are *global* 

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

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

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

617 in memory usage. 

618 

619 """ 

620 sqltype = types.to_instance(sqltype) 

621 

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

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

624 # so track our original type w/ columns 

625 if isinstance(sqltype, SchemaEventTarget): 

626 

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

628 def _add_column_memo(sqltyp, parent): 

629 parent.info["_ext_mutable_orig_type"] = sqltyp 

630 

631 schema_event_check = True 

632 else: 

633 schema_event_check = False 

634 

635 def listen_for_type(mapper, class_): 

636 if mapper.non_primary: 

637 return 

638 for prop in mapper.column_attrs: 

639 if ( 

640 schema_event_check 

641 and hasattr(prop.expression, "info") 

642 and prop.expression.info.get("_ext_mutable_orig_type") 

643 is sqltype 

644 ) or (prop.columns[0].type is sqltype): 

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

646 

647 event.listen(mapper, "mapper_configured", listen_for_type) 

648 

649 return sqltype 

650 

651 

652class MutableComposite(MutableBase): 

653 """Mixin that defines transparent propagation of change 

654 events on a SQLAlchemy "composite" object to its 

655 owning parent or parents. 

656 

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

658 

659 """ 

660 

661 @classmethod 

662 def _get_listen_keys(cls, attribute): 

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

664 

665 def changed(self): 

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

667 

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

669 

670 prop = parent.mapper.get_property(key) 

671 for value, attr_name in zip( 

672 self.__composite_values__(), prop._attribute_keys 

673 ): 

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

675 

676 

677def _setup_composite_listener(): 

678 def _listen_for_type(mapper, class_): 

679 for prop in mapper.iterate_properties: 

680 if ( 

681 hasattr(prop, "composite_class") 

682 and isinstance(prop.composite_class, type) 

683 and issubclass(prop.composite_class, MutableComposite) 

684 ): 

685 prop.composite_class._listen_on_attribute( 

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

687 ) 

688 

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

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

691 

692 

693_setup_composite_listener() 

694 

695 

696class MutableDict(Mutable, dict): 

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

698 

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

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

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

702 

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

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

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

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

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

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

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

710 

711 .. seealso:: 

712 

713 :class:`.MutableList` 

714 

715 :class:`.MutableSet` 

716 

717 """ 

718 

719 def __setitem__(self, key, value): 

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

721 dict.__setitem__(self, key, value) 

722 self.changed() 

723 

724 def setdefault(self, key, value): 

725 result = dict.setdefault(self, key, value) 

726 self.changed() 

727 return result 

728 

729 def __delitem__(self, key): 

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

731 dict.__delitem__(self, key) 

732 self.changed() 

733 

734 def update(self, *a, **kw): 

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

736 self.changed() 

737 

738 def pop(self, *arg): 

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

740 self.changed() 

741 return result 

742 

743 def popitem(self): 

744 result = dict.popitem(self) 

745 self.changed() 

746 return result 

747 

748 def clear(self): 

749 dict.clear(self) 

750 self.changed() 

751 

752 @classmethod 

753 def coerce(cls, key, value): 

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

755 if not isinstance(value, cls): 

756 if isinstance(value, dict): 

757 return cls(value) 

758 return Mutable.coerce(key, value) 

759 else: 

760 return value 

761 

762 def __getstate__(self): 

763 return dict(self) 

764 

765 def __setstate__(self, state): 

766 self.update(state) 

767 

768 

769class MutableList(Mutable, list): 

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

771 

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

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

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

775 

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

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

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

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

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

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

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

783 

784 .. versionadded:: 1.1 

785 

786 .. seealso:: 

787 

788 :class:`.MutableDict` 

789 

790 :class:`.MutableSet` 

791 

792 """ 

793 

794 def __reduce_ex__(self, proto): 

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

796 

797 # needed for backwards compatibility with 

798 # older pickles 

799 def __setstate__(self, state): 

800 self[:] = state 

801 

802 def __setitem__(self, index, value): 

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

804 list.__setitem__(self, index, value) 

805 self.changed() 

806 

807 def __setslice__(self, start, end, value): 

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

809 list.__setslice__(self, start, end, value) 

810 self.changed() 

811 

812 def __delitem__(self, index): 

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

814 list.__delitem__(self, index) 

815 self.changed() 

816 

817 def __delslice__(self, start, end): 

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

819 list.__delslice__(self, start, end) 

820 self.changed() 

821 

822 def pop(self, *arg): 

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

824 self.changed() 

825 return result 

826 

827 def append(self, x): 

828 list.append(self, x) 

829 self.changed() 

830 

831 def extend(self, x): 

832 list.extend(self, x) 

833 self.changed() 

834 

835 def __iadd__(self, x): 

836 self.extend(x) 

837 return self 

838 

839 def insert(self, i, x): 

840 list.insert(self, i, x) 

841 self.changed() 

842 

843 def remove(self, i): 

844 list.remove(self, i) 

845 self.changed() 

846 

847 def clear(self): 

848 list.clear(self) 

849 self.changed() 

850 

851 def sort(self, **kw): 

852 list.sort(self, **kw) 

853 self.changed() 

854 

855 def reverse(self): 

856 list.reverse(self) 

857 self.changed() 

858 

859 @classmethod 

860 def coerce(cls, index, value): 

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

862 if not isinstance(value, cls): 

863 if isinstance(value, list): 

864 return cls(value) 

865 return Mutable.coerce(index, value) 

866 else: 

867 return value 

868 

869 

870class MutableSet(Mutable, set): 

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

872 

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

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

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

876 

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

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

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

880 mutable structure. To support this use case, 

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

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

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

884 

885 .. versionadded:: 1.1 

886 

887 .. seealso:: 

888 

889 :class:`.MutableDict` 

890 

891 :class:`.MutableList` 

892 

893 

894 """ 

895 

896 def update(self, *arg): 

897 set.update(self, *arg) 

898 self.changed() 

899 

900 def intersection_update(self, *arg): 

901 set.intersection_update(self, *arg) 

902 self.changed() 

903 

904 def difference_update(self, *arg): 

905 set.difference_update(self, *arg) 

906 self.changed() 

907 

908 def symmetric_difference_update(self, *arg): 

909 set.symmetric_difference_update(self, *arg) 

910 self.changed() 

911 

912 def __ior__(self, other): 

913 self.update(other) 

914 return self 

915 

916 def __iand__(self, other): 

917 self.intersection_update(other) 

918 return self 

919 

920 def __ixor__(self, other): 

921 self.symmetric_difference_update(other) 

922 return self 

923 

924 def __isub__(self, other): 

925 self.difference_update(other) 

926 return self 

927 

928 def add(self, elem): 

929 set.add(self, elem) 

930 self.changed() 

931 

932 def remove(self, elem): 

933 set.remove(self, elem) 

934 self.changed() 

935 

936 def discard(self, elem): 

937 set.discard(self, elem) 

938 self.changed() 

939 

940 def pop(self, *arg): 

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

942 self.changed() 

943 return result 

944 

945 def clear(self): 

946 set.clear(self) 

947 self.changed() 

948 

949 @classmethod 

950 def coerce(cls, index, value): 

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

952 if not isinstance(value, cls): 

953 if isinstance(value, set): 

954 return cls(value) 

955 return Mutable.coerce(index, value) 

956 else: 

957 return value 

958 

959 def __getstate__(self): 

960 return set(self) 

961 

962 def __setstate__(self, state): 

963 self.update(state) 

964 

965 def __reduce_ex__(self, proto): 

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