Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/collections.py: 42%

736 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1# orm/collections.py 

2# Copyright (C) 2005-2023 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 

8"""Support for collections of mapped entities. 

9 

10The collections package supplies the machinery used to inform the ORM of 

11collection membership changes. An instrumentation via decoration approach is 

12used, allowing arbitrary types (including built-ins) to be used as entity 

13collections without requiring inheritance from a base class. 

14 

15Instrumentation decoration relays membership change events to the 

16:class:`.CollectionAttributeImpl` that is currently managing the collection. 

17The decorators observe function call arguments and return values, tracking 

18entities entering or leaving the collection. Two decorator approaches are 

19provided. One is a bundle of generic decorators that map function arguments 

20and return values to events:: 

21 

22 from sqlalchemy.orm.collections import collection 

23 class MyClass(object): 

24 # ... 

25 

26 @collection.adds(1) 

27 def store(self, item): 

28 self.data.append(item) 

29 

30 @collection.removes_return() 

31 def pop(self): 

32 return self.data.pop() 

33 

34 

35The second approach is a bundle of targeted decorators that wrap appropriate 

36append and remove notifiers around the mutation methods present in the 

37standard Python ``list``, ``set`` and ``dict`` interfaces. These could be 

38specified in terms of generic decorator recipes, but are instead hand-tooled 

39for increased efficiency. The targeted decorators occasionally implement 

40adapter-like behavior, such as mapping bulk-set methods (``extend``, 

41``update``, ``__setslice__``, etc.) into the series of atomic mutation events 

42that the ORM requires. 

43 

44The targeted decorators are used internally for automatic instrumentation of 

45entity collection classes. Every collection class goes through a 

46transformation process roughly like so: 

47 

481. If the class is a built-in, substitute a trivial sub-class 

492. Is this class already instrumented? 

503. Add in generic decorators 

514. Sniff out the collection interface through duck-typing 

525. Add targeted decoration to any undecorated interface method 

53 

54This process modifies the class at runtime, decorating methods and adding some 

55bookkeeping properties. This isn't possible (or desirable) for built-in 

56classes like ``list``, so trivial sub-classes are substituted to hold 

57decoration:: 

58 

59 class InstrumentedList(list): 

60 pass 

61 

62Collection classes can be specified in ``relationship(collection_class=)`` as 

63types or a function that returns an instance. Collection classes are 

64inspected and instrumented during the mapper compilation phase. The 

65collection_class callable will be executed once to produce a specimen 

66instance, and the type of that specimen will be instrumented. Functions that 

67return built-in types like ``lists`` will be adapted to produce instrumented 

68instances. 

69 

70When extending a known type like ``list``, additional decorations are not 

71generally not needed. Odds are, the extension method will delegate to a 

72method that's already instrumented. For example:: 

73 

74 class QueueIsh(list): 

75 def push(self, item): 

76 self.append(item) 

77 def shift(self): 

78 return self.pop(0) 

79 

80There's no need to decorate these methods. ``append`` and ``pop`` are already 

81instrumented as part of the ``list`` interface. Decorating them would fire 

82duplicate events, which should be avoided. 

83 

84The targeted decoration tries not to rely on other methods in the underlying 

85collection class, but some are unavoidable. Many depend on 'read' methods 

86being present to properly instrument a 'write', for example, ``__setitem__`` 

87needs ``__getitem__``. "Bulk" methods like ``update`` and ``extend`` may also 

88reimplemented in terms of atomic appends and removes, so the ``extend`` 

89decoration will actually perform many ``append`` operations and not call the 

90underlying method at all. 

91 

92Tight control over bulk operation and the firing of events is also possible by 

93implementing the instrumentation internally in your methods. The basic 

94instrumentation package works under the general assumption that collection 

95mutation will not raise unusual exceptions. If you want to closely 

96orchestrate append and remove events with exception management, internal 

97instrumentation may be the answer. Within your method, 

98``collection_adapter(self)`` will retrieve an object that you can use for 

99explicit control over triggering append and remove events. 

100 

101The owning object and :class:`.CollectionAttributeImpl` are also reachable 

102through the adapter, allowing for some very sophisticated behavior. 

103 

104""" 

105 

106import operator 

107import weakref 

108 

109from sqlalchemy.util.compat import inspect_getfullargspec 

110from . import base 

111from .. import exc as sa_exc 

112from .. import util 

113from ..sql import coercions 

114from ..sql import expression 

115from ..sql import roles 

116 

117__all__ = [ 

118 "collection", 

119 "collection_adapter", 

120 "mapped_collection", 

121 "column_mapped_collection", 

122 "attribute_mapped_collection", 

123] 

124 

125__instrumentation_mutex = util.threading.Lock() 

126 

127 

128class _PlainColumnGetter(object): 

129 """Plain column getter, stores collection of Column objects 

130 directly. 

131 

132 Serializes to a :class:`._SerializableColumnGetterV2` 

133 which has more expensive __call__() performance 

134 and some rare caveats. 

135 

136 """ 

137 

138 def __init__(self, cols): 

139 self.cols = cols 

140 self.composite = len(cols) > 1 

141 

142 def __reduce__(self): 

143 return _SerializableColumnGetterV2._reduce_from_cols(self.cols) 

144 

145 def _cols(self, mapper): 

146 return self.cols 

147 

148 def __call__(self, value): 

149 state = base.instance_state(value) 

150 m = base._state_mapper(state) 

151 

152 key = [ 

153 m._get_state_attr_by_column(state, state.dict, col) 

154 for col in self._cols(m) 

155 ] 

156 

157 if self.composite: 

158 return tuple(key) 

159 else: 

160 return key[0] 

161 

162 

163class _SerializableColumnGetter(object): 

164 """Column-based getter used in version 0.7.6 only. 

165 

166 Remains here for pickle compatibility with 0.7.6. 

167 

168 """ 

169 

170 def __init__(self, colkeys): 

171 self.colkeys = colkeys 

172 self.composite = len(colkeys) > 1 

173 

174 def __reduce__(self): 

175 return _SerializableColumnGetter, (self.colkeys,) 

176 

177 def __call__(self, value): 

178 state = base.instance_state(value) 

179 m = base._state_mapper(state) 

180 key = [ 

181 m._get_state_attr_by_column( 

182 state, state.dict, m.mapped_table.columns[k] 

183 ) 

184 for k in self.colkeys 

185 ] 

186 if self.composite: 

187 return tuple(key) 

188 else: 

189 return key[0] 

190 

191 

192class _SerializableColumnGetterV2(_PlainColumnGetter): 

193 """Updated serializable getter which deals with 

194 multi-table mapped classes. 

195 

196 Two extremely unusual cases are not supported. 

197 Mappings which have tables across multiple metadata 

198 objects, or which are mapped to non-Table selectables 

199 linked across inheriting mappers may fail to function 

200 here. 

201 

202 """ 

203 

204 def __init__(self, colkeys): 

205 self.colkeys = colkeys 

206 self.composite = len(colkeys) > 1 

