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

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/instrumentation.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
8"""Defines SQLAlchemy's system of class instrumentation.
10This module is usually not directly visible to user applications, but
11defines a large part of the ORM's interactivity.
13instrumentation.py deals with registration of end-user classes
14for state tracking. It interacts closely with state.py
15and attributes.py which establish per-instance and per-class-attribute
16instrumentation, respectively.
18The class instrumentation system can be customized on a per-class
19or global basis using the :mod:`sqlalchemy.ext.instrumentation`
20module, which provides the means to build and specify
21alternate instrumentation forms.
23.. versionchanged: 0.8
24 The instrumentation extension system was moved out of the
25 ORM and into the external :mod:`sqlalchemy.ext.instrumentation`
26 package. When that package is imported, it installs
27 itself within sqlalchemy.orm so that its more comprehensive
28 resolution mechanics take effect.
30"""
33from . import base
34from . import collections
35from . import exc
36from . import interfaces
37from . import state
38from .. import util
41_memoized_key_collection = util.group_expirable_memoized_property()
44class ClassManager(dict):
45 """tracks state information at the class level."""
47 MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR
48 STATE_ATTR = base.DEFAULT_STATE_ATTR
50 _state_setter = staticmethod(util.attrsetter(STATE_ATTR))
52 deferred_scalar_loader = None
54 original_init = object.__init__
56 factory = None
58 def __init__(self, class_):
59 self.class_ = class_
60 self.info = {}
61 self.new_init = None
62 self.local_attrs = {}
63 self.originals = {}
65 self._bases = [
66 mgr
67 for mgr in [
68 manager_of_class(base)
69 for base in self.class_.__bases__
70 if isinstance(base, type)
71 ]
72 if mgr is not None
73 ]
75 for base_ in self._bases:
76 self.update(base_)
78 self.dispatch._events._new_classmanager_instance(class_, self)
79 # events._InstanceEventsHold.populate(class_, self)
81 for basecls in class_.__mro__:
82 mgr = manager_of_class(basecls)
83 if mgr is not None:
84 self.dispatch._update(mgr.dispatch)
85 self.manage()
86 self._instrument_init()
88 if "__del__" in class_.__dict__:
89 util.warn(
90 "__del__() method on class %s will "
91 "cause unreachable cycles and memory leaks, "
92 "as SQLAlchemy instrumentation often creates "
93 "reference cycles. Please remove this method." % class_
94 )
96 def __hash__(self):
97 return id(self)
99 def __eq__(self, other):
100 return other is self
102 @property
103 def is_mapped(self):
104 return "mapper" in self.__dict__
106 @_memoized_key_collection
107 def _all_key_set(self):
108 return frozenset(self)
110 @_memoized_key_collection
111 def _collection_impl_keys(self):
112 return frozenset(
113 [attr.key for attr in self.values() if attr.impl.collection]
114 )
116 @_memoized_key_collection
117 def _scalar_loader_impls(self):
118 return frozenset(
119 [
120 attr.impl
121 for attr in self.values()
122 if attr.impl.accepts_scalar_loader
123 ]
124 )
126 @util.memoized_property
127 def mapper(self):
128 # raises unless self.mapper has been assigned
129 raise exc.UnmappedClassError(self.class_)
131 def _all_sqla_attributes(self, exclude=None):
132 """return an iterator of all classbound attributes that are
133 implement :class:`.InspectionAttr`.
135 This includes :class:`.QueryableAttribute` as well as extension
136 types such as :class:`.hybrid_property` and
137 :class:`.AssociationProxy`.
139 """
140 if exclude is None:
141 exclude = set()
142 for supercls in self.class_.__mro__:
143 for key in set(supercls.__dict__).difference(exclude):
144 exclude.add(key)
145 val = supercls.__dict__[key]
146 if (
147 isinstance(val, interfaces.InspectionAttr)
148 and val.is_attribute
149 ):
150 yield key, val
152 def _get_class_attr_mro(self, key, default=None):
153 """return an attribute on the class without tripping it."""
155 for supercls in self.class_.__mro__:
156 if key in supercls.__dict__:
157 return supercls.__dict__[key]
158 else:
159 return default
161 def _attr_has_impl(self, key):
162 """Return True if the given attribute is fully initialized.
164 i.e. has an impl.
165 """
167 return key in self and self[key].impl is not None
169 def _subclass_manager(self, cls):
170 """Create a new ClassManager for a subclass of this ClassManager's
171 class.
173 This is called automatically when attributes are instrumented so that
174 the attributes can be propagated to subclasses against their own
175 class-local manager, without the need for mappers etc. to have already
176 pre-configured managers for the full class hierarchy. Mappers
177 can post-configure the auto-generated ClassManager when needed.
179 """
180 manager = manager_of_class(cls)
181 if manager is None:
182 manager = _instrumentation_factory.create_manager_for_cls(cls)
183 return manager
185 def _instrument_init(self):
186 # TODO: self.class_.__init__ is often the already-instrumented
187 # __init__ from an instrumented superclass. We still need to make
188 # our own wrapper, but it would
189 # be nice to wrap the original __init__ and not our existing wrapper
190 # of such, since this adds method overhead.
191 self.original_init = self.class_.__init__
192 self.new_init = _generate_init(self.class_, self)
193 self.install_member("__init__", self.new_init)
195 def _uninstrument_init(self):
196 if self.new_init:
197 self.uninstall_member("__init__")
198 self.new_init = None
200 @util.memoized_property
201 def _state_constructor(self):
202 self.dispatch.first_init(self, self.class_)
203 return state.InstanceState
205 def manage(self):
206 """Mark this instance as the manager for its class."""
208 setattr(self.class_, self.MANAGER_ATTR, self)
210 def dispose(self):
211 """Dissasociate this manager from its class."""
213 delattr(self.class_, self.MANAGER_ATTR)
215 @util.hybridmethod
216 def manager_getter(self):
217 return _default_manager_getter
219 @util.hybridmethod
220 def state_getter(self):
221 """Return a (instance) -> InstanceState callable.
223 "state getter" callables should raise either KeyError or
224 AttributeError if no InstanceState could be found for the
225 instance.
226 """
228 return _default_state_getter
230 @util.hybridmethod
231 def dict_getter(self):
232 return _default_dict_getter
234 def instrument_attribute(self, key, inst, propagated=False):
235 if propagated:
236 if key in self.local_attrs:
237 return # don't override local attr with inherited attr
238 else:
239 self.local_attrs[key] = inst
240 self.install_descriptor(key, inst)
241 _memoized_key_collection.expire_instance(self)
242 self[key] = inst
244 for cls in self.class_.__subclasses__():
245 manager = self._subclass_manager(cls)
246 manager.instrument_attribute(key, inst, True)
248 def subclass_managers(self, recursive):
249 for cls in self.class_.__subclasses__():
250 mgr = manager_of_class(cls)
251 if mgr is not None and mgr is not self:
252 yield mgr
253 if recursive:
254 for m in mgr.subclass_managers(True):
255 yield m
257 def post_configure_attribute(self, key):
258 _instrumentation_factory.dispatch.attribute_instrument(
259 self.class_, key, self[key]
260 )
262 def uninstrument_attribute(self, key, propagated=False):
263 if key not in self:
264 return
265 if propagated:
266 if key in self.local_attrs:
267 return # don't get rid of local attr
268 else:
269 del self.local_attrs[key]
270 self.uninstall_descriptor(key)
271 _memoized_key_collection.expire_instance(self)
272 del self[key]
273 for cls in self.class_.__subclasses__():
274 manager = manager_of_class(cls)
275 if manager:
276 manager.uninstrument_attribute(key, True)
278 def unregister(self):
279 """remove all instrumentation established by this ClassManager."""
281 self._uninstrument_init()
283 self.mapper = self.dispatch = None
284 self.info.clear()
286 for key in list(self):
287 if key in self.local_attrs:
288 self.uninstrument_attribute(key)
290 def install_descriptor(self, key, inst):
291 if key in (self.STATE_ATTR, self.MANAGER_ATTR):
292 raise KeyError(
293 "%r: requested attribute name conflicts with "
294 "instrumentation attribute of the same name." % key
295 )
296 setattr(self.class_, key, inst)
298 def uninstall_descriptor(self, key):
299 delattr(self.class_, key)
301 def install_member(self, key, implementation):
302 if key in (self.STATE_ATTR, self.MANAGER_ATTR):
303 raise KeyError(
304 "%r: requested attribute name conflicts with "
305 "instrumentation attribute of the same name." % key
306 )
307 self.originals.setdefault(key, getattr(self.class_, key, None))
308 setattr(self.class_, key, implementation)
310 def uninstall_member(self, key):
311 original = self.originals.pop(key, None)
312 if original is not None:
313 setattr(self.class_, key, original)
315 def instrument_collection_class(self, key, collection_class):
316 return collections.prepare_instrumentation(collection_class)
318 def initialize_collection(self, key, state, factory):
319 user_data = factory()
320 adapter = collections.CollectionAdapter(
321 self.get_impl(key), state, user_data
322 )
323 return adapter, user_data
325 def is_instrumented(self, key, search=False):
326 if search:
327 return key in self
328 else:
329 return key in self.local_attrs
331 def get_impl(self, key):
332 return self[key].impl
334 @property
335 def attributes(self):
336 return iter(self.values())
338 # InstanceState management
340 def new_instance(self, state=None):
341 instance = self.class_.__new__(self.class_)
342 if state is None:
343 state = self._state_constructor(instance, self)
344 self._state_setter(instance, state)
345 return instance
347 def setup_instance(self, instance, state=None):
348 if state is None:
349 state = self._state_constructor(instance, self)
350 self._state_setter(instance, state)
352 def teardown_instance(self, instance):
353 delattr(instance, self.STATE_ATTR)
355 def _serialize(self, state, state_dict):
356 return _SerializeManager(state, state_dict)
358 def _new_state_if_none(self, instance):
359 """Install a default InstanceState if none is present.
361 A private convenience method used by the __init__ decorator.
363 """
364 if hasattr(instance, self.STATE_ATTR):
365 return False
366 elif self.class_ is not instance.__class__ and self.is_mapped:
367 # this will create a new ClassManager for the
368 # subclass, without a mapper. This is likely a
369 # user error situation but allow the object
370 # to be constructed, so that it is usable
371 # in a non-ORM context at least.
372 return self._subclass_manager(
373 instance.__class__
374 )._new_state_if_none(instance)
375 else:
376 state = self._state_constructor(instance, self)
377 self._state_setter(instance, state)
378 return state
380 def has_state(self, instance):
381 return hasattr(instance, self.STATE_ATTR)
383 def has_parent(self, state, key, optimistic=False):
384 """TODO"""
385 return self.get_impl(key).hasparent(state, optimistic=optimistic)
387 def __bool__(self):
388 """All ClassManagers are non-zero regardless of attribute state."""
389 return True
391 __nonzero__ = __bool__
393 def __repr__(self):
394 return "<%s of %r at %x>" % (
395 self.__class__.__name__,
396 self.class_,
397 id(self),
398 )
401class _SerializeManager(object):
402 """Provide serialization of a :class:`.ClassManager`.
404 The :class:`.InstanceState` uses ``__init__()`` on serialize
405 and ``__call__()`` on deserialize.
407 """
409 def __init__(self, state, d):
410 self.class_ = state.class_
411 manager = state.manager
412 manager.dispatch.pickle(state, d)
414 def __call__(self, state, inst, state_dict):
415 state.manager = manager = manager_of_class(self.class_)
416 if manager is None:
417 raise exc.UnmappedInstanceError(
418 inst,
419 "Cannot deserialize object of type %r - "
420 "no mapper() has "
421 "been configured for this class within the current "
422 "Python process!" % self.class_,
423 )
424 elif manager.is_mapped and not manager.mapper.configured:
425 manager.mapper._configure_all()
427 # setup _sa_instance_state ahead of time so that
428 # unpickle events can access the object normally.
429 # see [ticket:2362]
430 if inst is not None:
431 manager.setup_instance(inst, state)
432 manager.dispatch.unpickle(state, state_dict)
435class InstrumentationFactory(object):
436 """Factory for new ClassManager instances."""
438 def create_manager_for_cls(self, class_):
439 assert class_ is not None
440 assert manager_of_class(class_) is None
442 # give a more complicated subclass
443 # a chance to do what it wants here
444 manager, factory = self._locate_extended_factory(class_)
446 if factory is None:
447 factory = ClassManager
448 manager = factory(class_)
450 self._check_conflicts(class_, factory)
452 manager.factory = factory
454 self.dispatch.class_instrument(class_)
455 return manager
457 def _locate_extended_factory(self, class_):
458 """Overridden by a subclass to do an extended lookup."""
459 return None, None
461 def _check_conflicts(self, class_, factory):
462 """Overridden by a subclass to test for conflicting factories."""
463 return
465 def unregister(self, class_):
466 manager = manager_of_class(class_)
467 manager.unregister()
468 manager.dispose()
469 self.dispatch.class_uninstrument(class_)
470 if ClassManager.MANAGER_ATTR in class_.__dict__:
471 delattr(class_, ClassManager.MANAGER_ATTR)
474# this attribute is replaced by sqlalchemy.ext.instrumentation
475# when importred.
476_instrumentation_factory = InstrumentationFactory()
478# these attributes are replaced by sqlalchemy.ext.instrumentation
479# when a non-standard InstrumentationManager class is first
480# used to instrument a class.
481instance_state = _default_state_getter = base.instance_state
483instance_dict = _default_dict_getter = base.instance_dict
485manager_of_class = _default_manager_getter = base.manager_of_class
488def register_class(class_):
489 """Register class instrumentation.
491 Returns the existing or newly created class manager.
493 """
495 manager = manager_of_class(class_)
496 if manager is None:
497 manager = _instrumentation_factory.create_manager_for_cls(class_)
498 return manager
501def unregister_class(class_):
502 """Unregister class instrumentation."""
504 _instrumentation_factory.unregister(class_)
507def is_instrumented(instance, key):
508 """Return True if the given attribute on the given instance is
509 instrumented by the attributes package.
511 This function may be used regardless of instrumentation
512 applied directly to the class, i.e. no descriptors are required.
514 """
515 return manager_of_class(instance.__class__).is_instrumented(
516 key, search=True
517 )
520def _generate_init(class_, class_manager):
521 """Build an __init__ decorator that triggers ClassManager events."""
523 # TODO: we should use the ClassManager's notion of the
524 # original '__init__' method, once ClassManager is fixed
525 # to always reference that.
526 original__init__ = class_.__init__
527 assert original__init__
529 # Go through some effort here and don't change the user's __init__
530 # calling signature, including the unlikely case that it has
531 # a return value.
532 # FIXME: need to juggle local names to avoid constructor argument
533 # clashes.
534 func_body = """\
535def __init__(%(apply_pos)s):
536 new_state = class_manager._new_state_if_none(%(self_arg)s)
537 if new_state:
538 return new_state._initialize_instance(%(apply_kw)s)
539 else:
540 return original__init__(%(apply_kw)s)
541"""
542 func_vars = util.format_argspec_init(original__init__, grouped=False)
543 func_text = func_body % func_vars
545 if util.py2k:
546 func = getattr(original__init__, "im_func", original__init__)
547 func_defaults = getattr(func, "func_defaults", None)
548 else:
549 func_defaults = getattr(original__init__, "__defaults__", None)
550 func_kw_defaults = getattr(original__init__, "__kwdefaults__", None)
552 env = locals().copy()
553 exec(func_text, env)
554 __init__ = env["__init__"]
555 __init__.__doc__ = original__init__.__doc__
556 __init__._sa_original_init = original__init__
558 if func_defaults:
559 __init__.__defaults__ = func_defaults
560 if not util.py2k and func_kw_defaults:
561 __init__.__kwdefaults__ = func_kw_defaults
563 return __init__