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

848 statements  

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

1# orm/attributes.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"""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 DEFERRED_HISTORY_LOAD 

26from .base import INIT_OK 

27from .base import instance_dict 

28from .base import instance_state 

29from .base import instance_str 

30from .base import LOAD_AGAINST_COMMITTED 

31from .base import manager_of_class 

32from .base import NEVER_SET # noqa 

33from .base import NO_AUTOFLUSH 

34from .base import NO_CHANGE # noqa 

35from .base import NO_RAISE 

36from .base import NO_VALUE 

37from .base import NON_PERSISTENT_OK # noqa 

38from .base import PASSIVE_CLASS_MISMATCH # noqa 

39from .base import PASSIVE_NO_FETCH 

40from .base import PASSIVE_NO_FETCH_RELATED # noqa 

41from .base import PASSIVE_NO_INITIALIZE 

42from .base import PASSIVE_NO_RESULT 

43from .base import PASSIVE_OFF 

44from .base import PASSIVE_ONLY_PERSISTENT 

45from .base import PASSIVE_RETURN_NO_VALUE 

46from .base import RELATED_OBJECT_OK # noqa 

47from .base import SQL_OK # noqa 

48from .base import state_str 

49from .. import event 

50from .. import exc 

51from .. import inspection 

52from .. import util 

53from ..sql import base as sql_base 

54from ..sql import roles 

55from ..sql import traversals 

56from ..sql import visitors 

57from ..sql.traversals import HasCacheKey 

58from ..sql.visitors import InternalTraversal 

59 

60 

61class NoKey(str): 

62 pass 

63 

64 

65NO_KEY = NoKey("no name") 

66 

67 

68@inspection._self_inspects 

69class QueryableAttribute( 

70 interfaces._MappedAttribute, 

71 interfaces.InspectionAttr, 

72 interfaces.PropComparator, 

73 traversals.HasCopyInternals, 

74 roles.JoinTargetRole, 

75 roles.OnClauseRole, 

76 sql_base.Immutable, 

77 sql_base.MemoizedHasCacheKey, 

78): 

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

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

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

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

83 attribute. 

84 

85 

86 .. seealso:: 

87 

88 :class:`.InstrumentedAttribute` 

89 

90 :class:`.MapperProperty` 

91 

92 :attr:`_orm.Mapper.all_orm_descriptors` 

93 

94 :attr:`_orm.Mapper.attrs` 

95 """ 

96 

97 is_attribute = True 

98 

99 # PropComparator has a __visit_name__ to participate within 

100 # traversals. Disambiguate the attribute vs. a comparator. 

101 __visit_name__ = "orm_instrumented_attribute" 

102 

103 def __init__( 

104 self, 

105 class_, 

106 key, 

107 parententity, 

108 impl=None, 

109 comparator=None, 

110 of_type=None, 

111 extra_criteria=(), 

112 ): 

113 self.class_ = class_ 

114 self.key = key 

115 self._parententity = parententity 

116 self.impl = impl 

117 self.comparator = comparator 

118 self._of_type = of_type 

119 self._extra_criteria = extra_criteria 

120 

121 manager = manager_of_class(class_) 

122 # manager is None in the case of AliasedClass 

123 if manager: 

124 # propagate existing event listeners from 

125 # immediate superclass 

126 for base in manager._bases: 

127 if key in base: 

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

129 if base[key].dispatch._active_history: 

130 self.dispatch._active_history = True 

131 

132 _cache_key_traversal = [ 

133 ("key", visitors.ExtendedInternalTraversal.dp_string), 

134 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi), 

135 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi), 

136 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list), 

137 ] 

138 

139 def __reduce__(self): 

140 # this method is only used in terms of the 

141 # sqlalchemy.ext.serializer extension 

142 return ( 

143 _queryable_attribute_unreduce, 

144 ( 

145 self.key, 

146 self._parententity.mapper.class_, 

147 self._parententity, 

148 self._parententity.entity, 

149 ), 

150 ) 

151 

152 @util.memoized_property 

153 def _supports_population(self): 

154 return self.impl.supports_population 

155 

156 @property 

157 def _impl_uses_objects(self): 

158 return self.impl.uses_objects 

159 

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

161 return self.impl.get_history( 

162 instance_state(instance), instance_dict(instance), passive 

163 ) 

164 

165 @util.memoized_property 

166 def info(self): 

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

168 

169 The behavior here is as follows: 

170 

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

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

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

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

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

176 

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

178 any other kind of SQL expression other than a 

179 :class:`_schema.Column`, 

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

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

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

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

184 construct has defined one). 

185 

186 * If the attribute refers to any other kind of 

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

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

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

190 

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

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

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

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

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

196 ``MyClass.someattribute.property.info``. 

197 

198 .. seealso:: 

199 

200 :attr:`.SchemaItem.info` 

201 

202 :attr:`.MapperProperty.info` 

203 

204 """ 

205 return self.comparator.info 

206 

207 @util.memoized_property 

208 def parent(self): 

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

210 

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

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

213 of the parent entity which this attribute is associated 

214 with. 

215 

216 """ 

217 return inspection.inspect(self._parententity) 

218 

219 @util.memoized_property 

220 def expression(self): 

221 """The SQL expression object represented by this 

222 :class:`.QueryableAttribute`. 

223 

224 This will typically be an instance of a :class:`_sql.ColumnElement` 

225 subclass representing a column expression. 

226 

227 """ 

228 entity_namespace = self._entity_namespace 

229 assert isinstance(entity_namespace, HasCacheKey) 

230 

231 if self.key is NO_KEY: 

232 annotations = {"entity_namespace": entity_namespace} 

233 else: 

234 annotations = { 

235 "proxy_key": self.key, 

236 "proxy_owner": self._parententity, 

237 "entity_namespace": entity_namespace, 

238 } 

239 

240 ce = self.comparator.__clause_element__() 

241 try: 

242 anno = ce._annotate 

243 except AttributeError as ae: 

244 util.raise_( 

245 exc.InvalidRequestError( 

246 'When interpreting attribute "%s" as a SQL expression, ' 

247 "expected __clause_element__() to return " 

248 "a ClauseElement object, got: %r" % (self, ce) 

249 ), 

250 from_=ae, 

251 ) 

252 else: 

253 return anno(annotations) 

254 

255 @property 

256 def _entity_namespace(self): 

257 return self._parententity 

258 

259 @property 

260 def _annotations(self): 

261 return self.__clause_element__()._annotations 

262 

263 def __clause_element__(self): 

264 return self.expression 

265 

266 @property 

267 def _from_objects(self): 

268 return self.expression._from_objects 

269 

270 def _bulk_update_tuples(self, value): 

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

272 

273 return self.comparator._bulk_update_tuples(value) 

274 

275 def adapt_to_entity(self, adapt_to_entity): 

276 assert not self._of_type 

277 return self.__class__( 

278 adapt_to_entity.entity, 

279 self.key, 

280 impl=self.impl, 

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

282 parententity=adapt_to_entity, 

283 ) 

284 

285 def of_type(self, entity): 

286 return QueryableAttribute( 

287 self.class_, 

288 self.key, 

289 self._parententity, 

290 impl=self.impl, 

291 comparator=self.comparator.of_type(entity), 

292 of_type=inspection.inspect(entity), 

293 extra_criteria=self._extra_criteria, 

294 ) 

295 

296 def and_(self, *other): 

297 return QueryableAttribute( 

298 self.class_, 

299 self.key, 

300 self._parententity, 

301 impl=self.impl, 

302 comparator=self.comparator.and_(*other), 

303 of_type=self._of_type, 

304 extra_criteria=self._extra_criteria + other, 

305 ) 

306 

307 def _clone(self, **kw): 

308 return QueryableAttribute( 

309 self.class_, 

310 self.key, 

311 self._parententity, 

312 impl=self.impl, 

313 comparator=self.comparator, 

314 of_type=self._of_type, 

315 extra_criteria=self._extra_criteria, 

316 ) 

317 

318 def label(self, name): 

319 return self.__clause_element__().label(name) 

320 

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

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

323 

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

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

326 

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

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

329 

330 def __getattr__(self, key): 

331 try: 

332 return getattr(self.comparator, key) 

333 except AttributeError as err: 

334 util.raise_( 

335 AttributeError( 

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

337 "has an attribute %r" 

338 % ( 

339 type(self).__name__, 

340 type(self.comparator).__name__, 

341 self, 

342 key, 

343 ) 

344 ), 

345 replace_context=err, 

346 ) 

347 

348 def __str__(self): 

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

350 

351 @util.memoized_property 

352 def property(self): 

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

354 :class:`.QueryableAttribute`. 

355 

356 

357 Return values here will commonly be instances of 

358 :class:`.ColumnProperty` or :class:`.RelationshipProperty`. 

359 

360 

361 """ 

