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

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

783 statements  

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

10 

11""" 

12 

13from __future__ import annotations 

14 

15import typing 

16from typing import Any 

17from typing import Callable 

18from typing import cast 

19from typing import Dict 

20from typing import Final 

21from typing import Iterable 

22from typing import Optional 

23from typing import overload 

24from typing import Sequence 

25from typing import Tuple 

26from typing import Type 

27from typing import TypeVar 

28from typing import Union 

29 

30from . import util as orm_util 

31from ._typing import insp_is_aliased_class 

32from ._typing import insp_is_attribute 

33from ._typing import insp_is_mapper 

34from ._typing import insp_is_mapper_property 

35from .attributes import QueryableAttribute 

36from .base import InspectionAttr 

37from .interfaces import LoaderOption 

38from .path_registry import _DEFAULT_TOKEN 

39from .path_registry import _StrPathToken 

40from .path_registry import _WILDCARD_TOKEN 

41from .path_registry import AbstractEntityRegistry 

42from .path_registry import path_is_property 

43from .path_registry import PathRegistry 

44from .path_registry import TokenRegistry 

45from .util import _orm_full_deannotate 

46from .util import AliasedInsp 

47from .. import exc as sa_exc 

48from .. import inspect 

49from .. import util 

50from ..sql import and_ 

51from ..sql import cache_key 

52from ..sql import coercions 

53from ..sql import roles 

54from ..sql import traversals 

55from ..sql import visitors 

56from ..sql.base import _generative 

57from ..util.typing import Literal 

58from ..util.typing import Self 

59 

60_RELATIONSHIP_TOKEN: Final[Literal["relationship"]] = "relationship" 

61_COLUMN_TOKEN: Final[Literal["column"]] = "column" 

62 

63_FN = TypeVar("_FN", bound="Callable[..., Any]") 

64 

65if typing.TYPE_CHECKING: 

66 from ._typing import _EntityType 

67 from ._typing import _InternalEntityType 

68 from .context import _MapperEntity 

69 from .context import ORMCompileState 

70 from .context import QueryContext 

71 from .interfaces import _StrategyKey 

72 from .interfaces import MapperProperty 

73 from .interfaces import ORMOption 

74 from .mapper import Mapper 

75 from .path_registry import _PathRepresentation 

76 from ..sql._typing import _ColumnExpressionArgument 

77 from ..sql._typing import _FromClauseArgument 

78 from ..sql.cache_key import _CacheKeyTraversalType 

79 from ..sql.cache_key import CacheKey 

80 

81 

82_AttrType = Union[Literal["*"], "QueryableAttribute[Any]"] 

83 

84_WildcardKeyType = Literal["relationship", "column"] 

85_StrategySpec = Dict[str, Any] 

86_OptsType = Dict[str, Any] 

87_AttrGroupType = Tuple[_AttrType, ...] 

88 

89 

90class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): 

91 __slots__ = ("propagate_to_loaders",) 

92 

93 _is_strategy_option = True 

94 propagate_to_loaders: bool 

95 

96 def contains_eager( 

97 self, 

98 attr: _AttrType, 

99 alias: Optional[_FromClauseArgument] = None, 

100 _is_chain: bool = False, 

101 _propagate_to_loaders: bool = False, 

102 ) -> Self: 

103 r"""Indicate that the given attribute should be eagerly loaded from 

104 columns stated manually in the query. 

105 

106 This function is part of the :class:`_orm.Load` interface and supports 

107 both method-chained and standalone operation. 

108 

109 The option is used in conjunction with an explicit join that loads 

110 the desired rows, i.e.:: 

111 

112 sess.query(Order).join(Order.user).options( 

113 contains_eager(Order.user) 

114 ) 

115 

116 The above query would join from the ``Order`` entity to its related 

117 ``User`` entity, and the returned ``Order`` objects would have the 

118 ``Order.user`` attribute pre-populated. 

119 

120 It may also be used for customizing the entries in an eagerly loaded 

121 collection; queries will normally want to use the 

122 :ref:`orm_queryguide_populate_existing` execution option assuming the 

123 primary collection of parent objects may already have been loaded:: 

124 

125 sess.query(User).join(User.addresses).filter( 

126 Address.email_address.like("%@aol.com") 

127 ).options(contains_eager(User.addresses)).populate_existing() 

128 

129 See the section :ref:`contains_eager` for complete usage details. 

130 

131 .. seealso:: 

132 

133 :ref:`loading_toplevel` 

134 

135 :ref:`contains_eager` 

136 

137 """ 

138 if alias is not None: 

139 if not isinstance(alias, str): 

140 coerced_alias = coercions.expect(roles.FromClauseRole, alias) 

141 else: 

142 util.warn_deprecated( 

143 "Passing a string name for the 'alias' argument to " 

144 "'contains_eager()` is deprecated, and will not work in a " 

145 "future release. Please use a sqlalchemy.alias() or " 

146 "sqlalchemy.orm.aliased() construct.", 

147 version="1.4", 

148 ) 

149 coerced_alias = alias 

150 

151 elif getattr(attr, "_of_type", None): 

152 assert isinstance(attr, QueryableAttribute) 

153 ot: Optional[_InternalEntityType[Any]] = inspect(attr._of_type) 

154 assert ot is not None 

155 coerced_alias = ot.selectable 

156 else: 

157 coerced_alias = None 

158 

159 cloned = self._set_relationship_strategy( 

160 attr, 

161 {"lazy": "joined"}, 

162 propagate_to_loaders=_propagate_to_loaders, 

163 opts={"eager_from_alias": coerced_alias}, 

164 _reconcile_to_other=True if _is_chain else None, 

165 ) 

166 return cloned 

167 

168 def load_only(self, *attrs: _AttrType, raiseload: bool = False) -> Self: 

169 r"""Indicate that for a particular entity, only the given list 

170 of column-based attribute names should be loaded; all others will be 

171 deferred. 

172 

173 This function is part of the :class:`_orm.Load` interface and supports 

174 both method-chained and standalone operation. 

175 

176 Example - given a class ``User``, load only the ``name`` and 

177 ``fullname`` attributes:: 

178 

179 session.query(User).options(load_only(User.name, User.fullname)) 

180 

181 Example - given a relationship ``User.addresses -> Address``, specify 

182 subquery loading for the ``User.addresses`` collection, but on each 

183 ``Address`` object load only the ``email_address`` attribute:: 

184 

185 session.query(User).options( 

186 subqueryload(User.addresses).load_only(Address.email_address) 

187 ) 

188 

189 For a statement that has multiple entities, 

190 the lead entity can be 

191 specifically referred to using the :class:`_orm.Load` constructor:: 

192 

193 stmt = ( 

194 select(User, Address) 

195 .join(User.addresses) 

196 .options( 

197 Load(User).load_only(User.name, User.fullname), 

198 Load(Address).load_only(Address.email_address), 

199 ) 

200 ) 

201 

202 When used together with the 

203 :ref:`populate_existing <orm_queryguide_populate_existing>` 

204 execution option only the attributes listed will be refreshed. 

205 

206 :param \*attrs: Attributes to be loaded, all others will be deferred. 

207 

208 :param raiseload: raise :class:`.InvalidRequestError` rather than 

209 lazy loading a value when a deferred attribute is accessed. Used 

210 to prevent unwanted SQL from being emitted. 

211 

212 .. versionadded:: 2.0 

213 

214 .. seealso:: 

215 

216 :ref:`orm_queryguide_column_deferral` - in the 

217 :ref:`queryguide_toplevel` 

218 

219 :param \*attrs: Attributes to be loaded, all others will be deferred. 

220 

221 :param raiseload: raise :class:`.InvalidRequestError` rather than 

222 lazy loading a value when a deferred attribute is accessed. Used 

223 to prevent unwanted SQL from being emitted. 

224 

225 .. versionadded:: 2.0 

226 

227 """ 

228 cloned = self._set_column_strategy( 

229 attrs, 

230 {"deferred": False, "instrument": True}, 

231 ) 

232 

233 wildcard_strategy = {"deferred": True, "instrument": True} 

234 if raiseload: 

235 wildcard_strategy["raiseload"] = True 

236 

237 cloned = cloned._set_column_strategy( 

238 ("*",), 

239 wildcard_strategy, 

240 ) 

241 return cloned 

242 

243 def joinedload( 

244 self, 

245 attr: _AttrType, 

246 innerjoin: Optional[bool] = None, 

247 ) -> Self: 

248 """Indicate that the given attribute should be loaded using joined 

249 eager loading. 

250 

251 This function is part of the :class:`_orm.Load` interface and supports 

252 both method-chained and standalone operation. 

253 

254 examples:: 

255 

256 # joined-load the "orders" collection on "User" 

257 select(User).options(joinedload(User.orders)) 

258 

259 # joined-load Order.items and then Item.keywords 

260 select(Order).options( 

261 joinedload(Order.items).joinedload(Item.keywords) 

262 ) 

263 

264 # lazily load Order.items, but when Items are loaded, 

265 # joined-load the keywords collection 

266 select(Order).options( 

267 lazyload(Order.items).joinedload(Item.keywords) 

268 ) 

269 

270 :param innerjoin: if ``True``, indicates that the joined eager load 

271 should use an inner join instead of the default of left outer join:: 

272 

273 select(Order).options(joinedload(Order.user, innerjoin=True)) 

274 

275 In order to chain multiple eager joins together where some may be 

276 OUTER and others INNER, right-nested joins are used to link them:: 

277 

278 select(A).options( 

279 joinedload(A.bs, innerjoin=False).joinedload( 

280 B.cs, innerjoin=True 

281 ) 

282 ) 

283 

284 The above query, linking A.bs via "outer" join and B.cs via "inner" 

285 join would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When 

286 using older versions of SQLite (< 3.7.16), this form of JOIN is 

287 translated to use full subqueries as this syntax is otherwise not 

288 directly supported. 

289 

290 The ``innerjoin`` flag can also be stated with the term ``"unnested"``. 

291 This indicates that an INNER JOIN should be used, *unless* the join 

292 is linked to a LEFT OUTER JOIN to the left, in which case it 

293 will render as LEFT OUTER JOIN. For example, supposing ``A.bs`` 

294 is an outerjoin:: 

295 

296 select(A).options( 

297 joinedload(A.bs).joinedload(B.cs, innerjoin="unnested") 

298 ) 

299 

300 

301 The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c", 

302 rather than as "a LEFT OUTER JOIN (b JOIN c)". 

303 

304 .. note:: The "unnested" flag does **not** affect the JOIN rendered 

305 from a many-to-many association table, e.g. a table configured as 

306 :paramref:`_orm.relationship.secondary`, to the target table; for 

307 correctness of results, these joins are always INNER and are 

308 therefore right-nested if linked to an OUTER join. 

309 

310 .. note:: 

311 

312 The joins produced by :func:`_orm.joinedload` are **anonymously 

313 aliased**. The criteria by which the join proceeds cannot be 

314 modified, nor can the ORM-enabled :class:`_sql.Select` or legacy 

315 :class:`_query.Query` refer to these joins in any way, including 

316 ordering. See :ref:`zen_of_eager_loading` for further detail. 

317 

