Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# orm/attributes.py 

2# Copyright (C) 2005-2020 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: http://www.opensource.org/licenses/mit-license.php 

7 

8"""Defines instrumentation for class attributes and their interaction 

9with instances. 

10 

11This module is usually not directly visible to user applications, but 

12defines a large part of the ORM's interactivity. 

13 

14 

15""" 

16 

17import operator 

18 

19from . import collections 

20from . import exc as orm_exc 

21from . import interfaces 

22from .base import ATTR_EMPTY 

23from .base import ATTR_WAS_SET 

24from .base import CALLABLES_OK 

25from .base import INIT_OK 

26from .base import instance_dict 

27from .base import instance_state 

28from .base import instance_str 

29from .base import LOAD_AGAINST_COMMITTED 

30from .base import manager_of_class 

31from .base import NEVER_SET 

32from .base import NO_AUTOFLUSH 

33from .base import NO_CHANGE # noqa 

34from .base import NO_RAISE 

35from .base import NO_VALUE 

36from .base import NON_PERSISTENT_OK # noqa 

37from .base import PASSIVE_CLASS_MISMATCH # noqa 

38from .base import PASSIVE_NO_FETCH 

39from .base import PASSIVE_NO_FETCH_RELATED # noqa 

40from .base import PASSIVE_NO_INITIALIZE 

41from .base import PASSIVE_NO_RESULT 

42from .base import PASSIVE_OFF 

43from .base import PASSIVE_ONLY_PERSISTENT 

44from .base import PASSIVE_RETURN_NEVER_SET 

45from .base import RELATED_OBJECT_OK # noqa 

46from .base import SQL_OK # noqa 

47from .base import state_str 

48from .. import event 

49from .. import inspection 

50from .. import util 

51 

52 

53@inspection._self_inspects 

54class QueryableAttribute( 

55 interfaces._MappedAttribute, 

56 interfaces.InspectionAttr, 

57 interfaces.PropComparator, 

58): 

59 """Base class for :term:`descriptor` objects that intercept 

60 attribute events on behalf of a :class:`.MapperProperty` 

61 object. The actual :class:`.MapperProperty` is accessible 

62 via the :attr:`.QueryableAttribute.property` 

63 attribute. 

64 

65 

66 .. seealso:: 

67 

68 :class:`.InstrumentedAttribute` 

69 

70 :class:`.MapperProperty` 

71 

72 :attr:`_orm.Mapper.all_orm_descriptors` 

73 

74 :attr:`_orm.Mapper.attrs` 

75 """ 

76 

77 is_attribute = True 

78 

79 def __init__( 

80 self, 

81 class_, 

82 key, 

83 impl=None, 

84 comparator=None, 

85 parententity=None, 

86 of_type=None, 

87 ): 

88 self.class_ = class_ 

89 self.key = key 

90 self.impl = impl 

91 self.comparator = comparator 

92 self._parententity = parententity 

93 self._of_type = of_type 

94 

95 manager = manager_of_class(class_) 

96 # manager is None in the case of AliasedClass 

97 if manager: 

98 # propagate existing event listeners from 

99 # immediate superclass 

100 for base in manager._bases: 

101 if key in base: 

102 self.dispatch._update(base[key].dispatch) 

103 if base[key].dispatch._active_history: 

104 self.dispatch._active_history = True 

105 

106 @util.memoized_property 

107 def _supports_population(self): 

108 return self.impl.supports_population 

109 

110 @property 

111 def _impl_uses_objects(self): 

112 return self.impl.uses_objects 

113 

114 def get_history(self, instance, passive=PASSIVE_OFF): 

115 return self.impl.get_history( 

116 instance_state(instance), instance_dict(instance), passive 

117 ) 

118 

119 def __selectable__(self): 

120 # TODO: conditionally attach this method based on clause_element ? 

121 return self 

122 

123 @util.memoized_property 

124 def info(self): 

125 """Return the 'info' dictionary for the underlying SQL element. 

126 

127 The behavior here is as follows: 

128 

129 * If the attribute is a column-mapped property, i.e. 

130 :class:`.ColumnProperty`, which is mapped directly 

131 to a schema-level :class:`_schema.Column` object, this attribute 

132 will return the :attr:`.SchemaItem.info` dictionary associated 

133 with the core-level :class:`_schema.Column` object. 

134 

135 * If the attribute is a :class:`.ColumnProperty` but is mapped to 

136 any other kind of SQL expression other than a 

137 :class:`_schema.Column`, 

138 the attribute will refer to the :attr:`.MapperProperty.info` 

139 dictionary associated directly with the :class:`.ColumnProperty`, 

140 assuming the SQL expression itself does not have its own ``.info`` 

141 attribute (which should be the case, unless a user-defined SQL 

142 construct has defined one). 

143 

144 * If the attribute refers to any other kind of 

145 :class:`.MapperProperty`, including :class:`.RelationshipProperty`, 

146 the attribute will refer to the :attr:`.MapperProperty.info` 

147 dictionary associated with that :class:`.MapperProperty`. 

148 

149 * To access the :attr:`.MapperProperty.info` dictionary of the 

150 :class:`.MapperProperty` unconditionally, including for a 

151 :class:`.ColumnProperty` that's associated directly with a 

152 :class:`_schema.Column`, the attribute can be referred to using 

153 :attr:`.QueryableAttribute.property` attribute, as 

154 ``MyClass.someattribute.property.info``. 

155 

156 .. seealso:: 

157 

158 :attr:`.SchemaItem.info` 

159 

160 :attr:`.MapperProperty.info` 

161 

162 """ 

163 return self.comparator.info 

164 

165 @util.memoized_property 

166 def parent(self): 

167 """Return an inspection instance representing the parent. 

168 

169 This will be either an instance of :class:`_orm.Mapper` 

170 or :class:`.AliasedInsp`, depending upon the nature 

171 of the parent entity which this attribute is associated 

172 with. 

173 

174 """ 

175 return inspection.inspect(self._parententity) 

176 

177 @property 

178 def expression(self): 

179 return self.comparator.__clause_element__() 

180 

181 def __clause_element__(self): 

182 return self.comparator.__clause_element__() 

183 

184 def _query_clause_element(self): 

185 """like __clause_element__(), but called specifically 

186 by :class:`_query.Query` to allow special behavior.""" 

187 

188 return self.comparator._query_clause_element() 

189 

190 def _bulk_update_tuples(self, value): 

191 """Return setter tuples for a bulk UPDATE.""" 

192 

193 return self.comparator._bulk_update_tuples(value) 

194 

195 def adapt_to_entity(self, adapt_to_entity): 

196 assert not self._of_type 

197 return self.__class__( 

198 adapt_to_entity.entity, 

199 self.key, 

200 impl=self.impl, 

201 comparator=self.comparator.adapt_to_entity(adapt_to_entity), 

202 parententity=adapt_to_entity, 

203 ) 

204 

205 def of_type(self, cls): 

206 return QueryableAttribute( 

207 self.class_, 

208 self.key, 

209 self.impl, 

210 self.comparator.of_type(cls), 

211 self._parententity, 

212 of_type=cls, 

213 ) 

214 

215 def label(self, name): 

216 return self._query_clause_element().label(name) 

217 

218 def operate(self, op, *other, **kwargs): 

219 return op(self.comparator, *other, **kwargs) 

220 

221 def reverse_operate(self, op, other, **kwargs): 

222 return op(other, self.comparator, **kwargs) 

223 

224 def hasparent(self, state, optimistic=False): 

225 return self.impl.hasparent(state, optimistic=optimistic) is not False 

226 

227 def __getattr__(self, key): 

228 try: 

229 return getattr(self.comparator, key) 

230 except AttributeError as err: 

231 util.raise_( 

232 AttributeError( 

233 "Neither %r object nor %r object associated with %s " 

234 "has an attribute %r" 

235 % ( 

236 type(self).__name__, 

237 type(self.comparator).__name__, 

238 self, 

239 key, 

240 ) 

241 ), 

242 replace_context=err, 

243 ) 

244 

245 def __str__(self): 

246 return "%s.%s" % (self.class_.__name__, self.key) 

247 

248 @util.memoized_property 

249 def property(self): 

250 """Return the :class:`.MapperProperty` associated with this 

251 :class:`.QueryableAttribute`. 

252 

253 

254 Return values here will commonly be instances of 

255 :class:`.ColumnProperty` or :class:`.RelationshipProperty`. 

256 

257 

258 """ 

259 return self.comparator.property 

