summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/instrumentation.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2010-09-14 20:43:48 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2010-09-14 20:43:48 -0400
commit3d389b19b70b65cb76226c3f3aa4c5d926e1f12b (patch)
treebdc597ebf2786f0514c8029dd2dbe2385bb9c2df /lib/sqlalchemy/orm/instrumentation.py
parent60b82d6e13246a3d88e7288e863a5231b7572c6a (diff)
downloadsqlalchemy-3d389b19b70b65cb76226c3f3aa4c5d926e1f12b.tar.gz
- reorganization
- attrbutes.py splits into attribtes.py and instrumentation.py - all the various Event subclasses go into events.py modules - some ideas for orm events - move *Extension out to deprecated_interfaces
Diffstat (limited to 'lib/sqlalchemy/orm/instrumentation.py')
-rw-r--r--lib/sqlalchemy/orm/instrumentation.py661
1 files changed, 661 insertions, 0 deletions
diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py
new file mode 100644
index 000000000..3f134c58a
--- /dev/null
+++ b/lib/sqlalchemy/orm/instrumentation.py
@@ -0,0 +1,661 @@
+"""Defines SQLAlchemy's system of class instrumentation.
+
+This module is usually not directly visible to user applications, but
+defines a large part of the ORM's interactivity.
+
+instrumentation.py deals with registration of end-user classes
+for state tracking. It interacts closely with state.py
+and attributes.py which establish per-instance and per-class-attribute
+instrumentation, respectively.
+
+SQLA's instrumentation system is completely customizable, in which
+case an understanding of the general mechanics of this module is helpful.
+An example of full customization is in /examples/custom_attributes.
+
+"""
+
+
+from sqlalchemy.orm import exc, collections, events
+from operator import attrgetter, itemgetter
+from sqlalchemy import event, util
+import weakref
+from sqlalchemy.orm import state, attributes
+
+
+INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__'
+"""Attribute, elects custom instrumentation when present on a mapped class.
+
+Allows a class to specify a slightly or wildly different technique for
+tracking changes made to mapped attributes and collections.
+
+Only one instrumentation implementation is allowed in a given object
+inheritance hierarchy.
+
+The value of this attribute must be a callable and will be passed a class
+object. The callable must return one of:
+
+ - An instance of an interfaces.InstrumentationManager or subclass
+ - An object implementing all or some of InstrumentationManager (TODO)
+ - A dictionary of callables, implementing all or some of the above (TODO)
+ - An instance of a ClassManager or subclass
+
+interfaces.InstrumentationManager is public API and will remain stable
+between releases. ClassManager is not public and no guarantees are made
+about stability. Caveat emptor.
+
+This attribute is consulted by the default SQLAlchemy instrumentation
+resolution code. If custom finders are installed in the global
+instrumentation_finders list, they may or may not choose to honor this
+attribute.
+
+"""
+
+instrumentation_finders = []
+"""An extensible sequence of instrumentation implementation finding callables.
+
+Finders callables will be passed a class object. If None is returned, the
+next finder in the sequence is consulted. Otherwise the return must be an
+instrumentation factory that follows the same guidelines as
+INSTRUMENTATION_MANAGER.
+
+By default, the only finder is find_native_user_instrumentation_hook, which
+searches for INSTRUMENTATION_MANAGER. If all finders return None, standard
+ClassManager instrumentation is used.
+
+"""
+
+
+class ClassManager(dict):
+ """tracks state information at the class level."""
+
+ MANAGER_ATTR = '_sa_class_manager'
+ STATE_ATTR = '_sa_instance_state'
+
+ deferred_scalar_loader = None
+
+ original_init = object.__init__
+
+ def __init__(self, class_):
+ self.class_ = class_
+ self.factory = None # where we came from, for inheritance bookkeeping
+ self.info = {}
+ self.new_init = None
+ self.mutable_attributes = set()
+ self.local_attrs = {}
+ self.originals = {}
+ for base in class_.__mro__[-2:0:-1]: # reverse, skipping 1st and last
+ if not isinstance(base, type):
+ continue
+ cls_state = manager_of_class(base)
+ if cls_state:
+ self.update(cls_state)
+ self.manage()
+ self._instrument_init()
+
+ dispatch = event.dispatcher(events.ClassEvents)
+
+ @property
+ def is_mapped(self):
+ return 'mapper' in self.__dict__
+
+ @util.memoized_property
+ def mapper(self):
+ raise exc.UnmappedClassError(self.class_)
+
+ def _attr_has_impl(self, key):
+ """Return True if the given attribute is fully initialized.
+
+ i.e. has an impl.
+ """
+
+ return key in self and self[key].impl is not None
+
+ def _configure_create_arguments(self,
+ _source=None,
+ deferred_scalar_loader=None):
+ """Accept extra **kw arguments passed to create_manager_for_cls.
+
+ The current contract of ClassManager and other managers is that they
+ take a single "cls" argument in their constructor (as per
+ test/orm/instrumentation.py InstrumentationCollisionTest). This
+ is to provide consistency with the current API of "class manager"
+ callables and such which may return various ClassManager and
+ ClassManager-like instances. So create_manager_for_cls sends
+ in ClassManager-specific arguments via this method once the
+ non-proxied ClassManager is available.
+
+ """
+ if _source:
+ deferred_scalar_loader = _source.deferred_scalar_loader
+
+ if deferred_scalar_loader:
+ self.deferred_scalar_loader = deferred_scalar_loader
+
+ def _subclass_manager(self, cls):
+ """Create a new ClassManager for a subclass of this ClassManager's
+ class.
+
+ This is called automatically when attributes are instrumented so that
+ the attributes can be propagated to subclasses against their own
+ class-local manager, without the need for mappers etc. to have already
+ pre-configured managers for the full class hierarchy. Mappers
+ can post-configure the auto-generated ClassManager when needed.
+
+ """
+ manager = manager_of_class(cls)
+ if manager is None:
+ manager = _create_manager_for_cls(cls, _source=self)
+ return manager
+
+ def _instrument_init(self):
+ # TODO: self.class_.__init__ is often the already-instrumented
+ # __init__ from an instrumented superclass. We still need to make
+ # our own wrapper, but it would
+ # be nice to wrap the original __init__ and not our existing wrapper
+ # of such, since this adds method overhead.
+ self.original_init = self.class_.__init__
+ self.new_init = _generate_init(self.class_, self)
+ self.install_member('__init__', self.new_init)
+
+ def _uninstrument_init(self):
+ if self.new_init:
+ self.uninstall_member('__init__')
+ self.new_init = None
+
+ def _create_instance_state(self, instance):
+ if self.mutable_attributes:
+ return state.MutableAttrInstanceState(instance, self)
+ else:
+ return state.InstanceState(instance, self)
+
+ def manage(self):
+ """Mark this instance as the manager for its class."""
+
+ setattr(self.class_, self.MANAGER_ATTR, self)
+
+ def dispose(self):
+ """Dissasociate this manager from its class."""
+
+ delattr(self.class_, self.MANAGER_ATTR)
+
+ def manager_getter(self):
+ return attrgetter(self.MANAGER_ATTR)
+
+ def instrument_attribute(self, key, inst, propagated=False):
+ if propagated:
+ if key in self.local_attrs:
+ return # don't override local attr with inherited attr
+ else:
+ self.local_attrs[key] = inst
+ self.install_descriptor(key, inst)
+ self[key] = inst
+
+ for cls in self.class_.__subclasses__():
+ manager = self._subclass_manager(cls)
+ manager.instrument_attribute(key, inst, True)
+
+ def post_configure_attribute(self, key):
+ pass
+
+ def uninstrument_attribute(self, key, propagated=False):
+ if key not in self:
+ return
+ if propagated:
+ if key in self.local_attrs:
+ return # don't get rid of local attr
+ else:
+ del self.local_attrs[key]
+ self.uninstall_descriptor(key)
+ del self[key]
+ if key in self.mutable_attributes:
+ self.mutable_attributes.remove(key)
+ for cls in self.class_.__subclasses__():
+ manager = self._subclass_manager(cls)
+ manager.uninstrument_attribute(key, True)
+
+ def unregister(self):
+ """remove all instrumentation established by this ClassManager."""
+
+ self._uninstrument_init()
+
+ self.mapper = self.dispatch = None
+ self.info.clear()
+
+ for key in list(self):
+ if key in self.local_attrs:
+ self.uninstrument_attribute(key)
+
+ def install_descriptor(self, key, inst):
+ if key in (self.STATE_ATTR, self.MANAGER_ATTR):
+ raise KeyError("%r: requested attribute name conflicts with "
+ "instrumentation attribute of the same name." %
+ key)
+ setattr(self.class_, key, inst)
+
+ def uninstall_descriptor(self, key):
+ delattr(self.class_, key)
+
+ def install_member(self, key, implementation):
+ if key in (self.STATE_ATTR, self.MANAGER_ATTR):
+ raise KeyError("%r: requested attribute name conflicts with "
+ "instrumentation attribute of the same name." %
+ key)
+ self.originals.setdefault(key, getattr(self.class_, key, None))
+ setattr(self.class_, key, implementation)
+
+ def uninstall_member(self, key):
+ original = self.originals.pop(key, None)
+ if original is not None:
+ setattr(self.class_, key, original)
+
+ def instrument_collection_class(self, key, collection_class):
+ return collections.prepare_instrumentation(collection_class)
+
+ def initialize_collection(self, key, state, factory):
+ user_data = factory()
+ adapter = collections.CollectionAdapter(
+ self.get_impl(key), state, user_data)
+ return adapter, user_data
+
+ def is_instrumented(self, key, search=False):
+ if search:
+ return key in self
+ else:
+ return key in self.local_attrs
+
+ def get_impl(self, key):
+ return self[key].impl
+
+ @property
+ def attributes(self):
+ return self.itervalues()
+
+ ## InstanceState management
+
+ def new_instance(self, state=None):
+ instance = self.class_.__new__(self.class_)
+ setattr(instance, self.STATE_ATTR,
+ state or self._create_instance_state(instance))
+ return instance
+
+ def setup_instance(self, instance, state=None):
+ setattr(instance, self.STATE_ATTR,
+ state or self._create_instance_state(instance))
+
+ def teardown_instance(self, instance):
+ delattr(instance, self.STATE_ATTR)
+
+ def _new_state_if_none(self, instance):
+ """Install a default InstanceState if none is present.
+
+ A private convenience method used by the __init__ decorator.
+
+ """
+ if hasattr(instance, self.STATE_ATTR):
+ return False
+ elif self.class_ is not instance.__class__ and \
+ self.is_mapped:
+ # this will create a new ClassManager for the
+ # subclass, without a mapper. This is likely a
+ # user error situation but allow the object
+ # to be constructed, so that it is usable
+ # in a non-ORM context at least.
+ return self._subclass_manager(instance.__class__).\
+ _new_state_if_none(instance)
+ else:
+ state = self._create_instance_state(instance)
+ setattr(instance, self.STATE_ATTR, state)
+ return state
+
+ def state_getter(self):
+ """Return a (instance) -> InstanceState callable.
+
+ "state getter" callables should raise either KeyError or
+ AttributeError if no InstanceState could be found for the
+ instance.
+ """
+
+ return attrgetter(self.STATE_ATTR)
+
+ def dict_getter(self):
+ return attrgetter('__dict__')
+
+ def has_state(self, instance):
+ return hasattr(instance, self.STATE_ATTR)
+
+ def has_parent(self, state, key, optimistic=False):
+ """TODO"""
+ return self.get_impl(key).hasparent(state, optimistic=optimistic)
+
+ def __nonzero__(self):
+ """All ClassManagers are non-zero regardless of attribute state."""
+ return True
+
+ def __repr__(self):
+ return '<%s of %r at %x>' % (
+ self.__class__.__name__, self.class_, id(self))
+
+class _ClassInstrumentationAdapter(ClassManager):
+ """Adapts a user-defined InstrumentationManager to a ClassManager."""
+
+ def __init__(self, class_, override, **kw):
+ self._adapted = override
+ self._get_state = self._adapted.state_getter(class_)
+ self._get_dict = self._adapted.dict_getter(class_)
+
+ ClassManager.__init__(self, class_, **kw)
+
+ def manage(self):
+ self._adapted.manage(self.class_, self)
+
+ def dispose(self):
+ self._adapted.dispose(self.class_)
+
+ def manager_getter(self):
+ return self._adapted.manager_getter(self.class_)
+
+ def instrument_attribute(self, key, inst, propagated=False):
+ ClassManager.instrument_attribute(self, key, inst, propagated)
+ if not propagated:
+ self._adapted.instrument_attribute(self.class_, key, inst)
+
+ def post_configure_attribute(self, key):
+ self._adapted.post_configure_attribute(self.class_, key, self[key])
+
+ def install_descriptor(self, key, inst):
+ self._adapted.install_descriptor(self.class_, key, inst)
+
+ def uninstall_descriptor(self, key):
+ self._adapted.uninstall_descriptor(self.class_, key)
+
+ def install_member(self, key, implementation):
+ self._adapted.install_member(self.class_, key, implementation)
+
+ def uninstall_member(self, key):
+ self._adapted.uninstall_member(self.class_, key)
+
+ def instrument_collection_class(self, key, collection_class):
+ return self._adapted.instrument_collection_class(
+ self.class_, key, collection_class)
+
+ def initialize_collection(self, key, state, factory):
+ delegate = getattr(self._adapted, 'initialize_collection', None)
+ if delegate:
+ return delegate(key, state, factory)
+ else:
+ return ClassManager.initialize_collection(self, key,
+ state, factory)
+
+ def new_instance(self, state=None):
+ instance = self.class_.__new__(self.class_)
+ self.setup_instance(instance, state)
+ return instance
+
+ def _new_state_if_none(self, instance):
+ """Install a default InstanceState if none is present.
+
+ A private convenience method used by the __init__ decorator.
+ """
+ if self.has_state(instance):
+ return False
+ else:
+ return self.setup_instance(instance)
+
+ def setup_instance(self, instance, state=None):
+ self._adapted.initialize_instance_dict(self.class_, instance)
+
+ if state is None:
+ state = self._create_instance_state(instance)
+
+ # the given instance is assumed to have no state
+ self._adapted.install_state(self.class_, instance, state)
+ return state
+
+ def teardown_instance(self, instance):
+ self._adapted.remove_state(self.class_, instance)
+
+ def has_state(self, instance):
+ try:
+ state = self._get_state(instance)
+ except exc.NO_STATE:
+ return False
+ else:
+ return True
+
+ def state_getter(self):
+ return self._get_state
+
+ def dict_getter(self):
+ return self._get_dict
+
+def register_class(class_, **kw):
+ """Register class instrumentation.
+
+ Returns the existing or newly created class manager.
+ """
+
+ manager = manager_of_class(class_)
+ if manager is None:
+ manager = _create_manager_for_cls(class_, **kw)
+ return manager
+
+def unregister_class(class_):
+ """Unregister class instrumentation."""
+
+ instrumentation_registry.unregister(class_)
+
+
+def is_instrumented(instance, key):
+ """Return True if the given attribute on the given instance is
+ instrumented by the attributes package.
+
+ This function may be used regardless of instrumentation
+ applied directly to the class, i.e. no descriptors are required.
+
+ """
+ return manager_of_class(instance.__class__).\
+ is_instrumented(key, search=True)
+
+class InstrumentationRegistry(object):
+ """Private instrumentation registration singleton.
+
+ All classes are routed through this registry
+ when first instrumented, however the InstrumentationRegistry
+ is not actually needed unless custom ClassManagers are in use.
+
+ """
+
+ _manager_finders = weakref.WeakKeyDictionary()
+ _state_finders = util.WeakIdentityMapping()
+ _dict_finders = util.WeakIdentityMapping()
+ _extended = False
+
+ def create_manager_for_cls(self, class_, **kw):
+ assert class_ is not None
+ assert manager_of_class(class_) is None
+
+ for finder in instrumentation_finders:
+ factory = finder(class_)
+ if factory is not None:
+ break
+ else:
+ factory = ClassManager
+
+ existing_factories = self._collect_management_factories_for(class_).\
+ difference([factory])
+ if existing_factories:
+ raise TypeError(
+ "multiple instrumentation implementations specified "
+ "in %s inheritance hierarchy: %r" % (
+ class_.__name__, list(existing_factories)))
+
+ manager = factory(class_)
+ if not isinstance(manager, ClassManager):
+ manager = _ClassInstrumentationAdapter(class_, manager)
+
+ if factory != ClassManager and not self._extended:
+ # somebody invoked a custom ClassManager.
+ # reinstall global "getter" functions with the more
+ # expensive ones.
+ self._extended = True
+ _install_lookup_strategy(self)
+
+ manager._configure_create_arguments(**kw)
+
+ manager.factory = factory
+ self._manager_finders[class_] = manager.manager_getter()
+ self._state_finders[class_] = manager.state_getter()
+ self._dict_finders[class_] = manager.dict_getter()
+ return manager
+
+ def _collect_management_factories_for(self, cls):
+ """Return a collection of factories in play or specified for a
+ hierarchy.
+
+ Traverses the entire inheritance graph of a cls and returns a
+ collection of instrumentation factories for those classes. Factories
+ are extracted from active ClassManagers, if available, otherwise
+ instrumentation_finders is consulted.
+
+ """
+ hierarchy = util.class_hierarchy(cls)
+ factories = set()
+ for member in hierarchy:
+ manager = manager_of_class(member)
+ if manager is not None:
+ factories.add(manager.factory)
+ else:
+ for finder in instrumentation_finders:
+ factory = finder(member)
+ if factory is not None:
+ break
+ else:
+ factory = None
+ factories.add(factory)
+ factories.discard(None)
+ return factories
+
+ def manager_of_class(self, cls):
+ # this is only called when alternate instrumentation
+ # has been established
+ if cls is None:
+ return None
+ try:
+ finder = self._manager_finders[cls]
+ except KeyError:
+ return None
+ else:
+ return finder(cls)
+
+ def state_of(self, instance):
+ # this is only called when alternate instrumentation
+ # has been established
+ if instance is None:
+ raise AttributeError("None has no persistent state.")
+ try:
+ return self._state_finders[instance.__class__](instance)
+ except KeyError:
+ raise AttributeError("%r is not instrumented" %
+ instance.__class__)
+
+ def dict_of(self, instance):
+ # this is only called when alternate instrumentation
+ # has been established
+ if instance is None:
+ raise AttributeError("None has no persistent state.")
+ try:
+ return self._dict_finders[instance.__class__](instance)
+ except KeyError:
+ raise AttributeError("%r is not instrumented" %
+ instance.__class__)
+
+ def unregister(self, class_):
+ if class_ in self._manager_finders:
+ manager = self.manager_of_class(class_)
+ manager.unregister()
+ manager.dispose()
+ del self._manager_finders[class_]
+ del self._state_finders[class_]
+ del self._dict_finders[class_]
+ if ClassManager.MANAGER_ATTR in class_.__dict__:
+ delattr(class_, ClassManager.MANAGER_ATTR)
+
+instrumentation_registry = InstrumentationRegistry()
+
+
+def _install_lookup_strategy(implementation):
+ """Replace global class/object management functions
+ with either faster or more comprehensive implementations,
+ based on whether or not extended class instrumentation
+ has been detected.
+
+ This function is called only by InstrumentationRegistry()
+ and unit tests specific to this behavior.
+
+ """
+ global instance_state, instance_dict, manager_of_class
+ if implementation is util.symbol('native'):
+ instance_state = attrgetter(ClassManager.STATE_ATTR)
+ instance_dict = attrgetter("__dict__")
+ def manager_of_class(cls):
+ return cls.__dict__.get(ClassManager.MANAGER_ATTR, None)
+ else:
+ instance_state = instrumentation_registry.state_of
+ instance_dict = instrumentation_registry.dict_of
+ manager_of_class = instrumentation_registry.manager_of_class
+ attributes.instance_state = instance_state
+ attributes.instance_dict = instance_dict
+ attributes.manager_of_class = manager_of_class
+
+_create_manager_for_cls = instrumentation_registry.create_manager_for_cls
+
+# Install default "lookup" strategies. These are basically
+# very fast attrgetters for key attributes.
+# When a custom ClassManager is installed, more expensive per-class
+# strategies are copied over these.
+_install_lookup_strategy(util.symbol('native'))
+
+
+def find_native_user_instrumentation_hook(cls):
+ """Find user-specified instrumentation management for a class."""
+ return getattr(cls, INSTRUMENTATION_MANAGER, None)
+instrumentation_finders.append(find_native_user_instrumentation_hook)
+
+def _generate_init(class_, class_manager):
+ """Build an __init__ decorator that triggers ClassManager events."""
+
+ # TODO: we should use the ClassManager's notion of the
+ # original '__init__' method, once ClassManager is fixed
+ # to always reference that.
+ original__init__ = class_.__init__
+ assert original__init__
+
+ # Go through some effort here and don't change the user's __init__
+ # calling signature.
+ # FIXME: need to juggle local names to avoid constructor argument
+ # clashes.
+ func_body = """\
+def __init__(%(apply_pos)s):
+ new_state = class_manager._new_state_if_none(%(self_arg)s)
+ if new_state:
+ return new_state.initialize_instance(%(apply_kw)s)
+ else:
+ return original__init__(%(apply_kw)s)
+"""
+ func_vars = util.format_argspec_init(original__init__, grouped=False)
+ func_text = func_body % func_vars
+
+ # Py3K
+ #func_defaults = getattr(original__init__, '__defaults__', None)
+ # Py2K
+ func = getattr(original__init__, 'im_func', original__init__)
+ func_defaults = getattr(func, 'func_defaults', None)
+ # end Py2K
+
+ env = locals().copy()
+ exec func_text in env
+ __init__ = env['__init__']
+ __init__.__doc__ = original__init__.__doc__
+ if func_defaults:
+ __init__.func_defaults = func_defaults
+ return __init__