Last active
August 29, 2015 14:05
-
-
Save DipSwitch/3a052a59c83220c41092 to your computer and use it in GitHub Desktop.
Python Event object With Signature Checking on Handler Addition
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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