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

836 statements  

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

1# orm/attributes.py 

2# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7 

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 

57 

58 

59class NoKey(str): 

60 pass 

61 

62 

63NO_KEY = NoKey("no name") 

64 

65 

66@inspection._self_inspects 

67class QueryableAttribute( 

68 interfaces._MappedAttribute, 

69 interfaces.InspectionAttr, 

70 interfaces.PropComparator, 

71 traversals.HasCopyInternals, 

72 roles.JoinTargetRole, 

73 roles.OnClauseRole, 

74 sql_base.Immutable, 

75 sql_base.MemoizedHasCacheKey, 

76): 

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

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

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

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

81 attribute. 

82 

83 

84 .. seealso:: 

85 

86 :class:`.InstrumentedAttribute` 

87 

88 :class:`.MapperProperty` 

89 

90 :attr:`_orm.Mapper.all_orm_descriptors` 

91 

92 :attr:`_orm.Mapper.attrs` 

93 """ 

94 

95 is_attribute = True 

96 

97 # PropComparator has a __visit_name__ to participate within 

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

99 __visit_name__ = "orm_instrumented_attribute" 

100 

101 def __init__( 

102 self, 

103 class_, 

104 key, 

105 parententity, 

106 impl=None, 

107 comparator=None, 

108 of_type=None, 

109 extra_criteria=(), 

110 ): 

111 self.class_ = class_ 

112 self.key = key 

113 self._parententity = parententity 

114 self.impl = impl 

115 self.comparator = comparator 

116 self._of_type = of_type 

117 self._extra_criteria = extra_criteria 

118 

119 manager = manager_of_class(class_) 

120 # manager is None in the case of AliasedClass 

121 if manager: 

122 # propagate existing event listeners from 

123 # immediate superclass 

124 for base in manager._bases: 

125 if key in base: 

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

127 if base[key].dispatch._active_history: 

128 self.dispatch._active_history = True 

129 

130 _cache_key_traversal = [ 

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

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

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

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

135 ] 

136 

137 def __reduce__(self): 

138 # this method is only used in terms of the 

139 # sqlalchemy.ext.serializer extension 

140 return ( 

141 _queryable_attribute_unreduce, 

142 ( 

143 self.key, 

144 self._parententity.mapper.class_, 

145 self._parententity, 

146 self._parententity.entity, 

147 ), 

148 ) 

149 

150 @util.memoized_property 

151 def _supports_population(self): 

152 return self.impl.supports_population 

153 

154 @property 

155 def _impl_uses_objects(self): 

156 return self.impl.uses_objects 

157 

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

159 return self.impl.get_history( 

160 instance_state(instance), instance_dict(instance), passive 

161 ) 

162 

163 @util.memoized_property 

164 def info(self): 

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

166 

167 The behavior here is as follows: 

168 

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

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

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

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

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

174 

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

176 any other kind of SQL expression other than a 

177 :class:`_schema.Column`, 

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

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

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

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

182 construct has defined one). 

183 

184 * If the attribute refers to any other kind of 

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

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

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

188 

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

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

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

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

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

194 ``MyClass.someattribute.property.info``. 

195 

196 .. seealso:: 

197 

198 :attr:`.SchemaItem.info` 

199 

200 :attr:`.MapperProperty.info` 

201 

202 """ 

203 return self.comparator.info 

204 

205 @util.memoized_property 

206 def parent(self): 

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

208 

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

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

211 of the parent entity which this attribute is associated 

212 with. 

213 

214 """ 

215 return inspection.inspect(self._parententity) 

216 

217 @util.memoized_property 

218 def expression(self): 

219 """The SQL expression object represented by this 

220 :class:`.QueryableAttribute`. 

221 

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

223 subclass representing a column expression. 

224 

225 """ 

226 if self.key is NO_KEY: 

227 annotations = {"entity_namespace": self._entity_namespace} 

228 else: 

229 annotations = { 

230 "proxy_key": self.key, 

231 "proxy_owner": self._parententity, 

232 "entity_namespace": self._entity_namespace, 

233 } 

234 

235 ce = self.comparator.__clause_element__() 

236 try: 

237 anno = ce._annotate 

238 except AttributeError as ae: 

239 util.raise_( 

240 exc.InvalidRequestError( 

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

242 "expected __clause_element__() to return " 

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

244 ), 

245 from_=ae, 

246 ) 

247 else: 

248 return anno(annotations) 

249 

250 @property 

251 def _entity_namespace(self): 

252 return self._parententity 

253 

254 @property 

255 def _annotations(self): 

256 return self.__clause_element__()._annotations 

257 

258 def __clause_element__(self): 

259 return self.expression 

260 

261 @property 

262 def _from_objects(self): 

263 return self.expression._from_objects 

264 

265 def _bulk_update_tuples(self, value): 

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

267 

268 return self.comparator._bulk_update_tuples(value) 

269 

270 def adapt_to_entity(self, adapt_to_entity): 

271 assert not self._of_type 

272 return self.__class__( 

273 adapt_to_entity.entity, 

274 self.key, 

275 impl=self.impl, 

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

277 parententity=adapt_to_entity, 

278 ) 

279 

280 def of_type(self, entity): 

281 return QueryableAttribute( 

282 self.class_, 

283 self.key, 

284 self._parententity, 

285 impl=self.impl, 

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

287 of_type=inspection.inspect(entity), 

288 extra_criteria=self._extra_criteria, 

289 ) 

290 

291 def and_(self, *other): 

292 return QueryableAttribute( 

293 self.class_, 

294 self.key, 

295 self._parententity, 

296 impl=self.impl, 

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

298 of_type=self._of_type, 

299 extra_criteria=self._extra_criteria + other, 

300 ) 

301 

302 def _clone(self, **kw): 

303 return QueryableAttribute( 

304 self.class_, 

305 self.key, 

306 self._parententity, 

307 impl=self.impl, 

308 comparator=self.comparator, 

309 of_type=self._of_type, 

310 extra_criteria=self._extra_criteria, 

311 ) 

312 

313 def label(self, name): 

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

315 

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

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

318 

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

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

321 

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

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

324 

325 def __getattr__(self, key): 

326 try: 

327 return getattr(self.comparator, key) 

328 except AttributeError as err: 

329 util.raise_( 

330 AttributeError( 

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

332 "has an attribute %r" 

333 % ( 

334 type(self).__name__, 

335 type(self.comparator).__name__, 

336 self, 

337 key, 

338 ) 

339 ), 

340 replace_context=err, 

341 ) 

342 

343 def __str__(self): 

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

345 

346 @util.memoized_property 

347 def property(self): 

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

349 :class:`.QueryableAttribute`. 

350 

351 

352 Return values here will commonly be instances of 

353 :class:`.ColumnProperty` or :class:`.RelationshipProperty`. 

354 

355 

356 """ 