260 

261 

262class InstrumentedAttribute(QueryableAttribute): 

263 """Class bound instrumented attribute which adds basic 

264 :term:`descriptor` methods. 

265 

266 See :class:`.QueryableAttribute` for a description of most features. 

267 

268 

269 """ 

270 

271 def __set__(self, instance, value): 

272 self.impl.set( 

273 instance_state(instance), instance_dict(instance), value, None 

274 ) 

275 

276 def __delete__(self, instance): 

277 self.impl.delete(instance_state(instance), instance_dict(instance)) 

278 

279 def __get__(self, instance, owner): 

280 if instance is None: 

281 return self 

282 

283 dict_ = instance_dict(instance) 

284 if self._supports_population and self.key in dict_: 

285 return dict_[self.key] 

286 else: 

287 return self.impl.get(instance_state(instance), dict_) 

288 

289 

290def create_proxied_attribute(descriptor): 

291 """Create an QueryableAttribute / user descriptor hybrid. 

292 

293 Returns a new QueryableAttribute type that delegates descriptor 

294 behavior and getattr() to the given descriptor. 

295 """ 

296 

297 # TODO: can move this to descriptor_props if the need for this 

298 # function is removed from ext/hybrid.py 

299 

300 class Proxy(QueryableAttribute): 

301 """Presents the :class:`.QueryableAttribute` interface as a 

302 proxy on top of a Python descriptor / :class:`.PropComparator` 

303 combination. 

304 

305 """ 

306 

307 def __init__( 

308 self, 

309 class_, 

310 key, 

311 descriptor, 

312 comparator, 

313 adapt_to_entity=None, 

314 doc=None, 

315 original_property=None, 

316 ): 

317 self.class_ = class_ 

318 self.key = key 

319 self.descriptor = descriptor 

320 self.original_property = original_property 

321 self._comparator = comparator 

322 self._adapt_to_entity = adapt_to_entity 

323 self.__doc__ = doc 

324 

325 _is_internal_proxy = True 

326 

327 @property 

328 def _impl_uses_objects(self): 

329 return ( 

330 self.original_property is not None 

331 and getattr(self.class_, self.key).impl.uses_objects 

332 ) 

333 

334 @property 

335 def property(self): 

336 return self.comparator.property 

337 

338 @util.memoized_property 

339 def comparator(self): 

340 if util.callable(self._comparator): 

341 self._comparator = self._comparator() 

342 if self._adapt_to_entity: 

343 self._comparator = self._comparator.adapt_to_entity( 

344 self._adapt_to_entity 

345 ) 

346 return self._comparator 

347 

348 def adapt_to_entity(self, adapt_to_entity): 

349 return self.__class__( 

350 adapt_to_entity.entity, 

351 self.key, 

352 self.descriptor, 

353 self._comparator, 

354 adapt_to_entity, 

355 ) 

356 

357 def __get__(self, instance, owner): 

358 retval = self.descriptor.__get__(instance, owner) 

359 # detect if this is a plain Python @property, which just returns 

360 # itself for class level access. If so, then return us. 

361 # Otherwise, return the object returned by the descriptor. 

362 if retval is self.descriptor and instance is None: 

363 return self 

364 else: 

365 return retval 

366 

367 def __str__(self): 

368 return "%s.%s" % (self.class_.__name__, self.key) 

369 

370 def __getattr__(self, attribute): 

371 """Delegate __getattr__ to the original descriptor and/or 

372 comparator.""" 

373 try: 

374 return getattr(descriptor, attribute) 

375 except AttributeError as err: 

376 if attribute == "comparator": 

377 util.raise_( 

378 AttributeError("comparator"), replace_context=err 

379 ) 

380 try: 

381 # comparator itself might be unreachable 

382 comparator = self.comparator 

383 except AttributeError as err2: 

384 util.raise_( 

385 AttributeError( 

386 "Neither %r object nor unconfigured comparator " 

387 "object associated with %s has an attribute %r" 

388 % (type(descriptor).__name__, self, attribute) 

389 ), 

390 replace_context=err2, 

391 ) 

392 else: 

393 try: 

394 return getattr(comparator, attribute) 

395 except AttributeError as err3: 

396 util.raise_( 

397 AttributeError( 

398 "Neither %r object nor %r object " 

399 "associated with %s has an attribute %r" 

400 % ( 

401 type(descriptor).__name__, 

402 type(comparator).__name__, 

403 self, 

404 attribute, 

405 ) 

406 ), 

407 replace_context=err3, 

408 ) 

409 

410 Proxy.__name__ = type(descriptor).__name__ + "Proxy" 

411 

412 util.monkeypatch_proxied_specials( 

413 Proxy, type(descriptor), name="descriptor", from_instance=descriptor 

414 ) 

415 return Proxy 

416 

417 

418OP_REMOVE = util.symbol("REMOVE") 

419OP_APPEND = util.symbol("APPEND") 

420OP_REPLACE = util.symbol("REPLACE") 

421OP_BULK_REPLACE = util.symbol("BULK_REPLACE") 

422OP_MODIFIED = util.symbol("MODIFIED") 

423 

424 

425class Event(object): 

426 """A token propagated throughout the course of a chain of attribute 

427 events. 

428 

429 Serves as an indicator of the source of the event and also provides 

430 a means of controlling propagation across a chain of attribute 

431 operations. 

432 

433 The :class:`.Event` object is sent as the ``initiator`` argument 

434 when dealing with events such as :meth:`.AttributeEvents.append`, 

435 :meth:`.AttributeEvents.set`, 

436 and :meth:`.AttributeEvents.remove`. 

437 

438 The :class:`.Event` object is currently interpreted by the backref 

439 event handlers, and is used to control the propagation of operations 

440 across two mutually-dependent attributes. 

441 

442 .. versionadded:: 0.9.0 

443 

444 :attribute impl: The :class:`.AttributeImpl` which is the current event 

445 initiator. 

446 

447 :attribute op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`, 

448 :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the 

449 source operation. 

450 

451 """ 

452 

453 __slots__ = "impl", "op", "parent_token" 

454 

455 def __init__(self, attribute_impl, op): 

456 self.impl = attribute_impl 

457 self.op = op 

458 self.parent_token = self.impl.parent_token 

459 

460 def __eq__(self, other): 

461 return ( 

462 isinstance(other, Event) 

463 and other.impl is self.impl 

464 and other.op == self.op 

465 ) 

466 

467 @property 

468 def key(self): 

469 return self.impl.key 

470 

471 def hasparent(self, state): 

472 return self.impl.hasparent(state) 

473 

474 

475class AttributeImpl(object): 

476 """internal implementation for instrumented attributes.""" 

477 

478 def __init__( 

479 self, 

480 class_, 

481 key, 

482 callable_, 

483 dispatch, 

484 trackparent=False, 

485 extension=None, 

486 compare_function=None, 

487 active_history=False, 

488 parent_token=None, 

489 expire_missing=True, 

490 send_modified_events=True, 

491 accepts_scalar_loader=None, 

492 **kwargs 

493 ): 

494 r"""Construct an AttributeImpl. 

495 

496 :param \class_: associated class 

497 

498 :param key: string name of the attribute 

499 

500 :param \callable_: 

501 optional function which generates a callable based on a parent 

502 instance, which produces the "default" values for a scalar or 

503 collection attribute when it's first accessed, if not present 

504 already. 

505 

506 :param trackparent: 

507 if True, attempt to track if an instance has a parent attached 

508 to it via this attribute. 

509 

510 :param extension: 

511 a single or list of AttributeExtension object(s) which will 

512 receive set/delete/append/remove/etc. events. 

513 The event package is now used. 

514 

515 .. deprecated:: 1.3 

516 

517 The :paramref:`.AttributeImpl.extension` parameter is deprecated 

518 and will be removed in a future release, corresponding to the 

519 "extension" parameter on the :class:`.MapperProprty` classes 

520 like :func:`.column_property` and :func:`_orm.relationship` The 

521 events system is now used. 

522 

523 :param compare_function: 

524 a function that compares two values which are normally 

525 assignable to this attribute. 

526 

527 :param active_history: 

528 indicates that get_history() should always return the "old" value, 

529 even if it means executing a lazy callable upon attribute change. 

530 

531 :param parent_token: 

532 Usually references the MapperProperty, used as a key for 

533 the hasparent() function to identify an "owning" attribute. 

534 Allows multiple AttributeImpls to all match a single 

535 owner attribute. 

536 

537 :param expire_missing: 

538 if False, don't add an "expiry" callable to this attribute 

539 during state.expire_attributes(None), if no value is present 

540 for this key. 

541 

542 :param send_modified_events: 

543 if False, the InstanceState._modified_event method will have no 

544 effect; this means the attribute will never show up as changed in a 

545 history entry. 

546 

547 """ 

