path_registry.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. # orm/path_registry.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. """Path tracking utilities, representing mapper graph traversals."""
  8. from __future__ import annotations
  9. from functools import reduce
  10. from itertools import chain
  11. import logging
  12. import operator
  13. from typing import Any
  14. from typing import cast
  15. from typing import Dict
  16. from typing import Iterator
  17. from typing import List
  18. from typing import Optional
  19. from typing import overload
  20. from typing import Sequence
  21. from typing import Tuple
  22. from typing import TYPE_CHECKING
  23. from typing import Union
  24. from . import base as orm_base
  25. from ._typing import insp_is_mapper_property
  26. from .. import exc
  27. from .. import util
  28. from ..sql import visitors
  29. from ..sql.cache_key import HasCacheKey
  30. if TYPE_CHECKING:
  31. from ._typing import _InternalEntityType
  32. from .interfaces import StrategizedProperty
  33. from .mapper import Mapper
  34. from .relationships import RelationshipProperty
  35. from .util import AliasedInsp
  36. from ..sql.cache_key import _CacheKeyTraversalType
  37. from ..sql.elements import BindParameter
  38. from ..sql.visitors import anon_map
  39. from ..util.typing import _LiteralStar
  40. from ..util.typing import TypeGuard
  41. def is_root(path: PathRegistry) -> TypeGuard[RootRegistry]: ...
  42. def is_entity(path: PathRegistry) -> TypeGuard[AbstractEntityRegistry]: ...
  43. else:
  44. is_root = operator.attrgetter("is_root")
  45. is_entity = operator.attrgetter("is_entity")
  46. _SerializedPath = List[Any]
  47. _StrPathToken = str
  48. _PathElementType = Union[
  49. _StrPathToken, "_InternalEntityType[Any]", "StrategizedProperty[Any]"
  50. ]
  51. # the representation is in fact
  52. # a tuple with alternating:
  53. # [_InternalEntityType[Any], Union[str, StrategizedProperty[Any]],
  54. # _InternalEntityType[Any], Union[str, StrategizedProperty[Any]], ...]
  55. # this might someday be a tuple of 2-tuples instead, but paths can be
  56. # chopped at odd intervals as well so this is less flexible
  57. _PathRepresentation = Tuple[_PathElementType, ...]
  58. # NOTE: these names are weird since the array is 0-indexed,
  59. # the "_Odd" entries are at 0, 2, 4, etc
  60. _OddPathRepresentation = Sequence["_InternalEntityType[Any]"]
  61. _EvenPathRepresentation = Sequence[Union["StrategizedProperty[Any]", str]]
  62. log = logging.getLogger(__name__)
  63. def _unreduce_path(path: _SerializedPath) -> PathRegistry:
  64. return PathRegistry.deserialize(path)
  65. _WILDCARD_TOKEN: _LiteralStar = "*"
  66. _DEFAULT_TOKEN = "_sa_default"
  67. class PathRegistry(HasCacheKey):
  68. """Represent query load paths and registry functions.
  69. Basically represents structures like:
  70. (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
  71. These structures are generated by things like
  72. query options (joinedload(), subqueryload(), etc.) and are
  73. used to compose keys stored in the query._attributes dictionary
  74. for various options.
  75. They are then re-composed at query compile/result row time as
  76. the query is formed and as rows are fetched, where they again
  77. serve to compose keys to look up options in the context.attributes
  78. dictionary, which is copied from query._attributes.
  79. The path structure has a limited amount of caching, where each
  80. "root" ultimately pulls from a fixed registry associated with
  81. the first mapper, that also contains elements for each of its
  82. property keys. However paths longer than two elements, which
  83. are the exception rather than the rule, are generated on an
  84. as-needed basis.
  85. """
  86. __slots__ = ()
  87. is_token = False
  88. is_root = False
  89. has_entity = False
  90. is_property = False
  91. is_entity = False
  92. is_unnatural: bool
  93. path: _PathRepresentation
  94. natural_path: _PathRepresentation
  95. parent: Optional[PathRegistry]
  96. root: RootRegistry
  97. _cache_key_traversal: _CacheKeyTraversalType = [
  98. ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list)
  99. ]
  100. def __eq__(self, other: Any) -> bool:
  101. try:
  102. return other is not None and self.path == other._path_for_compare
  103. except AttributeError:
  104. util.warn(
  105. "Comparison of PathRegistry to %r is not supported"
  106. % (type(other))
  107. )
  108. return False
  109. def __ne__(self, other: Any) -> bool:
  110. try:
  111. return other is None or self.path != other._path_for_compare
  112. except AttributeError:
  113. util.warn(
  114. "Comparison of PathRegistry to %r is not supported"
  115. % (type(other))
  116. )
  117. return True
  118. @property
  119. def _path_for_compare(self) -> Optional[_PathRepresentation]:
  120. return self.path
  121. def odd_element(self, index: int) -> _InternalEntityType[Any]:
  122. return self.path[index] # type: ignore
  123. def set(self, attributes: Dict[Any, Any], key: Any, value: Any) -> None:
  124. log.debug("set '%s' on path '%s' to '%s'", key, self, value)
  125. attributes[(key, self.natural_path)] = value
  126. def setdefault(
  127. self, attributes: Dict[Any, Any], key: Any, value: Any
  128. ) -> None:
  129. log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
  130. attributes.setdefault((key, self.natural_path), value)
  131. def get(
  132. self, attributes: Dict[Any, Any], key: Any, value: Optional[Any] = None
  133. ) -> Any:
  134. key = (key, self.natural_path)
  135. if key in attributes:
  136. return attributes[key]
  137. else:
  138. return value
  139. def __len__(self) -> int:
  140. return len(self.path)
  141. def __hash__(self) -> int:
  142. return id(self)
  143. @overload
  144. def __getitem__(self, entity: _StrPathToken) -> TokenRegistry: ...
  145. @overload
  146. def __getitem__(self, entity: int) -> _PathElementType: ...
  147. @overload
  148. def __getitem__(self, entity: slice) -> _PathRepresentation: ...
  149. @overload
  150. def __getitem__(
  151. self, entity: _InternalEntityType[Any]
  152. ) -> AbstractEntityRegistry: ...
  153. @overload
  154. def __getitem__(
  155. self, entity: StrategizedProperty[Any]
  156. ) -> PropRegistry: ...
  157. def __getitem__(
  158. self,
  159. entity: Union[
  160. _StrPathToken,
  161. int,
  162. slice,
  163. _InternalEntityType[Any],
  164. StrategizedProperty[Any],
  165. ],
  166. ) -> Union[
  167. TokenRegistry,
  168. _PathElementType,
  169. _PathRepresentation,
  170. PropRegistry,
  171. AbstractEntityRegistry,
  172. ]:
  173. raise NotImplementedError()
  174. # TODO: what are we using this for?
  175. @property
  176. def length(self) -> int:
  177. return len(self.path)
  178. def pairs(
  179. self,
  180. ) -> Iterator[
  181. Tuple[_InternalEntityType[Any], Union[str, StrategizedProperty[Any]]]
  182. ]:
  183. odd_path = cast(_OddPathRepresentation, self.path)
  184. even_path = cast(_EvenPathRepresentation, odd_path)
  185. for i in range(0, len(odd_path), 2):
  186. yield odd_path[i], even_path[i + 1]
  187. def contains_mapper(self, mapper: Mapper[Any]) -> bool:
  188. _m_path = cast(_OddPathRepresentation, self.path)
  189. for path_mapper in [_m_path[i] for i in range(0, len(_m_path), 2)]:
  190. if path_mapper.mapper.isa(mapper):
  191. return True
  192. else:
  193. return False
  194. def contains(self, attributes: Dict[Any, Any], key: Any) -> bool:
  195. return (key, self.path) in attributes
  196. def __reduce__(self) -> Any:
  197. return _unreduce_path, (self.serialize(),)
  198. @classmethod
  199. def _serialize_path(cls, path: _PathRepresentation) -> _SerializedPath:
  200. _m_path = cast(_OddPathRepresentation, path)
  201. _p_path = cast(_EvenPathRepresentation, path)
  202. return list(
  203. zip(
  204. tuple(
  205. m.class_ if (m.is_mapper or m.is_aliased_class) else str(m)
  206. for m in [_m_path[i] for i in range(0, len(_m_path), 2)]
  207. ),
  208. tuple(
  209. p.key if insp_is_mapper_property(p) else str(p)
  210. for p in [_p_path[i] for i in range(1, len(_p_path), 2)]
  211. )
  212. + (None,),
  213. )
  214. )
  215. @classmethod
  216. def _deserialize_path(cls, path: _SerializedPath) -> _PathRepresentation:
  217. def _deserialize_mapper_token(mcls: Any) -> Any:
  218. return (
  219. # note: we likely dont want configure=True here however
  220. # this is maintained at the moment for backwards compatibility
  221. orm_base._inspect_mapped_class(mcls, configure=True)
  222. if mcls not in PathToken._intern
  223. else PathToken._intern[mcls]
  224. )
  225. def _deserialize_key_token(mcls: Any, key: Any) -> Any:
  226. if key is None:
  227. return None
  228. elif key in PathToken._intern:
  229. return PathToken._intern[key]
  230. else:
  231. mp = orm_base._inspect_mapped_class(mcls, configure=True)
  232. assert mp is not None
  233. return mp.attrs[key]
  234. p = tuple(
  235. chain(
  236. *[
  237. (
  238. _deserialize_mapper_token(mcls),
  239. _deserialize_key_token(mcls, key),
  240. )
  241. for mcls, key in path
  242. ]
  243. )
  244. )
  245. if p and p[-1] is None:
  246. p = p[0:-1]
  247. return p
  248. def serialize(self) -> _SerializedPath:
  249. path = self.path
  250. return self._serialize_path(path)
  251. @classmethod
  252. def deserialize(cls, path: _SerializedPath) -> PathRegistry:
  253. assert path is not None
  254. p = cls._deserialize_path(path)
  255. return cls.coerce(p)
  256. @overload
  257. @classmethod
  258. def per_mapper(cls, mapper: Mapper[Any]) -> CachingEntityRegistry: ...
  259. @overload
  260. @classmethod
  261. def per_mapper(cls, mapper: AliasedInsp[Any]) -> SlotsEntityRegistry: ...
  262. @classmethod
  263. def per_mapper(
  264. cls, mapper: _InternalEntityType[Any]
  265. ) -> AbstractEntityRegistry:
  266. if mapper.is_mapper:
  267. return CachingEntityRegistry(cls.root, mapper)
  268. else:
  269. return SlotsEntityRegistry(cls.root, mapper)
  270. @classmethod
  271. def coerce(cls, raw: _PathRepresentation) -> PathRegistry:
  272. def _red(prev: PathRegistry, next_: _PathElementType) -> PathRegistry:
  273. return prev[next_]
  274. # can't quite get mypy to appreciate this one :)
  275. return reduce(_red, raw, cls.root) # type: ignore
  276. def __add__(self, other: PathRegistry) -> PathRegistry:
  277. def _red(prev: PathRegistry, next_: _PathElementType) -> PathRegistry:
  278. return prev[next_]
  279. return reduce(_red, other.path, self)
  280. def __str__(self) -> str:
  281. return f"ORM Path[{' -> '.join(str(elem) for elem in self.path)}]"
  282. def __repr__(self) -> str:
  283. return f"{self.__class__.__name__}({self.path!r})"
  284. class CreatesToken(PathRegistry):
  285. __slots__ = ()
  286. is_aliased_class: bool
  287. is_root: bool
  288. def token(self, token: _StrPathToken) -> TokenRegistry:
  289. if token.endswith(f":{_WILDCARD_TOKEN}"):
  290. return TokenRegistry(self, token)
  291. elif token.endswith(f":{_DEFAULT_TOKEN}"):
  292. return TokenRegistry(self.root, token)
  293. else:
  294. raise exc.ArgumentError(f"invalid token: {token}")
  295. class RootRegistry(CreatesToken):
  296. """Root registry, defers to mappers so that
  297. paths are maintained per-root-mapper.
  298. """
  299. __slots__ = ()
  300. inherit_cache = True
  301. path = natural_path = ()
  302. has_entity = False
  303. is_aliased_class = False
  304. is_root = True
  305. is_unnatural = False
  306. def _getitem(
  307. self, entity: Any
  308. ) -> Union[TokenRegistry, AbstractEntityRegistry]:
  309. if entity in PathToken._intern:
  310. if TYPE_CHECKING:
  311. assert isinstance(entity, _StrPathToken)
  312. return TokenRegistry(self, PathToken._intern[entity])
  313. else:
  314. try:
  315. return entity._path_registry # type: ignore
  316. except AttributeError:
  317. raise IndexError(
  318. f"invalid argument for RootRegistry.__getitem__: {entity}"
  319. )
  320. def _truncate_recursive(self) -> RootRegistry:
  321. return self
  322. if not TYPE_CHECKING:
  323. __getitem__ = _getitem
  324. PathRegistry.root = RootRegistry()
  325. class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
  326. """cacheable string token"""
  327. _intern: Dict[str, PathToken] = {}
  328. def _gen_cache_key(
  329. self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
  330. ) -> Tuple[Any, ...]:
  331. return (str(self),)
  332. @property
  333. def _path_for_compare(self) -> Optional[_PathRepresentation]:
  334. return None
  335. @classmethod
  336. def intern(cls, strvalue: str) -> PathToken:
  337. if strvalue in cls._intern:
  338. return cls._intern[strvalue]
  339. else:
  340. cls._intern[strvalue] = result = PathToken(strvalue)
  341. return result
  342. class TokenRegistry(PathRegistry):
  343. __slots__ = ("token", "parent", "path", "natural_path")
  344. inherit_cache = True
  345. token: _StrPathToken
  346. parent: CreatesToken
  347. def __init__(self, parent: CreatesToken, token: _StrPathToken):
  348. token = PathToken.intern(token)
  349. self.token = token
  350. self.parent = parent
  351. self.path = parent.path + (token,)
  352. self.natural_path = parent.natural_path + (token,)
  353. has_entity = False
  354. is_token = True
  355. def generate_for_superclasses(self) -> Iterator[PathRegistry]:
  356. # NOTE: this method is no longer used. consider removal
  357. parent = self.parent
  358. if is_root(parent):
  359. yield self
  360. return
  361. if TYPE_CHECKING:
  362. assert isinstance(parent, AbstractEntityRegistry)
  363. if not parent.is_aliased_class:
  364. for mp_ent in parent.mapper.iterate_to_root():
  365. yield TokenRegistry(parent.parent[mp_ent], self.token)
  366. elif (
  367. parent.is_aliased_class
  368. and cast(
  369. "AliasedInsp[Any]",
  370. parent.entity,
  371. )._is_with_polymorphic
  372. ):
  373. yield self
  374. for ent in cast(
  375. "AliasedInsp[Any]", parent.entity
  376. )._with_polymorphic_entities:
  377. yield TokenRegistry(parent.parent[ent], self.token)
  378. else:
  379. yield self
  380. def _generate_natural_for_superclasses(
  381. self,
  382. ) -> Iterator[_PathRepresentation]:
  383. parent = self.parent
  384. if is_root(parent):
  385. yield self.natural_path
  386. return
  387. if TYPE_CHECKING:
  388. assert isinstance(parent, AbstractEntityRegistry)
  389. for mp_ent in parent.mapper.iterate_to_root():
  390. yield TokenRegistry(parent.parent[mp_ent], self.token).natural_path
  391. if (
  392. parent.is_aliased_class
  393. and cast(
  394. "AliasedInsp[Any]",
  395. parent.entity,
  396. )._is_with_polymorphic
  397. ):
  398. yield self.natural_path
  399. for ent in cast(
  400. "AliasedInsp[Any]", parent.entity
  401. )._with_polymorphic_entities:
  402. yield (
  403. TokenRegistry(parent.parent[ent], self.token).natural_path
  404. )
  405. else:
  406. yield self.natural_path
  407. def _getitem(self, entity: Any) -> Any:
  408. try:
  409. return self.path[entity]
  410. except TypeError as err:
  411. raise IndexError(f"{entity}") from err
  412. if not TYPE_CHECKING:
  413. __getitem__ = _getitem
  414. class PropRegistry(PathRegistry):
  415. __slots__ = (
  416. "prop",
  417. "parent",
  418. "path",
  419. "natural_path",
  420. "has_entity",
  421. "entity",
  422. "mapper",
  423. "_wildcard_path_loader_key",
  424. "_default_path_loader_key",
  425. "_loader_key",
  426. "is_unnatural",
  427. )
  428. inherit_cache = True
  429. is_property = True
  430. prop: StrategizedProperty[Any]
  431. mapper: Optional[Mapper[Any]]
  432. entity: Optional[_InternalEntityType[Any]]
  433. def __init__(
  434. self, parent: AbstractEntityRegistry, prop: StrategizedProperty[Any]
  435. ):
  436. # restate this path in terms of the
  437. # given StrategizedProperty's parent.
  438. insp = cast("_InternalEntityType[Any]", parent[-1])
  439. natural_parent: AbstractEntityRegistry = parent
  440. # inherit "is_unnatural" from the parent
  441. self.is_unnatural = parent.parent.is_unnatural or bool(
  442. parent.mapper.inherits
  443. )
  444. if not insp.is_aliased_class or insp._use_mapper_path: # type: ignore
  445. parent = natural_parent = parent.parent[prop.parent]
  446. elif (
  447. insp.is_aliased_class
  448. and insp.with_polymorphic_mappers
  449. and prop.parent in insp.with_polymorphic_mappers
  450. ):
  451. subclass_entity: _InternalEntityType[Any] = parent[-1]._entity_for_mapper(prop.parent) # type: ignore # noqa: E501
  452. parent = parent.parent[subclass_entity]
  453. # when building a path where with_polymorphic() is in use,
  454. # special logic to determine the "natural path" when subclass
  455. # entities are used.
  456. #
  457. # here we are trying to distinguish between a path that starts
  458. # on a with_polymorphic entity vs. one that starts on a
  459. # normal entity that introduces a with_polymorphic() in the
  460. # middle using of_type():
  461. #
  462. # # as in test_polymorphic_rel->
  463. # # test_subqueryload_on_subclass_uses_path_correctly
  464. # wp = with_polymorphic(RegularEntity, "*")
  465. # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
  466. #
  467. # vs
  468. #
  469. # # as in test_relationship->JoinedloadWPolyOfTypeContinued
  470. # wp = with_polymorphic(SomeFoo, "*")
  471. # sess.query(RegularEntity).options(
  472. # someload(RegularEntity.foos.of_type(wp))
  473. # .someload(wp.SubFoo.bar)
  474. # )
  475. #
  476. # in the former case, the Query as it generates a path that we
  477. # want to match will be in terms of the with_polymorphic at the
  478. # beginning. in the latter case, Query will generate simple
  479. # paths that don't know about this with_polymorphic, so we must
  480. # use a separate natural path.
  481. #
  482. #
  483. if parent.parent:
  484. natural_parent = parent.parent[subclass_entity.mapper]
  485. self.is_unnatural = True
  486. else:
  487. natural_parent = parent
  488. elif (
  489. natural_parent.parent
  490. and insp.is_aliased_class
  491. and prop.parent # this should always be the case here
  492. is not insp.mapper
  493. and insp.mapper.isa(prop.parent)
  494. ):
  495. natural_parent = parent.parent[prop.parent]
  496. self.prop = prop
  497. self.parent = parent
  498. self.path = parent.path + (prop,)
  499. self.natural_path = natural_parent.natural_path + (prop,)
  500. self.has_entity = prop._links_to_entity
  501. if prop._is_relationship:
  502. if TYPE_CHECKING:
  503. assert isinstance(prop, RelationshipProperty)
  504. self.entity = prop.entity
  505. self.mapper = prop.mapper
  506. else:
  507. self.entity = None
  508. self.mapper = None
  509. self._wildcard_path_loader_key = (
  510. "loader",
  511. parent.natural_path + self.prop._wildcard_token,
  512. )
  513. self._default_path_loader_key = self.prop._default_path_loader_key
  514. self._loader_key = ("loader", self.natural_path)
  515. def _truncate_recursive(self) -> PropRegistry:
  516. earliest = None
  517. for i, token in enumerate(reversed(self.path[:-1])):
  518. if token is self.prop:
  519. earliest = i
  520. if earliest is None:
  521. return self
  522. else:
  523. return self.coerce(self.path[0 : -(earliest + 1)]) # type: ignore
  524. @property
  525. def entity_path(self) -> AbstractEntityRegistry:
  526. assert self.entity is not None
  527. return self[self.entity]
  528. def _getitem(
  529. self, entity: Union[int, slice, _InternalEntityType[Any]]
  530. ) -> Union[AbstractEntityRegistry, _PathElementType, _PathRepresentation]:
  531. if isinstance(entity, (int, slice)):
  532. return self.path[entity]
  533. else:
  534. return SlotsEntityRegistry(self, entity)
  535. if not TYPE_CHECKING:
  536. __getitem__ = _getitem
  537. class AbstractEntityRegistry(CreatesToken):
  538. __slots__ = (
  539. "key",
  540. "parent",
  541. "is_aliased_class",
  542. "path",
  543. "entity",
  544. "natural_path",
  545. )
  546. has_entity = True
  547. is_entity = True
  548. parent: Union[RootRegistry, PropRegistry]
  549. key: _InternalEntityType[Any]
  550. entity: _InternalEntityType[Any]
  551. is_aliased_class: bool
  552. def __init__(
  553. self,
  554. parent: Union[RootRegistry, PropRegistry],
  555. entity: _InternalEntityType[Any],
  556. ):
  557. self.key = entity
  558. self.parent = parent
  559. self.is_aliased_class = entity.is_aliased_class
  560. self.entity = entity
  561. self.path = parent.path + (entity,)
  562. # the "natural path" is the path that we get when Query is traversing
  563. # from the lead entities into the various relationships; it corresponds
  564. # to the structure of mappers and relationships. when we are given a
  565. # path that comes from loader options, as of 1.3 it can have ac-hoc
  566. # with_polymorphic() and other AliasedInsp objects inside of it, which
  567. # are usually not present in mappings. So here we track both the
  568. # "enhanced" path in self.path and the "natural" path that doesn't
  569. # include those objects so these two traversals can be matched up.
  570. # the test here for "(self.is_aliased_class or parent.is_unnatural)"
  571. # are to avoid the more expensive conditional logic that follows if we
  572. # know we don't have to do it. This conditional can just as well be
  573. # "if parent.path:", it just is more function calls.
  574. #
  575. # This is basically the only place that the "is_unnatural" flag
  576. # actually changes behavior.
  577. if parent.path and (self.is_aliased_class or parent.is_unnatural):
  578. # this is an infrequent code path used only for loader strategies
  579. # that also make use of of_type().
  580. if entity.mapper.isa(parent.natural_path[-1].mapper): # type: ignore # noqa: E501
  581. self.natural_path = parent.natural_path + (entity.mapper,)
  582. else:
  583. self.natural_path = parent.natural_path + (
  584. parent.natural_path[-1].entity, # type: ignore
  585. )
  586. # it seems to make sense that since these paths get mixed up
  587. # with statements that are cached or not, we should make
  588. # sure the natural path is cacheable across different occurrences
  589. # of equivalent AliasedClass objects. however, so far this
  590. # does not seem to be needed for whatever reason.
  591. # elif not parent.path and self.is_aliased_class:
  592. # self.natural_path = (self.entity._generate_cache_key()[0], )
  593. else:
  594. self.natural_path = self.path
  595. def _truncate_recursive(self) -> AbstractEntityRegistry:
  596. return self.parent._truncate_recursive()[self.entity]
  597. @property
  598. def root_entity(self) -> _InternalEntityType[Any]:
  599. return self.odd_element(0)
  600. @property
  601. def entity_path(self) -> PathRegistry:
  602. return self
  603. @property
  604. def mapper(self) -> Mapper[Any]:
  605. return self.entity.mapper
  606. def __bool__(self) -> bool:
  607. return True
  608. def _getitem(
  609. self, entity: Any
  610. ) -> Union[_PathElementType, _PathRepresentation, PathRegistry]:
  611. if isinstance(entity, (int, slice)):
  612. return self.path[entity]
  613. elif entity in PathToken._intern:
  614. return TokenRegistry(self, PathToken._intern[entity])
  615. else:
  616. return PropRegistry(self, entity)
  617. if not TYPE_CHECKING:
  618. __getitem__ = _getitem
  619. class SlotsEntityRegistry(AbstractEntityRegistry):
  620. # for aliased class, return lightweight, no-cycles created
  621. # version
  622. inherit_cache = True
  623. class _ERDict(Dict[Any, Any]):
  624. def __init__(self, registry: CachingEntityRegistry):
  625. self.registry = registry
  626. def __missing__(self, key: Any) -> PropRegistry:
  627. self[key] = item = PropRegistry(self.registry, key)
  628. return item
  629. class CachingEntityRegistry(AbstractEntityRegistry):
  630. # for long lived mapper, return dict based caching
  631. # version that creates reference cycles
  632. __slots__ = ("_cache",)
  633. inherit_cache = True
  634. def __init__(
  635. self,
  636. parent: Union[RootRegistry, PropRegistry],
  637. entity: _InternalEntityType[Any],
  638. ):
  639. super().__init__(parent, entity)
  640. self._cache = _ERDict(self)
  641. def pop(self, key: Any, default: Any) -> Any:
  642. return self._cache.pop(key, default)
  643. def _getitem(self, entity: Any) -> Any:
  644. if isinstance(entity, (int, slice)):
  645. return self.path[entity]
  646. elif isinstance(entity, PathToken):
  647. return TokenRegistry(self, entity)
  648. else:
  649. return self._cache[entity]
  650. if not TYPE_CHECKING:
  651. __getitem__ = _getitem
  652. if TYPE_CHECKING:
  653. def path_is_entity(
  654. path: PathRegistry,
  655. ) -> TypeGuard[AbstractEntityRegistry]: ...
  656. def path_is_property(path: PathRegistry) -> TypeGuard[PropRegistry]: ...
  657. else:
  658. path_is_entity = operator.attrgetter("is_entity")
  659. path_is_property = operator.attrgetter("is_property")