357 return self.comparator.property 

358 

359 

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

361 # this method is only used in terms of the 

362 # sqlalchemy.ext.serializer extension 

363 if parententity.is_aliased_class: 

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

365 else: 

366 return getattr(entity, key) 

367 

368 

369if util.py3k: 

370 from typing import TypeVar, Generic 

371 

372 _T = TypeVar("_T") 

373 _Generic_T = Generic[_T] 

374else: 

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

376 

377 

378class Mapped(QueryableAttribute, _Generic_T): 

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

380 purposes. 

381 

382 This class represents the complete descriptor interface for any class 

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

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

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

386 behavioral contract for the attribute. 

387 

388 .. tip:: 

389 

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

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

392 Python descriptor classes that are provided as extensions, including 

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

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

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

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

397 when they are accessed on a class. 

398 

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

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

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

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

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

404 more indirect mapping styles such as 

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

406 typically applied explicitly to class level attributes that expect 

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

408 

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

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

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

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

413 

414 class MyMappedClass(Base): 

415 __table_ = Table( 

416 "some_table", Base.metadata, 

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

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

419 Column("created_at", DateTime) 

420 ) 

421 

422 id : Mapped[int] 

423 data: Mapped[str] 

424 created_at: Mapped[datetime] 

425 

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

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

428 Mypy plugin. 

429 

430 .. versionadded:: 1.4 

431 

432 .. seealso:: 

433 

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

435 

436 """ 

437 

438 def __get__(self, instance, owner): 

439 raise NotImplementedError() 

440 

441 def __set__(self, instance, value): 

442 raise NotImplementedError() 

443 

444 def __delete__(self, instance): 

445 raise NotImplementedError() 

446 

447 

448class InstrumentedAttribute(Mapped): 

449 """Class bound instrumented attribute which adds basic 

450 :term:`descriptor` methods. 

451 

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

453 

454 

455 """ 

456 

457 inherit_cache = True 

458 

459 def __set__(self, instance, value): 

460 self.impl.set( 

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

462 ) 

463 

464 def __delete__(self, instance): 

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

466 

467 def __get__(self, instance, owner): 

468 if instance is None: 

469 return self 

470 

471 dict_ = instance_dict(instance) 

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

473 return dict_[self.key] 

474 else: 

475 try: 

476 state = instance_state(instance) 

477 except AttributeError as err: 

478 util.raise_( 

479 orm_exc.UnmappedInstanceError(instance), 

480 replace_context=err, 

481 ) 

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

483 

484 

485HasEntityNamespace = util.namedtuple( 

486 "HasEntityNamespace", ["entity_namespace"] 

487) 

488HasEntityNamespace.is_mapper = HasEntityNamespace.is_aliased_class = False 

489 

490 

491def create_proxied_attribute(descriptor): 

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

493 

494 Returns a new QueryableAttribute type that delegates descriptor 

495 behavior and getattr() to the given descriptor. 

496 """ 

497 

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

499 # function is removed from ext/hybrid.py 

500 

501 class Proxy(QueryableAttribute): 

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

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

504 combination. 

505 

506 """ 

507 

508 _extra_criteria = () 

509 

510 def __init__( 

511 self, 

512 class_, 

513 key, 

514 descriptor, 

515 comparator, 

516 adapt_to_entity=None, 

517 doc=None, 

518 original_property=None, 

519 ): 

520 self.class_ = class_ 

521 self.key = key 

522 self.descriptor = descriptor 

523 self.original_property = original_property 

524 self._comparator = comparator 

525 self._adapt_to_entity = adapt_to_entity 

526 self.__doc__ = doc 

527 

528 _is_internal_proxy = True 

529 

530 _cache_key_traversal = [ 

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

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

533 ] 

534 

535 @property 

536 def _impl_uses_objects(self): 

537 return ( 

538 self.original_property is not None 

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

540 ) 

541 

542 @property 

543 def _parententity(self): 

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

545 

546 @property 

547 def _entity_namespace(self): 

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

549 return self._comparator._parententity 

550 else: 

551 # used by hybrid attributes which try to remain 

552 # agnostic of any ORM concepts like mappers 

553 return HasEntityNamespace(self.class_) 

554 

555 @property 

556 def property(self): 

557 return self.comparator.property 

558 

559 @util.memoized_property 

560 def comparator(self): 

561 if callable(self._comparator): 

562 self._comparator = self._comparator() 

563 if self._adapt_to_entity: 

564 self._comparator = self._comparator.adapt_to_entity( 

565 self._adapt_to_entity 

566 ) 

567 return self._comparator 

568 

569 def adapt_to_entity(self, adapt_to_entity): 

570 return self.__class__( 

571 adapt_to_entity.entity, 

572 self.key, 

573 self.descriptor, 

574 self._comparator, 

575 adapt_to_entity, 

576 ) 

577 

578 def _clone(self, **kw): 

579 return self.__class__( 

580 self.class_, 

581 self.key, 

582 self.descriptor, 

583 self._comparator, 

584 adapt_to_entity=self._adapt_to_entity, 

585 original_property=self.original_property, 

586 ) 

587 

588 def __get__(self, instance, owner): 

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

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

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

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

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

594 return self 

595 else: 

596 return retval 

597 

598 def __str__(self): 

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

600 

601 def __getattr__(self, attribute): 

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

603 comparator.""" 

604 try: 

605 return getattr(descriptor, attribute) 

606 except AttributeError as err: 

607 if attribute == "comparator": 

608 util.raise_( 

609 AttributeError("comparator"), replace_context=err 

610 ) 

611 try: 

612 # comparator itself might be unreachable 

613 comparator = self.comparator 

614 except AttributeError as err2: 

615 util.raise_( 

616 AttributeError( 

617 "Neither %r object nor unconfigured comparator " 

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

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

620 ), 

621 replace_context=err2, 

622 ) 

623 else: 

624 try: 

625 return getattr(comparator, attribute) 

626 except AttributeError as err3: 

627 util.raise_( 

628 AttributeError( 

629 "Neither %r object nor %r object " 

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

631 % ( 

632 type(descriptor).__name__, 

633 type(comparator).__name__, 

634 self, 

635 attribute, 

636 ) 

637 ), 

638 replace_context=err3, 

639 ) 

640 

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

642 

643 util.monkeypatch_proxied_specials( 

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

645 ) 