207 

208 def __reduce__(self): 

209 return self.__class__, (self.colkeys,) 

210 

211 @classmethod 

212 def _reduce_from_cols(cls, cols): 

213 def _table_key(c): 

214 if not isinstance(c.table, expression.TableClause): 

215 return None 

216 else: 

217 return c.table.key 

218 

219 colkeys = [(c.key, _table_key(c)) for c in cols] 

220 return _SerializableColumnGetterV2, (colkeys,) 

221 

222 def _cols(self, mapper): 

223 cols = [] 

224 metadata = getattr(mapper.local_table, "metadata", None) 

225 for (ckey, tkey) in self.colkeys: 

226 if tkey is None or metadata is None or tkey not in metadata: 

227 cols.append(mapper.local_table.c[ckey]) 

228 else: 

229 cols.append(metadata.tables[tkey].c[ckey]) 

230 return cols 

231 

232 

233def column_mapped_collection(mapping_spec): 

234 """A dictionary-based collection type with column-based keying. 

235 

236 Returns a :class:`.MappedCollection` factory with a keying function 

237 generated from mapping_spec, which may be a Column or a sequence 

238 of Columns. 

239 

240 The key value must be immutable for the lifetime of the object. You 

241 can not, for example, map on foreign key values if those key values will 

242 change during the session, i.e. from None to a database-assigned integer 

243 after a session flush. 

244 

245 """ 

246 cols = [ 

247 coercions.expect(roles.ColumnArgumentRole, q, argname="mapping_spec") 

248 for q in util.to_list(mapping_spec) 

249 ] 

250 keyfunc = _PlainColumnGetter(cols) 

251 return lambda: MappedCollection(keyfunc) 

252 

253 

254class _SerializableAttrGetter(object): 

255 def __init__(self, name): 

256 self.name = name 

257 self.getter = operator.attrgetter(name) 

258 

259 def __call__(self, target): 

260 return self.getter(target) 

261 

262 def __reduce__(self): 

263 return _SerializableAttrGetter, (self.name,) 

264 

265 

266def attribute_mapped_collection(attr_name): 

267 """A dictionary-based collection type with attribute-based keying. 

268 

269 Returns a :class:`.MappedCollection` factory with a keying based on the 

270 'attr_name' attribute of entities in the collection, where ``attr_name`` 

271 is the string name of the attribute. 

272 

273 .. warning:: the key value must be assigned to its final value 

274 **before** it is accessed by the attribute mapped collection. 

275 Additionally, changes to the key attribute are **not tracked** 

276 automatically, which means the key in the dictionary is not 

277 automatically synchronized with the key value on the target object 

278 itself. See the section :ref:`key_collections_mutations` 

279 for an example. 

280 

281 """ 

282 getter = _SerializableAttrGetter(attr_name) 

283 return lambda: MappedCollection(getter) 

284 

285 

286def mapped_collection(keyfunc): 

287 """A dictionary-based collection type with arbitrary keying. 

288 

289 Returns a :class:`.MappedCollection` factory with a keying function 

290 generated from keyfunc, a callable that takes an entity and returns a 

291 key value. 

292 

293 The key value must be immutable for the lifetime of the object. You 

294 can not, for example, map on foreign key values if those key values will 

295 change during the session, i.e. from None to a database-assigned integer 

296 after a session flush. 

297 

298 """ 

299 return lambda: MappedCollection(keyfunc) 

300 

301 

302class collection(object): 

303 """Decorators for entity collection classes. 

304 

305 The decorators fall into two groups: annotations and interception recipes. 

306 

307 The annotating decorators (appender, remover, iterator, converter, 

308 internally_instrumented) indicate the method's purpose and take no 

309 arguments. They are not written with parens:: 

310 

311 @collection.appender 

312 def append(self, append): ... 

313 

314 The recipe decorators all require parens, even those that take no 

315 arguments:: 

316 

317 @collection.adds('entity') 

318 def insert(self, position, entity): ... 

319 

320 @collection.removes_return() 

321 def popitem(self): ... 

322 

323 """ 

324 

325 # Bundled as a class solely for ease of use: packaging, doc strings, 

326 # importability. 

327 

328 @staticmethod 

329 def appender(fn): 

330 """Tag the method as the collection appender. 

331 

332 The appender method is called with one positional argument: the value 

333 to append. The method will be automatically decorated with 'adds(1)' 

334 if not already decorated:: 

335 

336 @collection.appender 

337 def add(self, append): ... 

338 

339 # or, equivalently 

340 @collection.appender 

341 @collection.adds(1) 

342 def add(self, append): ... 

343 

344 # for mapping type, an 'append' may kick out a previous value 

345 # that occupies that slot. consider d['a'] = 'foo'- any previous 

346 # value in d['a'] is discarded. 

347 @collection.appender 

348 @collection.replaces(1) 

349 def add(self, entity): 

350 key = some_key_func(entity) 

351 previous = None 

352 if key in self: 

353 previous = self[key] 

354 self[key] = entity 

355 return previous 

356 

357 If the value to append is not allowed in the collection, you may 

358 raise an exception. Something to remember is that the appender 

359 will be called for each object mapped by a database query. If the 

360 database contains rows that violate your collection semantics, you 

361 will need to get creative to fix the problem, as access via the 

362 collection will not work. 

363 

364 If the appender method is internally instrumented, you must also 

365 receive the keyword argument '_sa_initiator' and ensure its 

366 promulgation to collection events. 

367 

368 """ 

369 fn._sa_instrument_role = "appender" 

370 return fn 

371 

372 @staticmethod 

373 def remover(fn): 

374 """Tag the method as the collection remover. 

375 

376 The remover method is called with one positional argument: the value 

377 to remove. The method will be automatically decorated with 

378 :meth:`removes_return` if not already decorated:: 

379 

380 @collection.remover 

381 def zap(self, entity): ... 

382 

383 # or, equivalently 

384 @collection.remover 

385 @collection.removes_return() 

386 def zap(self, ): ... 

387 

388 If the value to remove is not present in the collection, you may 

389 raise an exception or return None to ignore the error. 

390 

391 If the remove method is internally instrumented, you must also 

392 receive the keyword argument '_sa_initiator' and ensure its 

393 promulgation to collection events. 

394 

395 """ 

396 fn._sa_instrument_role = "remover" 

397 return fn 

398 

399 @staticmethod 

400 def iterator(fn): 

401 """Tag the method as the collection remover. 

402 

403 The iterator method is called with no arguments. It is expected to 

404 return an iterator over all collection members:: 

405 

406 @collection.iterator 

407 def __iter__(self): ... 

408 

409 """ 

410 fn._sa_instrument_role = "iterator" 

411 return fn 

412 

413 @staticmethod 

414 def internally_instrumented(fn): 