318 To produce a specific SQL JOIN which is explicitly available, use 

319 :meth:`_sql.Select.join` and :meth:`_query.Query.join`. To combine 

320 explicit JOINs with eager loading of collections, use 

321 :func:`_orm.contains_eager`; see :ref:`contains_eager`. 

322 

323 .. seealso:: 

324 

325 :ref:`loading_toplevel` 

326 

327 :ref:`joined_eager_loading` 

328 

329 """ 

330 loader = self._set_relationship_strategy( 

331 attr, 

332 {"lazy": "joined"}, 

333 opts=( 

334 {"innerjoin": innerjoin} 

335 if innerjoin is not None 

336 else util.EMPTY_DICT 

337 ), 

338 ) 

339 return loader 

340 

341 def subqueryload(self, attr: _AttrType) -> Self: 

342 """Indicate that the given attribute should be loaded using 

343 subquery eager loading. 

344 

345 This function is part of the :class:`_orm.Load` interface and supports 

346 both method-chained and standalone operation. 

347 

348 examples:: 

349 

350 # subquery-load the "orders" collection on "User" 

351 select(User).options(subqueryload(User.orders)) 

352 

353 # subquery-load Order.items and then Item.keywords 

354 select(Order).options( 

355 subqueryload(Order.items).subqueryload(Item.keywords) 

356 ) 

357 

358 # lazily load Order.items, but when Items are loaded, 

359 # subquery-load the keywords collection 

360 select(Order).options( 

361 lazyload(Order.items).subqueryload(Item.keywords) 

362 ) 

363 

364 

365 .. seealso:: 

366 

367 :ref:`loading_toplevel` 

368 

369 :ref:`subquery_eager_loading` 

370 

371 """ 

372 return self._set_relationship_strategy(attr, {"lazy": "subquery"}) 

373 

374 def selectinload( 

375 self, 

376 attr: _AttrType, 

377 recursion_depth: Optional[int] = None, 

378 ) -> Self: 

379 """Indicate that the given attribute should be loaded using 

380 SELECT IN eager loading. 

381 

382 This function is part of the :class:`_orm.Load` interface and supports 

383 both method-chained and standalone operation. 

384 

385 examples:: 

386 

387 # selectin-load the "orders" collection on "User" 

388 select(User).options(selectinload(User.orders)) 

389 

390 # selectin-load Order.items and then Item.keywords 

391 select(Order).options( 

392 selectinload(Order.items).selectinload(Item.keywords) 

393 ) 

394 

395 # lazily load Order.items, but when Items are loaded, 

396 # selectin-load the keywords collection 

397 select(Order).options( 

398 lazyload(Order.items).selectinload(Item.keywords) 

399 ) 

400 

401 :param recursion_depth: optional int; when set to a positive integer 

402 in conjunction with a self-referential relationship, 

403 indicates "selectin" loading will continue that many levels deep 

404 automatically until no items are found. 

405 

406 .. note:: The :paramref:`_orm.selectinload.recursion_depth` option 

407 currently supports only self-referential relationships. There 

408 is not yet an option to automatically traverse recursive structures 

409 with more than one relationship involved. 

410 

411 Additionally, the :paramref:`_orm.selectinload.recursion_depth` 

412 parameter is new and experimental and should be treated as "alpha" 

413 status for the 2.0 series. 

414 

415 .. versionadded:: 2.0 added 

416 :paramref:`_orm.selectinload.recursion_depth` 

417 

418 

419 .. seealso:: 

420 

421 :ref:`loading_toplevel` 

422 

423 :ref:`selectin_eager_loading` 

424 

425 """ 

426 return self._set_relationship_strategy( 

427 attr, 

428 {"lazy": "selectin"}, 

429 opts={"recursion_depth": recursion_depth}, 

430 ) 

431 

432 def lazyload(self, attr: _AttrType) -> Self: 

433 """Indicate that the given attribute should be loaded using "lazy" 

434 loading. 

435 

436 This function is part of the :class:`_orm.Load` interface and supports 

437 both method-chained and standalone operation. 

438 

439 .. seealso:: 

440 

441 :ref:`loading_toplevel` 

442 

443 :ref:`lazy_loading` 

444 

445 """ 

446 return self._set_relationship_strategy(attr, {"lazy": "select"}) 

447 

448 def immediateload( 

449 self, 

450 attr: _AttrType, 

451 recursion_depth: Optional[int] = None, 

452 ) -> Self: 

453 """Indicate that the given attribute should be loaded using 

454 an immediate load with a per-attribute SELECT statement. 

455 

456 The load is achieved using the "lazyloader" strategy and does not 

457 fire off any additional eager loaders. 

458 

459 The :func:`.immediateload` option is superseded in general 

460 by the :func:`.selectinload` option, which performs the same task 

461 more efficiently by emitting a SELECT for all loaded objects. 

462 

463 This function is part of the :class:`_orm.Load` interface and supports 

464 both method-chained and standalone operation. 

465 

466 :param recursion_depth: optional int; when set to a positive integer 

467 in conjunction with a self-referential relationship, 

468 indicates "selectin" loading will continue that many levels deep 

469 automatically until no items are found. 

470 

471 .. note:: The :paramref:`_orm.immediateload.recursion_depth` option 

472 currently supports only self-referential relationships. There 

473 is not yet an option to automatically traverse recursive structures 

474 with more than one relationship involved. 

475 

476 .. warning:: This parameter is new and experimental and should be 

477 treated as "alpha" status 

478 

479 .. versionadded:: 2.0 added 

480 :paramref:`_orm.immediateload.recursion_depth` 

481 

482 

483 .. seealso:: 

484 

485 :ref:`loading_toplevel` 

486 

487 :ref:`selectin_eager_loading` 

488 

489 """ 

490 loader = self._set_relationship_strategy( 

491 attr, 

492 {"lazy": "immediate"}, 

493 opts={"recursion_depth": recursion_depth}, 

494 ) 

495 return loader 

496 

497 def noload(self, attr: _AttrType) -> Self: 

498 """Indicate that the given relationship attribute should remain 

499 unloaded. 

500 

501 The relationship attribute will return ``None`` when accessed without 

502 producing any loading effect. 

503 

504 This function is part of the :class:`_orm.Load` interface and supports 

505 both method-chained and standalone operation. 

506 

507 :func:`_orm.noload` applies to :func:`_orm.relationship` attributes 

508 only. 

509 

510 .. legacy:: The :func:`_orm.noload` option is **legacy**. As it 

511 forces collections to be empty, which invariably leads to 

512 non-intuitive and difficult to predict results. There are no 

513 legitimate uses for this option in modern SQLAlchemy. 

514 

515 .. seealso:: 

516 

517 :ref:`loading_toplevel` 

518 

519 """ 

520 

521 return self._set_relationship_strategy(attr, {"lazy": "noload"}) 

522 

523 def raiseload(self, attr: _AttrType, sql_only: bool = False) -> Self: 

524 """Indicate that the given attribute should raise an error if accessed. 

525 

526 A relationship attribute configured with :func:`_orm.raiseload` will 

527 raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The 

528 typical way this is useful is when an application is attempting to 

529 ensure that all relationship attributes that are accessed in a 

530 particular context would have been already loaded via eager loading. 

531 Instead of having to read through SQL logs to ensure lazy loads aren't 

532 occurring, this strategy will cause them to raise immediately. 

533 

534 :func:`_orm.raiseload` applies to :func:`_orm.relationship` attributes 

535 only. In order to apply raise-on-SQL behavior to a column-based 

536 attribute, use the :paramref:`.orm.defer.raiseload` parameter on the 

537 :func:`.defer` loader option. 

538 

539 :param sql_only: if True, raise only if the lazy load would emit SQL, 

540 but not if it is only checking the identity map, or determining that 

541 the related value should just be None due to missing keys. When False, 

542 the strategy will raise for all varieties of relationship loading. 

543 

544 This function is part of the :class:`_orm.Load` interface and supports 

545 both method-chained and standalone operation. 

546 

547 .. seealso:: 

548 

549 :ref:`loading_toplevel` 

550 

551 :ref:`prevent_lazy_with_raiseload` 

552 

553 :ref:`orm_queryguide_deferred_raiseload` 

554 

555 """ 

556 

557 return self._set_relationship_strategy( 

558 attr, {"lazy": "raise_on_sql" if sql_only else "raise"} 

559 ) 

560 

561 def defaultload(self, attr: _AttrType) -> Self: 

562 """Indicate an attribute should load using its predefined loader style. 

563 

564 The behavior of this loading option is to not change the current 

565 loading style of the attribute, meaning that the previously configured 

566 one is used or, if no previous style was selected, the default 

567 loading will be used. 

568 

569 This method is used to link to other loader options further into 

570 a chain of attributes without altering the loader style of the links 

571 along the chain. For example, to set joined eager loading for an 

572 element of an element:: 

573 

574 session.query(MyClass).options( 

575 defaultload(MyClass.someattribute).joinedload( 

576 MyOtherClass.someotherattribute 

577 ) 

578 ) 

579 

580 :func:`.defaultload` is also useful for setting column-level options on 

581 a related class, namely that of :func:`.defer` and :func:`.undefer`:: 

582 

583 session.scalars( 

584 select(MyClass).options( 

585 defaultload(MyClass.someattribute) 

586 .defer("some_column") 

587 .undefer("some_other_column") 

588 ) 

589 ) 

590 

591 .. seealso:: 

592 

593 :ref:`orm_queryguide_relationship_sub_options` 

594 

595 :meth:`_orm.Load.options` 

596 

597 """ 

598 return self._set_relationship_strategy(attr, None) 

599 

600 def defer(self, key: _AttrType, raiseload: bool = False) -> Self: 

601 r"""Indicate that the given column-oriented attribute should be 

602 deferred, e.g. not loaded until accessed. 

603 

604 This function is part of the :class:`_orm.Load` interface and supports 

605 both method-chained and standalone operation. 

606 

607 e.g.:: 

608 

609 from sqlalchemy.orm import defer 

610 

611 session.query(MyClass).options( 

612 defer(MyClass.attribute_one), 

613 defer(MyClass.attribute_two) 

614 ) 

615 

616 To specify a deferred load of an attribute on a related class, 

617 the path can be specified one token at a time, specifying the loading 

618 style for each link along the chain. To leave the loading style 

619 for a link unchanged, use :func:`_orm.defaultload`:: 

620 

621 session.query(MyClass).options( 

622 defaultload(MyClass.someattr).defer(RelatedClass.some_column) 

623 ) 

624 

625 Multiple deferral options related to a relationship can be bundled 

626 at once using :meth:`_orm.Load.options`:: 

627 

628 

629 select(MyClass).options( 

630 defaultload(MyClass.someattr).options( 

631 defer(RelatedClass.some_column), 

632 defer(RelatedClass.some_other_column), 

633 defer(RelatedClass.another_column) 

634 ) 

635 ) 

636 

637 :param key: Attribute to be deferred. 

638 

639 :param raiseload: raise :class:`.InvalidRequestError` rather than 

640 lazy loading a value when the deferred attribute is accessed. Used 

641 to prevent unwanted SQL from being emitted. 

642 

643 .. versionadded:: 1.4 

