"""
constraints provides several facilities:
* The abstract class generator :class:`Constraints`
* 3 Design by contract style condition classes:
* :class:`Precondition`
* :class:`Postcondition`
* :class:`Invariant`
Constraint classes generated by :class:`Constraints` can validate instance
attribute values when used as descriptors. The condition classes provide
context managers, and both :class:`Precondition` and :class:`Postcondition`
can be used as function decorators.
"""
from sys import _getframe
from abc import ABCMeta
from proxy import Symbol
from decorator import decorator
from inspect import getcallargs
def _frame_value(name):
# We have to break out of all the decorators in order to get the outer scope
frame = _getframe(3)
return frame.f_locals[name]
[docs]class Constraints(ABCMeta):
"""
Metaclass which provides constraint verification for objects. Constraints
are specified as arguments to the __new__ method. Constraints can be either
no argument callables or Symbol expressions.
isinstance(obj, ConstraintsInstance) will return True iff obj satisfies all
constraints.
"""
def __init__(self, *args):
pass
def __new__(self, *args):
return super(Constraints, self).__new__(
self,
"Constraint",
(ConstraintBase,),
{"args":args}
)
def __instancecheck__(self, other):
# We need to replace the placeholder object with other
return all(
not isinstance(arg, Symbol) and arg(other) or \
arg.__evaluate__(other) for arg in self.args
)
[docs]class ConstraintBase(object):
"""Constraint base class. Constraints are usable as descriptors."""
def __init__(self, callable_=None, name=None):
if name is None and isinstance(callable, basestring):
# If someone wants to pass name where callable should be, we are ok with that.
(name, callable_) = (callable_, name)
self.name = name
self.callable = callable_
def __get__(self, obj, type_=None):
return getattr(self, "value", None)
def __set__(self, obj, value):
if isinstance(value, type(self)):
self.value = value
else:
raise AssertionError("Specified value (%s) does not satisfy this"
" constraint" % value)
def __delete__(self, obj):
try:
del self.value
except AttributeError:
pass
@classmethod
def precondition(cls, callable_=None, name=None):
"""
Returns a Precondition instance.
:param callable_: A no argument callable.
:param name: The name of the variable to verify.
:type name: string
.. note::
`callable`, if specified, takes precedence over `name` in situations
where either could be used. If `name` is not specified and `callable`
is a string, `callable` will be used as `name`.
"""
return Precondition(cls, callable_, name)
@classmethod
def postcondition(cls, callable_=None, name=None):
"""
Returns a Postcondition instance.
:param callable_: A no argument callable.
:param name: The name of the variable to verify.
:type name: string
.. note::
`callable`, if specified, takes precedence over `name` in situations
where either could be used. If `name` is not specified and `callable`
is a string, `callable` will be used as `name`.
"""
return Postcondition(cls, callable_, name)
@classmethod
def invariant(cls, callable_=None, name=None):
"""
Returns an Invariant instance.
:param callable_: A no argument callable.
:param name: The name of the variable to verify.
:type name: string
.. note::
`callable`, if specified, takes precedence over `name` in situations
where either could be used. If `name` is not specified and `callable`
is a string, `callable` will be used as `name`.
"""
return Invariant(cls, callable_, name)
[docs]class ConditionBase(object):
"""Base class for design by contract style conditions."""
def __init__(self, constraint, target=None, name=None):
self.constraint = constraint
if name is None and isinstance(target, basestring):
# If someone wants to pass name where callable should be, we are ok with that.
(name, target) = (target, name)
self.target = target
self.name = name
def __call__(self, f):
return decorator(self.decorator, f)
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
[docs]class Precondition(ConditionBase):
"""
Constraint container that verifies a precondition. Usable as a decorator or
context manager.
.. note::
You must specify a string argument name for the precondition or the decorator
will not function properly.
"""
def __enter__(self):
value = (getattr(self, "target", None) or (lambda: _frame_value(self.name)))()
if not isinstance(value, self.constraint):
raise AssertionError("The value (%s) did not meet the specified"
" pre-condition" % value)
[docs] def decorator(self, f, *args, **kwargs):
"""
Precondition decorator, looks at the bound value of the arg with the
key equal to self.name.
"""
arg_values = getcallargs(f, *args, **kwargs)
arg = arg_values[self.name]
if not isinstance(arg, self.constraint):
raise AssertionError("The value (%s) did not meet the specified"
" pre-condition" % arg)
return f(*args, **kwargs)
[docs]class Postcondition(ConditionBase):
"""
Constraint container that verifies a postcondition. Usable as a decorator
or context manager.
.. note::
The decorator constrains the return value of the decorated function. It
ignores the callable and name attributes of the condition if they are present.
"""
def __exit__(self, exc_type, exc_val, exc_tb):
value = (getattr(self, "target", None) or (lambda: _frame_value(self.name)))()
if not isinstance(value, self.constraint):
raise AssertionError("The value (%s) did not meet the specified"
" post-condition" % value)
[docs] def decorator(self, f, *args, **kwargs):
"""
Postcondition decorator, looks at the result of the function call and
verify that it is satisfies the constraint condition.
"""
result = f(*args, **kwargs)
if not isinstance(result, self.constraint):
raise AssertionError("The value (%s) did not meet the specified"
" post-condition" % result)
return result
[docs]class Invariant(ConditionBase):
"""
Constraint container that verifies an invariant condition. Usable as a
context manager.
"""
def __enter__(self):
value = (getattr(self, "target", None) or (lambda: _frame_value(self.name)))()
if not isinstance(value, self.constraint):
raise AssertionError("The value (%s) did not meet the specified"
" invariant condition" % value)
def __exit__(self, exc_type, exc_val, exc_tb):
value = (getattr(self, "target", None) or (lambda: _frame_value(self.name)))()
if not isinstance(value, self.constraint):
raise AssertionError("The value (%s) did not meet the specified"
" invariant condition" % value)
def __call__(self, f):
raise NotImplementedError("Invariant objects do not provide decorator functionality")