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