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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

983 statements  

1# orm/attributes.py 

2# Copyright (C) 2005-2024 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# mypy: allow-untyped-defs, allow-untyped-calls 

8 

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

10with instances. 

11 

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

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

14 

15 

16""" 

17 

18from __future__ import annotations 

19 

20import dataclasses 

21import operator 

22from typing import Any 

23from typing import Callable 

24from typing import cast 

25from typing import ClassVar 

26from typing import Dict 

27from typing import Iterable 

28from typing import List 

29from typing import NamedTuple 

30from typing import Optional 

31from typing import overload 

32from typing import Sequence 

33from typing import Tuple 

34from typing import Type 

35from typing import TYPE_CHECKING 

36from typing import TypeVar 

37from typing import Union 

38 

39from . import collections 

40from . import exc as orm_exc 

41from . import interfaces 

42from ._typing import insp_is_aliased_class 

43from .base import _DeclarativeMapped 

44from .base import ATTR_EMPTY 

45from .base import ATTR_WAS_SET 

46from .base import CALLABLES_OK 

47from .base import DEFERRED_HISTORY_LOAD 

48from .base import INCLUDE_PENDING_MUTATIONS # noqa 

49from .base import INIT_OK 

50from .base import instance_dict as instance_dict 

51from .base import instance_state as instance_state 

52from .base import instance_str 

53from .base import LOAD_AGAINST_COMMITTED 

54from .base import LoaderCallableStatus 

55from .base import manager_of_class as manager_of_class 

56from .base import Mapped as Mapped # noqa 

57from .base import NEVER_SET # noqa 

58from .base import NO_AUTOFLUSH 

59from .base import NO_CHANGE # noqa 

60from .base import NO_KEY 

61from .base import NO_RAISE 

62from .base import NO_VALUE 

63from .base import NON_PERSISTENT_OK # noqa 

64from .base import opt_manager_of_class as opt_manager_of_class 

65from .base import PASSIVE_CLASS_MISMATCH # noqa 

66from .base import PASSIVE_NO_FETCH 

67from .base import PASSIVE_NO_FETCH_RELATED # noqa 

68from .base import PASSIVE_NO_INITIALIZE 

69from .base import PASSIVE_NO_RESULT 

70from .base import PASSIVE_OFF 

71from .base import PASSIVE_ONLY_PERSISTENT 

72from .base import PASSIVE_RETURN_NO_VALUE 

73from .base import PassiveFlag 

74from .base import RELATED_OBJECT_OK # noqa 

75from .base import SQL_OK # noqa 

76from .base import SQLORMExpression 

77from .base import state_str 

78from .. import event 

79from .. import exc 

80from .. import inspection 

81from .. import util 

82from ..event import dispatcher 

83from ..event import EventTarget 

84from ..sql import base as sql_base 

85from ..sql import cache_key 

86from ..sql import coercions 

87from ..sql import roles 

88from ..sql import visitors 

89from ..sql.cache_key import HasCacheKey 

90from ..sql.visitors import _TraverseInternalsType 

91from ..sql.visitors import InternalTraversal 

92from ..util.typing import Literal 

93from ..util.typing import Self 

94from ..util.typing import TypeGuard 

95 

96if TYPE_CHECKING: 

97 from ._typing import _EntityType 

98 from ._typing import _ExternalEntityType 

99 from ._typing import _InstanceDict 

100 from ._typing import _InternalEntityType 

101 from ._typing import _LoaderCallable 

102 from ._typing import _O 

103 from .collections import _AdaptedCollectionProtocol 

104 from .collections import CollectionAdapter 

105 from .interfaces import MapperProperty 

106 from .relationships import RelationshipProperty 

107 from .state import InstanceState 

108 from .util import AliasedInsp 

109 from .writeonly import WriteOnlyAttributeImpl 

110 from ..event.base import _Dispatch 

111 from ..sql._typing import _ColumnExpressionArgument 

112 from ..sql._typing import _DMLColumnArgument 

113 from ..sql._typing import _InfoType 

114 from ..sql._typing import _PropagateAttrsType 

115 from ..sql.annotation import _AnnotationDict 

116 from ..sql.elements import ColumnElement 

117 from ..sql.elements import Label 

118 from ..sql.operators import OperatorType 

119 from ..sql.selectable import FromClause 

120 

121 

122_T = TypeVar("_T") 

123_T_co = TypeVar("_T_co", bound=Any, covariant=True) 

124 

125 

126_AllPendingType = Sequence[ 

127 Tuple[Optional["InstanceState[Any]"], Optional[object]] 

128] 

129 

130 

131_UNKNOWN_ATTR_KEY = object() 

132 

133 

134@inspection._self_inspects 

135class QueryableAttribute( 

136 _DeclarativeMapped[_T_co], 

137 SQLORMExpression[_T_co], 

138 interfaces.InspectionAttr, 

139 interfaces.PropComparator[_T_co], 

140 roles.JoinTargetRole, 

141 roles.OnClauseRole, 

142 sql_base.Immutable, 

143 cache_key.SlotsMemoizedHasCacheKey, 

144 util.MemoizedSlots, 

145 EventTarget, 

146): 

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

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

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

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

151 attribute. 

152 

153 

154 .. seealso:: 

155 

156 :class:`.InstrumentedAttribute` 

157 

158 :class:`.MapperProperty` 

159 

160 :attr:`_orm.Mapper.all_orm_descriptors` 

161 

162 :attr:`_orm.Mapper.attrs` 

163 """ 

164 

165 __slots__ = ( 

166 "class_", 

167 "key", 

168 "impl", 

169 "comparator", 

170 "property", 

171 "parent", 

172 "expression", 

173 "_of_type", 

174 "_extra_criteria", 

175 "_slots_dispatch", 

176 "_propagate_attrs", 

177 "_doc", 

178 ) 

179 

180 is_attribute = True 

181 

182 dispatch: dispatcher[QueryableAttribute[_T_co]] 

183 

184 class_: _ExternalEntityType[Any] 

185 key: str 

186 parententity: _InternalEntityType[Any] 

187 impl: AttributeImpl 

188 comparator: interfaces.PropComparator[_T_co] 

189 _of_type: Optional[_InternalEntityType[Any]] 

190 _extra_criteria: Tuple[ColumnElement[bool], ...] 

191 _doc: Optional[str] 

192 

193 # PropComparator has a __visit_name__ to participate within 

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

195 __visit_name__ = "orm_instrumented_attribute" 

196 

197 def __init__( 

198 self, 

199 class_: _ExternalEntityType[_O], 

200 key: str, 

201 parententity: _InternalEntityType[_O], 

202 comparator: interfaces.PropComparator[_T_co], 

203 impl: Optional[AttributeImpl] = None, 

204 of_type: Optional[_InternalEntityType[Any]] = None, 

205 extra_criteria: Tuple[ColumnElement[bool], ...] = (), 

206 ): 

207 self.class_ = class_ 

208 self.key = key 

209 

210 self._parententity = self.parent = parententity 

211 

212 # this attribute is non-None after mappers are set up, however in the 

213 # interim class manager setup, there's a check for None to see if it 

214 # needs to be populated, so we assign None here leaving the attribute 

215 # in a temporarily not-type-correct state 

216 self.impl = impl # type: ignore 

217 

218 assert comparator is not None 

219 self.comparator = comparator 

220 self._of_type = of_type 

221 self._extra_criteria = extra_criteria 

222 self._doc = None 

223 

224 manager = opt_manager_of_class(class_) 

225 # manager is None in the case of AliasedClass 

226 if manager: 

227 # propagate existing event listeners from 

228 # immediate superclass 

229 for base in manager._bases: 

230 if key in base: 

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

232 if base[key].dispatch._active_history: 

233 self.dispatch._active_history = True # type: ignore 

234 

235 _cache_key_traversal = [ 

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

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

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

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

240 ] 

241 

242 def __reduce__(self) -> Any: 

243 # this method is only used in terms of the 

244 # sqlalchemy.ext.serializer extension 

245 return ( 

246 _queryable_attribute_unreduce, 

247 ( 

248 self.key, 

249 self._parententity.mapper.class_, 

250 self._parententity, 

251 self._parententity.entity, 

252 ), 

253 ) 

254 

255 @property 

256 def _impl_uses_objects(self) -> bool: 

257 return self.impl.uses_objects 

258 

259 def get_history( 

260 self, instance: Any, passive: PassiveFlag = PASSIVE_OFF 

261 ) -> History: 

262 return self.impl.get_history( 

263 instance_state(instance), instance_dict(instance), passive 

264 ) 

265 

266 @property 

267 def info(self) -> _InfoType: 

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

269 

270 The behavior here is as follows: 

271 

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

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

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

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

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

277 

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

279 any other kind of SQL expression other than a 

280 :class:`_schema.Column`, 

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

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

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

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

285 construct has defined one). 

286 

287 * If the attribute refers to any other kind of 

288 :class:`.MapperProperty`, including :class:`.Relationship`, 

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

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

291 

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

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

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

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

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

297 ``MyClass.someattribute.property.info``. 

298 

299 .. seealso:: 

300 

301 :attr:`.SchemaItem.info` 

302 

303 :attr:`.MapperProperty.info` 

304 

305 """ 

306 return self.comparator.info 

307 

308 parent: _InternalEntityType[Any] 

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

310 

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

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

313 of the parent entity which this attribute is associated 

314 with. 

315 

316 """ 

317 

318 expression: ColumnElement[_T_co] 

319 """The SQL expression object represented by this 

320 :class:`.QueryableAttribute`. 

321 

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

323 subclass representing a column expression. 

324 

325 """ 

326 

327 def _memoized_attr_expression(self) -> ColumnElement[_T]: 

328 annotations: _AnnotationDict 

329 

330 # applies only to Proxy() as used by hybrid. 

331 # currently is an exception to typing rather than feeding through 

332 # non-string keys. 

333 # ideally Proxy() would have a separate set of methods to deal 

334 # with this case. 