548 self.class_ = class_ 

549 self.key = key 

550 self.callable_ = callable_ 

551 self.dispatch = dispatch 

552 self.trackparent = trackparent 

553 self.parent_token = parent_token or self 

554 self.send_modified_events = send_modified_events 

555 if compare_function is None: 

556 self.is_equal = operator.eq 

557 else: 

558 self.is_equal = compare_function 

559 

560 if accepts_scalar_loader is not None: 

561 self.accepts_scalar_loader = accepts_scalar_loader 

562 else: 

563 self.accepts_scalar_loader = self.default_accepts_scalar_loader 

564 

565 # TODO: pass in the manager here 

566 # instead of doing a lookup 

567 attr = manager_of_class(class_)[key] 

568 

569 for ext in util.to_list(extension or []): 

570 ext._adapt_listener(attr, ext) 

571 

572 if active_history: 

573 self.dispatch._active_history = True 

574 

575 self.expire_missing = expire_missing 

576 self._modified_token = Event(self, OP_MODIFIED) 

577 

578 __slots__ = ( 

579 "class_", 

580 "key", 

581 "callable_", 

582 "dispatch", 

583 "trackparent", 

584 "parent_token", 

585 "send_modified_events", 

586 "is_equal", 

587 "expire_missing", 

588 "_modified_token", 

589 "accepts_scalar_loader", 

590 ) 

591 

592 def __str__(self): 

593 return "%s.%s" % (self.class_.__name__, self.key) 

594 

595 def _get_active_history(self): 

596 """Backwards compat for impl.active_history""" 

597 

598 return self.dispatch._active_history 

599 

600 def _set_active_history(self, value): 

601 self.dispatch._active_history = value 

602 

603 active_history = property(_get_active_history, _set_active_history) 

604 

605 def hasparent(self, state, optimistic=False): 

606 """Return the boolean value of a `hasparent` flag attached to 

607 the given state. 

608 

609 The `optimistic` flag determines what the default return value 

610 should be if no `hasparent` flag can be located. 

611 

612 As this function is used to determine if an instance is an 

613 *orphan*, instances that were loaded from storage should be 

614 assumed to not be orphans, until a True/False value for this 

615 flag is set. 

616 

617 An instance attribute that is loaded by a callable function 

618 will also not have a `hasparent` flag. 

619 

620 """ 

621 msg = "This AttributeImpl is not configured to track parents." 

622 assert self.trackparent, msg 

623 

624 return ( 

625 state.parents.get(id(self.parent_token), optimistic) is not False 

626 ) 

627 

628 def sethasparent(self, state, parent_state, value): 

629 """Set a boolean flag on the given item corresponding to 

630 whether or not it is attached to a parent object via the 

631 attribute represented by this ``InstrumentedAttribute``. 

632 

633 """ 

634 msg = "This AttributeImpl is not configured to track parents." 

635 assert self.trackparent, msg 

636 

637 id_ = id(self.parent_token) 

638 if value: 

639 state.parents[id_] = parent_state 

640 else: 

641 if id_ in state.parents: 

642 last_parent = state.parents[id_] 

643 

644 if ( 

645 last_parent is not False 

646 and last_parent.key != parent_state.key 

647 ): 

648 

649 if last_parent.obj() is None: 

650 raise orm_exc.StaleDataError( 

651 "Removing state %s from parent " 

652 "state %s along attribute '%s', " 

653 "but the parent record " 

654 "has gone stale, can't be sure this " 

655 "is the most recent parent." 

656 % ( 

657 state_str(state), 

658 state_str(parent_state), 

659 self.key, 

660 ) 

661 ) 

662 

663 return 

664 

665 state.parents[id_] = False 

666 

667 def get_history(self, state, dict_, passive=PASSIVE_OFF): 

668 raise NotImplementedError() 

669 

670 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE): 

671 """Return a list of tuples of (state, obj) 

672 for all objects in this attribute's current state 

673 + history. 

674 

675 Only applies to object-based attributes. 

676 

677 This is an inlining of existing functionality 

678 which roughly corresponds to: 

679 

680 get_state_history( 

681 state, 

682 key, 

683 passive=PASSIVE_NO_INITIALIZE).sum() 

684 

685 """ 

686 raise NotImplementedError() 

687 

688 def initialize(self, state, dict_): 

689 """Initialize the given state's attribute with an empty value.""" 

690 

691 value = None 

692 for fn in self.dispatch.init_scalar: 

693 ret = fn(state, value, dict_) 

694 if ret is not ATTR_EMPTY: 

695 value = ret 

696 

697 return value 

698 

699 def get(self, state, dict_, passive=PASSIVE_OFF): 

700 """Retrieve a value from the given object. 

701 If a callable is assembled on this object's attribute, and 

702 passive is False, the callable will be executed and the 

703 resulting value will be set as the new value for this attribute. 

704 """ 

705 if self.key in dict_: 

706 return dict_[self.key] 

707 else: 

708 # if history present, don't load 

709 key = self.key 

710 if ( 

711 key not in state.committed_state 

712 or state.committed_state[key] is NEVER_SET 

713 ): 

714 if not passive & CALLABLES_OK: 

715 return PASSIVE_NO_RESULT 

716 

717 if key in state.expired_attributes: 

718 value = state._load_expired(state, passive) 

719 elif key in state.callables: 

720 callable_ = state.callables[key] 

721 value = callable_(state, passive) 

722 elif self.callable_: 

723 value = self.callable_(state, passive) 

724 else: 

725 value = ATTR_EMPTY 

726 

727 if value is PASSIVE_NO_RESULT or value is NEVER_SET: 

728 return value 

729 elif value is ATTR_WAS_SET: 

730 try: 

731 return dict_[key] 

732 except KeyError as err: 

733 # TODO: no test coverage here. 

734 util.raise_( 

735 KeyError( 

736 "Deferred loader for attribute " 

737 "%r failed to populate " 

738 "correctly" % key 

739 ), 

740 replace_context=err, 

741 ) 

742 elif value is not ATTR_EMPTY: 

743 return self.set_committed_value(state, dict_, value) 

744 

745 if not passive & INIT_OK: 

746 return NEVER_SET 

747 else: 

748 # Return a new, empty value 

749 return self.initialize(state, dict_) 

750 

751 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 

752 self.set(state, dict_, value, initiator, passive=passive) 

753 

754 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 

755 self.set( 

756 state, dict_, None, initiator, passive=passive, check_old=value 

757 ) 

758 

759 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 

760 self.set( 

761 state, 

762 dict_, 

763 None, 

764 initiator, 

765 passive=passive, 

766 check_old=value, 

767 pop=True, 

768 ) 

769 

770 def set( 

771 self, 

772 state, 

773 dict_, 

774 value, 

775 initiator, 

776 passive=PASSIVE_OFF, 

777 check_old=None, 

778 pop=False, 

779 ): 

780 raise NotImplementedError() 

781 

782 def get_committed_value(self, state, dict_, passive=PASSIVE_OFF): 

783 """return the unchanged value of this attribute""" 

784 

785 if self.key in state.committed_state: 

786 value = state.committed_state[self.key] 

787 if value in (NO_VALUE, NEVER_SET): 

788 return None 

789 else: 

790 return value 

791 else: 

792 return self.get(state, dict_, passive=passive) 

793 

794 def set_committed_value(self, state, dict_, value): 

795 """set an attribute value on the given instance and 'commit' it.""" 

796 

797 dict_[self.key] = value 

798 state._commit(dict_, [self.key]) 

799 return value 

800 

801 

802class ScalarAttributeImpl(AttributeImpl): 

803 """represents a scalar value-holding InstrumentedAttribute.""" 

804 

805 default_accepts_scalar_loader = True 

806 uses_objects = False 

807 supports_population = True 

808 collection = False 

809 dynamic = False 

810 

811 __slots__ = "_replace_token", "_append_token", "_remove_token" 

812 

813 def __init__(self, *arg, **kw): 

814 super(ScalarAttributeImpl, self).__init__(*arg, **kw) 

815 self._replace_token = self._append_token = Event(self, OP_REPLACE) 

816 self._remove_token = Event(self, OP_REMOVE) 

