Skip to content

Instantly share code, notes, and snippets.

@DipSwitch
Last active August 29, 2015 14:05
Show Gist options
  • Save DipSwitch/3a052a59c83220c41092 to your computer and use it in GitHub Desktop.
Save DipSwitch/3a052a59c83220c41092 to your computer and use it in GitHub Desktop.
Python Event object With Signature Checking on Handler Addition
"""
The Event logic.
Including short unit test.
Why did I make this? Because I don't want to try and trigger
events which are almost never called. And now at leased the assigned
handlers will be raising an exception directly when they are assigned
with the wrong signature (which is at the start of the application most
of the times) instead of when the Event occurs.
Copyleft: Nick van IJzendoorn 2014
"""
import inspect
# get the function signature of the given function
def getSignature(func):
if func:
return "%s(%s)" % (func.__name__, ', '.join([ a for a in inspect.getargspec(func).args ]) )
else:
return None
class EventException(Exception):
"""The EventException object is thrown when the caller or the handler does not
comply with the given signature while constructing the Event."""
def __init__(self, got, require, argCount=False, invalidArg=False, invalidType=False, eventHandler=None, event=None):
self._got = got
self._require = require
self._invalidArgumentCount = argCount
self._invalidArguments = invalidArg
self._invalidArgumentTypes = invalidType
self._eventHandler = getSignature(eventHandler)
self._event = str(event)
self._buildMessage()
# build a readable error message
def _buildMessage(self):
if self._invalidArgumentCount:
self._message = "event handler must have %d arguments, has %d" % (
self._got, self._require )
elif self._invalidArguments:
self._message = "wrong event argument names: '%s' require prototype: %s" % (
self._got, self._require )
elif self._invalidArgumentTypes:
self._message = "wrong event argument types: '%s' require call: %s" % (
self._got, self._require )
else:
raise Exception("EventException must be a argumentCount or invalidArguments exception")
if self._eventHandler:
self._message += " handle signature: %s" % self._eventHandler
if self._event:
self._message += " event signature: %s" % self._event
@property
def message(self):
"""Get the message of the exception"""
return self._message
@property
def got(self):
"""Get the signature of the caller or the handler
(depends on when the exception is thrown)"""
return self._got
@property
def require(self):
"""Get the required signature of the Event
(depends on when the exception is thrown)"""
return self._require
@property
def invalidArgumentCount(self):
"""Is True if this exception is thrown because of an invalid
argument count of the caller or the handler."""
return self._invalidArgumentCount
@property
def invalidArguments(self):
"""Returns the names of the invalid argument names of aigther the caller or the handler."""
return self._invalidArguments
@property
def invalidArgumentType(self):
"""Returns the names of the invalid arguments types of the caller."""
return self._invalidArgumentTypes
def __str__(self):
return self._message
class Event(object):
"""This event class can be used for global and Object Event generation.
The idea is to create an Event object in one of the ways described below.
And if we use some form of checking on the Event the Event can also be
placed in `notifyOnChange` mode, with this you can always call the Event
and and let the Event object decide if the arguments have been changed
since the last call. Only if the arguments are changed we will call all
the event handlers assigned.
- Unchecked Events:
When no arguments are given the Event object it will not check the
handlers and calls made. When function signatures are wrong the
python will throw an exception if it can't call the function.
UncheckedEvent = Event()
- Type Check Event:
When only values are given to the constructor, the Event object
can only check the argument count of the hooked handlers and for
the caller it will validate the argument count and the if the argument
types are correct.
TypeCheckEvent = Event(bool, bool, int, str)
- Name and Type Check Event:
When we give all the parameters to the Event constructor in key
value pairs. With the name of the argument as key and the type of the
argument as value. We are able to fully validate the the hooked handlers
when they are assigned. Also the caller will be checked if he's using
proper argument names and assigned values to them.
NameCheckEvent = Event(success=bool, transfersMade=int, peripheralIdentifier=str)
!! Note: I can't check if the arguments have the proper type on the caller yet, this
due to being to lazy and write a sorted list in python. Cheers.
But what if you want to add validation to an Event() with no arguments?
NoArgumentEvent = Event(None)
Output of unit test below:
$ python event.py
Testing Event():
add 'noArg()': ok
add 'objNoArg(self)': ok
add 'good(a, b, c, d)': ok
add 'objGood(self, a, b, c, d)': ok
add 'fault(a, b, c)': ok
add 'objFault(self, a, b, c)': ok
add 'fault2(a, b, c, e)': ok
add 'objFault2(self, a, b, c, e)': ok
Testing Event(None):
add 'noArg()': ok
add 'objNoArg(self)': ok
add 'good(a, b, c, d)': EventException: event handler must have 0 arguments, has 4 handle signature: good(a, b, c, d) event signature: None
add 'objGood(self, a, b, c, d)': EventException: event handler must have 0 arguments, has 4 handle signature: objGood(self, a, b, c, d) event signature: None
add 'fault(a, b, c)': EventException: event handler must have 0 arguments, has 3 handle signature: fault(a, b, c) event signature: None
add 'objFault(self, a, b, c)': EventException: event handler must have 0 arguments, has 3 handle signature: objFault(self, a, b, c) event signature: None
add 'fault2(a, b, c, e)': EventException: event handler must have 0 arguments, has 4 handle signature: fault2(a, b, c, e) event signature: None
add 'objFault2(self, a, b, c, e)': EventException: event handler must have 0 arguments, has 4 handle signature: objFault2(self, a, b, c, e) event signature: None
Testing Event(int, int, str, bool):
add 'noArg()': EventException: event handler must have 4 arguments, has 0 handle signature: noArg() event signature: None
add 'objNoArg(self)': EventException: event handler must have 4 arguments, has 0 handle signature: objNoArg(self) event signature: None
add 'good(a, b, c, d)': ok
add 'objGood(self, a, b, c, d)': ok
add 'fault(a, b, c)': EventException: event handler must have 4 arguments, has 3 handle signature: fault(a, b, c) event signature: None
add 'objFault(self, a, b, c)': EventException: event handler must have 4 arguments, has 3 handle signature: objFault(self, a, b, c) event signature: None
add 'fault2(a, b, c, e)': ok
add 'objFault2(self, a, b, c, e)': ok
Testing Event(a=int, c=str, b=int, d=bool):
add 'noArg()': EventException: event handler must have 4 arguments, has 0 handle signature: noArg() event signature: None
add 'objNoArg(self)': EventException: event handler must have 4 arguments, has 0 handle signature: objNoArg(self) event signature: None
add 'good(a, b, c, d)': ok
add 'objGood(self, a, b, c, d)': ok
add 'fault(a, b, c)': EventException: event handler must have 4 arguments, has 3 handle signature: fault(a, b, c) event signature: None
add 'objFault(self, a, b, c)': EventException: event handler must have 4 arguments, has 3 handle signature: objFault(self, a, b, c) event signature: None
add 'fault2(a, b, c, e)': EventException: wrong event argument names: 'e, d' require prototype: Event(a=int, c=str, b=int, d=bool) handle signature: fault2(a, b, c, e) event signature: None
add 'objFault2(self, a, b, c, e)': EventException: wrong event argument names: 'e, d' require prototype: Event(a=int, c=str, b=int, d=bool) handle signature: objFault2(self, a, b, c, e) event signature: None
"""
def __init__(self, *types, **kwargs):
self._force = False
self._handlers = []
self._lastValues = None
self._notifyOnChanges = False
self._validateSignature(types, kwargs)
# code for getting the signature of the call made on the constructor
def _validateSignature(self, types, kwargs):
self._mustValidate = types or kwargs
self._types = []
self._argnames = [] if not kwargs else kwargs
if kwargs:
self._argnames = kwargs.keys()
self._types = [ kwargs[n] for n in self._argnames ]
elif types:
if types[0] == None:
self._types = []
else:
self._types = types
# -------------------- Event Properties to change the operational modes
@property
def force(self):
"""Returns True if the Event will call all it's handlers on the next call."""
return self._force
@force.setter
def force(self, value):
"""Force calling all EventHandlers on the next call from the caller.
Even if there are no changes in the arguments provided by the caller.
This value is automatically reset after the call has ocured."""
self._force = value
@property
def notifyOnChanges(self):
"""Returns True when the Event can be called always but will only call the handlers
if there are changes in the callers arguments."""
return self._notifyOnChanges
@notifyOnChanges.setter
def notifyOnChanges(self, value):
"""Set to True if you want the Event object to only call the handlers if there
are changes in the callers given arguments. Or when the force flag is set to True.
If the Event signature is Event(None) and this mode is enabled, the handlers will only
be called when the force flag is set."""
if not self._mustValidate:
raise Exception("can't use notifyOnChanges when call signature isn't validated")
self._notifyOnChanges = value
self._lastValues = None
# -------------------- EventHandler Add and Remove and validation logic
def __iadd__(self, handler):
"""Try to add a handler to the EventHandler list"""
self._validateHandler(handler)
self._handlers.append(handler)
return self
def _validateHandler(self, handler):
if not self._mustValidate:
return
# get handler signature
args = inspect.getargspec(handler).args
if args and args[0] == 'self':
del(args[0])
# check if argument count is the same
if len(args) != len(self._types):
raise EventException(len(self._types), len(args), argCount=True, eventHandler=handler)
# if a dictionary is used, check all the argument names against the constructor signature
if self._argnames:
wrongNames = [ a for a in args if a not in self._argnames ] + [ a for a in self._argnames if a not in args ]
if wrongNames:
raise EventException(', '.join(wrongNames), str(self), invalidArg=True, eventHandler=handler)
def __isub__(self, handler):
"""Try to remove a handler from the EventHandler list"""
self._handlers.remove(handler)
return self
# -------------------- Event caller validation, change monitor and propogation logic
def __call__(self, *args, **keywargs):
"""When this object is called it will call all it's handlers currently hooked"""
self._validateCaller(*args, **keywargs)
if self._mayMayCallHandlers(*args, **keywargs):
self._force = False
if keywargs:
self._lastValues = keywargs
elif args:
self._lastValues = args
for handler in self._handlers:
handler(*args, **keywargs)
def _validateCaller(self, *args, **kwargs):
if not self._mustValidate:
return
if kwargs:
# a 'dictionary' is used, check all the argument names and values types against the constructor signature
wrongNames = [ b for a, b in zip(self._argnames, kwargs.keys()) if a != b ]
if wrongNames:
raise EventException(', '.join(wrongNames), str(self), invalidArg=True, event=self)
wrongTypes = []
for key in kwargs.keys():
idx = self._argnames.index(key)
if type(kwargs[key]) != self._types[idx] and kwargs[key] is not None:
wrongTypes.append("%s=%s" % (key, kwargs[key]))
if wrongTypes:
raise EventException(', '.join(wrongTypes), str(self), invalidType=True, event=self)
elif len(args) != len(self._types):
# only check the argument count to the signature
# (TODO add type validation, problem was that lists are not sorted? To long ago)
raise EventException(len(args), len(self._types), argCount=True, event=self)
def _mayCallHandlers(self, *args, **kwargs):
if not self._notifyOnChanges:
return True
if self._force or self._lastValues is None:
return True
elif kwargs:
return set(self._lastValues.items()) != set(kwargs.items())
elif args:
return set(self._lastValues) != set(args)
def __len__(self):
"""Get the count of handlers in the event list"""
return len(self._handlers)
def __nonzero__(self):
"""Returns True if there are handlers assigned to the event list"""
return len(self._handlers) > 0
def clear(self, fromObject=None):
"""Remove all handlers from the event list"""
if not fromObject:
self._handlers = []
else:
for hander in self._handlers:
if hander.im_self == fromObject:
self -= handler
def __str__(self):
"""Print the Event signature"""
args = ''
if self._argnames:
args = ', '.join([ "%s=%s" % (n, str(t.__name__)) for n, t in zip(self._argnames, self._types) ])
elif self._types:
args = ', '.join([ str(t.__name__) for t in self._types ])
elif self._mustValidate:
args = 'None'
return "%s(%s)" % (self.__class__.__name__, args)
class EventWrapper(object):
"""Unused... This event wrapper can be used if you want to dynamically add Events and Handlers
But both objects are annonymouse to each other. Not used in a long time so might not even work..."""
def __init__(self):
self._masters = []
def __iadd__(self, handler):
if type(handler) == Event:
self._masters.append(handler)
else:
self._addToMasters(handler)
return self
def __isub__(self, master):
if type(master) == Event:
self._masters.remove(handler)
else:
self._subToMasters(handler)
return self
def _addToMasters(self, handler):
for m in self._masters:
m += handler
def _subToMasters(self, handler):
for m in self._masters:
m -= handler
def clear(self, fromObject=None):
for m in self._masters:
m.clear(fromObject)
# UNIT TEST CODE
class Handler(object):
def objNoArg(self):
pass
def objGood(self, a, b, c, d):
pass
def objFault(self, a, b, c):
pass
def objFault2(self, a, b, c, e):
pass
def noArg():
pass
def good(a, b, c, d):
pass
def fault(a, b, c):
pass
def fault2(a, b, c, e):
pass
def testHandler(event, handler, verbose=False):
if verbose:
print "add '%s': " % getSignature(handler),
try:
event += handler
ret = 'ok'
except EventException as e:
ret = "%s: %s" % (type(e).__name__, e.message)
if verbose:
print ret
return ret
def testEvent(event, handlers):
print "\nTesting %s:" % event
for handler in handlers:
testHandler(event, handler, True)
if __name__ == "__main__":
h = Handler()
handlers = [ noArg, h.objNoArg, good, h.objGood, fault, h.objFault, fault2, h.objFault2 ]
testEvent(Event(), handlers)
testEvent(Event(None), handlers)
testEvent(Event(int, int, str, bool), handlers)
testEvent(Event(a=int, b=int, c=str, d=bool), handlers)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment