Decovent - Python event handling using decorators
Documentation
Version: 1.1.1
Last Updated: May 20th, 2010
Description
Decovent is a very small Python library that allows an easy and elegant event
rising and handling, using decorators.
- Features:
- Decovent has been tested with Python's both productive versions,
Python 2.6.4 and Python 3.1.1
- events and handlers are tied to the local-thread
- event name is case sensitive, Unicode safe and not required if it
equals the decorated method name
- for an event can be registered as many handlers as necessary
- handlers are registered for (class, event) pair, to differentiate between
events with similar names, but raised by different classes
- a handler can be registered many times, but will be executed only
once for (class, event) pair
- handlers execution order is the same as the registration order
- handlers are always executed in parallel threads, synchronous or asynchronous
- parameters received by the handlers are being sent by the event
- no arguments are required for a handler at registration time
- a handler can be flagged to run only once for an event and then unregister itself
- @classmethods can be raised as events or
registered as handlers
- events and handlers can be memoized at local or global level
- events and handlers can be synchronized on the same lock
- the time allocated for the execution of an event or handler is controllable
- the number of methods that can be executed in parallel is controllable
- Restrictions:
- events and handlers must be methods that belong to new-style classes
- @staticmethods can?t be raised as events or registered as handlers
- one handler can be registered for only one event
It's important to understand that events and handlers are not classes,
but decorated methods that may belong to any new-style class. There are
no restrictions on the class itself regarding inheritance or the interfaces
that are implemented.
top
Download
The latest version of Decovent can be downloaded from
http://pypi.python.org/pypi/Decovent.
top
Examples
How to import Decovent
from decovent import * #the only required import
top
How to create and raise an event
class Mouse(object):
@raise_event()
def click(self, x, y):
return (x, y)
mouse = Mouse()
mouse.click(10, 20)
top
How to create and register a handler
class Mouse(object):
@set_handler('click')
def on_click(self, x, y):
return (x, y)
mouse = Mouse()
mouse.on_click() #no arguments at registration
top
How to create a class that handles it own events
class Mouse(object):
def __init__(self):
self.on_click()
@raise_event()
def click(self, x, y):
return (x, y)
@set_handler('click')
def on_click(self, x, y):
return (x, y)
mouse = Mouse()
mouse.click(10, 20)
top
How to create a class that handles the events of another class
class Mouse(object):
@raise_event()
def click(self, x, y):
return (x, y)
class Screen(object):
@set_handler('click', Mouse)
def on_click(self, x, y):
return (x, y)
screen = Screen()
screen.on_click()
mouse = Mouse()
mouse.click(10, 20)
top
How to create a @classmethod event or handler
- @classmethod decorator must always be placed
before @raise_event or @set_handler
- It is not required that a class method event has to be handled by a class method handler or
the other way around.
- @staticmethod can't be raised as events or
registered as handlers because they don't provide self.__class__
in the arguments.
Without this information is impossible to determine
to which class the raised event belongs.
class Mouse(object):
def __init__(self):
self.on_click()
@classmethod
@raise_event()
def click(self, x, y):
return (x, y)
@set_handler('click')
def on_click(self, x, y):
return (x, y)
Mouse.click()
class Mouse(object):
@classmethod
@raise_event()
def click(self, x, y):
return (x, y)
class Screen(object):
@classmethod
@set_handler('click', Mouse)
def on_click(self, x, y):
return (x, y)
Screen.on_click()
Mouse.click(10, 20)
top
How to raise and handle events with different names than the decorated method name
class Mouse(object):
def __init__(self):
self.on_move()
@raise_event('move')
def click(self, x, y):
return (x, y)
@set_handler('move')
def on_move(self, x, y):
return (x, y)
mouse = Mouse()
mouse.click(10, 20)
top
How to raise and handle events with non-ASCII names
class Mouse(object):
def __init__(self):
self.on_move()
@raise_event('щелкать')
def click(self, x, y):
return (x, y)
@set_handler('щелкать')
def on_click(self, x, y):
return (x, y)
mouse = Mouse()
mouse.click(10, 20)
top
How to execute handlers synchronous or asynchronous
- By default, handlers are executed synchronous in parallel threads.
To execute them asynchronous, set decovent.asynchronous
= True.
See Attributes section for more details
top
How to retrieve the execution result of an event and registered handlers
class Mouse(object):
def __init__(self):
self.on_click()
@raise_event()
def click(self, x, y):
return (x, y)
@set_handler('click')
def on_click(self, x, y):
return (x, y)
mouse = Mouse()
e_result, h_result = mouse.click(10, 20)
pprint(e_result)
pprint(h_result)
>>(True, (10, 20), <class '__main__.Mouse'>, <function click at 0x00BC5F30>)
>>((True, (10, 20), <class '__main__.Mouse'>, <function on_click at 0x00BC5FB0>),)
decovent.exc_info = True
decovent.traceback = True
class Mouse(object):
def __init__(self):
self.on_click()
@raise_event()
def click(self, x, y):
return (x, y)
@set_handler('click')
def on_click(self, x, y):
raise TypeError('Wrong values')
mouse = Mouse()
e_result, h_result = mouse.click(10, 20)
pprint(e_result)
pprint(h_result)
>>(True, (10, 20), <class '__main__.Mouse'>, <function click at 0x00BC5F30>)
>>((False,
>> (<type 'exceptions.TypeError'>,
>> TypeError('Wrong values',),
>> <traceback object at 0x00BB4468>),
>> <class '__main__.Mouse'>,
>> <function on_click at 0x00BC5FB0>),)
top
How to unregister a handler after its first execution
class Mouse(object):
def __init__(self):
self.on_click()
@raise_event()
def click(self, x, y):
return (x, y)
@set_handler('click', unregister=True)
def on_click(self, x, y):
return (x, y)
top
How to unregister handlers at various levels
class Mouse(object):
def __init__(self):
self.on_click()
@raise_event()
def click(self, x, y):
return (x, y)
@set_handler('click', unregister=True)
def on_click(self, x, y):
return (x, y)
decovent.reset(Mouse, 'click') #remove handlers for Mouse().click()
decovent.reset(Mouse) #remove handlers for Mouse()
decovent.reset() #remove handlers for any class
top
How to integrate events and handlers with custom decorators
As opposed to most decorators, that will check some conditions before the
decorated method is executed, Decovent executes the decorated event and handler
methods and returns the results. The execution occurs only when the event is being
raised. If you need to decorate a method and in the same time raise it as an event
or register it as a handler, a few points have to be considered:
- it is recommended that raise_event or
set_handler to be the last decorator in the stack
class Mouse(object):
@some_decorator
@another_decorator
@raise_event()
def click(self, x, y):
return (x, y)
- if you want to decorate a handler, please note that these decorators
will be executed at handler's registration time.
authorizations = ['do_click', 'do_move']
class authorize(object):
""" Authorization check decorator """
def __init__(self, auth_object):
self.auth_object = auth_object
def __call__(self, f):
def auth_f(*args, **kwargs):
if self.auth_object in authorizations:
return f(*args, **kwargs)
else:
raise Exception('Unauthorized')
return auth_f
class Mouse(object):
@classmethod
@authorize('do_click')
@raise_event()
def click(self, x, y):
return (x, y)
@classmethod
@authorize('do_click')
@set_handler('click')
def on_click(self, x, y):
return (x, y)
Mouse.on_click() #@authorize() will be executed here
pprint(Mouse.click(10, 20)) #@authorize() will NOT be executed here
>>((True, (10, 20), <class '__main__.Mouse'>, <function click at 0x00BCAFB0>),
>> ((True,
>> (10, 20),
>> <class '__main__.Mouse'>,
>> <function on_click at 0x00BD10B0>),))
- raise_event or set_handler
will not work with decorators that also execute the decorated method and
return the execution result. This is actually a good side-effect as it will
prevent the same method to be executed several times by each of these decorators.
top
How to memoize events and handlers
http://en.wikipedia.org/wiki/Memoization
Memoization can be activated at local or global level.
When the global level is active, will overwrite the local level.
To activate memoization at local level, set parameter memoize_ of
set_handler or raise_event to
True. To activate it at global level,
set decovent.memoize = True.
Only successful executions are memoized
Please make sure you actually need memoization (especially at global level),
otherwise might be just overhead.
decovent.memoize = True #global
class Mouse(object):
@raise_event()
def click(self, x, y):
return (x, y)
or
class Mouse(object):
@raise_event(memoize_=True) #local
def click(self, x, y):
return (x, y)
top
How to synchronize events and handlers
http://en.wikipedia.org/wiki/Synchronization_(computer_science)#Process_synchronization
To synchronize execution of an event and registered handlers on the same lock,
an external threading.RLock() has to be provided for
lock argument of
raise_event.
Please make sure you understand the difference between running handlers
synchronous/asynchronous and synchronization, otherwise you may cause yourself
performance issues.
lock = threading.RLock()
class Mouse(object):
@raise_event(lock=lock)
def click(self, x, y):
return (x, y)
top
How to control the execution time of an event or handler
The execution time allocated for an event or handler can be controlled by
setting the timeout argument of
raise_event or set_handler,
to the desired limit (positive integer or float).
If the limit is reached before the execution ends, a RuntimeError is
raised and stored in the execution result of the corresponding event or handler.
Please note that a new thread is created for each method that has a restricted
execution interval.
class Mouse(object):
def __init__(self):
self.on_click()
self.long_on_click()
self.err_on_click()
@raise_event(timeout=1)
def click(self, x, y):
return (x, y)
@set_handler('click', timeout=1)
def on_click(self, x, y):
return (x, y)
@set_handler('click', timeout=1)
def long_on_click(self, x, y):
sleep(2)
return (x, y)
@set_handler('click', timeout=1)
def err_on_click(self, x, y):
raise TypeError('Wrong values')
mouse = Mouse()
pprint(mouse.click(10, 20))
>>((True, (10, 20), <class '__main__.Mouse'>, <function click at 0x00BCA030>),
>> ((False,
>> (<type 'exceptions.TypeError'>,
>> TypeError('Wrong values',),
>> <traceback object at 0x00BCB1E8>),
>> <class '__main__.Mouse'>,
>> <function err_on_click at 0x00BCA1B0>),
>> (True,
>> (10, 20),
>> <class '__main__.Mouse'>,
>> <function on_click at 0x00BCA0B0>),
>> (False,
>> (<type 'exceptions.RuntimeError'>,
>> RuntimeError('[Thread-6] Execution was forcefully terminated',),
>> <traceback object at 0x00BCB288>),
>> <class '__main__.Mouse'>,
>> <function long_on_click at 0x00BCA130>)))
top
How to control the number of active parallel executions
To control the number of methods that can be executed in parallel at one time,
you may use method decovent.active(value). By default,
maximum 3 methods are allowed to run in parallel.
decovent.active(5) # max. 5 methods are executed in parallel
top
Internals
Classes
- raise_event
- constructor parameters
- event - event to be raised. Optional, defaults to the decorated method name.
- memoize_ - caches the event execution result. Optional, defaults to False.
- lock - synchronizes execution of the event and registered handlers,
must be a threading.RLock(), if provided.
Optional, defaults to None.
- timeout - restricts the time allocated for execution.
Optional, defaults to None.
- set_handler
- constructor parameters
- event - event name, required.
- class_ - event owner class. Optional, defaults to None (assumes the current class).
- unregister - if True the handler is unregistered after its first execution. Optional, defaults to False.
- memoize_ - caches the handler execution result. Optional, defaults to False.
- timeout - restricts the time allocated for execution.
Optional, defaults to None.
- spawn_thread
- constructor parameters
- target - target method
- args - arguments passed to target at run-time
- kwargs - arguments passed to target at run-time
- default - result returned if the thread is interrupted
top
Attributes
- decovent.asynchronous
- if True, handlers are executed asynchronous. Default is False.
- decovent.debug
- if True, logging is active. Logger's name is
'decovent'. Default is False.
- decovent.encoding
- encoding used for event name encoding.
Default is 'UTF-8'.
- decovent.errors
- encoding errors mode. Default is strict.
- decovent.exc_info
- if True, sys.exc_info()[:2] is available on error,
for event execution and synchronous handler execution.
Default is False.
- decovent.memoize
- activates memoization at global level. Default is False.
- decovent.threads
- maximum number of threads that must be started
for handlers execution. The actual number of started threads is the
smallest value comparing decovent.threads and handlers count.
Default is 3.
- decovent.traceback
- if True, the traceback (complete sys.exc_info()) will be provided
in the execution result in case of error. Default is False.
decovent.exc_info must be True
in order to receive any error details.
top
Execution model
- events are always executed synchronous in the current thread
- handlers are always executed in parallel threads and may be
executed asynchronous. By default the execution is synchronous.
top
Execution result
- the result of the event and handlers execution is returned as a
tuple (e_result, h_result) when the event is being raised
- e_result is a tuple that contains:
- on success: (True, result, class, method)
- on error: (False, error, class, method)
- h_result is a tuple that contains, for each handler:
- if execution is synchronous
- on success: (True, result, class, method)
- on error: (False, error, class, method)
- if execution is asynchronous
- (None, None, class, method)
class Mouse(object):
def __init__(self):
self.on_click()
self.long_on_click()
self.err_on_click()
@raise_event(timeout=1)
def click(self, x, y):
return (x, y)
@set_handler('click', timeout=1)
def on_click(self, x, y):
return (x, y)
@set_handler('click', timeout=1)
def long_on_click(self, x, y):
sleep(2)
return (x, y)
@set_handler('click', timeout=1)
def err_on_click(self, x, y):
raise TypeError('Wrong values')
mouse = Mouse()
pprint(mouse.click(10, 20))
>>((True, (10, 20), <class '__main__.Mouse'>, <function click at 0x00BCA030>),
>> ((False,
>> (<type 'exceptions.TypeError'>,
>> TypeError('Wrong values',),
>> <traceback object at 0x00BCB1E8>),
>> <class '__main__.Mouse'>,
>> <function err_on_click at 0x00BCA1B0>),
>> (True,
>> (10, 20),
>> <class '__main__.Mouse'>,
>> <function on_click at 0x00BCA0B0>),
>> (False,
>> (<type 'exceptions.RuntimeError'>,
>> RuntimeError('[Thread-6] Execution was forcefully terminated',),
>> <traceback object at 0x00BCB288>),
>> <class '__main__.Mouse'>,
>> <function long_on_click at 0x00BCA130>)))
top
Helpers
- decovent.active(value)
- using this method, you can control the number of methods that
are executed in parallel at one time. By default, maximum 3
methods can be executed at one time.
- decovent.reset(class_=None,
event=None)
- using this method, handlers can be unregistered at the following levels
- if class_ and event
are provided, all registered handlers for this pair are unregistered
- if only class_ is provided, all registered
handlers for this class are unregistered
- if no arguments are provided, all registered handlers for any class are unregistered
- Note that a class not an
instance has to be provided
top
Storage model
- events are stored in a thread-local dictionary, decovent._local.events
- the following structure is used to store the events:
events = {
hash:((event, handler_class, handler, unregister, memoize, timeout),
(event, handler_class, handler, unregister, memoize, timeout),
...),
hash:((event, handler_class, handler, unregister, memoize, timeout),
(event, handler_class, handler, unregister, memoize, timeout),
...),
...}
- if memoization is active at any level, the following decovent._memoize structure is used as cache:
memoize = {
hash: ((args, kwargs, result),
(args, kwargs, result),
...),
hash: ((args, kwargs, result),
(args, kwargs, result),
...),
...}
top