Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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

786 statements  

1# orm/strategy_options.py 

2# Copyright (C) 2005-2025 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 

11from __future__ import annotations 

12 

13import typing 

14from typing import Any 

15from typing import Callable 

16from typing import cast 

17from typing import Dict 

18from typing import Iterable 

19from typing import Optional 

20from typing import overload 

21from typing import Sequence 

22from typing import Tuple 

23from typing import Type 

24from typing import TypeVar 

25from typing import Union 

26 

27from . import util as orm_util 

28from ._typing import insp_is_aliased_class 

29from ._typing import insp_is_attribute 

30from ._typing import insp_is_mapper 

31from ._typing import insp_is_mapper_property 

32from .attributes import QueryableAttribute 

33from .base import InspectionAttr 

34from .interfaces import LoaderOption 

35from .path_registry import _DEFAULT_TOKEN 

36from .path_registry import _StrPathToken 

37from .path_registry import _WILDCARD_TOKEN 

38from .path_registry import AbstractEntityRegistry 

39from .path_registry import path_is_property 

40from .path_registry import PathRegistry 

41from .path_registry import TokenRegistry 

42from .util import _orm_full_deannotate 

43from .util import AliasedInsp 

44from .. import exc as sa_exc 

45from .. import inspect 

46from .. import util 

47from ..sql import and_ 

48from ..sql import cache_key 

49from ..sql import coercions 

50from ..sql import roles 

51from ..sql import traversals 

52from ..sql import visitors 

53from ..sql.base import _generative 

54from ..util.typing import Final 

55from ..util.typing import Literal 

56from ..util.typing import Self 

57 

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

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

60 

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

62 

63if typing.TYPE_CHECKING: 

64 from ._typing import _EntityType 

65 from ._typing import _InternalEntityType 

66 from .context import _MapperEntity 

67 from .context import ORMCompileState 

68 from .context import QueryContext 

69 from .interfaces import _StrategyKey 

70 from .interfaces import MapperProperty 

71 from .interfaces import ORMOption 

72 from .mapper import Mapper 

73 from .path_registry import _PathRepresentation 

74 from ..sql._typing import _ColumnExpressionArgument 

75 from ..sql._typing import _FromClauseArgument 

76 from ..sql.cache_key import _CacheKeyTraversalType 

77 from ..sql.cache_key import CacheKey 

78 

79 

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

81 

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

83_StrategySpec = Dict[str, Any] 

84_OptsType = Dict[str, Any] 

85_AttrGroupType = Tuple[_AttrType, ...] 

86 

87 

88class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption): 

89 __slots__ = ("propagate_to_loaders",) 

90 

91 _is_strategy_option = True 

92 propagate_to_loaders: bool 

93 

94 def contains_eager( 

95 self, 

96 attr: _AttrType, 

97 alias: Optional[_FromClauseArgument] = None, 

98 _is_chain: bool = False, 

99 _propagate_to_loaders: bool = False, 

100 ) -> Self: 

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

102 columns stated manually in the query. 

103 

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

105 both method-chained and standalone operation. 

106 

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

108 the desired rows, i.e.:: 

109 

110 sess.query(Order).join(Order.user).options(contains_eager(Order.user)) 

111 

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

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

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

115 

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

117 collection; queries will normally want to use the 

118 :ref:`orm_queryguide_populate_existing` execution option assuming the 

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

120 

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

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

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

124 

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

126 

127 .. seealso:: 

128 

129 :ref:`loading_toplevel` 

130 

131 :ref:`contains_eager` 

132 

133 """ 

134 if alias is not None: 

135 if not isinstance(alias, str): 

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

137 else: 

138 util.warn_deprecated( 

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

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

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

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

143 version="1.4", 

144 ) 

145 coerced_alias = alias 

146 

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

148 assert isinstance(attr, QueryableAttribute) 

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

150 assert ot is not None 

151 coerced_alias = ot.selectable 

152 else: 

153 coerced_alias = None 

154 

155 cloned = self._set_relationship_strategy( 

156 attr, 

157 {"lazy": "joined"}, 

158 propagate_to_loaders=_propagate_to_loaders, 

159 opts={"eager_from_alias": coerced_alias}, 

160 _reconcile_to_other=True if _is_chain else None, 

161 ) 

162 return cloned 

163 

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

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

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

167 deferred. 

168 

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

170 both method-chained and standalone operation. 

171 

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

173 ``fullname`` attributes:: 

174 

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

176 

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

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

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

180 

181 session.query(User).options( 

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

183 ) 

184 

185 For a statement that has multiple entities, 

186 the lead entity can be 

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

188 

189 stmt = ( 

190 select(User, Address) 

191 .join(User.addresses) 

192 .options( 

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

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

195 ) 

196 ) 

197 

198 When used together with the 

199 :ref:`populate_existing <orm_queryguide_populate_existing>` 

200 execution option only the attributes listed will be refreshed. 

201 

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

203 

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

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

206 to prevent unwanted SQL from being emitted. 

207 

208 .. versionadded:: 2.0 

209 

210 .. seealso:: 

211 

212 :ref:`orm_queryguide_column_deferral` - in the 

213 :ref:`queryguide_toplevel` 

214 

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

216 

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

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

219 to prevent unwanted SQL from being emitted. 

220 

221 .. versionadded:: 2.0 

222 

223 """ 

224 cloned = self._set_column_strategy( 

225 _expand_column_strategy_attrs(attrs), 

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

227 ) 

228 

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

230 if raiseload: 

231 wildcard_strategy["raiseload"] = True 

232 

233 cloned = cloned._set_column_strategy( 

234 ("*",), 

235 wildcard_strategy, 

236 ) 

237 return cloned 

238 

239 def joinedload( 

240 self, 

241 attr: _AttrType, 

242 innerjoin: Optional[bool] = None, 

243 ) -> Self: 

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

245 eager loading. 

246 

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

248 both method-chained and standalone operation. 

249 

250 examples:: 

251 

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

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

254 

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

256 select(Order).options(joinedload(Order.items).joinedload(Item.keywords)) 

257 

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

259 # joined-load the keywords collection 

260 select(Order).options(lazyload(Order.items).joinedload(Item.keywords)) 

261 

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

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

264 

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

266 

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

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

269 

270 select(A).options( 

271 joinedload(A.bs, innerjoin=False).joinedload(B.cs, innerjoin=True) 

272 ) 

273 

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

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

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

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

278 directly supported. 

279 

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

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

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

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

284 is an outerjoin:: 

285 

286 select(A).options(joinedload(A.bs).joinedload(B.cs, innerjoin="unnested")) 

287 

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

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

290 

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

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

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

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

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

296 

297 .. note:: 

298 

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

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

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

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

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

304 

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

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

307 explicit JOINs with eager loading of collections, use 

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

309 

310 .. seealso:: 

311 

312 :ref:`loading_toplevel` 

313 

314 :ref:`joined_eager_loading` 

315 

316 """ # noqa: E501 

317 loader = self._set_relationship_strategy( 

318 attr, 

319 {"lazy": "joined"}, 

320 opts=( 

321 {"innerjoin": innerjoin} 

322 if innerjoin is not None 

323 else util.EMPTY_DICT 

324 ), 

325 ) 

326 return loader 

327 

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

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

330 subquery eager loading. 

331 

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

333 both method-chained and standalone operation. 

334 

335 examples:: 

336 

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

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

339 

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

341 select(Order).options( 

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

343 ) 

344 

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

346 # subquery-load the keywords collection 

347 select(Order).options(lazyload(Order.items).subqueryload(Item.keywords)) 

348 

349 .. seealso:: 

350 

351 :ref:`loading_toplevel` 

352 

353 :ref:`subquery_eager_loading` 

354 

355 """ 

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

357 

