1# orm/path_registry.py
2# Copyright (C) 2005-2026 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
9from __future__ import annotations
10
11from functools import reduce
12from itertools import chain
13import logging
14import operator
15from typing import Any
16from typing import cast
17from typing import Dict
18from typing import Iterator
19from typing import List
20from typing import Mapping
21from typing import Optional
22from typing import overload
23from typing import Sequence
24from typing import Tuple
25from typing import TYPE_CHECKING
26from typing import Union
27
28from . import base as orm_base
29from ._typing import insp_is_mapper_property
30from .. import exc
31from .. import util
32from ..sql import visitors
33from ..sql.cache_key import HasCacheKey
34from ..util.typing import Final
35from ..util.typing import Literal
36
37if TYPE_CHECKING:
38 from ._typing import _InternalEntityType
39 from .interfaces import StrategizedProperty
40 from .mapper import Mapper
41 from .relationships import RelationshipProperty
42 from .util import AliasedInsp
43 from ..sql.cache_key import _CacheKeyTraversalType
44 from ..sql.elements import BindParameter
45 from ..sql.visitors import anon_map
46 from ..util.typing import _LiteralStar
47 from ..util.typing import TypeGuard
48
49 def is_root(path: PathRegistry) -> TypeGuard[RootRegistry]: ...
50
51 def is_entity(path: PathRegistry) -> TypeGuard[AbstractEntityRegistry]: ...
52
53else:
54 is_root = operator.attrgetter("is_root")
55 is_entity = operator.attrgetter("is_entity")
56
57
58_SerializedPath = List[Any]
59_StrPathToken = str
60_PathElementType = Union[
61 _StrPathToken, "_InternalEntityType[Any]", "StrategizedProperty[Any]"
62]
63
64# the representation is in fact
65# a tuple with alternating:
66# [_InternalEntityType[Any], Union[str, StrategizedProperty[Any]],
67# _InternalEntityType[Any], Union[str, StrategizedProperty[Any]], ...]
68# this might someday be a tuple of 2-tuples instead, but paths can be
69# chopped at odd intervals as well so this is less flexible
70_PathRepresentation = Tuple[_PathElementType, ...]
71
72# NOTE: these names are weird since the array is 0-indexed,
73# the "_Odd" entries are at 0, 2, 4, etc
74_OddPathRepresentation = Sequence["_InternalEntityType[Any]"]
75_EvenPathRepresentation = Sequence[Union["StrategizedProperty[Any]", str]]
76
77
78log = logging.getLogger(__name__)
79
80
81def _unreduce_path(path: _SerializedPath) -> PathRegistry:
82 return PathRegistry.deserialize(path)
83
84
85_WILDCARD_TOKEN: _LiteralStar = "*"
86_DEFAULT_TOKEN = "_sa_default"
87
88_RELATIONSHIP_TOKEN: Final[Literal["relationship"]] = "relationship"
89_COLUMN_TOKEN: Final[Literal["column"]] = "column"
90
91_UNPREFIXED_TOKENS = frozenset([_WILDCARD_TOKEN, _DEFAULT_TOKEN])
92"""the wildcard strings accepted from the user in a loader option.
93
94these are prefixed with the target property's ``strategy_wildcard_key`` to
95form the tokens in :data:`._PATH_TOKENS`, and are not themselves valid as an
96element of a path.
97
98"""
99
100_PATH_TOKENS = frozenset(
101 f"{wildcard_key}:{suffix}"
102 for wildcard_key in (_RELATIONSHIP_TOKEN, _COLUMN_TOKEN)
103 for suffix in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
104)
105"""the complete set of tokens which may appear within a path.
106
107:attr:`.PathToken._intern` is populated from this collection at module import
108time, so that a token is present in every process, including one which has
109not yet run any query.
110
111"""
112
113_ACCEPTED_TOKENS = _UNPREFIXED_TOKENS | _PATH_TOKENS
114"""every string a loader option may accept in place of an attribute name.
115
116this is the union of the bare wildcards the user writes and the prefixed
117tokens which the loader option internals hand back to themselves; only the
118latter may appear in a path.
119
120"""
121
122
123class PathRegistry(HasCacheKey):
124 """Represent query load paths and registry functions.
125
126 Basically represents structures like:
127
128 (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
129
130 These structures are generated by things like
131 query options (joinedload(), subqueryload(), etc.) and are
132 used to compose keys stored in the query._attributes dictionary
133 for various options.
134
135 They are then re-composed at query compile/result row time as
136 the query is formed and as rows are fetched, where they again
137 serve to compose keys to look up options in the context.attributes
138 dictionary, which is copied from query._attributes.
139
140 The path structure has a limited amount of caching, where each
141 "root" ultimately pulls from a fixed registry associated with
142 the first mapper, that also contains elements for each of its
143 property keys. However paths longer than two elements, which
144 are the exception rather than the rule, are generated on an
145 as-needed basis.
146
147 """
148
149 __slots__ = ()
150
151 is_token = False
152 is_root = False
153 has_entity = False
154 is_property = False
155 is_entity = False
156
157 is_unnatural: bool
158
159 path: _PathRepresentation
160 natural_path: _PathRepresentation
161 parent: Optional[PathRegistry]
162 root: RootRegistry
163
164 _cache_key_traversal: _CacheKeyTraversalType = [
165 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list)
166 ]
167
168 def __eq__(self, other: Any) -> bool:
169 try:
170 return other is not None and self.path == other._path_for_compare
171 except AttributeError:
172 util.warn(
173 "Comparison of PathRegistry to %r is not supported"
174 % (type(other))
175 )
176 return False
177
178 def __ne__(self, other: Any) -> bool:
179 try:
180 return other is None or self.path != other._path_for_compare
181 except AttributeError:
182 util.warn(
183 "Comparison of PathRegistry to %r is not supported"
184 % (type(other))
185 )
186 return True
187
188 @property
189 def _path_for_compare(self) -> Optional[_PathRepresentation]:
190 return self.path
191
192 def odd_element(self, index: int) -> _InternalEntityType[Any]:
193 return self.path[index] # type: ignore
194
195 def set(self, attributes: Dict[Any, Any], key: Any, value: Any) -> None:
196 log.debug("set '%s' on path '%s' to '%s'", key, self, value)
197 attributes[(key, self.natural_path)] = value
198
199 def setdefault(
200 self, attributes: Dict[Any, Any], key: Any, value: Any
201 ) -> None:
202 log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
203 attributes.setdefault((key, self.natural_path), value)
204
205 def get(
206 self, attributes: Dict[Any, Any], key: Any, value: Optional[Any] = None
207 ) -> Any:
208 key = (key, self.natural_path)
209 if key in attributes:
210 return attributes[key]
211 else:
212 return value
213
214 def __len__(self) -> int:
215 return len(self.path)
216
217 def __hash__(self) -> int:
218 return id(self)
219
220 @overload
221 def __getitem__(self, entity: _StrPathToken) -> TokenRegistry: ...
222
223 @overload
224 def __getitem__(self, entity: int) -> _PathElementType: ...
225
226 @overload
227 def __getitem__(self, entity: slice) -> _PathRepresentation: ...
228
229 @overload
230 def __getitem__(
231 self, entity: _InternalEntityType[Any]
232 ) -> AbstractEntityRegistry: ...
233
234 @overload
235 def __getitem__(
236 self, entity: StrategizedProperty[Any]
237 ) -> PropRegistry: ...
238
239 def __getitem__(
240 self,
241 entity: Union[
242 _StrPathToken,
243 int,
244 slice,
245 _InternalEntityType[Any],
246 StrategizedProperty[Any],
247 ],
248 ) -> Union[
249 TokenRegistry,
250 _PathElementType,
251 _PathRepresentation,
252 PropRegistry,
253 AbstractEntityRegistry,
254 ]:
255 raise NotImplementedError()
256
257 # TODO: what are we using this for?
258 @property
259 def length(self) -> int:
260 return len(self.path)
261
262 def pairs(
263 self,
264 ) -> Iterator[
265 Tuple[_InternalEntityType[Any], Union[str, StrategizedProperty[Any]]]
266 ]:
267 odd_path = cast(_OddPathRepresentation, self.path)
268 even_path = cast(_EvenPathRepresentation, odd_path)
269 for i in range(0, len(odd_path), 2):
270 yield odd_path[i], even_path[i + 1]
271
272 def contains_mapper(self, mapper: Mapper[Any]) -> bool:
273 _m_path = cast(_OddPathRepresentation, self.path)
274 for path_mapper in [_m_path[i] for i in range(0, len(_m_path), 2)]:
275 if path_mapper.mapper.isa(mapper):
276 return True
277 else:
278 return False
279
280 def contains(self, attributes: Dict[Any, Any], key: Any) -> bool:
281 return (key, self.path) in attributes
282
283 def __reduce__(self) -> Any:
284 return _unreduce_path, (self.serialize(),)
285
286 @classmethod
287 def _serialize_path(cls, path: _PathRepresentation) -> _SerializedPath:
288 _m_path = cast(_OddPathRepresentation, path)
289 _p_path = cast(_EvenPathRepresentation, path)
290
291 return list(
292 zip(
293 tuple(
294 m.class_ if (m.is_mapper or m.is_aliased_class) else str(m)
295 for m in [_m_path[i] for i in range(0, len(_m_path), 2)]
296 ),
297 tuple(
298 p.key if insp_is_mapper_property(p) else str(p)
299 for p in [_p_path[i] for i in range(1, len(_p_path), 2)]
300 )
301 + (None,),
302 )
303 )
304
305 @classmethod
306 def _deserialize_path(cls, path: _SerializedPath) -> _PathRepresentation:
307 def _deserialize_mapper_token(mcls: Any) -> Any:
308 return (
309 # note: we likely dont want configure=True here however
310 # this is maintained at the moment for backwards compatibility
311 orm_base._inspect_mapped_class(mcls, configure=True)
312 if mcls not in PathToken._intern
313 else PathToken._intern[mcls]
314 )
315
316 def _deserialize_key_token(mcls: Any, key: Any) -> Any:
317 if key is None:
318 return None
319 elif key in PathToken._intern:
320 return PathToken._intern[key]
321 else:
322 mp = orm_base._inspect_mapped_class(mcls, configure=True)
323 assert mp is not None
324 return mp.attrs[key]
325
326 p = tuple(
327 chain(
328 *[
329 (
330 _deserialize_mapper_token(mcls),
331 _deserialize_key_token(mcls, key),
332 )
333 for mcls, key in path
334 ]
335 )
336 )
337 if p and p[-1] is None:
338 p = p[0:-1]
339 return p
340
341 def serialize(self) -> _SerializedPath:
342 path = self.path
343 return self._serialize_path(path)
344
345 @classmethod
346 def deserialize(cls, path: _SerializedPath) -> PathRegistry:
347 assert path is not None
348 p = cls._deserialize_path(path)
349 return cls.coerce(p)
350
351 @overload
352 @classmethod
353 def per_mapper(cls, mapper: Mapper[Any]) -> CachingEntityRegistry: ...
354
355 @overload
356 @classmethod
357 def per_mapper(cls, mapper: AliasedInsp[Any]) -> SlotsEntityRegistry: ...
358
359 @classmethod
360 def per_mapper(
361 cls, mapper: _InternalEntityType[Any]
362 ) -> AbstractEntityRegistry:
363 if mapper.is_mapper:
364 return CachingEntityRegistry(cls.root, mapper)
365 else:
366 return SlotsEntityRegistry(cls.root, mapper)
367
368 @classmethod
369 def coerce(cls, raw: _PathRepresentation) -> PathRegistry:
370 def _red(prev: PathRegistry, next_: _PathElementType) -> PathRegistry:
371 return prev[next_]
372
373 # can't quite get mypy to appreciate this one :)
374 return reduce(_red, raw, cls.root) # type: ignore
375
376 def __add__(self, other: PathRegistry) -> PathRegistry:
377 def _red(prev: PathRegistry, next_: _PathElementType) -> PathRegistry:
378 return prev[next_]
379
380 return reduce(_red, other.path, self)
381
382 def __str__(self) -> str:
383 return f"ORM Path[{' -> '.join(str(elem) for elem in self.path)}]"
384
385 def __repr__(self) -> str:
386 return f"{self.__class__.__name__}({self.path!r})"
387
388
389class CreatesToken(PathRegistry):
390 __slots__ = ()
391
392 is_aliased_class: bool
393 is_root: bool
394
395 def token(self, token: _StrPathToken) -> TokenRegistry:
396 if token not in PathToken._intern:
397 raise exc.ArgumentError(f"invalid token: {token}")
398 elif token.endswith(f":{_WILDCARD_TOKEN}"):
399 return TokenRegistry(self, token)
400 else:
401 return TokenRegistry(self.root, token)
402
403
404class RootRegistry(CreatesToken):
405 """Root registry, defers to mappers so that
406 paths are maintained per-root-mapper.
407
408 """
409
410 __slots__ = ()
411
412 inherit_cache = True
413
414 path = natural_path = ()
415 has_entity = False
416 is_aliased_class = False
417 is_root = True
418 is_unnatural = False
419
420 def _getitem(
421 self, entity: Any
422 ) -> Union[TokenRegistry, AbstractEntityRegistry]:
423 if entity in PathToken._intern:
424 if TYPE_CHECKING:
425 assert isinstance(entity, _StrPathToken)
426 return TokenRegistry(self, PathToken._intern[entity])
427 else:
428 try:
429 return entity._path_registry # type: ignore
430 except AttributeError:
431 raise IndexError(
432 f"invalid argument for RootRegistry.__getitem__: {entity}"
433 )
434
435 def _truncate_recursive(self) -> RootRegistry:
436 return self
437
438 if not TYPE_CHECKING:
439 __getitem__ = _getitem
440
441
442PathRegistry.root = RootRegistry()
443
444
445class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
446 """cacheable string token"""
447
448 _intern: Mapping[str, PathToken]
449 """the :class:`.PathToken` for each of :data:`._PATH_TOKENS`.
450
451 this collection is fully populated below at module import time and is
452 never added to afterwards; it's typed as :class:`.Mapping` so that a
453 mutation is flagged by type checkers.
454 :meth:`.PathRegistry._deserialize_path` relies on it being complete,
455 distinguishing a token from a mapped attribute key by testing
456 membership here.
457
458 """
459
460 def _gen_cache_key(
461 self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
462 ) -> Tuple[Any, ...]:
463 return (str(self),)
464
465 @property
466 def _path_for_compare(self) -> Optional[_PathRepresentation]:
467 return None
468
469
470PathToken._intern = {token: PathToken(token) for token in _PATH_TOKENS}
471
472
473class TokenRegistry(PathRegistry):
474 __slots__ = ("token", "parent", "path", "natural_path")
475
476 inherit_cache = True
477
478 token: _StrPathToken
479 parent: CreatesToken
480
481 def __init__(self, parent: CreatesToken, token: _StrPathToken):
482 token = PathToken._intern[token]
483
484 self.token = token
485 self.parent = parent
486 self.path = parent.path + (token,)
487 self.natural_path = parent.natural_path + (token,)
488
489 has_entity = False
490
491 is_token = True
492
493 def generate_for_superclasses(self) -> Iterator[PathRegistry]:
494 # NOTE: this method is no longer used. consider removal
495 parent = self.parent
496 if is_root(parent):
497 yield self
498 return
499
500 if TYPE_CHECKING:
501 assert isinstance(parent, AbstractEntityRegistry)
502 if not parent.is_aliased_class:
503 for mp_ent in parent.mapper.iterate_to_root():
504 yield TokenRegistry(parent.parent[mp_ent], self.token)
505 elif (
506 parent.is_aliased_class
507 and cast(
508 "AliasedInsp[Any]",
509 parent.entity,
510 )._is_with_polymorphic
511 ):
512 yield self
513 for ent in cast(
514 "AliasedInsp[Any]", parent.entity
515 )._with_polymorphic_entities:
516 yield TokenRegistry(parent.parent[ent], self.token)
517 else:
518 yield self
519
520 def _generate_natural_for_superclasses(
521 self,
522 ) -> Iterator[_PathRepresentation]:
523 parent = self.parent
524 if is_root(parent):
525 yield self.natural_path
526 return
527
528 if TYPE_CHECKING:
529 assert isinstance(parent, AbstractEntityRegistry)
530 for mp_ent in parent.mapper.iterate_to_root():
531 yield TokenRegistry(parent.parent[mp_ent], self.token).natural_path
532 if (
533 parent.is_aliased_class
534 and cast(
535 "AliasedInsp[Any]",
536 parent.entity,
537 )._is_with_polymorphic
538 ):
539 yield self.natural_path
540 for ent in cast(
541 "AliasedInsp[Any]", parent.entity
542 )._with_polymorphic_entities:
543 yield (
544 TokenRegistry(parent.parent[ent], self.token).natural_path
545 )
546 else:
547 yield self.natural_path
548
549 def _getitem(self, entity: Any) -> Any:
550 try:
551 return self.path[entity]
552 except TypeError as err:
553 raise IndexError(f"{entity}") from err
554
555 if not TYPE_CHECKING:
556 __getitem__ = _getitem
557
558
559class PropRegistry(PathRegistry):
560 __slots__ = (
561 "prop",
562 "parent",
563 "path",
564 "natural_path",
565 "has_entity",
566 "entity",
567 "mapper",
568 "_wildcard_path_loader_key",
569 "_default_path_loader_key",
570 "_loader_key",
571 "is_unnatural",
572 )
573 inherit_cache = True
574 is_property = True
575
576 prop: StrategizedProperty[Any]
577 mapper: Optional[Mapper[Any]]
578 entity: Optional[_InternalEntityType[Any]]
579
580 def __init__(
581 self, parent: AbstractEntityRegistry, prop: StrategizedProperty[Any]
582 ):
583
584 # restate this path in terms of the
585 # given StrategizedProperty's parent.
586 insp = cast("_InternalEntityType[Any]", parent[-1])
587 natural_parent: AbstractEntityRegistry = parent
588
589 # inherit "is_unnatural" from the parent
590 self.is_unnatural = parent.parent.is_unnatural or bool(
591 parent.mapper.inherits
592 )
593
594 if not insp.is_aliased_class or insp._use_mapper_path: # type: ignore
595 parent = natural_parent = parent.parent[prop.parent]
596 elif (
597 insp.is_aliased_class
598 and insp.with_polymorphic_mappers
599 and prop.parent in insp.with_polymorphic_mappers
600 ):
601 subclass_entity: _InternalEntityType[Any] = parent[-1]._entity_for_mapper(prop.parent) # type: ignore # noqa: E501
602 parent = parent.parent[subclass_entity]
603
604 # when building a path where with_polymorphic() is in use,
605 # special logic to determine the "natural path" when subclass
606 # entities are used.
607 #
608 # here we are trying to distinguish between a path that starts
609 # on a with_polymorphic entity vs. one that starts on a
610 # normal entity that introduces a with_polymorphic() in the
611 # middle using of_type():
612 #
613 # # as in test_polymorphic_rel->
614 # # test_subqueryload_on_subclass_uses_path_correctly
615 # wp = with_polymorphic(RegularEntity, "*")
616 # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
617 #
618 # vs
619 #
620 # # as in test_relationship->JoinedloadWPolyOfTypeContinued
621 # wp = with_polymorphic(SomeFoo, "*")
622 # sess.query(RegularEntity).options(
623 # someload(RegularEntity.foos.of_type(wp))
624 # .someload(wp.SubFoo.bar)
625 # )
626 #
627 # in the former case, the Query as it generates a path that we
628 # want to match will be in terms of the with_polymorphic at the
629 # beginning. in the latter case, Query will generate simple
630 # paths that don't know about this with_polymorphic, so we must
631 # use a separate natural path.
632 #
633 #
634 if parent.parent:
635 natural_parent = parent.parent[subclass_entity.mapper]
636 self.is_unnatural = True
637 else:
638 natural_parent = parent
639 elif (
640 natural_parent.parent
641 and insp.is_aliased_class
642 and prop.parent # this should always be the case here
643 is not insp.mapper
644 and insp.mapper.isa(prop.parent)
645 ):
646 natural_parent = parent.parent[prop.parent]
647
648 self.prop = prop
649 self.parent = parent
650 self.path = parent.path + (prop,)
651 self.natural_path = natural_parent.natural_path + (prop,)
652
653 self.has_entity = prop._links_to_entity
654 if prop._is_relationship:
655 if TYPE_CHECKING:
656 assert isinstance(prop, RelationshipProperty)
657 self.entity = prop.entity
658 self.mapper = prop.mapper
659 else:
660 self.entity = None
661 self.mapper = None
662
663 self._wildcard_path_loader_key = (
664 "loader",
665 parent.natural_path + self.prop._wildcard_token,
666 )
667 self._default_path_loader_key = self.prop._default_path_loader_key
668 self._loader_key = ("loader", self.natural_path)
669
670 def _truncate_recursive(self) -> PropRegistry:
671 earliest = None
672 for i, token in enumerate(reversed(self.path[:-1])):
673 if token is self.prop:
674 earliest = i
675
676 if earliest is None:
677 return self
678 else:
679 return self.coerce(self.path[0 : -(earliest + 1)]) # type: ignore
680
681 @property
682 def entity_path(self) -> AbstractEntityRegistry:
683 assert self.entity is not None
684 return self[self.entity]
685
686 def _getitem(
687 self, entity: Union[int, slice, _InternalEntityType[Any]]
688 ) -> Union[AbstractEntityRegistry, _PathElementType, _PathRepresentation]:
689 if isinstance(entity, (int, slice)):
690 return self.path[entity]
691 else:
692 return SlotsEntityRegistry(self, entity)
693
694 if not TYPE_CHECKING:
695 __getitem__ = _getitem
696
697
698class AbstractEntityRegistry(CreatesToken):
699 __slots__ = (
700 "key",
701 "parent",
702 "is_aliased_class",
703 "path",
704 "entity",
705 "natural_path",
706 )
707
708 has_entity = True
709 is_entity = True
710
711 parent: Union[RootRegistry, PropRegistry]
712 key: _InternalEntityType[Any]
713 entity: _InternalEntityType[Any]
714 is_aliased_class: bool
715
716 def __init__(
717 self,
718 parent: Union[RootRegistry, PropRegistry],
719 entity: _InternalEntityType[Any],
720 ):
721 self.key = entity
722 self.parent = parent
723 self.is_aliased_class = entity.is_aliased_class
724 self.entity = entity
725 self.path = parent.path + (entity,)
726
727 # the "natural path" is the path that we get when Query is traversing
728 # from the lead entities into the various relationships; it corresponds
729 # to the structure of mappers and relationships. when we are given a
730 # path that comes from loader options, as of 1.3 it can have ac-hoc
731 # with_polymorphic() and other AliasedInsp objects inside of it, which
732 # are usually not present in mappings. So here we track both the
733 # "enhanced" path in self.path and the "natural" path that doesn't
734 # include those objects so these two traversals can be matched up.
735
736 # the test here for "(self.is_aliased_class or parent.is_unnatural)"
737 # are to avoid the more expensive conditional logic that follows if we
738 # know we don't have to do it. This conditional can just as well be
739 # "if parent.path:", it just is more function calls.
740 #
741 # This is basically the only place that the "is_unnatural" flag
742 # actually changes behavior.
743 if parent.path and (self.is_aliased_class or parent.is_unnatural):
744 # this is an infrequent code path used for loader strategies that
745 # also make use of of_type() or other intricate polymorphic
746 # base/subclass combinations
747 parent_natural_entity = parent.natural_path[-1]
748
749 if entity.mapper.isa(
750 parent_natural_entity.mapper # type: ignore
751 ) or parent_natural_entity.mapper.isa( # type: ignore
752 entity.mapper
753 ):
754 # when the entity mapper and parent mapper are in an
755 # inheritance relationship, use entity.mapper in natural_path.
756 # First case: entity.mapper inherits from parent mapper (e.g.,
757 # accessing a subclass mapper through parent path). Second case
758 # (issue #13193): parent mapper inherits from entity.mapper
759 # (e.g., parent path has Sub(Base) but we're accessing with
760 # Base where Base.related is declared, so use Base in
761 # natural_path).
762 self.natural_path = parent.natural_path + (entity.mapper,)
763 else:
764 self.natural_path = parent.natural_path + (
765 parent_natural_entity.entity, # type: ignore
766 )
767 # it seems to make sense that since these paths get mixed up
768 # with statements that are cached or not, we should make
769 # sure the natural path is cacheable across different occurrences
770 # of equivalent AliasedClass objects. however, so far this
771 # does not seem to be needed for whatever reason.
772 # elif not parent.path and self.is_aliased_class:
773 # self.natural_path = (self.entity._generate_cache_key()[0], )
774 else:
775 self.natural_path = self.path
776
777 def _truncate_recursive(self) -> AbstractEntityRegistry:
778 return self.parent._truncate_recursive()[self.entity]
779
780 @property
781 def root_entity(self) -> _InternalEntityType[Any]:
782 return self.odd_element(0)
783
784 @property
785 def entity_path(self) -> PathRegistry:
786 return self
787
788 @property
789 def mapper(self) -> Mapper[Any]:
790 return self.entity.mapper
791
792 def __bool__(self) -> bool:
793 return True
794
795 def _getitem(
796 self, entity: Any
797 ) -> Union[_PathElementType, _PathRepresentation, PathRegistry]:
798 if isinstance(entity, (int, slice)):
799 return self.path[entity]
800 elif entity in PathToken._intern:
801 return TokenRegistry(self, PathToken._intern[entity])
802 else:
803 return PropRegistry(self, entity)
804
805 if not TYPE_CHECKING:
806 __getitem__ = _getitem
807
808
809class SlotsEntityRegistry(AbstractEntityRegistry):
810 # for aliased class, return lightweight, no-cycles created
811 # version
812 inherit_cache = True
813
814
815class _ERDict(Dict[Any, Any]):
816 def __init__(self, registry: CachingEntityRegistry):
817 self.registry = registry
818
819 def __missing__(self, key: Any) -> PropRegistry:
820 self[key] = item = PropRegistry(self.registry, key)
821
822 return item
823
824
825class CachingEntityRegistry(AbstractEntityRegistry):
826 # for long lived mapper, return dict based caching
827 # version that creates reference cycles
828
829 __slots__ = ("_cache",)
830
831 inherit_cache = True
832
833 def __init__(
834 self,
835 parent: Union[RootRegistry, PropRegistry],
836 entity: _InternalEntityType[Any],
837 ):
838 super().__init__(parent, entity)
839 self._cache = _ERDict(self)
840
841 def pop(self, key: Any, default: Any) -> Any:
842 return self._cache.pop(key, default)
843
844 def _getitem(self, entity: Any) -> Any:
845 if isinstance(entity, (int, slice)):
846 return self.path[entity]
847 elif isinstance(entity, PathToken):
848 return TokenRegistry(self, entity)
849 else:
850 return self._cache[entity]
851
852 if not TYPE_CHECKING:
853 __getitem__ = _getitem
854
855
856if TYPE_CHECKING:
857
858 def path_is_entity(
859 path: PathRegistry,
860 ) -> TypeGuard[AbstractEntityRegistry]: ...
861
862 def path_is_property(path: PathRegistry) -> TypeGuard[PropRegistry]: ...
863
864else:
865 path_is_entity = operator.attrgetter("is_entity")
866 path_is_property = operator.attrgetter("is_property")