14
14
and attributes.py which establish per-instance and per-class-attribute
15
15
instrumentation, respectively.
17
SQLA's instrumentation system is completely customizable, in which
18
case an understanding of the general mechanics of this module is helpful.
19
An example of full customization is in /examples/custom_attributes.
24
from sqlalchemy.orm import exc, collections, events
25
from operator import attrgetter, itemgetter
26
from sqlalchemy import event, util
28
from sqlalchemy.orm import state, attributes
31
INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__'
32
"""Attribute, elects custom instrumentation when present on a mapped class.
34
Allows a class to specify a slightly or wildly different technique for
35
tracking changes made to mapped attributes and collections.
37
Only one instrumentation implementation is allowed in a given object
38
inheritance hierarchy.
40
The value of this attribute must be a callable and will be passed a class
41
object. The callable must return one of:
43
- An instance of an interfaces.InstrumentationManager or subclass
44
- An object implementing all or some of InstrumentationManager (TODO)
45
- A dictionary of callables, implementing all or some of the above (TODO)
46
- An instance of a ClassManager or subclass
48
interfaces.InstrumentationManager is public API and will remain stable
49
between releases. ClassManager is not public and no guarantees are made
50
about stability. Caveat emptor.
52
This attribute is consulted by the default SQLAlchemy instrumentation
53
resolution code. If custom finders are installed in the global
54
instrumentation_finders list, they may or may not choose to honor this
59
instrumentation_finders = []
60
"""An extensible sequence of instrumentation implementation finding callables.
62
Finders callables will be passed a class object. If None is returned, the
63
next finder in the sequence is consulted. Otherwise the return must be an
64
instrumentation factory that follows the same guidelines as
65
INSTRUMENTATION_MANAGER.
67
By default, the only finder is find_native_user_instrumentation_hook, which
68
searches for INSTRUMENTATION_MANAGER. If all finders return None, standard
69
ClassManager instrumentation is used.
17
The class instrumentation system can be customized on a per-class
18
or global basis using the :mod:`sqlalchemy.ext.instrumentation`
19
module, which provides the means to build and specify
20
alternate instrumentation forms.
22
.. versionchanged: 0.8
23
The instrumentation extension system was moved out of the
24
ORM and into the external :mod:`sqlalchemy.ext.instrumentation`
25
package. When that package is imported, it installs
26
itself within sqlalchemy.orm so that its more comprehensive
27
resolution mechanics take effect.
32
from . import exc, collections, events, interfaces
33
from operator import attrgetter
34
from .. import event, util
35
state = util.importlater("sqlalchemy.orm", "state")
74
38
class ClassManager(dict):
338
333
return '<%s of %r at %x>' % (
339
334
self.__class__.__name__, self.class_, id(self))
341
class _ClassInstrumentationAdapter(ClassManager):
342
"""Adapts a user-defined InstrumentationManager to a ClassManager."""
344
def __init__(self, class_, override, **kw):
345
self._adapted = override
346
self._get_state = self._adapted.state_getter(class_)
347
self._get_dict = self._adapted.dict_getter(class_)
349
ClassManager.__init__(self, class_, **kw)
352
self._adapted.manage(self.class_, self)
355
self._adapted.dispose(self.class_)
357
def manager_getter(self):
358
return self._adapted.manager_getter(self.class_)
360
def instrument_attribute(self, key, inst, propagated=False):
361
ClassManager.instrument_attribute(self, key, inst, propagated)
363
self._adapted.instrument_attribute(self.class_, key, inst)
365
def post_configure_attribute(self, key):
366
super(_ClassInstrumentationAdapter, self).post_configure_attribute(key)
367
self._adapted.post_configure_attribute(self.class_, key, self[key])
369
def install_descriptor(self, key, inst):
370
self._adapted.install_descriptor(self.class_, key, inst)
372
def uninstall_descriptor(self, key):
373
self._adapted.uninstall_descriptor(self.class_, key)
375
def install_member(self, key, implementation):
376
self._adapted.install_member(self.class_, key, implementation)
378
def uninstall_member(self, key):
379
self._adapted.uninstall_member(self.class_, key)
381
def instrument_collection_class(self, key, collection_class):
382
return self._adapted.instrument_collection_class(
383
self.class_, key, collection_class)
385
def initialize_collection(self, key, state, factory):
386
delegate = getattr(self._adapted, 'initialize_collection', None)
388
return delegate(key, state, factory)
390
return ClassManager.initialize_collection(self, key,
393
def new_instance(self, state=None):
394
instance = self.class_.__new__(self.class_)
395
self.setup_instance(instance, state)
398
def _new_state_if_none(self, instance):
399
"""Install a default InstanceState if none is present.
401
A private convenience method used by the __init__ decorator.
403
if self.has_state(instance):
406
return self.setup_instance(instance)
408
def setup_instance(self, instance, state=None):
409
self._adapted.initialize_instance_dict(self.class_, instance)
412
state = self._state_constructor(instance, self)
414
# the given instance is assumed to have no state
415
self._adapted.install_state(self.class_, instance, state)
418
def teardown_instance(self, instance):
419
self._adapted.remove_state(self.class_, instance)
421
def has_state(self, instance):
423
state = self._get_state(instance)
429
def state_getter(self):
430
return self._get_state
432
def dict_getter(self):
433
return self._get_dict
435
def register_class(class_, **kw):
337
class InstrumentationFactory(object):
338
"""Factory for new ClassManager instances."""
340
dispatch = event.dispatcher(events.InstrumentationEvents)
342
def create_manager_for_cls(self, class_):
343
assert class_ is not None
344
assert manager_of_class(class_) is None
346
# give a more complicated subclass
347
# a chance to do what it wants here
348
manager, factory = self._locate_extended_factory(class_)
351
factory = ClassManager
352
manager = factory(class_)
354
self._check_conflicts(class_, factory)
356
manager.factory = factory
358
self.dispatch.class_instrument(class_)
361
def _locate_extended_factory(self, class_):
362
"""Overridden by a subclass to do an extended lookup."""
365
def _check_conflicts(self, class_, factory):
366
"""Overridden by a subclass to test for conflicting factories."""
369
def unregister(self, class_):
370
manager = manager_of_class(class_)
373
self.dispatch.class_uninstrument(class_)
374
if ClassManager.MANAGER_ATTR in class_.__dict__:
375
delattr(class_, ClassManager.MANAGER_ATTR)
377
# this attribute is replaced by sqlalchemy.ext.instrumentation
379
_instrumentation_factory = InstrumentationFactory()
382
def register_class(class_):
436
383
"""Register class instrumentation.
438
385
Returns the existing or newly created class manager.
441
389
manager = manager_of_class(class_)
442
390
if manager is None:
443
manager = _create_manager_for_cls(class_, **kw)
391
manager = _instrumentation_factory.create_manager_for_cls(class_)
446
395
def unregister_class(class_):
447
396
"""Unregister class instrumentation."""
449
instrumentation_registry.unregister(class_)
398
_instrumentation_factory.unregister(class_)
452
401
def is_instrumented(instance, key):
460
409
return manager_of_class(instance.__class__).\
461
410
is_instrumented(key, search=True)
463
class InstrumentationRegistry(object):
464
"""Private instrumentation registration singleton.
466
All classes are routed through this registry
467
when first instrumented, however the InstrumentationRegistry
468
is not actually needed unless custom ClassManagers are in use.
472
_manager_finders = weakref.WeakKeyDictionary()
473
_state_finders = util.WeakIdentityMapping()
474
_dict_finders = util.WeakIdentityMapping()
477
dispatch = event.dispatcher(events.InstrumentationEvents)
479
def create_manager_for_cls(self, class_, **kw):
480
assert class_ is not None
481
assert manager_of_class(class_) is None
483
for finder in instrumentation_finders:
484
factory = finder(class_)
485
if factory is not None:
488
factory = ClassManager
490
existing_factories = self._collect_management_factories_for(class_).\
491
difference([factory])
492
if existing_factories:
494
"multiple instrumentation implementations specified "
495
"in %s inheritance hierarchy: %r" % (
496
class_.__name__, list(existing_factories)))
498
manager = factory(class_)
499
if not isinstance(manager, ClassManager):
500
manager = _ClassInstrumentationAdapter(class_, manager)
502
if factory != ClassManager and not self._extended:
503
# somebody invoked a custom ClassManager.
504
# reinstall global "getter" functions with the more
506
self._extended = True
507
_install_lookup_strategy(self)
509
manager.factory = factory
510
self._manager_finders[class_] = manager.manager_getter()
511
self._state_finders[class_] = manager.state_getter()
512
self._dict_finders[class_] = manager.dict_getter()
514
self.dispatch.class_instrument(class_)
518
def _collect_management_factories_for(self, cls):
519
"""Return a collection of factories in play or specified for a
522
Traverses the entire inheritance graph of a cls and returns a
523
collection of instrumentation factories for those classes. Factories
524
are extracted from active ClassManagers, if available, otherwise
525
instrumentation_finders is consulted.
528
hierarchy = util.class_hierarchy(cls)
530
for member in hierarchy:
531
manager = manager_of_class(member)
532
if manager is not None:
533
factories.add(manager.factory)
535
for finder in instrumentation_finders:
536
factory = finder(member)
537
if factory is not None:
541
factories.add(factory)
542
factories.discard(None)
545
def manager_of_class(self, cls):
546
# this is only called when alternate instrumentation
547
# has been established
551
finder = self._manager_finders[cls]
557
def state_of(self, instance):
558
# this is only called when alternate instrumentation
559
# has been established
561
raise AttributeError("None has no persistent state.")
563
return self._state_finders[instance.__class__](instance)
565
raise AttributeError("%r is not instrumented" %
568
def dict_of(self, instance):
569
# this is only called when alternate instrumentation
570
# has been established
572
raise AttributeError("None has no persistent state.")
574
return self._dict_finders[instance.__class__](instance)
576
raise AttributeError("%r is not instrumented" %
579
def unregister(self, class_):
580
if class_ in self._manager_finders:
581
manager = self.manager_of_class(class_)
582
self.dispatch.class_uninstrument(class_)
585
del self._manager_finders[class_]
586
del self._state_finders[class_]
587
del self._dict_finders[class_]
588
if ClassManager.MANAGER_ATTR in class_.__dict__:
589
delattr(class_, ClassManager.MANAGER_ATTR)
591
instrumentation_registry = InstrumentationRegistry()
594
def _install_lookup_strategy(implementation):
595
"""Replace global class/object management functions
596
with either faster or more comprehensive implementations,
597
based on whether or not extended class instrumentation
600
This function is called only by InstrumentationRegistry()
601
and unit tests specific to this behavior.
604
global instance_state, instance_dict, manager_of_class
605
if implementation is util.symbol('native'):
606
instance_state = attrgetter(ClassManager.STATE_ATTR)
607
instance_dict = attrgetter("__dict__")
608
def manager_of_class(cls):
609
return cls.__dict__.get(ClassManager.MANAGER_ATTR, None)
611
instance_state = instrumentation_registry.state_of
612
instance_dict = instrumentation_registry.dict_of
613
manager_of_class = instrumentation_registry.manager_of_class
614
attributes.instance_state = instance_state
615
attributes.instance_dict = instance_dict
616
attributes.manager_of_class = manager_of_class
618
_create_manager_for_cls = instrumentation_registry.create_manager_for_cls
620
# Install default "lookup" strategies. These are basically
621
# very fast attrgetters for key attributes.
622
# When a custom ClassManager is installed, more expensive per-class
623
# strategies are copied over these.
624
_install_lookup_strategy(util.symbol('native'))
627
def find_native_user_instrumentation_hook(cls):
628
"""Find user-specified instrumentation management for a class."""
629
return getattr(cls, INSTRUMENTATION_MANAGER, None)
630
instrumentation_finders.append(find_native_user_instrumentation_hook)
412
# these attributes are replaced by sqlalchemy.ext.instrumentation
413
# when a non-standard InstrumentationManager class is first
414
# used to instrument a class.
415
instance_state = _default_state_getter = ClassManager.state_getter()
417
instance_dict = _default_dict_getter = ClassManager.dict_getter()
419
manager_of_class = _default_manager_getter = ClassManager.manager_getter()
632
422
def _generate_init(class_, class_manager):
633
423
"""Build an __init__ decorator that triggers ClassManager events."""