358 def selectinload( 

359 self, 

360 attr: _AttrType, 

361 recursion_depth: Optional[int] = None, 

362 ) -> Self: 

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

364 SELECT IN eager loading. 

365 

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

367 both method-chained and standalone operation. 

368 

369 examples:: 

370 

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

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

373 

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

375 select(Order).options( 

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

377 ) 

378 

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

380 # selectin-load the keywords collection 

381 select(Order).options(lazyload(Order.items).selectinload(Item.keywords)) 

382 

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

384 in conjunction with a self-referential relationship, 

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

386 automatically until no items are found. 

387 

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

389 currently supports only self-referential relationships. There 

390 is not yet an option to automatically traverse recursive structures 

391 with more than one relationship involved. 

392 

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

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

395 status for the 2.0 series. 

396 

397 .. versionadded:: 2.0 added 

398 :paramref:`_orm.selectinload.recursion_depth` 

399 

400 

401 .. seealso:: 

402 

403 :ref:`loading_toplevel` 

404 

405 :ref:`selectin_eager_loading` 

406 

407 """ 

408 return self._set_relationship_strategy( 

409 attr, 

410 {"lazy": "selectin"}, 

411 opts={"recursion_depth": recursion_depth}, 

412 ) 

413 

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

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

416 loading. 

417 

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

419 both method-chained and standalone operation. 

420 

421 .. seealso:: 

422 

423 :ref:`loading_toplevel` 

424 

425 :ref:`lazy_loading` 

426 

427 """ 

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

429 

430 def immediateload( 

431 self, 

432 attr: _AttrType, 

433 recursion_depth: Optional[int] = None, 

434 ) -> Self: 

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

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

437 

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

439 fire off any additional eager loaders. 

440 

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

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

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

444 

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

446 both method-chained and standalone operation. 

447 

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

449 in conjunction with a self-referential relationship, 

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

451 automatically until no items are found. 

452 

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

454 currently supports only self-referential relationships. There 

455 is not yet an option to automatically traverse recursive structures 

456 with more than one relationship involved. 

457 

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

459 treated as "alpha" status 

460 

461 .. versionadded:: 2.0 added 

462 :paramref:`_orm.immediateload.recursion_depth` 

463 

464 

465 .. seealso:: 

466 

467 :ref:`loading_toplevel` 

468 

469 :ref:`selectin_eager_loading` 

470 

471 """ 

472 loader = self._set_relationship_strategy( 

473 attr, 

474 {"lazy": "immediate"}, 

475 opts={"recursion_depth": recursion_depth}, 

476 ) 

477 return loader 

478 

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

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

481 unloaded. 

482 

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

484 producing any loading effect. 

485 

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

487 both method-chained and standalone operation. 

488 

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

490 only. 

491 

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

493 forces collections to be empty, which invariably leads to 

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

495 legitimate uses for this option in modern SQLAlchemy. 

496 

497 .. seealso:: 

498 

499 :ref:`loading_toplevel` 

500 

501 """ 

502 

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

504 

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

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

507 

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

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

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

511 ensure that all relationship attributes that are accessed in a 

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

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

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

515 

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

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

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

519 :func:`.defer` loader option. 

520 

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

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

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

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

525 

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

527 both method-chained and standalone operation. 

528 

529 .. seealso:: 

530 

531 :ref:`loading_toplevel` 

532 

533 :ref:`prevent_lazy_with_raiseload` 

534 

535 :ref:`orm_queryguide_deferred_raiseload` 

536 

537 """ 

538 

539 return self._set_relationship_strategy( 

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

541 ) 

542 

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

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

545 

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

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

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

549 loading will be used. 

550 

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

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

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

554 element of an element:: 

555 

556 session.query(MyClass).options( 

557 defaultload(MyClass.someattribute).joinedload( 

558 MyOtherClass.someotherattribute 

559 ) 

560 ) 

561 

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

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

564 

565 session.scalars( 

566 select(MyClass).options( 

567 defaultload(MyClass.someattribute) 

568 .defer("some_column") 

569 .undefer("some_other_column") 

570 ) 

571 ) 

572 

573 .. seealso:: 

574 

575 :ref:`orm_queryguide_relationship_sub_options` 

576 

577 :meth:`_orm.Load.options` 

578 

579 """ 

580 return self._set_relationship_strategy(attr, None) 

581 

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

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

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

585 

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

587 both method-chained and standalone operation. 

588 

589 e.g.:: 

590 

591 from sqlalchemy.orm import defer 

592 

593 session.query(MyClass).options( 

594 defer(MyClass.attribute_one), defer(MyClass.attribute_two) 

595 ) 

596 

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

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

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

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

601 

602 session.query(MyClass).options( 

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

604 ) 

605 

606 Multiple deferral options related to a relationship can be bundled 

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

608 

609 

610 select(MyClass).options( 

611 defaultload(MyClass.someattr).options( 

612 defer(RelatedClass.some_column), 

613 defer(RelatedClass.some_other_column), 

614 defer(RelatedClass.another_column), 

615 ) 

616 ) 

617 

618 :param key: Attribute to be deferred. 

619 

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

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

622 to prevent unwanted SQL from being emitted. 

623 

624 .. versionadded:: 1.4 

625 

626 .. seealso:: 

627 

628 :ref:`orm_queryguide_column_deferral` - in the 

629 :ref:`queryguide_toplevel` 

630 

631 :func:`_orm.load_only` 

632 

633 :func:`_orm.undefer` 

634 

635 """ 

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

637 if raiseload: 

638 strategy["raiseload"] = True 

639 return self._set_column_strategy( 

640 _expand_column_strategy_attrs((key,)), strategy 

641 ) 

642 

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

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

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

646 as a whole. 

647 

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

649 :func:`.deferred` attribute. 

650 

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

652 both method-chained and standalone operation. 

653 

654 Examples:: 

655 

656 # undefer two columns 

657 session.query(MyClass).options( 

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

659 ) 

660 

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

662 session.query(MyClass, MyOtherClass).options(Load(MyClass).undefer("*")) 

663 

664 # undefer a column on a related object 

665 select(MyClass).options(defaultload(MyClass.items).undefer(MyClass.text)) 

666 

667 :param key: Attribute to be undeferred. 

668 

669 .. seealso:: 

670 

671 :ref:`orm_queryguide_column_deferral` - in the 

672 :ref:`queryguide_toplevel` 

673 

674 :func:`_orm.defer` 

675 

676 :func:`_orm.undefer_group` 

677 

678 """ # noqa: E501 

679 return self._set_column_strategy( 

680 _expand_column_strategy_attrs((key,)), 

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

682 ) 

683 

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

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

686 undeferred. 

687 

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

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

690 

691 E.g:: 

692 

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

694 

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

696 spelled out using relationship loader options, such as 

697 :func:`_orm.defaultload`:: 

698 

699 select(MyClass).options( 

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

701 ) 

702 

703 .. seealso:: 

704 

705 :ref:`orm_queryguide_column_deferral` - in the 

706 :ref:`queryguide_toplevel` 

707 

708 :func:`_orm.defer` 

709 

710 :func:`_orm.undefer` 

711 

712 """ 

713 return self._set_column_strategy( 

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

715 ) 

716 

717 def with_expression( 

718 self, 

719 key: _AttrType, 

720 expression: _ColumnExpressionArgument[Any], 

721 ) -> Self: 

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

723 attribute. 

724 

725 This option is used in conjunction with the 

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

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

728 

729 E.g.:: 

730 

731 stmt = select(SomeClass).options( 

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

733 ) 

734 

735 .. versionadded:: 1.2 

736 

737 :param key: Attribute to be populated 

738 

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

740 

741 .. seealso:: 

742 

743 :ref:`orm_queryguide_with_expression` - background and usage 

744 examples 

745 

746 """ 