362 return self.comparator.property 

363 

364 

365def _queryable_attribute_unreduce(key, mapped_class, parententity, entity): 

366 # this method is only used in terms of the 

367 # sqlalchemy.ext.serializer extension 

368 if parententity.is_aliased_class: 

369 return entity._get_from_serialized(key, mapped_class, parententity) 

370 else: 

371 return getattr(entity, key) 

372 

373 

374if util.py3k: 

375 from typing import TypeVar, Generic 

376 

377 _T = TypeVar("_T") 

378 _Generic_T = Generic[_T] 

379else: 

380 _Generic_T = type("_Generic_T", (), {}) 

381 

382 

383class Mapped(QueryableAttribute, _Generic_T): 

384 """Represent an ORM mapped :term:`descriptor` attribute for typing 

385 purposes. 

386 

387 This class represents the complete descriptor interface for any class 

388 attribute that will have been :term:`instrumented` by the ORM 

389 :class:`_orm.Mapper` class. When used with typing stubs, it is the final 

390 type that would be used by a type checker such as mypy to provide the full 

391 behavioral contract for the attribute. 

392 

393 .. tip:: 

394 

395 The :class:`_orm.Mapped` class represents attributes that are handled 

396 directly by the :class:`_orm.Mapper` class. It does not include other 

397 Python descriptor classes that are provided as extensions, including 

398 :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`. 

399 While these systems still make use of ORM-specific superclasses 

400 and structures, they are not :term:`instrumented` by the 

401 :class:`_orm.Mapper` and instead provide their own functionality 

402 when they are accessed on a class. 

403 

404 When using the :ref:`SQLAlchemy Mypy plugin <mypy_toplevel>`, the 

405 :class:`_orm.Mapped` construct is used in typing annotations to indicate to 

406 the plugin those attributes that are expected to be mapped; the plugin also 

407 applies :class:`_orm.Mapped` as an annotation automatically when it scans 

408 through declarative mappings in :ref:`orm_declarative_table` style. For 

409 more indirect mapping styles such as 

410 :ref:`imperative table <orm_imperative_table_configuration>` it is 

411 typically applied explicitly to class level attributes that expect 

412 to be mapped based on a given :class:`_schema.Table` configuration. 

413 

414 :class:`_orm.Mapped` is defined in the 

415 `sqlalchemy2-stubs <https://pypi.org/project/sqlalchemy2-stubs>`_ project 

416 as a :pep:`484` generic class which may subscribe to any arbitrary Python 

417 type, which represents the Python type handled by the attribute:: 

418 

419 class MyMappedClass(Base): 

420 __table_ = Table( 

421 "some_table", Base.metadata, 

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

423 Column("data", String(50)), 

424 Column("created_at", DateTime) 

425 ) 

426 

427 id : Mapped[int] 

428 data: Mapped[str] 

429 created_at: Mapped[datetime] 

430 

431 For complete background on how to use :class:`_orm.Mapped` with 

432 pep-484 tools like Mypy, see the link below for background on SQLAlchemy's 

433 Mypy plugin. 

434 

435 .. versionadded:: 1.4 

436 

437 .. seealso:: 

438 

439 :ref:`mypy_toplevel` - complete background on Mypy integration 

440 

441 """ 

442 

443 def __get__(self, instance, owner): 

444 raise NotImplementedError() 

445 

446 def __set__(self, instance, value): 

447 raise NotImplementedError() 

448 

449 def __delete__(self, instance): 

450 raise NotImplementedError() 

451 

452 

453class InstrumentedAttribute(Mapped): 

454 """Class bound instrumented attribute which adds basic 

455 :term:`descriptor` methods. 

456 

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

458 

459 

460 """ 

461 

462 inherit_cache = True 

463 

464 def __set__(self, instance, value): 

465 self.impl.set( 

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

467 ) 

468 

469 def __delete__(self, instance): 

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

471 

472 def __get__(self, instance, owner): 

473 if instance is None: 

474 return self 

475 

476 dict_ = instance_dict(instance) 

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

478 return dict_[self.key] 

479 else: 

480 try: 

481 state = instance_state(instance) 

482 except AttributeError as err: 

483 util.raise_( 

484 orm_exc.UnmappedInstanceError(instance), 

485 replace_context=err, 

486 ) 

487 return self.impl.get(state, dict_) 

488 

489 

490class HasEntityNamespace(HasCacheKey): 

491 __slots__ = ("_entity_namespace",) 

492 

493 is_mapper = False 

494 is_aliased_class = False 

495 

496 _traverse_internals = [ 

497 ("_entity_namespace", InternalTraversal.dp_has_cache_key), 

498 ] 

499 

500 def __init__(self, ent): 

501 self._entity_namespace = ent 

502 

503 @property 

504 def entity_namespace(self): 

505 return self._entity_namespace.entity_namespace 

506 

507 

508def create_proxied_attribute(descriptor): 

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

510 

511 Returns a new QueryableAttribute type that delegates descriptor 

512 behavior and getattr() to the given descriptor. 

513 """ 

514 

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

516 # function is removed from ext/hybrid.py 

517 

518 class Proxy(QueryableAttribute): 

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

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

521 combination. 

522 

523 """ 

524 

525 _extra_criteria = () 

526 

527 def __init__( 

528 self, 

529 class_, 

530 key, 

531 descriptor, 

532 comparator, 

533 adapt_to_entity=None, 

534 doc=None, 

535 original_property=None, 

536 ): 

537 self.class_ = class_ 

538 self.key = key 

539 self.descriptor = descriptor 

540 self.original_property = original_property 

541 self._comparator = comparator 

542 self._adapt_to_entity = adapt_to_entity 

543 self.__doc__ = doc 

544 

545 _is_internal_proxy = True 

546 

547 _cache_key_traversal = [ 

548 ("key", visitors.ExtendedInternalTraversal.dp_string), 

549 ("_parententity", visitors.ExtendedInternalTraversal.dp_multi), 

550 ] 

