Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/path_registry.py: 43%
244 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# orm/path_registry.py
2# Copyright (C) 2005-2023 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.
9"""
11from itertools import chain
12import logging
14from . import base as orm_base
15from .. import exc
16from .. import inspection
17from .. import util
18from ..sql import visitors
19from ..sql.traversals import HasCacheKey
21log = logging.getLogger(__name__)
24def _unreduce_path(path):
25 return PathRegistry.deserialize(path)
28_WILDCARD_TOKEN = "*"
29_DEFAULT_TOKEN = "_sa_default"
32class PathRegistry(HasCacheKey):
33 """Represent query load paths and registry functions.
35 Basically represents structures like:
37 (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
39 These structures are generated by things like
40 query options (joinedload(), subqueryload(), etc.) and are
41 used to compose keys stored in the query._attributes dictionary
42 for various options.
44 They are then re-composed at query compile/result row time as
45 the query is formed and as rows are fetched, where they again
46 serve to compose keys to look up options in the context.attributes
47 dictionary, which is copied from query._attributes.
49 The path structure has a limited amount of caching, where each
50 "root" ultimately pulls from a fixed registry associated with
51 the first mapper, that also contains elements for each of its
52 property keys. However paths longer than two elements, which
53 are the exception rather than the rule, are generated on an
54 as-needed basis.
56 """
58 __slots__ = ()
60 is_token = False
61 is_root = False
63 _cache_key_traversal = [
64 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list)
65 ]
67 def __eq__(self, other):
68 try:
69 return other is not None and self.path == other._path_for_compare
70 except AttributeError:
71 util.warn(
72 "Comparison of PathRegistry to %r is not supported"
73 % (type(other))
74 )
75 return False
77 def __ne__(self, other):
78 try:
79 return other is None or self.path != other._path_for_compare
80 except AttributeError:
81 util.warn(
82 "Comparison of PathRegistry to %r is not supported"
83 % (type(other))
84 )
85 return True
87 @property
88 def _path_for_compare(self):
89 return self.path
91 def set(self, attributes, key, value):
92 log.debug("set '%s' on path '%s' to '%s'", key, self, value)
93 attributes[(key, self.natural_path)] = value
95 def setdefault(self, attributes, key, value):
96 log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
97 attributes.setdefault((key, self.natural_path), value)
99 def get(self, attributes, key, value=None):
100 key = (key, self.natural_path)
101 if key in attributes:
102 return attributes[key]
103 else:
104 return value
106 def __len__(self):
107 return len(self.path)
109 def __hash__(self):
110 return id(self)
112 @property
113 def length(self):
114 return len(self.path)
116 def pairs(self):
117 path = self.path
118 for i in range(0, len(path), 2):
119 yield path[i], path[i + 1]
121 def contains_mapper(self, mapper):
122 for path_mapper in [self.path[i] for i in range(0, len(self.path), 2)]:
123 if path_mapper.mapper.isa(mapper):
124 return True
125 else:
126 return False
128 def contains(self, attributes, key):
129 return (key, self.path) in attributes
131 def __reduce__(self):
132 return _unreduce_path, (self.serialize(),)
134 @classmethod
135 def _serialize_path(cls, path):
136 return list(
137 zip(
138 [
139 m.class_ if (m.is_mapper or m.is_aliased_class) else str(m)
140 for m in [path[i] for i in range(0, len(path), 2)]
141 ],
142 [
143 path[i].key if (path[i].is_property) else str(path[i])
144 for i in range(1, len(path), 2)
145 ]
146 + [None],
147 )
148 )
150 @classmethod
151 def _deserialize_path(cls, path):
152 def _deserialize_mapper_token(mcls):
153 return (
154 # note: we likely dont want configure=True here however
155 # this is maintained at the moment for backwards compatibility
156 orm_base._inspect_mapped_class(mcls, configure=True)
157 if mcls not in PathToken._intern
158 else PathToken._intern[mcls]
159 )
161 def _deserialize_key_token(mcls, key):
162 if key is None:
163 return None
164 elif key in PathToken._intern:
165 return PathToken._intern[key]
166 else:
167 return orm_base._inspect_mapped_class(
168 mcls, configure=True
169 ).attrs[key]
171 p = tuple(
172 chain(
173 *[
174 (
175 _deserialize_mapper_token(mcls),
176 _deserialize_key_token(mcls, key),
177 )
178 for mcls, key in path
179 ]
180 )
181 )
182 if p and p[-1] is None:
183 p = p[0:-1]
184 return p
186 @classmethod
187 def serialize_context_dict(cls, dict_, tokens):
188 return [
189 ((key, cls._serialize_path(path)), value)
190 for (key, path), value in [
191 (k, v)
192 for k, v in dict_.items()
193 if isinstance(k, tuple) and k[0] in tokens
194 ]
195 ]
197 @classmethod
198 def deserialize_context_dict(cls, serialized):
199 return util.OrderedDict(
200 ((key, tuple(cls._deserialize_path(path))), value)
201 for (key, path), value in serialized
202 )
204 def serialize(self):
205 path = self.path
206 return self._serialize_path(path)
208 @classmethod
209 def deserialize(cls, path):
210 if path is None:
211 return None
212 p = cls._deserialize_path(path)
213 return cls.coerce(p)
215 @classmethod
216 def per_mapper(cls, mapper):
217 if mapper.is_mapper:
218 return CachingEntityRegistry(cls.root, mapper)
219 else:
220 return SlotsEntityRegistry(cls.root, mapper)
222 @classmethod
223 def coerce(cls, raw):
224 return util.reduce(lambda prev, next: prev[next], raw, cls.root)
226 def token(self, token):
227 if token.endswith(":" + _WILDCARD_TOKEN):
228 return TokenRegistry(self, token)
229 elif token.endswith(":" + _DEFAULT_TOKEN):
230 return TokenRegistry(self.root, token)
231 else:
232 raise exc.ArgumentError("invalid token: %s" % token)
234 def __add__(self, other):
235 return util.reduce(lambda prev, next: prev[next], other.path, self)
237 def __repr__(self):
238 return "%s(%r)" % (self.__class__.__name__, self.path)
241class RootRegistry(PathRegistry):
242 """Root registry, defers to mappers so that
243 paths are maintained per-root-mapper.
245 """
247 inherit_cache = True
249 path = natural_path = ()
250 has_entity = False
251 is_aliased_class = False
252 is_root = True
254 def __getitem__(self, entity):
255 if entity in PathToken._intern:
256 return PathToken._intern[entity]
257 else:
258 return entity._path_registry
261PathRegistry.root = RootRegistry()
264class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
265 """cacheable string token"""
267 _intern = {}
269 def _gen_cache_key(self, anon_map, bindparams):
270 return (str(self),)
272 @property
273 def _path_for_compare(self):
274 return None
276 @classmethod
277 def intern(cls, strvalue):
278 if strvalue in cls._intern:
279 return cls._intern[strvalue]
280 else:
281 cls._intern[strvalue] = result = PathToken(strvalue)
282 return result
285class TokenRegistry(PathRegistry):
286 __slots__ = ("token", "parent", "path", "natural_path")
288 inherit_cache = True
290 def __init__(self, parent, token):
291 token = PathToken.intern(token)
293 self.token = token
294 self.parent = parent
295 self.path = parent.path + (token,)
296 self.natural_path = parent.natural_path + (token,)
298 has_entity = False
300 is_token = True
302 def generate_for_superclasses(self):
303 if not self.parent.is_aliased_class and not self.parent.is_root:
304 for ent in self.parent.mapper.iterate_to_root():
305 yield TokenRegistry(self.parent.parent[ent], self.token)
306 elif (
307 self.parent.is_aliased_class
308 and self.parent.entity._is_with_polymorphic
309 ):
310 yield self
311 for ent in self.parent.entity._with_polymorphic_entities:
312 yield TokenRegistry(self.parent.parent[ent], self.token)
313 else:
314 yield self
316 def __getitem__(self, entity):
317 raise NotImplementedError()
320class PropRegistry(PathRegistry):
321 is_unnatural = False
322 inherit_cache = True
324 def __init__(self, parent, prop):
325 # restate this path in terms of the
326 # given MapperProperty's parent.
327 insp = inspection.inspect(parent[-1])
328 natural_parent = parent
330 if not insp.is_aliased_class or insp._use_mapper_path:
331 parent = natural_parent = parent.parent[prop.parent]
332 elif (
333 insp.is_aliased_class
334 and insp.with_polymorphic_mappers
335 and prop.parent in insp.with_polymorphic_mappers
336 ):
337 subclass_entity = parent[-1]._entity_for_mapper(prop.parent)
338 parent = parent.parent[subclass_entity]
340 # when building a path where with_polymorphic() is in use,
341 # special logic to determine the "natural path" when subclass
342 # entities are used.
343 #
344 # here we are trying to distinguish between a path that starts
345 # on a the with_polymorhpic entity vs. one that starts on a
346 # normal entity that introduces a with_polymorphic() in the
347 # middle using of_type():
348 #
349 # # as in test_polymorphic_rel->
350 # # test_subqueryload_on_subclass_uses_path_correctly
351 # wp = with_polymorphic(RegularEntity, "*")
352 # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
353 #
354 # vs
355 #
356 # # as in test_relationship->JoinedloadWPolyOfTypeContinued
357 # wp = with_polymorphic(SomeFoo, "*")
358 # sess.query(RegularEntity).options(
359 # someload(RegularEntity.foos.of_type(wp))
360 # .someload(wp.SubFoo.bar)
361 # )
362 #
363 # in the former case, the Query as it generates a path that we
364 # want to match will be in terms of the with_polymorphic at the
365 # beginning. in the latter case, Query will generate simple
366 # paths that don't know about this with_polymorphic, so we must
367 # use a separate natural path.
368 #
369 #
370 if parent.parent:
371 natural_parent = parent.parent[subclass_entity.mapper]
372 self.is_unnatural = True
373 else:
374 natural_parent = parent
375 elif (
376 natural_parent.parent
377 and insp.is_aliased_class
378 and prop.parent # this should always be the case here
379 is not insp.mapper
380 and insp.mapper.isa(prop.parent)
381 ):
382 natural_parent = parent.parent[prop.parent]
384 self.prop = prop
385 self.parent = parent
386 self.path = parent.path + (prop,)
387 self.natural_path = natural_parent.natural_path + (prop,)
389 self._wildcard_path_loader_key = (
390 "loader",
391 parent.path + self.prop._wildcard_token,
392 )
393 self._default_path_loader_key = self.prop._default_path_loader_key
394 self._loader_key = ("loader", self.natural_path)
396 def __str__(self):
397 return " -> ".join(str(elem) for elem in self.path)
399 @util.memoized_property
400 def has_entity(self):
401 return self.prop._links_to_entity
403 @util.memoized_property
404 def entity(self):
405 return self.prop.entity
407 @property
408 def mapper(self):
409 return self.prop.mapper
411 @property
412 def entity_path(self):
413 return self[self.entity]
415 def __getitem__(self, entity):
416 if isinstance(entity, (int, slice)):
417 return self.path[entity]
418 else:
419 return SlotsEntityRegistry(self, entity)
422class AbstractEntityRegistry(PathRegistry):
423 __slots__ = ()
425 has_entity = True
427 def __init__(self, parent, entity):
428 self.key = entity
429 self.parent = parent
430 self.is_aliased_class = entity.is_aliased_class
431 self.entity = entity
432 self.path = parent.path + (entity,)
434 # the "natural path" is the path that we get when Query is traversing
435 # from the lead entities into the various relationships; it corresponds
436 # to the structure of mappers and relationships. when we are given a
437 # path that comes from loader options, as of 1.3 it can have ac-hoc
438 # with_polymorphic() and other AliasedInsp objects inside of it, which
439 # are usually not present in mappings. So here we track both the
440 # "enhanced" path in self.path and the "natural" path that doesn't
441 # include those objects so these two traversals can be matched up.
443 # the test here for "(self.is_aliased_class or parent.is_unnatural)"
444 # are to avoid the more expensive conditional logic that follows if we
445 # know we don't have to do it. This conditional can just as well be
446 # "if parent.path:", it just is more function calls.
447 if parent.path and (self.is_aliased_class or parent.is_unnatural):
448 # this is an infrequent code path used only for loader strategies
449 # that also make use of of_type().
450 if entity.mapper.isa(parent.natural_path[-1].entity):
451 self.natural_path = parent.natural_path + (entity.mapper,)
452 else:
453 self.natural_path = parent.natural_path + (
454 parent.natural_path[-1].entity,
455 )
456 # it seems to make sense that since these paths get mixed up
457 # with statements that are cached or not, we should make
458 # sure the natural path is cacheable across different occurrences
459 # of equivalent AliasedClass objects. however, so far this
460 # does not seem to be needed for whatever reason.
461 # elif not parent.path and self.is_aliased_class:
462 # self.natural_path = (self.entity._generate_cache_key()[0], )
463 else:
464 # self.natural_path = parent.natural_path + (entity, )
465 self.natural_path = self.path
467 @property
468 def entity_path(self):
469 return self
471 @property
472 def mapper(self):
473 return inspection.inspect(self.entity).mapper
475 def __bool__(self):
476 return True
478 __nonzero__ = __bool__
480 def __getitem__(self, entity):
481 if isinstance(entity, (int, slice)):
482 return self.path[entity]
483 elif entity in PathToken._intern:
484 return TokenRegistry(self, PathToken._intern[entity])
485 else:
486 return PropRegistry(self, entity)
489class SlotsEntityRegistry(AbstractEntityRegistry):
490 # for aliased class, return lightweight, no-cycles created
491 # version
492 inherit_cache = True
494 __slots__ = (
495 "key",
496 "parent",
497 "is_aliased_class",
498 "entity",
499 "path",
500 "natural_path",
501 )
504class CachingEntityRegistry(AbstractEntityRegistry, dict):
505 # for long lived mapper, return dict based caching
506 # version that creates reference cycles
508 inherit_cache = True
510 def __getitem__(self, entity):
511 if isinstance(entity, (int, slice)):
512 return self.path[entity]
513 else:
514 return dict.__getitem__(self, entity)
516 def __missing__(self, key):
517 self[key] = item = PropRegistry(self, key)
519 return item