747 

748 expression = _orm_full_deannotate( 

749 coercions.expect(roles.LabeledColumnExprRole, expression) 

750 ) 

751 

752 return self._set_column_strategy( 

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

754 ) 

755 

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

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

758 specific to a subclass. 

759 

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

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

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

763 

764 .. versionadded:: 1.2 

765 

766 .. seealso:: 

767 

768 :ref:`polymorphic_selectin` 

769 

770 """ 

771 self = self._set_class_strategy( 

772 {"selectinload_polymorphic": True}, 

773 opts={ 

774 "entities": tuple( 

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

776 ) 

777 }, 

778 ) 

779 return self 

780 

781 @overload 

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

783 

784 @overload 

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

786 

787 def _coerce_strat( 

788 self, strategy: Optional[_StrategySpec] 

789 ) -> Optional[_StrategyKey]: 

790 if strategy is not None: 

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

792 else: 

793 strategy_key = None 

794 return strategy_key 

795 

796 @_generative 

797 def _set_relationship_strategy( 

798 self, 

799 attr: _AttrType, 

800 strategy: Optional[_StrategySpec], 

801 propagate_to_loaders: bool = True, 

802 opts: Optional[_OptsType] = None, 

803 _reconcile_to_other: Optional[bool] = None, 

804 ) -> Self: 

805 strategy_key = self._coerce_strat(strategy) 

806 

807 self._clone_for_bind_strategy( 

808 (attr,), 

809 strategy_key, 

810 _RELATIONSHIP_TOKEN, 

811 opts=opts, 

812 propagate_to_loaders=propagate_to_loaders, 

813 reconcile_to_other=_reconcile_to_other, 

814 ) 

815 return self 

816 

817 @_generative 

818 def _set_column_strategy( 

819 self, 

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

821 strategy: Optional[_StrategySpec], 

822 opts: Optional[_OptsType] = None, 

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

824 ) -> Self: 

825 strategy_key = self._coerce_strat(strategy) 

826 

827 self._clone_for_bind_strategy( 

828 attrs, 

829 strategy_key, 

830 _COLUMN_TOKEN, 

831 opts=opts, 

832 attr_group=attrs, 

833 extra_criteria=extra_criteria, 

834 ) 

835 return self 

836 

837 @_generative 

838 def _set_generic_strategy( 

839 self, 

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

841 strategy: _StrategySpec, 

842 _reconcile_to_other: Optional[bool] = None, 

843 ) -> Self: 

844 strategy_key = self._coerce_strat(strategy) 

845 self._clone_for_bind_strategy( 

846 attrs, 

847 strategy_key, 

848 None, 

849 propagate_to_loaders=True, 

850 reconcile_to_other=_reconcile_to_other, 

851 ) 

852 return self 

853 

854 @_generative 

855 def _set_class_strategy( 

856 self, strategy: _StrategySpec, opts: _OptsType 

857 ) -> Self: 

858 strategy_key = self._coerce_strat(strategy) 

859 

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

861 return self 

862 

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

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

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

866 

867 Implementation is provided by subclasses. 

868 

869 """ 

870 raise NotImplementedError() 

871 

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

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

874 :class:`_orm._AbstractLoad` object. 

875 

876 Implementation is provided by subclasses. 

877 

878 """ 

879 raise NotImplementedError() 

880 

881 def _clone_for_bind_strategy( 

882 self, 

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

884 strategy: Optional[_StrategyKey], 

885 wildcard_key: Optional[_WildcardKeyType], 

886 opts: Optional[_OptsType] = None, 

887 attr_group: Optional[_AttrGroupType] = None, 

888 propagate_to_loaders: bool = True, 

889 reconcile_to_other: Optional[bool] = None, 

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

891 ) -> Self: 

892 raise NotImplementedError() 

893 

894 def process_compile_state_replaced_entities( 

895 self, 

896 compile_state: ORMCompileState, 

897 mapper_entities: Sequence[_MapperEntity], 

898 ) -> None: 

899 if not compile_state.compile_options._enable_eagerloads: 

900 return 

901 

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

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

904 # for the entities having been replaced with equivalents 

905 self._process( 

906 compile_state, 

907 mapper_entities, 

908 not bool(compile_state.current_path), 

909 ) 

910 

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

912 if not compile_state.compile_options._enable_eagerloads: 

913 return 

914 

915 self._process( 

916 compile_state, 

917 compile_state._lead_mapper_entities, 

918 not bool(compile_state.current_path) 

919 and not compile_state.compile_options._for_refresh_state, 

920 ) 

921 

922 def _process( 

923 self, 

924 compile_state: ORMCompileState, 

925 mapper_entities: Sequence[_MapperEntity], 

926 raiseerr: bool, 

927 ) -> None: 

928 """implemented by subclasses""" 

929 raise NotImplementedError() 

930 

931 @classmethod 

932 def _chop_path( 

933 cls, 

934 to_chop: _PathRepresentation, 

935 path: PathRegistry, 

936 debug: bool = False, 

937 ) -> Optional[_PathRepresentation]: 

938 i = -1 

939 

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

941 zip(to_chop, path.natural_path) 

942 ): 

943 if isinstance(c_token, str): 

944 if i == 0 and ( 

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

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

947 ): 

948 return to_chop 

949 elif ( 

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

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

952 ): 

953 return None 

954 

955 if c_token is p_token: 

956 continue 

957 elif ( 

958 isinstance(c_token, InspectionAttr) 

959 and insp_is_mapper(c_token) 

960 and insp_is_mapper(p_token) 

961 and c_token.isa(p_token) 

962 ): 

963 continue 

964 

965 else: 

966 return None 

967 return to_chop[i + 1 :] 

968 

969 

970class Load(_AbstractLoad): 

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

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

973 order to affect how various mapped attributes are loaded. 

974 

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

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

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

978 except for in some very specific cases. 

979 

980 .. seealso:: 

981 

982 :ref:`orm_queryguide_relationship_per_entity_wildcard` - illustrates an 

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

984 

985 """ 

986 

987 __slots__ = ( 

988 "path", 

989 "context", 

990 "additional_source_entities", 

991 ) 

992 

993 _traverse_internals = [ 

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

995 ( 

996 "context", 

997 visitors.InternalTraversal.dp_has_cache_key_list, 

998 ), 

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

1000 ( 

1001 "additional_source_entities", 

1002 visitors.InternalTraversal.dp_has_cache_key_list, 

1003 ), 

1004 ] 

1005 _cache_key_traversal = None 

1006 

1007 path: PathRegistry 

1008 context: Tuple[_LoadElement, ...] 

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

1010 

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

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

1013 insp._post_inspect 

1014 

1015 self.path = insp._path_registry 

1016 self.context = () 

1017 self.propagate_to_loaders = False 

1018 self.additional_source_entities = () 

1019 

1020 def __str__(self) -> str: 

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

1022 

1023 @classmethod 

1024 def _construct_for_existing_path( 

1025 cls, path: AbstractEntityRegistry 

1026 ) -> Load: 

1027 load = cls.__new__(cls) 

1028 load.path = path 

1029 load.context = () 

1030 load.propagate_to_loaders = False 

1031 load.additional_source_entities = () 

1032 return load 

1033 

1034 def _adapt_cached_option_to_uncached_option( 

1035 self, context: QueryContext, uncached_opt: ORMOption 

1036 ) -> ORMOption: 

1037 if uncached_opt is self: 

1038 return self 

1039 return self._adjust_for_extra_criteria(context) 

1040 

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

1042 cloned = self._clone() 

1043 cloned.context = tuple( 

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

1045 ) 

1046 return cloned 

1047 

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

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

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

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

1052 