415 """Tag the method as instrumented. 

416 

417 This tag will prevent any decoration from being applied to the 

418 method. Use this if you are orchestrating your own calls to 

419 :func:`.collection_adapter` in one of the basic SQLAlchemy 

420 interface methods, or to prevent an automatic ABC method 

421 decoration from wrapping your implementation:: 

422 

423 # normally an 'extend' method on a list-like class would be 

424 # automatically intercepted and re-implemented in terms of 

425 # SQLAlchemy events and append(). your implementation will 

426 # never be called, unless: 

427 @collection.internally_instrumented 

428 def extend(self, items): ... 

429 

430 """ 

431 fn._sa_instrumented = True 

432 return fn 

433 

434 @staticmethod 

435 @util.deprecated( 

436 "1.3", 

437 "The :meth:`.collection.converter` handler is deprecated and will " 

438 "be removed in a future release. Please refer to the " 

439 ":class:`.AttributeEvents.bulk_replace` listener interface in " 

440 "conjunction with the :func:`.event.listen` function.", 

441 ) 

442 def converter(fn): 

443 """Tag the method as the collection converter. 

444 

445 This optional method will be called when a collection is being 

446 replaced entirely, as in:: 

447 

448 myobj.acollection = [newvalue1, newvalue2] 

449 

450 The converter method will receive the object being assigned and should 

451 return an iterable of values suitable for use by the ``appender`` 

452 method. A converter must not assign values or mutate the collection, 

453 its sole job is to adapt the value the user provides into an iterable 

454 of values for the ORM's use. 

455 

456 The default converter implementation will use duck-typing to do the 

457 conversion. A dict-like collection will be convert into an iterable 

458 of dictionary values, and other types will simply be iterated:: 

459 

460 @collection.converter 

461 def convert(self, other): ... 

462 

463 If the duck-typing of the object does not match the type of this 

464 collection, a TypeError is raised. 

465 

466 Supply an implementation of this method if you want to expand the 

467 range of possible types that can be assigned in bulk or perform 

468 validation on the values about to be assigned. 

469 

470 """ 

471 fn._sa_instrument_role = "converter" 

472 return fn 

473 

474 @staticmethod 

475 def adds(arg): 

476 """Mark the method as adding an entity to the collection. 

477 

478 Adds "add to collection" handling to the method. The decorator 

479 argument indicates which method argument holds the SQLAlchemy-relevant 

480 value. Arguments can be specified positionally (i.e. integer) or by 

481 name:: 

482 

483 @collection.adds(1) 

484 def push(self, item): ... 

485 

486 @collection.adds('entity') 

487 def do_stuff(self, thing, entity=None): ... 

488 

489 """ 

490 

491 def decorator(fn): 

492 fn._sa_instrument_before = ("fire_append_event", arg) 

493 return fn 

494 

495 return decorator 

496 

497 @staticmethod 

498 def replaces(arg): 

499 """Mark the method as replacing an entity in the collection. 

500 

501 Adds "add to collection" and "remove from collection" handling to 

502 the method. The decorator argument indicates which method argument 

503 holds the SQLAlchemy-relevant value to be added, and return value, if 

504 any will be considered the value to remove. 

505 

506 Arguments can be specified positionally (i.e. integer) or by name:: 

507 

508 @collection.replaces(2) 

509 def __setitem__(self, index, item): ... 

510 

511 """ 

512 

513 def decorator(fn): 

514 fn._sa_instrument_before = ("fire_append_event", arg) 

515 fn._sa_instrument_after = "fire_remove_event" 

516 return fn 

517 

518 return decorator 

519 

520 @staticmethod 

521 def removes(arg): 

522 """Mark the method as removing an entity in the collection. 

523 

524 Adds "remove from collection" handling to the method. The decorator 

525 argument indicates which method argument holds the SQLAlchemy-relevant 

526 value to be removed. Arguments can be specified positionally (i.e. 

527 integer) or by name:: 

528 

529 @collection.removes(1) 

530 def zap(self, item): ... 

531 

532 For methods where the value to remove is not known at call-time, use 

533 collection.removes_return. 

534 

535 """ 

536 

537 def decorator(fn): 

538 fn._sa_instrument_before = ("fire_remove_event", arg) 

539 return fn 

540 

541 return decorator 

542 

543 @staticmethod 

544 def removes_return(): 

545 """Mark the method as removing an entity in the collection. 

546 

547 Adds "remove from collection" handling to the method. The return 

548 value of the method, if any, is considered the value to remove. The 

549 method arguments are not inspected:: 

550 

551 @collection.removes_return() 

552 def pop(self): ... 

553 

554 For methods where the value to remove is known at call-time, use 

555 collection.remove. 

556 

557 """ 

558 

559 def decorator(fn): 

560 fn._sa_instrument_after = "fire_remove_event" 

561 return fn 

562 

563 return decorator 

564 

565 

566collection_adapter = operator.attrgetter("_sa_adapter") 

567"""Fetch the :class:`.CollectionAdapter` for a collection.""" 

568 

569 

570class CollectionAdapter(object): 

571 """Bridges between the ORM and arbitrary Python collections. 

572 

573 Proxies base-level collection operations (append, remove, iterate) 

574 to the underlying Python collection, and emits add/remove events for 

575 entities entering or leaving the collection. 

576 

577 The ORM uses :class:`.CollectionAdapter` exclusively for interaction with 

578 entity collections. 

579 

580 

581 """ 

582 

583 __slots__ = ( 

584 "attr", 

585 "_key", 

586 "_data", 

587 "owner_state", 

588 "_converter", 

589 "invalidated", 

590 "empty", 

591 ) 

592 

593 def __init__(self, attr, owner_state, data): 

594 self.attr = attr 

595 self._key = attr.key 

596 self._data = weakref.ref(data) 

597 self.owner_state = owner_state 

598 data._sa_adapter = self 

599 self._converter = data._sa_converter 

600 self.invalidated = False 

601 self.empty = False 

602 

603 def _warn_invalidated(self): 

604 util.warn("This collection has been invalidated.") 

605 

606 @property 

607 def data(self): 

608 "The entity collection being adapted." 

609 return self._data() 

610 

611 @property 

612 def _referenced_by_owner(self): 

613 """return True if the owner state still refers to this collection. 

614 

615 This will return False within a bulk replace operation, 

616 where this collection is the one being replaced. 

617 

618 """ 

619 return self.owner_state.dict[self._key] is self._data() 

620 

621 def bulk_appender(self): 

622 return self._data()._sa_appender 

623 

624 def append_with_event(self, item, initiator=None): 

625 """Add an entity to the collection, firing mutation events.""" 

626 

627 self._data()._sa_appender(item, _sa_initiator=initiator) 

628 

629 def _set_empty(self, user_data): 

630 assert ( 

631 not self.empty 

632 ), "This collection adapter is already in the 'empty' state" 

633 self.empty = True 

634 self.owner_state._empty_collections[self._key] = user_data 

635 

636 def _reset_empty(self): 

637 assert ( 

638 self.empty 

639 ), "This collection adapter is not in the 'empty' state" 

640 self.empty = False 

641 self.owner_state.dict[ 

642 self._key 

643 ] = self.owner_state._empty_collections.pop(self._key) 

644 

645 def _refuse_empty(self): 

646 raise sa_exc.InvalidRequestError( 

647 "This is a special 'empty' collection which cannot accommodate " 

648 "internal mutation operations" 

649 ) 

650 

651 def append_without_event(self, item): 

652 """Add or restore an entity to the collection, firing no events.""" 

653 

654 if self.empty: 

655 self._refuse_empty() 

656 self._data()._sa_appender(item, _sa_initiator=False) 

657 

658 def append_multiple_without_event(self, items): 

659 """Add or restore an entity to the collection, firing no events.""" 

660 if self.empty: 

661 self._refuse_empty() 

662 appender = self._data()._sa_appender 

663 for item in items: 

664 appender(item, _sa_initiator=False) 

665 

666 def bulk_remover(self): 

667 return self._data()._sa_remover 

668 

669 def remove_with_event(self, item, initiator=None): 

670 """Remove an entity from the collection, firing mutation events.""" 

671 self._data()._sa_remover(item, _sa_initiator=initiator) 

672 

673 def remove_without_event(self, item): 

674 """Remove an entity from the collection, firing no events.""" 

675 if self.empty: 

676 self._refuse_empty() 

677 self._data()._sa_remover(item, _sa_initiator=False) 

678 

679 def clear_with_event(self, initiator=None): 

680 """Empty the collection, firing a mutation event for each entity.""" 

681 

682 if self.empty: 

683 self._refuse_empty() 

684 remover = self._data()._sa_remover 

685 for item in list(self): 

686 remover(item, _sa_initiator=initiator) 

687 

688 def clear_without_event(self): 

689 """Empty the collection, firing no events.""" 

690 

691 if self.empty: 

692 self._refuse_empty() 

693 remover = self._data()._sa_remover 

694 for item in list(self): 

695 remover(item, _sa_initiator=False) 

696 

697 def __iter__(self): 

698 """Iterate over entities in the collection.""" 

699 

700 return iter(self._data()._sa_iterator()) 

701 

702 def __len__(self): 

703 """Count entities in the collection.""" 

704 return len(list(self._data()._sa_iterator())) 

705 

706 def __bool__(self): 

707 return True 

708 

709 __nonzero__ = __bool__ 

710 

711 def fire_append_wo_mutation_event(self, item, initiator=None): 

712 """Notify that a entity is entering the collection but is already 

713 present. 

714 

715 

716 Initiator is a token owned by the InstrumentedAttribute that 

717 initiated the membership mutation, and should be left as None 

718 unless you are passing along an initiator value from a chained 

719 operation. 

720 

721 .. versionadded:: 1.4.15 

722 

723 """ 

724 if initiator is not False: 

725 if self.invalidated: 

726 self._warn_invalidated() 

727 

728 if self.empty: 

729 self._reset_empty() 

730 

731 return self.attr.fire_append_wo_mutation_event( 

732 self.owner_state, self.owner_state.dict, item, initiator 

733 ) 

734 else: 

735 return item 

736 

737 def fire_append_event(self, item, initiator=None): 

738 """Notify that a entity has entered the collection. 

739 

740 Initiator is a token owned by the InstrumentedAttribute that 

741 initiated the membership mutation, and should be left as None 

742 unless you are passing along an initiator value from a chained 

743 operation. 

744 

745 """ 

746 if initiator is not False: 

747 if self.invalidated: 

748 self._warn_invalidated() 

749 

750 if self.empty: 

751 self._reset_empty() 

752 

753 return self.attr.fire_append_event( 

754 self.owner_state, self.owner_state.dict, item, initiator 

755 ) 

756 else: 

757 return item 

758 

759 def fire_remove_event(self, item, initiator=None): 

760 """Notify that a entity has been removed from the collection. 

761 

762 Initiator is the InstrumentedAttribute that initiated the membership 

763 mutation, and should be left as None unless you are passing along 

764 an initiator value from a chained operation. 

765 

766 """ 

767 if initiator is not False: 

768 if self.invalidated: 

769 self._warn_invalidated() 

770 

771 if self.empty: 

772 self._reset_empty() 

773 

774 self.attr.fire_remove_event( 

775 self.owner_state, self.owner_state.dict, item, initiator 

776 ) 

777 

778 def fire_pre_remove_event(self, initiator=None): 

779 """Notify that an entity is about to be removed from the collection. 

780 

781 Only called if the entity cannot be removed after calling 

782 fire_remove_event(). 

783 

784 """ 

785 if self.invalidated: 

786 self._warn_invalidated() 

787 self.attr.fire_pre_remove_event( 

788 self.owner_state, self.owner_state.dict, initiator=initiator 

789 ) 

790 

791 def __getstate__(self): 

792 return { 

793 "key": self._key, 

794 "owner_state": self.owner_state, 

795 "owner_cls": self.owner_state.class_, 

796 "data": self.data, 

797 "invalidated": self.invalidated, 

798 "empty": self.empty, 

799 } 

800 

801 def __setstate__(self, d): 

802 self._key = d["key"] 

803 self.owner_state = d["owner_state"] 

804 self._data = weakref.ref(d["data"]) 

805 self._converter = d["data"]._sa_converter 

806 d["data"]._sa_adapter = self 

807 self.invalidated = d["invalidated"] 

808 self.attr = getattr(d["owner_cls"], self._key).impl 

809 self.empty = d.get("empty", False) 

810 

811 

812def bulk_replace(values, existing_adapter, new_adapter, initiator=None): 

813 """Load a new collection, firing events based on prior like membership. 

814 

815 Appends instances in ``values`` onto the ``new_adapter``. Events will be 

816 fired for any instance not present in the ``existing_adapter``. Any 

817 instances in ``existing_adapter`` not present in ``values`` will have 

818 remove events fired upon them. 

819 

820 :param values: An iterable of collection member instances 

821 

822 :param existing_adapter: A :class:`.CollectionAdapter` of 

823 instances to be replaced 

824 

825 :param new_adapter: An empty :class:`.CollectionAdapter` 

826 to load with ``values`` 

827 

828 

829 """ 

830 

831 assert isinstance(values, list) 

832 

833 idset = util.IdentitySet 

834 existing_idset = idset(existing_adapter or ()) 

835 constants = existing_idset.intersection(values or ()) 

836 additions = idset(values or ()).difference(constants) 

837 removals = existing_idset.difference(constants) 

838 

839 appender = new_adapter.bulk_appender() 

840 

841 for member in values or (): 

842 if member in additions: 

843 appender(member, _sa_initiator=initiator) 

844 elif member in constants: 

845 appender(member, _sa_initiator=False) 

846 

847 if existing_adapter: 

848 for member in removals: 

849 existing_adapter.fire_remove_event(member, initiator=initiator) 

850 

851 

852def prepare_instrumentation(factory): 

853 """Prepare a callable for future use as a collection class factory. 

854 

855 Given a collection class factory (either a type or no-arg callable), 

856 return another factory that will produce compatible instances when 

857 called. 

858 

859 This function is responsible for converting collection_class=list 

860 into the run-time behavior of collection_class=InstrumentedList. 

861 

862 """ 

863 # Convert a builtin to 'Instrumented*' 

864 if factory in __canned_instrumentation: 

865 factory = __canned_instrumentation[factory] 

866 

867 # Create a specimen 

868 cls = type(factory()) 

869 

870 # Did factory callable return a builtin? 

871 if cls in __canned_instrumentation: 

872 # Wrap it so that it returns our 'Instrumented*' 

873 factory = __converting_factory(cls, factory) 

874 cls = factory() 

875 

876 # Instrument the class if needed. 

877 if __instrumentation_mutex.acquire(): 

878 try: 

879 if getattr(cls, "_sa_instrumented", None) != id(cls): 

880 _instrument_class(cls) 

881 finally: 

882 __instrumentation_mutex.release() 

883 

884 return factory 

885 

886 

887def __converting_factory(specimen_cls, original_factory): 

888 """Return a wrapper that converts a "canned" collection like 

889 set, dict, list into the Instrumented* version. 

890 

891 """ 

892 

893 instrumented_cls = __canned_instrumentation[specimen_cls] 

894 

895 def wrapper(): 

896 collection = original_factory() 

897 return instrumented_cls(collection) 

898 

899 # often flawed but better than nothing 

900 wrapper.__name__ = "%sWrapper" % original_factory.__name__ 

901 wrapper.__doc__ = original_factory.__doc__ 

902 

903 return wrapper 

904 

905 

906def _instrument_class(cls): 

907 """Modify methods in a class and install instrumentation.""" 

908 

909 # In the normal call flow, a request for any of the 3 basic collection 

910 # types is transformed into one of our trivial subclasses 

911 # (e.g. InstrumentedList). Catch anything else that sneaks in here... 

912 if cls.__module__ == "__builtin__": 

913 raise sa_exc.ArgumentError( 

914 "Can not instrument a built-in type. Use a " 

915 "subclass, even a trivial one." 

916 ) 

917 

918 roles, methods = _locate_roles_and_methods(cls) 

919 

920 _setup_canned_roles(cls, roles, methods) 

921 

922 _assert_required_roles(cls, roles, methods) 

923 

924 _set_collection_attributes(cls, roles, methods) 

925 

926 

927def _locate_roles_and_methods(cls): 

928 """search for _sa_instrument_role-decorated methods in 

929 method resolution order, assign to roles. 

930 

931 """ 

932 

933 roles = {} 

934 methods = {} 

935 

936 for supercls in cls.__mro__: 

937 for name, method in vars(supercls).items(): 

938 if not callable(method): 

939 continue 

940 

941 # note role declarations 

942 if hasattr(method, "_sa_instrument_role"): 

943 role = method._sa_instrument_role 

944 assert role in ( 

945 "appender", 

946 "remover", 

947 "iterator", 

948 "converter", 

949 ) 

950 roles.setdefault(role, name) 

951 

952 # transfer instrumentation requests from decorated function 

953 # to the combined queue 

954 before, after = None, None 

955 if hasattr(method, "_sa_instrument_before"): 

956 op, argument = method._sa_instrument_before 

957 assert op in ("fire_append_event", "fire_remove_event") 

958 before = op, argument 

959 if hasattr(method, "_sa_instrument_after"): 

960 op = method._sa_instrument_after 

961 assert op in ("fire_append_event", "fire_remove_event") 

962 after = op 

963 if before: 

964 methods[name] = before + (after,) 

965 elif after: 

966 methods[name] = None, None, after 

967 return roles, methods 

968 

969 

970def _setup_canned_roles(cls, roles, methods): 

971 """see if this class has "canned" roles based on a known 

972 collection type (dict, set, list). Apply those roles 

973 as needed to the "roles" dictionary, and also 

974 prepare "decorator" methods 

975 

976 """ 

977 collection_type = util.duck_type_collection(cls) 

978 if collection_type in __interfaces: 

979 canned_roles, decorators = __interfaces[collection_type] 

980 for role, name in canned_roles.items(): 

981 roles.setdefault(role, name) 

982 

983 # apply ABC auto-decoration to methods that need it 

984 for method, decorator in decorators.items(): 

985 fn = getattr(cls, method, None) 

986 if ( 

987 fn 

988 and method not in methods 

989 and not hasattr(fn, "_sa_instrumented") 

990 ): 

991 setattr(cls, method, decorator(fn)) 

992 

993 

994def _assert_required_roles(cls, roles, methods): 

995 """ensure all roles are present, and apply implicit instrumentation if 

996 needed 

997 

998 """ 

999 if "appender" not in roles or not hasattr(cls, roles["appender"]): 

1000 raise sa_exc.ArgumentError( 

1001 "Type %s must elect an appender method to be " 

1002 "a collection class" % cls.__name__ 

1003 ) 

1004 elif roles["appender"] not in methods and not hasattr( 

1005 getattr(cls, roles["appender"]), "_sa_instrumented" 

1006 ): 

1007 methods[roles["appender"]] = ("fire_append_event", 1, None) 

1008 

1009 if "remover" not in roles or not hasattr(cls, roles["remover"]): 

1010 raise sa_exc.ArgumentError( 

1011 "Type %s must elect a remover method to be " 

1012 "a collection class" % cls.__name__ 

1013 ) 

1014 elif roles["remover"] not in methods and not hasattr( 

1015 getattr(cls, roles["remover"]), "_sa_instrumented" 

1016 ): 

1017 methods[roles["remover"]] = ("fire_remove_event", 1, None) 

1018 

1019 if "iterator" not in roles or not hasattr(cls, roles["iterator"]): 

1020 raise sa_exc.ArgumentError( 

1021 "Type %s must elect an iterator method to be " 

1022 "a collection class" % cls.__name__ 

1023 ) 

1024 

1025 

1026def _set_collection_attributes(cls, roles, methods): 

1027 """apply ad-hoc instrumentation from decorators, class-level defaults 

1028 and implicit role declarations 

1029 

1030 """ 

1031 for method_name, (before, argument, after) in methods.items(): 

1032 setattr( 

1033 cls, 

1034 method_name, 

1035 _instrument_membership_mutator( 

1036 getattr(cls, method_name), before, argument, after 

1037 ), 

1038 ) 

1039 # intern the role map 

1040 for role, method_name in roles.items(): 

1041 setattr(cls, "_sa_%s" % role, getattr(cls, method_name)) 

1042 

1043 cls._sa_adapter = None 

1044 

1045 if not hasattr(cls, "_sa_converter"): 

1046 cls._sa_converter = None 

1047 cls._sa_instrumented = id(cls) 

1048 

1049 

1050def _instrument_membership_mutator(method, before, argument, after): 

1051 """Route method args and/or return value through the collection 

1052 adapter.""" 

1053 # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))' 

1054 if before: 

1055 fn_args = list( 

1056 util.flatten_iterator(inspect_getfullargspec(method)[0]) 

1057 ) 

1058 if isinstance(argument, int): 