551 

552 @property 

553 def _impl_uses_objects(self): 

554 return ( 

555 self.original_property is not None 

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

557 ) 

558 

559 @property 

560 def _parententity(self): 

561 return inspection.inspect(self.class_, raiseerr=False) 

562 

563 @property 

564 def _entity_namespace(self): 

565 if hasattr(self._comparator, "_parententity"): 

566 return self._comparator._parententity 

567 else: 

568 # used by hybrid attributes which try to remain 

569 # agnostic of any ORM concepts like mappers 

570 return HasEntityNamespace(self._parententity) 

571 

572 @property 

573 def property(self): 

574 return self.comparator.property 

575 

576 @util.memoized_property 

577 def comparator(self): 

578 if callable(self._comparator): 

579 self._comparator = self._comparator() 

580 if self._adapt_to_entity: 

581 self._comparator = self._comparator.adapt_to_entity( 

582 self._adapt_to_entity 

583 ) 

584 return self._comparator 

585 

586 def adapt_to_entity(self, adapt_to_entity): 

587 return self.__class__( 

588 adapt_to_entity.entity, 

589 self.key, 

590 self.descriptor, 

591 self._comparator, 

592 adapt_to_entity, 

593 ) 

594 

595 def _clone(self, **kw): 

596 return self.__class__( 

597 self.class_, 

598 self.key, 

599 self.descriptor, 

600 self._comparator, 

601 adapt_to_entity=self._adapt_to_entity, 

602 original_property=self.original_property, 

603 ) 

604 

605 def __get__(self, instance, owner): 

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

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

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

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

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

611 return self 

612 else: 

613 return retval 

614 

615 def __str__(self): 

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

617 

618 def __getattr__(self, attribute): 

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

620 comparator.""" 

621 try: 

622 return getattr(descriptor, attribute) 

623 except AttributeError as err: 

624 if attribute == "comparator": 

625 util.raise_( 

626 AttributeError("comparator"), replace_context=err 

627 ) 

628 try: 

629 # comparator itself might be unreachable 

630 comparator = self.comparator 

631 except AttributeError as err2: 

632 util.raise_( 

633 AttributeError( 

634 "Neither %r object nor unconfigured comparator " 

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

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

637 ), 

638 replace_context=err2, 

639 ) 

640 else: 

641 try: 

642 return getattr(comparator, attribute) 

643 except AttributeError as err3: 

644 util.raise_( 

645 AttributeError( 

646 "Neither %r object nor %r object " 

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

648 % ( 

649 type(descriptor).__name__, 

650 type(comparator).__name__, 

651 self, 

652 attribute, 

653 ) 

654 ), 

655 replace_context=err3, 

656 ) 

657 

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

659 

660 util.monkeypatch_proxied_specials( 

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

662 ) 

663 return Proxy 

664 

665 

666OP_REMOVE = util.symbol("REMOVE") 

667OP_APPEND = util.symbol("APPEND") 

668OP_REPLACE = util.symbol("REPLACE") 

669OP_BULK_REPLACE = util.symbol("BULK_REPLACE") 

670OP_MODIFIED = util.symbol("MODIFIED") 

671 

672 

673class AttributeEvent(object): 

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

675 events. 

676 

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

678 a means of controlling propagation across a chain of attribute 

679 operations. 

680 

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

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

683 :meth:`.AttributeEvents.set`, 

684 and :meth:`.AttributeEvents.remove`. 

685 

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

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

688 across two mutually-dependent attributes. 

689 

690 .. versionadded:: 0.9.0 

691 

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

693 initiator. 

694 

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

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

697 source operation. 

698 

699 """ 

700 

701 __slots__ = "impl", "op", "parent_token" 

702 

703 def __init__(self, attribute_impl, op): 

704 self.impl = attribute_impl 

705 self.op = op 

706 self.parent_token = self.impl.parent_token 

707 

708 def __eq__(self, other): 

709 return ( 

710 isinstance(other, AttributeEvent) 

711 and other.impl is self.impl 

712 and other.op == self.op 

713 ) 

714 

715 @property 

716 def key(self): 

717 return self.impl.key 

718 

719 def hasparent(self, state): 

720 return self.impl.hasparent(state) 

721 

722 

723Event = AttributeEvent 

724 

725 

726class AttributeImpl(object): 

727 """internal implementation for instrumented attributes.""" 

728 

729 def __init__( 

730 self, 

731 class_, 

732 key, 

733 callable_, 

734 dispatch, 

735 trackparent=False, 

736 compare_function=None, 

737 active_history=False, 

738 parent_token=None, 

739 load_on_unexpire=True, 

740 send_modified_events=True, 

741 accepts_scalar_loader=None, 

742 **kwargs 

743 ): 

744 r"""Construct an AttributeImpl. 

745 

746 :param \class_: associated class 

747 

748 :param key: string name of the attribute 

749 

750 :param \callable_: 

751 optional function which generates a callable based on a parent 

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

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

754 already. 

755 

756 :param trackparent: 

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

758 to it via this attribute. 

759 

760 :param compare_function: 

761 a function that compares two values which are normally 

762 assignable to this attribute. 

763 

764 :param active_history: 

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

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

767 

768 :param parent_token: 

769 Usually references the MapperProperty, used as a key for 

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

771 Allows multiple AttributeImpls to all match a single 

772 owner attribute. 

773 

774 :param load_on_unexpire: 

775 if False, don't include this attribute in a load-on-expired 

776 operation, i.e. the "expired_attribute_loader" process. 

777 The attribute can still be in the "expired" list and be 

778 considered to be "expired". Previously, this flag was called 

779 "expire_missing" and is only used by a deferred column 

780 attribute. 

781 

782 :param send_modified_events: 

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

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

785 history entry. 

786 

787 """ 

788 self.class_ = class_ 

789 self.key = key 

790 self.callable_ = callable_ 

791 self.dispatch = dispatch 

792 self.trackparent = trackparent 

793 self.parent_token = parent_token or self 

794 self.send_modified_events = send_modified_events 

795 if compare_function is None: 

796 self.is_equal = operator.eq 

797 else: 

798 self.is_equal = compare_function 

799 

800 if accepts_scalar_loader is not None: 

801 self.accepts_scalar_loader = accepts_scalar_loader 

802 else: 

803 self.accepts_scalar_loader = self.default_accepts_scalar_loader 

804 

805 _deferred_history = kwargs.pop("_deferred_history", False) 

806 self._deferred_history = _deferred_history 

807 

808 if active_history: 

809 self.dispatch._active_history = True 

810 

811 self.load_on_unexpire = load_on_unexpire 

812 self._modified_token = Event(self, OP_MODIFIED) 

813 

814 __slots__ = ( 

815 "class_", 

816 "key", 

817 "callable_", 

818 "dispatch", 

819 "trackparent", 

820 "parent_token", 

821 "send_modified_events", 

822 "is_equal", 

823 "load_on_unexpire", 

824 "_modified_token", 

825 "accepts_scalar_loader", 

826 "_deferred_history", 

827 ) 

828 

829 def __str__(self): 

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

831 

832 def _get_active_history(self): 

833 """Backwards compat for impl.active_history""" 

834 

835 return self.dispatch._active_history 

836 

837 def _set_active_history(self, value): 