646 return Proxy 

647 

648 

649OP_REMOVE = util.symbol("REMOVE") 

650OP_APPEND = util.symbol("APPEND") 

651OP_REPLACE = util.symbol("REPLACE") 

652OP_BULK_REPLACE = util.symbol("BULK_REPLACE") 

653OP_MODIFIED = util.symbol("MODIFIED") 

654 

655 

656class AttributeEvent(object): 

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

658 events. 

659 

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

661 a means of controlling propagation across a chain of attribute 

662 operations. 

663 

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

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

666 :meth:`.AttributeEvents.set`, 

667 and :meth:`.AttributeEvents.remove`. 

668 

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

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

671 across two mutually-dependent attributes. 

672 

673 .. versionadded:: 0.9.0 

674 

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

676 initiator. 

677 

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

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

680 source operation. 

681 

682 """ 

683 

684 __slots__ = "impl", "op", "parent_token" 

685 

686 def __init__(self, attribute_impl, op): 

687 self.impl = attribute_impl 

688 self.op = op 

689 self.parent_token = self.impl.parent_token 

690 

691 def __eq__(self, other): 

692 return ( 

693 isinstance(other, AttributeEvent) 

694 and other.impl is self.impl 

695 and other.op == self.op 

696 ) 

697 

698 @property 

699 def key(self): 

700 return self.impl.key 

701 

702 def hasparent(self, state): 

703 return self.impl.hasparent(state) 

704 

705 

706Event = AttributeEvent 

707 

708 

709class AttributeImpl(object): 

710 """internal implementation for instrumented attributes.""" 

711 

712 def __init__( 

713 self, 

714 class_, 

715 key, 

716 callable_, 

717 dispatch, 

718 trackparent=False, 

719 compare_function=None, 

720 active_history=False, 

721 parent_token=None, 

722 load_on_unexpire=True, 

723 send_modified_events=True, 

724 accepts_scalar_loader=None, 

725 **kwargs 

726 ): 

727 r"""Construct an AttributeImpl. 

728 

729 :param \class_: associated class 

730 

731 :param key: string name of the attribute 

732 

733 :param \callable_: 

734 optional function which generates a callable based on a parent 

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

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

737 already. 

738 

739 :param trackparent: 

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

741 to it via this attribute. 

742 

743 :param compare_function: 

744 a function that compares two values which are normally 

745 assignable to this attribute. 

746 

747 :param active_history: 

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

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

750 

751 :param parent_token: 

752 Usually references the MapperProperty, used as a key for 

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

754 Allows multiple AttributeImpls to all match a single 

755 owner attribute. 

756 

757 :param load_on_unexpire: 

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

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

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

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

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

763 attribute. 

764 

765 :param send_modified_events: 

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

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

768 history entry. 

769 

770 """ 

771 self.class_ = class_ 

772 self.key = key 

773 self.callable_ = callable_ 

774 self.dispatch = dispatch 

775 self.trackparent = trackparent 

776 self.parent_token = parent_token or self 

777 self.send_modified_events = send_modified_events 

778 if compare_function is None: 

779 self.is_equal = operator.eq 

780 else: 

781 self.is_equal = compare_function 

782 

783 if accepts_scalar_loader is not None: 

784 self.accepts_scalar_loader = accepts_scalar_loader 

785 else: 

786 self.accepts_scalar_loader = self.default_accepts_scalar_loader 

787 

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

789 self._deferred_history = _deferred_history 

790 

791 if active_history: 

792 self.dispatch._active_history = True 

793 

794 self.load_on_unexpire = load_on_unexpire 

795 self._modified_token = Event(self, OP_MODIFIED) 

796 

797 __slots__ = ( 

798 "class_", 

799 "key", 

800 "callable_", 

801 "dispatch", 

802 "trackparent", 

803 "parent_token", 

804 "send_modified_events", 

805 "is_equal", 

806 "load_on_unexpire", 

807 "_modified_token", 

808 "accepts_scalar_loader", 

809 "_deferred_history", 

810 ) 

811 

812 def __str__(self): 

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

814 

815 def _get_active_history(self): 

816 """Backwards compat for impl.active_history""" 

817 

818 return self.dispatch._active_history 

819 

820 def _set_active_history(self, value): 

821 self.dispatch._active_history = value 

822 

823 active_history = property(_get_active_history, _set_active_history) 

824 

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

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

827 the given state. 

828 

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

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

831 

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

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

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

835 flag is set. 

836 

837 An instance attribute that is loaded by a callable function 

838 will also not have a `hasparent` flag. 

839 

840 """ 

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

842 assert self.trackparent, msg 

843 

844 return ( 

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

846 ) 

847 

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

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

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

851 attribute represented by this ``InstrumentedAttribute``. 

852 

853 """ 

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

855 assert self.trackparent, msg 

856 

857 id_ = id(self.parent_token) 

858 if value: 

859 state.parents[id_] = parent_state 

860 else: 

861 if id_ in state.parents: 

862 last_parent = state.parents[id_] 

863 

864 if ( 

865 last_parent is not False 

866 and last_parent.key != parent_state.key 

867 ): 

868 

869 if last_parent.obj() is None: 

870 raise orm_exc.StaleDataError( 

871 "Removing state %s from parent " 

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

873 "but the parent record " 

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

875 "is the most recent parent." 

876 % ( 

877 state_str(state), 

878 state_str(parent_state), 

879 self.key, 

880 ) 

881 ) 

882 

883 return 

884 

885 state.parents[id_] = False 

886 

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

888 raise NotImplementedError() 

889 

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

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

892 for all objects in this attribute's current state 

893 + history. 

894 

895 Only applies to object-based attributes. 

896 

897 This is an inlining of existing functionality 

898 which roughly corresponds to: 

899 

900 get_state_history( 

901 state, 

902 key, 

903 passive=PASSIVE_NO_INITIALIZE).sum() 

904 

905 """ 

906 raise NotImplementedError() 

907 

908 def _default_value(self, state, dict_): 

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

910 

911 assert self.key not in dict_, ( 

912 "_default_value should only be invoked for an " 

913 "uninitialized or expired attribute" 

914 ) 

915 

916 value = None 

917 for fn in self.dispatch.init_scalar: 

918 ret = fn(state, value, dict_) 

919 if ret is not ATTR_EMPTY: 

920 value = ret 

921 

922 return value 

923 

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

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

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

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

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

