Source code for sure.core

# -*- coding: utf-8 -*-
# <sure - utility belt for automated testing in python>
# Copyright (C) <2010-2023>  Gabriel Falcão <gabriel@nacaolivre.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals

import os

try:
    from mock import _CallList
except ImportError:
    from mock.mock import _CallList

import inspect
from six import (
    text_type, integer_types, string_types, binary_type,
    get_function_code
)

from sure.terminal import red, green, yellow
from sure.compat import safe_repr, OrderedDict


[docs]class Anything(object): """Represents any possible value. Its existence is solely for idiomatic purposes. """ def __eq__(self, _): return True
anything = Anything()
[docs]class DeepExplanation(text_type): def get_header(self, X, Y, suffix): params = (safe_repr(X), safe_repr(Y), text_type(suffix)) header = "given\nX = {0}\n and\nY = {1}\n{2}".format(*params) return yellow(header).strip() def get_assertion(self, X, Y): return AssertionError(self.get_header(X, Y, self)) def as_assertion(self, X, Y): raise self.get_assertion(X, Y)
class DeepComparison(object): def __init__(self, X, Y, epsilon=None, parent=None): self.complex_cmp_funcs = { float: self.compare_floats, dict: self.compare_dicts, list: self.compare_iterables, tuple: self.compare_iterables, OrderedDict: self.compare_ordereddict } self.operands = X, Y self.epsilon = epsilon self.parent = parent self._context = None def is_simple(self, obj): return isinstance(obj, ( string_types, integer_types, binary_type, Anything )) def is_complex(self, obj): return isinstance(obj, tuple(self.complex_cmp_funcs.keys())) def compare_complex_stuff(self, X, Y): return self.complex_cmp_funcs.get(type(X), self.compare_generic)(X, Y) def compare_generic(self, X, Y, msg_format='X{0} != Y{1}'): c = self.get_context() if X == Y: return True else: m = msg_format.format(red(c.current_X_keys), green(c.current_Y_keys)) return DeepExplanation(m) def compare_floats(self, X, Y): c = self.get_context() if self.epsilon is None: return self.compare_generic(X, Y) if abs(X - Y) <= self.epsilon: return True else: m = 'X{0}±{1} != Y{2}±{3}'.format( red(c.current_X_keys), self.epsilon, green(c.current_Y_keys), self.epsilon) return DeepExplanation(m) def compare_dicts(self, X, Y): c = self.get_context() x_keys = list(X.keys()) y_keys = list(Y.keys()) diff_x = list(set(x_keys).difference(set(y_keys))) diff_y = list(set(y_keys).difference(set(x_keys))) if diff_x: msg = "X{0} has the key {1!r} whereas Y{2} does not".format( red(c.current_X_keys), safe_repr(diff_x[0]), green(c.current_Y_keys)) return DeepExplanation(msg) elif diff_y: msg = "X{0} does not have the key {1!r} whereas Y{2} has it".format( red(c.current_X_keys), safe_repr(diff_y[0]), green(c.current_Y_keys)) return DeepExplanation(msg) elif X == Y: return True else: for key_X in x_keys: self.key_X = key_X self.key_Y = key_X value_X = X[key_X] value_Y = Y[key_X] child = DeepComparison( value_X, value_Y, epsilon=self.epsilon, parent=self, ).compare() if isinstance(child, DeepExplanation): return child def compare_ordereddict(self, X, Y): """Compares two instances of an OrderedDict.""" # check if OrderedDict instances have the same keys and values child = self.compare_dicts(X, Y) if isinstance(child, DeepExplanation): return child # check if the order of the keys is the same for i, j in zip(X.items(), Y.items()): if i[0] != j[0]: c = self.get_context() msg = "X{0} and Y{1} are in a different order".format( red(c.current_X_keys), green(c.current_Y_keys) ) return DeepExplanation(msg) return True def get_context(self): if self._context: return self._context X_keys = [] Y_keys = [] comp = self while comp.parent: X_keys.insert(0, comp.parent.key_X) Y_keys.insert(0, comp.parent.key_Y) comp = comp.parent def get_keys(i): if not i: return '' return '[{0}]'.format(']['.join(map(safe_repr, i))) class ComparisonContext: current_X_keys = get_keys(X_keys) current_Y_keys = get_keys(Y_keys) parent = comp self._context = ComparisonContext() return self._context def compare_iterables(self, X, Y): len_X, len_Y = map(len, (X, Y)) if len_X > len_Y: msg = "X has {0} items whereas Y has only {1}".format(len_X, len_Y) return DeepExplanation(msg) elif len_X < len_Y: msg = "Y has {0} items whereas X has only {1}".format(len_Y, len_X) return DeepExplanation(msg) elif X == Y: return True else: for i, (value_X, value_Y) in enumerate(zip(X, Y)): self.key_X = self.key_Y = i child = DeepComparison( value_X, value_Y, epsilon=self.epsilon, parent=self, ).compare() if isinstance(child, DeepExplanation): return child def compare(self): X, Y = self.operands if isinstance(X, _CallList): X = list(X) if isinstance(Y, _CallList): X = list(Y) c = self.get_context() if self.is_complex(X) and type(X) is type(Y): return self.compare_complex_stuff(X, Y) def safe_format_repr(string): "Escape '{' and '}' in string for use with str.format()" if not isinstance(string, (string_types, binary_type)): return string orig_str_type = type(string) if isinstance(string, binary_type): safe_repr = string.replace(b'{', b'{{').replace(b'}', b'}}') else: safe_repr = string.replace('{', '{{').replace('}', '}}') # NOTE: str.replace() automatically converted the 'string' to 'unicode' in Python 2 return orig_str_type(safe_repr) # get safe representation for X and Y safe_X, safe_Y = safe_format_repr(X), safe_format_repr(Y) # maintaining backwards compatibility between error messages kwargs = {} if self.is_simple(X) and self.is_simple(Y): kwargs['msg_format'] = 'X{{0}} is {0!r} whereas Y{{1}} is {1!r}'.format(safe_X, safe_Y) elif type(X) is not type(Y): kwargs['msg_format'] = 'X{{0}} is a {0} and Y{{1}} is a {1} instead'.format( type(X).__name__, type(Y).__name__) exp = self.compare_generic(X, Y, **kwargs) if isinstance(exp, DeepExplanation): original_X, original_Y = c.parent.operands raise exp.as_assertion(original_X, original_Y) return exp def explanation(self): return self._explanation def _get_file_name(func): try: name = inspect.getfile(func) except AttributeError: name = get_function_code(func).co_filename return os.path.abspath(name) def _get_line_number(func): try: return inspect.getlineno(func) except AttributeError: return get_function_code(func).co_firstlineno def itemize_length(items): length = len(items) return '{0} item{1}'.format(length, length > 1 and "s" or "")