335 entity_namespace = self._entity_namespace 

336 assert isinstance(entity_namespace, HasCacheKey) 

337 

338 if self.key is _UNKNOWN_ATTR_KEY: 

339 annotations = {"entity_namespace": entity_namespace} 

340 else: 

341 annotations = { 

342 "proxy_key": self.key, 

343 "proxy_owner": self._parententity, 

344 "entity_namespace": entity_namespace, 

345 } 

346 

347 ce = self.comparator.__clause_element__() 

348 try: 

349 if TYPE_CHECKING: 

350 assert isinstance(ce, ColumnElement) 

351 anno = ce._annotate 

352 except AttributeError as ae: 

353 raise exc.InvalidRequestError( 

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

355 "expected __clause_element__() to return " 

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

357 ) from ae 

358 else: 

359 return anno(annotations) 

360 

361 def _memoized_attr__propagate_attrs(self) -> _PropagateAttrsType: 

362 # this suits the case in coercions where we don't actually 

363 # call ``__clause_element__()`` but still need to get 

364 # resolved._propagate_attrs. See #6558. 

365 return util.immutabledict( 

366 { 

367 "compile_state_plugin": "orm", 

368 "plugin_subject": self._parentmapper, 

369 } 

370 ) 

371 

372 @property 

373 def _entity_namespace(self) -> _InternalEntityType[Any]: 

374 return self._parententity 

375 

376 @property 

377 def _annotations(self) -> _AnnotationDict: 

378 return self.__clause_element__()._annotations 

379 

380 def __clause_element__(self) -> ColumnElement[_T_co]: 

381 return self.expression 

382 

383 @property 

384 def _from_objects(self) -> List[FromClause]: 

385 return self.expression._from_objects 

386 

387 def _bulk_update_tuples( 

388 self, value: Any 

389 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]: 

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

391 

392 return self.comparator._bulk_update_tuples(value) 

393 

394 def adapt_to_entity(self, adapt_to_entity: AliasedInsp[Any]) -> Self: 

395 assert not self._of_type 

396 return self.__class__( 

397 adapt_to_entity.entity, 

398 self.key, 

399 impl=self.impl, 

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

401 parententity=adapt_to_entity, 

402 ) 

403 

404 def of_type(self, entity: _EntityType[_T]) -> QueryableAttribute[_T]: 

405 return QueryableAttribute( 

406 self.class_, 

407 self.key, 

408 self._parententity, 

409 impl=self.impl, 

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

411 of_type=inspection.inspect(entity), 

412 extra_criteria=self._extra_criteria, 

413 ) 

414 

415 def and_( 

416 self, *clauses: _ColumnExpressionArgument[bool] 

417 ) -> QueryableAttribute[bool]: 

418 if TYPE_CHECKING: 

419 assert isinstance(self.comparator, RelationshipProperty.Comparator) 

420 

421 exprs = tuple( 

422 coercions.expect(roles.WhereHavingRole, clause) 

423 for clause in util.coerce_generator_arg(clauses) 

424 ) 

425 

426 return QueryableAttribute( 

427 self.class_, 

428 self.key, 

429 self._parententity, 

430 impl=self.impl, 

431 comparator=self.comparator.and_(*exprs), 

432 of_type=self._of_type, 

433 extra_criteria=self._extra_criteria + exprs, 

434 ) 

435 

436 def _clone(self, **kw: Any) -> QueryableAttribute[_T]: 

437 return QueryableAttribute( 

438 self.class_, 

439 self.key, 

440 self._parententity, 

441 impl=self.impl, 

442 comparator=self.comparator, 

443 of_type=self._of_type, 

444 extra_criteria=self._extra_criteria, 

445 ) 

446 

447 def label(self, name: Optional[str]) -> Label[_T_co]: 

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

449 

450 def operate( 

451 self, op: OperatorType, *other: Any, **kwargs: Any 

452 ) -> ColumnElement[Any]: 

453 return op(self.comparator, *other, **kwargs) # type: ignore[no-any-return] # noqa: E501 

454 

455 def reverse_operate( 

456 self, op: OperatorType, other: Any, **kwargs: Any 

457 ) -> ColumnElement[Any]: 

458 return op(other, self.comparator, **kwargs) # type: ignore[no-any-return] # noqa: E501 

459 

460 def hasparent( 

461 self, state: InstanceState[Any], optimistic: bool = False 

462 ) -> bool: 

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

464 

465 def __getattr__(self, key: str) -> Any: 

466 try: 

467 return util.MemoizedSlots.__getattr__(self, key) 

468 except AttributeError: 

469 pass 

470 

471 try: 

472 return getattr(self.comparator, key) 

473 except AttributeError as err: 

474 raise AttributeError( 

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

476 "has an attribute %r" 

477 % ( 

478 type(self).__name__, 

479 type(self.comparator).__name__, 

480 self, 

481 key, 

482 ) 

483 ) from err 

484 

485 def __str__(self) -> str: 

486 return f"{self.class_.__name__}.{self.key}" 

487 

488 def _memoized_attr_property(self) -> Optional[MapperProperty[Any]]: 

489 return self.comparator.property 

490 

491 

492def _queryable_attribute_unreduce( 

493 key: str, 

494 mapped_class: Type[_O], 

495 parententity: _InternalEntityType[_O], 

496 entity: _ExternalEntityType[Any], 

497) -> Any: 

498 # this method is only used in terms of the 

499 # sqlalchemy.ext.serializer extension 

500 if insp_is_aliased_class(parententity): 

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

502 else: 

503 return getattr(entity, key) 

504 

505 

506class InstrumentedAttribute(QueryableAttribute[_T_co]): 

507 """Class bound instrumented attribute which adds basic 

508 :term:`descriptor` methods. 

509 

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

511 

512 

513 """ 

514 

515 __slots__ = () 

516 

517 inherit_cache = True 

518 """:meta private:""" 

519 

520 # hack to make __doc__ writeable on instances of 

521 # InstrumentedAttribute, while still keeping classlevel 

522 # __doc__ correct 

523 

524 @util.rw_hybridproperty 

525 def __doc__(self) -> Optional[str]: 

526 return self._doc 

527 

528 @__doc__.setter # type: ignore 

529 def __doc__(self, value: Optional[str]) -> None: 

530 self._doc = value 

531 

532 @__doc__.classlevel # type: ignore 

533 def __doc__(cls) -> Optional[str]: 

534 return super().__doc__ 

535 

536 def __set__(self, instance: object, value: Any) -> None: 

537 self.impl.set( 

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

539 ) 

540 

541 def __delete__(self, instance: object) -> None: 

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

543 

544 @overload 

545 def __get__( 

546 self, instance: None, owner: Any 

547 ) -> InstrumentedAttribute[_T_co]: ... 

548 

549 @overload 

550 def __get__(self, instance: object, owner: Any) -> _T_co: ... 

551 

552 def __get__( 

553 self, instance: Optional[object], owner: Any 

554 ) -> Union[InstrumentedAttribute[_T_co], _T_co]: 

555 if instance is None: 

556 return self 

557 

558 dict_ = instance_dict(instance) 

559 if self.impl.supports_population and self.key in dict_: 

560 return dict_[self.key] # type: ignore[no-any-return] 

561 else: 

562 try: 

563 state = instance_state(instance) 

564 except AttributeError as err: 

565 raise orm_exc.UnmappedInstanceError(instance) from err 

566 return self.impl.get(state, dict_) # type: ignore[no-any-return] 

567 

568 

569@dataclasses.dataclass(frozen=True) 

570class AdHocHasEntityNamespace(HasCacheKey): 

571 _traverse_internals: ClassVar[_TraverseInternalsType] = [ 

572 ("_entity_namespace", InternalTraversal.dp_has_cache_key), 

573 ] 

574 

575 # py37 compat, no slots=True on dataclass 

576 __slots__ = ("_entity_namespace",) 

577 _entity_namespace: _InternalEntityType[Any] 

578 is_mapper: ClassVar[bool] = False 

579 is_aliased_class: ClassVar[bool] = False 

580 

581 @property 

582 def entity_namespace(self): 

583 return self._entity_namespace.entity_namespace 

584 

585 

586def create_proxied_attribute( 

587 descriptor: Any, 

588) -> Callable[..., QueryableAttribute[Any]]: 

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

590 

591 Returns a new QueryableAttribute type that delegates descriptor 

592 behavior and getattr() to the given descriptor. 

593 """ 

594 

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

596 # function is removed from ext/hybrid.py 

597 

598 class Proxy(QueryableAttribute[Any]): 

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

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

601 combination. 

602 

603 """ 

604 

605 _extra_criteria = () 

606 

607 # the attribute error catches inside of __getattr__ basically create a 

608 # singularity if you try putting slots on this too 

609 # __slots__ = ("descriptor", "original_property", "_comparator") 

610 

611 def __init__( 

612 self, 

613 class_, 

614 key, 

615 descriptor, 

616 comparator, 

617 adapt_to_entity=None, 

618 doc=None, 

619 original_property=None, 

620 ): 

621 self.class_ = class_ 

622 self.key = key 

623 self.descriptor = descriptor 

624 self.original_property = original_property 

625 self._comparator = comparator 

626 self._adapt_to_entity = adapt_to_entity 

627 self._doc = self.__doc__ = doc 

628 

629 @property 

630 def _parententity(self): 

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

632 

633 @property 

634 def parent(self): 

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

636 

637 _is_internal_proxy = True 

638 

639 _cache_key_traversal = [ 

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

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

642 ] 

643 

644 @property 

645 def _impl_uses_objects(self): 

646 return ( 

647 self.original_property is not None 

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

649 ) 

650 

651 @property 

652 def _entity_namespace(self): 

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

654 return self._comparator._parententity 

655 else: 

656 # used by hybrid attributes which try to remain 

657 # agnostic of any ORM concepts like mappers 

658 return AdHocHasEntityNamespace(self._parententity) 

659 

660 @property 

661 def property(self): 

662 return self.comparator.property 

663 

664 @util.memoized_property 

665 def comparator(self): 

666 if callable(self._comparator): 

667 self._comparator = self._comparator() 

668 if self._adapt_to_entity: 

669 self._comparator = self._comparator.adapt_to_entity( 

670 self._adapt_to_entity 

671 ) 

672 return self._comparator 

673 

674 def adapt_to_entity(self, adapt_to_entity): 

675 return self.__class__( 

676 adapt_to_entity.entity, 

677 self.key, 

678 self.descriptor, 

679 self._comparator, 

680 adapt_to_entity, 

681 ) 

682 

683 def _clone(self, **kw): 

684 return self.__class__( 

685 self.class_, 

686 self.key, 

687 self.descriptor, 

688 self._comparator, 

689 adapt_to_entity=self._adapt_to_entity, 

690 original_property=self.original_property, 

691 ) 

692 

693 def __get__(self, instance, owner): 

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

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

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

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

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

699 return self 

700 else: 

701 return retval 

702 

703 def __str__(self) -> str: 

704 return f"{self.class_.__name__}.{self.key}" 

705 

706 def __getattr__(self, attribute): 

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

708 comparator.""" 

709 

710 # this is unfortunately very complicated, and is easily prone 

711 # to recursion overflows when implementations of related 

712 # __getattr__ schemes are changed 

713 

714 try: 

715 return util.MemoizedSlots.__getattr__(self, attribute) 

716 except AttributeError: 

717 pass 

718 

719 try: 

720 return getattr(descriptor, attribute) 

721 except AttributeError as err: 

722 if attribute == "comparator": 

723 raise AttributeError("comparator") from err 

724 try: 

725 # comparator itself might be unreachable 

726 comparator = self.comparator 

727 except AttributeError as err2: 

728 raise AttributeError( 

729 "Neither %r object nor unconfigured comparator " 

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

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

732 ) from err2 

733 else: 

734 try: 

735 return getattr(comparator, attribute) 

736 except AttributeError as err3: 

737 raise AttributeError( 

738 "Neither %r object nor %r object " 

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

740 % ( 

741 type(descriptor).__name__, 

742 type(comparator).__name__, 

743 self, 

744 attribute, 

745 ) 

746 ) from err3 

747 

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

749 

750 util.monkeypatch_proxied_specials( 

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

752 ) 

753 return Proxy 

754 

755 

756OP_REMOVE = util.symbol("REMOVE") 

757OP_APPEND = util.symbol("APPEND") 

758OP_REPLACE = util.symbol("REPLACE") 

759OP_BULK_REPLACE = util.symbol("BULK_REPLACE") 

760OP_MODIFIED = util.symbol("MODIFIED") 

761 

762 

763class AttributeEventToken: 

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

765 events. 

766 

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

768 a means of controlling propagation across a chain of attribute 

769 operations. 

770 

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

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

773 :meth:`.AttributeEvents.set`, 

774 and :meth:`.AttributeEvents.remove`. 

775 

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

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

778 across two mutually-dependent attributes. 

779 

780 .. versionchanged:: 2.0 Changed the name from ``AttributeEvent`` 

781 to ``AttributeEventToken``. 

782 

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

784 initiator. 

785 

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

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

788 source operation. 

789 

790 """ 

791 

792 __slots__ = "impl", "op", "parent_token" 

793 

794 def __init__(self, attribute_impl: AttributeImpl, op: util.symbol): 

795 self.impl = attribute_impl 

796 self.op = op 

797 self.parent_token = self.impl.parent_token 

798 

799 def __eq__(self, other): 

800 return ( 

801 isinstance(other, AttributeEventToken) 

802 and other.impl is self.impl 

803 and other.op == self.op 

804 ) 

805 

806 @property 

807 def key(self): 

808 return self.impl.key 

809 

810 def hasparent(self, state): 

811 return self.impl.hasparent(state) 

812 

813 

814AttributeEvent = AttributeEventToken # legacy 

815Event = AttributeEventToken # legacy 

816 

817 

818class AttributeImpl: 

819 """internal implementation for instrumented attributes.""" 

820 

821 collection: bool 

822 default_accepts_scalar_loader: bool 

823 uses_objects: bool 

824 supports_population: bool 

825 dynamic: bool 

826 

827 _is_has_collection_adapter = False 

828 

829 _replace_token: AttributeEventToken 

830 _remove_token: AttributeEventToken 

831 _append_token: AttributeEventToken 

832 

833 def __init__( 

834 self, 

835 class_: _ExternalEntityType[_O], 

836 key: str, 

837 callable_: Optional[_LoaderCallable], 

838 dispatch: _Dispatch[QueryableAttribute[Any]], 

839 trackparent: bool = False, 

840 compare_function: Optional[Callable[..., bool]] = None, 

841 active_history: bool = False, 

842 parent_token: Optional[AttributeEventToken] = None, 

843 load_on_unexpire: bool = True, 

844 send_modified_events: bool = True, 

845 accepts_scalar_loader: Optional[bool] = None, 

846 **kwargs: Any, 

847 ): 

848 r"""Construct an AttributeImpl. 

849 

850 :param \class_: associated class 

851 

852 :param key: string name of the attribute 

853 

854 :param \callable_: 

855 optional function which generates a callable based on a parent 

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

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

858 already. 

859 

860 :param trackparent: 

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

862 to it via this attribute. 

863 

864 :param compare_function: 

865 a function that compares two values which are normally 

866 assignable to this attribute. 

867 

868 :param active_history: 

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

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

871 

872 :param parent_token: 

873 Usually references the MapperProperty, used as a key for 

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

875 Allows multiple AttributeImpls to all match a single 

876 owner attribute. 

877 

878 :param load_on_unexpire: 

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

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

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

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

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

884 attribute. 

885 

886 :param send_modified_events: 

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

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

889 history entry. 

890 

891 """ 

892 self.class_ = class_ 

893 self.key = key 

894 self.callable_ = callable_ 

895 self.dispatch = dispatch 

896 self.trackparent = trackparent 

897 self.parent_token = parent_token or self 

898 self.send_modified_events = send_modified_events 

899 if compare_function is None: 

900 self.is_equal = operator.eq 

901 else: 

902 self.is_equal = compare_function 

903 

904 if accepts_scalar_loader is not None: 

905 self.accepts_scalar_loader = accepts_scalar_loader 

906 else: 

907 self.accepts_scalar_loader = self.default_accepts_scalar_loader 

908 

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

910 self._deferred_history = _deferred_history 

911 

912 if active_history: 

913 self.dispatch._active_history = True 

914 

915 self.load_on_unexpire = load_on_unexpire 

916 self._modified_token = AttributeEventToken(self, OP_MODIFIED) 

917 

918 __slots__ = ( 

919 "class_", 

920 "key", 

921 "callable_", 

922 "dispatch", 

923 "trackparent", 

924 "parent_token", 

925 "send_modified_events", 

926 "is_equal", 

927 "load_on_unexpire", 

928 "_modified_token", 

929 "accepts_scalar_loader", 

930 "_deferred_history", 

931 ) 

932 

933 def __str__(self) -> str: 

934 return f"{self.class_.__name__}.{self.key}" 

935 

936 def _get_active_history(self): 

937 """Backwards compat for impl.active_history""" 

938 

939 return self.dispatch._active_history 

940 

941 def _set_active_history(self, value): 

942 self.dispatch._active_history = value 

943 

944 active_history = property(_get_active_history, _set_active_history) 

945 

946 def hasparent( 

947 self, state: InstanceState[Any], optimistic: bool = False 

948 ) -> bool: 

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

950 the given state. 

951 

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

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

954 

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

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

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

958 flag is set. 

959 

960 An instance attribute that is loaded by a callable function 

961 will also not have a `hasparent` flag. 

962 

963 """ 

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

965 assert self.trackparent, msg 

966 

967 return ( 

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

969 ) 

970 

971 def sethasparent( 

972 self, 

973 state: InstanceState[Any], 

974 parent_state: InstanceState[Any], 

975 value: bool, 

976 ) -> None: 

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

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

979 attribute represented by this ``InstrumentedAttribute``. 

980 

981 """ 

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

983 assert self.trackparent, msg 

984 

985 id_ = id(self.parent_token) 

986 if value: 

987 state.parents[id_] = parent_state 

988 else: 

989 if id_ in state.parents: 

990 last_parent = state.parents[id_] 

991 

992 if ( 

993 last_parent is not False 

994 and last_parent.key != parent_state.key 

995 ): 

996 if last_parent.obj() is None: 

997 raise orm_exc.StaleDataError( 

998 "Removing state %s from parent " 

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

1000 "but the parent record " 

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

1002 "is the most recent parent." 

1003 % ( 

1004 state_str(state), 

1005 state_str(parent_state), 

1006 self.key, 

1007 ) 

1008 ) 

1009 

1010 return 

1011 

1012 state.parents[id_] = False 

1013 

1014 def get_history( 

1015 self, 

1016 state: InstanceState[Any], 

1017 dict_: _InstanceDict, 

1018 passive: PassiveFlag = PASSIVE_OFF, 

1019 ) -> History: 

1020 raise NotImplementedError() 

1021 

1022 def get_all_pending( 

1023 self, 

1024 state: InstanceState[Any], 

1025 dict_: _InstanceDict, 

1026 passive: PassiveFlag = PASSIVE_NO_INITIALIZE, 

1027 ) -> _AllPendingType: 

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

1029 for all objects in this attribute's current state 

1030 + history. 

1031 

1032 Only applies to object-based attributes. 

1033 

1034 This is an inlining of existing functionality 

1035 which roughly corresponds to: 

1036 

1037 get_state_history( 

1038 state, 

1039 key, 

1040 passive=PASSIVE_NO_INITIALIZE).sum() 

1041 