838 self.dispatch._active_history = value 

839 

840 active_history = property(_get_active_history, _set_active_history) 

841 

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

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

844 the given state. 

845 

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

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

848 

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

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

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

852 flag is set. 

853 

854 An instance attribute that is loaded by a callable function 

855 will also not have a `hasparent` flag. 

856 

857 """ 

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

859 assert self.trackparent, msg 

860 

861 return ( 

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

863 ) 

864 

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

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

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

868 attribute represented by this ``InstrumentedAttribute``. 

869 

870 """ 

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

872 assert self.trackparent, msg 

873 

874 id_ = id(self.parent_token) 

875 if value: 

876 state.parents[id_] = parent_state 

877 else: 

878 if id_ in state.parents: 

879 last_parent = state.parents[id_] 

880 

881 if ( 

882 last_parent is not False 

883 and last_parent.key != parent_state.key 

884 ): 

885 

886 if last_parent.obj() is None: 

887 raise orm_exc.StaleDataError( 

888 "Removing state %s from parent " 

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

890 "but the parent record " 

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

892 "is the most recent parent." 

893 % ( 

894 state_str(state), 

895 state_str(parent_state), 

896 self.key, 

897 ) 

898 ) 

899 

900 return 

901 

902 state.parents[id_] = False 

903 

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

905 raise NotImplementedError() 

906 

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

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

909 for all objects in this attribute's current state 

910 + history. 

911 

912 Only applies to object-based attributes. 

913 

914 This is an inlining of existing functionality 

915 which roughly corresponds to: 

916 

917 get_state_history( 

918 state, 

919 key, 

920 passive=PASSIVE_NO_INITIALIZE).sum() 

921 

922 """ 

923 raise NotImplementedError() 

924 

925 def _default_value(self, state, dict_): 

926 """Produce an empty value for an uninitialized scalar attribute.""" 

927 

928 assert self.key not in dict_, ( 

929 "_default_value should only be invoked for an " 

930 "uninitialized or expired attribute" 

931 ) 

932 

933 value = None 

934 for fn in self.dispatch.init_scalar: 

935 ret = fn(state, value, dict_) 

936 if ret is not ATTR_EMPTY: 

937 value = ret 

938 

939 return value 

940 

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

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

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

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

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

946 """ 

947 if self.key in dict_: 

948 return dict_[self.key] 

949 else: 

950 # if history present, don't load 

951 key = self.key 

952 if ( 

953 key not in state.committed_state 

954 or state.committed_state[key] is NO_VALUE 

955 ): 

956 if not passive & CALLABLES_OK: 

957 return PASSIVE_NO_RESULT 

958 

959 value = self._fire_loader_callables(state, key, passive) 

960 

961 if value is PASSIVE_NO_RESULT or value is NO_VALUE: 

962 return value 

963 elif value is ATTR_WAS_SET: 

964 try: 

965 return dict_[key] 

966 except KeyError as err: 

967 # TODO: no test coverage here. 

968 util.raise_( 

969 KeyError( 

970 "Deferred loader for attribute " 

971 "%r failed to populate " 

972 "correctly" % key 

973 ), 

974 replace_context=err, 

975 ) 

976 elif value is not ATTR_EMPTY: 

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

978 

979 if not passive & INIT_OK: 

980 return NO_VALUE 

981 else: 

982 return self._default_value(state, dict_) 

983 

984 def _fire_loader_callables(self, state, key, passive): 

985 if ( 

986 self.accepts_scalar_loader 

987 and self.load_on_unexpire 

988 and key in state.expired_attributes 

989 ): 

990 return state._load_expired(state, passive) 

991 elif key in state.callables: 

992 callable_ = state.callables[key] 

993 return callable_(state, passive) 

994 elif self.callable_: 

995 return self.callable_(state, passive) 

996 else: 

997 return ATTR_EMPTY 

998 

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

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

1001 

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

1003 self.set( 

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

1005 ) 

1006 

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

1008 self.set( 

1009 state, 

1010 dict_, 

1011 None, 

1012 initiator, 

1013 passive=passive, 

1014 check_old=value, 

1015 pop=True, 

1016 ) 

1017 

1018 def set( 

1019 self, 

1020 state, 

1021 dict_, 

1022 value, 

1023 initiator, 

1024 passive=PASSIVE_OFF, 

1025 check_old=None, 

1026 pop=False, 

1027 ): 

1028 raise NotImplementedError() 

1029 

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

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

1032 

1033 if self.key in state.committed_state: 

1034 value = state.committed_state[self.key] 

1035 if value is NO_VALUE: 

1036 return None 

1037 else: 

1038 return value 

1039 else: 

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

1041 

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

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

1044 

1045 dict_[self.key] = value 

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

1047 return value 

1048 

1049 

1050class ScalarAttributeImpl(AttributeImpl): 

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

1052 

1053 default_accepts_scalar_loader = True 

1054 uses_objects = False 

1055 supports_population = True 

1056 collection = False 

1057 dynamic = False 

1058 

1059 __slots__ = "_replace_token", "_append_token", "_remove_token" 

1060 

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

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

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

1064 self._remove_token = Event(self, OP_REMOVE) 

1065 

1066 def delete(self, state, dict_): 

1067 if self.dispatch._active_history: 

1068 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE) 

1069 else: 

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

1071 

1072 if self.dispatch.remove: 

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

1074 state._modified_event(dict_, self, old) 

1075 

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

1077 if ( 

1078 existing is NO_VALUE 

1079 and old is NO_VALUE 

1080 and not state.expired 

1081 and self.key not in state.expired_attributes 

1082 ): 

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

1084 

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

1086 if self.key in dict_: 

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

1088 elif self.key in state.committed_state: 

1089 return History.from_scalar_attribute(self, state, NO_VALUE) 

1090 else: 

1091 if passive & INIT_OK: 

1092 passive ^= INIT_OK 

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

1094 if current is PASSIVE_NO_RESULT: 

1095 return HISTORY_BLANK 

1096 else: 

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

1098 

1099 def set( 

1100 self, 

1101 state, 

1102 dict_, 

1103 value, 

1104 initiator, 

1105 passive=PASSIVE_OFF, 

1106 check_old=None, 

1107 pop=False, 

1108 ): 

1109 if self.dispatch._active_history: 

1110 old = self.get(state, dict_, PASSIVE_RETURN_NO_VALUE) 

1111 else: 

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

1113 

1114 if self.dispatch.set: 

1115 value = self.fire_replace_event( 

1116 state, dict_, value, old, initiator 

1117 ) 

1118 state._modified_event(dict_, self, old) 

1119 dict_[self.key] = value 

1120 

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

1122 for fn in self.dispatch.set: 

1123 value = fn( 

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

1125 ) 

1126 return value 

1127 

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

1129 for fn in self.dispatch.remove: 

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

1131 

1132 @property 

1133 def type(self): 

1134 self.property.columns[0].type 

1135 

1136 

1137class ScalarObjectAttributeImpl(ScalarAttributeImpl): 

1138 """represents a scalar-holding InstrumentedAttribute, 

1139 where the target object is also instrumented. 

1140 

1141 Adds events to delete/set operations. 

1142 

1143 """ 

1144 

1145 default_accepts_scalar_loader = False 

1146 uses_objects = True 

1147 supports_population = True 

1148 collection = False 

1149 

1150 __slots__ = () 