1053 """ 

1054 

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

1056 # actually have any extra_criteria options, which is the 

1057 # common case 

1058 for value in self.context: 

1059 if value._extra_criteria: 

1060 break 

1061 else: 

1062 return self 

1063 

1064 replacement_cache_key = context.user_passed_query._generate_cache_key() 

1065 

1066 if replacement_cache_key is None: 

1067 return self 

1068 

1069 orig_query = context.compile_state.select_statement 

1070 orig_cache_key = orig_query._generate_cache_key() 

1071 assert orig_cache_key is not None 

1072 

1073 def process( 

1074 opt: _LoadElement, 

1075 replacement_cache_key: CacheKey, 

1076 orig_cache_key: CacheKey, 

1077 ) -> _LoadElement: 

1078 cloned_opt = opt._clone() 

1079 

1080 cloned_opt._extra_criteria = tuple( 

1081 replacement_cache_key._apply_params_to_element( 

1082 orig_cache_key, crit 

1083 ) 

1084 for crit in cloned_opt._extra_criteria 

1085 ) 

1086 

1087 return cloned_opt 

1088 

1089 cloned = self._clone() 

1090 cloned.context = tuple( 

1091 ( 

1092 process(value, replacement_cache_key, orig_cache_key) 

1093 if value._extra_criteria 

1094 else value 

1095 ) 

1096 for value in self.context 

1097 ) 

1098 return cloned 

1099 

1100 def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr): 

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

1102 entity inside of _LoadElement objects. 

1103 

1104 """ 

1105 path = self.path 

1106 

1107 for ent in mapper_entities: 

1108 ezero = ent.entity_zero 

1109 if ezero and orm_util._entity_corresponds_to( 

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

1111 # safe to pass to _entity_corresponds_to() 

1112 ezero, 

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

1114 ): 

1115 return ezero 

1116 

1117 return None 

1118 

1119 def _process( 

1120 self, 

1121 compile_state: ORMCompileState, 

1122 mapper_entities: Sequence[_MapperEntity], 

1123 raiseerr: bool, 

1124 ) -> None: 

1125 reconciled_lead_entity = self._reconcile_query_entities_with_us( 

1126 mapper_entities, raiseerr 

1127 ) 

1128 

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

1130 has_current_path = bool(compile_state.compile_options._current_path) 

1131 

1132 for loader in self.context: 

1133 # issue #11292 

1134 # historically, propagate_to_loaders was only considered at 

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

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

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

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

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

1140 # so we check again 

1141 if has_current_path and not loader.propagate_to_loaders: 

1142 continue 

1143 loader.process_compile_state( 

1144 self, 

1145 compile_state, 

1146 mapper_entities, 

1147 reconciled_lead_entity, 

1148 raiseerr, 

1149 ) 

1150 

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

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

1153 :class:`_orm.Load` object. 

1154 

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

1156 

1157 """ 

1158 cloned = self._generate() 

1159 

1160 assert cloned.propagate_to_loaders == self.propagate_to_loaders 

1161 

1162 if not any( 

1163 orm_util._entity_corresponds_to_use_path_impl( 

1164 elem, cloned.path.odd_element(0) 

1165 ) 

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

1167 + parent.additional_source_entities 

1168 ): 

1169 if len(cloned.path) > 1: 

1170 attrname = cloned.path[1] 

1171 parent_entity = cloned.path[0] 

1172 else: 

1173 attrname = cloned.path[0] 

1174 parent_entity = cloned.path[0] 

1175 _raise_for_does_not_link(parent.path, attrname, parent_entity) 

1176 

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

1178 

1179 if self.context: 

1180 cloned.context = tuple( 

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

1182 ) 

1183 

1184 if cloned.context: 

1185 parent.context += cloned.context 

1186 parent.additional_source_entities += ( 

1187 cloned.additional_source_entities 

1188 ) 

1189 

1190 @_generative 

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

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

1193 :class:`_orm.Load` 

1194 object. 

1195 

1196 E.g.:: 

1197 

1198 query = session.query(Author) 

1199 query = query.options( 

1200 joinedload(Author.book).options( 

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

1202 joinedload(Book.citations).options(joinedload(Citation.author)), 

1203 ) 

1204 ) 

1205 

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

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

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

1209 

1210 .. versionadded:: 1.3.6 

1211 

1212 .. seealso:: 

1213 

1214 :func:`.defaultload` 

1215 

1216 :ref:`orm_queryguide_relationship_sub_options` 

1217 

1218 """ 

1219 for opt in opts: 

1220 try: 

1221 opt._apply_to_parent(self) 

1222 except AttributeError as ae: 

1223 if not isinstance(opt, _AbstractLoad): 

1224 raise sa_exc.ArgumentError( 

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

1226 "Load.options() method." 

1227 ) from ae 

1228 else: 

1229 raise 

1230 return self 

1231 

1232 def _clone_for_bind_strategy( 

1233 self, 

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

1235 strategy: Optional[_StrategyKey], 

1236 wildcard_key: Optional[_WildcardKeyType], 

1237 opts: Optional[_OptsType] = None, 

1238 attr_group: Optional[_AttrGroupType] = None, 

1239 propagate_to_loaders: bool = True, 

1240 reconcile_to_other: Optional[bool] = None, 

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

1242 ) -> Self: 

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

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

1245 # InstanceState.load_options 

1246 if propagate_to_loaders: 

1247 self.propagate_to_loaders = True 

1248 

1249 if self.path.is_token: 

1250 raise sa_exc.ArgumentError( 

1251 "Wildcard token cannot be followed by another entity" 

1252 ) 

1253 

1254 elif path_is_property(self.path): 

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

1256 # LoaderStrategyException 

1257 if strategy: 

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

1259 else: 

1260 raise sa_exc.ArgumentError( 

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

1262 "refer to a mapped entity" 

1263 ) 

1264 

1265 if attrs is None: 

1266 load_element = _ClassStrategyLoad.create( 

1267 self.path, 

1268 None, 

1269 strategy, 

1270 wildcard_key, 

1271 opts, 

1272 propagate_to_loaders, 

1273 attr_group=attr_group, 

1274 reconcile_to_other=reconcile_to_other, 

1275 extra_criteria=extra_criteria, 

1276 ) 

1277 if load_element: 

1278 self.context += (load_element,) 

1279 assert opts is not None 

1280 self.additional_source_entities += cast( 

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

1282 ) 

1283 

1284 else: 

1285 for attr in attrs: 

1286 if isinstance(attr, str): 

1287 load_element = _TokenStrategyLoad.create( 

1288 self.path, 

1289 attr, 

1290 strategy, 

1291 wildcard_key, 

1292 opts, 

1293 propagate_to_loaders, 

1294 attr_group=attr_group, 

1295 reconcile_to_other=reconcile_to_other, 

1296 extra_criteria=extra_criteria, 

1297 ) 

1298 else: 

1299 load_element = _AttributeStrategyLoad.create( 

1300 self.path, 

1301 attr, 

1302 strategy, 

1303 wildcard_key, 

1304 opts, 

1305 propagate_to_loaders, 

1306 attr_group=attr_group, 

1307 reconcile_to_other=reconcile_to_other, 

1308 extra_criteria=extra_criteria, 

1309 ) 

1310 

1311 if load_element: 

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

1313 # object with the latest path. 

1314 if wildcard_key is _RELATIONSHIP_TOKEN: 

1315 self.path = load_element.path 

1316 self.context += (load_element,) 

1317 

1318 # this seems to be effective for selectinloader, 

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

1320 # but does not work for immediateloader, which still 

1321 # must add additional options at load time 

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

1323 r1 = load_element._recurse() 

1324 self.context += (r1,) 

1325 

1326 return self 

1327 

1328 def __getstate__(self): 

1329 d = self._shallow_to_dict() 

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

1331 return d 

1332 

1333 def __setstate__(self, state): 

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

1335 self._shallow_from_dict(state) 

1336 

1337 

1338class _WildcardLoad(_AbstractLoad): 

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

1340 

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

1342 

1343 _traverse_internals = [ 

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

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

1346 ( 

1347 "local_opts", 

1348 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1349 ), 

1350 ] 

1351 cache_key_traversal: _CacheKeyTraversalType = None 

1352 

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

1354 local_opts: _OptsType 

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

1356 propagate_to_loaders = False 

1357 

1358 def __init__(self) -> None: 

1359 self.path = () 

1360 self.strategy = None 

1361 self.local_opts = util.EMPTY_DICT 

1362 

1363 def _clone_for_bind_strategy( 

1364 self, 

1365 attrs, 

1366 strategy, 

1367 wildcard_key, 

1368 opts=None, 

1369 attr_group=None, 

1370 propagate_to_loaders=True, 

1371 reconcile_to_other=None, 

1372 extra_criteria=None, 

1373 ): 

1374 assert attrs is not None 

1375 attr = attrs[0] 

1376 assert ( 

1377 wildcard_key 

1378 and isinstance(attr, str) 

1379 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN) 

1380 ) 

1381 

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

1383 

1384 self.strategy = strategy 

1385 self.path = (attr,) 

1386 if opts: 

1387 self.local_opts = util.immutabledict(opts) 

1388 

1389 assert extra_criteria is None 

1390 

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

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

1393 

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

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

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

1397 

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

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

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

1401 

1402 """ 

1403 assert self.path 

1404 attr = self.path[0] 

1405 if attr.endswith(_DEFAULT_TOKEN): 

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

1407 

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

1409 

1410 assert effective_path.is_token 

1411 

1412 loader = _TokenStrategyLoad.create( 

1413 effective_path, 

1414 None, 

1415 self.strategy, 

1416 None, 

1417 self.local_opts, 

1418 self.propagate_to_loaders, 

1419 ) 

1420 

1421 parent.context += (loader,) 

1422 

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

1424 is_refresh = compile_state.compile_options._for_refresh_state 

1425 

1426 if is_refresh and not self.propagate_to_loaders: 

1427 return 

1428 

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

1430 current_path = compile_state.current_path 

1431 

1432 start_path: _PathRepresentation = self.path 

1433 

1434 if current_path: 

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

1436 # None back here 

1437 new_path = self._chop_path(start_path, current_path) 

1438 if new_path is None: 

1439 return 

1440 

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

1442 # just returns it 

1443 assert new_path == start_path 

1444 

1445 # start_path is a single-token tuple 

1446 assert start_path and len(start_path) == 1 

1447 

1448 token = start_path[0] 

1449 assert isinstance(token, str) 

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

1451 

1452 if not entity: 

1453 return 

1454 

1455 path_element = entity 

1456 

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

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

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

1460 # tokens and populate into the Load(). 

1461 

1462 assert isinstance(token, str) 

1463 loader = _TokenStrategyLoad.create( 

1464 path_element._path_registry, 

1465 token, 

1466 self.strategy, 

1467 None, 

1468 self.local_opts, 

1469 self.propagate_to_loaders, 

1470 raiseerr=raiseerr, 

1471 ) 

1472 if not loader: 

1473 return 

1474 

1475 assert loader.path.is_token 

1476 

1477 # don't pass a reconciled lead entity here 

1478 loader.process_compile_state( 

1479 self, compile_state, mapper_entities, None, raiseerr 

1480 ) 

1481 

1482 return loader 

1483 

1484 def _find_entity_basestring( 

1485 self, 

1486 entities: Iterable[_InternalEntityType[Any]], 

1487 token: str, 

1488 raiseerr: bool, 

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

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

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

1492 if raiseerr: 

1493 raise sa_exc.ArgumentError( 

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

1495 f"loader option to multiple entities " 

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

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

1498 f"""{ 

1499 ", ".join( 

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

1501 for ent in entities 

1502 ) 

1503 }.""" 

1504 ) 

1505 elif token.endswith(_DEFAULT_TOKEN): 

1506 raiseerr = False 

1507 

1508 for ent in entities: 

1509 # return only the first _MapperEntity when searching 

1510 # based on string prop name. Ideally object 

1511 # attributes are used to specify more exactly. 

1512 return ent 

1513 else: 

1514 if raiseerr: 

1515 raise sa_exc.ArgumentError( 

1516 "Query has only expression-based entities - " 

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

1518 ) 

1519 else: 

1520 return None 

1521 

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

1523 d = self._shallow_to_dict() 

1524 return d 

1525 

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

1527 self._shallow_from_dict(state) 

1528 

1529 

1530class _LoadElement( 

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

1532): 

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

1534 and pass options to it. 

1535 

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

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

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

1539 

1540 .. versionadded:: 2.0 

1541 

1542 """ 

1543 

1544 __slots__ = ( 

1545 "path", 

1546 "strategy", 

1547 "propagate_to_loaders", 

1548 "local_opts", 

1549 "_extra_criteria", 

1550 "_reconcile_to_other", 

1551 ) 

1552 __visit_name__ = "load_element" 

1553 

1554 _traverse_internals = [ 

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

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

1557 ( 

1558 "local_opts", 

1559 visitors.ExtendedInternalTraversal.dp_string_multi_dict, 

1560 ), 

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

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

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

1564 ] 

1565 _cache_key_traversal = None 

1566 

1567 _extra_criteria: Tuple[Any, ...] 

1568 

1569 _reconcile_to_other: Optional[bool] 

1570 strategy: Optional[_StrategyKey] 

1571 path: PathRegistry 

1572 propagate_to_loaders: bool 

1573 

1574 local_opts: util.immutabledict[str, Any] 

1575 

1576 is_token_strategy: bool 

1577 is_class_strategy: bool 

1578 

1579 def __hash__(self) -> int: 

1580 return id(self) 

1581 

1582 def __eq__(self, other): 

1583 return traversals.compare(self, other) 

1584 

1585 @property 

1586 def is_opts_only(self) -> bool: 

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

1588 

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

1590 cls = self.__class__ 

1591 s = cls.__new__(cls) 

1592 

1593 self._shallow_copy_to(s) 

1594 return s 

1595 

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

1597 new = self._clone() 

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

1599 return new 

1600 

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

1602 d = self._shallow_to_dict() 

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

1604 return d 

1605 

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

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

1608 self._shallow_from_dict(state) 

1609 

1610 def _raise_for_no_match(self, parent_loader, mapper_entities): 

1611 path = parent_loader.path 

1612 

1613 found_entities = False 

1614 for ent in mapper_entities: 

1615 ezero = ent.entity_zero 

1616 if ezero: 

1617 found_entities = True 

1618 break 

1619 

1620 if not found_entities: 

1621 raise sa_exc.ArgumentError( 

1622 "Query has only expression-based entities; " 

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

1624 "be applied here." 

1625 ) 

1626 else: 

1627 raise sa_exc.ArgumentError( 

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

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

1630 f"""{ 

1631 ", ".join( 

1632 str(x.entity_zero) 

1633 for x in mapper_entities if x.entity_zero 

1634 )}. Please """ 

1635 "specify the full path " 

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

1637 "attribute. " 

1638 ) 

1639 

1640 def _adjust_effective_path_for_current_path( 

1641 self, effective_path: PathRegistry, current_path: PathRegistry 

1642 ) -> Optional[PathRegistry]: 

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

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

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

1646 current_path. 

1647 

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

1649 

1650 .. sourcecode:: text 

1651 

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

1653 

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

1655 

1656 The adjusted path would be: 

1657 

1658 .. sourcecode:: text 

1659 

1660 Item -> keywords -> Keyword 

1661 

1662 