929 """ 

930 if self.key in dict_: 

931 return dict_[self.key] 

932 else: 

933 # if history present, don't load 

934 key = self.key 

935 if ( 

936 key not in state.committed_state 

937 or state.committed_state[key] is NO_VALUE 

938 ): 

939 if not passive & CALLABLES_OK: 

940 return PASSIVE_NO_RESULT 

941 

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

943 

944 if value is PASSIVE_NO_RESULT or value is NO_VALUE: 

945 return value 

946 elif value is ATTR_WAS_SET: 

947 try: 

948 return dict_[key] 

949 except KeyError as err: 

950 # TODO: no test coverage here. 

951 util.raise_( 

952 KeyError( 

953 "Deferred loader for attribute " 

954 "%r failed to populate " 

955 "correctly" % key 

956 ), 

957 replace_context=err, 

958 ) 

959 elif value is not ATTR_EMPTY: 

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

961 

962 if not passive & INIT_OK: 

963 return NO_VALUE 

964 else: 

965 return self._default_value(state, dict_) 

966 

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

968 if ( 

969 self.accepts_scalar_loader 

970 and self.load_on_unexpire 

971 and key in state.expired_attributes 

972 ): 

973 return state._load_expired(state, passive) 

974 elif key in state.callables: 

975 callable_ = state.callables[key] 

976 return callable_(state, passive) 

977 elif self.callable_: 

978 return self.callable_(state, passive) 

979 else: 

980 return ATTR_EMPTY 

981 

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

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

984 

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

986 self.set( 

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

988 ) 

989 

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

991 self.set( 

992 state, 

993 dict_, 

994 None, 

995 initiator, 

996 passive=passive, 

997 check_old=value, 

998 pop=True, 

999 ) 

1000 

1001 def set( 

1002 self, 

1003 state, 

1004 dict_, 

1005 value, 

1006 initiator, 

1007 passive=PASSIVE_OFF, 

1008 check_old=None, 

1009 pop=False, 

1010 ): 

1011 raise NotImplementedError() 

1012 

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

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

1015 

1016 if self.key in state.committed_state: 

1017 value = state.committed_state[self.key] 

1018 if value is NO_VALUE: 

1019 return None 

1020 else: 

1021 return value 

1022 else: 

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

1024 

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

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

1027 

1028 dict_[self.key] = value 

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

1030 return value 

1031 

1032 

1033class ScalarAttributeImpl(AttributeImpl): 

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

1035 

1036 default_accepts_scalar_loader = True 

1037 uses_objects = False 

1038 supports_population = True 

1039 collection = False 

1040 dynamic = False 

1041 

1042 __slots__ = "_replace_token", "_append_token", "_remove_token" 

1043 

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

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

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

1047 self._remove_token = Event(self, OP_REMOVE) 

1048 

1049 def delete(self, state, dict_): 

1050 if self.dispatch._active_history: 

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

1052 else: 

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

1054 

1055 if self.dispatch.remove: 

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

1057 state._modified_event(dict_, self, old) 

1058 

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

1060 if ( 

1061 existing is NO_VALUE 

1062 and old is NO_VALUE 

1063 and not state.expired 

1064 and self.key not in state.expired_attributes 

1065 ): 

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

1067 

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

1069 if self.key in dict_: 

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

1071 elif self.key in state.committed_state: 

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

1073 else: 

1074 if passive & INIT_OK: 

1075 passive ^= INIT_OK 

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

1077 if current is PASSIVE_NO_RESULT: 

1078 return HISTORY_BLANK 

1079 else: 

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

1081 

1082 def set( 

1083 self, 

1084 state, 

1085 dict_, 

1086 value, 

1087 initiator, 

1088 passive=PASSIVE_OFF, 

1089 check_old=None, 

1090 pop=False, 

1091 ): 

1092 if self.dispatch._active_history: 

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

1094 else: 

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

1096 

1097 if self.dispatch.set: 

1098 value = self.fire_replace_event( 

1099 state, dict_, value, old, initiator 

1100 ) 

1101 state._modified_event(dict_, self, old) 

1102 dict_[self.key] = value 

1103 

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

1105 for fn in self.dispatch.set: 

1106 value = fn( 

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

1108 ) 

1109 return value 

1110 

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

1112 for fn in self.dispatch.remove: 

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

1114 

1115 @property 

1116 def type(self): 

1117 self.property.columns[0].type 

1118 

1119 

1120class ScalarObjectAttributeImpl(ScalarAttributeImpl): 

1121 """represents a scalar-holding InstrumentedAttribute, 

1122 where the target object is also instrumented. 

1123 

1124 Adds events to delete/set operations. 

1125 

