server/hook.py
branchstable
changeset 7677 134613d3b353
parent 7638 cc7cde77184f
child 7879 9aae456abab5
--- a/server/hook.py	Wed Jul 20 14:09:42 2011 +0200
+++ b/server/hook.py	Wed Jul 20 18:22:41 2011 +0200
@@ -69,12 +69,19 @@
 ~~~~~~~~~~
 
 Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class
-that may be created by hooks and scheduled to happen just before (or after) the
-`precommit`, `postcommit` or `rollback` event. Hooks are being fired immediately
-on data operations, and it is sometime necessary to delay the actual work down
-to a time where all other hooks have run. Also while the order of execution of
-hooks is data dependant (and thus hard to predict), it is possible to force an
-order on operations.
+that may be created by hooks and scheduled to happen on `precommit`,
+`postcommit` or `rollback` event (i.e. respectivly before/after a commit or
+before a rollback of a transaction).
+
+Hooks are being fired immediately on data operations, and it is sometime
+necessary to delay the actual work down to a time where we can expect all
+information to be there, or when all other hooks have run (though take case
+since operations may themselves trigger hooks). Also while the order of
+execution of hooks is data dependant (and thus hard to predict), it is possible
+to force an order on operations.
+
+So, for such case where you may miss some information that may be set later in
+the transaction, you should instantiate an operation in the hook.
 
 Operations may be used to:
 
@@ -248,7 +255,7 @@
 from logging import getLogger
 from itertools import chain
 
-from logilab.common.decorators import classproperty
+from logilab.common.decorators import classproperty, cached
 from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.logging_ext import set_log_methods
 
@@ -257,7 +264,7 @@
 from cubicweb.cwvreg import CWRegistry, VRegistry
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
                                 is_instance)
-from cubicweb.appobject import AppObject
+from cubicweb.appobject import AppObject, NotSelector, OrSelector
 from cubicweb.server.session import security_enabled
 
 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
@@ -318,15 +325,83 @@
             else:
                 entities = []
                 eids_from_to = []
+            pruned = self.get_pruned_hooks(session, event,
+                                           entities, eids_from_to, kwargs)
             # by default, hooks are executed with security turned off
             with security_enabled(session, read=False):
                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
-                    hooks = sorted(self.possible_objects(session, **_kwargs),
+                    hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs),
                                    key=lambda x: x.order)
                     with security_enabled(session, write=False):
                         for hook in hooks:
-                            #print hook.category, hook.__regid__
-                            hook()
+                           hook()
+
+    def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
+        """return a set of hooks that should not be considered by filtered_possible objects
+
+        the idea is to make a first pass over all the hooks in the
+        registry and to mark put some of them in a pruned list. The
+        pruned hooks are the one which:
+
+        * are disabled at the session level
+        * have a match_rtype or an is_instance selector which does not
+          match the rtype / etype of the relations / entities for
+          which we are calling the hooks. This works because the
+          repository calls the hooks grouped by rtype or by etype when
+          using the entities or eids_to_from keyword arguments
+
+        Only hooks with a simple selector or an AndSelector of simple
+        selectors are considered for disabling.
+
+        """
+        if 'entity' in kwargs:
+            entities = [kwargs['entity']]
+        if len(entities):
+            look_for_selector = is_instance
+            etype = entities[0].__regid__
+        elif 'rtype' in kwargs:
+            look_for_selector = match_rtype
+            etype = None
+        else: # nothing to prune, how did we get there ???
+            return set()
+        cache_key = (event, kwargs.get('rtype'), etype)
+        pruned = session.pruned_hooks_cache.get(cache_key)
+        if pruned is not None:
+            return pruned
+        pruned = set()
+        session.pruned_hooks_cache[cache_key] = pruned
+        if look_for_selector is not None:
+            for id, hooks in self.iteritems():
+                for hook in hooks:
+                    enabled_cat, main_filter = hook.filterable_selectors()
+                    if enabled_cat is not None:
+                        if not enabled_cat(hook, session):
+                            pruned.add(hook)
+                            continue
+                    if main_filter is not None:
+                        if isinstance(main_filter, match_rtype) and \
+                           (main_filter.frometypes is not None  or \
+                            main_filter.toetypes is not None):
+                            continue
+                        first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next()
+                        if not main_filter(hook, session, **first_kwargs):
+                            pruned.add(hook)
+        return pruned
+
+
+    def filtered_possible_objects(self, pruned, *args, **kwargs):
+        for appobjects in self.itervalues():
+            if pruned:
+                filtered_objects = [obj for obj in appobjects if obj not in pruned]
+                if not filtered_objects:
+                    continue
+            else:
+                filtered_objects = appobjects
+            obj = self._select_best(filtered_objects,
+                                    *args, **kwargs)
+            if obj is None:
+                continue
+            yield obj
 
 class HooksManager(object):
     def __init__(self, vreg):
@@ -464,6 +539,15 @@
     # stop pylint from complaining about missing attributes in Hooks classes
     eidfrom = eidto = entity = rtype = None
 
+    @classmethod
+    @cached
+    def filterable_selectors(cls):
+        search = cls.__select__.search_selector
+        if search((NotSelector, OrSelector)):
+            return None, None
+        enabled_cat = search(enabled_category)
+        main_filter = search((is_instance, match_rtype))
+        return enabled_cat, main_filter
 
     @classmethod
     def check_events(cls):
@@ -653,8 +737,8 @@
     operation. These keyword arguments will be accessible as attributes from the
     operation instance.
 
-    An operation is triggered on connections pool events related to
-    commit / rollback transations. Possible events are:
+    An operation is triggered on connections set events related to commit /
+    rollback transations. Possible events are:
 
     * `precommit`:
 
@@ -728,7 +812,7 @@
         getattr(self, event)()
 
     def precommit_event(self):
-        """the observed connections pool is preparing a commit"""
+        """the observed connections set is preparing a commit"""
 
     def revertprecommit_event(self):
         """an error went when pre-commiting this operation or a later one
@@ -738,14 +822,13 @@
         """
 
     def rollback_event(self):
-        """the observed connections pool has been rollbacked
+        """the observed connections set has been rollbacked
 
-        do nothing by default, the operation will just be removed from the pool
-        operation list
+        do nothing by default
         """
 
     def postcommit_event(self):
-        """the observed connections pool has committed"""
+        """the observed connections set has committed"""
 
     @property
     @deprecated('[3.6] use self.session.user')
@@ -1028,7 +1111,7 @@
     data_key = 'neweids'
 
     def rollback_event(self):
-        """the observed connections pool has been rollbacked,
+        """the observed connections set has been rollbacked,
         remove inserted eid from repository type/source cache
         """
         try:
@@ -1042,7 +1125,7 @@
     """
     data_key = 'pendingeids'
     def postcommit_event(self):
-        """the observed connections pool has been rollbacked,
+        """the observed connections set has been rollbacked,
         remove inserted eid from repository type/source cache
         """
         try: