Source code for contracts.interface

# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod
import sys

from .metaclass import with_metaclass



[docs]class Where(object): """ An object of this class represents a place in a file, or an interval. All parsed elements contain a reference to a :py:class:`Where` object so that we can output pretty error messages. Character should be >= len(string) (possibly outside the string). Character_end should be >= character (so that you can splice with string[character:character_end]) """ def __init__(self, string, character, character_end=None): from contracts.utils import raise_desc if not isinstance(string, str): raise ValueError('I expect the string to be a str, not %r' % string) if not (0 <= character <= len(string)): msg = ('Invalid character loc %s for string of len %s.' % (character, len(string))) raise_desc(ValueError, msg, string=string.__repr__()) # Advance pointer if whitespace # if False: # while string[character] == ' ': # if character_end is not None: # assert character <= character_end # if (character < (len(string) - 2)) and ((character_end is None) # or (character <= character_end - 1)): # character += 1 # else: # break self.line, self.col = line_and_col(character, string) if character_end is not None: if not (0 <= character_end <= len(string)): msg = ('Invalid character_end loc %s for string of len %s.'% (character_end, len(string))) raise_desc(ValueError, msg, string=string.__repr__()) if not (character_end >= character): msg= 'Invalid interval [%d:%d]' % (character, character_end) raise ValueError(msg) self.line_end, self.col_end = line_and_col(character_end, string) else: self.line_end, self.col_end = None, None self.string = string self.character = character self.character_end = character_end self.filename = None
[docs] def get_substring(self): """ Returns the substring to which we refer. Raises error if character_end is None """ from contracts.utils import raise_desc if self.character_end is None: msg = 'Character end is None' raise_desc(ValueError, msg, where=self) return self.string[self.character:self.character_end]
def __repr__(self): if self.character_end is not None: part = self.string[self.character:self.character_end] return 'Where(%r)' % part else: return 'Where(s=...,char=%s-%s,line=%s,col=%s)' % (self.character, self.character_end, self.line, self.col)
[docs] def with_filename(self, filename): if self.character is not None: w2 = Where(string=self.string, character=self.character, character_end=self.character_end) else: w2 = Where(string=self.string, line=self.line, column=self.col) w2.filename = filename return w2
def __str__(self): return format_where(self)
# mark = 'here or nearby'
[docs]def format_where(w, context_before=3, mark=None, arrow=True, use_unicode=True, no_mark_arrow_if_longer_than=3): s = '' if w.filename: s += 'In file %r:\n' % w.filename lines = w.string.split('\n') start = max(0, w.line - context_before) pattern = 'line %2d |' i = 0 maxi = i + 1 assert 0 <= w.line < len(lines), (w.character, w.line, w.string.__repr__()) # skip only initial empty lines - if one was written do not skip one_written = False for i in range(start, w.line + 1): # suppress empty lines if one_written or lines[i].strip(): s += ("%s%s\n" % (pattern % (i+1), lines[i])) one_written = True fill = len(pattern % maxi) # select the space before the string in the same column char0 = location(w.line, 0, w.string) # from col 0 char0_end = location(w.line, w.col, w.string) # to w.col space_before = Where(w.string, char0, char0_end) nindent = printable_length_where(space_before) space = ' ' * fill + ' ' * nindent if w.col_end is not None: if w.line == w.line_end: num_highlight = printable_length_where(w) s += space + '~' * num_highlight + '\n' space += ' ' * (num_highlight/2) else: # cannot highlight if on different lines num_highlight = None pass else: num_highlight = None # Do not add the arrow and the mark if we have a long underline string disable_mark_arrow = (num_highlight is not None) and (no_mark_arrow_if_longer_than <num_highlight) if not disable_mark_arrow: if arrow: if use_unicode: s += space + '↑\n' else: s += space + '^\n' s += space + '|\n' if mark is not None: s += space + mark s = s.rstrip() # from .utils import indent # s +='\n' + indent(w.string, '> ') return s
[docs]def printable_length_where(w): """ Returns the printable length of the substring """ if sys.version_info[0] >= 3: # pragma: no cover stype = str else: stype = unicode sub = w.string[w.character:w.character_end] # return len(stype(sub, 'utf-8')) # I am not really sure this is what we want return len(stype(sub))
[docs]def line_and_col(loc, strg): """Returns (line, col), both 0 based.""" from .utils import check_isinstance check_isinstance(loc, int) check_isinstance(strg, str) # first find the line lines = strg.split('\n') if loc == len(strg): # Special case: we mark the end of the string last_line = len(lines) - 1 last_char = len(lines[-1]) return last_line, last_char if loc > len(strg): msg = ('Invalid loc = %d for s of len %d (%r)' % (loc, len(strg), strg)) raise ValueError(msg) res_line = 0 l = loc while True: if not lines: assert loc == 0, (loc, strg.__repr__()) break first = lines[0] if l >= len(first) + len('\n'): lines = lines[1:] l -= (len(first) + len('\n')) res_line += 1 else: break res_col = l inverse = location(res_line, res_col, strg) if inverse != loc: msg = 'Could not find line and col' from .utils import raise_desc raise_desc(AssertionError, msg, s=strg, loc=loc, res_line=res_line, res_col=res_col, loc_recon=inverse) return (res_line, res_col)
[docs]def location(line, col, s): from .utils import check_isinstance check_isinstance(line, int) check_isinstance(col, int) check_isinstance(s, str) lines = s.split('\n') previous_lines = sum(len(l) + len('\n') for l in lines[:line]) offset = col return previous_lines + offset
[docs]def add_prefix(s, prefix): result = "" for l in s.split('\n'): result += prefix + l + '\n' # chop last newline result = result[:-1] return result
[docs]class ContractException(Exception): """ The base class for the exceptions thrown by this module. """
[docs]class MissingContract(ContractException): pass
[docs]class ContractDefinitionError(ContractException): """ Thrown when defining the contracts """
[docs] def copy(self): """ Returns a copy of the exception so we can re-raise it by erasing the stack. """ # print('type is %r' % type(self)) return type(self)(*self.args)
[docs]class ExternalScopedVariableNotFound(ContractDefinitionError): def __init__(self, token): ContractDefinitionError.__init__(self, token) def __str__(self): token = self.get_token() return 'Token not found: %r.' % (token)
[docs] def get_token(self): return self.args[0]
[docs]class CannotDecorateClassmethods(ContractDefinitionError): pass
[docs]class ContractSyntaxError(ContractDefinitionError): """ Exception thrown when there is a syntax error in the contracts. """ def __init__(self, error, where=None): self.error = error self.where = where ContractDefinitionError.__init__(self, error, where) def __str__(self): error, where = self.args s = error if where is not None: s += "\n\n" + add_prefix(where.__str__(), ' ') return s
[docs]class ContractNotRespected(ContractException): """ Exception thrown when a value does not respect a contract. """ def __init__(self, contract, error, value, context): # XXX: solves pickling problem in multiprocess problem, but not the # real solution Exception.__init__(self, contract, error, value, context) assert isinstance(contract, Contract), contract assert isinstance(context, dict), context assert isinstance(error, str), error self.contract = contract self.error = error self.value = value self.context = context self.stack = [] def __str__(self): msg = str(self.error) def context_to_string(context): keys = sorted(context) # don't display these two if are not used for x in ['args', 'kwargs']: if x in keys and not context[x]: keys.remove(x) try: varss = ['- %s: %s' % (k, describe_value(context[k], clip=70)) for k in keys] contexts = "\n".join(varss) except: contexts = '! cannot write context' return contexts align = [] for (contract, context, value) in self.stack: # @UnusedVariable # cons = ("%s %s" % (contract, contexts)).ljust(30) row = ['checking: %s' % contract, 'for value: %s' % describe_value(value, clip=70)] align.append(row) msg += format_table(align, colspacing=3) context0 = self.stack[0][1] if context0: msg += ('\nVariables bound in inner context:\n%s' % context_to_string(context0)) return msg
[docs]def format_table(rows, colspacing=1): sizes = [] for i in range(len(rows[0])): sizes.append(max(len(row[i]) for row in rows)) s = '' for row in rows: s += '\n' for size, cell in zip(sizes, row): s += cell.ljust(size) s += ' ' * colspacing return s
[docs]class RValue(with_metaclass(ABCMeta, object)):
[docs] @abstractmethod def eval(self, context): # @UnusedVariable @ReservedAssignment """ Can raise ValueError; will be wrapped in ContractNotRespected. """
def __eq__(self, other): return (self.__class__ == other.__class__ and self.__repr__() == other.__repr__()) @abstractmethod def __repr__(self): """ Same constraints as :py:func:`Contract.__repr__()`. """ @abstractmethod def __str__(self): """ Same constraints as :py:func:`Contract.__str__()`. """
[docs]def eval_in_context(context, value, contract): assert isinstance(contract, Contract) assert isinstance(value, RValue), describe_value(value) try: return value.eval(context) except ValueError as e: msg = 'Error while evaluating RValue %r: %s' % (value, e) raise ContractNotRespected(contract, msg, value, context)
[docs]class Contract(with_metaclass(ABCMeta, object)): def __init__(self, where): assert ((where is None) or (isinstance(where, Where), 'Wrong type %s' % where)) self.where = where self.enable()
[docs] def enable(self): self._enabled = True
[docs] def disable(self): self._enabled = False
[docs] def enabled(self): return self._enabled
[docs] def check(self, value): """ Checks that the value satisfies this contract. :raise: ContractNotRespected """ return self.check_contract({}, value, silent=False)
[docs] def fail(self, value): """ Checks that the value **does not** respect this contract. Raises an exception if it does. :raise: ValueError """ try: context = self.check(value) except ContractNotRespected: pass else: msg = ('I did not expect that this value would ' 'satisfy this contract.\n') msg += '- value: %s\n' % describe_value(value) msg += '- contract: %s\n' % self msg += '- context: %r' % context raise ValueError(msg)
[docs] @abstractmethod def check_contract(self, context, value, silent): # @UnusedVariable """ Checks that value is ok with this contract in the specific context. This is the function that subclasses must implement. If silent = False, do not bother with creating detailed error messages yet. This is for performance optimization. :param context: The context in which expressions are evaluated. :type context: """
def _check_contract(self, context, value, silent): """ Recursively checks the contracts; it calls check_contract, but the error is wrapped recursively. This is the function that subclasses must call when checking their sub-contracts. """ if not self._enabled: return variables = context.copy() try: self.check_contract(context, value, silent) except ContractNotRespected as e: e.stack.append((self, variables, value)) raise
[docs] @abstractmethod def __repr__(self): """ Returns a string representation of a contract that can be evaluated by Python's :py:func:`eval()`. It must hold that: ``eval(contract.__repr__()) == contract``. This is checked in the unit-tests. Example: >>> from contracts import parse >>> contract = parse('list[N]') >>> contract.__repr__() "List(BindVariable('N',int),None)" All the symbols you need to eval() the expression are in :py:mod:`contracts.library`. >>> from contracts.library import * >>> contract == eval("%r"%contract) True """
[docs] @abstractmethod def __str__(self): """ Returns a string representation of a contract that can be reparsed by :py:func:`contracts.parse()`. It must hold that: ``parse(str(contract)) == contract``. This is checked in the unit-tests. Example: >>> from contracts import parse >>> spec = 'list[N]' >>> contract = parse(spec) >>> contract List(BindVariable('N',int),None) >>> str(contract) == spec True The expressions generated by :py:func:`Contract.__str__` will be exactly the same as what was parsed (this is checked in the unittests as well) if and only if the expression is "minimal". If it isn't (there is whitespace or redundant symbols), the returned expression will be an equivalent minimal one. Example with extra parenthesis and whitespace: >>> from contracts import parse >>> verbose_spec = 'list[((N))]( int, > 0)' >>> contract = parse(verbose_spec) >>> str(contract) 'list[N](int,>0)' Example that removes extra parentheses around arithmetic operators: >>> verbose_spec = '=1+(1*2)+(2+4)' >>> str(parse(verbose_spec)) '=1+1*2+2+4' This is an example with logical operators precedence. The AND operator ``,`` (comma) has more precedence than the OR (``|``). >>> verbose_spec = '(a|(b,c)),e' >>> str(parse(verbose_spec)) '(a|b,c),e' Not that only the outer parenthesis is kept as it is the only one needed. """
def __eq__(self, other): return (self.__class__ == other.__class__ and self.__repr__() == other.__repr__())
inPy2 = sys.version_info[0] == 2 if inPy2: from types import ClassType
[docs]def clipped_repr(x, clip): s = "{0!r}".format(x) if len(s) > clip: clip_tag = '... [clip]' cut = clip - len(clip_tag) s = "%s%s" % (s[:cut], clip_tag) return s
# TODO: add checks for these functions
[docs]def remove_newlines(s): return s.replace('\n', ' ')
[docs]def describe_type(x): """ Returns a friendly description of the type of x. """ if inPy2 and isinstance(x, ClassType): class_name = '(old-style class) %s' % x else: if hasattr(x, '__class__'): c = x.__class__ if hasattr(x, '__name__'): class_name = '%s' % c.__name__ else: class_name = str(c) else: # for extension classes (spmatrix) class_name = str(type(x)) return class_name
[docs]def describe_value(x, clip=80): """ Describes an object, for use in the error messages. Short description, no multiline. """ if hasattr(x, 'shape') and hasattr(x, 'dtype'): shape_desc = 'x'.join(str(i) for i in x.shape) desc = 'array[%r](%s) ' % (shape_desc, x.dtype) final = desc + clipped_repr(x, clip - len(desc)) return remove_newlines(final) else: class_name = describe_type(x) desc = 'Instance of %s: ' % class_name final = desc + clipped_repr(x, clip - len(desc)) return remove_newlines(final)
[docs]def describe_value_multiline(x): """ Describes an object, for use in the error messages. """ if hasattr(x, 'shape') and hasattr(x, 'dtype'): # XXX this fails for bs4, Tag shape_desc = 'x'.join(str(i) for i in x.shape) desc = 'array[%r](%s) ' % (shape_desc, x.dtype) final = desc + '\n' + x.__repr__() return final else: if isinstance(x, str): if x == '': return "''" return x # XXX: this does not represent strings # if '\n' in x: # # long multiline # return x # else: # # short string # return x.__repr__() else: class_name = describe_type(x) # TODO: add all types desc = 'Instance of %s.' % class_name try: # This fails for classes final = desc + '\n' + x.__repr__() except: final = desc + '\n' + str(x) return final