1126 """ 

1127 

1128 default_accepts_scalar_loader = False 

1129 uses_objects = True 

1130 supports_population = True 

1131 collection = False 

1132 

1133 __slots__ = () 

1134 

1135 def delete(self, state, dict_): 

1136 if self.dispatch._active_history: 

1137 old = self.get( 

1138 state, 

1139 dict_, 

1140 passive=PASSIVE_ONLY_PERSISTENT 

1141 | NO_AUTOFLUSH 

1142 | LOAD_AGAINST_COMMITTED, 

1143 ) 

1144 else: 

1145 old = self.get( 

1146 state, 

1147 dict_, 

1148 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1149 | LOAD_AGAINST_COMMITTED 

1150 | NO_RAISE, 

1151 ) 

1152 

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

1154 

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

1156 

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

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

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

1160 if ( 

1161 existing is NO_VALUE 

1162 and old is not PASSIVE_NO_RESULT 

1163 and state.key is None 

1164 ): 

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

1166 

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

1168 if self.key in dict_: 

1169 current = dict_[self.key] 

1170 else: 

1171 if passive & INIT_OK: 

1172 passive ^= INIT_OK 

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

1174 if current is PASSIVE_NO_RESULT: 

1175 return HISTORY_BLANK 

1176 

1177 if not self._deferred_history: 

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

1179 else: 

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

1181 if original is PASSIVE_NO_RESULT: 

1182 

1183 loader_passive = passive | ( 

1184 PASSIVE_ONLY_PERSISTENT 

1185 | NO_AUTOFLUSH 

1186 | LOAD_AGAINST_COMMITTED 

1187 | NO_RAISE 

1188 | DEFERRED_HISTORY_LOAD 

1189 ) 

1190 original = self._fire_loader_callables( 

1191 state, self.key, loader_passive 

1192 ) 

1193 return History.from_object_attribute( 

1194 self, state, current, original=original 

1195 ) 

1196 

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

1198 if self.key in dict_: 

1199 current = dict_[self.key] 

1200 elif passive & CALLABLES_OK: 

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

1202 else: 

1203 return [] 

1204 

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

1206 if ( 

1207 current is not None 

1208 and current is not PASSIVE_NO_RESULT 

1209 and current is not NO_VALUE 

1210 ): 

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

1212 else: 

1213 ret = [(None, None)] 

1214 

1215 if self.key in state.committed_state: 

1216 original = state.committed_state[self.key] 

1217 if ( 

1218 original is not None 

1219 and original is not PASSIVE_NO_RESULT 

1220 and original is not NO_VALUE 

1221 and original is not current 

1222 ): 

1223 

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

1225 return ret 

1226 

1227 def set( 

1228 self, 

1229 state, 

1230 dict_, 

1231 value, 

1232 initiator, 

1233 passive=PASSIVE_OFF, 

1234 check_old=None, 

1235 pop=False, 

1236 ): 

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

1238 

1239 if self.dispatch._active_history: 

1240 old = self.get( 

1241 state, 

1242 dict_, 

1243 passive=PASSIVE_ONLY_PERSISTENT 

1244 | NO_AUTOFLUSH 

1245 | LOAD_AGAINST_COMMITTED, 

1246 ) 

1247 else: 

1248 old = self.get( 

1249 state, 

1250 dict_, 

1251 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1252 | LOAD_AGAINST_COMMITTED 

1253 | NO_RAISE, 

1254 ) 

1255 

1256 if ( 

1257 check_old is not None 

1258 and old is not PASSIVE_NO_RESULT 

1259 and check_old is not old 

1260 ): 

1261 if pop: 

1262 return 

1263 else: 

1264 raise ValueError( 

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

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

1267 ) 

1268 

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

1270 dict_[self.key] = value 

1271 

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

1273 if self.trackparent and value not in ( 

1274 None, 

1275 PASSIVE_NO_RESULT, 

1276 NO_VALUE, 

1277 ): 

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

1279 

1280 for fn in self.dispatch.remove: 

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

1282 

1283 state._modified_event(dict_, self, value) 

1284 

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

1286 if self.trackparent: 

1287 if previous is not value and previous not in ( 

1288 None, 

1289 PASSIVE_NO_RESULT, 

1290 NO_VALUE, 

1291 ): 

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

1293 

1294 for fn in self.dispatch.set: 

1295 value = fn( 

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

1297 ) 

1298 

1299 state._modified_event(dict_, self, previous) 

1300 

1301 if self.trackparent: 

1302 if value is not None: 

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

1304 

1305 return value 

1306 

1307 

1308class CollectionAttributeImpl(AttributeImpl): 

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

1310 

1311 Only handles collections of instrumented objects. 

1312 

1313 InstrumentedCollectionAttribute holds an arbitrary, user-specified 

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

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

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

1317 

1318 """ 

1319 

1320 default_accepts_scalar_loader = False 

1321 uses_objects = True 

1322 supports_population = True 

1323 collection = True 

1324 dynamic = False 

1325 

1326 __slots__ = ( 

1327 "copy", 

1328 "collection_factory", 

1329 "_append_token", 

1330 "_remove_token", 

1331 "_bulk_replace_token", 

1332 "_duck_typed_as", 

1333 ) 

1334 

1335 def __init__( 

1336 self, 

1337 class_, 

1338 key, 

1339 callable_, 

1340 dispatch, 

1341 typecallable=None, 

1342 trackparent=False, 

1343 copy_function=None, 

1344 compare_function=None, 

1345 **kwargs 

1346 ): 

1347 super(CollectionAttributeImpl, self).__init__( 

1348 class_, 

1349 key, 

1350 callable_, 

1351 dispatch, 

1352 trackparent=trackparent, 

1353 compare_function=compare_function, 

1354 **kwargs 

1355 ) 

1356 

1357 if copy_function is None: 

1358 copy_function = self.__copy 

1359 self.copy = copy_function 

1360 self.collection_factory = typecallable 

1361 self._append_token = Event(self, OP_APPEND) 

1362 self._remove_token = Event(self, OP_REMOVE) 

1363 self._bulk_replace_token = Event(self, OP_BULK_REPLACE) 

1364 self._duck_typed_as = util.duck_type_collection( 

1365 self.collection_factory() 

1366 ) 

1367 

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

1369 

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

1371 def link(target, collection, collection_adapter): 

1372 collection._sa_linker(collection_adapter) 

1373 

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

1375 def unlink(target, collection, collection_adapter): 

1376 collection._sa_linker(None) 

1377 

1378 def __copy(self, item): 

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

1380 

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

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

1383 if current is PASSIVE_NO_RESULT: 

1384 return HISTORY_BLANK 

1385 else: 

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

1387 

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

1389 # NOTE: passive is ignored here at the moment 

1390 

1391 if self.key not in dict_: 

1392 return [] 

1393 

1394 current = dict_[self.key] 

1395 current = getattr(current, "_sa_adapter") 

1396 

1397 if self.key in state.committed_state: 

1398 original = state.committed_state[self.key] 

1399 if original is not NO_VALUE: 

1400 current_states = [ 

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

1402 for c in current 

1403 ] 

1404 original_states = [ 

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

1406 for c in original 

1407 ] 

1408 

1409 current_set = dict(current_states) 

1410 original_set = dict(original_states) 

1411 

1412 return ( 

1413 [ 

1414 (s, o) 

1415 for s, o in current_states 

1416 if s not in original_set 

1417 ] 

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

1419 + [ 

1420 (s, o) 

1421 for s, o in original_states 

1422 if s not in current_set 

1423 ] 

1424 ) 

1425 

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

1427 

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

1429 for fn in self.dispatch.append: 

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

1431 

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

1433 

1434 if self.trackparent and value is not None: 

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

1436 

1437 return value 

1438 

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

1440 for fn in self.dispatch.append_wo_mutation: 

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

1442 

1443 return value 

1444 

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

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

1447 

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

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

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

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

1452 

