# HG changeset patch # User Sylvain Thénault # Date 1282753795 -7200 # Node ID 95c604ec89bfa6e0167f8b180b6ec00ee94d5481 # Parent f3d82f25ab61b14e0d7b99fb479b961056dd6f7f update documentation to follow 6142:8bc6eac1fac1 changes. Try to make it better and move most doc with code on the way diff -r f3d82f25ab61 -r 95c604ec89bf doc/book/en/devrepo/repo/hooks.rst --- 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, ): - # ... 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). diff -r f3d82f25ab61 -r 95c604ec89bf server/hook.py --- 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 . -"""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` 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) diff -r f3d82f25ab61 -r 95c604ec89bf server/session.py --- 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