Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/sqlalchemy/ext/declarative/clsregistry.py : 68%

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# ext/declarative/clsregistry.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"""Routines to handle the string class registry used by declarative.
9This system allows specification of classes and expressions used in
10:func:`_orm.relationship` using strings.
12"""
13import weakref
15from ... import exc
16from ... import inspection
17from ... import util
18from ...orm import class_mapper
19from ...orm import interfaces
20from ...orm.properties import ColumnProperty
21from ...orm.properties import RelationshipProperty
22from ...orm.properties import SynonymProperty
23from ...schema import _get_table_key
26# strong references to registries which we place in
27# the _decl_class_registry, which is usually weak referencing.
28# the internal registries here link to classes with weakrefs and remove
29# themselves when all references to contained classes are removed.
30_registries = set()
33def add_class(classname, cls):
34 """Add a class to the _decl_class_registry associated with the
35 given declarative class.
37 """
38 if classname in cls._decl_class_registry:
39 # class already exists.
40 existing = cls._decl_class_registry[classname]
41 if not isinstance(existing, _MultipleClassMarker):
42 existing = cls._decl_class_registry[
43 classname
44 ] = _MultipleClassMarker([cls, existing])
45 else:
46 cls._decl_class_registry[classname] = cls
48 try:
49 root_module = cls._decl_class_registry["_sa_module_registry"]
50 except KeyError:
51 cls._decl_class_registry[
52 "_sa_module_registry"
53 ] = root_module = _ModuleMarker("_sa_module_registry", None)
55 tokens = cls.__module__.split(".")
57 # build up a tree like this:
58 # modulename: myapp.snacks.nuts
59 #
60 # myapp->snack->nuts->(classes)
61 # snack->nuts->(classes)
62 # nuts->(classes)
63 #
64 # this allows partial token paths to be used.
65 while tokens:
66 token = tokens.pop(0)
67 module = root_module.get_module(token)
68 for token in tokens:
69 module = module.get_module(token)
70 module.add_class(classname, cls)
73class _MultipleClassMarker(object):
74 """refers to multiple classes of the same name
75 within _decl_class_registry.
77 """
79 __slots__ = "on_remove", "contents", "__weakref__"
81 def __init__(self, classes, on_remove=None):
82 self.on_remove = on_remove
83 self.contents = set(
84 [weakref.ref(item, self._remove_item) for item in classes]
85 )
86 _registries.add(self)
88 def __iter__(self):
89 return (ref() for ref in self.contents)
91 def attempt_get(self, path, key):
92 if len(self.contents) > 1:
93 raise exc.InvalidRequestError(
94 'Multiple classes found for path "%s" '
95 "in the registry of this declarative "
96 "base. Please use a fully module-qualified path."
97 % (".".join(path + [key]))
98 )
99 else:
100 ref = list(self.contents)[0]
101 cls = ref()
102 if cls is None:
103 raise NameError(key)
104 return cls
106 def _remove_item(self, ref):
107 self.contents.remove(ref)
108 if not self.contents:
109 _registries.discard(self)
110 if self.on_remove:
111 self.on_remove()
113 def add_item(self, item):
114 # protect against class registration race condition against
115 # asynchronous garbage collection calling _remove_item,
116 # [ticket:3208]
117 modules = set(
118 [
119 cls.__module__
120 for cls in [ref() for ref in self.contents]
121 if cls is not None
122 ]
123 )
124 if item.__module__ in modules:
125 util.warn(
126 "This declarative base already contains a class with the "
127 "same class name and module name as %s.%s, and will "
128 "be replaced in the string-lookup table."
129 % (item.__module__, item.__name__)
130 )
131 self.contents.add(weakref.ref(item, self._remove_item))
134class _ModuleMarker(object):
135 """"refers to a module name within
136 _decl_class_registry.
138 """
140 __slots__ = "parent", "name", "contents", "mod_ns", "path", "__weakref__"
142 def __init__(self, name, parent):
143 self.parent = parent
144 self.name = name
145 self.contents = {}
146 self.mod_ns = _ModNS(self)
147 if self.parent:
148 self.path = self.parent.path + [self.name]
149 else:
150 self.path = []
151 _registries.add(self)
153 def __contains__(self, name):
154 return name in self.contents
156 def __getitem__(self, name):
157 return self.contents[name]
159 def _remove_item(self, name):
160 self.contents.pop(name, None)
161 if not self.contents and self.parent is not None:
162 self.parent._remove_item(self.name)
163 _registries.discard(self)
165 def resolve_attr(self, key):
166 return getattr(self.mod_ns, key)
168 def get_module(self, name):
169 if name not in self.contents:
170 marker = _ModuleMarker(name, self)
171 self.contents[name] = marker
172 else:
173 marker = self.contents[name]
174 return marker
176 def add_class(self, name, cls):
177 if name in self.contents:
178 existing = self.contents[name]
179 existing.add_item(cls)
180 else:
181 existing = self.contents[name] = _MultipleClassMarker(
182 [cls], on_remove=lambda: self._remove_item(name)
183 )
186class _ModNS(object):
187 __slots__ = ("__parent",)
189 def __init__(self, parent):
190 self.__parent = parent
192 def __getattr__(self, key):
193 try:
194 value = self.__parent.contents[key]
195 except KeyError:
196 pass
197 else:
198 if value is not None:
199 if isinstance(value, _ModuleMarker):
200 return value.mod_ns
201 else:
202 assert isinstance(value, _MultipleClassMarker)
203 return value.attempt_get(self.__parent.path, key)
204 raise AttributeError(
205 "Module %r has no mapped classes "
206 "registered under the name %r" % (self.__parent.name, key)
207 )
210class _GetColumns(object):
211 __slots__ = ("cls",)
213 def __init__(self, cls):
214 self.cls = cls
216 def __getattr__(self, key):
217 mp = class_mapper(self.cls, configure=False)
218 if mp:
219 if key not in mp.all_orm_descriptors:
220 raise exc.InvalidRequestError(
221 "Class %r does not have a mapped column named %r"
222 % (self.cls, key)
223 )
225 desc = mp.all_orm_descriptors[key]
226 if desc.extension_type is interfaces.NOT_EXTENSION:
227 prop = desc.property
228 if isinstance(prop, SynonymProperty):
229 key = prop.name
230 elif not isinstance(prop, ColumnProperty):
231 raise exc.InvalidRequestError(
232 "Property %r is not an instance of"
233 " ColumnProperty (i.e. does not correspond"
234 " directly to a Column)." % key
235 )
236 return getattr(self.cls, key)
239inspection._inspects(_GetColumns)(
240 lambda target: inspection.inspect(target.cls)
241)
244class _GetTable(object):
245 __slots__ = "key", "metadata"
247 def __init__(self, key, metadata):
248 self.key = key
249 self.metadata = metadata
251 def __getattr__(self, key):
252 return self.metadata.tables[_get_table_key(key, self.key)]
255def _determine_container(key, value):
256 if isinstance(value, _MultipleClassMarker):
257 value = value.attempt_get([], key)
258 return _GetColumns(value)
261class _class_resolver(object):
262 def __init__(self, cls, prop, fallback, arg):
263 self.cls = cls
264 self.prop = prop
265 self.arg = self._declarative_arg = arg
266 self.fallback = fallback
267 self._dict = util.PopulateDict(self._access_cls)
268 self._resolvers = ()
270 def _access_cls(self, key):
271 cls = self.cls
272 if key in cls._decl_class_registry:
273 return _determine_container(key, cls._decl_class_registry[key])
274 elif key in cls.metadata.tables:
275 return cls.metadata.tables[key]
276 elif key in cls.metadata._schemas:
277 return _GetTable(key, cls.metadata)
278 elif (
279 "_sa_module_registry" in cls._decl_class_registry
280 and key in cls._decl_class_registry["_sa_module_registry"]
281 ):
282 registry = cls._decl_class_registry["_sa_module_registry"]
283 return registry.resolve_attr(key)
284 elif self._resolvers:
285 for resolv in self._resolvers:
286 value = resolv(key)
287 if value is not None:
288 return value
290 return self.fallback[key]
292 def _raise_for_name(self, name, err):
293 util.raise_(
294 exc.InvalidRequestError(
295 "When initializing mapper %s, expression %r failed to "
296 "locate a name (%r). If this is a class name, consider "
297 "adding this relationship() to the %r class after "
298 "both dependent classes have been defined."
299 % (self.prop.parent, self.arg, name, self.cls)
300 ),
301 from_=err,
302 )
304 def _resolve_name(self):
305 name = self.arg
306 d = self._dict
307 rval = None
308 try:
309 for token in name.split("."):
310 if rval is None:
311 rval = d[token]
312 else:
313 rval = getattr(rval, token)
314 except KeyError as err:
315 self._raise_for_name(name, err)
316 except NameError as n:
317 self._raise_for_name(n.args[0], n)
318 else:
319 if isinstance(rval, _GetColumns):
320 return rval.cls
321 else:
322 return rval
324 def __call__(self):
325 try:
326 x = eval(self.arg, globals(), self._dict)
328 if isinstance(x, _GetColumns):
329 return x.cls
330 else:
331 return x
332 except NameError as n:
333 self._raise_for_name(n.args[0], n)
336def _resolver(cls, prop):
337 import sqlalchemy
338 from sqlalchemy.orm import foreign, remote
340 fallback = sqlalchemy.__dict__.copy()
341 fallback.update({"foreign": foreign, "remote": remote})
343 def resolve_arg(arg):
344 return _class_resolver(cls, prop, fallback, arg)
346 def resolve_name(arg):
347 return _class_resolver(cls, prop, fallback, arg)._resolve_name
349 return resolve_name, resolve_arg
352def _deferred_relationship(cls, prop):
354 if isinstance(prop, RelationshipProperty):
355 resolve_name, resolve_arg = _resolver(cls, prop)
357 for attr in (
358 "order_by",
359 "primaryjoin",
360 "secondaryjoin",
361 "secondary",
362 "_user_defined_foreign_keys",
363 "remote_side",
364 ):
365 v = getattr(prop, attr)
366 if isinstance(v, util.string_types):
367 setattr(prop, attr, resolve_arg(v))
369 for attr in ("argument",):
370 v = getattr(prop, attr)
371 if isinstance(v, util.string_types):
372 setattr(prop, attr, resolve_name(v))
374 if prop.backref and isinstance(prop.backref, tuple):
375 key, kwargs = prop.backref
376 for attr in (
377 "primaryjoin",
378 "secondaryjoin",
379 "secondary",
380 "foreign_keys",
381 "remote_side",
382 "order_by",
383 ):
384 if attr in kwargs and isinstance(
385 kwargs[attr], util.string_types
386 ):
387 kwargs[attr] = resolve_arg(kwargs[attr])
389 return prop