1042 """ 

1043 raise NotImplementedError() 

1044 

1045 def _default_value( 

1046 self, state: InstanceState[Any], dict_: _InstanceDict 

1047 ) -> Any: 

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

1049 

1050 assert self.key not in dict_, ( 

1051 "_default_value should only be invoked for an " 

1052 "uninitialized or expired attribute" 

1053 ) 

1054 

1055 value = None 

1056 for fn in self.dispatch.init_scalar: 

1057 ret = fn(state, value, dict_) 

1058 if ret is not ATTR_EMPTY: 

1059 value = ret 

1060 

1061 return value 

1062 

1063 def get( 

1064 self, 

1065 state: InstanceState[Any], 

1066 dict_: _InstanceDict, 

1067 passive: PassiveFlag = PASSIVE_OFF, 

1068 ) -> Any: 

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

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

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

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

1073 """ 

1074 if self.key in dict_: 

1075 return dict_[self.key] 

1076 else: 

1077 # if history present, don't load 

1078 key = self.key 

1079 if ( 

1080 key not in state.committed_state 

1081 or state.committed_state[key] is NO_VALUE 

1082 ): 

1083 if not passive & CALLABLES_OK: 

1084 return PASSIVE_NO_RESULT 

1085 

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

1087 

1088 if value is PASSIVE_NO_RESULT or value is NO_VALUE: 

1089 return value 

1090 elif value is ATTR_WAS_SET: 

1091 try: 

1092 return dict_[key] 

1093 except KeyError as err: 

1094 # TODO: no test coverage here. 

1095 raise KeyError( 

1096 "Deferred loader for attribute " 

1097 "%r failed to populate " 

1098 "correctly" % key 

1099 ) from err 

1100 elif value is not ATTR_EMPTY: 

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

1102 

1103 if not passive & INIT_OK: 

1104 return NO_VALUE 

1105 else: 

1106 return self._default_value(state, dict_) 

1107 

1108 def _fire_loader_callables( 

1109 self, state: InstanceState[Any], key: str, passive: PassiveFlag 

1110 ) -> Any: 

1111 if ( 

1112 self.accepts_scalar_loader 

1113 and self.load_on_unexpire 

1114 and key in state.expired_attributes 

1115 ): 

1116 return state._load_expired(state, passive) 

1117 elif key in state.callables: 

1118 callable_ = state.callables[key] 

1119 return callable_(state, passive) 

1120 elif self.callable_: 

1121 return self.callable_(state, passive) 

1122 else: 

1123 return ATTR_EMPTY 

1124 

1125 def append( 

1126 self, 

1127 state: InstanceState[Any], 

1128 dict_: _InstanceDict, 

1129 value: Any, 

1130 initiator: Optional[AttributeEventToken], 

1131 passive: PassiveFlag = PASSIVE_OFF, 

1132 ) -> None: 

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

1134 

1135 def remove( 

1136 self, 

1137 state: InstanceState[Any], 

1138 dict_: _InstanceDict, 

1139 value: Any, 

1140 initiator: Optional[AttributeEventToken], 

1141 passive: PassiveFlag = PASSIVE_OFF, 

1142 ) -> None: 

1143 self.set( 

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

1145 ) 

1146 

1147 def pop( 

1148 self, 

1149 state: InstanceState[Any], 

1150 dict_: _InstanceDict, 

1151 value: Any, 

1152 initiator: Optional[AttributeEventToken], 

1153 passive: PassiveFlag = PASSIVE_OFF, 

1154 ) -> None: 

1155 self.set( 

1156 state, 

1157 dict_, 

1158 None, 

1159 initiator, 

1160 passive=passive, 

1161 check_old=value, 

1162 pop=True, 

1163 ) 

1164 

1165 def set( 

1166 self, 

1167 state: InstanceState[Any], 

1168 dict_: _InstanceDict, 

1169 value: Any, 

1170 initiator: Optional[AttributeEventToken] = None, 

1171 passive: PassiveFlag = PASSIVE_OFF, 

1172 check_old: Any = None, 

1173 pop: bool = False, 

1174 ) -> None: 

1175 raise NotImplementedError() 

1176 

1177 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None: 

1178 raise NotImplementedError() 

1179 

1180 def get_committed_value( 

1181 self, 

1182 state: InstanceState[Any], 

1183 dict_: _InstanceDict, 

1184 passive: PassiveFlag = PASSIVE_OFF, 

1185 ) -> Any: 

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

1187 

1188 if self.key in state.committed_state: 

1189 value = state.committed_state[self.key] 

1190 if value is NO_VALUE: 

1191 return None 

1192 else: 

1193 return value 

1194 else: 

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

1196 

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

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

1199 

1200 dict_[self.key] = value 

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

1202 return value 

1203 

1204 

1205class ScalarAttributeImpl(AttributeImpl): 

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

1207 

1208 default_accepts_scalar_loader = True 

1209 uses_objects = False 

1210 supports_population = True 

1211 collection = False 

1212 dynamic = False 

1213 

1214 __slots__ = "_replace_token", "_append_token", "_remove_token" 

1215 

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

1217 super().__init__(*arg, **kw) 

1218 self._replace_token = self._append_token = AttributeEventToken( 

1219 self, OP_REPLACE 

1220 ) 

1221 self._remove_token = AttributeEventToken(self, OP_REMOVE) 

1222 

1223 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None: 

1224 if self.dispatch._active_history: 

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

1226 else: 

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

1228 

1229 if self.dispatch.remove: 

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

1231 state._modified_event(dict_, self, old) 

1232 

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

1234 if ( 

1235 existing is NO_VALUE 

1236 and old is NO_VALUE 

1237 and not state.expired 

1238 and self.key not in state.expired_attributes 

1239 ): 

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

1241 

1242 def get_history( 

1243 self, 

1244 state: InstanceState[Any], 

1245 dict_: Dict[str, Any], 

1246 passive: PassiveFlag = PASSIVE_OFF, 

1247 ) -> History: 

1248 if self.key in dict_: 

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

1250 elif self.key in state.committed_state: 

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

1252 else: 

1253 if passive & INIT_OK: 

1254 passive ^= INIT_OK 

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

1256 if current is PASSIVE_NO_RESULT: 

1257 return HISTORY_BLANK 

1258 else: 

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

1260 

1261 def set( 

1262 self, 

1263 state: InstanceState[Any], 

1264 dict_: Dict[str, Any], 

1265 value: Any, 

1266 initiator: Optional[AttributeEventToken] = None, 

1267 passive: PassiveFlag = PASSIVE_OFF, 

1268 check_old: Optional[object] = None, 

1269 pop: bool = False, 

1270 ) -> None: 

1271 if self.dispatch._active_history: 

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

1273 else: 

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

1275 

1276 if self.dispatch.set: 

1277 value = self.fire_replace_event( 

1278 state, dict_, value, old, initiator 

1279 ) 

1280 state._modified_event(dict_, self, old) 

1281 dict_[self.key] = value 

1282 

1283 def fire_replace_event( 

1284 self, 

1285 state: InstanceState[Any], 

1286 dict_: _InstanceDict, 

1287 value: _T, 

1288 previous: Any, 

1289 initiator: Optional[AttributeEventToken], 

1290 ) -> _T: 

1291 for fn in self.dispatch.set: 

1292 value = fn( 

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

1294 ) 

1295 return value 

1296 

1297 def fire_remove_event( 

1298 self, 

1299 state: InstanceState[Any], 

1300 dict_: _InstanceDict, 

1301 value: Any, 

1302 initiator: Optional[AttributeEventToken], 

1303 ) -> None: 

1304 for fn in self.dispatch.remove: 

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

1306 

1307 

1308class ScalarObjectAttributeImpl(ScalarAttributeImpl): 

1309 """represents a scalar-holding InstrumentedAttribute, 

1310 where the target object is also instrumented. 

1311 

1312 Adds events to delete/set operations. 

1313 

1314 """ 

1315 

1316 default_accepts_scalar_loader = False 

1317 uses_objects = True 

1318 supports_population = True 

1319 collection = False 

1320 

1321 __slots__ = () 

1322 

1323 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None: 

1324 if self.dispatch._active_history: 

1325 old = self.get( 

1326 state, 

1327 dict_, 

1328 passive=PASSIVE_ONLY_PERSISTENT 

1329 | NO_AUTOFLUSH 

1330 | LOAD_AGAINST_COMMITTED, 

1331 ) 

1332 else: 

1333 old = self.get( 

1334 state, 

1335 dict_, 

1336 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1337 | LOAD_AGAINST_COMMITTED 

1338 | NO_RAISE, 

1339 ) 

1340 

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

1342 

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

1344 

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

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

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

1348 if ( 

1349 existing is NO_VALUE 

1350 and old is not PASSIVE_NO_RESULT 

1351 and state.key is None 

1352 ): 

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

1354 

1355 def get_history( 

1356 self, 

1357 state: InstanceState[Any], 

1358 dict_: _InstanceDict, 

1359 passive: PassiveFlag = PASSIVE_OFF, 

1360 ) -> History: 

1361 if self.key in dict_: 

1362 current = dict_[self.key] 

1363 else: 

1364 if passive & INIT_OK: 

1365 passive ^= INIT_OK 

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

1367 if current is PASSIVE_NO_RESULT: 

1368 return HISTORY_BLANK 

1369 

1370 if not self._deferred_history: 

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

1372 else: 

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

1374 if original is PASSIVE_NO_RESULT: 

1375 loader_passive = passive | ( 

1376 PASSIVE_ONLY_PERSISTENT 

1377 | NO_AUTOFLUSH 

1378 | LOAD_AGAINST_COMMITTED 

1379 | NO_RAISE 

1380 | DEFERRED_HISTORY_LOAD 

1381 ) 

1382 original = self._fire_loader_callables( 

1383 state, self.key, loader_passive 

1384 ) 

1385 return History.from_object_attribute( 

1386 self, state, current, original=original 

1387 ) 

1388 

1389 def get_all_pending( 

1390 self, 

1391 state: InstanceState[Any], 

1392 dict_: _InstanceDict, 

1393 passive: PassiveFlag = PASSIVE_NO_INITIALIZE, 

1394 ) -> _AllPendingType: 

