1"""Extensible class instrumentation.
2
3The :mod:`sqlalchemy.ext.instrumentation` package provides for alternate
4systems of class instrumentation within the ORM.  Class instrumentation
5refers to how the ORM places attributes on the class which maintain
6data and track changes to that data, as well as event hooks installed
7on the class.
8
9.. note::
10    The extension package is provided for the benefit of integration
11    with other object management packages, which already perform
12    their own instrumentation.  It is not intended for general use.
13
14For examples of how the instrumentation extension is used,
15see the example :ref:`examples_instrumentation`.
16
17"""
18import weakref
19
20from .. import util
21from ..orm import attributes
22from ..orm import base as orm_base
23from ..orm import collections
24from ..orm import exc as orm_exc
25from ..orm import instrumentation as orm_instrumentation
26from ..orm.instrumentation import _default_dict_getter
27from ..orm.instrumentation import _default_manager_getter
28from ..orm.instrumentation import _default_state_getter
29from ..orm.instrumentation import ClassManager
30from ..orm.instrumentation import InstrumentationFactory
31
32INSTRUMENTATION_MANAGER = "__sa_instrumentation_manager__"
33"""Attribute, elects custom instrumentation when present on a mapped class.
34
35Allows a class to specify a slightly or wildly different technique for
36tracking changes made to mapped attributes and collections.
37
38Only one instrumentation implementation is allowed in a given object
39inheritance hierarchy.
40
41The value of this attribute must be a callable and will be passed a class
42object.  The callable must return one of:
43
44  - An instance of an InstrumentationManager or subclass
45  - An object implementing all or some of InstrumentationManager (TODO)
46  - A dictionary of callables, implementing all or some of the above (TODO)
47  - An instance of a ClassManager or subclass
48
49This attribute is consulted by SQLAlchemy instrumentation
50resolution, once the :mod:`sqlalchemy.ext.instrumentation` module
51has been imported.  If custom finders are installed in the global
52instrumentation_finders list, they may or may not choose to honor this
53attribute.
54
55"""
56
57
58def find_native_user_instrumentation_hook(cls):
59    """Find user-specified instrumentation management for a class."""
60    return getattr(cls, INSTRUMENTATION_MANAGER, None)
61
62
63instrumentation_finders = [find_native_user_instrumentation_hook]
64"""An extensible sequence of callables which return instrumentation
65implementations
66
67When a class is registered, each callable will be passed a class object.
68If None is returned, the
69next finder in the sequence is consulted.  Otherwise the return must be an
70instrumentation factory that follows the same guidelines as
71sqlalchemy.ext.instrumentation.INSTRUMENTATION_MANAGER.
72
73By default, the only finder is find_native_user_instrumentation_hook, which
74searches for INSTRUMENTATION_MANAGER.  If all finders return None, standard
75ClassManager instrumentation is used.
76
77"""
78
79
80class ExtendedInstrumentationRegistry(InstrumentationFactory):
81    """Extends :class:`.InstrumentationFactory` with additional
82    bookkeeping, to accommodate multiple types of
83    class managers.
84
85    """
86
87    _manager_finders = weakref.WeakKeyDictionary()
88    _state_finders = weakref.WeakKeyDictionary()
89    _dict_finders = weakref.WeakKeyDictionary()
90    _extended = False
91
92    def _locate_extended_factory(self, class_):
93        for finder in instrumentation_finders:
94            factory = finder(class_)
95            if factory is not None:
96                manager = self._extended_class_manager(class_, factory)
97                return manager, factory
98        else:
99            return None, None
100
101    def _check_conflicts(self, class_, factory):
102        existing_factories = self._collect_management_factories_for(
103            class_
104        ).difference([factory])
105        if existing_factories:
106            raise TypeError(
107                "multiple instrumentation implementations specified "
108                "in %s inheritance hierarchy: %r"
109                % (class_.__name__, list(existing_factories))
110            )
111
112    def _extended_class_manager(self, class_, factory):
113        manager = factory(class_)
114        if not isinstance(manager, ClassManager):
115            manager = _ClassInstrumentationAdapter(class_, manager)
116
117        if factory != ClassManager and not self._extended:
118            # somebody invoked a custom ClassManager.
119            # reinstall global "getter" functions with the more
120            # expensive ones.
121            self._extended = True
122            _install_instrumented_lookups()
123
124        self._manager_finders[class_] = manager.manager_getter()
125        self._state_finders[class_] = manager.state_getter()
126        self._dict_finders[class_] = manager.dict_getter()
127        return manager
128
129    def _collect_management_factories_for(self, cls):
130        """Return a collection of factories in play or specified for a
131        hierarchy.
132
133        Traverses the entire inheritance graph of a cls and returns a
134        collection of instrumentation factories for those classes. Factories
135        are extracted from active ClassManagers, if available, otherwise
136        instrumentation_finders is consulted.
137
138        """
139        hierarchy = util.class_hierarchy(cls)
140        factories = set()
141        for member in hierarchy:
142            manager = self.manager_of_class(member)
143            if manager is not None:
144                factories.add(manager.factory)
145            else:
146                for finder in instrumentation_finders:
147                    factory = finder(member)
148                    if factory is not None:
149                        break
150                else:
151                    factory = None
152                factories.add(factory)
153        factories.discard(None)
154        return factories
155
156    def unregister(self, class_):
157        if class_ in self._manager_finders:
158            del self._manager_finders[class_]
159            del self._state_finders[class_]
160            del self._dict_finders[class_]
161        super(ExtendedInstrumentationRegistry, self).unregister(class_)
162
163    def manager_of_class(self, cls):
164        if cls is None:
165            return None
166        try:
167            finder = self._manager_finders.get(cls, _default_manager_getter)
168        except TypeError:
169            # due to weakref lookup on invalid object
170            return None
171        else:
172            return finder(cls)
173
174    def state_of(self, instance):
175        if instance is None:
176            raise AttributeError("None has no persistent state.")
177        return self._state_finders.get(
178            instance.__class__, _default_state_getter
179        )(instance)
180
181    def dict_of(self, instance):
182        if instance is None:
183            raise AttributeError("None has no persistent state.")
184        return self._dict_finders.get(
185            instance.__class__, _default_dict_getter
186        )(instance)
187
188
189orm_instrumentation._instrumentation_factory = (
190    _instrumentation_factory
191) = ExtendedInstrumentationRegistry()
192orm_instrumentation.instrumentation_finders = instrumentation_finders
193
194
195class InstrumentationManager(object):
196    """User-defined class instrumentation extension.
197
198    :class:`.InstrumentationManager` can be subclassed in order
199    to change
200    how class instrumentation proceeds. This class exists for
201    the purposes of integration with other object management
202    frameworks which would like to entirely modify the
203    instrumentation methodology of the ORM, and is not intended
204    for regular usage.  For interception of class instrumentation
205    events, see :class:`.InstrumentationEvents`.
206
207    The API for this class should be considered as semi-stable,
208    and may change slightly with new releases.
209
210    """
211
212    # r4361 added a mandatory (cls) constructor to this interface.
213    # given that, perhaps class_ should be dropped from all of these
214    # signatures.
215
216    def __init__(self, class_):
217        pass
218
219    def manage(self, class_, manager):
220        setattr(class_, "_default_class_manager", manager)
221
222    def dispose(self, class_, manager):
223        delattr(class_, "_default_class_manager")
224
225    def manager_getter(self, class_):
226        def get(cls):
227            return cls._default_class_manager
228
229        return get
230
231    def instrument_attribute(self, class_, key, inst):
232        pass
233
234    def post_configure_attribute(self, class_, key, inst):
235        pass
236
237    def install_descriptor(self, class_, key, inst):
238        setattr(class_, key, inst)
239
240    def uninstall_descriptor(self, class_, key):
241        delattr(class_, key)
242
243    def install_member(self, class_, key, implementation):
244        setattr(class_, key, implementation)
245
246    def uninstall_member(self, class_, key):
247        delattr(class_, key)
248
249    def instrument_collection_class(self, class_, key, collection_class):
250        return collections.prepare_instrumentation(collection_class)
251
252    def get_instance_dict(self, class_, instance):
253        return instance.__dict__
254
255    def initialize_instance_dict(self, class_, instance):
256        pass
257
258    def install_state(self, class_, instance, state):
259        setattr(instance, "_default_state", state)
260
261    def remove_state(self, class_, instance):
262        delattr(instance, "_default_state")
263
264    def state_getter(self, class_):
265        return lambda instance: getattr(instance, "_default_state")
266
267    def dict_getter(self, class_):
268        return lambda inst: self.get_instance_dict(class_, inst)
269
270
271class _ClassInstrumentationAdapter(ClassManager):
272    """Adapts a user-defined InstrumentationManager to a ClassManager."""
273
274    def __init__(self, class_, override):
275        self._adapted = override
276        self._get_state = self._adapted.state_getter(class_)
277        self._get_dict = self._adapted.dict_getter(class_)
278
279        ClassManager.__init__(self, class_)
280
281    def manage(self):
282        self._adapted.manage(self.class_, self)
283
284    def dispose(self):
285        self._adapted.dispose(self.class_)
286
287    def manager_getter(self):
288        return self._adapted.manager_getter(self.class_)
289
290    def instrument_attribute(self, key, inst, propagated=False):
291        ClassManager.instrument_attribute(self, key, inst, propagated)
292        if not propagated:
293            self._adapted.instrument_attribute(self.class_, key, inst)
294
295    def post_configure_attribute(self, key):
296        super(_ClassInstrumentationAdapter, self).post_configure_attribute(key)
297        self._adapted.post_configure_attribute(self.class_, key, self[key])
298
299    def install_descriptor(self, key, inst):
300        self._adapted.install_descriptor(self.class_, key, inst)
301
302    def uninstall_descriptor(self, key):
303        self._adapted.uninstall_descriptor(self.class_, key)
304
305    def install_member(self, key, implementation):
306        self._adapted.install_member(self.class_, key, implementation)
307
308    def uninstall_member(self, key):
309        self._adapted.uninstall_member(self.class_, key)
310
311    def instrument_collection_class(self, key, collection_class):
312        return self._adapted.instrument_collection_class(
313            self.class_, key, collection_class
314        )
315
316    def initialize_collection(self, key, state, factory):
317        delegate = getattr(self._adapted, "initialize_collection", None)
318        if delegate:
319            return delegate(key, state, factory)
320        else:
321            return ClassManager.initialize_collection(
322                self, key, state, factory
323            )
324
325    def new_instance(self, state=None):
326        instance = self.class_.__new__(self.class_)
327        self.setup_instance(instance, state)
328        return instance
329
330    def _new_state_if_none(self, instance):
331        """Install a default InstanceState if none is present.
332
333        A private convenience method used by the __init__ decorator.
334        """
335        if self.has_state(instance):
336            return False
337        else:
338            return self.setup_instance(instance)
339
340    def setup_instance(self, instance, state=None):
341        self._adapted.initialize_instance_dict(self.class_, instance)
342
343        if state is None:
344            state = self._state_constructor(instance, self)
345
346        # the given instance is assumed to have no state
347        self._adapted.install_state(self.class_, instance, state)
348        return state
349
350    def teardown_instance(self, instance):
351        self._adapted.remove_state(self.class_, instance)
352
353    def has_state(self, instance):
354        try:
355            self._get_state(instance)
356        except orm_exc.NO_STATE:
357            return False
358        else:
359            return True
360
361    def state_getter(self):
362        return self._get_state
363
364    def dict_getter(self):
365        return self._get_dict
366
367
368def _install_instrumented_lookups():
369    """Replace global class/object management functions
370    with ExtendedInstrumentationRegistry implementations, which
371    allow multiple types of class managers to be present,
372    at the cost of performance.
373
374    This function is called only by ExtendedInstrumentationRegistry
375    and unit tests specific to this behavior.
376
377    The _reinstall_default_lookups() function can be called
378    after this one to re-establish the default functions.
379
380    """
381    _install_lookups(
382        dict(
383            instance_state=_instrumentation_factory.state_of,
384            instance_dict=_instrumentation_factory.dict_of,
385            manager_of_class=_instrumentation_factory.manager_of_class,
386        )
387    )
388
389
390def _reinstall_default_lookups():
391    """Restore simplified lookups."""
392    _install_lookups(
393        dict(
394            instance_state=_default_state_getter,
395            instance_dict=_default_dict_getter,
396            manager_of_class=_default_manager_getter,
397        )
398    )
399    _instrumentation_factory._extended = False
400
401
402def _install_lookups(lookups):
403    global instance_state, instance_dict, manager_of_class
404    instance_state = lookups["instance_state"]
405    instance_dict = lookups["instance_dict"]
406    manager_of_class = lookups["manager_of_class"]
407    orm_base.instance_state = (
408        attributes.instance_state
409    ) = orm_instrumentation.instance_state = instance_state
410    orm_base.instance_dict = (
411        attributes.instance_dict
412    ) = orm_instrumentation.instance_dict = instance_dict
413    orm_base.manager_of_class = (
414        attributes.manager_of_class
415    ) = orm_instrumentation.manager_of_class = manager_of_class
416