1151 

1152 def delete(self, state, dict_): 

1153 if self.dispatch._active_history: 

1154 old = self.get( 

1155 state, 

1156 dict_, 

1157 passive=PASSIVE_ONLY_PERSISTENT 

1158 | NO_AUTOFLUSH 

1159 | LOAD_AGAINST_COMMITTED, 

1160 ) 

1161 else: 

1162 old = self.get( 

1163 state, 

1164 dict_, 

1165 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1166 | LOAD_AGAINST_COMMITTED 

1167 | NO_RAISE, 

1168 ) 

1169 

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

1171 

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

1173 

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

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

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

1177 if ( 

1178 existing is NO_VALUE 

1179 and old is not PASSIVE_NO_RESULT 

1180 and state.key is None 

1181 ): 

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

1183 

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

1185 if self.key in dict_: 

1186 current = dict_[self.key] 

1187 else: 

1188 if passive & INIT_OK: 

1189 passive ^= INIT_OK 

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

1191 if current is PASSIVE_NO_RESULT: 

1192 return HISTORY_BLANK 

1193 

1194 if not self._deferred_history: 

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

1196 else: 

1197 original = state.committed_state.get(self.key, _NO_HISTORY) 

1198 if original is PASSIVE_NO_RESULT: 

1199 

1200 loader_passive = passive | ( 

1201 PASSIVE_ONLY_PERSISTENT 

1202 | NO_AUTOFLUSH 

1203 | LOAD_AGAINST_COMMITTED 

1204 | NO_RAISE 

1205 | DEFERRED_HISTORY_LOAD 

1206 ) 

1207 original = self._fire_loader_callables( 

1208 state, self.key, loader_passive 

1209 ) 

1210 return History.from_object_attribute( 

1211 self, state, current, original=original 

1212 ) 

1213 

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

1215 if self.key in dict_: 

1216 current = dict_[self.key] 

1217 elif passive & CALLABLES_OK: 

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

1219 else: 

1220 return [] 

1221 

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

1223 if ( 

1224 current is not None 

1225 and current is not PASSIVE_NO_RESULT 

1226 and current is not NO_VALUE 

1227 ): 

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

1229 else: 

1230 ret = [(None, None)] 

1231 

1232 if self.key in state.committed_state: 

1233 original = state.committed_state[self.key] 

1234 if ( 

1235 original is not None 

1236 and original is not PASSIVE_NO_RESULT 

1237 and original is not NO_VALUE 

1238 and original is not current 

1239 ): 

1240 

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

1242 return ret 

1243 

1244 def set( 

1245 self, 

1246 state, 

1247 dict_, 

1248 value, 

1249 initiator, 

1250 passive=PASSIVE_OFF, 

1251 check_old=None, 

1252 pop=False, 

1253 ): 

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

1255 

1256 if self.dispatch._active_history: 

1257 old = self.get( 

1258 state, 

1259 dict_, 

1260 passive=PASSIVE_ONLY_PERSISTENT 

1261 | NO_AUTOFLUSH 

1262 | LOAD_AGAINST_COMMITTED, 

1263 ) 

1264 else: 

1265 old = self.get( 

1266 state, 

1267 dict_, 

1268 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1269 | LOAD_AGAINST_COMMITTED 

1270 | NO_RAISE, 

1271 ) 

1272 

1273 if ( 

1274 check_old is not None 

1275 and old is not PASSIVE_NO_RESULT 

1276 and check_old is not old 

1277 ): 

1278 if pop: 

1279 return 

1280 else: 

1281 raise ValueError( 

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

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

1284 ) 

1285 

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

1287 dict_[self.key] = value 

1288 

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

1290 if self.trackparent and value not in ( 

1291 None, 

1292 PASSIVE_NO_RESULT, 

1293 NO_VALUE, 

1294 ): 

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

1296 

1297 for fn in self.dispatch.remove: 

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

1299 

1300 state._modified_event(dict_, self, value) 

1301 

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

1303 if self.trackparent: 

1304 if previous is not value and previous not in ( 

1305 None, 

1306 PASSIVE_NO_RESULT, 

1307 NO_VALUE, 

1308 ): 

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

1310 

1311 for fn in self.dispatch.set: 

1312 value = fn( 

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

1314 ) 

1315 

1316 state._modified_event(dict_, self, previous) 

1317 

1318 if self.trackparent: 

1319 if value is not None: 

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

1321 

1322 return value 

1323 

1324 

1325class CollectionAttributeImpl(AttributeImpl): 

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

1327 

1328 Only handles collections of instrumented objects. 

1329 

1330 InstrumentedCollectionAttribute holds an arbitrary, user-specified 

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

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

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

1334 

1335 """ 

1336 

1337 default_accepts_scalar_loader = False 

1338 uses_objects = True 

1339 supports_population = True 

1340 collection = True 

1341 dynamic = False 

1342 

1343 __slots__ = ( 

1344 "copy", 

1345 "collection_factory", 

1346 "_append_token", 

1347 "_remove_token", 

1348 "_bulk_replace_token", 

1349 "_duck_typed_as", 

1350 ) 

1351 

1352 def __init__( 

1353 self, 

1354 class_, 

1355 key, 

1356 callable_, 

1357 dispatch, 

1358 typecallable=None, 

1359 trackparent=False, 

1360 copy_function=None, 

1361 compare_function=None, 

1362 **kwargs 

1363 ): 

1364 super(CollectionAttributeImpl, self).__init__( 

1365 class_, 

1366 key, 

1367 callable_, 

1368 dispatch, 

1369 trackparent=trackparent, 

1370 compare_function=compare_function, 

1371 **kwargs 

1372 ) 

1373 

1374 if copy_function is None: 

1375 copy_function = self.__copy 

1376 self.copy = copy_function 

1377 self.collection_factory = typecallable 

1378 self._append_token = Event(self, OP_APPEND) 

1379 self._remove_token = Event(self, OP_REMOVE) 

1380 self._bulk_replace_token = Event(self, OP_BULK_REPLACE) 

1381 self._duck_typed_as = util.duck_type_collection( 

1382 self.collection_factory() 

1383 ) 

1384 

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

1386 

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

1388 def link(target, collection, collection_adapter): 

1389 collection._sa_linker(collection_adapter) 

1390 

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

1392 def unlink(target, collection, collection_adapter): 

1393 collection._sa_linker(None) 

1394 

1395 def __copy(self, item): 

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

1397 

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

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

1400 if current is PASSIVE_NO_RESULT: 

1401 return HISTORY_BLANK 

1402 else: 

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

1404 

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

1406 # NOTE: passive is ignored here at the moment 

1407 

1408 if self.key not in dict_: 

1409 return [] 

1410 

1411 current = dict_[self.key] 

1412 current = getattr(current, "_sa_adapter") 

1413 

1414 if self.key in state.committed_state: 

1415 original = state.committed_state[self.key] 

1416 if original is not NO_VALUE: 

1417 current_states = [ 

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

1419 for c in current 

1420 ] 

1421 original_states = [ 

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

1423 for c in original 

1424 ] 

1425 

1426 current_set = dict(current_states) 

1427 original_set = dict(original_states) 

1428 

1429 return ( 

1430 [ 

1431 (s, o) 

1432 for s, o in current_states 

1433 if s not in original_set 

1434 ] 

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

1436 + [ 

1437 (s, o) 

1438 for s, o in original_states 

1439 if s not in current_set 

1440 ] 

1441 ) 

1442 

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

1444 

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

1446 for fn in self.dispatch.append: 

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

1448 

1449 state._modified_event(dict_, self, NO_VALUE, True) 

1450 

1451 if self.trackparent and value is not None: 

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

1453 

1454 return value 

1455 

1456 def fire_append_wo_mutation_event(self, state, dict_, value, initiator): 

1457 for fn in self.dispatch.append_wo_mutation: 

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

1459 

1460 return value 

1461 

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

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

1464 

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

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

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

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

1469 

1470 """ 

1471 state._modified_event(dict_, self, NO_VALUE, True) 

1472 

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

1474 if self.trackparent and value is not None: 

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

1476 

1477 for fn in self.dispatch.remove: 

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

1479 

1480 state._modified_event(dict_, self, NO_VALUE, True) 

1481 

1482 def delete(self, state, dict_): 

1483 if self.key not in dict_: 

1484 return 

1485 

1486 state._modified_event(dict_, self, NO_VALUE, True) 

1487 

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

1489 collection.clear_with_event() 

1490 

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

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

1493 del dict_[self.key] 

1494 

1495 def _default_value(self, state, dict_): 

1496 """Produce an empty collection for an un-initialized attribute""" 

1497 

1498 assert self.key not in dict_, ( 

1499 "_default_value should only be invoked for an " 

1500 "uninitialized or expired attribute" 

1501 ) 

1502 

1503 if self.key in state._empty_collections: 

1504 return state._empty_collections[self.key] 

1505 

1506 adapter, user_data = self._initialize_collection(state) 

1507 adapter._set_empty(user_data) 

1508 return user_data 

1509 

1510 def _initialize_collection(self, state): 

1511 

1512 adapter, collection = state.manager.initialize_collection( 

1513 self.key, state, self.collection_factory 

1514 ) 

1515 

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

1517 

1518 return adapter, collection 

1519 

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

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

1522 if collection is PASSIVE_NO_RESULT: 

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