1395 if self.key in dict_: 

1396 current = dict_[self.key] 

1397 elif passive & CALLABLES_OK: 

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

1399 else: 

1400 return [] 

1401 

1402 ret: _AllPendingType 

1403 

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

1405 if ( 

1406 current is not None 

1407 and current is not PASSIVE_NO_RESULT 

1408 and current is not NO_VALUE 

1409 ): 

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

1411 else: 

1412 ret = [(None, None)] 

1413 

1414 if self.key in state.committed_state: 

1415 original = state.committed_state[self.key] 

1416 if ( 

1417 original is not None 

1418 and original is not PASSIVE_NO_RESULT 

1419 and original is not NO_VALUE 

1420 and original is not current 

1421 ): 

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

1423 return ret 

1424 

1425 def set( 

1426 self, 

1427 state: InstanceState[Any], 

1428 dict_: _InstanceDict, 

1429 value: Any, 

1430 initiator: Optional[AttributeEventToken] = None, 

1431 passive: PassiveFlag = PASSIVE_OFF, 

1432 check_old: Any = None, 

1433 pop: bool = False, 

1434 ) -> None: 

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

1436 

1437 if self.dispatch._active_history: 

1438 old = self.get( 

1439 state, 

1440 dict_, 

1441 passive=PASSIVE_ONLY_PERSISTENT 

1442 | NO_AUTOFLUSH 

1443 | LOAD_AGAINST_COMMITTED, 

1444 ) 

1445 else: 

1446 old = self.get( 

1447 state, 

1448 dict_, 

1449 passive=PASSIVE_NO_FETCH ^ INIT_OK 

1450 | LOAD_AGAINST_COMMITTED 

1451 | NO_RAISE, 

1452 ) 

1453 

1454 if ( 

1455 check_old is not None 

1456 and old is not PASSIVE_NO_RESULT 

1457 and check_old is not old 

1458 ): 

1459 if pop: 

1460 return 

1461 else: 

1462 raise ValueError( 

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

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

1465 ) 

1466 

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

1468 dict_[self.key] = value 

1469 

1470 def fire_remove_event( 

1471 self, 

1472 state: InstanceState[Any], 

1473 dict_: _InstanceDict, 

1474 value: Any, 

1475 initiator: Optional[AttributeEventToken], 

1476 ) -> None: 

1477 if self.trackparent and value not in ( 

1478 None, 

1479 PASSIVE_NO_RESULT, 

1480 NO_VALUE, 

1481 ): 

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

1483 

1484 for fn in self.dispatch.remove: 

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

1486 

1487 state._modified_event(dict_, self, value) 

1488 

1489 def fire_replace_event( 

1490 self, 

1491 state: InstanceState[Any], 

1492 dict_: _InstanceDict, 

1493 value: _T, 

1494 previous: Any, 

1495 initiator: Optional[AttributeEventToken], 

1496 ) -> _T: 

1497 if self.trackparent: 

1498 if previous is not value and previous not in ( 

1499 None, 

1500 PASSIVE_NO_RESULT, 

1501 NO_VALUE, 

1502 ): 

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

1504 

1505 for fn in self.dispatch.set: 

1506 value = fn( 

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

1508 ) 

1509 

1510 state._modified_event(dict_, self, previous) 

1511 

1512 if self.trackparent: 

1513 if value is not None: 

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

1515 

1516 return value 

1517 

1518 

1519class HasCollectionAdapter: 

1520 __slots__ = () 

1521 

1522 collection: bool 

1523 _is_has_collection_adapter = True 

1524 

1525 def _dispose_previous_collection( 

1526 self, 

1527 state: InstanceState[Any], 

1528 collection: _AdaptedCollectionProtocol, 

1529 adapter: CollectionAdapter, 

1530 fire_event: bool, 

1531 ) -> None: 

1532 raise NotImplementedError() 

1533 

1534 @overload 

1535 def get_collection( 

1536 self, 

1537 state: InstanceState[Any], 

1538 dict_: _InstanceDict, 

1539 user_data: Literal[None] = ..., 

1540 passive: Literal[PassiveFlag.PASSIVE_OFF] = ..., 

1541 ) -> CollectionAdapter: ... 

1542 

1543 @overload 

1544 def get_collection( 

1545 self, 

1546 state: InstanceState[Any], 

1547 dict_: _InstanceDict, 

1548 user_data: _AdaptedCollectionProtocol = ..., 

1549 passive: PassiveFlag = ..., 

1550 ) -> CollectionAdapter: ... 

1551 

1552 @overload 

1553 def get_collection( 

1554 self, 

1555 state: InstanceState[Any], 

1556 dict_: _InstanceDict, 

1557 user_data: Optional[_AdaptedCollectionProtocol] = ..., 

1558 passive: PassiveFlag = ..., 

1559 ) -> Union[ 

1560 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter 

1561 ]: ... 

1562 

1563 def get_collection( 

1564 self, 

1565 state: InstanceState[Any], 

1566 dict_: _InstanceDict, 

1567 user_data: Optional[_AdaptedCollectionProtocol] = None, 

1568 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

1569 ) -> Union[ 

1570 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter 

1571 ]: 

1572 raise NotImplementedError() 

1573 

1574 def set( 

1575 self, 

1576 state: InstanceState[Any], 

1577 dict_: _InstanceDict, 

1578 value: Any, 

1579 initiator: Optional[AttributeEventToken] = None, 

1580 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

1581 check_old: Any = None, 

1582 pop: bool = False, 

1583 _adapt: bool = True, 

1584 ) -> None: 

1585 raise NotImplementedError() 

1586 

1587 

1588if TYPE_CHECKING: 

1589 

1590 def _is_collection_attribute_impl( 

1591 impl: AttributeImpl, 

1592 ) -> TypeGuard[CollectionAttributeImpl]: ... 

1593 

1594else: 

1595 _is_collection_attribute_impl = operator.attrgetter("collection") 

1596 

1597 

1598class CollectionAttributeImpl(HasCollectionAdapter, AttributeImpl): 

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

1600 

1601 Only handles collections of instrumented objects. 

1602 

1603 InstrumentedCollectionAttribute holds an arbitrary, user-specified 

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

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

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

1607 

1608 """ 

1609 

1610 uses_objects = True 

1611 collection = True 

1612 default_accepts_scalar_loader = False 

1613 supports_population = True 

1614 dynamic = False 

1615 

1616 _bulk_replace_token: AttributeEventToken 

1617 

1618 __slots__ = ( 

1619 "copy", 

1620 "collection_factory", 

1621 "_append_token", 

1622 "_remove_token", 

1623 "_bulk_replace_token", 

1624 "_duck_typed_as", 

1625 ) 

1626 

1627 def __init__( 

1628 self, 

1629 class_, 

1630 key, 

1631 callable_, 

1632 dispatch, 

1633 typecallable=None, 

1634 trackparent=False, 

1635 copy_function=None, 

1636 compare_function=None, 

1637 **kwargs, 

1638 ): 

1639 super().__init__( 

1640 class_, 

1641 key, 

1642 callable_, 

1643 dispatch, 

1644 trackparent=trackparent, 

1645 compare_function=compare_function, 

1646 **kwargs, 

1647 ) 

1648 

1649 if copy_function is None: 

1650 copy_function = self.__copy 

1651 self.copy = copy_function 

1652 self.collection_factory = typecallable 

1653 self._append_token = AttributeEventToken(self, OP_APPEND) 

1654 self._remove_token = AttributeEventToken(self, OP_REMOVE) 

1655 self._bulk_replace_token = AttributeEventToken(self, OP_BULK_REPLACE) 

1656 self._duck_typed_as = util.duck_type_collection( 

1657 self.collection_factory() 

1658 ) 

1659 

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

1661 

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

1663 def link(target, collection, collection_adapter): 

1664 collection._sa_linker(collection_adapter) 

1665 

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

1667 def unlink(target, collection, collection_adapter): 

1668 collection._sa_linker(None) 

1669 

1670 def __copy(self, item): 

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

1672 

1673 def get_history( 

1674 self, 

1675 state: InstanceState[Any], 

1676 dict_: _InstanceDict, 

1677 passive: PassiveFlag = PASSIVE_OFF, 

1678 ) -> History: 

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

1680 

1681 if current is PASSIVE_NO_RESULT: 

1682 if ( 

1683 passive & PassiveFlag.INCLUDE_PENDING_MUTATIONS 

1684 and self.key in state._pending_mutations 

1685 ): 

1686 pending = state._pending_mutations[self.key] 

1687 return pending.merge_with_history(HISTORY_BLANK) 

1688 else: 

1689 return HISTORY_BLANK 

1690 else: 

1691 if passive & PassiveFlag.INCLUDE_PENDING_MUTATIONS: 

1692 # this collection is loaded / present. should not be any 

1693 # pending mutations 

1694 assert self.key not in state._pending_mutations 

1695 

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

1697 

1698 def get_all_pending( 

1699 self, 

1700 state: InstanceState[Any], 

1701 dict_: _InstanceDict, 

1702 passive: PassiveFlag = PASSIVE_NO_INITIALIZE, 

1703 ) -> _AllPendingType: 

1704 # NOTE: passive is ignored here at the moment 

1705 

1706 if self.key not in dict_: 

1707 return [] 

1708 

1709 current = dict_[self.key] 

1710 current = getattr(current, "_sa_adapter") 

1711 

1712 if self.key in state.committed_state: 

1713 original = state.committed_state[self.key] 

1714 if original is not NO_VALUE: 

1715 current_states = [ 

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

1717 for c in current 

1718 ] 

1719 original_states = [ 

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

1721 for c in original 

1722 ] 

1723 

1724 current_set = dict(current_states) 

1725 original_set = dict(original_states) 

1726 

1727 return ( 

1728 [ 

1729 (s, o) 

1730 for s, o in current_states 

1731 if s not in original_set 

1732 ] 

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

1734 + [ 

1735 (s, o) 

1736 for s, o in original_states 

1737 if s not in current_set 

1738 ] 

1739 ) 

1740 

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

1742 

1743 def fire_append_event( 

1744 self, 

1745 state: InstanceState[Any], 

1746 dict_: _InstanceDict, 

1747 value: _T, 

1748 initiator: Optional[AttributeEventToken], 

1749 key: Optional[Any], 

1750 ) -> _T: 

1751 for fn in self.dispatch.append: 

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

1753 

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

1755 

1756 if self.trackparent and value is not None: 

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

1758 

1759 return value 

1760 

1761 def fire_append_wo_mutation_event( 

1762 self, 

1763 state: InstanceState[Any], 

1764 dict_: _InstanceDict, 

1765 value: _T, 

1766 initiator: Optional[AttributeEventToken], 

1767 key: Optional[Any], 

1768 ) -> _T: 

1769 for fn in self.dispatch.append_wo_mutation: 

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

1771 

1772 return value 

1773 

1774 def fire_pre_remove_event( 

1775 self, 

1776 state: InstanceState[Any], 

1777 dict_: _InstanceDict, 

1778 initiator: Optional[AttributeEventToken], 

1779 key: Optional[Any], 

1780 ) -> None: 

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

1782 

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

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

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

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

1787 

1788 """ 

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

