Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/sqlalchemy/orm/path_registry.py : 61%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# orm/path_registry.py
2# Copyright (C) 2005-2020 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: http://www.opensource.org/licenses/mit-license.php
7"""Path tracking utilities, representing mapper graph traversals.
9"""
11from itertools import chain
12import logging
14from .base import class_mapper
15from .. import exc
16from .. import inspection
17from .. import util
20log = logging.getLogger(__name__)
23def _unreduce_path(path):
24 return PathRegistry.deserialize(path)
27_WILDCARD_TOKEN = "*"
28_DEFAULT_TOKEN = "_sa_default"
31class PathRegistry(object):
32 """Represent query load paths and registry functions.
34 Basically represents structures like:
36 (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
38 These structures are generated by things like
39 query options (joinedload(), subqueryload(), etc.) and are
40 used to compose keys stored in the query._attributes dictionary
41 for various options.
43 They are then re-composed at query compile/result row time as
44 the query is formed and as rows are fetched, where they again
45 serve to compose keys to look up options in the context.attributes
46 dictionary, which is copied from query._attributes.
48 The path structure has a limited amount of caching, where each
49 "root" ultimately pulls from a fixed registry associated with
50 the first mapper, that also contains elements for each of its
51 property keys. However paths longer than two elements, which
52 are the exception rather than the rule, are generated on an
53 as-needed basis.
55 """
57 __slots__ = ()
59 is_token = False
60 is_root = False
62 def __eq__(self, other):
63 try:
64 return other is not None and self.path == other.path
65 except AttributeError:
66 util.warn(
67 "Comparison of PathRegistry to %r is not supported"
68 % (type(other))
69 )
70 return False
72 def __ne__(self, other):
73 try:
74 return other is None or self.path != other.path
75 except AttributeError:
76 util.warn(
77 "Comparison of PathRegistry to %r is not supported"
78 % (type(other))
79 )
80 return True
82 def set(self, attributes, key, value):
83 log.debug("set '%s' on path '%s' to '%s'", key, self, value)
84 attributes[(key, self.natural_path)] = value
86 def setdefault(self, attributes, key, value):
87 log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
88 attributes.setdefault((key, self.natural_path), value)
90 def get(self, attributes, key, value=None):
91 key = (key, self.natural_path)
92 if key in attributes:
93 return attributes[key]
94 else:
95 return value
97 def __len__(self):
98 return len(self.path)
100 @property
101 def length(self):
102 return len(self.path)
104 def pairs(self):
105 path = self.path
106 for i in range(0, len(path), 2):
107 yield path[i], path[i + 1]
109 def contains_mapper(self, mapper):
110 for path_mapper in [self.path[i] for i in range(0, len(self.path), 2)]:
111 if path_mapper.is_mapper and path_mapper.isa(mapper):
112 return True
113 else:
114 return False
116 def contains(self, attributes, key):
117 return (key, self.path) in attributes
119 def __reduce__(self):
120 return _unreduce_path, (self.serialize(),)
122 @classmethod
123 def _serialize_path(cls, path):
124 return list(
125 zip(
126 [m.class_ for m in [path[i] for i in range(0, len(path), 2)]],
127 [path[i].key for i in range(1, len(path), 2)] + [None],
128 )
129 )
131 @classmethod
132 def _deserialize_path(cls, path):
133 p = tuple(
134 chain(
135 *[
136 (
137 class_mapper(mcls),
138 class_mapper(mcls).attrs[key]
139 if key is not None
140 else None,
141 )
142 for mcls, key in path
143 ]
144 )
145 )
146 if p and p[-1] is None:
147 p = p[0:-1]
148 return p
150 @classmethod
151 def serialize_context_dict(cls, dict_, tokens):
152 return [
153 ((key, cls._serialize_path(path)), value)
154 for (key, path), value in [
155 (k, v)
156 for k, v in dict_.items()
157 if isinstance(k, tuple) and k[0] in tokens
158 ]
159 ]
161 @classmethod
162 def deserialize_context_dict(cls, serialized):
163 return util.OrderedDict(
164 ((key, tuple(cls._deserialize_path(path))), value)
165 for (key, path), value in serialized
166 )
168 def serialize(self):
169 path = self.path
170 return self._serialize_path(path)
172 @classmethod
173 def deserialize(cls, path):
174 if path is None:
175 return None
176 p = cls._deserialize_path(path)
177 return cls.coerce(p)
179 @classmethod
180 def per_mapper(cls, mapper):
181 if mapper.is_mapper:
182 return CachingEntityRegistry(cls.root, mapper)
183 else:
184 return SlotsEntityRegistry(cls.root, mapper)
186 @classmethod
187 def coerce(cls, raw):
188 return util.reduce(lambda prev, next: prev[next], raw, cls.root)
190 def token(self, token):
191 if token.endswith(":" + _WILDCARD_TOKEN):
192 return TokenRegistry(self, token)
193 elif token.endswith(":" + _DEFAULT_TOKEN):
194 return TokenRegistry(self.root, token)
195 else:
196 raise exc.ArgumentError("invalid token: %s" % token)
198 def __add__(self, other):
199 return util.reduce(lambda prev, next: prev[next], other.path, self)
201 def __repr__(self):
202 return "%s(%r)" % (self.__class__.__name__, self.path)
205class RootRegistry(PathRegistry):
206 """Root registry, defers to mappers so that
207 paths are maintained per-root-mapper.
209 """
211 path = natural_path = ()
212 has_entity = False
213 is_aliased_class = False
214 is_root = True
216 def __getitem__(self, entity):
217 return entity._path_registry
220PathRegistry.root = RootRegistry()
223class TokenRegistry(PathRegistry):
224 __slots__ = ("token", "parent", "path", "natural_path")
226 def __init__(self, parent, token):
227 self.token = token
228 self.parent = parent
229 self.path = parent.path + (token,)
230 self.natural_path = parent.natural_path + (token,)
232 has_entity = False
234 is_token = True
236 def generate_for_superclasses(self):
237 if not self.parent.is_aliased_class and not self.parent.is_root:
238 for ent in self.parent.mapper.iterate_to_root():
239 yield TokenRegistry(self.parent.parent[ent], self.token)
240 elif (
241 self.parent.is_aliased_class
242 and self.parent.entity._is_with_polymorphic
243 ):
244 yield self
245 for ent in self.parent.entity._with_polymorphic_entities:
246 yield TokenRegistry(self.parent.parent[ent], self.token)
247 else:
248 yield self
250 def __getitem__(self, entity):
251 raise NotImplementedError()
254class PropRegistry(PathRegistry):
255 is_unnatural = False
257 def __init__(self, parent, prop):
258 # restate this path in terms of the
259 # given MapperProperty's parent.
260 insp = inspection.inspect(parent[-1])
261 natural_parent = parent
263 if not insp.is_aliased_class or insp._use_mapper_path:
264 parent = natural_parent = parent.parent[prop.parent]
265 elif (
266 insp.is_aliased_class
267 and insp.with_polymorphic_mappers
268 and prop.parent in insp.with_polymorphic_mappers
269 ):
270 subclass_entity = parent[-1]._entity_for_mapper(prop.parent)
271 parent = parent.parent[subclass_entity]
273 # when building a path where with_polymorphic() is in use,
274 # special logic to determine the "natural path" when subclass
275 # entities are used.
276 #
277 # here we are trying to distinguish between a path that starts
278 # on a the with_polymorhpic entity vs. one that starts on a
279 # normal entity that introduces a with_polymorphic() in the
280 # middle using of_type():
281 #
282 # # as in test_polymorphic_rel->
283 # # test_subqueryload_on_subclass_uses_path_correctly
284 # wp = with_polymorphic(RegularEntity, "*")
285 # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
286 #
287 # vs
288 #
289 # # as in test_relationship->JoinedloadWPolyOfTypeContinued
290 # wp = with_polymorphic(SomeFoo, "*")
291 # sess.query(RegularEntity).options(
292 # someload(RegularEntity.foos.of_type(wp))
293 # .someload(wp.SubFoo.bar)
294 # )
295 #
296 # in the former case, the Query as it generates a path that we
297 # want to match will be in terms of the with_polymorphic at the
298 # beginning. in the latter case, Query will generate simple
299 # paths that don't know about this with_polymorphic, so we must
300 # use a separate natural path.
301 #
302 #
303 if parent.parent:
304 natural_parent = parent.parent[subclass_entity.mapper]
305 self.is_unnatural = True
306 else:
307 natural_parent = parent
308 elif (
309 natural_parent.parent
310 and insp.is_aliased_class
311 and prop.parent # this should always be the case here
312 is not insp.mapper
313 and insp.mapper.isa(prop.parent)
314 ):
315 natural_parent = parent.parent[prop.parent]
317 self.prop = prop
318 self.parent = parent
319 self.path = parent.path + (prop,)
320 self.natural_path = natural_parent.natural_path + (prop,)
322 self._wildcard_path_loader_key = (
323 "loader",
324 parent.path + self.prop._wildcard_token,
325 )
326 self._default_path_loader_key = self.prop._default_path_loader_key
327 self._loader_key = ("loader", self.path)
329 def __str__(self):
330 return " -> ".join(str(elem) for elem in self.path)
332 @util.memoized_property
333 def has_entity(self):
334 return hasattr(self.prop, "mapper")
336 @util.memoized_property
337 def entity(self):
338 return self.prop.mapper
340 @property
341 def mapper(self):
342 return self.entity
344 @property
345 def entity_path(self):
346 return self[self.entity]
348 def __getitem__(self, entity):
349 if isinstance(entity, (int, slice)):
350 return self.path[entity]
351 else:
352 return SlotsEntityRegistry(self, entity)
355class AbstractEntityRegistry(PathRegistry):
356 __slots__ = ()
358 has_entity = True
360 def __init__(self, parent, entity):
361 self.key = entity
362 self.parent = parent
363 self.is_aliased_class = entity.is_aliased_class
364 self.entity = entity
365 self.path = parent.path + (entity,)
367 # the "natural path" is the path that we get when Query is traversing
368 # from the lead entities into the various relationships; it corresponds
369 # to the structure of mappers and relationships. when we are given a
370 # path that comes from loader options, as of 1.3 it can have ac-hoc
371 # with_polymorphic() and other AliasedInsp objects inside of it, which
372 # are usually not present in mappings. So here we track both the
373 # "enhanced" path in self.path and the "natural" path that doesn't
374 # include those objects so these two traversals can be matched up.
376 # the test here for "(self.is_aliased_class or parent.is_unnatural)"
377 # are to avoid the more expensive conditional logic that follows if we
378 # know we don't have to do it. This conditional can just as well be
379 # "if parent.path:", it just is more function calls.
380 if parent.path and (self.is_aliased_class or parent.is_unnatural):
381 # this is an infrequent code path used only for loader strategies
382 # that also make use of of_type().
383 if entity.mapper.isa(parent.natural_path[-1].entity):
384 self.natural_path = parent.natural_path + (entity.mapper,)
385 else:
386 self.natural_path = parent.natural_path + (
387 parent.natural_path[-1].entity,
388 )
389 else:
390 self.natural_path = self.path
392 @property
393 def entity_path(self):
394 return self
396 @property
397 def mapper(self):
398 return inspection.inspect(self.entity).mapper
400 def __bool__(self):
401 return True
403 __nonzero__ = __bool__
405 def __getitem__(self, entity):
406 if isinstance(entity, (int, slice)):
407 return self.path[entity]
408 else:
409 return PropRegistry(self, entity)
412class SlotsEntityRegistry(AbstractEntityRegistry):
413 # for aliased class, return lightweight, no-cycles created
414 # version
416 __slots__ = (
417 "key",
418 "parent",
419 "is_aliased_class",
420 "entity",
421 "path",
422 "natural_path",
423 )
426class CachingEntityRegistry(AbstractEntityRegistry, dict):
427 # for long lived mapper, return dict based caching
428 # version that creates reference cycles
430 def __getitem__(self, entity):
431 if isinstance(entity, (int, slice)):
432 return self.path[entity]
433 else:
434 return dict.__getitem__(self, entity)
436 def __missing__(self, key):
437 self[key] = item = PropRegistry(self, key)
439 return item