644 

645 .. seealso:: 

646 

647 :ref:`orm_queryguide_column_deferral` - in the 

648 :ref:`queryguide_toplevel` 

649 

650 :func:`_orm.load_only` 

651 

652 :func:`_orm.undefer` 

653 

654 """ 

655 strategy = {"deferred": True, "instrument": True} 

656 if raiseload: 

657 strategy["raiseload"] = True 

658 return self._set_column_strategy((key,), strategy) 

659 

660 def undefer(self, key: _AttrType) -> Self: 

661 r"""Indicate that the given column-oriented attribute should be 

662 undeferred, e.g. specified within the SELECT statement of the entity 

663 as a whole. 

664 

665 The column being undeferred is typically set up on the mapping as a 

666 :func:`.deferred` attribute. 

667 

668 This function is part of the :class:`_orm.Load` interface and supports 

669 both method-chained and standalone operation. 

670 

671 Examples:: 

672 

673 # undefer two columns 

674 session.query(MyClass).options( 

675 undefer(MyClass.col1), undefer(MyClass.col2) 

676 ) 

677 

678 # undefer all columns specific to a single class using Load + * 

679 session.query(MyClass, MyOtherClass).options( 

680 Load(MyClass).undefer("*") 

681 ) 

682 

683 # undefer a column on a related object 

684 select(MyClass).options( 

685 defaultload(MyClass.items).undefer(MyClass.text) 

686 ) 

687 

688 :param key: Attribute to be undeferred. 

689 

690 .. seealso:: 

691 

692 :ref:`orm_queryguide_column_deferral` - in the 

693 :ref:`queryguide_toplevel` 

694 

695 :func:`_orm.defer` 

696 

697 :func:`_orm.undefer_group` 

698 

699 """ 

700 return self._set_column_strategy( 

701 (key,), {"deferred": False, "instrument": True} 

702 ) 

703 

704 def undefer_group(self, name: str) -> Self: 

705 """Indicate that columns within the given deferred group name should be 

706 undeferred. 

707 

708 The columns being undeferred are set up on the mapping as 

709 :func:`.deferred` attributes and include a "group" name. 

710 

711 E.g:: 

712 

713 session.query(MyClass).options(undefer_group("large_attrs")) 

714 

715 To undefer a group of attributes on a related entity, the path can be 

716 spelled out using relationship loader options, such as 

717 :func:`_orm.defaultload`:: 

718 

719 select(MyClass).options( 

720 defaultload("someattr").undefer_group("large_attrs") 

721 ) 

722 

723 .. seealso:: 

724 

725 :ref:`orm_queryguide_column_deferral` - in the 

726 :ref:`queryguide_toplevel` 

727 

728 :func:`_orm.defer` 

729 

730 :func:`_orm.undefer` 

731 

732 """ 

733 return self._set_column_strategy( 

734 (_WILDCARD_TOKEN,), None, {f"undefer_group_{name}": True} 

735 ) 

736 

737 def with_expression( 

738 self, 

739 key: _AttrType, 

740 expression: _ColumnExpressionArgument[Any], 

741 ) -> Self: 

742 r"""Apply an ad-hoc SQL expression to a "deferred expression" 

743 attribute. 

744 

745 This option is used in conjunction with the 

746 :func:`_orm.query_expression` mapper-level construct that indicates an 

747 attribute which should be the target of an ad-hoc SQL expression. 

748 

749 E.g.:: 

750 

751 stmt = select(SomeClass).options( 

752 with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y) 

753 ) 

754 

755 .. versionadded:: 1.2 

756 

757 :param key: Attribute to be populated 

758 

759 :param expr: SQL expression to be applied to the attribute. 

760 

761 .. seealso:: 

762 

763 :ref:`orm_queryguide_with_expression` - background and usage 

764 examples 

765 

766 """ 

767 

768 expression = _orm_full_deannotate( 

769 coercions.expect(roles.LabeledColumnExprRole, expression) 

770 ) 

771 

772 return self._set_column_strategy( 

773 (key,), {"query_expression": True}, extra_criteria=(expression,) 

774 ) 

775 

776 def selectin_polymorphic(self, classes: Iterable[Type[Any]]) -> Self: 

777 """Indicate an eager load should take place for all attributes 

778 specific to a subclass. 

779 

780 This uses an additional SELECT with IN against all matched primary 

781 key values, and is the per-query analogue to the ``"selectin"`` 

782 setting on the :paramref:`.mapper.polymorphic_load` parameter. 

783 

784 .. versionadded:: 1.2 

785 

786 .. seealso:: 

787 

788 :ref:`polymorphic_selectin` 

789 

790 """ 

791 self = self._set_class_strategy( 

792 {"selectinload_polymorphic": True}, 

793 opts={ 

794 "entities": tuple( 

795 sorted((inspect(cls) for cls in classes), key=id) 

796 ) 

797 }, 

798 ) 

799 return self 

800 

801 @overload 

802 def _coerce_strat(self, strategy: _StrategySpec) -> _StrategyKey: ... 

803 

804 @overload 

805 def _coerce_strat(self, strategy: Literal[None]) -> None: ... 

806 

807 def _coerce_strat( 

808 self, strategy: Optional[_StrategySpec] 

809 ) -> Optional[_StrategyKey]: 

810 if strategy is not None: 

811 strategy_key = tuple(sorted(strategy.items())) 

812 else: 

813 strategy_key = None 

814 return strategy_key 

815 

816 @_generative 

817 def _set_relationship_strategy( 

818 self, 

819 attr: _AttrType, 

820 strategy: Optional[_StrategySpec], 

821 propagate_to_loaders: bool = True, 

822 opts: Optional[_OptsType] = None, 

823 _reconcile_to_other: Optional[bool] = None, 

824 ) -> Self: 

825 strategy_key = self._coerce_strat(strategy) 

826 

827 self._clone_for_bind_strategy( 

828 (attr,), 

829 strategy_key, 

830 _RELATIONSHIP_TOKEN, 

831 opts=opts, 

832 propagate_to_loaders=propagate_to_loaders, 

833 reconcile_to_other=_reconcile_to_other, 

834 ) 

835 return self 

836 

837 @_generative 

838 def _set_column_strategy( 

839 self, 

840 attrs: Tuple[_AttrType, ...], 

841 strategy: Optional[_StrategySpec], 

842 opts: Optional[_OptsType] = None, 

843 extra_criteria: Optional[Tuple[Any, ...]] = None, 

844 ) -> Self: 

845 strategy_key = self._coerce_strat(strategy) 

846 

847 self._clone_for_bind_strategy( 

848 attrs, 

849 strategy_key, 

850 _COLUMN_TOKEN, 

851 opts=opts, 

852 attr_group=attrs, 

853 extra_criteria=extra_criteria, 

854 ) 

855 return self 

856 

857 @_generative 

858 def _set_generic_strategy( 

859 self, 

860 attrs: Tuple[_AttrType, ...], 

861 strategy: _StrategySpec, 

862 _reconcile_to_other: Optional[bool] = None, 

863 ) -> Self: 

864 strategy_key = self._coerce_strat(strategy) 

865 self._clone_for_bind_strategy( 

866 attrs, 

867 strategy_key, 

868 None, 

869 propagate_to_loaders=True, 

870 reconcile_to_other=_reconcile_to_other, 

871 ) 

872 return self 

873 

874 @_generative 

875 def _set_class_strategy( 

876 self, strategy: _StrategySpec, opts: _OptsType 

877 ) -> Self: 

878 strategy_key = self._coerce_strat(strategy) 

879 

880 self._clone_for_bind_strategy(None, strategy_key, None, opts=opts) 

881 return self 

882 

883 def _apply_to_parent(self, parent: Load) -> None: 

884 """apply this :class:`_orm._AbstractLoad` object as a sub-option o 

885 a :class:`_orm.Load` object. 

886 

887 Implementation is provided by subclasses. 

888 

889 """ 

890 raise NotImplementedError() 

891 

892 def options(self, *opts: _AbstractLoad) -> Self: 

893 r"""Apply a series of options as sub-options to this 

894 :class:`_orm._AbstractLoad` object. 

895 

896 Implementation is provided by subclasses. 

897 

898 """ 

899 raise NotImplementedError() 

900 

901 def _clone_for_bind_strategy( 

902 self, 

903 attrs: Optional[Tuple[_AttrType, ...]], 

904 strategy: Optional[_StrategyKey], 

905 wildcard_key: Optional[_WildcardKeyType], 

906 opts: Optional[_OptsType] = None, 

907 attr_group: Optional[_AttrGroupType] = None, 

908 propagate_to_loaders: bool = True, 

909 reconcile_to_other: Optional[bool] = None, 

910 extra_criteria: Optional[Tuple[Any, ...]] = None, 

911 ) -> Self: 

912 raise NotImplementedError() 

913 

914 def process_compile_state_replaced_entities( 

915 self, 

916 compile_state: ORMCompileState, 

917 mapper_entities: Sequence[_MapperEntity], 

918 ) -> None: 

919 if not compile_state.compile_options._enable_eagerloads: 

920 return 

921 

922 # process is being run here so that the options given are validated 

923 # against what the lead entities were, as well as to accommodate 

924 # for the entities having been replaced with equivalents 

925 self._process( 

926 compile_state, 

927 mapper_entities, 

928 not bool(compile_state.current_path), 

929 ) 

930 

931 def process_compile_state(self, compile_state: ORMCompileState) -> None: 

932 if not compile_state.compile_options._enable_eagerloads: 

933 return 

934 

935 self._process( 

936 compile_state, 

937 compile_state._lead_mapper_entities, 

938 not bool(compile_state.current_path) 

939 and not compile_state.compile_options._for_refresh_state, 

940 ) 

941 

942 def _process( 

943 self, 

944 compile_state: ORMCompileState, 

945 mapper_entities: Sequence[_MapperEntity], 

946 raiseerr: bool, 

947 ) -> None: 

948 """implemented by subclasses""" 

949 raise NotImplementedError() 

950 

951 @classmethod 

952 def _chop_path( 

953 cls, 

954 to_chop: _PathRepresentation, 

955 path: PathRegistry, 

956 debug: bool = False, 

957 ) -> Optional[_PathRepresentation]: 

958 i = -1 

959 

960 for i, (c_token, p_token) in enumerate( 

961 zip(to_chop, path.natural_path) 

962 ): 

963 if isinstance(c_token, str): 

964 if i == 0 and ( 

965 c_token.endswith(f":{_DEFAULT_TOKEN}") 

966 or c_token.endswith(f":{_WILDCARD_TOKEN}") 

967 ): 

968 return to_chop 

969 elif ( 

970 c_token != f"{_RELATIONSHIP_TOKEN}:{_WILDCARD_TOKEN}" 

971 and c_token != p_token.key # type: ignore 

972 ): 

973 return None 

974 

975 if c_token is p_token: 

976 continue 

977 elif ( 

978 isinstance(c_token, InspectionAttr) 

979 and insp_is_mapper(c_token) 

980 and insp_is_mapper(p_token) 

981 and c_token.isa(p_token) 

982 ): 

983 continue 

984 

985 else: 

986 return None 

987 return to_chop[i + 1 :] 

988 

989 

990class Load(_AbstractLoad): 

991 """Represents loader options which modify the state of a 

992 ORM-enabled :class:`_sql.Select` or a legacy :class:`_query.Query` in 

993 order to affect how various mapped attributes are loaded. 

994 

995 The :class:`_orm.Load` object is in most cases used implicitly behind the 

996 scenes when one makes use of a query option like :func:`_orm.joinedload`, 

997 :func:`_orm.defer`, or similar. It typically is not instantiated directly 

998 except for in some very specific cases. 

999 

1000 .. seealso:: 

1001 

1002 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an 

1003 example where direct use of :class:`_orm.Load` may be useful 

1004 

1005 """ 

1006 

1007 __slots__ = ( 

1008 "path", 

1009 "context", 

1010 "additional_source_entities", 

1011 ) 

1012 

1013 _traverse_internals = [ 

1014 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), 

1015 ( 

1016 "context", 

1017 visitors.InternalTraversal.dp_has_cache_key_list, 

1018 ), 

1019 ("propagate_to_loaders", visitors.InternalTraversal.dp_boolean), 

1020 ( 

1021 "additional_source_entities", 

1022 visitors.InternalTraversal.dp_has_cache_key_list, 

1023 ), 

1024 ] 

1025 _cache_key_traversal = None 

1026 

1027 path: PathRegistry 

1028 context: Tuple[_LoadElement, ...] 

1029 additional_source_entities: Tuple[_InternalEntityType[Any], ...] 

1030 

1031 def __init__(self, entity: _EntityType[Any]): 

1032 insp = cast("Union[Mapper[Any], AliasedInsp[Any]]", inspect(entity)) 

1033 insp._post_inspect 

1034 

1035 self.path = insp._path_registry 

1036 self.context = () 

1037 self.propagate_to_loaders = False 

1038 self.additional_source_entities = () 

1039 

1040 def __str__(self) -> str: 

1041 return f"Load({self.path[0]})" 

1042 

1043 @classmethod 

1044 def _construct_for_existing_path( 

1045 cls, path: AbstractEntityRegistry 

1046 ) -> Load: 

1047 load = cls.__new__(cls) 

1048 load.path = path 

1049 load.context = () 

1050 load.propagate_to_loaders = False 

1051 load.additional_source_entities = () 

1052 return load 

1053 

1054 def _adapt_cached_option_to_uncached_option( 

1055 self, context: QueryContext, uncached_opt: ORMOption 

1056 ) -> ORMOption: 

1057 if uncached_opt is self: 

1058 return self 

1059 return self._adjust_for_extra_criteria(context) 

1060 

1061 def _prepend_path(self, path: PathRegistry) -> Load: 

1062 cloned = self._clone() 

1063 cloned.context = tuple( 

1064 element._prepend_path(path) for element in self.context 

1065 ) 

1066 return cloned 

1067 

1068 def _adjust_for_extra_criteria(self, context: QueryContext) -> Load: 

1069 """Apply the current bound parameters in a QueryContext to all 

1070 occurrences "extra_criteria" stored within this ``Load`` object, 

1071 returning a new instance of this ``Load`` object. 

1072 

1073 """ 

1074 

1075 # avoid generating cache keys for the queries if we don't 

1076 # actually have any extra_criteria options, which is the 

1077 # common case 

1078 for value in self.context: 

1079 if value._extra_criteria: 

1080 break 

1081 else: 

1082 return self 

1083 

1084 replacement_cache_key = context.query._generate_cache_key() 

1085 

1086 if replacement_cache_key is None: 

1087 return self 

1088 

1089 orig_query = context.compile_state.select_statement 

1090 orig_cache_key = orig_query._generate_cache_key() 

1091 assert orig_cache_key is not None 

1092 

1093 def process( 

1094 opt: _LoadElement, 

1095 replacement_cache_key: CacheKey, 

1096 orig_cache_key: CacheKey, 

1097 ) -> _LoadElement: 

1098 cloned_opt = opt._clone() 

1099 

1100 cloned_opt._extra_criteria = tuple( 

1101 replacement_cache_key._apply_params_to_element( 

1102 orig_cache_key, crit 

1103 ) 

1104 for crit in cloned_opt._extra_criteria 

1105 ) 

1106 

1107 return cloned_opt 

1108 

1109 cloned = self._clone() 

1110 cloned.context = tuple( 

1111 ( 

1112 process(value, replacement_cache_key, orig_cache_key) 

1113 if value._extra_criteria 

1114 else value 

1115 ) 

1116 for value in self.context 

1117 ) 

1118 return cloned 

1119 

1120 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr): 

1121 """called at process time to allow adjustment of the root 

1122 entity inside of _LoadElement objects. 

1123 

1124 """ 

1125 path = self.path 

1126 

1127 ezero = None 

1128 for ent in mapper_entities: 

1129 ezero = ent.entity_zero 

1130 if ezero and orm_util._entity_corresponds_to( 

1131 # technically this can be a token also, but this is 

1132 # safe to pass to _entity_corresponds_to() 

1133 ezero, 

1134 cast("_InternalEntityType[Any]", path[0]), 

1135 ): 

1136 return ezero 

1137 

1138 return None 

1139 

1140 def _process( 

1141 self, 

1142 compile_state: ORMCompileState, 

1143 mapper_entities: Sequence[_MapperEntity], 

1144 raiseerr: bool, 

1145 ) -> None: 

1146 reconciled_lead_entity = self._reconcile_query_entities_with_us( 

1147 mapper_entities, raiseerr 

1148 ) 

1149 

1150 # if the context has a current path, this is a lazy load 

1151 has_current_path = bool(compile_state.compile_options._current_path) 

1152 

1153 for loader in self.context: 

1154 # issue #11292 

1155 # historically, propagate_to_loaders was only considered at 

1156 # object loading time, whether or not to carry along options 

1157 # onto an object's loaded state where it would be used by lazyload. 

1158 # however, the defaultload() option needs to propagate in case 

1159 # its sub-options propagate_to_loaders, but its sub-options 

1160 # that dont propagate should not be applied for lazy loaders. 

1161 # so we check again 

1162 if has_current_path and not loader.propagate_to_loaders: 

1163 continue 

1164 loader.process_compile_state( 

1165 self, 

1166 compile_state, 

1167 mapper_entities, 

1168 reconciled_lead_entity, 

1169 raiseerr, 

1170 ) 

1171 

1172 def _apply_to_parent(self, parent: Load) -> None: 

1173 """apply this :class:`_orm.Load` object as a sub-option of another 

1174 :class:`_orm.Load` object. 

1175 

1176 This method is used by the :meth:`_orm.Load.options` method. 

1177 

1178 """ 

1179 cloned = self._generate() 

1180 

1181 assert cloned.propagate_to_loaders == self.propagate_to_loaders 

1182 

1183 if not any( 

1184 orm_util._entity_corresponds_to_use_path_impl( 

1185 elem, cloned.path.odd_element(0) 

1186 ) 

1187 for elem in (parent.path.odd_element(-1),) 

1188 + parent.additional_source_entities 

1189 ): 

1190 if len(cloned.path) > 1: 

1191 attrname = cloned.path[1] 

1192 parent_entity = cloned.path[0] 

1193 else: 

1194 attrname = cloned.path[0] 

1195 parent_entity = cloned.path[0] 

1196 _raise_for_does_not_link(parent.path, attrname, parent_entity) 

1197 

1198 cloned.path = PathRegistry.coerce(parent.path[0:-1] + cloned.path[:]) 

1199 

1200 if self.context: 

1201 cloned.context = tuple( 

1202 value._prepend_path_from(parent) for value in self.context 

1203 ) 

1204 

1205 if cloned.context: 

1206 parent.context += cloned.context 

1207 parent.additional_source_entities += ( 

1208 cloned.additional_source_entities 

1209 ) 

1210 

1211 @_generative 

1212 def options(self, *opts: _AbstractLoad) -> Self: 

1213 r"""Apply a series of options as sub-options to this 

1214 :class:`_orm.Load` 

1215 object. 

1216 

1217 E.g.:: 

1218 

1219 query = session.query(Author) 

1220 query = query.options( 

1221 joinedload(Author.book).options( 

1222 load_only(Book.summary, Book.excerpt), 

1223 joinedload(Book.citations).options( 

1224 joinedload(Citation.author) 

1225 ) 

1226 ) 

1227 ) 

1228 

1229 :param \*opts: A series of loader option objects (ultimately 

1230 :class:`_orm.Load` objects) which should be applied to the path 

1231 specified by this :class:`_orm.Load` object. 

1232 

1233 .. versionadded:: 1.3.6 

1234 

1235 .. seealso:: 

1236 

1237 :func:`.defaultload` 

1238 

1239 :ref:`orm_queryguide_relationship_sub_options` 

1240 