1790 

1791 def fire_remove_event( 

1792 self, 

1793 state: InstanceState[Any], 

1794 dict_: _InstanceDict, 

1795 value: Any, 

1796 initiator: Optional[AttributeEventToken], 

1797 key: Optional[Any], 

1798 ) -> None: 

1799 if self.trackparent and value is not None: 

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

1801 

1802 for fn in self.dispatch.remove: 

1803 fn(state, value, initiator or self._remove_token, key=key) 

1804 

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

1806 

1807 def delete(self, state: InstanceState[Any], dict_: _InstanceDict) -> None: 

1808 if self.key not in dict_: 

1809 return 

1810 

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

1812 

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

1814 collection.clear_with_event() 

1815 

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

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

1818 del dict_[self.key] 

1819 

1820 def _default_value( 

1821 self, state: InstanceState[Any], dict_: _InstanceDict 

1822 ) -> _AdaptedCollectionProtocol: 

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

1824 

1825 assert self.key not in dict_, ( 

1826 "_default_value should only be invoked for an " 

1827 "uninitialized or expired attribute" 

1828 ) 

1829 

1830 if self.key in state._empty_collections: 

1831 return state._empty_collections[self.key] 

1832 

1833 adapter, user_data = self._initialize_collection(state) 

1834 adapter._set_empty(user_data) 

1835 return user_data 

1836 

1837 def _initialize_collection( 

1838 self, state: InstanceState[Any] 

1839 ) -> Tuple[CollectionAdapter, _AdaptedCollectionProtocol]: 

1840 adapter, collection = state.manager.initialize_collection( 

1841 self.key, state, self.collection_factory 

1842 ) 

1843 

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

1845 

1846 return adapter, collection 

1847 

1848 def append( 

1849 self, 

1850 state: InstanceState[Any], 

1851 dict_: _InstanceDict, 

1852 value: Any, 

1853 initiator: Optional[AttributeEventToken], 

1854 passive: PassiveFlag = PASSIVE_OFF, 

1855 ) -> None: 

1856 collection = self.get_collection( 

1857 state, dict_, user_data=None, passive=passive 

1858 ) 

1859 if collection is PASSIVE_NO_RESULT: 

1860 value = self.fire_append_event( 

1861 state, dict_, value, initiator, key=NO_KEY 

1862 ) 

