|
Description:
Implements class invariants, pre/postconditions in a way similar to PyDBC, but significantly better.
Source: Text Source
__all__ = ["ContractBase", "ContractViolationError", "InvariantViolationError",
"PreInvariantViolationError", "PostInvariantViolationError",
"PreConditionViolationError", "PostConditionViolationError",
"PreconditionViolationError", "PostconditionViolationError" ]
CONTRACT_CHECKS_ENABLED = True
class ContractViolationError(AssertionError): pass
class InvariantViolationError(ContractViolationError): pass
class PreInvariantViolationError(InvariantViolationError): pass
class PostInvariantViolationError(InvariantViolationError): pass
class PreConditionViolationError(ContractViolationError): pass
PreconditionViolationError = PreConditionViolationError
class PostConditionViolationError(ContractViolationError): pass
PostconditionViolationError = PostConditionViolationError
from types import FunctionType
from sys import hexversion
have_python_24 = hexversion >= 0x2040000
def any(s, f = lambda e: bool(e)):
for e in s:
if f(e):
return True
else:
return False
def none(s, f = lambda e: bool(e)):
return not any(s, f)
def empty(s):
return len(s) == 0
def pick_first(s, f = lambda e: bool(e)):
for e in s:
if f(e):
return e
else:
return None
if not have_python_24:
def reversed(s):
r = list(s)
r.reverse()
return r
def merged_mro(*classes):
"""
Returns list of all classes' bases merged and mro-correctly ordered,
implemented as per http://www.python.org/2.3/mro.html
"""
if any(classes, lambda c: not isinstance(c, type)):
raise TypeError("merged_mro expects all it's parameters to be classes, got %s" %
pick_first(classes, lambda c: not isinstance(c, type)))
def merge(lists):
result = []
lists = [ (list_[0], list_[1:]) for list_ in lists ]
while not empty(lists):
good_head, tail = pick_first(lists, lambda ht1: none(lists, lambda ht2: ht1[0] in ht2[1])) or (None, None)
if good_head is None:
raise TypeError("Cannot create a consistent method resolution "
"order (MRO) for bases %s" %
", ".join([ cls.__name__ for cls in classes ]))
result += [ good_head ]
i = 0
while i < len(lists):
head, tail = lists[i]
if head == good_head:
if empty(tail):
del(lists[i])
else:
lists[i] = ( tail[0], tail[1:] )
i += 1
else:
i += 1
return result
merged = [ cls.mro() for cls in classes ] + [ list(classes) ]
return merge(merged)
class ContractFactory(type):
def _wrap(_method, preinvariant, precondition, postcondition, postinvariant,
_classname, _methodname):
def preinvariant_check(result):
if not result:
raise PreInvariantViolationError(
"Class invariant does not hold before a call to %s.%s"
% (_classname, _methodname))
def precondition_check(result):
if not result:
raise PreConditionViolationError(
"Precondition failed before a call to %s.%s"
% (_classname, _methodname))
def postcondition_check(result):
if not result:
raise PostConditionViolationError(
"Postcondition failed after a call to %s.%s"
% (_classname, _methodname))
def postinvariant_check(result):
if not result:
raise PostInvariantViolationError(
"Class invariant does not hold after a call to %s.%s"
% (_classname, _methodname))
if preinvariant is not None and precondition is not None \
and postcondition is not None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
postinvariant_check(postinvariant(self))
return result
elif preinvariant is not None and precondition is not None \
and postcondition is not None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
return result
elif preinvariant is not None and precondition is not None \
and postcondition is None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
postinvariant_check(postinvariant(self))
return result
elif preinvariant is not None and precondition is not None \
and postcondition is None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
return result
elif preinvariant is not None and precondition is None \
and postcondition is not None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
postinvariant_check(postinvariant(self))
return result
elif preinvariant is not None and precondition is None \
and postcondition is not None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
return result
elif preinvariant is not None and precondition is None \
and postcondition is None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
result = _method(self, *args, **kwargs)
postinvariant_check(postinvariant(self))
return result
elif preinvariant is not None and precondition is None \
and postcondition is None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
preinvariant_check(preinvariant(self))
result = _method(self, *args, **kwargs)
return result
elif preinvariant is None and precondition is not None \
and postcondition is not None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
postinvariant_check(postinvariant(self))
return result
elif preinvariant is None and precondition is not None \
and postcondition is not None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
return result
elif preinvariant is None and precondition is not None \
and postcondition is None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
postinvariant_check(postinvariant(self))
return result
elif preinvariant is None and precondition is not None \
and postcondition is None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
precondition_check(precondition(self, *args, **kwargs))
result = _method(self, *args, **kwargs)
return result
elif preinvariant is None and precondition is None \
and postcondition is not None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
postinvariant_check(postinvariant(self))
return result
elif preinvariant is None and precondition is None \
and postcondition is not None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
result = _method(self, *args, **kwargs)
postcondition_check(postcondition(self, result, *args, **kwargs))
return result
elif preinvariant is None and precondition is None \
and postcondition is None and postinvariant is not None:
def dbc_wrapper(self, *args, **kwargs):
result = _method(self, *args, **kwargs)
postinvariant_check(postinvariant(self))
return result
elif preinvariant is None and precondition is None \
and postcondition is None and postinvariant is None:
def dbc_wrapper(self, *args, **kwargs):
result = _method(self, *args, **kwargs)
return result
if have_python_24:
dbc_wrapper.__name__ = _methodname
return dbc_wrapper
_wrap = staticmethod(_wrap)
def __new__(_class, _name, _bases, _dict):
mro = merged_mro(*_bases)
dict_with_bases = {}
for base in reversed(mro):
if hasattr(base, "__dict__"):
dict_with_bases.update(base.__dict__)
dict_with_bases.update(_dict)
try:
invariant = dict_with_bases["invariant"]
except KeyError:
invariant = None
for name, target in dict_with_bases.iteritems():
if isinstance(target, FunctionType) and name != "__del__" and name != "invariant" \
and not name.startswith("pre_") and not name.startswith("post_"):
try:
pre = dict_with_bases["pre_%s" % name]
except KeyError:
pre = None
try:
post = dict_with_bases["post_%s" % name]
except KeyError:
post = None
_dict[name] = ContractFactory._wrap(target,
name != "__init__" and invariant or None,
pre or None, post or None, invariant or None,
_name, name)
return super(ContractFactory, _class).__new__(_class, _name, _bases, _dict)
class ContractBase(object):
if CONTRACT_CHECKS_ENABLED:
__metaclass__ = ContractFactory
Discussion:
Why implementing DBC ? Because it's very useful.
Why another implementation ? Let's compare this implementation to PyDBC.
This implementation is better than PyDBC:
* It provides natural inheritance for all the DBC special methods so that the DBC constraints span class hierachies.
* It's DBC methods follow different call convention - they return True/False rather than throwing AssertionError's. The errors are thus reported in a consistent way, rather than forcing the developer to come up with a different message each time. The DBC exceptions being thrown form the hierarchy identical to PEP 316's, rather than ad-hoc.
* Postcondition guarding methods take not just the return value of the method being guarded, but the copy of it's parameters as well (although this is minor).
This implementation is different from PyDBC (better or worse to one's taste):
* To equip a class with DBC you must inherit from ContractBase, rather than set a metaclass (yet it's the same under the hood).
* Method naming is different: invariant, pre_foo and post_foo rather than __invar, foo__pre and foo__post.
* It doesn't decorate __setattr__ as the PyDBC does, so that direct assignments to instance variables could break the class invariant, but this is an intentional decision. For one, DBC is an interface, not syntax feature. For two, with Python anything can be bypassed. For three, __setattr__ could be used in a miriads of ways and it'd be bad if DBC gets in the way.
As to the PEP 316 which offers DBC for Python - I actively refuse to accept it, with it's docstrings code embedding leading to inheritance weirdness, although it appears to be better in that it follows the preweak/poststrengthen rule.
|