817 

818 def delete(self, state, dict_): 

819 if self.dispatch._active_history: 

820 old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) 

821 else: 

822 old = dict_.get(self.key, NO_VALUE) 

823 

824 if self.dispatch.remove: 

825 self.fire_remove_event(state, dict_, old, self._remove_token) 

826 state._modified_event(dict_, self, old) 

827 

828 existing = dict_.pop(self.key, NO_VALUE) 

829 if ( 

830 existing is NO_VALUE 

831 and old is NO_VALUE 

832 and not state.expired 

833 and self.key not in state.expired_attributes 

834 ): 

835 raise AttributeError("%s object does not have a value" % self) 

836 

837 def get_history(self, state, dict_, passive=PASSIVE_OFF): 

838 if self.key in dict_: 

839 return History.from_scalar_attribute(self, state, dict_[self.key]) 

840 else: 

841 if passive & INIT_OK: 

842 passive ^= INIT_OK 

843 current = self.get(state, dict_, passive=passive) 

844 if current is PASSIVE_NO_RESULT: 

845 return HISTORY_BLANK 

846 else: 

847 return History.from_scalar_attribute(self, state, current) 

848 

849 def set( 

850 self, 

851 state, 

852 dict_, 

853 value, 

854 initiator, 

855 passive=PASSIVE_OFF, 

856 check_old=None, 

857 pop=False, 

858 ): 

859 if self.dispatch._active_history: 

860 old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET) 

861 else: 

862 old = dict_.get(self.key, NO_VALUE) 

863 

864 if self.dispatch.set: 

865 value = self.fire_replace_event( 

866 state, dict_, value, old, initiator 

867 ) 

868 state._modified_event(dict_, self, old) 

869 dict_[self.key] = value 

870 

871 def fire_replace_event(self, state, dict_, value, previous, initiator): 

872 for fn in self.dispatch.set: 

873 value = fn( 

874 state, value, previous, initiator or self._replace_token 

875 ) 

876 return value 

877 

878 def fire_remove_event(self, state, dict_, value, initiator): 

879 for fn in self.dispatch.remove: 

880 fn(state, value, initiator or self._remove_token) 

881 

882 @property 

883 def type(self): 

884 self.property.columns[0].type 

885 

886 

887class ScalarObjectAttributeImpl(ScalarAttributeImpl): 

888 """represents a scalar-holding InstrumentedAttribute, 

889 where the target object is also instrumented. 

890 

891 Adds events to delete/set operations. 

892 

893 """ 

894 

895 default_accepts_scalar_loader = False 

896 uses_objects = True 

897 supports_population = True 

898 collection = False 

899 

900 __slots__ = () 

901 

902 def delete(self, state, dict_): 

903 if self.dispatch._active_history: 

904 old = self.get( 

905 state, 

906 dict_, 

907 passive=PASSIVE_ONLY_PERSISTENT 

908 | NO_AUTOFLUSH 

909 | LOAD_AGAINST_COMMITTED, 

910 ) 

911 else: 

912 old = self.get( 

913 state, 

914 dict_, 

915 passive=PASSIVE_NO_FETCH ^ INIT_OK 

916 | LOAD_AGAINST_COMMITTED 

917 | NO_RAISE, 

918 ) 

919 

920 self.fire_remove_event(state, dict_, old, self._remove_token) 

921 

922 existing = dict_.pop(self.key, NO_VALUE) 

923 

924 # if the attribute is expired, we currently have no way to tell 

925 # that an object-attribute was expired vs. not loaded. So 

926 # for this test, we look to see if the object has a DB identity. 

927 if ( 

928 existing is NO_VALUE 

929 and old is not PASSIVE_NO_RESULT 

930 and state.key is None 

931 ): 

932 raise AttributeError("%s object does not have a value" % self) 

933 

934 def get_history(self, state, dict_, passive=PASSIVE_OFF): 

935 if self.key in dict_: 

936 return History.from_object_attribute(self, state, dict_[self.key]) 

937 else: 

938 if passive & INIT_OK: 

939 passive ^= INIT_OK 

940 current = self.get(state, dict_, passive=passive) 

941 if current is PASSIVE_NO_RESULT: 

942 return HISTORY_BLANK 

943 else: 

944 return History.from_object_attribute(self, state, current) 

945 

946 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE): 

947 if self.key in dict_: 

948 current = dict_[self.key] 

949 elif passive & CALLABLES_OK: 

950 current = self.get(state, dict_, passive=passive) 

951 else: 

952 return [] 

953 

954 # can't use __hash__(), can't use __eq__() here 

955 if ( 

956 current is not None 

957 and current is not PASSIVE_NO_RESULT 

958 and current is not NEVER_SET 

959 ): 

960 ret = [(instance_state(current), current)] 

961 else: 

962 ret = [(None, None)] 

963 

964 if self.key in state.committed_state: 

965 original = state.committed_state[self.key] 

966 if ( 

967 original is not None 

968 and original is not PASSIVE_NO_RESULT 

969 and original is not NEVER_SET 

970 and original is not current 

971 ): 

972 

973 ret.append((instance_state(original), original)) 

974 return ret 

975 

976 def set( 

977 self, 

978 state, 

979 dict_, 

980 value, 

981 initiator, 

982 passive=PASSIVE_OFF, 

983 check_old=None, 

984 pop=False, 

985 ): 

986 """Set a value on the given InstanceState. 

987 

988 """ 

989 if self.dispatch._active_history: 

990 old = self.get( 

991 state, 

992 dict_, 

993 passive=PASSIVE_ONLY_PERSISTENT 

994 | NO_AUTOFLUSH 

995 | LOAD_AGAINST_COMMITTED, 

996 ) 

997 else: 

998 old = self.get( 

999 state, 

1000 dict_, 

1001 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1002 | LOAD_AGAINST_COMMITTED 

1003 | NO_RAISE, 

1004 ) 

1005 

1006 if ( 

1007 check_old is not None 

1008 and old is not PASSIVE_NO_RESULT 

1009 and check_old is not old 

1010 ): 

1011 if pop: 

1012 return 

1013 else: 

1014 raise ValueError( 

1015 "Object %s not associated with %s on attribute '%s'" 

1016 % (instance_str(check_old), state_str(state), self.key) 

1017 ) 

1018 

1019 value = self.fire_replace_event(state, dict_, value, old, initiator) 

1020 dict_[self.key] = value 

1021 

1022 def fire_remove_event(self, state, dict_, value, initiator): 

1023 if self.trackparent and value is not None: 

1024 self.sethasparent(instance_state(value), state, False) 

1025 

1026 for fn in self.dispatch.remove: 

1027 fn(state, value, initiator or self._remove_token) 

1028 

1029 state._modified_event(dict_, self, value) 

1030 

1031 def fire_replace_event(self, state, dict_, value, previous, initiator): 

1032 if self.trackparent: 

1033 if previous is not value and previous not in ( 

1034 None, 

1035 PASSIVE_NO_RESULT, 

1036 NEVER_SET, 

1037 ): 

1038 self.sethasparent(instance_state(previous), state, False) 

1039 

1040 for fn in self.dispatch.set: 

1041 value = fn( 

1042 state, value, previous, initiator or self._replace_token 

1043 ) 

1044 

1045 state._modified_event(dict_, self, previous) 

1046 

1047 if self.trackparent: 

1048 if value is not None: 

1049 self.sethasparent(instance_state(value), state, True) 

1050 

1051 return value 

1052 

1053 

1054class CollectionAttributeImpl(AttributeImpl): 

1055 """A collection-holding attribute that instruments changes in membership. 

1056 

1057 Only handles collections of instrumented objects. 

1058 

1059 InstrumentedCollectionAttribute holds an arbitrary, user-specified 

1060 container object (defaulting to a list) and brokers access to the 

1061 CollectionAdapter, a "view" onto that object that presents consistent bag 

1062 semantics to the orm layer independent of the user data implementation. 

1063 

1064 """ 

1065 

1066 default_accepts_scalar_loader = False 

1067 uses_objects = True 

1068 supports_population = True 

1069 collection = True 

1070 dynamic = False 

1071 

1072 __slots__ = ( 

1073 "copy", 

1074 "collection_factory", 

1075 "_append_token", 

1076 "_remove_token", 

1077 "_bulk_replace_token", 

1078 "_duck_typed_as", 

1079 ) 

1080 

