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