Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/instrumentation.py: 60%
304 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/instrumentation.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
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"""
33import weakref
35from . import base
36from . import collections
37from . import exc
38from . import interfaces
39from . import state
40from .. import util
41from ..util import HasMemoized
44DEL_ATTR = util.symbol("DEL_ATTR")
47class ClassManager(HasMemoized, dict):
48 """Tracks state information at the class level."""
50 MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR
51 STATE_ATTR = base.DEFAULT_STATE_ATTR
53 _state_setter = staticmethod(util.attrsetter(STATE_ATTR))
55 expired_attribute_loader = None
56 "previously known as deferred_scalar_loader"
58 init_method = None
60 factory = None
61 mapper = None
62 declarative_scan = None
63 registry = None
65 @property
66 @util.deprecated(
67 "1.4",
68 message="The ClassManager.deferred_scalar_loader attribute is now "
69 "named expired_attribute_loader",
70 )
71 def deferred_scalar_loader(self):
72 return self.expired_attribute_loader
74 @deferred_scalar_loader.setter
75 @util.deprecated(
76 "1.4",
77 message="The ClassManager.deferred_scalar_loader attribute is now "
78 "named expired_attribute_loader",
79 )
80 def deferred_scalar_loader(self, obj):
81 self.expired_attribute_loader = obj
83 def __init__(self, class_):
84 self.class_ = class_
85 self.info = {}
86 self.new_init = None
87 self.local_attrs = {}
88 self.originals = {}
89 self._finalized = False
91 self._bases = [
92 mgr
93 for mgr in [
94 manager_of_class(base)
95 for base in self.class_.__bases__
96 if isinstance(base, type)
97 ]
98 if mgr is not None
99 ]
101 for base_ in self._bases:
102 self.update(base_)
104 self.dispatch._events._new_classmanager_instance(class_, self)
106 for basecls in class_.__mro__:
107 mgr = manager_of_class(basecls)
108 if mgr is not None:
109 self.dispatch._update(mgr.dispatch)
111 self.manage()
113 if "__del__" in class_.__dict__:
114 util.warn(
115 "__del__() method on class %s will "
116 "cause unreachable cycles and memory leaks, "
117 "as SQLAlchemy instrumentation often creates "
118 "reference cycles. Please remove this method." % class_
119 )
121 def _update_state(
122 self,
123 finalize=False,
124 mapper=None,
125 registry=None,
126 declarative_scan=None,
127 expired_attribute_loader=None,
128 init_method=None,
129 ):
131 if mapper:
132 self.mapper = mapper
133 if registry:
134 registry._add_manager(self)
135 if declarative_scan:
136 self.declarative_scan = weakref.ref(declarative_scan)
137 if expired_attribute_loader:
138 self.expired_attribute_loader = expired_attribute_loader
140 if init_method:
141 assert not self._finalized, (
142 "class is already instrumented, "
143 "init_method %s can't be applied" % init_method
144 )
145 self.init_method = init_method
147 if not self._finalized:
148 self.original_init = (
149 self.init_method
150 if self.init_method is not None
151 and self.class_.__init__ is object.__init__
152 else self.class_.__init__
153 )
155 if finalize and not self._finalized:
156 self._finalize()
158 def _finalize(self):
159 if self._finalized:
160 return
161 self._finalized = True
163 self._instrument_init()
165 _instrumentation_factory.dispatch.class_instrument(self.class_)
167 def __hash__(self):
168 return id(self)
170 def __eq__(self, other):
171 return other is self
173 @property
174 def is_mapped(self):
175 return "mapper" in self.__dict__
177 @HasMemoized.memoized_attribute
178 def _all_key_set(self):
179 return frozenset(self)
181 @HasMemoized.memoized_attribute
182 def _collection_impl_keys(self):
183 return frozenset(
184 [attr.key for attr in self.values() if attr.impl.collection]
185 )
187 @HasMemoized.memoized_attribute
188 def _scalar_loader_impls(self):
189 return frozenset(
190 [
191 attr.impl
192 for attr in self.values()
193 if attr.impl.accepts_scalar_loader
194 ]
195 )
197 @HasMemoized.memoized_attribute
198 def _loader_impls(self):
199 return frozenset([attr.impl for attr in self.values()])
201 @util.memoized_property
202 def mapper(self):
203 # raises unless self.mapper has been assigned
204 raise exc.UnmappedClassError(self.class_)
206 def _all_sqla_attributes(self, exclude=None):
207 """return an iterator of all classbound attributes that are
208 implement :class:`.InspectionAttr`.
210 This includes :class:`.QueryableAttribute` as well as extension
211 types such as :class:`.hybrid_property` and
212 :class:`.AssociationProxy`.
214 """
216 found = {}
218 # constraints:
219 # 1. yield keys in cls.__dict__ order
220 # 2. if a subclass has the same key as a superclass, include that
221 # key as part of the ordering of the superclass, because an
222 # overridden key is usually installed by the mapper which is going
223 # on a different ordering
224 # 3. don't use getattr() as this fires off descriptors
226 for supercls in self.class_.__mro__[0:-1]:
227 inherits = supercls.__mro__[1]
228 for key in supercls.__dict__:
229 found.setdefault(key, supercls)
230 if key in inherits.__dict__:
231 continue
232 val = found[key].__dict__[key]
233 if (
234 isinstance(val, interfaces.InspectionAttr)
235 and val.is_attribute
236 ):
237 yield key, val
239 def _get_class_attr_mro(self, key, default=None):
240 """return an attribute on the class without tripping it."""
242 for supercls in self.class_.__mro__:
243 if key in supercls.__dict__:
244 return supercls.__dict__[key]
245 else:
246 return default
248 def _attr_has_impl(self, key):
249 """Return True if the given attribute is fully initialized.
251 i.e. has an impl.
252 """
254 return key in self and self[key].impl is not None
256 def _subclass_manager(self, cls):
257 """Create a new ClassManager for a subclass of this ClassManager's
258 class.
260 This is called automatically when attributes are instrumented so that
261 the attributes can be propagated to subclasses against their own
262 class-local manager, without the need for mappers etc. to have already
263 pre-configured managers for the full class hierarchy. Mappers
264 can post-configure the auto-generated ClassManager when needed.
266 """
267 return register_class(cls, finalize=False)
269 def _instrument_init(self):
270 self.new_init = _generate_init(self.class_, self, self.original_init)
271 self.install_member("__init__", self.new_init)
273 @util.memoized_property
274 def _state_constructor(self):
275 self.dispatch.first_init(self, self.class_)
276 return state.InstanceState
278 def manage(self):
279 """Mark this instance as the manager for its class."""
281 setattr(self.class_, self.MANAGER_ATTR, self)
283 @util.hybridmethod
284 def manager_getter(self):
285 return _default_manager_getter
287 @util.hybridmethod
288 def state_getter(self):
289 """Return a (instance) -> InstanceState callable.
291 "state getter" callables should raise either KeyError or
292 AttributeError if no InstanceState could be found for the
293 instance.
294 """
296 return _default_state_getter
298 @util.hybridmethod
299 def dict_getter(self):
300 return _default_dict_getter
302 def instrument_attribute(self, key, inst, propagated=False):
303 if propagated:
304 if key in self.local_attrs:
305 return # don't override local attr with inherited attr
306 else:
307 self.local_attrs[key] = inst
308 self.install_descriptor(key, inst)
309 self._reset_memoizations()
310 self[key] = inst
312 for cls in self.class_.__subclasses__():
313 manager = self._subclass_manager(cls)
314 manager.instrument_attribute(key, inst, True)
316 def subclass_managers(self, recursive):
317 for cls in self.class_.__subclasses__():
318 mgr = manager_of_class(cls)
319 if mgr is not None and mgr is not self:
320 yield mgr
321 if recursive:
322 for m in mgr.subclass_managers(True):
323 yield m
325 def post_configure_attribute(self, key):
326 _instrumentation_factory.dispatch.attribute_instrument(
327 self.class_, key, self[key]
328 )
330 def uninstrument_attribute(self, key, propagated=False):
331 if key not in self:
332 return
333 if propagated:
334 if key in self.local_attrs:
335 return # don't get rid of local attr
336 else:
337 del self.local_attrs[key]
338 self.uninstall_descriptor(key)
339 self._reset_memoizations()
340 del self[key]
341 for cls in self.class_.__subclasses__():
342 manager = manager_of_class(cls)
343 if manager:
344 manager.uninstrument_attribute(key, True)
346 def unregister(self):
347 """remove all instrumentation established by this ClassManager."""
349 for key in list(self.originals):
350 self.uninstall_member(key)
352 self.mapper = self.dispatch = self.new_init = None
353 self.info.clear()
355 for key in list(self):
356 if key in self.local_attrs:
357 self.uninstrument_attribute(key)
359 if self.MANAGER_ATTR in self.class_.__dict__:
360 delattr(self.class_, self.MANAGER_ATTR)
362 def install_descriptor(self, key, inst):
363 if key in (self.STATE_ATTR, self.MANAGER_ATTR):
364 raise KeyError(
365 "%r: requested attribute name conflicts with "
366 "instrumentation attribute of the same name." % key
367 )
368 setattr(self.class_, key, inst)
370 def uninstall_descriptor(self, key):
371 delattr(self.class_, key)
373 def install_member(self, key, implementation):
374 if key in (self.STATE_ATTR, self.MANAGER_ATTR):
375 raise KeyError(
376 "%r: requested attribute name conflicts with "
377 "instrumentation attribute of the same name." % key
378 )
379 self.originals.setdefault(key, self.class_.__dict__.get(key, DEL_ATTR))
380 setattr(self.class_, key, implementation)
382 def uninstall_member(self, key):
383 original = self.originals.pop(key, None)
384 if original is not DEL_ATTR:
385 setattr(self.class_, key, original)
386 else:
387 delattr(self.class_, key)
389 def instrument_collection_class(self, key, collection_class):
390 return collections.prepare_instrumentation(collection_class)
392 def initialize_collection(self, key, state, factory):
393 user_data = factory()
394 adapter = collections.CollectionAdapter(
395 self.get_impl(key), state, user_data
396 )
397 return adapter, user_data
399 def is_instrumented(self, key, search=False):
400 if search:
401 return key in self
402 else:
403 return key in self.local_attrs
405 def get_impl(self, key):
406 return self[key].impl
408 @property
409 def attributes(self):
410 return iter(self.values())
412 # InstanceState management
414 def new_instance(self, state=None):
415 instance = self.class_.__new__(self.class_)
416 if state is None:
417 state = self._state_constructor(instance, self)
418 self._state_setter(instance, state)
419 return instance
421 def setup_instance(self, instance, state=None):
422 if state is None:
423 state = self._state_constructor(instance, self)
424 self._state_setter(instance, state)
426 def teardown_instance(self, instance):
427 delattr(instance, self.STATE_ATTR)
429 def _serialize(self, state, state_dict):
430 return _SerializeManager(state, state_dict)
432 def _new_state_if_none(self, instance):
433 """Install a default InstanceState if none is present.
435 A private convenience method used by the __init__ decorator.
437 """
438 if hasattr(instance, self.STATE_ATTR):
439 return False
440 elif self.class_ is not instance.__class__ and self.is_mapped:
441 # this will create a new ClassManager for the
442 # subclass, without a mapper. This is likely a
443 # user error situation but allow the object
444 # to be constructed, so that it is usable
445 # in a non-ORM context at least.
446 return self._subclass_manager(
447 instance.__class__
448 )._new_state_if_none(instance)
449 else:
450 state = self._state_constructor(instance, self)
451 self._state_setter(instance, state)
452 return state
454 def has_state(self, instance):
455 return hasattr(instance, self.STATE_ATTR)
457 def has_parent(self, state, key, optimistic=False):
458 """TODO"""
459 return self.get_impl(key).hasparent(state, optimistic=optimistic)
461 def __bool__(self):
462 """All ClassManagers are non-zero regardless of attribute state."""
463 return True
465 __nonzero__ = __bool__
467 def __repr__(self):
468 return "<%s of %r at %x>" % (
469 self.__class__.__name__,
470 self.class_,
471 id(self),
472 )
475class _SerializeManager(object):
476 """Provide serialization of a :class:`.ClassManager`.
478 The :class:`.InstanceState` uses ``__init__()`` on serialize
479 and ``__call__()`` on deserialize.
481 """
483 def __init__(self, state, d):
484 self.class_ = state.class_
485 manager = state.manager
486 manager.dispatch.pickle(state, d)
488 def __call__(self, state, inst, state_dict):
489 state.manager = manager = manager_of_class(self.class_)
490 if manager is None:
491 raise exc.UnmappedInstanceError(
492 inst,
493 "Cannot deserialize object of type %r - "
494 "no mapper() has "
495 "been configured for this class within the current "
496 "Python process!" % self.class_,
497 )
498 elif manager.is_mapped and not manager.mapper.configured:
499 manager.mapper._check_configure()
501 # setup _sa_instance_state ahead of time so that
502 # unpickle events can access the object normally.
503 # see [ticket:2362]
504 if inst is not None:
505 manager.setup_instance(inst, state)
506 manager.dispatch.unpickle(state, state_dict)
509class InstrumentationFactory(object):
510 """Factory for new ClassManager instances."""
512 def create_manager_for_cls(self, class_):
513 assert class_ is not None
514 assert manager_of_class(class_) is None
516 # give a more complicated subclass
517 # a chance to do what it wants here
518 manager, factory = self._locate_extended_factory(class_)
520 if factory is None:
521 factory = ClassManager
522 manager = factory(class_)
524 self._check_conflicts(class_, factory)
526 manager.factory = factory
528 return manager
530 def _locate_extended_factory(self, class_):
531 """Overridden by a subclass to do an extended lookup."""
532 return None, None
534 def _check_conflicts(self, class_, factory):
535 """Overridden by a subclass to test for conflicting factories."""
536 return
538 def unregister(self, class_):
539 manager = manager_of_class(class_)
540 manager.unregister()
541 self.dispatch.class_uninstrument(class_)
544# this attribute is replaced by sqlalchemy.ext.instrumentation
545# when imported.
546_instrumentation_factory = InstrumentationFactory()
548# these attributes are replaced by sqlalchemy.ext.instrumentation
549# when a non-standard InstrumentationManager class is first
550# used to instrument a class.
551instance_state = _default_state_getter = base.instance_state
553instance_dict = _default_dict_getter = base.instance_dict
555manager_of_class = _default_manager_getter = base.manager_of_class
558def register_class(
559 class_,
560 finalize=True,
561 mapper=None,
562 registry=None,
563 declarative_scan=None,
564 expired_attribute_loader=None,
565 init_method=None,
566):
567 """Register class instrumentation.
569 Returns the existing or newly created class manager.
571 """
573 manager = manager_of_class(class_)
574 if manager is None:
575 manager = _instrumentation_factory.create_manager_for_cls(class_)
576 manager._update_state(
577 mapper=mapper,
578 registry=registry,
579 declarative_scan=declarative_scan,
580 expired_attribute_loader=expired_attribute_loader,
581 init_method=init_method,
582 finalize=finalize,
583 )
585 return manager
588def unregister_class(class_):
589 """Unregister class instrumentation."""
591 _instrumentation_factory.unregister(class_)
594def is_instrumented(instance, key):
595 """Return True if the given attribute on the given instance is
596 instrumented by the attributes package.
598 This function may be used regardless of instrumentation
599 applied directly to the class, i.e. no descriptors are required.
601 """
602 return manager_of_class(instance.__class__).is_instrumented(
603 key, search=True
604 )
607def _generate_init(class_, class_manager, original_init):
608 """Build an __init__ decorator that triggers ClassManager events."""
610 # TODO: we should use the ClassManager's notion of the
611 # original '__init__' method, once ClassManager is fixed
612 # to always reference that.
614 if original_init is None:
615 original_init = class_.__init__
617 # Go through some effort here and don't change the user's __init__
618 # calling signature, including the unlikely case that it has
619 # a return value.
620 # FIXME: need to juggle local names to avoid constructor argument
621 # clashes.
622 func_body = """\
623def __init__(%(apply_pos)s):
624 new_state = class_manager._new_state_if_none(%(self_arg)s)
625 if new_state:
626 return new_state._initialize_instance(%(apply_kw)s)
627 else:
628 return original_init(%(apply_kw)s)
629"""
630 func_vars = util.format_argspec_init(original_init, grouped=False)
631 func_text = func_body % func_vars
633 if util.py2k:
634 func = getattr(original_init, "im_func", original_init)
635 func_defaults = getattr(func, "func_defaults", None)
636 else:
637 func_defaults = getattr(original_init, "__defaults__", None)
638 func_kw_defaults = getattr(original_init, "__kwdefaults__", None)
640 env = locals().copy()
641 env["__name__"] = __name__
642 exec(func_text, env)
643 __init__ = env["__init__"]
644 __init__.__doc__ = original_init.__doc__
645 __init__._sa_original_init = original_init
647 if func_defaults:
648 __init__.__defaults__ = func_defaults
649 if not util.py2k and func_kw_defaults:
650 __init__.__kwdefaults__ = func_kw_defaults
652 return __init__