1081 def __init__( 

1082 self, 

1083 class_, 

1084 key, 

1085 callable_, 

1086 dispatch, 

1087 typecallable=None, 

1088 trackparent=False, 

1089 extension=None, 

1090 copy_function=None, 

1091 compare_function=None, 

1092 **kwargs 

1093 ): 

1094 super(CollectionAttributeImpl, self).__init__( 

1095 class_, 

1096 key, 

1097 callable_, 

1098 dispatch, 

1099 trackparent=trackparent, 

1100 extension=extension, 

1101 compare_function=compare_function, 

1102 **kwargs 

1103 ) 

1104 

1105 if copy_function is None: 

1106 copy_function = self.__copy 

1107 self.copy = copy_function 

1108 self.collection_factory = typecallable 

1109 self._append_token = Event(self, OP_APPEND) 

1110 self._remove_token = Event(self, OP_REMOVE) 

1111 self._bulk_replace_token = Event(self, OP_BULK_REPLACE) 

1112 self._duck_typed_as = util.duck_type_collection( 

1113 self.collection_factory() 

1114 ) 

1115 

1116 if getattr(self.collection_factory, "_sa_linker", None): 

1117 

1118 @event.listens_for(self, "init_collection") 

1119 def link(target, collection, collection_adapter): 

1120 collection._sa_linker(collection_adapter) 

1121 

1122 @event.listens_for(self, "dispose_collection") 

1123 def unlink(target, collection, collection_adapter): 

1124 collection._sa_linker(None) 

1125 

1126 def __copy(self, item): 

1127 return [y for y in collections.collection_adapter(item)] 

1128 

1129 def get_history(self, state, dict_, passive=PASSIVE_OFF): 

1130 current = self.get(state, dict_, passive=passive) 

1131 if current is PASSIVE_NO_RESULT: 

1132 return HISTORY_BLANK 

1133 else: 

1134 return History.from_collection(self, state, current) 

1135 

1136 def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE): 

1137 # NOTE: passive is ignored here at the moment 

1138 

1139 if self.key not in dict_: 

1140 return [] 

1141 

1142 current = dict_[self.key] 

1143 current = getattr(current, "_sa_adapter") 

1144 

1145 if self.key in state.committed_state: 

1146 original = state.committed_state[self.key] 

1147 if original not in (NO_VALUE, NEVER_SET): 

1148 current_states = [ 

1149 ((c is not None) and instance_state(c) or None, c) 

1150 for c in current 

1151 ] 

1152 original_states = [ 

1153 ((c is not None) and instance_state(c) or None, c) 

1154 for c in original 

1155 ] 

1156 

1157 current_set = dict(current_states) 

1158 original_set = dict(original_states) 

1159 

1160 return ( 

1161 [ 

1162 (s, o) 

1163 for s, o in current_states 

1164 if s not in original_set 

1165 ] 

1166 + [(s, o) for s, o in current_states if s in original_set] 

1167 + [ 

1168 (s, o) 

1169 for s, o in original_states 

1170 if s not in current_set 

1171 ] 

1172 ) 

1173 

1174 return [(instance_state(o), o) for o in current] 

1175 

1176 def fire_append_event(self, state, dict_, value, initiator): 

1177 for fn in self.dispatch.append: 

1178 value = fn(state, value, initiator or self._append_token) 

1179 

1180 state._modified_event(dict_, self, NEVER_SET, True) 

1181 

1182 if self.trackparent and value is not None: 

1183 self.sethasparent(instance_state(value), state, True) 

1184 

1185 return value 

1186 

1187 def fire_pre_remove_event(self, state, dict_, initiator): 

1188 """A special event used for pop() operations. 

1189 

1190 The "remove" event needs to have the item to be removed passed to 

1191 it, which in the case of pop from a set, we don't have a way to access 

1192 the item before the operation. the event is used for all pop() 

1193 operations (even though set.pop is the one where it is really needed). 

1194 

1195 """ 

1196 state._modified_event(dict_, self, NEVER_SET, True) 

1197 

1198 def fire_remove_event(self, state, dict_, value, initiator): 

1199 if self.trackparent and value is not None: 

1200 self.sethasparent(instance_state(value), state, False) 

1201 

1202 for fn in self.dispatch.remove: 

1203 fn(state, value, initiator or self._remove_token) 

1204 

1205 state._modified_event(dict_, self, NEVER_SET, True) 

1206 

1207 def delete(self, state, dict_): 

1208 if self.key not in dict_: 

1209 return 

1210 

1211 state._modified_event(dict_, self, NEVER_SET, True) 

1212 

1213 collection = self.get_collection(state, state.dict) 

1214 collection.clear_with_event() 

1215 

1216 # key is always present because we checked above. e.g. 

1217 # del is a no-op if collection not present. 

1218 del dict_[self.key] 

1219 

1220 def initialize(self, state, dict_): 

1221 """Initialize this attribute with an empty collection.""" 

1222 

1223 _, user_data = self._initialize_collection(state) 

1224 dict_[self.key] = user_data 

1225 return user_data 

1226 

1227 def _initialize_collection(self, state): 

1228 

1229 adapter, collection = state.manager.initialize_collection( 

1230 self.key, state, self.collection_factory 

1231 ) 

1232 

1233 self.dispatch.init_collection(state, collection, adapter) 

1234 

1235 return adapter, collection 

1236 

1237 def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 

1238 collection = self.get_collection(state, dict_, passive=passive) 

1239 if collection is PASSIVE_NO_RESULT: 

1240 value = self.fire_append_event(state, dict_, value, initiator) 

1241 assert ( 

1242 self.key not in dict_ 

1243 ), "Collection was loaded during event handling." 

1244 state._get_pending_mutation(self.key).append(value) 

1245 else: 

1246 collection.append_with_event(value, initiator) 

1247 

1248 def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 

1249 collection = self.get_collection(state, state.dict, passive=passive) 

1250 if collection is PASSIVE_NO_RESULT: 

1251 self.fire_remove_event(state, dict_, value, initiator) 

1252 assert ( 

1253 self.key not in dict_ 

1254 ), "Collection was loaded during event handling." 

1255 state._get_pending_mutation(self.key).remove(value) 

1256 else: 

1257 collection.remove_with_event(value, initiator) 

1258 

1259 def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): 

1260 try: 

1261 # TODO: better solution here would be to add 

1262 # a "popper" role to collections.py to complement 

1263 # "remover". 

1264 self.remove(state, dict_, value, initiator, passive=passive) 

1265 except (ValueError, KeyError, IndexError): 

1266 pass 

1267 

1268 def set( 

1269 self, 

1270 state, 

1271 dict_, 

1272 value, 

1273 initiator=None, 

1274 passive=PASSIVE_OFF, 

1275 pop=False, 

1276 _adapt=True, 

1277 ): 

1278 iterable = orig_iterable = value 

1279 

1280 # pulling a new collection first so that an adaptation exception does 

1281 # not trigger a lazy load of the old collection. 

1282 new_collection, user_data = self._initialize_collection(state) 

1283 if _adapt: 

1284 if new_collection._converter is not None: 

1285 iterable = new_collection._converter(iterable) 

1286 else: 

1287 setting_type = util.duck_type_collection(iterable) 

1288 receiving_type = self._duck_typed_as 

1289 

1290 if setting_type is not receiving_type: 

1291 given = ( 

1292 iterable is None 

1293 and "None" 

1294 or iterable.__class__.__name__ 

1295 ) 

1296 wanted = self._duck_typed_as.__name__ 

1297 raise TypeError( 

1298 "Incompatible collection type: %s is not %s-like" 

1299 % (given, wanted) 

1300 ) 

1301 

1302 # If the object is an adapted collection, return the (iterable) 

1303 # adapter. 

1304 if hasattr(iterable, "_sa_iterator"): 

1305 iterable = iterable._sa_iterator() 

1306 elif setting_type is dict: 

1307 if util.py3k: 

1308 iterable = iterable.values() 

1309 else: 

1310 iterable = getattr( 

1311 iterable, "itervalues", iterable.values 

1312 )() 

1313 else: 

1314 iterable = iter(iterable) 

1315 new_values = list(iterable) 

1316 

1317 evt = self._bulk_replace_token 

1318 

1319 self.dispatch.bulk_replace(state, new_values, evt) 

1320 

1321 old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT) 

1322 if old is PASSIVE_NO_RESULT: 

1323 old = self.initialize(state, dict_) 

1324 elif old is orig_iterable: 

1325 # ignore re-assignment of the current collection, as happens 

1326 # implicitly with in-place operators (foo.collection |= other) 

1327 return 

1328 

1329 # place a copy of "old" in state.committed_state 

1330 state._modified_event(dict_, self, old, True) 

1331 

1332 old_collection = old._sa_adapter 

1333 

1334 dict_[self.key] = user_data 

1335 

1336 collections.bulk_replace( 

1337 new_values, old_collection, new_collection, initiator=evt 

1338 ) 

1339 

1340 del old._sa_adapter 

1341 self.dispatch.dispose_collection(state, old, old_collection) 

1342 

1343 def _invalidate_collection(self, collection): 

1344 adapter = getattr(collection, "_sa_adapter") 

1345 adapter.invalidated = True 

1346 

1347 def set_committed_value(self, state, dict_, value): 

1348 """Set an attribute value on the given instance and 'commit' it.""" 

1349 

1350 collection, user_data = self._initialize_collection(state) 

1351 

1352 if value: 

1353 collection.append_multiple_without_event(value) 

1354 

1355 state.dict[self.key] = user_data 

1356 

1357 state._commit(dict_, [self.key]) 

1358 

1359 if self.key in state._pending_mutations: 

1360 # pending items exist. issue a modified event, 

1361 # add/remove new items. 

1362 state._modified_event(dict_, self, user_data, True) 

1363 

1364 pending = state._pending_mutations.pop(self.key) 

1365 added = pending.added_items 

1366 removed = pending.deleted_items 

1367 for item in added: 

1368 collection.append_without_event(item) 

1369 for item in removed: 

1370 collection.remove_without_event(item) 

1371 

1372 return user_data 

1373 

1374 def get_collection( 

1375 self, state, dict_, user_data=None, passive=PASSIVE_OFF 

1376 ): 

1377 """Retrieve the CollectionAdapter associated with the given state. 

1378 

1379 Creates a new CollectionAdapter if one does not exist. 

1380 

1381 """ 

1382 if user_data is None: 

1383 user_data = self.get(state, dict_, passive=passive) 

1384 if user_data is PASSIVE_NO_RESULT: 

1385 return user_data 

1386 

1387 return getattr(user_data, "_sa_adapter") 

1388 

1389 

1390def backref_listeners(attribute, key, uselist): 

1391 """Apply listeners to synchronize a two-way relationship.""" 

1392 

1393 # use easily recognizable names for stack traces. 

1394 

1395 # in the sections marked "tokens to test for a recursive loop", 

1396 # this is somewhat brittle and very performance-sensitive logic 

1397 # that is specific to how we might arrive at each event. a marker 

1398 # that can target us directly to arguments being invoked against 

1399 # the impl might be simpler, but could interfere with other systems. 

1400 

1401 parent_token = attribute.impl.parent_token 

1402 parent_impl = attribute.impl 

1403 

1404 def _acceptable_key_err(child_state, initiator, child_impl): 

1405 raise ValueError( 

1406 "Bidirectional attribute conflict detected: " 

1407 'Passing object %s to attribute "%s" ' 

1408 'triggers a modify event on attribute "%s" ' 

1409 'via the backref "%s".' 

1410 % ( 

1411 state_str(child_state), 

1412 initiator.parent_token, 

1413 child_impl.parent_token, 

1414 attribute.impl.parent_token, 

1415 ) 

1416 ) 

1417 

1418 def emit_backref_from_scalar_set_event(state, child, oldchild, initiator): 

1419 if oldchild is child: 

1420 return child 

1421 if ( 

1422 oldchild is not None 

1423 and oldchild is not PASSIVE_NO_RESULT 

1424 and oldchild is not NEVER_SET 

1425 ): 

1426 # With lazy=None, there's no guarantee that the full collection is 

1427 # present when updating via a backref. 

1428 old_state, old_dict = ( 

1429 instance_state(oldchild), 

1430 instance_dict(oldchild), 

1431 ) 

1432 impl = old_state.manager[key].impl 

1433 

1434 # tokens to test for a recursive loop. 

1435 if not impl.collection and not impl.dynamic: 

1436 check_recursive_token = impl._replace_token 

1437 else: 

1438 check_recursive_token = impl._remove_token 

1439 

1440 if initiator is not check_recursive_token: 

1441 impl.pop( 

1442 old_state, 

1443 old_dict, 

1444 state.obj(), 

1445 parent_impl._append_token, 

1446 passive=PASSIVE_NO_FETCH, 

1447 ) 

1448 

1449 if child is not None: 

1450 child_state, child_dict = ( 

1451 instance_state(child), 

1452 instance_dict(child), 

1453 ) 

1454 child_impl = child_state.manager[key].impl 

1455 

1456 if ( 

1457 initiator.parent_token is not parent_token 

1458 and initiator.parent_token is not child_impl.parent_token 

1459 ): 

1460 _acceptable_key_err(state, initiator, child_impl) 

1461 

1462 # tokens to test for a recursive loop. 

1463 check_append_token = child_impl._append_token 

1464 check_bulk_replace_token = ( 

1465 child_impl._bulk_replace_token 

1466 if child_impl.collection 

1467 else None 

1468 ) 

1469 

1470 if ( 

1471 initiator is not check_append_token 

1472 and initiator is not check_bulk_replace_token 

1473 ): 

1474 child_impl.append( 

1475 child_state, 

1476 child_dict, 

1477 state.obj(), 

1478 initiator, 

1479 passive=PASSIVE_NO_FETCH, 

1480 ) 

1481 return child 

1482 

1483 def emit_backref_from_collection_append_event(state, child, initiator): 

1484 if child is None: 

1485 return 

1486 

1487 child_state, child_dict = instance_state(child), instance_dict(child) 

1488 child_impl = child_state.manager[key].impl 

1489 

1490 if ( 

1491 initiator.parent_token is not parent_token 

1492 and initiator.parent_token is not child_impl.parent_token 

1493 ): 

1494 _acceptable_key_err(state, initiator, child_impl) 

1495 

1496 # tokens to test for a recursive loop. 

1497 check_append_token = child_impl._append_token 

1498 check_bulk_replace_token = ( 

1499 child_impl._bulk_replace_token if child_impl.collection else None 

1500 ) 

1501 

1502 if ( 

1503 initiator is not check_append_token 

1504 and initiator is not check_bulk_replace_token 

1505 ): 

1506 child_impl.append( 

1507 child_state, 

1508 child_dict, 

1509 state.obj(), 

1510 initiator, 

1511 passive=PASSIVE_NO_FETCH, 

1512 ) 

1513 return child 

1514 

1515 def emit_backref_from_collection_remove_event(state, child, initiator): 

1516 if ( 

1517 child is not None 

1518 and child is not PASSIVE_NO_RESULT 

1519 and child is not NEVER_SET 

1520 ): 

1521 child_state, child_dict = ( 

1522 instance_state(child), 

1523 instance_dict(child), 

1524 ) 

1525 child_impl = child_state.manager[key].impl 

1526 

1527 # tokens to test for a recursive loop. 

1528 if not child_impl.collection and not child_impl.dynamic: 

1529 check_remove_token = child_impl._remove_token 

1530 check_replace_token = child_impl._replace_token 

1531 check_for_dupes_on_remove = uselist and not parent_impl.dynamic 

1532 else: 

1533 check_remove_token = child_impl._remove_token 

1534 check_replace_token = ( 

1535 child_impl._bulk_replace_token 

1536 if child_impl.collection 

1537 else None 

1538 ) 

1539 check_for_dupes_on_remove = False 

1540 

1541 if ( 

1542 initiator is not check_remove_token 

1543 and initiator is not check_replace_token 

1544 ): 

1545 

1546 if not check_for_dupes_on_remove or not util.has_dupes( 

1547 # when this event is called, the item is usually 

1548 # present in the list, except for a pop() operation. 

1549 state.dict[parent_impl.key], 

1550 child, 

1551 ): 

1552 child_impl.pop( 

1553 child_state, 

1554 child_dict, 

1555 state.obj(), 

1556 initiator, 

1557 passive=PASSIVE_NO_FETCH, 

1558 ) 

1559 

1560 if uselist: 

1561 event.listen( 

1562 attribute, 

1563 "append", 

1564 emit_backref_from_collection_append_event, 

1565 retval=True, 

1566 raw=True, 

1567 ) 

1568 else: 

1569 event.listen( 

1570 attribute, 

1571 "set", 

1572 emit_backref_from_scalar_set_event, 

1573 retval=True, 

1574 raw=True, 

1575 ) 

1576 # TODO: need coverage in test/orm/ of remove event 