1524 assert ( 

1525 self.key not in dict_ 

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

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

1528 else: 

1529 collection.append_with_event(value, initiator) 

1530 

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

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

1533 if collection is PASSIVE_NO_RESULT: 

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

1535 assert ( 

1536 self.key not in dict_ 

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

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

1539 else: 

1540 collection.remove_with_event(value, initiator) 

1541 

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

1543 try: 

1544 # TODO: better solution here would be to add 

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

1546 # "remover". 

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

1548 except (ValueError, KeyError, IndexError): 

1549 pass 

1550 

1551 def set( 

1552 self, 

1553 state, 

1554 dict_, 

1555 value, 

1556 initiator=None, 

1557 passive=PASSIVE_OFF, 

1558 check_old=None, 

1559 pop=False, 

1560 _adapt=True, 

1561 ): 

1562 iterable = orig_iterable = value 

1563 

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

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

1566 new_collection, user_data = self._initialize_collection(state) 

1567 if _adapt: 

1568 if new_collection._converter is not None: 

1569 iterable = new_collection._converter(iterable) 

1570 else: 

1571 setting_type = util.duck_type_collection(iterable) 

1572 receiving_type = self._duck_typed_as 

1573 

1574 if setting_type is not receiving_type: 

1575 given = ( 

1576 iterable is None 

1577 and "None" 

1578 or iterable.__class__.__name__ 

1579 ) 

1580 wanted = self._duck_typed_as.__name__ 

1581 raise TypeError( 

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

1583 % (given, wanted) 

1584 ) 

1585 

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

1587 # adapter. 

1588 if hasattr(iterable, "_sa_iterator"): 

1589 iterable = iterable._sa_iterator() 

1590 elif setting_type is dict: 

1591 if util.py3k: 

1592 iterable = iterable.values() 

1593 else: 

1594 iterable = getattr( 

1595 iterable, "itervalues", iterable.values 

1596 )() 

1597 else: 

1598 iterable = iter(iterable) 

1599 new_values = list(iterable) 

1600 

1601 evt = self._bulk_replace_token 

1602 

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

1604 

1605 # propagate NO_RAISE in passive through to the get() for the 

1606 # existing object (ticket #8862) 

1607 old = self.get( 

1608 state, 

1609 dict_, 

1610 passive=PASSIVE_ONLY_PERSISTENT ^ (passive & NO_RAISE), 

1611 ) 

1612 if old is PASSIVE_NO_RESULT: 

1613 old = self._default_value(state, dict_) 

1614 elif old is orig_iterable: 

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

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

1617 return 

1618 

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

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

1621 

1622 old_collection = old._sa_adapter 

1623 

1624 dict_[self.key] = user_data 

1625 

1626 collections.bulk_replace( 

1627 new_values, old_collection, new_collection, initiator=evt 

1628 ) 

1629 

1630 self._dispose_previous_collection(state, old, old_collection, True) 

1631 

1632 def _dispose_previous_collection( 

1633 self, state, collection, adapter, fire_event 

1634 ): 

1635 del collection._sa_adapter 

1636 

1637 # discarding old collection make sure it is not referenced in empty 

1638 # collections. 

1639 state._empty_collections.pop(self.key, None) 

1640 if fire_event: 

1641 self.dispatch.dispose_collection(state, collection, adapter) 

1642 

1643 def _invalidate_collection(self, collection): 

1644 adapter = getattr(collection, "_sa_adapter") 

1645 adapter.invalidated = True 

1646 

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

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

1649 

1650 collection, user_data = self._initialize_collection(state) 

1651 

1652 if value: 

1653 collection.append_multiple_without_event(value) 

1654 

1655 state.dict[self.key] = user_data 

1656 

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

1658 

1659 if self.key in state._pending_mutations: 

1660 # pending items exist. issue a modified event, 

1661 # add/remove new items. 

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

1663 

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

1665 added = pending.added_items 

1666 removed = pending.deleted_items 

1667 for item in added: 

1668 collection.append_without_event(item) 

1669 for item in removed: 

1670 collection.remove_without_event(item) 

1671 

1672 return user_data 

1673 

1674 def get_collection( 

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

1676 ): 

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

1678 

1679 if user_data is None, retrieves it from the state using normal 

1680 "get()" rules, which will fire lazy callables or return the "empty" 

1681 collection value. 

1682 

1683 """ 

1684 if user_data is None: 

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

1686 if user_data is PASSIVE_NO_RESULT: 

1687 return user_data 

1688 

1689 return user_data._sa_adapter 

1690 

1691 

1692def backref_listeners(attribute, key, uselist): 

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

1694 

1695 # use easily recognizable names for stack traces. 

1696 

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

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

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

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

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

1702 

1703 parent_token = attribute.impl.parent_token 

1704 parent_impl = attribute.impl 

1705 

1706 def _acceptable_key_err(child_state, initiator, child_impl): 

1707 raise ValueError( 

1708 "Bidirectional attribute conflict detected: " 

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

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

1711 'via the backref "%s".' 

1712 % ( 

1713 state_str(child_state), 

1714 initiator.parent_token, 

1715 child_impl.parent_token, 

1716 attribute.impl.parent_token, 

1717 ) 

1718 ) 

1719 

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

1721 if oldchild is child: 

1722 return child 

1723 if ( 

1724 oldchild is not None 

1725 and oldchild is not PASSIVE_NO_RESULT 

1726 and oldchild is not NO_VALUE 

1727 ): 

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

1729 # present when updating via a backref. 

1730 old_state, old_dict = ( 

1731 instance_state(oldchild), 

1732 instance_dict(oldchild), 

1733 ) 

1734 impl = old_state.manager[key].impl 

1735 

1736 # tokens to test for a recursive loop. 

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

1738 check_recursive_token = impl._replace_token 

1739 else: 

1740 check_recursive_token = impl._remove_token 

1741 

1742 if initiator is not check_recursive_token: 

1743 impl.pop( 

1744 old_state, 

1745 old_dict, 

1746 state.obj(), 

1747 parent_impl._append_token, 

1748 passive=PASSIVE_NO_FETCH, 

1749 ) 

1750 

1751 if child is not None: 

1752 child_state, child_dict = ( 

1753 instance_state(child), 

1754 instance_dict(child), 

1755 ) 

1756 child_impl = child_state.manager[key].impl 

1757 

1758 if ( 

1759 initiator.parent_token is not parent_token 

1760 and initiator.parent_token is not child_impl.parent_token 

1761 ): 

1762 _acceptable_key_err(state, initiator, child_impl) 

1763 

1764 # tokens to test for a recursive loop. 

1765 check_append_token = child_impl._append_token 

1766 check_bulk_replace_token = ( 

1767 child_impl._bulk_replace_token 

1768 if child_impl.collection 

1769 else None 

1770 ) 

1771 

1772 if ( 

1773 initiator is not check_append_token 

1774 and initiator is not check_bulk_replace_token 

1775 ): 

1776 child_impl.append( 

1777 child_state, 

1778 child_dict, 

1779 state.obj(), 

1780 initiator, 

1781 passive=PASSIVE_NO_FETCH, 

1782 ) 

1783 return child 

1784 

1785 def emit_backref_from_collection_append_event(state, child, initiator): 

1786 if child is None: 

1787 return 

1788 

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

1790 child_impl = child_state.manager[key].impl 

1791 

1792 if ( 

1793 initiator.parent_token is not parent_token 

1794 and initiator.parent_token is not child_impl.parent_token 

1795 ): 

1796 _acceptable_key_err(state, initiator, child_impl) 

1797 

1798 # tokens to test for a recursive loop. 

1799 check_append_token = child_impl._append_token 

1800 check_bulk_replace_token = ( 

1801 child_impl._bulk_replace_token if child_impl.collection else None 

1802 ) 

1803 

1804 if ( 

1805 initiator is not check_append_token 

1806 and initiator is not check_bulk_replace_token 

1807 ): 

1808 child_impl.append( 

1809 child_state, 

1810 child_dict, 

1811 state.obj(), 

1812 initiator, 

1813 passive=PASSIVE_NO_FETCH, 

1814 ) 

1815 return child 

1816 

1817 def emit_backref_from_collection_remove_event(state, child, initiator): 

1818 if ( 

1819 child is not None 

1820 and child is not PASSIVE_NO_RESULT 

1821 and child is not NO_VALUE 

1822 ): 

1823 child_state, child_dict = ( 

1824 instance_state(child), 

1825 instance_dict(child), 

1826 ) 

1827 child_impl = child_state.manager[key].impl 

1828 

1829 # tokens to test for a recursive loop. 

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

1831 check_remove_token = child_impl._remove_token 

1832 check_replace_token = child_impl._replace_token 

1833 check_for_dupes_on_remove = uselist and not parent_impl.dynamic 

1834 else: 

1835 check_remove_token = child_impl._remove_token 

1836 check_replace_token = ( 

1837 child_impl._bulk_replace_token 

1838 if child_impl.collection 

1839 else None 

1840 ) 

1841 check_for_dupes_on_remove = False 

1842 

1843 if ( 

1844 initiator is not check_remove_token 

1845 and initiator is not check_replace_token 

1846 ): 

1847 

1848 if not check_for_dupes_on_remove or not util.has_dupes( 

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

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

1851 state.dict[parent_impl.key], 

1852 child, 

1853 ): 

1854 child_impl.pop( 

1855 child_state, 

1856 child_dict, 

1857 state.obj(), 

1858 initiator, 

1859 passive=PASSIVE_NO_FETCH, 

1860 ) 

1861 

1862 if uselist: 

1863 event.listen( 

1864 attribute, 

1865 "append", 

1866 emit_backref_from_collection_append_event, 

1867 retval=True, 

1868 raw=True, 

1869 ) 

1870 else: 

1871 event.listen( 

1872 attribute, 

1873 "set", 

1874 emit_backref_from_scalar_set_event, 

1875 retval=True, 

1876 raw=True, 

1877 ) 

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

1879 event.listen( 

1880 attribute, 

1881 "remove", 

1882 emit_backref_from_collection_remove_event, 

1883 retval=True, 

1884 raw=True, 

1885 ) 

1886 

1887 

1888_NO_HISTORY = util.symbol("NO_HISTORY") 

1889_NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)]) 

1890 

1891 

1892class History(util.namedtuple("History", ["added", "unchanged", "deleted"])): 

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

1894 representing the changes which have occurred on an instrumented 

1895 attribute. 

1896 

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

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

1899 

1900 from sqlalchemy import inspect 

1901 

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

1903 

1904 Each tuple member is an iterable sequence: 

1905 

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

1907 tuple element). 

1908 

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

1910 attribute (the second tuple element). 

1911 

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

1913 attribute (the third tuple element). 

1914 

1915 """ 

1916 

1917 def __bool__(self): 

1918 return self != HISTORY_BLANK 

1919 

1920 __nonzero__ = __bool__ 

1921 

1922 def empty(self): 

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

1924 and no existing, unchanged state. 

1925 

1926 """ 

1927 

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

1929 

1930 def sum(self): 

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

1932 

1933 return ( 

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

1935 ) 

1936 

1937 def non_deleted(self): 

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

1939 

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

1941 

1942 def non_added(self): 

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

1944 

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

1946 

1947 def has_changes(self): 

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

1949 

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

1951 

1952 def as_state(self): 

1953 return History( 

1954 [ 

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

1956 for c in self.added 

1957 ], 

1958 [ 

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

1960 for c in self.unchanged 

1961 ], 

1962 [ 

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

1964 for c in self.deleted 

1965 ], 

1966 ) 

1967 

1968 @classmethod 

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

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

1971 

1972 if original is _NO_HISTORY: 

1973 if current is NO_VALUE: 

1974 return cls((), (), ()) 

1975 else: 

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

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

1978 elif ( 

1979 current is not NO_VALUE 

1980 and attribute.is_equal(current, original) is True 

1981 ): 

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

1983 else: 

1984 # current convention on native scalars is to not 

1985 # include information 

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

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

1988 # key situations 

1989 if id(original) in _NO_STATE_SYMBOLS: 

1990 deleted = () 

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

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

1993 if id(current) in _NO_STATE_SYMBOLS: 

1994 current = None 

1995 else: 

1996 deleted = [original] 

1997 if current is NO_VALUE: 

1998 return cls((), (), deleted) 

1999 else: 

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

2001 

2002 @classmethod 

2003 def from_object_attribute( 

2004 cls, attribute, state, current, original=_NO_HISTORY 

2005 ): 

2006 if original is _NO_HISTORY: 

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

2008 

2009 if original is _NO_HISTORY: 

2010 if current is NO_VALUE: 

2011 return cls((), (), ()) 

2012 else: 

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

2014 elif current is original and current is not NO_VALUE: 

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

2016 else: 

2017 # current convention on related objects is to not 

2018 # include information 

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

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

2021 # ignore the None in any case. 

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

2023 deleted = () 

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

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

2026 if id(current) in _NO_STATE_SYMBOLS: 

2027 current = None 

2028 else: 

2029 deleted = [original] 

2030 if current is NO_VALUE: 

2031 return cls((), (), deleted) 

2032 else: 

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

2034 

2035 @classmethod 

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

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

2038 if current is NO_VALUE: 

2039 return cls((), (), ()) 

2040 

2041 current = getattr(current, "_sa_adapter") 

2042 if original is NO_VALUE: 

2043 return cls(list(current), (), ()) 

2044 elif original is _NO_HISTORY: 

2045 return cls((), list(current), ()) 

2046 else: 

2047 

2048 current_states = [ 

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

2050 for c in current 

2051 ] 

2052 original_states = [ 

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

2054 for c in original 

2055 ] 

2056 

2057 current_set = dict(current_states) 

2058 original_set = dict(original_states) 

2059 

2060 return cls( 

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

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

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

2064 ) 

2065 

2066 

2067HISTORY_BLANK = History(None, None, None) 

2068 

2069 

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

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

2072 and attribute key. 

2073 

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

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

2076 current database transaction. 

2077 

2078 .. note:: 

2079 

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

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

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

2083 

2084 

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

2086 attributes package. 

2087 

2088 :param key: string attribute name. 

2089 

2090 :param passive: indicates loading behavior for the attribute 

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

2092 bitflag attribute, which defaults to the symbol 

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

2094 should be emitted. 

2095 

2096 .. seealso:: 

2097 

2098 :attr:`.AttributeState.history` 

2099 

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

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

2102 

2103 """ 

2104 

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

2106 

2107 

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

2109 return state.get_history(key, passive) 

2110 

2111 

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

2113 """TODO""" 

2114 manager = manager_of_class(cls) 

2115 state = instance_state(obj) 

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

2117 

2118 

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

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

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

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

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

2124 register_attribute_impl(class_, key, **kw) 

2125 return desc 

2126 

2127 

2128def register_attribute_impl( 

2129 class_, 

2130 key, 

2131 uselist=False, 

2132 callable_=None, 

2133 useobject=False, 

2134 impl_class=None, 

2135 backref=None, 

2136 **kw 

2137): 

2138 

2139 manager = manager_of_class(class_) 

2140 if uselist: 

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

2142 typecallable = manager.instrument_collection_class( 

2143 key, factory or list 

2144 ) 

2145 else: 

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

2147 

2148 dispatch = manager[key].dispatch 

2149 

2150 if impl_class: 

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

2152 elif uselist: 

2153 impl = CollectionAttributeImpl( 

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

2155 ) 

2156 elif useobject: 

2157 impl = ScalarObjectAttributeImpl( 

2158 class_, key, callable_, dispatch, **kw 

2159 ) 

2160 else: 

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

2162 

2163 manager[key].impl = impl 

2164 

2165 if backref: 

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

2167 

2168 manager.post_configure_attribute(key) 

2169 return manager[key] 

2170 

2171 

2172def register_descriptor( 

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

2174): 

2175 manager = manager_of_class(class_) 

2176 

2177 descriptor = InstrumentedAttribute( 

2178 class_, key, comparator=comparator, parententity=parententity 

2179 ) 

2180 

2181 descriptor.__doc__ = doc 

2182 

2183 manager.instrument_attribute(key, descriptor) 

2184 return descriptor 

2185 

2186 

2187def unregister_attribute(class_, key): 

2188 manager_of_class(class_).uninstrument_attribute(key) 

2189 

2190 

2191def init_collection(obj, key): 

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

2193 

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

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

2196 

2197 collection_adapter = init_collection(someobject, 'elements') 

2198 for elem in values: 

2199 collection_adapter.append_without_event(elem) 

2200 

2201 For an easier way to do the above, see 

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

2203 

2204 :param obj: a mapped object 

2205 

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

2207 

2208 """ 

2209 state = instance_state(obj) 

2210 dict_ = state.dict 

2211 return init_state_collection(state, dict_, key) 

2212 

2213 

2214def init_state_collection(state, dict_, key): 

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

2216 

2217 Discards any existing collection which may be there. 

2218 

2219 """ 

2220 attr = state.manager[key].impl 

2221 

2222 old = dict_.pop(key, None) # discard old collection 

2223 if old is not None: 

2224 old_collection = old._sa_adapter 

2225 attr._dispose_previous_collection(state, old, old_collection, False) 

2226 

2227 user_data = attr._default_value(state, dict_) 

2228 adapter = attr.get_collection(state, dict_, user_data) 

2229 adapter._reset_empty() 

2230 

2231 return adapter 

2232 

2233 

2234def set_committed_value(instance, key, value): 

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

2236 

2237 Cancels any previous history present. The value should be 

2238 a scalar value for scalar-holding attributes, or 

2239 an iterable for any collection-holding attribute. 

2240 

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

2242 fires off and loads additional data from the database. 

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

2244 which has loaded additional attributes or collections through 

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

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

2247 

2248 """ 

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

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

2251 

2252 

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

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

2255 

2256 This function may be used regardless of instrumentation 

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

2258 Custom attribute management schemes will need to make usage 

2259 of this method to establish attribute state as understood 

2260 by SQLAlchemy. 

2261 

2262 :param instance: the object that will be modified 

2263 

2264 :param key: string name of the attribute 

2265 

2266 :param value: value to assign 

2267 

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

2269 been propagated from a previous event listener. This argument 

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

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

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

2273 chain of events. 

2274 

2275 .. versionadded:: 1.2.3 

2276 

2277 """ 

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

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

2280 

2281 

2282def get_attribute(instance, key): 

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

2284 

2285 This function may be used regardless of instrumentation 

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

2287 Custom attribute management schemes will need to make usage 

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

2289 by SQLAlchemy. 

2290 

2291 """ 

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

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

2294 

2295 

2296def del_attribute(instance, key): 

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

2298 

2299 This function may be used regardless of instrumentation 

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

2301 Custom attribute management schemes will need to make usage 

2302 of this method to establish attribute state as understood 

2303 by SQLAlchemy. 

2304 

2305 """ 

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

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

2308 

2309 

2310def flag_modified(instance, key): 

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

2312 

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

2314 establishes an unconditional change event for the given attribute. 

2315 The attribute must have a value present, else an 

2316 :class:`.InvalidRequestError` is raised. 

2317 

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

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

2320 :func:`.attributes.flag_dirty` call. 

2321 

2322 .. seealso:: 

2323 

2324 :func:`.attributes.flag_dirty` 

2325 

2326 """ 

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

2328 impl = state.manager[key].impl 

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

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

2331 

2332 

2333def flag_dirty(instance): 

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

2335 

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

2337 the flush process for interception by events such as 

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

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

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

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

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

2343 emitted. 

2344 

2345 .. versionadded:: 1.2 

2346 

2347 .. seealso:: 

2348 

2349 :func:`.attributes.flag_modified` 

2350 

2351 """ 

2352 

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

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