1241 """ 

1242 for opt in opts: 

1243 try: 

1244 opt._apply_to_parent(self) 

1245 except AttributeError as ae: 

1246 if not isinstance(opt, _AbstractLoad): 

1247 raise sa_exc.ArgumentError( 

1248 f"Loader option {opt} is not compatible with the " 

1249 "Load.options() method." 

1250 ) from ae 

1251 else: 

1252 raise 

1253 return self 

1254 

1255 def _clone_for_bind_strategy( 

1256 self, 

1257 attrs: Optional[Tuple[_AttrType, ...]], 

1258 strategy: Optional[_StrategyKey], 

1259 wildcard_key: Optional[_WildcardKeyType], 

1260 opts: Optional[_OptsType] = None, 

1261 attr_group: Optional[_AttrGroupType] = None, 

1262 propagate_to_loaders: bool = True, 

1263 reconcile_to_other: Optional[bool] = None, 

1264 extra_criteria: Optional[Tuple[Any, ...]] = None, 

1265 ) -> Self: 

1266 # for individual strategy that needs to propagate, set the whole 

1267 # Load container to also propagate, so that it shows up in 

1268 # InstanceState.load_options 

1269 if propagate_to_loaders: 

1270 self.propagate_to_loaders = True 

1271 

1272 if self.path.is_token: 

1273 raise sa_exc.ArgumentError( 

1274 "Wildcard token cannot be followed by another entity" 

1275 ) 

1276 

1277 elif path_is_property(self.path): 

1278 # re-use the lookup which will raise a nicely formatted 

1279 # LoaderStrategyException 

1280 if strategy: 

1281 self.path.prop._strategy_lookup(self.path.prop, strategy[0]) 

1282 else: 

1283 raise sa_exc.ArgumentError( 

1284 f"Mapped attribute '{self.path.prop}' does not " 

1285 "refer to a mapped entity" 

1286 ) 

1287 

1288 if attrs is None: 

1289 load_element = _ClassStrategyLoad.create( 

1290 self.path, 

1291 None, 

1292 strategy, 

1293 wildcard_key, 

1294 opts, 

1295 propagate_to_loaders, 

1296 attr_group=attr_group, 

1297 reconcile_to_other=reconcile_to_other, 

1298 extra_criteria=extra_criteria, 

1299 ) 

1300 if load_element: 

1301 self.context += (load_element,) 

1302 assert opts is not None 

1303 self.additional_source_entities += cast( 

1304 "Tuple[_InternalEntityType[Any]]", opts["entities"] 

1305 ) 

1306 

1307 else: 

1308 for attr in attrs: 

1309 if isinstance(attr, str): 

1310 load_element = _TokenStrategyLoad.create( 

1311 self.path, 

1312 attr, 

1313 strategy, 

1314 wildcard_key, 

1315 opts, 

1316 propagate_to_loaders, 

1317 attr_group=attr_group, 

1318 reconcile_to_other=reconcile_to_other, 

1319 extra_criteria=extra_criteria, 

1320 ) 

1321 else: 

1322 load_element = _AttributeStrategyLoad.create( 

1323 self.path, 

1324 attr, 

1325 strategy, 

1326 wildcard_key, 

1327 opts, 

1328 propagate_to_loaders, 

1329 attr_group=attr_group, 

1330 reconcile_to_other=reconcile_to_other, 

1331 extra_criteria=extra_criteria, 

1332 ) 

1333 

1334 if load_element: 

1335 # for relationship options, update self.path on this Load 

1336 # object with the latest path. 

1337 if wildcard_key is _RELATIONSHIP_TOKEN: 

1338 self.path = load_element.path 

1339 self.context += (load_element,) 

1340 

1341 # this seems to be effective for selectinloader, 

1342 # giving the extra match to one more level deep. 

1343 # but does not work for immediateloader, which still 

1344 # must add additional options at load time 

1345 if load_element.local_opts.get("recursion_depth", False): 

1346 r1 = load_element._recurse() 

1347 self.context += (r1,) 

1348 

1349 return self 

1350 

1351 def __getstate__(self): 

1352 d = self._shallow_to_dict() 

1353 d["path"] = self.path.serialize() 

1354 return d 

1355 

1356 def __setstate__(self, state): 

1357 state["path"] = PathRegistry.deserialize(state["path"]) 

1358 self._shallow_from_dict(state) 

1359 

1360 

1361class _WildcardLoad(_AbstractLoad): 

1362 """represent a standalone '*' load operation""" 

1363 

1364 __slots__ = ("strategy", "path", "local_opts") 

1365 

1366 _traverse_internals = [ 

1367 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), 

1368 ("path", visitors.ExtendedInternalTraversal.dp_plain_obj), 

1369 ( 

1370 "local_opts", 

1371 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1372 ), 

1373 ] 

1374 cache_key_traversal: _CacheKeyTraversalType = None 

1375 

1376 strategy: Optional[Tuple[Any, ...]] 

1377 local_opts: _OptsType 

1378 path: Union[Tuple[()], Tuple[str]] 

1379 propagate_to_loaders = False 

1380 

1381 def __init__(self) -> None: 

1382 self.path = () 

1383 self.strategy = None 

1384 self.local_opts = util.EMPTY_DICT 

1385 

1386 def _clone_for_bind_strategy( 

1387 self, 

1388 attrs, 

1389 strategy, 

1390 wildcard_key, 

1391 opts=None, 

1392 attr_group=None, 

1393 propagate_to_loaders=True, 

1394 reconcile_to_other=None, 

1395 extra_criteria=None, 

1396 ): 

1397 assert attrs is not None 

1398 attr = attrs[0] 

1399 assert ( 

1400 wildcard_key 

1401 and isinstance(attr, str) 

1402 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) 

1403 ) 

1404 

1405 attr = f"{wildcard_key}:{attr}" 

1406 

1407 self.strategy = strategy 

1408 self.path = (attr,) 

1409 if opts: 

1410 self.local_opts = util.immutabledict(opts) 

1411 

1412 assert extra_criteria is None 

1413 

1414 def options(self, *opts: _AbstractLoad) -> Self: 

1415 raise NotImplementedError("Star option does not support sub-options") 

1416 

1417 def _apply_to_parent(self, parent: Load) -> None: 

1418 """apply this :class:`_orm._WildcardLoad` object as a sub-option of 

1419 a :class:`_orm.Load` object. 

1420 

1421 This method is used by the :meth:`_orm.Load.options` method. Note 

1422 that :class:`_orm.WildcardLoad` itself can't have sub-options, but 

1423 it may be used as the sub-option of a :class:`_orm.Load` object. 

1424 

1425 """ 

1426 assert self.path 

1427 attr = self.path[0] 

1428 if attr.endswith(_DEFAULT_TOKEN): 

1429 attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}" 

1430 

1431 effective_path = cast(AbstractEntityRegistry, parent.path).token(attr) 

1432 

1433 assert effective_path.is_token 

1434 

1435 loader = _TokenStrategyLoad.create( 

1436 effective_path, 

1437 None, 

1438 self.strategy, 

1439 None, 

1440 self.local_opts, 

1441 self.propagate_to_loaders, 

1442 ) 

1443 

1444 parent.context += (loader,) 

1445 

1446 def _process(self, compile_state, mapper_entities, raiseerr): 

1447 is_refresh = compile_state.compile_options._for_refresh_state 

1448 

1449 if is_refresh and not self.propagate_to_loaders: 

1450 return 

1451 

1452 entities = [ent.entity_zero for ent in mapper_entities] 

1453 current_path = compile_state.current_path 

1454 

1455 start_path: _PathRepresentation = self.path 

1456 

1457 if current_path: 

1458 # TODO: no cases in test suite where we actually get 

1459 # None back here 

1460 new_path = self._chop_path(start_path, current_path) 

1461 if new_path is None: 

1462 return 

1463 

1464 # chop_path does not actually "chop" a wildcard token path, 

1465 # just returns it 

1466 assert new_path == start_path 

1467 

1468 # start_path is a single-token tuple 

1469 assert start_path and len(start_path) == 1 

1470 

1471 token = start_path[0] 

1472 assert isinstance(token, str) 

1473 entity = self._find_entity_basestring(entities, token, raiseerr) 

1474 

1475 if not entity: 

1476 return 

1477 

1478 path_element = entity 

1479 

1480 # transfer our entity-less state into a Load() object 

1481 # with a real entity path. Start with the lead entity 

1482 # we just located, then go through the rest of our path 

1483 # tokens and populate into the Load(). 

1484 

1485 assert isinstance(token, str) 

1486 loader = _TokenStrategyLoad.create( 

1487 path_element._path_registry, 

1488 token, 

1489 self.strategy, 

1490 None, 

1491 self.local_opts, 

1492 self.propagate_to_loaders, 

1493 raiseerr=raiseerr, 

1494 ) 

1495 if not loader: 

1496 return 

1497 

1498 assert loader.path.is_token 

1499 

1500 # don't pass a reconciled lead entity here 

1501 loader.process_compile_state( 

1502 self, compile_state, mapper_entities, None, raiseerr 

1503 ) 

1504 

1505 return loader 

1506 

1507 def _find_entity_basestring( 

1508 self, 

1509 entities: Iterable[_InternalEntityType[Any]], 

1510 token: str, 

1511 raiseerr: bool, 

1512 ) -> Optional[_InternalEntityType[Any]]: 

1513 if token.endswith(f":{_WILDCARD_TOKEN}"): 

1514 if len(list(entities)) != 1: 

1515 if raiseerr: 

1516 raise sa_exc.ArgumentError( 

1517 "Can't apply wildcard ('*') or load_only() " 

1518 f"loader option to multiple entities " 

1519 f"{', '.join(str(ent) for ent in entities)}. Specify " 

1520 "loader options for each entity individually, such as " 

1521 f"""{ 

1522 ", ".join( 

1523 f"Load({ent}).some_option('*')" 

1524 for ent in entities 

1525 ) 

1526 }.""" 

1527 ) 

1528 elif token.endswith(_DEFAULT_TOKEN): 

1529 raiseerr = False 

1530 

1531 for ent in entities: 

1532 # return only the first _MapperEntity when searching 

1533 # based on string prop name. Ideally object 

1534 # attributes are used to specify more exactly. 

1535 return ent 

1536 else: 

1537 if raiseerr: 

1538 raise sa_exc.ArgumentError( 

1539 "Query has only expression-based entities - " 

1540 f'can\'t find property named "{token}".' 

1541 ) 

1542 else: 

1543 return None 

1544 

1545 def __getstate__(self) -> Dict[str, Any]: 

1546 d = self._shallow_to_dict() 

1547 return d 

1548 

1549 def __setstate__(self, state: Dict[str, Any]) -> None: 

1550 self._shallow_from_dict(state) 

1551 

1552 

1553class _LoadElement( 

1554 cache_key.HasCacheKey, traversals.HasShallowCopy, visitors.Traversible 

1555): 

1556 """represents strategy information to select for a LoaderStrategy 

1557 and pass options to it. 

1558 

1559 :class:`._LoadElement` objects provide the inner datastructure 

1560 stored by a :class:`_orm.Load` object and are also the object passed 

1561 to methods like :meth:`.LoaderStrategy.setup_query`. 

1562 

1563 .. versionadded:: 2.0 

1564 

1565 """ 

1566 

1567 __slots__ = ( 

1568 "path", 

1569 "strategy", 

1570 "propagate_to_loaders", 

1571 "local_opts", 

1572 "_extra_criteria", 

1573 "_reconcile_to_other", 

1574 ) 

1575 __visit_name__ = "load_element" 

1576 

1577 _traverse_internals = [ 

1578 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key), 

1579 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj), 

1580 ( 

1581 "local_opts", 

1582 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1583 ), 

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

1585 ("propagate_to_loaders", visitors.InternalTraversal.dp_plain_obj), 

1586 ("_reconcile_to_other", visitors.InternalTraversal.dp_plain_obj), 

1587 ] 

1588 _cache_key_traversal = None 

1589 

1590 _extra_criteria: Tuple[Any, ...] 

1591 

1592 _reconcile_to_other: Optional[bool] 

1593 strategy: Optional[_StrategyKey] 

1594 path: PathRegistry 

1595 propagate_to_loaders: bool 

1596 

1597 local_opts: util.immutabledict[str, Any] 

1598 

1599 is_token_strategy: bool 

1600 is_class_strategy: bool 

1601 

1602 def __hash__(self) -> int: 

1603 return id(self) 

1604 

1605 def __eq__(self, other): 

1606 return traversals.compare(self, other) 

1607 

1608 @property 

1609 def is_opts_only(self) -> bool: 

1610 return bool(self.local_opts and self.strategy is None) 

1611 

1612 def _clone(self, **kw: Any) -> _LoadElement: 

1613 cls = self.__class__ 

1614 s = cls.__new__(cls) 

1615 

1616 self._shallow_copy_to(s) 

1617 return s 

1618 

1619 def _update_opts(self, **kw: Any) -> _LoadElement: 

1620 new = self._clone() 

1621 new.local_opts = new.local_opts.union(kw) 

1622 return new 

1623 

1624 def __getstate__(self) -> Dict[str, Any]: 

1625 d = self._shallow_to_dict() 

1626 d["path"] = self.path.serialize() 

1627 return d 

1628 

1629 def __setstate__(self, state: Dict[str, Any]) -> None: 

1630 state["path"] = PathRegistry.deserialize(state["path"]) 

1631 self._shallow_from_dict(state) 

1632 

1633 def _raise_for_no_match(self, parent_loader, mapper_entities): 

1634 path = parent_loader.path 

1635 

1636 found_entities = False 

1637 for ent in mapper_entities: 

1638 ezero = ent.entity_zero 

1639 if ezero: 

1640 found_entities = True 

1641 break 

1642 

1643 if not found_entities: 

1644 raise sa_exc.ArgumentError( 

1645 "Query has only expression-based entities; " 

1646 f"attribute loader options for {path[0]} can't " 

1647 "be applied here." 

1648 ) 

1649 else: 

1650 raise sa_exc.ArgumentError( 

1651 f"Mapped class {path[0]} does not apply to any of the " 

1652 f"root entities in this query, e.g. " 

1653 f"""{ 

1654 ", ".join( 

1655 str(x.entity_zero) 

1656 for x in mapper_entities if x.entity_zero 

1657 )}. Please """ 

1658 "specify the full path " 

1659 "from one of the root entities to the target " 

1660 "attribute. " 

1661 ) 

1662 

1663 def _adjust_effective_path_for_current_path( 

1664 self, effective_path: PathRegistry, current_path: PathRegistry 

1665 ) -> Optional[PathRegistry]: 

1666 """receives the 'current_path' entry from an :class:`.ORMCompileState` 