1577 event.listen( 

1578 attribute, 

1579 "remove", 

1580 emit_backref_from_collection_remove_event, 

1581 retval=True, 

1582 raw=True, 

1583 ) 

1584 

1585 

1586_NO_HISTORY = util.symbol("NO_HISTORY") 

1587_NO_STATE_SYMBOLS = frozenset( 

1588 [id(PASSIVE_NO_RESULT), id(NO_VALUE), id(NEVER_SET)] 

1589) 

1590 

1591History = util.namedtuple("History", ["added", "unchanged", "deleted"]) 

1592 

1593 

1594class History(History): 

1595 """A 3-tuple of added, unchanged and deleted values, 

1596 representing the changes which have occurred on an instrumented 

1597 attribute. 

1598 

1599 The easiest way to get a :class:`.History` object for a particular 

1600 attribute on an object is to use the :func:`_sa.inspect` function:: 

1601 

1602 from sqlalchemy import inspect 

1603 

1604 hist = inspect(myobject).attrs.myattribute.history 

1605 

1606 Each tuple member is an iterable sequence: 

1607 

1608 * ``added`` - the collection of items added to the attribute (the first 

1609 tuple element). 

1610 

1611 * ``unchanged`` - the collection of items that have not changed on the 

1612 attribute (the second tuple element). 

1613 

1614 * ``deleted`` - the collection of items that have been removed from the 

1615 attribute (the third tuple element). 

1616 

1617 """ 

1618 

1619 def __bool__(self): 

1620 return self != HISTORY_BLANK 

1621 

1622 __nonzero__ = __bool__ 

1623 

1624 def empty(self): 

1625 """Return True if this :class:`.History` has no changes 

1626 and no existing, unchanged state. 

1627 

1628 """ 

1629 

1630 return not bool((self.added or self.deleted) or self.unchanged) 

1631 

1632 def sum(self): 

1633 """Return a collection of added + unchanged + deleted.""" 

1634 

1635 return ( 

1636 (self.added or []) + (self.unchanged or []) + (self.deleted or []) 

1637 ) 

1638 

1639 def non_deleted(self): 

1640 """Return a collection of added + unchanged.""" 

1641 

1642 return (self.added or []) + (self.unchanged or []) 

1643 

1644 def non_added(self): 

1645 """Return a collection of unchanged + deleted.""" 

1646 

1647 return (self.unchanged or []) + (self.deleted or []) 

1648 

1649 def has_changes(self): 

1650 """Return True if this :class:`.History` has changes.""" 

1651 

1652 return bool(self.added or self.deleted) 

1653 

1654 def as_state(self): 

1655 return History( 

1656 [ 

1657 (c is not None) and instance_state(c) or None 

1658 for c in self.added 

1659 ], 

1660 [ 

1661 (c is not None) and instance_state(c) or None 

1662 for c in self.unchanged 

1663 ], 

1664 [ 

1665 (c is not None) and instance_state(c) or None 

1666 for c in self.deleted 

1667 ], 

1668 ) 

1669 

1670 @classmethod 

1671 def from_scalar_attribute(cls, attribute, state, current): 

1672 original = state.committed_state.get(attribute.key, _NO_HISTORY) 

1673 

1674 if original is _NO_HISTORY: 

1675 if current is NEVER_SET: 

1676 return cls((), (), ()) 

1677 else: 

1678 return cls((), [current], ()) 

1679 # don't let ClauseElement expressions here trip things up 

1680 elif attribute.is_equal(current, original) is True: 

1681 return cls((), [current], ()) 

1682 else: 

1683 # current convention on native scalars is to not 

1684 # include information 

1685 # about missing previous value in "deleted", but 

1686 # we do include None, which helps in some primary 

1687 # key situations 

1688 if id(original) in _NO_STATE_SYMBOLS: 

1689 deleted = () 

1690 # indicate a "del" operation occurred when we don't have 

1691 # the previous value as: ([None], (), ()) 

1692 if id(current) in _NO_STATE_SYMBOLS: 

1693 current = None 

1694 else: 

1695 deleted = [original] 

1696 if current is NEVER_SET: 

1697 return cls((), (), deleted) 

1698 else: 

1699 return cls([current], (), deleted) 

1700 

1701 @classmethod 

1702 def from_object_attribute(cls, attribute, state, current): 

1703 original = state.committed_state.get(attribute.key, _NO_HISTORY) 

1704 

1705 if original is _NO_HISTORY: 

1706 if current is NO_VALUE or current is NEVER_SET: 

1707 return cls((), (), ()) 

1708 else: 

1709 return cls((), [current], ()) 

1710 elif current is original and current is not NEVER_SET: 

1711 return cls((), [current], ()) 

1712 else: 

1713 # current convention on related objects is to not 

1714 # include information 

1715 # about missing previous value in "deleted", and 

1716 # to also not include None - the dependency.py rules 

1717 # ignore the None in any case. 

1718 if id(original) in _NO_STATE_SYMBOLS or original is None: 

1719 deleted = () 

1720 # indicate a "del" operation occurred when we don't have 

1721 # the previous value as: ([None], (), ()) 

1722 if id(current) in _NO_STATE_SYMBOLS: 

1723 current = None 

1724 else: 

1725 deleted = [original] 

1726 if current is NO_VALUE or current is NEVER_SET: 

1727 return cls((), (), deleted) 

1728 else: 

1729 return cls([current], (), deleted) 

1730 

1731 @classmethod 

1732 def from_collection(cls, attribute, state, current): 

1733 original = state.committed_state.get(attribute.key, _NO_HISTORY) 

1734 

1735 if current is NO_VALUE or current is NEVER_SET: 

1736 return cls((), (), ()) 

1737 

1738 current = getattr(current, "_sa_adapter") 

1739 if original in (NO_VALUE, NEVER_SET): 

1740 return cls(list(current), (), ()) 

1741 elif original is _NO_HISTORY: 

1742 return cls((), list(current), ()) 

1743 else: 

1744 

1745 current_states = [ 

1746 ((c is not None) and instance_state(c) or None, c) 

1747 for c in current 

1748 ] 

1749 original_states = [ 

1750 ((c is not None) and instance_state(c) or None, c) 

1751 for c in original 

1752 ] 

1753 

1754 current_set = dict(current_states) 

1755 original_set = dict(original_states) 

1756 

1757 return cls( 

1758 [o for s, o in current_states if s not in original_set], 

1759 [o for s, o in current_states if s in original_set], 

1760 [o for s, o in original_states if s not in current_set], 

1761 ) 

1762 

1763 

1764HISTORY_BLANK = History(None, None, None) 

1765 

1766 

1767def get_history(obj, key, passive=PASSIVE_OFF): 

1768 """Return a :class:`.History` record for the given object 

1769 and attribute key. 

1770 

1771 This is the **pre-flush** history for a given attribute, which is 

1772 reset each time the :class:`.Session` flushes changes to the 

1773 current database transaction. 

1774 

1775 .. note:: 

1776 

1777 Prefer to use the :attr:`.AttributeState.history` and 

1778 :meth:`.AttributeState.load_history` accessors to retrieve the 

1779 :class:`.History` for instance attributes. 

1780 

1781 

1782 :param obj: an object whose class is instrumented by the 

1783 attributes package. 

1784 

1785 :param key: string attribute name. 

1786 

1787 :param passive: indicates loading behavior for the attribute 

1788 if the value is not already present. This is a 

1789 bitflag attribute, which defaults to the symbol 

1790 :attr:`.PASSIVE_OFF` indicating all necessary SQL 

1791 should be emitted. 

1792 

1793 .. seealso:: 

1794 

1795 :attr:`.AttributeState.history` 

1796 

1797 :meth:`.AttributeState.load_history` - retrieve history 

1798 using loader callables if the value is not locally present. 

1799 

1800 """ 

1801 if passive is True: 

1802 util.warn_deprecated( 

1803 "Passing True for 'passive' is deprecated. " 

1804 "Use attributes.PASSIVE_NO_INITIALIZE" 

1805 ) 

1806 passive = PASSIVE_NO_INITIALIZE 

1807 elif passive is False: 

1808 util.warn_deprecated( 

1809 "Passing False for 'passive' is " 

1810 "deprecated. Use attributes.PASSIVE_OFF" 

1811 ) 

1812 passive = PASSIVE_OFF 

1813 

1814 return get_state_history(instance_state(obj), key, passive) 

1815 

1816 