1663 """ 

1664 chopped_start_path = Load._chop_path( 

1665 effective_path.natural_path, current_path 

1666 ) 

1667 if not chopped_start_path: 

1668 return None 

1669 

1670 tokens_removed_from_start_path = len(effective_path) - len( 

1671 chopped_start_path 

1672 ) 

1673 

1674 loader_lead_path_element = self.path[tokens_removed_from_start_path] 

1675 

1676 effective_path = PathRegistry.coerce( 

1677 (loader_lead_path_element,) + chopped_start_path[1:] 

1678 ) 

1679 

1680 return effective_path 

1681 

1682 def _init_path( 

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

1684 ): 

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

1686 a new path. 

1687 

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

1689 a :class:`._LoadElement` object. 

1690 

1691 """ 

1692 raise NotImplementedError() 

1693 

1694 def _prepare_for_compile_state( 

1695 self, 

1696 parent_loader, 

1697 compile_state, 

1698 mapper_entities, 

1699 reconciled_lead_entity, 

1700 raiseerr, 

1701 ): 

1702 """implemented by subclasses.""" 

1703 raise NotImplementedError() 

1704 

1705 def process_compile_state( 

1706 self, 

1707 parent_loader, 

1708 compile_state, 

1709 mapper_entities, 

1710 reconciled_lead_entity, 

1711 raiseerr, 

1712 ): 

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

1714 _LoadElement. 

1715 

1716 """ 

1717 keys = self._prepare_for_compile_state( 

1718 parent_loader, 

1719 compile_state, 

1720 mapper_entities, 

1721 reconciled_lead_entity, 

1722 raiseerr, 

1723 ) 

1724 for key in keys: 

1725 if key in compile_state.attributes: 

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

1727 self, compile_state.attributes[key] 

1728 ) 

1729 else: 

1730 compile_state.attributes[key] = self 

1731 

1732 @classmethod 

1733 def create( 

1734 cls, 

1735 path: PathRegistry, 

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

1737 strategy: Optional[_StrategyKey], 

1738 wildcard_key: Optional[_WildcardKeyType], 

1739 local_opts: Optional[_OptsType], 

1740 propagate_to_loaders: bool, 

1741 raiseerr: bool = True, 

1742 attr_group: Optional[_AttrGroupType] = None, 

1743 reconcile_to_other: Optional[bool] = None, 

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

1745 ) -> _LoadElement: 

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

1747 

1748 opt = cls.__new__(cls) 

1749 opt.path = path 

1750 opt.strategy = strategy 

1751 opt.propagate_to_loaders = propagate_to_loaders 

1752 opt.local_opts = ( 

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

1754 ) 

1755 opt._extra_criteria = () 

1756 

1757 if reconcile_to_other is not None: 

1758 opt._reconcile_to_other = reconcile_to_other 

1759 elif strategy is None and not local_opts: 

1760 opt._reconcile_to_other = True 

1761 else: 

1762 opt._reconcile_to_other = None 

1763 

1764 path = opt._init_path( 

1765 path, attr, wildcard_key, attr_group, raiseerr, extra_criteria 

1766 ) 

1767 

1768 if not path: 

1769 return None # type: ignore 

1770 

1771 assert opt.is_token_strategy == path.is_token 

1772 

1773 opt.path = path 

1774 return opt 

1775 

1776 def __init__(self) -> None: 

1777 raise NotImplementedError() 

1778 

1779 def _recurse(self) -> _LoadElement: 

1780 cloned = self._clone() 

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

1782 

1783 return cloned 

1784 

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

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

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

1788 path. 

1789 

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

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

1792 

1793 """ 

1794 

1795 if not any( 

1796 orm_util._entity_corresponds_to_use_path_impl( 

1797 elem, 

1798 self.path.odd_element(0), 

1799 ) 

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

1801 + parent.additional_source_entities 

1802 ): 

1803 raise sa_exc.ArgumentError( 

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

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

1806 ) 

1807 

1808 return self._prepend_path(parent.path) 

1809 

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

1811 cloned = self._clone() 

1812 

1813 assert cloned.strategy == self.strategy 

1814 assert cloned.local_opts == self.local_opts 

1815 assert cloned.is_class_strategy == self.is_class_strategy 

1816 

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

1818 

1819 return cloned 

1820 

1821 @staticmethod 

1822 def _reconcile( 

1823 replacement: _LoadElement, existing: _LoadElement 

1824 ) -> _LoadElement: 

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

1826 the context.attributes under the same key. 

1827 

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

1829 existing one 

1830 

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

1832 

1833 """ 

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

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

1836 # see test_poly_loading.py 

1837 

1838 if replacement._reconcile_to_other: 

1839 return existing 

1840 elif replacement._reconcile_to_other is False: 

1841 return replacement 

1842 elif existing._reconcile_to_other: 

1843 return replacement 

1844 elif existing._reconcile_to_other is False: 

1845 return existing 

1846 

1847 if existing is replacement: 

1848 return replacement 

1849 elif ( 

1850 existing.strategy == replacement.strategy 

1851 and existing.local_opts == replacement.local_opts 

1852 ): 

1853 return replacement 

1854 elif replacement.is_opts_only: 

1855 existing = existing._clone() 

1856 existing.local_opts = existing.local_opts.union( 

1857 replacement.local_opts 

1858 ) 

1859 existing._extra_criteria += replacement._extra_criteria 

1860 return existing 

1861 elif existing.is_opts_only: 

1862 replacement = replacement._clone() 

1863 replacement.local_opts = replacement.local_opts.union( 

1864 existing.local_opts 

1865 ) 

1866 replacement._extra_criteria += existing._extra_criteria 

1867 return replacement 

1868 elif replacement.path.is_token: 

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

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

1871 # will raise as below 

1872 return replacement 

1873 

1874 raise sa_exc.InvalidRequestError( 

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

1876 ) 

1877 

1878 

1879class _AttributeStrategyLoad(_LoadElement): 

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

1881 

1882 e.g.:: 

1883 

1884 joinedload(User.addresses) 

1885 defer(Order.name) 

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

1887 

1888 """ 

1889 

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

1891 

1892 __visit_name__ = "attribute_strategy_load_element" 

1893 

1894 _traverse_internals = _LoadElement._traverse_internals + [ 

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

1896 ( 

1897 "_path_with_polymorphic_path", 

1898 visitors.ExtendedInternalTraversal.dp_has_cache_key, 

1899 ), 

1900 ] 

1901 

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

1903 _path_with_polymorphic_path: Optional[PathRegistry] 

1904 

1905 is_class_strategy = False 

1906 is_token_strategy = False 

1907 

1908 def _init_path( 

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

1910 ): 

1911 assert attr is not None 

1912 self._of_type = None 

1913 self._path_with_polymorphic_path = None 

1914 insp, _, prop = _parse_attr_argument(attr) 

1915 

1916 if insp.is_property: 

1917 # direct property can be sent from internal strategy logic 

1918 # that sets up specific loaders, such as 

1919 # emit_lazyload->_lazyload_reverse 

1920 # prop = found_property = attr 

1921 prop = attr 

1922 path = path[prop] 

1923 

1924 if path.has_entity: 

1925 path = path.entity_path 

1926 return path 

1927 

1928 elif not insp.is_attribute: 

1929 # should not reach here; 

1930 assert False 

1931 

1932 # here we assume we have user-passed InstrumentedAttribute 

1933 if not orm_util._entity_corresponds_to_use_path_impl( 

1934 path[-1], attr.parent 

1935 ): 

1936 if raiseerr: 

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

1938 raise sa_exc.ArgumentError( 

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

1940 "loader option to multiple entities in the " 

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

1942 ) 

1943 else: 

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

1945 else: 

1946 return None 

1947 

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

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

1950 # test_relationship_criteria.py::RelationshipCriteriaTest:: 

1951 # test_selectinload_nested_criteria[True] if an existing 

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

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

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

1955 # poorly defined. 

1956 if extra_criteria: 

1957 assert not attr._extra_criteria 

1958 self._extra_criteria = extra_criteria 

1959 else: 

1960 self._extra_criteria = attr._extra_criteria 

1961 

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

1963 ac = attr._of_type 

1964 ext_info = inspect(ac) 

1965 self._of_type = ext_info 

1966 

1967 self._path_with_polymorphic_path = path.entity_path[prop] 

1968 

1969 path = path[prop][ext_info] 

1970 

1971 else: 

1972 path = path[prop] 

1973 

1974 if path.has_entity: 

1975 path = path.entity_path 

1976 

1977 return path 

1978 

1979 def _generate_extra_criteria(self, context): 

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

1981 immediate "extra_criteria" stored with this Load object. 

1982 

1983 Load objects are typically pulled from the cached version of 

1984 the statement from a QueryContext. The statement currently being 

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

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

1987 they handle this criteria for a result set. 

1988 

1989 """ 

1990 

1991 assert ( 

1992 self._extra_criteria 

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

1994 

1995 orig_query = context.compile_state.select_statement 

1996 current_query = context.query 

1997 

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

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

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

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

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

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

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

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

2006 

2007 # if orig_query is current_query: 

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

2009 # return and_(*self._extra_criteria) 

2010 

2011 k1 = orig_query._generate_cache_key() 

2012 k2 = current_query._generate_cache_key() 

2013 

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

2015 

2016 def _set_of_type_info(self, context, current_path): 

2017 assert self._path_with_polymorphic_path 

2018 

2019 pwpi = self._of_type 

2020 assert pwpi 

2021 if not pwpi.is_aliased_class: 

2022 pwpi = inspect( 

2023 orm_util.AliasedInsp._with_polymorphic_factory( 

2024 pwpi.mapper.base_mapper, 

2025 (pwpi.mapper,), 

2026 aliased=True, 

2027 _use_mapper_path=True, 

2028 ) 

2029 ) 

2030 start_path = self._path_with_polymorphic_path 

2031 if current_path: 

2032 new_path = self._adjust_effective_path_for_current_path( 

2033 start_path, current_path 

2034 ) 

2035 if new_path is None: 

2036 return 

2037 start_path = new_path 

2038 

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

2040 if key in context: 

2041 existing_aliased_insp = context[key] 

2042 this_aliased_insp = pwpi 

2043 new_aliased_insp = existing_aliased_insp._merge_with( 

2044 this_aliased_insp 

2045 ) 

2046 context[key] = new_aliased_insp 

2047 else: 

2048 context[key] = pwpi 

2049 

2050 def _prepare_for_compile_state( 

2051 self, 

2052 parent_loader, 

2053 compile_state, 

2054 mapper_entities, 

2055 reconciled_lead_entity, 

2056 raiseerr, 

2057 ): 

2058 # _AttributeStrategyLoad 

2059 

2060 current_path = compile_state.current_path 

2061 is_refresh = compile_state.compile_options._for_refresh_state 

2062 assert not self.path.is_token 

2063 

2064 if is_refresh and not self.propagate_to_loaders: 

2065 return [] 

2066 

2067 if self._of_type: 

2068 # apply additional with_polymorphic alias that may have been 

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

2070 self._set_of_type_info(compile_state.attributes, current_path) 

2071 

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

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

2074 return [] 

2075 

2076 if raiseerr and not reconciled_lead_entity: 

2077 self._raise_for_no_match(parent_loader, mapper_entities) 

2078 

2079 if self.path.has_entity: 

2080 effective_path = self.path.parent 

2081 else: 

2082 effective_path = self.path 

2083 

2084 if current_path: 

2085 assert effective_path is not None 

2086 effective_path = self._adjust_effective_path_for_current_path( 

2087 effective_path, current_path 

2088 ) 

2089 if effective_path is None: 

2090 return [] 

2091 

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

2093 

2094 def __getstate__(self): 

2095 d = super().__getstate__() 

2096 

2097 # can't pickle this. See 

2098 # test_pickled.py -> test_lazyload_extra_criteria_not_supported 

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

2100 # would be non-None 

2101 d["_extra_criteria"] = () 

2102 

2103 if self._path_with_polymorphic_path: 

2104 d["_path_with_polymorphic_path"] = ( 

2105 self._path_with_polymorphic_path.serialize() 

2106 ) 

2107 

2108 if self._of_type: 

2109 if self._of_type.is_aliased_class: 

2110 d["_of_type"] = None 

2111 elif self._of_type.is_mapper: 

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

2113 else: 

2114 assert False, "unexpected object for _of_type" 

2115 

2116 return d 

2117 

2118 def __setstate__(self, state): 

2119 super().__setstate__(state) 

2120 

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

2122 self._path_with_polymorphic_path = PathRegistry.deserialize( 

2123 state["_path_with_polymorphic_path"] 

2124 ) 

2125 else: 

2126 self._path_with_polymorphic_path = None 

2127 

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

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

2130 else: 

2131 self._of_type = None 

2132 

2133 

2134class _TokenStrategyLoad(_LoadElement): 

2135 """Loader strategies against wildcard attributes 

2136 

2137 e.g.:: 

2138 

2139 raiseload("*") 

2140 Load(User).lazyload("*") 

2141 defer("*") 

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

2143 joinedload(User.addresses).raiseload("*") 

2144 

2145 """ 

2146 

2147 __visit_name__ = "token_strategy_load_element" 

2148 

2149 inherit_cache = True 

2150 is_class_strategy = False 

2151 is_token_strategy = True 

2152 

2153 def _init_path( 

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

2155 ): 

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

2157 if attr is not None: 

2158 default_token = attr.endswith(_DEFAULT_TOKEN) 

2159 if attr.endswith(_WILDCARD_TOKEN) or default_token: 

2160 if wildcard_key: 

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

2162 

2163 path = path.token(attr) 

2164 return path 

2165 else: 

2166 raise sa_exc.ArgumentError( 

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

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

2169 ) 

2170 return path 

2171 

2172 def _prepare_for_compile_state( 

2173 self, 

2174 parent_loader, 

2175 compile_state, 

2176 mapper_entities, 

2177 reconciled_lead_entity, 

2178 raiseerr, 

2179 ): 

2180 # _TokenStrategyLoad 

2181 

2182 current_path = compile_state.current_path 

2183 is_refresh = compile_state.compile_options._for_refresh_state 

2184 

2185 assert self.path.is_token 

2186 

2187 if is_refresh and not self.propagate_to_loaders: 

2188 return [] 

2189 

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

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

2192 return [] 

2193 

2194 effective_path = self.path 

2195 if reconciled_lead_entity: 

2196 effective_path = PathRegistry.coerce( 

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

2198 ) 

2199 

2200 if current_path: 

2201 new_effective_path = self._adjust_effective_path_for_current_path( 

2202 effective_path, current_path 

2203 ) 

2204 if new_effective_path is None: 

2205 return [] 

2206 effective_path = new_effective_path 

2207 

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

2209 # to encompass everything from the query entity on 

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

2211 # is set. 

2212 

2213 return [ 

2214 ("loader", natural_path) 

2215 for natural_path in ( 

2216 cast( 

2217 TokenRegistry, effective_path 

2218 )._generate_natural_for_superclasses() 

2219 ) 

2220 ] 

2221 

2222 

2223class _ClassStrategyLoad(_LoadElement): 

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

2225 an attribute path 

2226 

2227 e.g.:: 

2228 

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

2230 selectin_polymorphic(Person, [Engineer, Manager]) 

2231 ) 

2232 

2233 """ 

2234 

2235 inherit_cache = True 

2236 is_class_strategy = True 

2237 is_token_strategy = False 

