1# orm/path_registry.py
2# Copyright (C) 2005-2024 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
9"""
10
11from __future__ import annotations
12
13from functools import reduce
14from itertools import chain
15import logging
16import operator
17from typing import Any
18from typing import cast
19from typing import Dict
20from typing import Iterator
21from typing import List
22from typing import Optional
23from typing import overload
24from typing import Sequence
25from typing import Tuple
26from typing import TYPE_CHECKING
27from typing import Union
28
29from . import base as orm_base
30from ._typing import insp_is_mapper_property
31from .. import exc
32from .. import util
33from ..sql import visitors
34from ..sql.cache_key import HasCacheKey
35
36if TYPE_CHECKING:
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 from ..util.typing import TypeGuard
47
48 def is_root(path: PathRegistry) -> TypeGuard[RootRegistry]: ...
49
50 def is_entity(path: PathRegistry) -> 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(parent.parent[mp_ent], self.token).natural_path
492 if (
493 parent.is_aliased_class
494 and cast(
495 "AliasedInsp[Any]",
496 parent.entity,
497 )._is_with_polymorphic
498 ):
499 yield self.natural_path
500 for ent in cast(
501 "AliasedInsp[Any]", parent.entity
502 )._with_polymorphic_entities:
503 yield (
504 TokenRegistry(parent.parent[ent], self.token).natural_path
505 )
506 else:
507 yield self.natural_path
508
509 def _getitem(self, entity: Any) -> Any:
510 try:
511 return self.path[entity]
512 except TypeError as err:
513 raise IndexError(f"{entity}") from err
514
515 if not TYPE_CHECKING:
516 __getitem__ = _getitem
517
518
519class PropRegistry(PathRegistry):
520 __slots__ = (
521 "prop",
522 "parent",
523 "path",
524 "natural_path",
525 "has_entity",
526 "entity",
527 "mapper",
528 "_wildcard_path_loader_key",
529 "_default_path_loader_key",
530 "_loader_key",
531 "is_unnatural",
532 )
533 inherit_cache = True
534 is_property = True
535
536 prop: StrategizedProperty[Any]
537 mapper: Optional[Mapper[Any]]
538 entity: Optional[_InternalEntityType[Any]]
539
540 def __init__(
541 self, parent: AbstractEntityRegistry, prop: StrategizedProperty[Any]
542 ):
543
544 # restate this path in terms of the
545 # given StrategizedProperty's parent.
546 insp = cast("_InternalEntityType[Any]", parent[-1])
547 natural_parent: AbstractEntityRegistry = parent
548
549 # inherit "is_unnatural" from the parent
550 self.is_unnatural = parent.parent.is_unnatural or bool(
551 parent.mapper.inherits
552 )
553
554 if not insp.is_aliased_class or insp._use_mapper_path: # type: ignore
555 parent = natural_parent = parent.parent[prop.parent]
556 elif (
557 insp.is_aliased_class
558 and insp.with_polymorphic_mappers
559 and prop.parent in insp.with_polymorphic_mappers
560 ):
561 subclass_entity: _InternalEntityType[Any] = parent[-1]._entity_for_mapper(prop.parent) # type: ignore # noqa: E501
562 parent = parent.parent[subclass_entity]
563
564 # when building a path where with_polymorphic() is in use,
565 # special logic to determine the "natural path" when subclass
566 # entities are used.
567 #
568 # here we are trying to distinguish between a path that starts
569 # on a the with_polymorhpic entity vs. one that starts on a
570 # normal entity that introduces a with_polymorphic() in the
571 # middle using of_type():
572 #
573 # # as in test_polymorphic_rel->
574 # # test_subqueryload_on_subclass_uses_path_correctly
575 # wp = with_polymorphic(RegularEntity, "*")
576 # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
577 #
578 # vs
579 #
580 # # as in test_relationship->JoinedloadWPolyOfTypeContinued
581 # wp = with_polymorphic(SomeFoo, "*")
582 # sess.query(RegularEntity).options(
583 # someload(RegularEntity.foos.of_type(wp))
584 # .someload(wp.SubFoo.bar)
585 # )
586 #
587 # in the former case, the Query as it generates a path that we
588 # want to match will be in terms of the with_polymorphic at the
589 # beginning. in the latter case, Query will generate simple
590 # paths that don't know about this with_polymorphic, so we must
591 # use a separate natural path.
592 #
593 #
594 if parent.parent:
595 natural_parent = parent.parent[subclass_entity.mapper]
596 self.is_unnatural = True
597 else:
598 natural_parent = parent
599 elif (
600 natural_parent.parent
601 and insp.is_aliased_class
602 and prop.parent # this should always be the case here
603 is not insp.mapper
604 and insp.mapper.isa(prop.parent)
605 ):
606 natural_parent = parent.parent[prop.parent]
607
608 self.prop = prop
609 self.parent = parent
610 self.path = parent.path + (prop,)
611 self.natural_path = natural_parent.natural_path + (prop,)
612
613 self.has_entity = prop._links_to_entity
614 if prop._is_relationship:
615 if TYPE_CHECKING:
616 assert isinstance(prop, RelationshipProperty)
617 self.entity = prop.entity
618 self.mapper = prop.mapper
619 else:
620 self.entity = None
621 self.mapper = None
622
623 self._wildcard_path_loader_key = (
624 "loader",
625 parent.natural_path + self.prop._wildcard_token,
626 )
627 self._default_path_loader_key = self.prop._default_path_loader_key
628 self._loader_key = ("loader", self.natural_path)
629
630 def _truncate_recursive(self) -> PropRegistry:
631 earliest = None
632 for i, token in enumerate(reversed(self.path[:-1])):
633 if token is self.prop:
634 earliest = i
635
636 if earliest is None:
637 return self
638 else:
639 return self.coerce(self.path[0 : -(earliest + 1)]) # type: ignore
640
641 @property
642 def entity_path(self) -> AbstractEntityRegistry:
643 assert self.entity is not None
644 return self[self.entity]
645
646 def _getitem(
647 self, entity: Union[int, slice, _InternalEntityType[Any]]
648 ) -> Union[AbstractEntityRegistry, _PathElementType, _PathRepresentation]:
649 if isinstance(entity, (int, slice)):
650 return self.path[entity]
651 else:
652 return SlotsEntityRegistry(self, entity)
653
654 if not TYPE_CHECKING:
655 __getitem__ = _getitem
656
657
658class AbstractEntityRegistry(CreatesToken):
659 __slots__ = (
660 "key",
661 "parent",
662 "is_aliased_class",
663 "path",
664 "entity",
665 "natural_path",
666 )
667
668 has_entity = True
669 is_entity = True
670
671 parent: Union[RootRegistry, PropRegistry]
672 key: _InternalEntityType[Any]
673 entity: _InternalEntityType[Any]
674 is_aliased_class: bool
675
676 def __init__(
677 self,
678 parent: Union[RootRegistry, PropRegistry],
679 entity: _InternalEntityType[Any],
680 ):
681 self.key = entity
682 self.parent = parent
683 self.is_aliased_class = entity.is_aliased_class
684 self.entity = entity
685 self.path = parent.path + (entity,)
686
687 # the "natural path" is the path that we get when Query is traversing
688 # from the lead entities into the various relationships; it corresponds
689 # to the structure of mappers and relationships. when we are given a
690 # path that comes from loader options, as of 1.3 it can have ac-hoc
691 # with_polymorphic() and other AliasedInsp objects inside of it, which
692 # are usually not present in mappings. So here we track both the
693 # "enhanced" path in self.path and the "natural" path that doesn't
694 # include those objects so these two traversals can be matched up.
695
696 # the test here for "(self.is_aliased_class or parent.is_unnatural)"
697 # are to avoid the more expensive conditional logic that follows if we
698 # know we don't have to do it. This conditional can just as well be
699 # "if parent.path:", it just is more function calls.
700 #
701 # This is basically the only place that the "is_unnatural" flag
702 # actually changes behavior.
703 if parent.path and (self.is_aliased_class or parent.is_unnatural):
704 # this is an infrequent code path used only for loader strategies
705 # that also make use of of_type().
706 if entity.mapper.isa(parent.natural_path[-1].mapper): # type: ignore # noqa: E501
707 self.natural_path = parent.natural_path + (entity.mapper,)
708 else:
709 self.natural_path = parent.natural_path + (
710 parent.natural_path[-1].entity, # type: ignore
711 )
712 # it seems to make sense that since these paths get mixed up
713 # with statements that are cached or not, we should make
714 # sure the natural path is cacheable across different occurrences
715 # of equivalent AliasedClass objects. however, so far this
716 # does not seem to be needed for whatever reason.
717 # elif not parent.path and self.is_aliased_class:
718 # self.natural_path = (self.entity._generate_cache_key()[0], )
719 else:
720 self.natural_path = self.path
721
722 def _truncate_recursive(self) -> AbstractEntityRegistry:
723 return self.parent._truncate_recursive()[self.entity]
724
725 @property
726 def root_entity(self) -> _InternalEntityType[Any]:
727 return self.odd_element(0)
728
729 @property
730 def entity_path(self) -> PathRegistry:
731 return self
732
733 @property
734 def mapper(self) -> Mapper[Any]:
735 return self.entity.mapper
736
737 def __bool__(self) -> bool:
738 return True
739
740 def _getitem(
741 self, entity: Any
742 ) -> Union[_PathElementType, _PathRepresentation, PathRegistry]:
743 if isinstance(entity, (int, slice)):
744 return self.path[entity]
745 elif entity in PathToken._intern:
746 return TokenRegistry(self, PathToken._intern[entity])
747 else:
748 return PropRegistry(self, entity)
749
750 if not TYPE_CHECKING:
751 __getitem__ = _getitem
752
753
754class SlotsEntityRegistry(AbstractEntityRegistry):
755 # for aliased class, return lightweight, no-cycles created
756 # version
757 inherit_cache = True
758
759
760class _ERDict(Dict[Any, Any]):
761 def __init__(self, registry: CachingEntityRegistry):
762 self.registry = registry
763
764 def __missing__(self, key: Any) -> PropRegistry:
765 self[key] = item = PropRegistry(self.registry, key)
766
767 return item
768
769
770class CachingEntityRegistry(AbstractEntityRegistry):
771 # for long lived mapper, return dict based caching
772 # version that creates reference cycles
773
774 __slots__ = ("_cache",)
775
776 inherit_cache = True
777
778 def __init__(
779 self,
780 parent: Union[RootRegistry, PropRegistry],
781 entity: _InternalEntityType[Any],
782 ):
783 super().__init__(parent, entity)
784 self._cache = _ERDict(self)
785
786 def pop(self, key: Any, default: Any) -> Any:
787 return self._cache.pop(key, default)
788
789 def _getitem(self, entity: Any) -> Any:
790 if isinstance(entity, (int, slice)):
791 return self.path[entity]
792 elif isinstance(entity, PathToken):
793 return TokenRegistry(self, entity)
794 else:
795 return self._cache[entity]
796
797 if not TYPE_CHECKING:
798 __getitem__ = _getitem
799
800
801if TYPE_CHECKING:
802
803 def path_is_entity(
804 path: PathRegistry,
805 ) -> TypeGuard[AbstractEntityRegistry]: ...
806
807 def path_is_property(path: PathRegistry) -> TypeGuard[PropRegistry]: ...
808
809else:
810 path_is_entity = operator.attrgetter("is_entity")
811 path_is_property = operator.attrgetter("is_property")