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