2238 

2239 __visit_name__ = "class_strategy_load_element" 

2240 

2241 def _init_path( 

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

2243 ): 

2244 return path 

2245 

2246 def _prepare_for_compile_state( 

2247 self, 

2248 parent_loader, 

2249 compile_state, 

2250 mapper_entities, 

2251 reconciled_lead_entity, 

2252 raiseerr, 

2253 ): 

2254 # _ClassStrategyLoad 

2255 

2256 current_path = compile_state.current_path 

2257 is_refresh = compile_state.compile_options._for_refresh_state 

2258 

2259 if is_refresh and not self.propagate_to_loaders: 

2260 return [] 

2261 

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

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

2264 return [] 

2265 

2266 effective_path = self.path 

2267 

2268 if current_path: 

2269 new_effective_path = self._adjust_effective_path_for_current_path( 

2270 effective_path, current_path 

2271 ) 

2272 if new_effective_path is None: 

2273 return [] 

2274 effective_path = new_effective_path 

2275 

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

2277 

2278 

2279def _generate_from_keys( 

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

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

2282 chained: bool, 

2283 kw: Any, 

2284) -> _AbstractLoad: 

2285 lead_element: Optional[_AbstractLoad] = None 

2286 

2287 attr: Any 

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

2289 for attr in _keys: 

2290 if isinstance(attr, str): 

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

2292 util.warn_deprecated( 

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

2294 "deprecated " 

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

2296 "it is " 

2297 "believed to be unused. " 

2298 "If you have been using this functionality, " 

2299 "please " 

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

2301 "tracker.", 

2302 version="1.4", 

2303 ) 

2304 attr = attr[1:] 

2305 

2306 if attr == _WILDCARD_TOKEN: 

2307 if is_default: 

2308 raise sa_exc.ArgumentError( 

2309 "Wildcard token cannot be followed by " 

2310 "another entity", 

2311 ) 

2312 

2313 if lead_element is None: 

2314 lead_element = _WildcardLoad() 

2315 

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

2317 

2318 else: 

2319 raise sa_exc.ArgumentError( 

2320 "Strings are not accepted for attribute names in " 

2321 "loader options; please use class-bound " 

2322 "attributes directly.", 

2323 ) 

2324 else: 

2325 if lead_element is None: 

2326 _, lead_entity, _ = _parse_attr_argument(attr) 

2327 lead_element = Load(lead_entity) 

2328 

2329 if is_default: 

2330 if not chained: 

2331 lead_element = lead_element.defaultload(attr) 

2332 else: 

2333 lead_element = meth( 

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

2335 ) 

2336 else: 

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

2338 

2339 assert lead_element 

2340 return lead_element 

2341 

2342 

2343def _parse_attr_argument( 

2344 attr: _AttrType, 

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

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

2347 :class:`._AbstractLoad` instance. 

2348 

2349 This is used by the standalone loader strategy functions like 

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

2351 :class:`._WildcardLoad` objects. 

2352 

2353 """ 

2354 try: 

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

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

2357 # if at all 

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

2359 except sa_exc.NoInspectionAvailable as err: 

2360 raise sa_exc.ArgumentError( 

2361 "expected ORM mapped attribute for loader strategy argument" 

2362 ) from err 

2363 

2364 lead_entity: _InternalEntityType[Any] 

2365 

2366 if insp_is_mapper_property(insp): 

2367 lead_entity = insp.parent 

2368 prop = insp 

2369 elif insp_is_attribute(insp): 

2370 lead_entity = insp.parent 

2371 prop = insp.prop 

2372 else: 

2373 raise sa_exc.ArgumentError( 

2374 "expected ORM mapped attribute for loader strategy argument" 

2375 ) 

2376 

2377 return insp, lead_entity, prop 

2378 

2379 

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

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

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

2383 

2384 """ 

2385 bound_fn = getattr(_AbstractLoad, fn.__name__) 

2386 fn_doc = bound_fn.__doc__ 

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

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

2389 

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

2391 

2392""" 

2393 

2394 fn.__doc__ = fn_doc 

2395 return fn 

2396 

2397 

2398def _expand_column_strategy_attrs( 

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

2400) -> Tuple[_AttrType, ...]: 

2401 return cast( 

2402 "Tuple[_AttrType, ...]", 

2403 tuple( 

2404 a 

2405 for attr in attrs 

2406 for a in ( 

2407 cast("QueryableAttribute[Any]", attr)._column_strategy_attrs() 

2408 if hasattr(attr, "_column_strategy_attrs") 

2409 else (attr,) 

2410 ) 

2411 ), 

2412 ) 

2413 

2414 

2415# standalone functions follow. docstrings are filled in 

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

2417 

2418 

2419@loader_unbound_fn 

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

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

2422 

2423 

2424@loader_unbound_fn 

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

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

2427 # add some extra state to Load of some kind 

2428 attrs = _expand_column_strategy_attrs(attrs) 

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

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

2431 

2432 

2433@loader_unbound_fn 

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

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

2436 

2437 

2438@loader_unbound_fn 

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

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

2441 

2442 

2443@loader_unbound_fn 

2444def selectinload( 

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

2446) -> _AbstractLoad: 

2447 return _generate_from_keys( 

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

2449 ) 

2450 

2451 

2452@loader_unbound_fn 

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

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

2455 

2456 

2457@loader_unbound_fn 

2458def immediateload( 

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

2460) -> _AbstractLoad: 

2461 return _generate_from_keys( 

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

2463 ) 

2464 

2465 

2466@loader_unbound_fn 

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

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

2469 

2470 

2471@loader_unbound_fn 

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

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

2474 

2475 

2476@loader_unbound_fn 

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

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

2479 

2480 

2481@loader_unbound_fn 

2482def defer( 

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

2484) -> _AbstractLoad: 

2485 if addl_attrs: 

2486 util.warn_deprecated( 

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

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

2489 "indicate a path.", 

2490 version="1.3", 

2491 ) 

2492 

2493 if raiseload: 

2494 kw = {"raiseload": raiseload} 

2495 else: 

2496 kw = {} 

2497 

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

2499 

2500 

2501@loader_unbound_fn 

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

2503 if addl_attrs: 

2504 util.warn_deprecated( 

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

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

2507 "indicate a path.", 

2508 version="1.3", 

2509 ) 

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

2511 

2512 

2513@loader_unbound_fn 

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

2515 element = _WildcardLoad() 

2516 return element.undefer_group(name) 

2517 

2518 

2519@loader_unbound_fn 

2520def with_expression( 

2521 key: _AttrType, expression: _ColumnExpressionArgument[Any] 

2522) -> _AbstractLoad: 

2523 return _generate_from_keys( 

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

2525 ) 

2526 

2527 

2528@loader_unbound_fn 

2529def selectin_polymorphic( 

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

2531) -> _AbstractLoad: 

2532 ul = Load(base_cls) 

2533 return ul.selectin_polymorphic(classes) 

2534 

2535 

2536def _raise_for_does_not_link(path, attrname, parent_entity): 

2537 if len(path) > 1: 

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

2539 if insp_is_aliased_class(parent_entity): 

2540 parent_entity_str = str(parent_entity) 

2541 else: 

2542 parent_entity_str = parent_entity.class_.__name__ 

2543 

2544 raise sa_exc.ArgumentError( 

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

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

2547 % ( 

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

2549 ( 

2550 " Did you mean to use " 

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

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

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

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

2555 if not path_is_of_type 

2556 and not path[-1].is_aliased_class 

2557 and orm_util._entity_corresponds_to( 

2558 path.entity, inspect(parent_entity).mapper 

2559 ) 

2560 else "" 

2561 ), 

2562 ) 

2563 ) 

2564 else: 

2565 raise sa_exc.ArgumentError( 

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

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

2568 )