1817def get_state_history(state, key, passive=PASSIVE_OFF): 

1818 return state.get_history(key, passive) 

1819 

1820 

1821def has_parent(cls, obj, key, optimistic=False): 

1822 """TODO""" 

1823 manager = manager_of_class(cls) 

1824 state = instance_state(obj) 

1825 return manager.has_parent(state, key, optimistic) 

1826 

1827 

1828def register_attribute(class_, key, **kw): 

1829 comparator = kw.pop("comparator", None) 

1830 parententity = kw.pop("parententity", None) 

1831 doc = kw.pop("doc", None) 

1832 desc = register_descriptor(class_, key, comparator, parententity, doc=doc) 

1833 register_attribute_impl(class_, key, **kw) 

1834 return desc 

1835 

1836 

1837def register_attribute_impl( 

1838 class_, 

1839 key, 

1840 uselist=False, 

1841 callable_=None, 

1842 useobject=False, 

1843 impl_class=None, 

1844 backref=None, 

1845 **kw 

1846): 

1847 

1848 manager = manager_of_class(class_) 

1849 if uselist: 

1850 factory = kw.pop("typecallable", None) 

1851 typecallable = manager.instrument_collection_class( 

1852 key, factory or list 

1853 ) 

1854 else: 

1855 typecallable = kw.pop("typecallable", None) 

1856 

1857 dispatch = manager[key].dispatch 

1858 

1859 if impl_class: 

1860 impl = impl_class(class_, key, typecallable, dispatch, **kw) 

1861 elif uselist: 

1862 impl = CollectionAttributeImpl( 

1863 class_, key, callable_, dispatch, typecallable=typecallable, **kw 

1864 ) 

1865 elif useobject: 

1866 impl = ScalarObjectAttributeImpl( 

1867 class_, key, callable_, dispatch, **kw 

1868 ) 

1869 else: 

1870 impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw) 

1871 

1872 manager[key].impl = impl 

1873 

1874 if backref: 

1875 backref_listeners(manager[key], backref, uselist) 

1876 

1877 manager.post_configure_attribute(key) 

1878 return manager[key] 

1879 

1880 

1881def register_descriptor( 

1882 class_, key, comparator=None, parententity=None, doc=None 

1883): 

1884 manager = manager_of_class(class_) 

1885 

1886 descriptor = InstrumentedAttribute( 

1887 class_, key, comparator=comparator, parententity=parententity 

1888 ) 

1889 

1890 descriptor.__doc__ = doc 

1891 

1892 manager.instrument_attribute(key, descriptor) 

1893 return descriptor 

1894 

1895 

1896def unregister_attribute(class_, key): 

1897 manager_of_class(class_).uninstrument_attribute(key) 

1898 

1899 

1900def init_collection(obj, key): 

1901 """Initialize a collection attribute and return the collection adapter. 

1902 

1903 This function is used to provide direct access to collection internals 

1904 for a previously unloaded attribute. e.g.:: 

1905 

1906 collection_adapter = init_collection(someobject, 'elements') 

1907 for elem in values: 

1908 collection_adapter.append_without_event(elem) 

1909 

1910 For an easier way to do the above, see 

1911 :func:`~sqlalchemy.orm.attributes.set_committed_value`. 

1912 

1913 :param obj: a mapped object 

1914 

1915 :param key: string attribute name where the collection is located. 

1916 

1917 """ 

1918 state = instance_state(obj) 

1919 dict_ = state.dict 

1920 return init_state_collection(state, dict_, key) 

1921 

1922 

1923def init_state_collection(state, dict_, key): 

1924 """Initialize a collection attribute and return the collection adapter.""" 

1925 

1926 attr = state.manager[key].impl 

1927 user_data = attr.initialize(state, dict_) 

1928 return attr.get_collection(state, dict_, user_data) 

1929 

1930 

1931def set_committed_value(instance, key, value): 

1932 """Set the value of an attribute with no history events. 

1933 

1934 Cancels any previous history present. The value should be 

1935 a scalar value for scalar-holding attributes, or 

1936 an iterable for any collection-holding attribute. 

1937 

1938 This is the same underlying method used when a lazy loader 

1939 fires off and loads additional data from the database. 

1940 In particular, this method can be used by application code 

1941 which has loaded additional attributes or collections through 

1942 separate queries, which can then be attached to an instance 

1943 as though it were part of its original loaded state. 

1944 

1945 """ 

1946 state, dict_ = instance_state(instance), instance_dict(instance) 

1947 state.manager[key].impl.set_committed_value(state, dict_, value) 

1948 

1949 

1950def set_attribute(instance, key, value, initiator=None): 

1951 """Set the value of an attribute, firing history events. 

1952 

1953 This function may be used regardless of instrumentation 

1954 applied directly to the class, i.e. no descriptors are required. 

1955 Custom attribute management schemes will need to make usage 

1956 of this method to establish attribute state as understood 

1957 by SQLAlchemy. 

1958 

1959 :param instance: the object that will be modified 

1960 

1961 :param key: string name of the attribute 

1962 

1963 :param value: value to assign 

1964 

1965 :param initiator: an instance of :class:`.Event` that would have 

1966 been propagated from a previous event listener. This argument 

1967 is used when the :func:`.set_attribute` function is being used within 

1968 an existing event listening function where an :class:`.Event` object 

1969 is being supplied; the object may be used to track the origin of the 

1970 chain of events. 

1971 

1972 .. versionadded:: 1.2.3 

1973 

1974 """ 

1975 state, dict_ = instance_state(instance), instance_dict(instance) 

1976 state.manager[key].impl.set(state, dict_, value, initiator) 

1977 

1978 

1979def get_attribute(instance, key): 

1980 """Get the value of an attribute, firing any callables required. 

1981 

1982 This function may be used regardless of instrumentation 

1983 applied directly to the class, i.e. no descriptors are required. 

1984 Custom attribute management schemes will need to make usage 

1985 of this method to make usage of attribute state as understood 

1986 by SQLAlchemy. 

1987 

1988 """ 

1989 state, dict_ = instance_state(instance), instance_dict(instance) 

1990 return state.manager[key].impl.get(state, dict_) 

1991 

1992 

1993def del_attribute(instance, key): 

1994 """Delete the value of an attribute, firing history events. 

1995 

1996 This function may be used regardless of instrumentation 

1997 applied directly to the class, i.e. no descriptors are required. 

1998 Custom attribute management schemes will need to make usage 

1999 of this method to establish attribute state as understood 

2000 by SQLAlchemy. 

2001 

2002 """ 

2003 state, dict_ = instance_state(instance), instance_dict(instance) 

2004 state.manager[key].impl.delete(state, dict_) 

2005 

2006 

2007def flag_modified(instance, key): 

2008 """Mark an attribute on an instance as 'modified'. 

2009 

2010 This sets the 'modified' flag on the instance and 

2011 establishes an unconditional change event for the given attribute. 

2012 The attribute must have a value present, else an 

2013 :class:`.InvalidRequestError` is raised. 

2014 

2015 To mark an object "dirty" without referring to any specific attribute 

2016 so that it is considered within a flush, use the 

2017 :func:`.attributes.flag_dirty` call. 

2018 

2019 .. seealso:: 

2020 

2021 :func:`.attributes.flag_dirty` 

2022 

2023 """ 

2024 state, dict_ = instance_state(instance), instance_dict(instance) 

2025 impl = state.manager[key].impl 

2026 impl.dispatch.modified(state, impl._modified_token) 

2027 state._modified_event(dict_, impl, NO_VALUE, is_userland=True) 

2028 

2029 

2030def flag_dirty(instance): 

2031 """Mark an instance as 'dirty' without any specific attribute mentioned. 

2032 

2033 This is a special operation that will allow the object to travel through 

2034 the flush process for interception by events such as 

2035 :meth:`.SessionEvents.before_flush`. Note that no SQL will be emitted in 

2036 the flush process for an object that has no changes, even if marked dirty 

2037 via this method. However, a :meth:`.SessionEvents.before_flush` handler 

2038 will be able to see the object in the :attr:`.Session.dirty` collection and 

2039 may establish changes on it, which will then be included in the SQL 

2040 emitted. 

2041 

2042 .. versionadded:: 1.2 

2043 

2044 .. seealso:: 

2045 

2046 :func:`.attributes.flag_modified` 

2047 

2048 """ 

2049 

2050 state, dict_ = instance_state(instance), instance_dict(instance) 

2051 state._modified_event(dict_, None, NO_VALUE, is_userland=True)