update documentation to follow 6142:8bc6eac1fac1 changes. Try to make it better and move most doc with code on the way
--- a/doc/book/en/devrepo/repo/hooks.rst Wed Aug 25 18:13:05 2010 +0200
+++ b/doc/book/en/devrepo/repo/hooks.rst Wed Aug 25 18:29:55 2010 +0200
@@ -1,158 +1,26 @@
.. -*- coding: utf-8 -*-
-
.. _hooks:
Hooks and Operations
====================
-Generalities
-------------
-
-Paraphrasing the `emacs`_ documentation, let us say that hooks are an
-important mechanism for customizing an application. A hook is
-basically a list of functions to be called on some well-defined
-occasion (this is called `running the hook`).
-
-.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
-
-In CubicWeb, hooks are subclasses of the Hook class in
-`server/hook.py`, implementing their own `call` method, and selected
-over a set of pre-defined `events` (and possibly more conditions,
-hooks being selectable AppObjects like views and components).
-
-There are two families of events: data events and server events. In a
-typical application, most of the Hooks are defined over data
-events.
-
-The purpose of data hooks is to complement the data model as defined
-in the schema.py, which is static by nature, with dynamic or value
-driven behaviours. It is functionally equivalent to a `database
-trigger`_, except that database triggers definition languages are not
-standardized, hence not portable (for instance, PL/SQL works with
-Oracle and PostgreSQL but not SqlServer nor Sqlite).
-
-.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
-
-Data hooks can serve the following purposes:
-
-* enforcing constraints that the static schema cannot express
- (spanning several entities/relations, exotic value ranges and
- cardinalities, etc.)
-
-* implement computed attributes
-
-Operations are Hook-like objects that may be created by Hooks and
-scheduled to happen just before (or after) the `commit` event. Hooks
-being fired immediately on data operations, it is sometime necessary
-to delay the actual work down to a time where all other Hooks have
-run, for instance a validation check which needs that all relations be
-already set on an entity. 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.
-
-Operations also may be used to process various side effects associated
-with a transaction such as filesystem udpates, mail notifications,
-etc.
-
-Operations are subclasses of the Operation class in `server/hook.py`,
-implementing `precommit_event` and other standard methods (wholly
-described in :ref:`operations_api`).
-
-Events
-------
-
-Hooks are mostly defined and used to handle `dataflow`_ operations. It
-means as data gets in (entities added, updated, relations set or
-unset), specific events are issued and the Hooks matching these events
-are called.
-
-.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
-
-Below comes a list of the dataflow events related to entities operations:
-
-* before_add_entity
-
-* before_update_entity
+.. autodocstring:: cubicweb.server.hook
-* before_delete_entity
-
-* after_add_entity
-
-* after_update_entity
-
-* after_delete_entity
-
-These define ENTTIES HOOKS. RELATIONS HOOKS are defined
-over the following events:
-
-* after_add_relation
-
-* after_delete_relation
-
-* before_add_relation
-
-* before_delete_relation
-
-This is an occasion to remind us that relations support the add/delete
-operation, but no update.
-
-Non data events also exist. These are called SYSTEM HOOKS.
-
-* server_startup
-
-* server_shutdown
-
-* server_maintenance
-
-* server_backup
-
-* server_restore
+Example using dataflow hooks
+----------------------------
-* session_open
-
-* session_close
-
-
-Using dataflow Hooks
---------------------
-
-Dataflow hooks either automate data operations or maintain the
-consistency of the data model. In the later case, we must use a
-specific exception named ValidationError
-
-Validation Errors
-~~~~~~~~~~~~~~~~~
-
-When a condition is not met in a Hook/Operation, it must raise a
-`ValidationError`. Raising anything but a (subclass of)
-ValidationError is a programming error. Raising a ValidationError
-entails aborting the current transaction.
-
-The ValidationError exception is used to convey enough information up
-to the user interface. Hence its constructor is different from the
-default Exception constructor. It accepts, positionally:
-
-* an entity eid,
-
-* a dict whose keys represent attribute (or relation) names and values
- an end-user facing message (hence properly translated) relating the
- problem.
-
-An entity hook
-~~~~~~~~~~~~~~
-
-We will use a very simple example to show hooks usage. Let us start
-with the following schema.
+We will use a very simple example to show hooks usage. Let us start with the
+following schema.
.. sourcecode:: python
class Person(EntityType):
age = Int(required=True)
-We would like to add a range constraint over a person's age. Let's
-write an hook. It shall be placed into mycube/hooks.py. If this file
-were to grow too much, we can easily have a mycube/hooks/... package
-containing hooks in various modules.
+We would like to add a range constraint over a person's age. Let's write an hook
+(supposing yams can not handle this nativly, which is wrong). It shall be placed
+into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
+`mycube/hooks/... package` containing hooks in various modules.
.. sourcecode:: python
@@ -166,64 +34,26 @@
__select__ = Hook.__select__ & implements('Person')
def __call__(self):
- if 0 >= self.entity.age <= 120:
- return
- msg = self._cw._('age must be between 0 and 120')
- raise ValidationError(self.entity.eid, {'age': msg})
-
-Hooks being AppObjects like views, they have a __regid__ and a
-__select__ class attribute. The base __select__ is augmented with an
-`implements` selector matching the desired entity type. The `events`
-tuple is used by the Hook.__select__ base selector to dispatch the
-hook on the right events. In an entity hook, it is possible to
-dispatch on any entity event (e.g. 'before_add_entity',
-'before_update_entity') at once if needed.
+ if 'age' in self.entity.cw_edited:
+ if 0 >= self.entity.age <= 120:
+ return
+ msg = self._cw._('age must be between 0 and 120')
+ raise ValidationError(self.entity.eid, {'age': msg})
-Like all appobjects, hooks have the `self._cw` attribute which
-represents the current session. In entity hooks, a `self.entity`
-attribute is also present.
-
-
-A relation hook
-~~~~~~~~~~~~~~~
-
-Let us add another entity type with a relation to person (in
-mycube/schema.py).
-
-.. sourcecode:: python
+In our example the base `__select__` is augmented with an `implements` selector
+matching the desired entity type.
- class Company(EntityType):
- name = String(required=True)
- boss = SubjectRelation('Person', cardinality='1*')
+The `events` tuple is used specify that our hook should be called before the
+entity is added or updated.
-We would like to constrain the company's bosses to have a minimum
-(legal) age. Let's write an hook for this, which will be fired when
-the `boss` relation is established.
-
-.. sourcecode:: python
-
- class CompanyBossLegalAge(Hook):
- __regid__ = 'company_boss_legal_age'
- events = ('before_add_relation',)
- __select__ = Hook.__select__ & match_rtype('boss')
+Then in the hook's `__call__` method, we:
- def __call__(self):
- boss = self._cw.entity_from_eid(self.eidto)
- if boss.age < 18:
- msg = self._cw._('the minimum age for a boss is 18')
- raise ValidationError(self.eidfrom, {'boss': msg})
-
-We use the `match_rtype` selector to select the proper relation type.
+* check if the 'age' attribute is edited
+* if so, check the value is in the range
+* if not, raise a validation error properly
-The essential difference with respect to an entity hook is that there
-is no self.entity, but `self.eidfrom` and `self.eidto` hook attributes
-which represent the subject and object eid of the relation.
-
-
-Using Operations
-----------------
-
-Let's augment our example with a new `subsidiary_of` relation on Company.
+Now Let's augment our schema with new `Company` entity type with some relation to
+`Person` (in 'mycube/schema.py').
.. sourcecode:: python
@@ -232,12 +62,37 @@
boss = SubjectRelation('Person', cardinality='1*')
subsidiary_of = SubjectRelation('Company', cardinality='*?')
-Base example
-~~~~~~~~~~~~
+
+We would like to constrain the company's bosses to have a minimum (legal)
+age. Let's write an hook for this, which will be fired when the `boss` relation
+is established (still supposing we could not specify that kind of thing in the
+schema).
+
+.. sourcecode:: python
+
+ class CompanyBossLegalAge(Hook):
+ __regid__ = 'company_boss_legal_age'
+ __select__ = Hook.__select__ & match_rtype('boss')
+ events = ('before_add_relation',)
-We would like to check that there is no cycle by the `subsidiary_of`
-relation. This is best achieved in an Operation since all relations
-are likely to be set at commit time.
+ def __call__(self):
+ boss = self._cw.entity_from_eid(self.eidto)
+ if boss.age < 18:
+ msg = self._cw._('the minimum age for a boss is 18')
+ raise ValidationError(self.eidfrom, {'boss': msg})
+
+.. Note::
+
+ We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the
+ proper relation type.
+
+ The essential difference with respect to an entity hook is that there is no
+ self.entity, but `self.eidfrom` and `self.eidto` hook attributes which
+ represent the subject and object **eid** of the relation.
+
+Suppose we want to check that there is no cycle by the `subsidiary_of`
+relation. This is best achieved in an operation since all relations are likely to
+be set at commit time.
.. sourcecode:: python
@@ -251,6 +106,7 @@
raise ValidationError(eid, {rtype: msg})
parents.add(parent.eid)
+
class CheckSubsidiaryCycleOp(Operation):
def precommit_event(self):
@@ -259,30 +115,20 @@
class CheckSubsidiaryCycleHook(Hook):
__regid__ = 'check_no_subsidiary_cycle'
+ __select__ = Hook.__select__ & match_rtype('subsidiary_of')
events = ('after_add_relation',)
- __select__ = Hook.__select__ & match_rtype('subsidiary_of')
def __call__(self):
CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto)
-The operation is instantiated in the Hook.__call__ method.
-An operation always takes a session object as first argument
-(accessible as `.session` from the operation instance), and optionally
-all keyword arguments needed by the operation. These keyword arguments
-will be accessible as attributes from the operation instance.
+Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other
+exceptions are usually programming errors.
-Like in Hooks, ValidationError can be raised in Operations. Other
-exceptions are programming errors.
-
-Notice how our hook will instantiate an operation each time the Hook
-is called, i.e. each time the `subsidiary_of` relation is set.
-
-Using set_operation
-~~~~~~~~~~~~~~~~~~~
-
-There is an alternative method to schedule an Operation from a Hook,
-using the `set_operation` function.
+In the above example, our hook will instantiate an operation each time the hook
+is called, i.e. each time the `subsidiary_of` relation is set. There is an
+alternative method to schedule an operation from a hook, using the
+:func:`set_operation` function.
.. sourcecode:: python
@@ -295,142 +141,91 @@
def __call__(self):
set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto,
- CheckSubsidiaryCycleOp, rtype=self.rtype)
+ CheckSubsidiaryCycleOp)
class CheckSubsidiaryCycleOp(Operation):
def precommit_event(self):
- for eid in self._cw.transaction_data['subsidiary_cycle_detection']:
- check_cycle(self.session, eid, self.rtype)
+ for eid in self._cw.transaction_data.pop('subsidiary_cycle_detection'):
+ check_cycle(self.session, eid, 'subsidiary_of')
-Here, we call set_operation with a session object, a specially forged
-key, a value that is the actual payload of an individual operation (in
-our case, the object of the subsidiary_of relation) , the class of the
-Operation, and more optional parameters to give to the operation (here
-the rtype which do not vary accross operations).
-The body of the operation must then iterate over the values that have
-been mapped in the transaction_data dictionary to the forged key.
-
-This mechanism is especially useful on two occasions (not shown in our
-example):
-
-* massive data import (reduced memory consumption within a large
- transaction)
-
-* when several hooks need to instantiate the same operation (e.g. an
- entity and a relation hook).
+Here, we call :func:`set_operation` so that we will simply accumulate eids of
+entities to check at the end in a single CheckSubsidiaryCycleOp operation. Value
+are stored in a set associated to the 'subsidiary_cycle_detection' transaction
+data key. The set initialization and operation creation are handled nicely by
+:func:set_operation.
-.. note::
-
- A more realistic example can be found in the advanced tutorial
- chapter :ref:`adv_tuto_security_propagation`.
+A more realistic example can be found in the advanced tutorial chapter
+:ref:`adv_tuto_security_propagation`.
-.. _operations_api:
-Operation: a small API overview
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Hooks writing tips
+------------------
-.. autoclass:: cubicweb.server.hook.Operation
-.. autoclass:: cubicweb.server.hook.LateOperation
-.. autofunction:: cubicweb.server.hook.set_operation
+Reminder
+~~~~~~~~
-Hooks writing rules
--------------------
+Never, ever use the `entity.foo = 42` notation to update an entity. It will not
+work.To updating an entity attribute or relation, uses :meth:`set_attributes` and
+:meth:`set_relations` methods.
-Remainder
-~~~~~~~~~
-
-Never, ever use the `entity.foo = 42` notation to update an entity. It
-will not work.
How to choose between a before and an after event ?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Before hooks give you access to the old attribute (or relation)
-values. By definition the database is not yet updated in a before
-hook.
-
-To access old and new values in an before_update_entity hook, one can
-use the `server.hook.entity_oldnewvalue` function which returns a
-tuple of the old and new values. This function takes an entity and an
-attribute name as parameters.
-
-In a 'before_add|update_entity' hook the self.entity contains the new
-values. One is allowed to further modify them before database
-operations, using the dictionary notation.
-
-.. sourcecode:: python
-
- self.entity['age'] = 42
+'before_*' hooks give you access to the old attribute (or relation)
+values. You can also hi-jack actually edited stuff in the case of entity
+modification. Needing one of this will definitly guide your choice.
-This is because using self.entity.set_attributes(age=42) will
-immediately update the database (which does not make sense in a
-pre-database hook), and will trigger any existing
-before_add|update_entity hook, thus leading to infinite hook loops or
-such awkward situations.
-
-Beyond these specific cases, updating an entity attribute or relation
-must *always* be done using `set_attributes` and `set_relations`
-methods.
+Else the question is: should I need to do things before or after the actual
+modification. If the answer is "it doesn't matter", use an 'after' event.
-(Of course, ValidationError will always abort the current transaction,
-whetever the event).
-Peculiarities of inlined relations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Some relations are defined in the schema as `inlined` (see
-:ref:`RelationType` for details). In this case, they are inserted in
-the database at the same time as entity attributes.
-
-Hence in the case of before_add_relation, such relations already exist
-in the database.
-
-Edited attributes
+Validation Errors
~~~~~~~~~~~~~~~~~
-On udpates, it is possible to ask the `entity.edited_attributes`
-variable whether one attribute has been updated.
+When a hook is responsible to maintain the consistency of the data model detect
+an error, it must use a specific exception named
+:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of)
+:exc:`~cubicweb.ValidationError` is a programming error. Raising a it entails
+aborting the current transaction.
-.. sourcecode:: python
+This exception is used to convey enough information up to the user
+interface. Hence its constructor is different from the default Exception
+constructor. It accepts, positionally:
+
+* an entity eid,
- if 'age' not in entity.edited_attribute:
- return
+* a dict whose keys represent attribute (or relation) names and values
+ an end-user facing message (hence properly translated) relating the
+ problem.
+
+
+Checking for object created/deleted in the current transaction
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Deleted in transaction
-~~~~~~~~~~~~~~~~~~~~~~
+In hooks, you can use the
+:meth:`~cubicweb.server.session.Session.added_in_transaction` or
+:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session
+object to check if an eid has been created or deleted during the hook's
+transaction.
-The session object has a deleted_in_transaction method, which can help
-writing deletion Hooks.
+This is useful to enable or disable some stuff if some entity is being added or
+deleted.
.. sourcecode:: python
if self._cw.deleted_in_transaction(self.eidto):
return
-Given this predicate, we can avoid scheduling an operation.
-Disabling hooks
-~~~~~~~~~~~~~~~
-
-It is sometimes convenient to disable some hooks. For instance to
-avoid infinite Hook loops. One uses the `hooks_control` context
-manager.
-
-This can be controlled more finely through the `category` Hook class
-attribute, which is a string.
+Peculiarities of inlined relations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-.. sourcecode:: python
-
- with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, <category>):
- # ... do stuff
-
-.. autoclass:: cubicweb.server.session.hooks_control
-
-The existing categories are: ``email``, ``syncsession``,
-``syncschema``, ``bookmark``, ``security``, ``worfklow``,
-``metadata``, ``notification``, ``integrity``, ``activeintegrity``.
-
-Nothing precludes one to invent new categories and use the
-hooks_control context manager to filter them (in or out).
+Relations which are defined in the schema as `inlined` (see :ref:`RelationType`
+for details) are inserted in the database at the same time as entity attributes.
+This may have some side effect, for instance when creating entity and setting an
+inlined relation in the same rql query, when 'before_add_relation' for that
+relation will be run, the relation will already exist in the database (it's
+usually not the case).
--- a/server/hook.py Wed Aug 25 18:13:05 2010 +0200
+++ b/server/hook.py Wed Aug 25 18:29:55 2010 +0200
@@ -15,37 +15,228 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Hooks management
+"""
+Generalities
+------------
+
+Paraphrasing the `emacs`_ documentation, let us say that hooks are an important
+mechanism for customizing an application. A hook is basically a list of
+functions to be called on some well-defined occasion (this is called `running
+the hook`).
+
+.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
+
+Hooks
+~~~~~
+
+In |cubicweb|, hooks are subclasses of the :class:`~cubicweb.server.hook.Hook`
+class. They are selected over a set of pre-defined `events` (and possibly more
+conditions, hooks being selectable appobjects like views and components). They
+should implement a :meth:`~cubicweb.server.hook.Hook.__call__` method that will
+be called when the hook is triggered.
-This module defined the `Hook` class and registry and a set of abstract classes
-for operations.
+There are two families of events: data events (before / after any individual
+update of an entity / or a relation in the repository) and server events (such
+as server startup or shutdown). In a typical application, most of the hooks are
+defined over data events.
+
+Also, some :class:`~cubicweb.server.hook.Operation` may be registered by hooks,
+which will be fired when the transaction is commited or rollbacked.
+
+The purpose of data event hooks is usually to complement the data model as
+defined in the schema, which is static by nature and only provide a restricted
+builtin set of dynamic constraints, with dynamic or value driven behaviours.
+For instance they can serve the following purposes:
+
+* enforcing constraints that the static schema cannot express (spanning several
+ entities/relations, exotic value ranges and cardinalities, etc.)
+
+* implement computed attributes
+
+It is functionally equivalent to a `database trigger`_, except that database
+triggers definition languages are not standardized, hence not portable (for
+instance, PL/SQL works with Oracle and PostgreSQL but not SqlServer nor Sqlite).
+
+.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
-Hooks are called before / after any individual update of entities / relations
-in the repository and on special events such as server startup or shutdown.
+Operations
+~~~~~~~~~~
+
+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.
+
+Operations may be used to:
+
+* implements a validation check which needs that all relations be already set on
+ an entity
+
+* process various side effects associated with a transaction such as filesystem
+ udpates, mail notifications, etc.
-Operations may be registered by hooks during a transaction, which will be
-fired when the pool is commited or rollbacked.
+Events
+------
+
+Hooks are mostly defined and used to handle `dataflow`_ operations. It
+means as data gets in (entities added, updated, relations set or
+unset), specific events are issued and the Hooks matching these events
+are called.
+
+You can get the event that triggered a hook by accessing its :attr:event
+attribute.
+
+.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
-Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
-after_update_entity, before_delete_entity, after_delete_entity) all have an
-`entity` attribute
+Entity modification related events
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When called for one of these events, hook will have an `entity` attribute
+containing the entity instance.
+
+* 'before_add_entity', 'before_update_entity':
+
+ - on those events, you can check what attributes of the entity are modified in
+ `entity.cw_edited` (by definition the database is not yet updated in a before
+ event)
+
+ - you are allowed to further modify the entity before database operations,
+ using the dictionary notation. By doing this, you'll avoid the need for a
+ whole new rql query processing, the only difference is that the underlying
+ backend query (eg usually sql) will contains the additional data. For
+ example:
+
+ .. sourcecode:: python
+
+ self.entity.set_attributes(age=42)
+
+ will set the `age` attribute of the entity to 42. But to do so, it will
+ generate a rql query that will have to be processed, then trigger some
+ hooks, and so one (potentially leading to infinite hook loops or such
+ awkward situations..) You can avoid this by doing the modification that way:
+
+ .. sourcecode:: python
+
+ self.entity.cw_edited['age'] = 42
+
+ Here the attribute will simply be edited in the same query that the
+ one that triggered the hook.
-Relation (eg before_add_relation, after_add_relation, before_delete_relation,
-after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
+ Similarly, removing an attribute from `cw_edited` will cancel its
+ modification.
+
+ - on 'before_update_entity' event, you can access to old and new values in
+ this hook, by using `entity.cw_edited.oldnewvalue(attr)`
+
+
+* 'after_add_entity', 'after_update_entity'
+
+ - on those events, you can still check what attributes of the entity are
+ modified in `entity.cw_edited` but you can't get anymore the old value, nor
+ modify it.
+
+* 'before_delete_entity', 'after_delete_entity'
+
+ - on those events, the entity has no `cw_edited` set.
+
+
+Relation modification related events
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When called for one of these events, hook will have `eidfrom`, `rtype`, `eidto`
+attributes containing respectivly the eid of the subject entity, the relation
+type and the eid of the object entity.
+
+* 'before_add_relation', 'before_delete_relation'
+
+ - on those events, you can still get original relation by issuing a rql query
+
+* 'after_add_relation', 'after_delete_relation'
+
+This is an occasion to remind us that relations support the add / delete
+operation, but no update.
+
+
+Non data events
+~~~~~~~~~~~~~~~
-Server start/maintenance/stop hooks (eg server_startup, server_maintenance,
-server_shutdown) have a `repo` attribute, but *their `_cw` attribute is None*.
-The `server_startup` is called on regular startup, while `server_maintenance`
-is called on cubicweb-ctl upgrade or shell commands. `server_shutdown` is
-called anyway.
+Hooks called on server start/maintenance/stop event (eg 'server_startup',
+'server_maintenance', 'server_shutdown') have a `repo` attribute, but *their
+`_cw` attribute is None*. The `server_startup` is called on regular startup,
+while `server_maintenance` is called on cubicweb-ctl upgrade or shell
+commands. `server_shutdown` is called anyway.
+
+Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have
+a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*.
+
+Hooks called on session event (eg 'session_open', 'session_close') have no
+special attribute.
+
+
+API
+---
+
+Hooks control
+~~~~~~~~~~~~~
+
+It is sometimes convenient to explicitly enable or disable some hooks. For
+instance if you want to disable some integrity checking hook. This can be
+controlled more finely through the `category` class attribute, which is a string
+giving a category name. One can then uses the
+:class:`~cubicweb.server.session.hooks_control` context manager to explicitly
+enable or disable some categories.
+
+.. autoclass:: cubicweb.server.session.hooks_control
+
+
+The existing categories are:
+
+* ``security``, security checking hooks
+
+* ``worfklow``, workflow handling hooks
-Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
-`timestamp` attributes, but *their `_cw` attribute is None*.
+* ``metadata``, hooks setting meta-data on newly created entities
+
+* ``notification``, email notification hooks
+
+* ``integrity``, data integrity checking hooks
+
+* ``activeintegrity``, data integrity consistency hooks, that you should *never*
+ want to disable
+
+* ``syncsession``, hooks synchronizing existing sessions
+
+* ``syncschema``, hooks synchronizing instance schema (including the physical database)
+
+* ``email``, email address handling hooks
+
+* ``bookmark``, bookmark entities handling hooks
+
-Session hooks (eg session_open, session_close) have no special attribute.
+Nothing precludes one to invent new categories and use the
+:class:`~cubicweb.server.session.hooks_control` context manager to filter them
+in or out.
+
+
+Hooks specific selector
+~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.server.hook.match_rtype
+.. autoclass:: cubicweb.server.hook.match_rtype_sets
+
+
+Hooks and operations classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.server.hook.Hook
+.. autoclass:: cubicweb.server.hook.Operation
+.. autoclass:: cubicweb.server.hook.LateOperation
+.. autofunction:: cubicweb.server.hook.set_operation
+
"""
from __future__ import with_statement
@@ -192,6 +383,29 @@
# base class for hook ##########################################################
class Hook(AppObject):
+ """Base class for hook.
+
+ Hooks being appobjects like views, they have a `__regid__` and a `__select__`
+ class attribute. Like all appobjects, hooks have the `self._cw` attribute which
+ represents the current session. In entity hooks, a `self.entity` attribute is
+ also present.
+
+ The `events` tuple is used by the base class selector to dispatch the hook
+ on the right events. It is possible to dispatch on multiple events at once
+ if needed (though take care as hook attribute may vary as described above).
+
+ .. Note::
+
+ Do not forget to extend the base class selectors as in ::
+
+ .. sourcecode:: python
+
+ class MyHook(Hook):
+ __regid__ = 'whatever'
+ __select__ = Hook.__select__ & implements('Person')
+
+ else your hooks will be called madly, whatever the event.
+ """
__select__ = enabled_category()
# set this in derivated classes
events = None
@@ -353,40 +567,53 @@
# abstract classes for operation ###############################################
class Operation(object):
- """an operation is triggered on connections pool events related to
+ """Base class for operations.
+
+ Operation may be instantiated in the hooks' `__call__` method. It always
+ takes a session object as first argument (accessible as `.session` from the
+ operation instance), and optionally all keyword arguments needed by the
+ 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:
- precommit:
- the pool is preparing to commit. You shouldn't do anything which
- has to be reverted if the commit fails at this point, but you can freely
- do any heavy computation or raise an exception if the commit can't go.
- You can add some new operations during this phase but their precommit
- event won't be triggered
+ * 'precommit':
- commit:
- the pool is preparing to commit. You should avoid to do to expensive
- stuff or something that may cause an exception in this event
+ the transaction is being prepared for commit. You can freely do any heavy
+ computation, raise an exception if the commit can't go. or even add some
+ new operations during this phase. If you do anything which has to be
+ reverted if the commit fails afterwards (eg altering the file system for
+ instance), you'll have to support the 'revertprecommit' event to revert
+ things by yourself
- revertcommit:
- if an operation failed while commited, this event is triggered for
- all operations which had their commit event already to let them
- revert things (including the operation which made fail the commit)
+ * 'revertprecommit':
+
+ if an operation failed while being pre-commited, this event is triggered
+ for all operations which had their 'precommit' event already fired to let
+ them revert things (including the operation which made the commit fail)
+
+ * 'rollback':
- rollback:
the transaction has been either rollbacked either:
+
* intentionaly
- * a precommit event failed, all operations are rollbacked
- * a commit event failed, all operations which are not been triggered for
- commit are rollbacked
+ * a 'precommit' event failed, in which case all operations are rollbacked
+ once 'revertprecommit'' has been called
+
+ * 'postcommit':
- postcommit:
- The transaction is over. All the ORM entities are
- invalid. If you need to work on the database, you need to stard
- a new transaction, for instance using a new internal_session,
- which you will need to commit (and close!).
+ the transaction is over. All the ORM entities accessed by the earlier
+ transaction are invalid. If you need to work on the database, you need to
+ start a new transaction, for instance using a new internal session, which
+ you will need to commit (and close!).
- order of operations may be important, and is controlled according to
- the insert_index's method output
+ For an operation to support an event, one has to implement the `<event
+ name>_event` method with no arguments.
+
+ Notice order of operations may be important, and is controlled according to
+ the insert_index's method output (whose implementation vary according to the
+ base hook class used).
"""
def __init__(self, session, **kwargs):
@@ -468,20 +695,55 @@
{set: set.add, list: list.append}[container.__class__](container, value)
def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs):
- """Search for session.transaction_data[`datakey`] (expected to be a set):
+ """Function to ease applying a single operation on a set of data, avoiding
+ to create as many as operation as they are individual modification. You
+ should try to use this instead of creating on operation for each `value`,
+ since handling operations becomes coslty on massive data import.
+
+ Arguments are:
+
+ * the `session` object
- * if found, simply append `value`
+ * `datakey`, a specially forged key that will be used as key in
+ session.transaction_data
+
+ * `value` that is the actual payload of an individual operation
+
+ * `opcls`, the class of the operation. An instance is created on the first
+ call for the given key, and then subsequent calls will simply add the
+ payload to the container (hence `opkwargs` is only used on that first
+ call)
- * else, initialize it to containercls([`value`]) and instantiate the given
- `opcls` operation class with additional keyword arguments. `containercls`
- is a set by default. Give `list` if you want to keep arrival ordering.
+ * `containercls`, the container class that should be instantiated to hold
+ payloads. An instance is created on the first call for the given key, and
+ then subsequent calls will add the data to the existing container. Default
+ to a set. Give `list` if you want to keep arrival ordering.
+
+ * more optional parameters to give to the operation (here the rtype which do not
+ vary accross operations).
+
+ The body of the operation must then iterate over the values that have been mapped
+ in the transaction_data dictionary to the forged key, e.g.:
+
+ .. sourcecode:: python
- You should use this instead of creating on operation for each `value`,
- since handling operations becomes coslty on massive data import.
+ for value in self._cw.transaction_data.pop(datakey):
+ ...
+
+ .. Note::
+ **poping** the key from `transaction_data` is not an option, else you may
+ get unexpected data loss in some case of nested hooks.
"""
+
+
+
try:
+ # Search for session.transaction_data[`datakey`] (expected to be a set):
+ # if found, simply append `value`
_container_add(session.transaction_data[datakey], value)
except KeyError:
+ # else, initialize it to containercls([`value`]) and instantiate the given
+ # `opcls` operation class with additional keyword arguments
opcls(session, **opkwargs)
session.transaction_data[datakey] = containercls()
_container_add(session.transaction_data[datakey], value)
--- a/server/session.py Wed Aug 25 18:13:05 2010 +0200
+++ b/server/session.py Wed Aug 25 18:29:55 2010 +0200
@@ -64,6 +64,14 @@
If mode is session.`HOOKS_ALLOW_ALL`, given hooks categories will
be disabled.
+
+ .. sourcecode:: python
+
+ with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'integrity'):
+ # ... do stuff with all but 'integrity' hooks activated
+
+ with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'):
+ # ... do stuff with none but 'integrity' hooks activated
"""
def __init__(self, session, mode, *categories):
self.session = session