1059 pos_arg = argument 

1060 named_arg = len(fn_args) > argument and fn_args[argument] or None 

1061 else: 

1062 if argument in fn_args: 

1063 pos_arg = fn_args.index(argument) 

1064 else: 

1065 pos_arg = None 

1066 named_arg = argument 

1067 del fn_args 

1068 

1069 def wrapper(*args, **kw): 

1070 if before: 

1071 if pos_arg is None: 

1072 if named_arg not in kw: 

1073 raise sa_exc.ArgumentError( 

1074 "Missing argument %s" % argument 

1075 ) 

1076 value = kw[named_arg] 

1077 else: 

1078 if len(args) > pos_arg: 

1079 value = args[pos_arg] 

1080 elif named_arg in kw: 

1081 value = kw[named_arg] 

1082 else: 

1083 raise sa_exc.ArgumentError( 

1084 "Missing argument %s" % argument 

1085 ) 

1086 

1087 initiator = kw.pop("_sa_initiator", None) 

1088 if initiator is False: 

1089 executor = None 

1090 else: 

1091 executor = args[0]._sa_adapter 

1092 

1093 if before and executor: 

1094 getattr(executor, before)(value, initiator) 

1095 

1096 if not after or not executor: 

1097 return method(*args, **kw) 

1098 else: 

1099 res = method(*args, **kw) 

1100 if res is not None: 

1101 getattr(executor, after)(res, initiator) 

1102 return res 

1103 

1104 wrapper._sa_instrumented = True 

1105 if hasattr(method, "_sa_instrument_role"): 

1106 wrapper._sa_instrument_role = method._sa_instrument_role 

1107 wrapper.__name__ = method.__name__ 

1108 wrapper.__doc__ = method.__doc__ 

1109 return wrapper 

1110 

1111 

1112def __set_wo_mutation(collection, item, _sa_initiator=None): 

1113 """Run set wo mutation events. 

1114 

1115 The collection is not mutated. 

1116 

1117 """ 

1118 if _sa_initiator is not False: 

1119 executor = collection._sa_adapter 

1120 if executor: 

1121 executor.fire_append_wo_mutation_event(item, _sa_initiator) 

1122 

1123 

1124def __set(collection, item, _sa_initiator=None): 

1125 """Run set events. 

1126 

1127 This event always occurs before the collection is actually mutated. 

1128 

1129 """ 

1130 

1131 if _sa_initiator is not False: 

1132 executor = collection._sa_adapter 

1133 if executor: 

1134 item = executor.fire_append_event(item, _sa_initiator) 

1135 return item 

1136 

1137 

1138def __del(collection, item, _sa_initiator=None): 

1139 """Run del events. 

1140 

1141 This event occurs before the collection is actually mutated, *except* 

1142 in the case of a pop operation, in which case it occurs afterwards. 

1143 For pop operations, the __before_pop hook is called before the 

1144 operation occurs. 

1145 

1146 """ 

1147 if _sa_initiator is not False: 

1148 executor = collection._sa_adapter 

1149 if executor: 

1150 executor.fire_remove_event(item, _sa_initiator) 

1151 

1152 

1153def __before_pop(collection, _sa_initiator=None): 

1154 """An event which occurs on a before a pop() operation occurs.""" 

1155 executor = collection._sa_adapter 

1156 if executor: 

1157 executor.fire_pre_remove_event(_sa_initiator) 

1158 

1159 

1160def _list_decorators(): 

1161 """Tailored instrumentation wrappers for any list-like class.""" 

1162 

1163 def _tidy(fn): 

1164 fn._sa_instrumented = True 

1165 fn.__doc__ = getattr(list, fn.__name__).__doc__ 

1166 

1167 def append(fn): 

1168 def append(self, item, _sa_initiator=None): 

1169 item = __set(self, item, _sa_initiator) 

1170 fn(self, item) 

1171 

1172 _tidy(append) 

1173 return append 

1174 

1175 def remove(fn): 

1176 def remove(self, value, _sa_initiator=None): 

1177 __del(self, value, _sa_initiator) 

1178 # testlib.pragma exempt:__eq__ 

1179 fn(self, value) 

1180 

1181 _tidy(remove) 

1182 return remove 

1183 

1184 def insert(fn): 

1185 def insert(self, index, value): 

1186 value = __set(self, value) 

1187 fn(self, index, value) 

1188 

1189 _tidy(insert) 

1190 return insert 

1191 

1192 def __setitem__(fn): 

1193 def __setitem__(self, index, value): 

1194 if not isinstance(index, slice): 

1195 existing = self[index] 

1196 if existing is not None: 

1197 __del(self, existing) 

1198 value = __set(self, value) 

1199 fn(self, index, value) 

1200 else: 

1201 # slice assignment requires __delitem__, insert, __len__ 

1202 step = index.step or 1 

1203 start = index.start or 0 

1204 if start < 0: 

1205 start += len(self) 

1206 if index.stop is not None: 

1207 stop = index.stop 

1208 else: 

1209 stop = len(self) 

1210 if stop < 0: 

1211 stop += len(self) 

1212 

1213 if step == 1: 

1214 if value is self: 

1215 return 

1216 for i in range(start, stop, step): 

1217 if len(self) > start: 

1218 del self[start] 

1219 

1220 for i, item in enumerate(value): 

1221 self.insert(i + start, item) 

1222 else: 

1223 rng = list(range(start, stop, step)) 

1224 if len(value) != len(rng): 

1225 raise ValueError( 

1226 "attempt to assign sequence of size %s to " 

1227 "extended slice of size %s" 

1228 % (len(value), len(rng)) 

1229 ) 

1230 for i, item in zip(rng, value): 

1231 self.__setitem__(i, item) 

1232 

1233 _tidy(__setitem__) 

1234 return __setitem__ 

1235 

1236 def __delitem__(fn): 

1237 def __delitem__(self, index): 

1238 if not isinstance(index, slice): 

1239 item = self[index] 

1240 __del(self, item) 

1241 fn(self, index) 

1242 else: 

1243 # slice deletion requires __getslice__ and a slice-groking 

1244 # __getitem__ for stepped deletion 

1245 # note: not breaking this into atomic dels 

1246 for item in self[index]: 

1247 __del(self, item) 

1248 fn(self, index) 

1249 

1250 _tidy(__delitem__) 

1251 return __delitem__ 

1252 

1253 if util.py2k: 

1254 

1255 def __setslice__(fn): 

1256 def __setslice__(self, start, end, values): 

1257 for value in self[start:end]: 

1258 __del(self, value) 

1259 values = [__set(self, value) for value in values] 

1260 fn(self, start, end, values) 

1261 

1262 _tidy(__setslice__) 

1263 return __setslice__ 

1264 

1265 def __delslice__(fn): 

1266 def __delslice__(self, start, end): 

1267 for value in self[start:end]: 

1268 __del(self, value) 

1269 fn(self, start, end) 

1270 

1271 _tidy(__delslice__) 

1272 return __delslice__ 

1273 

1274 def extend(fn): 

1275 def extend(self, iterable): 

1276 for value in list(iterable): 

1277 self.append(value) 

1278 

1279 _tidy(extend) 

1280 return extend 

1281 

1282 def __iadd__(fn): 

1283 def __iadd__(self, iterable): 

1284 # list.__iadd__ takes any iterable and seems to let TypeError 

1285 # raise as-is instead of returning NotImplemented 

1286 for value in list(iterable): 

1287 self.append(value) 

1288 return self 

1289 

1290 _tidy(__iadd__) 

1291 return __iadd__ 

1292 

1293 def pop(fn): 

1294 def pop(self, index=-1): 

1295 __before_pop(self) 

1296 item = fn(self, index) 

1297 __del(self, item) 

1298 return item 

1299 

1300 _tidy(pop) 

1301 return pop 

1302 

1303 if not util.py2k: 

1304 

1305 def clear(fn): 

1306 def clear(self, index=-1): 

1307 for item in self: 

1308 __del(self, item) 

1309 fn(self) 

1310 

1311 _tidy(clear) 

1312 return clear 

1313 

1314 # __imul__ : not wrapping this. all members of the collection are already 

1315 # present, so no need to fire appends... wrapping it with an explicit 

1316 # decorator is still possible, so events on *= can be had if they're 

1317 # desired. hard to imagine a use case for __imul__, though. 

1318 

1319 l = locals().copy() 

1320 l.pop("_tidy") 

1321 return l 

1322 

1323 

1324def _dict_decorators(): 

1325 """Tailored instrumentation wrappers for any dict-like mapping class.""" 

1326 

1327 def _tidy(fn): 

1328 fn._sa_instrumented = True 

1329 fn.__doc__ = getattr(dict, fn.__name__).__doc__ 

1330 

1331 Unspecified = util.symbol("Unspecified") 

1332 

1333 def __setitem__(fn): 

1334 def __setitem__(self, key, value, _sa_initiator=None): 

1335 if key in self: 

1336 __del(self, self[key], _sa_initiator) 

1337 value = __set(self, value, _sa_initiator) 

1338 fn(self, key, value) 

1339 

1340 _tidy(__setitem__) 

1341 return __setitem__ 

1342 

1343 def __delitem__(fn): 

1344 def __delitem__(self, key, _sa_initiator=None): 

1345 if key in self: 

1346 __del(self, self[key], _sa_initiator) 

1347 fn(self, key) 

1348 

1349 _tidy(__delitem__) 

1350 return __delitem__ 

1351 

1352 def clear(fn): 

1353 def clear(self): 

1354 for key in self: 

1355 __del(self, self[key]) 

1356 fn(self) 

1357 

1358 _tidy(clear) 

1359 return clear 

1360 

1361 def pop(fn): 

1362 def pop(self, key, default=Unspecified): 

1363 __before_pop(self) 

1364 _to_del = key in self 

1365 if default is Unspecified: 

1366 item = fn(self, key) 

1367 else: 

1368 item = fn(self, key, default) 

1369 if _to_del: 

1370 __del(self, item) 

1371 return item 

1372 

1373 _tidy(pop) 

1374 return pop 

1375 

1376 def popitem(fn): 

1377 def popitem(self): 

1378 __before_pop(self) 

1379 item = fn(self) 

1380 __del(self, item[1]) 

1381 return item 

1382 

1383 _tidy(popitem) 

1384 return popitem 

1385 

1386 def setdefault(fn): 

1387 def setdefault(self, key, default=None): 

1388 if key not in self: 

1389 self.__setitem__(key, default) 

1390 return default 

1391 else: 

1392 value = self.__getitem__(key) 

1393 if value is default: 

1394 __set_wo_mutation(self, value, None) 

1395 

1396 return value 

1397 

1398 _tidy(setdefault) 

1399 return setdefault 

1400 

1401 def update(fn): 

1402 def update(self, __other=Unspecified, **kw): 

1403 if __other is not Unspecified: 

1404 if hasattr(__other, "keys"): 

1405 for key in list(__other): 

1406 if key not in self or self[key] is not __other[key]: 

1407 self[key] = __other[key] 

1408 else: 

1409 __set_wo_mutation(self, __other[key], None) 

1410 else: 

1411 for key, value in __other: 

1412 if key not in self or self[key] is not value: 

1413 self[key] = value 

1414 else: 

1415 __set_wo_mutation(self, value, None) 

1416 for key in kw: 

1417 if key not in self or self[key] is not kw[key]: 

1418 self[key] = kw[key] 

1419 else: 

1420 __set_wo_mutation(self, kw[key], None) 

1421 

1422 _tidy(update) 

1423 return update 

1424 

1425 l = locals().copy() 

1426 l.pop("_tidy") 

1427 l.pop("Unspecified") 

1428 return l 

1429 

1430 

1431_set_binop_bases = (set, frozenset) 

1432 

1433 

1434def _set_binops_check_strict(self, obj): 

1435 """Allow only set, frozenset and self.__class__-derived 

1436 objects in binops.""" 

1437 return isinstance(obj, _set_binop_bases + (self.__class__,)) 

1438 

1439 

1440def _set_binops_check_loose(self, obj): 

1441 """Allow anything set-like to participate in set binops.""" 

1442 return ( 

1443 isinstance(obj, _set_binop_bases + (self.__class__,)) 

1444 or util.duck_type_collection(obj) == set 

1445 ) 

1446 

1447 

1448def _set_decorators(): 

1449 """Tailored instrumentation wrappers for any set-like class.""" 

1450 

1451 def _tidy(fn): 

1452 fn._sa_instrumented = True 

1453 fn.__doc__ = getattr(set, fn.__name__).__doc__ 

1454 

1455 Unspecified = util.symbol("Unspecified") 

1456 

1457 def add(fn): 

1458 def add(self, value, _sa_initiator=None): 

1459 if value not in self: 

1460 value = __set(self, value, _sa_initiator) 

1461 else: 

1462 __set_wo_mutation(self, value, _sa_initiator) 

1463 # testlib.pragma exempt:__hash__ 

1464 fn(self, value) 

1465 

1466 _tidy(add) 

1467 return add 

1468 

1469 def discard(fn): 

1470 def discard(self, value, _sa_initiator=None): 

1471 # testlib.pragma exempt:__hash__ 

1472 if value in self: 

1473 __del(self, value, _sa_initiator) 

1474 # testlib.pragma exempt:__hash__ 

1475 fn(self, value) 

1476 

1477 _tidy(discard) 

1478 return discard 

1479 

1480 def remove(fn): 

1481 def remove(self, value, _sa_initiator=None): 

1482 # testlib.pragma exempt:__hash__ 

1483 if value in self: 

1484 __del(self, value, _sa_initiator) 

1485 # testlib.pragma exempt:__hash__ 

1486 fn(self, value) 

1487 

1488 _tidy(remove) 

1489 return remove 

1490 

1491 def pop(fn): 

1492 def pop(self): 

1493 __before_pop(self) 

1494 item = fn(self) 

1495 # for set in particular, we have no way to access the item 

1496 # that will be popped before pop is called. 

1497 __del(self, item) 

1498 return item 

1499 

1500 _tidy(pop) 

1501 return pop 

1502 

1503 def clear(fn): 

1504 def clear(self): 

1505 for item in list(self): 

1506 self.remove(item) 

1507 

1508 _tidy(clear) 

1509 return clear 

1510 

1511 def update(fn): 

1512 def update(self, value): 

1513 for item in value: 

1514 self.add(item) 

1515 

1516 _tidy(update) 

1517 return update 

1518 

1519 def __ior__(fn): 

1520 def __ior__(self, value): 

1521 if not _set_binops_check_strict(self, value): 

1522 return NotImplemented 

1523 for item in value: 

1524 self.add(item) 

1525 return self 

1526 

1527 _tidy(__ior__) 

1528 return __ior__ 

1529 

1530 def difference_update(fn): 

1531 def difference_update(self, value): 

1532 for item in value: 

1533 self.discard(item) 

1534 

1535 _tidy(difference_update) 

1536 return difference_update 

1537 

1538 def __isub__(fn): 

1539 def __isub__(self, value): 

1540 if not _set_binops_check_strict(self, value): 

1541 return NotImplemented 

1542 for item in value: 

1543 self.discard(item) 

1544 return self 

1545 

1546 _tidy(__isub__) 

1547 return __isub__ 

1548 

1549 def intersection_update(fn): 

1550 def intersection_update(self, other): 

1551 want, have = self.intersection(other), set(self) 

1552 remove, add = have - want, want - have 

1553 

1554 for item in remove: 

1555 self.remove(item) 

1556 for item in add: 

1557 self.add(item) 

1558 

1559 _tidy(intersection_update) 

1560 return intersection_update 

1561 

1562 def __iand__(fn): 

1563 def __iand__(self, other): 

1564 if not _set_binops_check_strict(self, other): 

1565 return NotImplemented 

1566 want, have = self.intersection(other), set(self) 

1567 remove, add = have - want, want - have 

1568 

1569 for item in remove: 

1570 self.remove(item) 

1571 for item in add: 

1572 self.add(item) 

1573 return self 

1574 

1575 _tidy(__iand__) 

1576 return __iand__ 

1577 

1578 def symmetric_difference_update(fn): 

1579 def symmetric_difference_update(self, other): 

1580 want, have = self.symmetric_difference(other), set(self) 

1581 remove, add = have - want, want - have 

1582 

1583 for item in remove: 

1584 self.remove(item) 

1585 for item in add: 

1586 self.add(item) 

1587 

1588 _tidy(symmetric_difference_update) 

1589 return symmetric_difference_update 

1590 

1591 def __ixor__(fn): 

1592 def __ixor__(self, other): 

1593 if not _set_binops_check_strict(self, other): 

1594 return NotImplemented 

1595 want, have = self.symmetric_difference(other), set(self) 

1596 remove, add = have - want, want - have 

1597 

1598 for item in remove: 

1599 self.remove(item) 

1600 for item in add: 

1601 self.add(item) 

1602 return self 

1603 

1604 _tidy(__ixor__) 

1605 return __ixor__ 

1606 

1607 l = locals().copy() 

1608 l.pop("_tidy") 

1609 l.pop("Unspecified") 

1610 return l 

1611 

1612 

1613class InstrumentedList(list): 

1614 """An instrumented version of the built-in list.""" 

1615 

1616 

1617class InstrumentedSet(set): 

1618 """An instrumented version of the built-in set.""" 

1619 

1620 

1621class InstrumentedDict(dict): 

1622 """An instrumented version of the built-in dict.""" 

1623 

1624 

1625__canned_instrumentation = { 

1626 list: InstrumentedList, 

1627 set: InstrumentedSet, 

1628 dict: InstrumentedDict, 

1629} 

1630 

1631__interfaces = { 

1632 list: ( 

1633 {"appender": "append", "remover": "remove", "iterator": "__iter__"}, 

1634 _list_decorators(), 

1635 ), 

1636 set: ( 

1637 {"appender": "add", "remover": "remove", "iterator": "__iter__"}, 

1638 _set_decorators(), 

1639 ), 

1640 # decorators are required for dicts and object collections. 

1641 dict: ({"iterator": "values"}, _dict_decorators()) 

1642 if util.py3k 

1643 else ({"iterator": "itervalues"}, _dict_decorators()), 

1644} 

1645 

1646 

1647class MappedCollection(dict): 

1648 """A basic dictionary-based collection class. 

1649 

1650 Extends dict with the minimal bag semantics that collection 

1651 classes require. ``set`` and ``remove`` are implemented in terms 

1652 of a keying function: any callable that takes an object and 

1653 returns an object for use as a dictionary key. 

1654 

1655 """ 

1656 

1657 def __init__(self, keyfunc): 

1658 """Create a new collection with keying provided by keyfunc. 

1659 

1660 keyfunc may be any callable that takes an object and returns an object 

1661 for use as a dictionary key. 

1662 

1663 The keyfunc will be called every time the ORM needs to add a member by 

1664 value-only (such as when loading instances from the database) or 

1665 remove a member. The usual cautions about dictionary keying apply- 

1666 ``keyfunc(object)`` should return the same output for the life of the 

1667 collection. Keying based on mutable properties can result in 

1668 unreachable instances "lost" in the collection. 

1669 

1670 """ 

1671 self.keyfunc = keyfunc 

1672 

1673 @collection.appender 

1674 @collection.internally_instrumented 

1675 def set(self, value, _sa_initiator=None): 

1676 """Add an item by value, consulting the keyfunc for the key.""" 

1677 

1678 key = self.keyfunc(value) 

1679 self.__setitem__(key, value, _sa_initiator) 

1680 

1681 @collection.remover 

1682 @collection.internally_instrumented 

1683 def remove(self, value, _sa_initiator=None): 

1684 """Remove an item by value, consulting the keyfunc for the key.""" 

1685 

1686 key = self.keyfunc(value) 

1687 # Let self[key] raise if key is not in this collection 

1688 # testlib.pragma exempt:__ne__ 

1689 if self[key] != value: 

1690 raise sa_exc.InvalidRequestError( 

1691 "Can not remove '%s': collection holds '%s' for key '%s'. " 

1692 "Possible cause: is the MappedCollection key function " 

1693 "based on mutable properties or properties that only obtain " 

1694 "values after flush?" % (value, self[key], key) 

1695 ) 

1696 self.__delitem__(key, _sa_initiator) 

1697 

1698 

1699# ensure instrumentation is associated with 

1700# these built-in classes; if a user-defined class 

1701# subclasses these and uses @internally_instrumented, 

1702# the superclass is otherwise not instrumented. 

1703# see [ticket:2406]. 

1704_instrument_class(MappedCollection) 

1705_instrument_class(InstrumentedList) 

1706_instrument_class(InstrumentedSet)