1667 instance, which is set during lazy loads and secondary loader strategy 

1668 loads, and adjusts the given path to be relative to the 

1669 current_path. 

1670 

1671 E.g. given a loader path and current path:: 

1672 

1673 lp: User -> orders -> Order -> items -> Item -> keywords -> Keyword 

1674 

1675 cp: User -> orders -> Order -> items 

1676 

1677 The adjusted path would be:: 

1678 

1679 Item -> keywords -> Keyword 

1680 

1681 

1682 """ 

1683 chopped_start_path = Load._chop_path( 

1684 effective_path.natural_path, current_path 

1685 ) 

1686 if not chopped_start_path: 

1687 return None 

1688 

1689 tokens_removed_from_start_path = len(effective_path) - len( 

1690 chopped_start_path 

1691 ) 

1692 

1693 loader_lead_path_element = self.path[tokens_removed_from_start_path] 

1694 

1695 effective_path = PathRegistry.coerce( 

1696 (loader_lead_path_element,) + chopped_start_path[1:] 

1697 ) 

1698 

1699 return effective_path 

1700 

1701 def _init_path( 

1702 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1703 ): 

1704 """Apply ORM attributes and/or wildcard to an existing path, producing 

1705 a new path. 

1706 

1707 This method is used within the :meth:`.create` method to initialize 

1708 a :class:`._LoadElement` object. 

1709 

1710 """ 

1711 raise NotImplementedError() 

1712 

1713 def _prepare_for_compile_state( 

1714 self, 

1715 parent_loader, 

1716 compile_state, 

1717 mapper_entities, 

1718 reconciled_lead_entity, 

1719 raiseerr, 

1720 ): 

1721 """implemented by subclasses.""" 

1722 raise NotImplementedError() 

1723 

1724 def process_compile_state( 

1725 self, 

1726 parent_loader, 

1727 compile_state, 

1728 mapper_entities, 

1729 reconciled_lead_entity, 

1730 raiseerr, 

1731 ): 

1732 """populate ORMCompileState.attributes with loader state for this 

1733 _LoadElement. 

1734 

1735 """ 

1736 keys = self._prepare_for_compile_state( 

1737 parent_loader, 

1738 compile_state, 

1739 mapper_entities, 

1740 reconciled_lead_entity, 

1741 raiseerr, 

1742 ) 

1743 for key in keys: 

1744 if key in compile_state.attributes: 

1745 compile_state.attributes[key] = _LoadElement._reconcile( 

1746 self, compile_state.attributes[key] 

1747 ) 

1748 else: 

1749 compile_state.attributes[key] = self 

1750 

1751 @classmethod 

1752 def create( 

1753 cls, 

1754 path: PathRegistry, 

1755 attr: Union[_AttrType, _StrPathToken, None], 

1756 strategy: Optional[_StrategyKey], 

1757 wildcard_key: Optional[_WildcardKeyType], 

1758 local_opts: Optional[_OptsType], 

1759 propagate_to_loaders: bool, 

1760 raiseerr: bool = True, 

1761 attr_group: Optional[_AttrGroupType] = None, 

1762 reconcile_to_other: Optional[bool] = None, 

1763 extra_criteria: Optional[Tuple[Any, ...]] = None, 

1764 ) -> _LoadElement: 

1765 """Create a new :class:`._LoadElement` object.""" 

1766 

1767 opt = cls.__new__(cls) 

1768 opt.path = path 

1769 opt.strategy = strategy 

1770 opt.propagate_to_loaders = propagate_to_loaders 

1771 opt.local_opts = ( 

1772 util.immutabledict(local_opts) if local_opts else util.EMPTY_DICT 

1773 ) 

1774 opt._extra_criteria = () 

1775 

1776 if reconcile_to_other is not None: 

1777 opt._reconcile_to_other = reconcile_to_other 

1778 elif strategy is None and not local_opts: 

1779 opt._reconcile_to_other = True 

1780 else: 

1781 opt._reconcile_to_other = None 

1782 

1783 path = opt._init_path( 

1784 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1785 ) 

1786 

1787 if not path: 

1788 return None # type: ignore 

1789 

1790 assert opt.is_token_strategy == path.is_token 

1791 

1792 opt.path = path 

1793 return opt 

1794 

1795 def __init__(self) -> None: 

1796 raise NotImplementedError() 

1797 

1798 def _recurse(self) -> _LoadElement: 

1799 cloned = self._clone() 

1800 cloned.path = PathRegistry.coerce(self.path[:] + self.path[-2:]) 

1801 

1802 return cloned 

1803 

1804 def _prepend_path_from(self, parent: Load) -> _LoadElement: 

1805 """adjust the path of this :class:`._LoadElement` to be 

1806 a subpath of that of the given parent :class:`_orm.Load` object's 

1807 path. 

1808 

1809 This is used by the :meth:`_orm.Load._apply_to_parent` method, 

1810 which is in turn part of the :meth:`_orm.Load.options` method. 

1811 

1812 """ 

1813 

1814 if not any( 

1815 orm_util._entity_corresponds_to_use_path_impl( 

1816 elem, 

1817 self.path.odd_element(0), 

1818 ) 

1819 for elem in (parent.path.odd_element(-1),) 

1820 + parent.additional_source_entities 

1821 ): 

1822 raise sa_exc.ArgumentError( 

1823 f'Attribute "{self.path[1]}" does not link ' 

1824 f'from element "{parent.path[-1]}".' 

1825 ) 

1826 

1827 return self._prepend_path(parent.path) 

1828 

1829 def _prepend_path(self, path: PathRegistry) -> _LoadElement: 

1830 cloned = self._clone() 

1831 

1832 assert cloned.strategy == self.strategy 

1833 assert cloned.local_opts == self.local_opts 

1834 assert cloned.is_class_strategy == self.is_class_strategy 

1835 

1836 cloned.path = PathRegistry.coerce(path[0:-1] + cloned.path[:]) 

1837 

1838 return cloned 

1839 

1840 @staticmethod 

1841 def _reconcile( 

1842 replacement: _LoadElement, existing: _LoadElement 

1843 ) -> _LoadElement: 

1844 """define behavior for when two Load objects are to be put into 

1845 the context.attributes under the same key. 

1846 

1847 :param replacement: ``_LoadElement`` that seeks to replace the 

1848 existing one 

1849 

1850 :param existing: ``_LoadElement`` that is already present. 

1851 

1852 """ 

1853 # mapper inheritance loading requires fine-grained "block other 

1854 # options" / "allow these options to be overridden" behaviors 

1855 # see test_poly_loading.py 

1856 

1857 if replacement._reconcile_to_other: 

1858 return existing 

1859 elif replacement._reconcile_to_other is False: 

1860 return replacement 

1861 elif existing._reconcile_to_other: 

1862 return replacement 

1863 elif existing._reconcile_to_other is False: 

1864 return existing 

1865 

1866 if existing is replacement: 

1867 return replacement 

1868 elif ( 

1869 existing.strategy == replacement.strategy 

1870 and existing.local_opts == replacement.local_opts 

1871 ): 

1872 return replacement 

1873 elif replacement.is_opts_only: 

1874 existing = existing._clone() 

1875 existing.local_opts = existing.local_opts.union( 

1876 replacement.local_opts 

1877 ) 

1878 existing._extra_criteria += replacement._extra_criteria 

1879 return existing 

1880 elif existing.is_opts_only: 

1881 replacement = replacement._clone() 

1882 replacement.local_opts = replacement.local_opts.union( 

1883 existing.local_opts 

1884 ) 

1885 replacement._extra_criteria += existing._extra_criteria 

1886 return replacement 

1887 elif replacement.path.is_token: 

1888 # use 'last one wins' logic for wildcard options. this is also 

1889 # kind of inconsistent vs. options that are specific paths which 

1890 # will raise as below 

1891 return replacement 

1892 

1893 raise sa_exc.InvalidRequestError( 

1894 f"Loader strategies for {replacement.path} conflict" 

1895 ) 

1896 

1897 

1898class _AttributeStrategyLoad(_LoadElement): 

1899 """Loader strategies against specific relationship or column paths. 

1900 

1901 e.g.:: 

1902 

1903 joinedload(User.addresses) 

1904 defer(Order.name) 

1905 selectinload(User.orders).lazyload(Order.items) 

1906 

1907 """ 

1908 

1909 __slots__ = ("_of_type", "_path_with_polymorphic_path") 

1910 

1911 __visit_name__ = "attribute_strategy_load_element" 

1912 

1913 _traverse_internals = _LoadElement._traverse_internals + [ 

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

1915 ( 

1916 "_path_with_polymorphic_path", 

1917 visitors.ExtendedInternalTraversal.dp_has_cache_key, 

1918 ), 

1919 ] 

1920 

1921 _of_type: Union[Mapper[Any], AliasedInsp[Any], None] 

1922 _path_with_polymorphic_path: Optional[PathRegistry] 

1923 

1924 is_class_strategy = False 

1925 is_token_strategy = False 

1926 

1927 def _init_path( 

1928 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1929 ): 

1930 assert attr is not None 

1931 self._of_type = None 

1932 self._path_with_polymorphic_path = None 

1933 insp, _, prop = _parse_attr_argument(attr) 

1934 

1935 if insp.is_property: 

1936 # direct property can be sent from internal strategy logic 

1937 # that sets up specific loaders, such as 

1938 # emit_lazyload->_lazyload_reverse 

1939 # prop = found_property = attr 

1940 prop = attr 

1941 path = path[prop] 

1942 

1943 if path.has_entity: 

1944 path = path.entity_path 

1945 return path 

1946 

1947 elif not insp.is_attribute: 

1948 # should not reach here; 

1949 assert False 

1950 

1951 # here we assume we have user-passed InstrumentedAttribute 

1952 if not orm_util._entity_corresponds_to_use_path_impl( 

1953 path[-1], attr.parent 

1954 ): 

1955 if raiseerr: 

1956 if attr_group and attr is not attr_group[0]: 

1957 raise sa_exc.ArgumentError( 

1958 "Can't apply wildcard ('*') or load_only() " 

1959 "loader option to multiple entities in the " 

1960 "same option. Use separate options per entity." 

1961 ) 

1962 else: 

1963 _raise_for_does_not_link(path, str(attr), attr.parent) 

1964 else: 

1965 return None 

1966 

1967 # note the essential logic of this attribute was very different in 

1968 # 1.4, where there were caching failures in e.g. 

1969 # test_relationship_criteria.py::RelationshipCriteriaTest:: 

1970 # test_selectinload_nested_criteria[True] if an existing 

1971 # "_extra_criteria" on a Load object were replaced with that coming 

1972 # from an attribute. This appears to have been an artifact of how 

1973 # _UnboundLoad / Load interacted together, which was opaque and 

1974 # poorly defined. 

1975 if extra_criteria: 

1976 assert not attr._extra_criteria 

1977 self._extra_criteria = extra_criteria 

1978 else: 

1979 self._extra_criteria = attr._extra_criteria 

1980 

1981 if getattr(attr, "_of_type", None): 

1982 ac = attr._of_type 

1983 ext_info = inspect(ac) 

1984 self._of_type = ext_info 

1985 

1986 self._path_with_polymorphic_path = path.entity_path[prop] 

1987 

1988 path = path[prop][ext_info] 

1989 

1990 else: 

1991 path = path[prop] 

1992 

1993 if path.has_entity: 

1994 path = path.entity_path 

1995 

1996 return path 

1997 

1998 def _generate_extra_criteria(self, context): 

1999 """Apply the current bound parameters in a QueryContext to the 