1453 """ 

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

1455 

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

1457 if self.trackparent and value is not None: 

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

1459 

1460 for fn in self.dispatch.remove: 

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

1462 

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

1464 

1465 def delete(self, state, dict_): 

1466 if self.key not in dict_: 

1467 return 

1468 

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

1470 

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

1472 collection.clear_with_event() 

1473 

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

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

1476 del dict_[self.key] 

1477 

1478 def _default_value(self, state, dict_): 

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

1480 

1481 assert self.key not in dict_, ( 

1482 "_default_value should only be invoked for an " 

1483 "uninitialized or expired attribute" 

1484 ) 

1485 

1486 if self.key in state._empty_collections: 

1487 return state._empty_collections[self.key] 

1488 

1489 adapter, user_data = self._initialize_collection(state) 

1490 adapter._set_empty(user_data) 

1491 return user_data 

1492 

1493 def _initialize_collection(self, state): 

1494 

1495 adapter, collection = state.manager.initialize_collection( 

1496 self.key, state, self.collection_factory 

1497 ) 

1498 

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

1500 

1501 return adapter, collection 

1502 

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

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

1505 if collection is PASSIVE_NO_RESULT: 

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

1507 assert ( 

1508 self.key not in dict_ 

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

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

1511 else: 

1512 collection.append_with_event(value, initiator) 

1513 

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

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

1516 if collection is PASSIVE_NO_RESULT: 

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

1518 assert ( 

1519 self.key not in dict_ 

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

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

1522 else: 

1523 collection.remove_with_event(value, initiator) 

1524 

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

1526 try: 

1527 # TODO: better solution here would be to add 

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

1529 # "remover". 

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

1531 except (ValueError, KeyError, IndexError): 

1532 pass 

1533 

1534 def set( 

1535 self, 

1536 state, 

1537 dict_, 

1538 value, 

1539 initiator=None, 

1540 passive=PASSIVE_OFF, 

1541 check_old=None, 

1542 pop=False, 

1543 _adapt=True, 

1544 ): 

1545 iterable = orig_iterable = value 

1546 

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

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

1549 new_collection, user_data = self._initialize_collection(state) 

1550 if _adapt: 

1551 if new_collection._converter is not None: 

1552 iterable = new_collection._converter(iterable) 

1553 else: 

1554 setting_type = util.duck_type_collection(iterable) 

1555 receiving_type = self._duck_typed_as 

1556 

1557 if setting_type is not receiving_type: 

1558 given = ( 

1559 iterable is None 

1560 and "None" 

1561 or iterable.__class__.__name__ 

1562 ) 

1563 wanted = self._duck_typed_as.__name__ 

1564 raise TypeError( 

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

1566 % (given, wanted) 

1567 ) 

1568 

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

1570 # adapter. 

1571 if hasattr(iterable, "_sa_iterator"): 

1572 iterable = iterable._sa_iterator() 

1573 elif setting_type is dict: 

1574 if util.py3k: 

1575 iterable = iterable.values() 

1576 else: 

1577 iterable = getattr( 

1578 iterable, "itervalues", iterable.values 

1579 )() 

1580 else: 

1581 iterable = iter(iterable) 

1582 new_values = list(iterable) 

1583 

1584 evt = self._bulk_replace_token 

1585 

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

1587 

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

1589 # existing object (ticket #8862) 

1590 old = self.get( 

1591 state, 

1592 dict_, 

1593 passive=PASSIVE_ONLY_PERSISTENT ^ (passive & NO_RAISE), 

1594 ) 

1595 if old is PASSIVE_NO_RESULT: 

1596 old = self._default_value(state, dict_) 

1597 elif old is orig_iterable: 

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

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

1600 return 

1601 

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

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

1604 

1605 old_collection = old._sa_adapter 

1606 

1607 dict_[self.key] = user_data 

1608 

1609 collections.bulk_replace( 

1610 new_values, old_collection, new_collection, initiator=evt 

1611 ) 

1612 

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

1614 

1615 def _dispose_previous_collection( 

1616 self, state, collection, adapter, fire_event 

1617 ): 

1618 del collection._sa_adapter 

1619 

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

1621 # collections. 

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

1623 if fire_event: 

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

1625 

1626 def _invalidate_collection(self, collection): 

1627 adapter = getattr(collection, "_sa_adapter") 

1628 adapter.invalidated = True 

1629 

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

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

1632 

1633 collection, user_data = self._initialize_collection(state) 

1634 

1635 if value: 

1636 collection.append_multiple_without_event(value) 

1637 

1638 state.dict[self.key] = user_data 

1639 

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

1641 

1642 if self.key in state._pending_mutations: 

1643 # pending items exist. issue a modified event, 

1644 # add/remove new items. 

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

1646 

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

1648 added = pending.added_items 

1649 removed = pending.deleted_items 

1650 for item in added: 

1651 collection.append_without_event(item) 

1652 for item in removed: 

1653 collection.remove_without_event(item) 

1654 

1655 return user_data 

1656 

1657 def get_collection( 

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

1659 ): 

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

1661 

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

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

1664 collection value. 

1665 

1666 """ 

1667 if user_data is None: 

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

1669 if user_data is PASSIVE_NO_RESULT: 

1670 return user_data 

1671 

1672 return user_data._sa_adapter 

1673 

1674 

1675def backref_listeners(attribute, key, uselist): 

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

1677 

1678 # use easily recognizable names for stack traces. 

1679 

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

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

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

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

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

1685 

1686 parent_token = attribute.impl.parent_token 

1687 parent_impl = attribute.impl 

1688 

1689 def _acceptable_key_err(child_state, initiator, child_impl): 

1690 raise ValueError( 

1691 "Bidirectional attribute conflict detected: " 

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

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

1694 'via the backref "%s".' 

1695 % ( 

1696 state_str(child_state), 

1697 initiator.parent_token, 

1698 child_impl.parent_token, 

1699 attribute.impl.parent_token, 

1700 ) 

1701 ) 

1702 

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

1704 if oldchild is child: 

1705 return child 

1706 if ( 

1707 oldchild is not None 

1708 and oldchild is not PASSIVE_NO_RESULT 

1709 and oldchild is not NO_VALUE 

1710 ): 

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

1712 # present when updating via a backref. 

1713 old_state, old_dict = ( 

1714 instance_state(oldchild), 

1715 instance_dict(oldchild), 

1716 ) 

1717 impl = old_state.manager[key].impl 

1718 

1719 # tokens to test for a recursive loop. 

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

1721 check_recursive_token = impl._replace_token 

1722 else: 

1723 check_recursive_token = impl._remove_token 

1724 

1725 if initiator is not check_recursive_token: 

1726 impl.pop( 

1727 old_state, 

1728 old_dict, 

1729 state.obj(), 

1730 parent_impl._append_token, 

1731 passive=PASSIVE_NO_FETCH, 

1732 ) 

1733 

1734 if child is not None: 

1735 child_state, child_dict = ( 

1736 instance_state(child), 

1737 instance_dict(child), 

1738 ) 

1739 child_impl = child_state.manager[key].impl 

1740 

1741 if ( 

1742 initiator.parent_token is not parent_token 

1743 and initiator.parent_token is not child_impl.parent_token 

1744 ): 

1745 _acceptable_key_err(state, initiator, child_impl) 