1863 assert ( 

1864 self.key not in dict_ 

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

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

1867 else: 

1868 if TYPE_CHECKING: 

1869 assert isinstance(collection, CollectionAdapter) 

1870 collection.append_with_event(value, initiator) 

1871 

1872 def remove( 

1873 self, 

1874 state: InstanceState[Any], 

1875 dict_: _InstanceDict, 

1876 value: Any, 

1877 initiator: Optional[AttributeEventToken], 

1878 passive: PassiveFlag = PASSIVE_OFF, 

1879 ) -> None: 

1880 collection = self.get_collection( 

1881 state, state.dict, user_data=None, passive=passive 

1882 ) 

1883 if collection is PASSIVE_NO_RESULT: 

1884 self.fire_remove_event(state, dict_, value, initiator, key=NO_KEY) 

1885 assert ( 

1886 self.key not in dict_ 

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

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

1889 else: 

1890 if TYPE_CHECKING: 

1891 assert isinstance(collection, CollectionAdapter) 

1892 collection.remove_with_event(value, initiator) 

1893 

1894 def pop( 

1895 self, 

1896 state: InstanceState[Any], 

1897 dict_: _InstanceDict, 

1898 value: Any, 

1899 initiator: Optional[AttributeEventToken], 

1900 passive: PassiveFlag = PASSIVE_OFF, 

1901 ) -> None: 

1902 try: 

1903 # TODO: better solution here would be to add 

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

1905 # "remover". 

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

1907 except (ValueError, KeyError, IndexError): 

1908 pass 

1909 

1910 def set( 

1911 self, 

1912 state: InstanceState[Any], 

1913 dict_: _InstanceDict, 

1914 value: Any, 

1915 initiator: Optional[AttributeEventToken] = None, 

1916 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF, 

1917 check_old: Any = None, 

1918 pop: bool = False, 

1919 _adapt: bool = True, 

1920 ) -> None: 

1921 iterable = orig_iterable = value 

1922 new_keys = None 

1923 

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

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

1926 new_collection, user_data = self._initialize_collection(state) 

1927 if _adapt: 

1928 if new_collection._converter is not None: 

1929 iterable = new_collection._converter(iterable) 

1930 else: 

1931 setting_type = util.duck_type_collection(iterable) 

1932 receiving_type = self._duck_typed_as 

1933 

1934 if setting_type is not receiving_type: 

1935 given = ( 

1936 iterable is None 

1937 and "None" 

1938 or iterable.__class__.__name__ 

1939 ) 

1940 wanted = self._duck_typed_as.__name__ 

1941 raise TypeError( 

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

1943 % (given, wanted) 

1944 ) 

1945 

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

1947 # adapter. 

1948 if hasattr(iterable, "_sa_iterator"): 

1949 iterable = iterable._sa_iterator() 

1950 elif setting_type is dict: 

1951 new_keys = list(iterable) 

1952 iterable = iterable.values() 

1953 else: 

1954 iterable = iter(iterable) 

1955 elif util.duck_type_collection(iterable) is dict: 

1956 new_keys = list(value) 

1957 

1958 new_values = list(iterable) 

1959 

1960 evt = self._bulk_replace_token 

1961 

1962 self.dispatch.bulk_replace(state, new_values, evt, keys=new_keys) 

1963 

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

1965 # existing object (ticket #8862) 

1966 old = self.get( 

1967 state, 

1968 dict_, 

1969 passive=PASSIVE_ONLY_PERSISTENT ^ (passive & PassiveFlag.NO_RAISE), 

1970 ) 

1971 if old is PASSIVE_NO_RESULT: 

1972 old = self._default_value(state, dict_) 

1973 elif old is orig_iterable: 

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

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

1976 return 

1977 

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

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

1980 

1981 old_collection = old._sa_adapter 

1982 

1983 dict_[self.key] = user_data 

1984 

1985 collections.bulk_replace( 

1986 new_values, old_collection, new_collection, initiator=evt 

1987 ) 

1988 

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

1990 

1991 def _dispose_previous_collection( 

1992 self, 

1993 state: InstanceState[Any], 

1994 collection: _AdaptedCollectionProtocol, 

1995 adapter: CollectionAdapter, 

1996 fire_event: bool, 

1997 ) -> None: 

1998 del collection._sa_adapter 

1999 

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

2001 # collections. 

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

2003 if fire_event: 

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

2005 

2006 def _invalidate_collection( 

2007 self, collection: _AdaptedCollectionProtocol 

2008 ) -> None: 

2009 adapter = getattr(collection, "_sa_adapter") 

2010 adapter.invalidated = True 

2011 

2012 def set_committed_value( 

2013 self, state: InstanceState[Any], dict_: _InstanceDict, value: Any 

2014 ) -> _AdaptedCollectionProtocol: 

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

2016 

2017 collection, user_data = self._initialize_collection(state) 

2018 

2019 if value: 

2020 collection.append_multiple_without_event(value) 

2021 

2022 state.dict[self.key] = user_data 

2023 

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

2025 

2026 if self.key in state._pending_mutations: 

2027 # pending items exist. issue a modified event, 

2028 # add/remove new items. 

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

2030 

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

2032 added = pending.added_items 

2033 removed = pending.deleted_items 

2034 for item in added: 

2035 collection.append_without_event(item) 

2036 for item in removed: 

2037 collection.remove_without_event(item) 

2038 

2039 return user_data 

2040 

2041 @overload 

2042 def get_collection( 

2043 self, 

2044 state: InstanceState[Any], 

2045 dict_: _InstanceDict, 

2046 user_data: Literal[None] = ..., 

2047 passive: Literal[PassiveFlag.PASSIVE_OFF] = ..., 

2048 ) -> CollectionAdapter: ... 

2049 

2050 @overload 

2051 def get_collection( 

2052 self, 

2053 state: InstanceState[Any], 

2054 dict_: _InstanceDict, 

2055 user_data: _AdaptedCollectionProtocol = ..., 

2056 passive: PassiveFlag = ..., 

2057 ) -> CollectionAdapter: ... 

2058 

2059 @overload 

2060 def get_collection( 

2061 self, 

2062 state: InstanceState[Any], 

2063 dict_: _InstanceDict, 

2064 user_data: Optional[_AdaptedCollectionProtocol] = ..., 

2065 passive: PassiveFlag = PASSIVE_OFF, 

2066 ) -> Union[ 

2067 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter 

2068 ]: ... 

2069 

2070 def get_collection( 

2071 self, 

2072 state: InstanceState[Any], 

2073 dict_: _InstanceDict, 

2074 user_data: Optional[_AdaptedCollectionProtocol] = None, 

2075 passive: PassiveFlag = PASSIVE_OFF, 

2076 ) -> Union[ 

2077 Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter 

2078 ]: 

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

2080 

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

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

2083 collection value. 

2084 

2085 """ 

2086 if user_data is None: 

2087 fetch_user_data = self.get(state, dict_, passive=passive) 

2088 if fetch_user_data is LoaderCallableStatus.PASSIVE_NO_RESULT: 

2089 return fetch_user_data 

2090 else: 

2091 user_data = cast("_AdaptedCollectionProtocol", fetch_user_data) 

2092 

2093 return user_data._sa_adapter 

2094 

2095 

2096def backref_listeners( 

2097 attribute: QueryableAttribute[Any], key: str, uselist: bool 

2098) -> None: 

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

2100 

2101 # use easily recognizable names for stack traces. 

2102 

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

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

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

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

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

2108 

2109 parent_token = attribute.impl.parent_token 

2110 parent_impl = attribute.impl 

2111 

2112 def _acceptable_key_err(child_state, initiator, child_impl): 

2113 raise ValueError( 

2114 "Bidirectional attribute conflict detected: " 

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

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

2117 'via the backref "%s".' 

2118 % ( 

2119 state_str(child_state), 

2120 initiator.parent_token, 

2121 child_impl.parent_token, 

2122 attribute.impl.parent_token, 

2123 ) 

2124 ) 

2125 

2126 def emit_backref_from_scalar_set_event( 

2127 state, child, oldchild, initiator, **kw 

2128 ): 

2129 if oldchild is child: 

2130 return child 

2131 if ( 

2132 oldchild is not None 

2133 and oldchild is not PASSIVE_NO_RESULT 

2134 and oldchild is not NO_VALUE 

2135 ): 

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

2137 # present when updating via a backref. 

2138 old_state, old_dict = ( 

2139 instance_state(oldchild), 

2140 instance_dict(oldchild), 

2141 ) 

2142 impl = old_state.manager[key].impl 

2143 

2144 # tokens to test for a recursive loop. 

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

2146 check_recursive_token = impl._replace_token 

2147 else: 

2148 check_recursive_token = impl._remove_token 

2149 

2150 if initiator is not check_recursive_token: 

2151 impl.pop( 

2152 old_state, 

2153 old_dict, 

2154 state.obj(), 

2155 parent_impl._append_token, 

2156 passive=PASSIVE_NO_FETCH, 

2157 ) 

2158 

2159 if child is not None: 

2160 child_state, child_dict = ( 

2161 instance_state(child), 

2162 instance_dict(child), 

2163 ) 

2164 child_impl = child_state.manager[key].impl 

2165 

2166 if ( 

2167 initiator.parent_token is not parent_token 

2168 and initiator.parent_token is not child_impl.parent_token 

2169 ): 

2170 _acceptable_key_err(state, initiator, child_impl) 

2171 

2172 # tokens to test for a recursive loop. 

2173 check_append_token = child_impl._append_token 

2174 check_bulk_replace_token = ( 

2175 child_impl._bulk_replace_token 

2176 if _is_collection_attribute_impl(child_impl) 

2177 else None 

2178 ) 

2179 

2180 if ( 

2181 initiator is not check_append_token 

2182 and initiator is not check_bulk_replace_token 

2183 ): 

2184 child_impl.append( 

2185 child_state, 

2186 child_dict, 

2187 state.obj(), 

2188 initiator, 

2189 passive=PASSIVE_NO_FETCH, 

2190 ) 

2191 return child 

2192 

2193 def emit_backref_from_collection_append_event( 

2194 state, child, initiator, **kw 

2195 ): 

2196 if child is None: 

2197 return 

2198 

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

2200 child_impl = child_state.manager[key].impl 

2201 

2202 if ( 

2203 initiator.parent_token is not parent_token 

2204 and initiator.parent_token is not child_impl.parent_token 

2205 ): 

2206 _acceptable_key_err(state, initiator, child_impl) 

2207 

2208 # tokens to test for a recursive loop. 

2209 check_append_token = child_impl._append_token 

2210 check_bulk_replace_token = ( 

2211 child_impl._bulk_replace_token 

2212 if _is_collection_attribute_impl(child_impl) 

2213 else None 

2214 ) 

2215 

2216 if ( 

2217 initiator is not check_append_token 

2218 and initiator is not check_bulk_replace_token 

2219 ): 

2220 child_impl.append( 

2221 child_state, 

2222 child_dict, 

2223 state.obj(), 

2224 initiator, 

2225 passive=PASSIVE_NO_FETCH, 

2226 ) 

2227 return child 

2228 

2229 def emit_backref_from_collection_remove_event( 

2230 state, child, initiator, **kw 

2231 ): 

2232 if ( 

2233 child is not None 

2234 and child is not PASSIVE_NO_RESULT 

2235 and child is not NO_VALUE 

2236 ): 

2237 child_state, child_dict = ( 

2238 instance_state(child), 

2239 instance_dict(child), 

2240 ) 

2241 child_impl = child_state.manager[key].impl 

2242 

2243 check_replace_token: Optional[AttributeEventToken] 

2244 

2245 # tokens to test for a recursive loop. 

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

2247 check_remove_token = child_impl._remove_token 

2248 check_replace_token = child_impl._replace_token 

2249 check_for_dupes_on_remove = uselist and not parent_impl.dynamic 

2250 else: 

2251 check_remove_token = child_impl._remove_token 

2252 check_replace_token = ( 

2253 child_impl._bulk_replace_token 

2254 if _is_collection_attribute_impl(child_impl) 

2255 else None 

2256 ) 

2257 check_for_dupes_on_remove = False 

2258 

2259 if ( 

2260 initiator is not check_remove_token 

2261 and initiator is not check_replace_token 

2262 ): 

2263 if not check_for_dupes_on_remove or not util.has_dupes( 

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

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

2266 state.dict[parent_impl.key], 

2267 child, 

2268 ): 

2269 child_impl.pop( 

2270 child_state, 

2271 child_dict, 

2272 state.obj(), 

2273 initiator, 

2274 passive=PASSIVE_NO_FETCH, 

2275 ) 

2276 

2277 if uselist: 

2278 event.listen( 

2279 attribute, 

2280 "append", 

2281 emit_backref_from_collection_append_event, 

2282 retval=True, 

2283 raw=True, 

2284 include_key=True, 

2285 ) 

2286 else: 

2287 event.listen( 

2288 attribute, 

2289 "set", 

2290 emit_backref_from_scalar_set_event, 

2291 retval=True, 

2292 raw=True, 

2293 include_key=True, 

2294 ) 

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

2296 event.listen( 

2297 attribute, 

2298 "remove", 

2299 emit_backref_from_collection_remove_event, 

2300 retval=True, 

2301 raw=True, 

2302 include_key=True, 

2303 ) 

2304 

2305 

2306_NO_HISTORY = util.symbol("NO_HISTORY") 

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

2308 

2309 

2310class History(NamedTuple): 

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

2312 representing the changes which have occurred on an instrumented 

2313 attribute. 

2314 

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

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

2317 

2318 from sqlalchemy import inspect 

2319 

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

2321 

2322 Each tuple member is an iterable sequence: 

2323 

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

2325 tuple element). 

2326 

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

2328 attribute (the second tuple element). 

2329 

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

2331 attribute (the third tuple element). 

2332 

2333 """ 

2334 

2335 added: Union[Tuple[()], List[Any]] 

2336 unchanged: Union[Tuple[()], List[Any]] 

2337 deleted: Union[Tuple[()], List[Any]] 

2338 

2339 def __bool__(self) -> bool: 

2340 return self != HISTORY_BLANK 

2341 

2342 def empty(self) -> bool: 

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

2344 and no existing, unchanged state. 

2345 

2346 """ 

2347 

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

2349 

2350 def sum(self) -> Sequence[Any]: 

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

2352 

2353 return ( 

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

2355 ) 

2356 

2357 def non_deleted(self) -> Sequence[Any]: 

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

2359 

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

2361 

2362 def non_added(self) -> Sequence[Any]: 

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

2364 

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

2366 

2367 def has_changes(self) -> bool: 

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

2369 

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

2371 

2372 def _merge(self, added: Iterable[Any], deleted: Iterable[Any]) -> History: 

2373 return History( 

2374 list(self.added) + list(added), 

2375 self.unchanged, 

2376 list(self.deleted) + list(deleted), 

2377 ) 

2378 

2379 def as_state(self) -> History: 

2380 return History( 

2381 [ 

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

2383 for c in self.added 

2384 ], 

2385 [ 

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

2387 for c in self.unchanged 

2388 ], 

2389 [ 

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

2391 for c in self.deleted 

2392 ], 

2393 ) 

2394 

2395 @classmethod 

2396 def from_scalar_attribute( 

2397 cls, 

2398 attribute: ScalarAttributeImpl, 

2399 state: InstanceState[Any], 

2400 current: Any, 

2401 ) -> History: 

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

2403 

2404 deleted: Union[Tuple[()], List[Any]] 

2405 

2406 if original is _NO_HISTORY: 

2407 if current is NO_VALUE: 

2408 return cls((), (), ()) 

2409 else: 

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

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

2412 elif ( 

2413 current is not NO_VALUE 

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

2415 ): 

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

2417 else: 

2418 # current convention on native scalars is to not 

2419 # include information 

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

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

2422 # key situations 

2423 if id(original) in _NO_STATE_SYMBOLS: 

2424 deleted = () 

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

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

2427 if id(current) in _NO_STATE_SYMBOLS: 

2428 current = None 

2429 else: 

2430 deleted = [original] 

2431 if current is NO_VALUE: 

2432 return cls((), (), deleted) 

2433 else: 

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

2435 

2436 @classmethod 

2437 def from_object_attribute( 

2438 cls, 

2439 attribute: ScalarObjectAttributeImpl, 

2440 state: InstanceState[Any], 

2441 current: Any, 

2442 original: Any = _NO_HISTORY, 

2443 ) -> History: 

2444 deleted: Union[Tuple[()], List[Any]] 

2445 

2446 if original is _NO_HISTORY: 

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

2448 

2449 if original is _NO_HISTORY: 

2450 if current is NO_VALUE: 

2451 return cls((), (), ()) 

2452 else: 

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

2454 elif current is original and current is not NO_VALUE: 

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

2456 else: 

2457 # current convention on related objects is to not 

2458 # include information 

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

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

2461 # ignore the None in any case. 

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

2463 deleted = () 

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

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

2466 if id(current) in _NO_STATE_SYMBOLS: 

2467 current = None 

2468 else: 

2469 deleted = [original] 

2470 if current is NO_VALUE: 

2471 return cls((), (), deleted) 

2472 else: 

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

2474 

2475 @classmethod 

2476 def from_collection( 

2477 cls, 

2478 attribute: CollectionAttributeImpl, 

2479 state: InstanceState[Any], 

2480 current: Any, 

2481 ) -> History: 

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

2483 if current is NO_VALUE: 

2484 return cls((), (), ()) 

2485 

2486 current = getattr(current, "_sa_adapter") 

2487 if original is NO_VALUE: 

2488 return cls(list(current), (), ()) 

2489 elif original is _NO_HISTORY: 

2490 return cls((), list(current), ()) 

2491 else: 

2492 current_states = [ 

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

2494 for c in current 

2495 ] 

2496 original_states = [ 

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

2498 for c in original 

2499 ] 

2500 

2501 current_set = dict(current_states) 

2502 original_set = dict(original_states) 

2503 

2504 return cls( 

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

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

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

2508 ) 

2509 

2510 

2511HISTORY_BLANK = History((), (), ()) 

2512 

2513 

2514def get_history( 

2515 obj: object, key: str, passive: PassiveFlag = PASSIVE_OFF 

2516) -> History: 

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

2518 and attribute key. 

2519 

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

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

2522 current database transaction. 

2523 

2524 .. note:: 

2525 

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

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

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

2529 

2530 

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

2532 attributes package. 

2533 

2534 :param key: string attribute name. 

2535 

2536 :param passive: indicates loading behavior for the attribute 

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

2538 bitflag attribute, which defaults to the symbol 

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

2540 should be emitted. 

2541 

2542 .. seealso:: 

2543 

2544 :attr:`.AttributeState.history` 

2545 

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

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

2548 

2549 """ 

2550 

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

2552 

2553 

2554def get_state_history( 

2555 state: InstanceState[Any], key: str, passive: PassiveFlag = PASSIVE_OFF 

2556) -> History: 

2557 return state.get_history(key, passive) 

2558 

2559 

2560def has_parent( 

2561 cls: Type[_O], obj: _O, key: str, optimistic: bool = False 

2562) -> bool: 

2563 """TODO""" 

2564 manager = manager_of_class(cls) 

2565 state = instance_state(obj) 

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

2567 

2568 

2569def register_attribute( 

2570 class_: Type[_O], 

2571 key: str, 

2572 *, 

2573 comparator: interfaces.PropComparator[_T], 

2574 parententity: _InternalEntityType[_O], 

2575 doc: Optional[str] = None, 

2576 **kw: Any, 

2577) -> InstrumentedAttribute[_T]: 

2578 desc = register_descriptor( 

2579 class_, key, comparator=comparator, parententity=parententity, doc=doc 

2580 ) 

2581 register_attribute_impl(class_, key, **kw) 

2582 return desc 

2583 

2584 

2585def register_attribute_impl( 

2586 class_: Type[_O], 

2587 key: str, 

2588 uselist: bool = False, 

2589 callable_: Optional[_LoaderCallable] = None, 

2590 useobject: bool = False, 

2591 impl_class: Optional[Type[AttributeImpl]] = None, 

2592 backref: Optional[str] = None, 

2593 **kw: Any, 

2594) -> QueryableAttribute[Any]: 

2595 manager = manager_of_class(class_) 

2596 if uselist: 

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

2598 typecallable = manager.instrument_collection_class( 

2599 key, factory or list 

2600 ) 

2601 else: 

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

2603 

2604 dispatch = cast( 

2605 "_Dispatch[QueryableAttribute[Any]]", manager[key].dispatch 

2606 ) # noqa: E501 

2607 

2608 impl: AttributeImpl 

2609 

2610 if impl_class: 

2611 # TODO: this appears to be the WriteOnlyAttributeImpl / 

2612 # DynamicAttributeImpl constructor which is hardcoded 

2613 impl = cast("Type[WriteOnlyAttributeImpl]", impl_class)( 

2614 class_, key, dispatch, **kw 

2615 ) 

2616 elif uselist: 

2617 impl = CollectionAttributeImpl( 

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

2619 ) 

2620 elif useobject: 

2621 impl = ScalarObjectAttributeImpl( 

2622 class_, key, callable_, dispatch, **kw 

2623 ) 

2624 else: 

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

2626 

2627 manager[key].impl = impl 

2628 

2629 if backref: 

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

2631 

2632 manager.post_configure_attribute(key) 

2633 return manager[key] 

2634 

2635 

2636def register_descriptor( 

2637 class_: Type[Any], 

2638 key: str, 

2639 *, 

2640 comparator: interfaces.PropComparator[_T], 

2641 parententity: _InternalEntityType[Any], 

2642 doc: Optional[str] = None, 

2643) -> InstrumentedAttribute[_T]: 

2644 manager = manager_of_class(class_) 

2645 

2646 descriptor = InstrumentedAttribute( 

2647 class_, key, comparator=comparator, parententity=parententity 

2648 ) 

2649 

2650 descriptor.__doc__ = doc # type: ignore 

2651 

2652 manager.instrument_attribute(key, descriptor) 

2653 return descriptor 

2654 

2655 

2656def unregister_attribute(class_: Type[Any], key: str) -> None: 

2657 manager_of_class(class_).uninstrument_attribute(key) 

2658 

2659 

2660def init_collection(obj: object, key: str) -> CollectionAdapter: 

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

2662 

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

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

2665 

2666 collection_adapter = init_collection(someobject, 'elements') 

2667 for elem in values: 

2668 collection_adapter.append_without_event(elem) 

2669 

2670 For an easier way to do the above, see 

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

2672 

2673 :param obj: a mapped object 

2674 

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

2676 

2677 """ 

2678 state = instance_state(obj) 

2679 dict_ = state.dict 

2680 return init_state_collection(state, dict_, key) 

2681 

2682 

2683def init_state_collection( 

2684 state: InstanceState[Any], dict_: _InstanceDict, key: str 

2685) -> CollectionAdapter: 

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

2687 

2688 Discards any existing collection which may be there. 

2689 

2690 """ 

2691 attr = state.manager[key].impl 

2692 

2693 if TYPE_CHECKING: 

2694 assert isinstance(attr, HasCollectionAdapter) 

2695 

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

2697 if old is not None: 

2698 old_collection = old._sa_adapter 

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

2700 

2701 user_data = attr._default_value(state, dict_) 

2702 adapter: CollectionAdapter = attr.get_collection( 

2703 state, dict_, user_data, passive=PassiveFlag.PASSIVE_NO_FETCH 

2704 ) 

2705 adapter._reset_empty() 

2706 

2707 return adapter 

2708 

2709 

2710def set_committed_value(instance, key, value): 

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

2712 

2713 Cancels any previous history present. The value should be 

2714 a scalar value for scalar-holding attributes, or 

2715 an iterable for any collection-holding attribute. 

2716 

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

2718 fires off and loads additional data from the database. 

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

2720 which has loaded additional attributes or collections through 

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

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

2723 

2724 """ 

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

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

2727 

2728 

2729def set_attribute( 

2730 instance: object, 

2731 key: str, 

2732 value: Any, 

2733 initiator: Optional[AttributeEventToken] = None, 

2734) -> None: 

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

2736 

2737 This function may be used regardless of instrumentation 

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

2739 Custom attribute management schemes will need to make usage 

2740 of this method to establish attribute state as understood 

2741 by SQLAlchemy. 

2742 

2743 :param instance: the object that will be modified 

2744 

2745 :param key: string name of the attribute 

2746 

2747 :param value: value to assign 

2748 

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

2750 been propagated from a previous event listener. This argument 

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

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

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

2754 chain of events. 

2755 

2756 .. versionadded:: 1.2.3 

2757 

2758 """ 

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

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

2761 

2762 

2763def get_attribute(instance: object, key: str) -> Any: 

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

2765 

2766 This function may be used regardless of instrumentation 

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

2768 Custom attribute management schemes will need to make usage 

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

2770 by SQLAlchemy. 

2771 

2772 """ 

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

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

2775 

2776 

2777def del_attribute(instance: object, key: str) -> None: 

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

2779 

2780 This function may be used regardless of instrumentation 

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

2782 Custom attribute management schemes will need to make usage 

2783 of this method to establish attribute state as understood 

2784 by SQLAlchemy. 

2785 

2786 """ 

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

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

2789 

2790 

2791def flag_modified(instance: object, key: str) -> None: 

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

2793 

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

2795 establishes an unconditional change event for the given attribute. 

2796 The attribute must have a value present, else an 

2797 :class:`.InvalidRequestError` is raised. 

2798 

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

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

2801 :func:`.attributes.flag_dirty` call. 

2802 

2803 .. seealso:: 

2804 

2805 :func:`.attributes.flag_dirty` 

2806 

2807 """ 

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

2809 impl = state.manager[key].impl 

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

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

2812 

2813 

2814def flag_dirty(instance: object) -> None: 

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

2816 

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

2818 the flush process for interception by events such as 

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

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

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

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

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

2824 emitted. 

2825 

2826 .. versionadded:: 1.2 

2827 

2828 .. seealso:: 

2829 

2830 :func:`.attributes.flag_modified` 

2831 

2832 """ 

2833 

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

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