2000 immediate "extra_criteria" stored with this Load object. 

2001 

2002 Load objects are typically pulled from the cached version of 

2003 the statement from a QueryContext. The statement currently being 

2004 executed will have new values (and keys) for bound parameters in the 

2005 extra criteria which need to be applied by loader strategies when 

2006 they handle this criteria for a result set. 

2007 

2008 """ 

2009 

2010 assert ( 

2011 self._extra_criteria 

2012 ), "this should only be called if _extra_criteria is present" 

2013 

2014 orig_query = context.compile_state.select_statement 

2015 current_query = context.query 

2016 

2017 # NOTE: while it seems like we should not do the "apply" operation 

2018 # here if orig_query is current_query, skipping it in the "optimized" 

2019 # case causes the query to be different from a cache key perspective, 

2020 # because we are creating a copy of the criteria which is no longer 

2021 # the same identity of the _extra_criteria in the loader option 

2022 # itself. cache key logic produces a different key for 

2023 # (A, copy_of_A) vs. (A, A), because in the latter case it shortens 

2024 # the second part of the key to just indicate on identity. 

2025 

2026 # if orig_query is current_query: 

2027 # not cached yet. just do the and_() 

2028 # return and_(*self._extra_criteria) 

2029 

2030 k1 = orig_query._generate_cache_key() 

2031 k2 = current_query._generate_cache_key() 

2032 

2033 return k2._apply_params_to_element(k1, and_(*self._extra_criteria)) 

2034 

2035 def _set_of_type_info(self, context, current_path): 

2036 assert self._path_with_polymorphic_path 

2037 

2038 pwpi = self._of_type 

2039 assert pwpi 

2040 if not pwpi.is_aliased_class: 

2041 pwpi = inspect( 

2042 orm_util.AliasedInsp._with_polymorphic_factory( 

2043 pwpi.mapper.base_mapper, 

2044 (pwpi.mapper,), 

2045 aliased=True, 

2046 _use_mapper_path=True, 

2047 ) 

2048 ) 

2049 start_path = self._path_with_polymorphic_path 

2050 if current_path: 

2051 new_path = self._adjust_effective_path_for_current_path( 

2052 start_path, current_path 

2053 ) 

2054 if new_path is None: 

2055 return 

2056 start_path = new_path 

2057 

2058 key = ("path_with_polymorphic", start_path.natural_path) 

2059 if key in context: 

2060 existing_aliased_insp = context[key] 

2061 this_aliased_insp = pwpi 

2062 new_aliased_insp = existing_aliased_insp._merge_with( 

2063 this_aliased_insp 

2064 ) 

2065 context[key] = new_aliased_insp 

2066 else: 

2067 context[key] = pwpi 

2068 

2069 def _prepare_for_compile_state( 

2070 self, 

2071 parent_loader, 

2072 compile_state, 

2073 mapper_entities, 

2074 reconciled_lead_entity, 

2075 raiseerr, 

2076 ): 

2077 # _AttributeStrategyLoad 

2078 

2079 current_path = compile_state.current_path 

2080 is_refresh = compile_state.compile_options._for_refresh_state 

2081 assert not self.path.is_token 

2082 

2083 if is_refresh and not self.propagate_to_loaders: 

2084 return [] 

2085 

2086 if self._of_type: 

2087 # apply additional with_polymorphic alias that may have been 

2088 # generated. this has to happen even if this is a defaultload 

2089 self._set_of_type_info(compile_state.attributes, current_path) 

2090 

2091 # omit setting loader attributes for a "defaultload" type of option 

2092 if not self.strategy and not self.local_opts: 

2093 return [] 

2094 

2095 if raiseerr and not reconciled_lead_entity: 

2096 self._raise_for_no_match(parent_loader, mapper_entities) 

2097 

2098 if self.path.has_entity: 

2099 effective_path = self.path.parent 

2100 else: 

2101 effective_path = self.path 

2102 

2103 if current_path: 

2104 assert effective_path is not None 

2105 effective_path = self._adjust_effective_path_for_current_path( 

2106 effective_path, current_path 

2107 ) 

2108 if effective_path is None: 

2109 return [] 

2110 

2111 return [("loader", cast(PathRegistry, effective_path).natural_path)] 

2112 

2113 def __getstate__(self): 

2114 d = super().__getstate__() 

2115 

2116 # can't pickle this. See 

2117 # test_pickled.py -> test_lazyload_extra_criteria_not_supported 

2118 # where we should be emitting a warning for the usual case where this 

2119 # would be non-None 

2120 d["_extra_criteria"] = () 

2121 

2122 if self._path_with_polymorphic_path: 

2123 d["_path_with_polymorphic_path"] = ( 

2124 self._path_with_polymorphic_path.serialize() 

2125 ) 

2126 

2127 if self._of_type: 

2128 if self._of_type.is_aliased_class: 

2129 d["_of_type"] = None 

2130 elif self._of_type.is_mapper: 

2131 d["_of_type"] = self._of_type.class_ 

2132 else: 

2133 assert False, "unexpected object for _of_type" 

2134 

2135 return d 

2136 

2137 def __setstate__(self, state): 

2138 super().__setstate__(state) 

2139 

2140 if state.get("_path_with_polymorphic_path", None): 

2141 self._path_with_polymorphic_path = PathRegistry.deserialize( 

2142 state["_path_with_polymorphic_path"] 

2143 ) 

2144 else: 

2145 self._path_with_polymorphic_path = None 

2146 

2147 if state.get("_of_type", None): 

2148 self._of_type = inspect(state["_of_type"]) 

2149 else: 

2150 self._of_type = None 

2151 

2152 

2153class _TokenStrategyLoad(_LoadElement): 

2154 """Loader strategies against wildcard attributes 

2155 

2156 e.g.:: 

2157 

2158 raiseload('*') 

2159 Load(User).lazyload('*') 

2160 defer('*') 

2161 load_only(User.name, User.email) # will create a defer('*') 

2162 joinedload(User.addresses).raiseload('*') 

2163 

2164 """ 

2165 

2166 __visit_name__ = "token_strategy_load_element" 

2167 

2168 inherit_cache = True 

2169 is_class_strategy = False 

2170 is_token_strategy = True 

2171 

2172 def _init_path( 

2173 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

2174 ): 

2175 # assert isinstance(attr, str) or attr is None 

2176 if attr is not None: 

2177 default_token = attr.endswith(_DEFAULT_TOKEN) 

2178 if attr.endswith(_WILDCARD_TOKEN) or default_token: 

2179 if wildcard_key: 

2180 attr = f"{wildcard_key}:{attr}" 

2181 

2182 path = path.token(attr) 

2183 return path 

2184 else: 

2185 raise sa_exc.ArgumentError( 

2186 "Strings are not accepted for attribute names in loader " 

2187 "options; please use class-bound attributes directly." 

2188 ) 

2189 return path 

2190 

2191 def _prepare_for_compile_state( 

2192 self, 

2193 parent_loader, 

2194 compile_state, 

2195 mapper_entities, 

2196 reconciled_lead_entity, 

2197 raiseerr, 

2198 ): 

2199 # _TokenStrategyLoad 

2200 

2201 current_path = compile_state.current_path 

2202 is_refresh = compile_state.compile_options._for_refresh_state 

2203 

2204 assert self.path.is_token 

2205 

2206 if is_refresh and not self.propagate_to_loaders: 

2207 return [] 

2208 

2209 # omit setting attributes for a "defaultload" type of option 

2210 if not self.strategy and not self.local_opts: 

2211 return [] 

2212 

2213 effective_path = self.path 

2214 if reconciled_lead_entity: 

2215 effective_path = PathRegistry.coerce( 

2216 (reconciled_lead_entity,) + effective_path.path[1:] 

2217 ) 

2218 

2219 if current_path: 

2220 new_effective_path = self._adjust_effective_path_for_current_path( 

2221 effective_path, current_path 

2222 ) 

2223 if new_effective_path is None: 

2224 return [] 

2225 effective_path = new_effective_path 

2226 

2227 # for a wildcard token, expand out the path we set 

2228 # to encompass everything from the query entity on 

2229 # forward. not clear if this is necessary when current_path 

2230 # is set. 

2231 

2232 return [ 

2233 ("loader", natural_path) 

2234 for natural_path in ( 

2235 cast( 

2236 TokenRegistry, effective_path 

2237 )._generate_natural_for_superclasses() 

2238 ) 

2239 ] 

2240 

2241 

2242class _ClassStrategyLoad(_LoadElement): 

2243 """Loader strategies that deals with a class as a target, not 

2244 an attribute path 

2245 

2246 e.g.:: 

2247 

2248 q = s.query(Person).options( 

2249 selectin_polymorphic(Person, [Engineer, Manager]) 

2250 ) 

2251 

2252 """ 

2253 

2254 inherit_cache = True 

2255 is_class_strategy = True 

2256 is_token_strategy = False 

2257 

2258 __visit_name__ = "class_strategy_load_element" 

2259 

2260 def _init_path( 

2261 self, path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

2262 ): 

2263 return path 

2264 

2265 def _prepare_for_compile_state( 

2266 self, 

2267 parent_loader, 

2268 compile_state, 

2269 mapper_entities, 

2270 reconciled_lead_entity, 

2271 raiseerr, 

2272 ): 

2273 # _ClassStrategyLoad 

2274 

2275 current_path = compile_state.current_path 

2276 is_refresh = compile_state.compile_options._for_refresh_state 

2277 

2278 if is_refresh and not self.propagate_to_loaders: 

2279 return [] 

2280 

2281 # omit setting attributes for a "defaultload" type of option 

2282 if not self.strategy and not self.local_opts: 

2283 return [] 

2284 

2285 effective_path = self.path 

2286 

2287 if current_path: 

2288 new_effective_path = self._adjust_effective_path_for_current_path( 

2289 effective_path, current_path 

2290 ) 

2291 if new_effective_path is None: 

2292 return [] 

2293 effective_path = new_effective_path 

2294 

2295 return [("loader", effective_path.natural_path)] 

2296 

2297 

2298def _generate_from_keys( 

2299 meth: Callable[..., _AbstractLoad], 

2300 keys: Tuple[_AttrType, ...], 

2301 chained: bool, 

2302 kw: Any, 

2303) -> _AbstractLoad: 

2304 lead_element: Optional[_AbstractLoad] = None 

2305 

2306 attr: Any 

2307 for is_default, _keys in (True, keys[0:-1]), (False, keys[-1:]): 

2308 for attr in _keys: 

2309 if isinstance(attr, str): 

2310 if attr.startswith("." + _WILDCARD_TOKEN): 

2311 util.warn_deprecated( 

2312 "The undocumented `.{WILDCARD}` format is " 

2313 "deprecated " 

2314 "and will be removed in a future version as " 

2315 "it is " 

2316 "believed to be unused. " 

2317 "If you have been using this functionality, " 

2318 "please " 

2319 "comment on Issue #4390 on the SQLAlchemy project " 

2320 "tracker.", 

2321 version="1.4", 

2322 ) 

2323 attr = attr[1:] 

2324 

2325 if attr == _WILDCARD_TOKEN: 

2326 if is_default: 

2327 raise sa_exc.ArgumentError( 

2328 "Wildcard token cannot be followed by " 

2329 "another entity", 

2330 ) 

2331 

2332 if lead_element is None: 

2333 lead_element = _WildcardLoad() 

2334 

2335 lead_element = meth(lead_element, _DEFAULT_TOKEN, **kw) 

2336 

2337 else: 

2338 raise sa_exc.ArgumentError( 

2339 "Strings are not accepted for attribute names in " 

2340 "loader options; please use class-bound " 

2341 "attributes directly.", 

2342 ) 

2343 else: 

2344 if lead_element is None: 

2345 _, lead_entity, _ = _parse_attr_argument(attr) 

2346 lead_element = Load(lead_entity) 

2347 

2348 if is_default: 

2349 if not chained: 

2350 lead_element = lead_element.defaultload(attr) 

2351 else: 

2352 lead_element = meth( 

2353 lead_element, attr, _is_chain=True, **kw 

2354 ) 

2355 else: 

2356 lead_element = meth(lead_element, attr, **kw) 

2357 

2358 assert lead_element 

2359 return lead_element 

2360 

2361 

2362def _parse_attr_argument( 

2363 attr: _AttrType, 

2364) -> Tuple[InspectionAttr, _InternalEntityType[Any], MapperProperty[Any]]: 

2365 """parse an attribute or wildcard argument to produce an 

2366 :class:`._AbstractLoad` instance. 

2367 

2368 This is used by the standalone loader strategy functions like 

2369 ``joinedload()``, ``defer()``, etc. to produce :class:`_orm.Load` or 

2370 :class:`._WildcardLoad` objects. 

2371 

2372 """ 

2373 try: 

2374 # TODO: need to figure out this None thing being returned by 

2375 # inspect(), it should not have None as an option in most cases 

2376 # if at all 

2377 insp: InspectionAttr = inspect(attr) # type: ignore 

2378 except sa_exc.NoInspectionAvailable as err: 

2379 raise sa_exc.ArgumentError( 

2380 "expected ORM mapped attribute for loader strategy argument" 

2381 ) from err 

2382 

2383 lead_entity: _InternalEntityType[Any] 

2384 

2385 if insp_is_mapper_property(insp): 

2386 lead_entity = insp.parent 

2387 prop = insp 

2388 elif insp_is_attribute(insp): 

2389 lead_entity = insp.parent 

2390 prop = insp.prop 

2391 else: 

2392 raise sa_exc.ArgumentError( 

2393 "expected ORM mapped attribute for loader strategy argument" 

2394 ) 

2395 

2396 return insp, lead_entity, prop 

2397 

2398 

2399def loader_unbound_fn(fn: _FN) -> _FN: 

2400 """decorator that applies docstrings between standalone loader functions 

2401 and the loader methods on :class:`._AbstractLoad`. 

2402 

2403 """ 

2404 bound_fn = getattr(_AbstractLoad, fn.__name__) 

2405 fn_doc = bound_fn.__doc__ 

2406 bound_fn.__doc__ = f"""Produce a new :class:`_orm.Load` object with the 

2407:func:`_orm.{fn.__name__}` option applied. 

2408 

2409See :func:`_orm.{fn.__name__}` for usage examples. 

2410 

2411""" 

2412 

2413 fn.__doc__ = fn_doc 

2414 return fn 

2415 

2416 

2417# standalone functions follow. docstrings are filled in 

2418# by the ``@loader_unbound_fn`` decorator. 

2419 

2420 

2421@loader_unbound_fn 

2422def contains_eager(*keys: _AttrType, **kw: Any) -> _AbstractLoad: 

2423 return _generate_from_keys(Load.contains_eager, keys, True, kw) 

2424 

2425 

2426@loader_unbound_fn 

2427def load_only(*attrs: _AttrType, raiseload: bool = False) -> _AbstractLoad: 

2428 # TODO: attrs against different classes. we likely have to 

2429 # add some extra state to Load of some kind 

2430 _, lead_element, _ = _parse_attr_argument(attrs[0]) 

2431 return Load(lead_element).load_only(*attrs, raiseload=raiseload) 

2432 

2433 

2434@loader_unbound_fn 

2435def joinedload(*keys: _AttrType, **kw: Any) -> _AbstractLoad: 

2436 return _generate_from_keys(Load.joinedload, keys, False, kw) 

2437 

2438 

2439@loader_unbound_fn 

2440def subqueryload(*keys: _AttrType) -> _AbstractLoad: 

2441 return _generate_from_keys(Load.subqueryload, keys, False, {}) 

2442 

2443 

2444@loader_unbound_fn 

2445def selectinload( 

2446 *keys: _AttrType, recursion_depth: Optional[int] = None 

2447) -> _AbstractLoad: 

2448 return _generate_from_keys( 

2449 Load.selectinload, keys, False, {"recursion_depth": recursion_depth} 

2450 ) 

2451 

2452 

2453@loader_unbound_fn 

2454def lazyload(*keys: _AttrType) -> _AbstractLoad: 

2455 return _generate_from_keys(Load.lazyload, keys, False, {}) 

2456 

2457 

2458@loader_unbound_fn 

2459def immediateload( 

2460 *keys: _AttrType, recursion_depth: Optional[int] = None 

2461) -> _AbstractLoad: 

2462 return _generate_from_keys( 

2463 Load.immediateload, keys, False, {"recursion_depth": recursion_depth} 

2464 ) 

2465 

2466 

2467@loader_unbound_fn 

2468def noload(*keys: _AttrType) -> _AbstractLoad: 

2469 return _generate_from_keys(Load.noload, keys, False, {}) 

2470 

2471 

2472@loader_unbound_fn 

2473def raiseload(*keys: _AttrType, **kw: Any) -> _AbstractLoad: 

2474 return _generate_from_keys(Load.raiseload, keys, False, kw) 

2475 

2476 

2477@loader_unbound_fn 

2478def defaultload(*keys: _AttrType) -> _AbstractLoad: 

2479 return _generate_from_keys(Load.defaultload, keys, False, {}) 

2480 

2481 

2482@loader_unbound_fn 

2483def defer( 

2484 key: _AttrType, *addl_attrs: _AttrType, raiseload: bool = False 

2485) -> _AbstractLoad: 

2486 if addl_attrs: 

2487 util.warn_deprecated( 

2488 "The *addl_attrs on orm.defer is deprecated. Please use " 

2489 "method chaining in conjunction with defaultload() to " 

2490 "indicate a path.", 

2491 version="1.3", 

2492 ) 

2493 

2494 if raiseload: 

2495 kw = {"raiseload": raiseload} 

2496 else: 

2497 kw = {} 

2498 

2499 return _generate_from_keys(Load.defer, (key,) + addl_attrs, False, kw) 

2500 

2501 

2502@loader_unbound_fn 

2503def undefer(key: _AttrType, *addl_attrs: _AttrType) -> _AbstractLoad: 

2504 if addl_attrs: 

2505 util.warn_deprecated( 

2506 "The *addl_attrs on orm.undefer is deprecated. Please use " 

2507 "method chaining in conjunction with defaultload() to " 

2508 "indicate a path.", 

2509 version="1.3", 

2510 ) 

2511 return _generate_from_keys(Load.undefer, (key,) + addl_attrs, False, {}) 

2512 

2513 

2514@loader_unbound_fn 

2515def undefer_group(name: str) -> _AbstractLoad: 

2516 element = _WildcardLoad() 

2517 return element.undefer_group(name) 

2518 

2519 

2520@loader_unbound_fn 

2521def with_expression( 

2522 key: _AttrType, expression: _ColumnExpressionArgument[Any] 

2523) -> _AbstractLoad: 

2524 return _generate_from_keys( 

2525 Load.with_expression, (key,), False, {"expression": expression} 

2526 ) 

2527 

2528 

2529@loader_unbound_fn 

2530def selectin_polymorphic( 

2531 base_cls: _EntityType[Any], classes: Iterable[Type[Any]] 

2532) -> _AbstractLoad: 

2533 ul = Load(base_cls) 

2534 return ul.selectin_polymorphic(classes) 

2535 

2536 

2537def _raise_for_does_not_link(path, attrname, parent_entity): 

2538 if len(path) > 1: 

2539 path_is_of_type = path[-1].entity is not path[-2].mapper.class_ 

2540 if insp_is_aliased_class(parent_entity): 

2541 parent_entity_str = str(parent_entity) 

2542 else: 

2543 parent_entity_str = parent_entity.class_.__name__ 

2544 

2545 raise sa_exc.ArgumentError( 

2546 f'ORM mapped entity or attribute "{attrname}" does not ' 

2547 f'link from relationship "{path[-2]}%s".%s' 

2548 % ( 

2549 f".of_type({path[-1]})" if path_is_of_type else "", 

2550 ( 

2551 " Did you mean to use " 

2552 f'"{path[-2]}' 

2553 f'.of_type({parent_entity_str})" or "loadopt.options(' 

2554 f"selectin_polymorphic({path[-2].mapper.class_.__name__}, " 

2555 f'[{parent_entity_str}]), ...)" ?' 

2556 if not path_is_of_type 

2557 and not path[-1].is_aliased_class 

2558 and orm_util._entity_corresponds_to( 

2559 path.entity, inspect(parent_entity).mapper 

2560 ) 

2561 else "" 

2562 ), 

2563 ) 

2564 ) 

2565 else: 

2566 raise sa_exc.ArgumentError( 

2567 f'ORM mapped attribute "{attrname}" does not ' 

2568 f'link mapped class "{path[-1]}"' 

2569 )