1746 

1747 # tokens to test for a recursive loop. 

1748 check_append_token = child_impl._append_token 

1749 check_bulk_replace_token = ( 

1750 child_impl._bulk_replace_token 

1751 if child_impl.collection 

1752 else None 

1753 ) 

1754 

1755 if ( 

1756 initiator is not check_append_token 

1757 and initiator is not check_bulk_replace_token 

1758 ): 

1759 child_impl.append( 

1760 child_state, 

1761 child_dict, 

1762 state.obj(), 

1763 initiator, 

1764 passive=PASSIVE_NO_FETCH, 

1765 ) 

1766 return child 

1767 

1768 def emit_backref_from_collection_append_event(state, child, initiator): 

1769 if child is None: 

1770 return 

1771 

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

1773 child_impl = child_state.manager[key].impl 

1774 

1775 if ( 

1776 initiator.parent_token is not parent_token 

1777 and initiator.parent_token is not child_impl.parent_token 

1778 ): 

1779 _acceptable_key_err(state, initiator, child_impl) 

1780 

1781 # tokens to test for a recursive loop. 

1782 check_append_token = child_impl._append_token 

1783 check_bulk_replace_token = ( 

1784 child_impl._bulk_replace_token if child_impl.collection else None 

1785 ) 

1786 

1787 if ( 

1788 initiator is not check_append_token 

1789 and initiator is not check_bulk_replace_token 

1790 ): 

1791 child_impl.append( 

1792 child_state, 

1793 child_dict, 

1794 state.obj(), 

1795 initiator, 

1796 passive=PASSIVE_NO_FETCH, 

1797 ) 

1798 return child 

1799 

1800 def emit_backref_from_collection_remove_event(state, child, initiator): 

1801 if ( 

1802 child is not None 

1803 and child is not PASSIVE_NO_RESULT 

1804 and child is not NO_VALUE 

1805 ): 

1806 child_state, child_dict = ( 

1807 instance_state(child), 

1808 instance_dict(child), 

1809 ) 

1810 child_impl = child_state.manager[key].impl 

1811 

1812 # tokens to test for a recursive loop. 

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

1814 check_remove_token = child_impl._remove_token 

1815 check_replace_token = child_impl._replace_token 

1816 check_for_dupes_on_remove = uselist and not parent_impl.dynamic 

1817 else: 

1818 check_remove_token = child_impl._remove_token 

1819 check_replace_token = ( 

1820 child_impl._bulk_replace_token 

1821 if child_impl.collection 

1822 else None 

1823 ) 

1824 check_for_dupes_on_remove = False 

1825 

1826 if ( 

1827 initiator is not check_remove_token 

1828 and initiator is not check_replace_token 

1829 ): 

1830 

1831 if not check_for_dupes_on_remove or not util.has_dupes( 

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

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

1834 state.dict[parent_impl.key], 

1835 child, 

1836 ): 

1837 child_impl.pop( 

1838 child_state, 

1839 child_dict, 

1840 state.obj(), 

1841 initiator, 

1842 passive=PASSIVE_NO_FETCH, 

1843 ) 

1844 

1845 if uselist: 

1846 event.listen( 

1847 attribute, 

1848 "append", 

1849 emit_backref_from_collection_append_event, 

1850 retval=True, 

1851 raw=True, 

1852 ) 

1853 else: 

1854 event.listen( 

1855 attribute, 

1856 "set", 

1857 emit_backref_from_scalar_set_event, 

1858 retval=True, 

1859 raw=True, 

1860 ) 

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

1862 event.listen( 

1863 attribute, 

1864 "remove", 

1865 emit_backref_from_collection_remove_event, 

1866 retval=True, 

1867 raw=True, 

1868 ) 

1869 

1870 

1871_NO_HISTORY = util.symbol("NO_HISTORY") 

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

1873 

1874 

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

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

1877 representing the changes which have occurred on an instrumented 

1878 attribute. 

1879 

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

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

1882 

1883 from sqlalchemy import inspect 

1884 

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

1886 

1887 Each tuple member is an iterable sequence: 

1888 

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

1890 tuple element). 

1891 

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

1893 attribute (the second tuple element). 

1894 

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

1896 attribute (the third tuple element). 

1897 

1898 """ 

1899 

1900 def __bool__(self): 

1901 return self != HISTORY_BLANK 

1902 

1903 __nonzero__ = __bool__ 

1904 

1905 def empty(self): 

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

1907 and no existing, unchanged state. 

1908 

1909 """ 

1910 

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

1912 

1913 def sum(self): 

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

1915 

1916 return ( 

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

1918 ) 

1919 

1920 def non_deleted(self): 

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

1922 

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

1924 

1925 def non_added(self): 

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

1927 

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

1929 

1930 def has_changes(self): 

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

1932 

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

1934 

1935 def as_state(self): 

1936 return History( 

1937 [ 

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

1939 for c in self.added 

1940 ], 

1941 [ 

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

1943 for c in self.unchanged 

1944 ], 

1945 [ 

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

1947 for c in self.deleted 

1948 ], 

1949 ) 

1950 

1951 @classmethod 

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

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

1954 

1955 if original is _NO_HISTORY: 

1956 if current is NO_VALUE: 

1957 return cls((), (), ()) 

1958 else: 

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

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

1961 elif ( 

1962 current is not NO_VALUE 

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

1964 ): 

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

1966 else: 

1967 # current convention on native scalars is to not 

1968 # include information 

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

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

1971 # key situations 

1972 if id(original) in _NO_STATE_SYMBOLS: 

1973 deleted = () 

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

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

1976 if id(current) in _NO_STATE_SYMBOLS: 

1977 current = None 

1978 else: 

1979 deleted = [original] 

1980 if current is NO_VALUE: 

1981 return cls((), (), deleted) 

1982 else: 

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

1984 

1985 @classmethod 

1986 def from_object_attribute( 

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

1988 ): 

1989 if original is _NO_HISTORY: 

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

1991 

1992 if original is _NO_HISTORY: 

1993 if current is NO_VALUE: 

1994 return cls((), (), ()) 

1995 else: 

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

1997 elif current is original and current is not NO_VALUE: 

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

1999 else: 

2000 # current convention on related objects is to not 

2001 # include information 

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

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

2004 # ignore the None in any case. 

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

2006 deleted = () 

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

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

2009 if id(current) in _NO_STATE_SYMBOLS: 

2010 current = None 

2011 else: 

2012 deleted = [original] 

2013 if current is NO_VALUE: 

2014 return cls((), (), deleted) 

2015 else: 

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

2017 

2018 @classmethod 

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

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

2021 if current is NO_VALUE: 

2022 return cls((), (), ()) 

2023 

2024 current = getattr(current, "_sa_adapter") 

2025 if original is NO_VALUE: 

2026 return cls(list(current), (), ()) 

2027 elif original is _NO_HISTORY: 

2028 return cls((), list(current), ()) 

2029 else: 

2030 

2031 current_states = [ 

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

2033 for c in current 

2034 ] 

2035 original_states = [ 

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

2037 for c in original 

2038 ] 

2039 

2040 current_set = dict(current_states) 

2041 original_set = dict(original_states) 

2042 

2043 return cls( 

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

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

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

2047 ) 

2048 

2049 

2050HISTORY_BLANK = History(None, None, None) 

2051 

2052 

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

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

2055 and attribute key. 

2056 

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

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

2059 current database transaction. 

2060 

2061 .. note:: 

2062 

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

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

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

2066 

2067 

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

2069 attributes package. 

2070 

2071 :param key: string attribute name. 

2072 

2073 :param passive: indicates loading behavior for the attribute 

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

2075 bitflag attribute, which defaults to the symbol 

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

2077 should be emitted. 

2078 

2079 .. seealso:: 

2080 

2081 :attr:`.AttributeState.history` 

2082 

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

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

2085 

2086 """ 

2087 

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

2089 

2090 

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

2092 return state.get_history(key, passive) 

2093 

2094 

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

2096 """TODO""" 

2097 manager = manager_of_class(cls) 

2098 state = instance_state(obj) 

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

2100 

2101 

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

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

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

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

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

2107 register_attribute_impl(class_, key, **kw) 

2108 return desc 

2109 

2110 

2111def register_attribute_impl( 

2112 class_, 

2113 key, 

2114 uselist=False, 

2115 callable_=None, 

2116 useobject=False, 

2117 impl_class=None, 

2118 backref=None, 

2119 **kw 

2120): 

2121 

2122 manager = manager_of_class(class_) 

2123 if uselist: 

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

2125 typecallable = manager.instrument_collection_class( 

2126 key, factory or list 

2127 ) 

2128 else: 

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

2130 

2131 dispatch = manager[key].dispatch 

2132 

2133 if impl_class: 

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

2135 elif uselist: 

2136 impl = CollectionAttributeImpl( 

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

2138 ) 

2139 elif useobject: 

2140 impl = ScalarObjectAttributeImpl( 

2141 class_, key, callable_, dispatch, **kw 

2142 ) 

2143 else: 

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

2145 

2146 manager[key].impl = impl 

2147 

2148 if backref: 

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

2150 

2151 manager.post_configure_attribute(key) 

2152 return manager[key] 

2153 

2154 

2155def register_descriptor( 

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

2157): 

2158 manager = manager_of_class(class_) 

2159 

2160 descriptor = InstrumentedAttribute( 

2161 class_, key, comparator=comparator, parententity=parententity 

2162 ) 

2163 

2164 descriptor.__doc__ = doc 

2165 

2166 manager.instrument_attribute(key, descriptor) 

2167 return descriptor 

2168 

2169 

2170def unregister_attribute(class_, key): 

2171 manager_of_class(class_).uninstrument_attribute(key) 

2172 

2173 

2174def init_collection(obj, key): 

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

2176 

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

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

2179 

2180 collection_adapter = init_collection(someobject, 'elements') 

2181 for elem in values: 

2182 collection_adapter.append_without_event(elem) 

2183 

2184 For an easier way to do the above, see 

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

2186 

2187 :param obj: a mapped object 

2188 

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

2190 

2191 """ 

2192 state = instance_state(obj) 

2193 dict_ = state.dict 

2194 return init_state_collection(state, dict_, key) 

2195 

2196 

2197def init_state_collection(state, dict_, key): 

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

2199 

2200 Discards any existing collection which may be there. 

2201 

2202 """ 

2203 attr = state.manager[key].impl 

2204 

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

2206 if old is not None: 

2207 old_collection = old._sa_adapter 

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

2209 

2210 user_data = attr._default_value(state, dict_) 

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

2212 adapter._reset_empty() 

2213 

2214 return adapter 

2215 

2216 

2217def set_committed_value(instance, key, value): 

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

2219 

2220 Cancels any previous history present. The value should be 

2221 a scalar value for scalar-holding attributes, or 

2222 an iterable for any collection-holding attribute. 

2223 

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

2225 fires off and loads additional data from the database. 

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

2227 which has loaded additional attributes or collections through 

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

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

2230 

2231 """ 

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

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

2234 

2235 

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

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

2238 

2239 This function may be used regardless of instrumentation 

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

2241 Custom attribute management schemes will need to make usage 

2242 of this method to establish attribute state as understood 

2243 by SQLAlchemy. 

2244 

2245 :param instance: the object that will be modified 

2246 

2247 :param key: string name of the attribute 

2248 

2249 :param value: value to assign 

2250 

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

2252 been propagated from a previous event listener. This argument 

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

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

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

2256 chain of events. 

2257 

2258 .. versionadded:: 1.2.3 

2259 

2260 """ 

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

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

2263 

2264 

2265def get_attribute(instance, key): 

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

2267 

2268 This function may be used regardless of instrumentation 

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

2270 Custom attribute management schemes will need to make usage 

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

2272 by SQLAlchemy. 

2273 

2274 """ 

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

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

2277 

2278 

2279def del_attribute(instance, key): 

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

2281 

2282 This function may be used regardless of instrumentation 

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

2284 Custom attribute management schemes will need to make usage 

2285 of this method to establish attribute state as understood 

2286 by SQLAlchemy. 

2287 

2288 """ 

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

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

2291 

2292 

2293def flag_modified(instance, key): 

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

2295 

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

2297 establishes an unconditional change event for the given attribute. 

2298 The attribute must have a value present, else an 

2299 :class:`.InvalidRequestError` is raised. 

2300 

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

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

2303 :func:`.attributes.flag_dirty` call. 

2304 

2305 .. seealso:: 

2306 

2307 :func:`.attributes.flag_dirty` 

2308 

2309 """ 

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

2311 impl = state.manager[key].impl 

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

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

2314 

2315 

2316def flag_dirty(instance): 

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

2318 

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

2320 the flush process for interception by events such as 

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

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

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

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

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

2326 emitted. 

2327 

2328 .. versionadded:: 1.2 

2329 

2330 .. seealso:: 

2331 

2332 :func:`.attributes.flag_modified` 

2333 

2334 """ 

2335 

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

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