Source code for sure

# -*- 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 re
import os
import sys

import difflib
import inspect
import traceback

from functools import wraps, partial
from datetime import datetime

from six import string_types, text_type, PY2, get_function_code
from six.moves import reduce

from sure.old import AssertionHelper
from sure.old import Iterable
from sure.old import builtins

from sure.core import DeepComparison
from sure.core import DeepExplanation
from sure.core import _get_file_name
from sure.core import _get_line_number
from sure.core import safe_repr
from sure.core import anything  # noqa

from sure.magic import is_cpython, patchable_builtin
from sure.registry import context as _registry


if not PY2:
    basestring = str

version = "1.4.11"


not_here_error = (
    "you have tried to access the attribute %r from the context "
    "(aka VariablesBag), but there is no such attribute assigned to it. "
    "Maybe you misspelled it ? Well, here are the options: %s"
)


original_obj_attrs = dir(object)


[docs]class VariablesBag(dict): __varnames__ = None __sure_actions_ran__ = None __sure_action_results__ = None __sure_providers_of__ = None def __init__(self, *args, **kw): self.__varnames__ = [] self.__sure_actions_ran__ = [] self.__sure_action_results__ = [] self.__sure_providers_of__ = {} return super(VariablesBag, self).__init__(*args, **kw) def __setattr__(self, attr, value): if attr not in dir(VariablesBag): self[attr] = value self.__varnames__.append(attr) return super(VariablesBag, self).__setattr__(attr, value) def __getattr__(self, attr): try: return super(VariablesBag, self).__getattribute__(attr) except AttributeError: if attr not in dir(VariablesBag): raise AssertionError( not_here_error % ( attr, safe_repr(self.__varnames__), ) )
def ensure_type(caller_name, cast, obj): try: return cast(obj) except TypeError: raise AssertionError("{0} tried to ") class CallBack(object): context_error = ( "the function %s defined at %s line %d, is being " "decorated by either @that_with_context or @scenario, so it should " "take at least 1 parameter, which is the test context" ) def __init__(self, cb, args, kwargs): self.callback = cb self.args = args or [] self.kwargs = kwargs or {} self.callback_name = cb.__name__ self.callback_filename = os.path.split(get_function_code(cb).co_filename)[-1] self.callback_lineno = get_function_code(cb).co_firstlineno + 1 def apply(self, *optional_args): args = list(optional_args) args.extend(self.args) try: return self.callback(*args, **self.kwargs) except Exception: exc_klass, exc_value, tb = sys.exc_info() err = traceback.format_exc().splitlines()[-1] err = err.replace("{0}:".format(exc_klass.__name__), "").strip() if err.startswith(self.callback_name) and ( "takes no arguments (1 given)" in err) or \ "takes 0 positional arguments but 1 was given" in err: raise TypeError( self.context_error % ( self.callback_name, self.callback_filename, self.callback_lineno, ) ) raise def that_with_context(setup=None, teardown=None): def dec(func): @wraps(func) def wrap(*args, **kw): context = VariablesBag() if callable(setup): cb = CallBack(setup, args, kw) cb.apply(context) elif isinstance(setup, Iterable): for s in setup: cb = CallBack(s, args, kw) cb.apply(context) test = CallBack(func, args, kw) try: res = test.apply(context) finally: if callable(teardown): cb = CallBack(teardown, args, kw) cb.apply(context) elif isinstance(teardown, Iterable): for s in teardown: cb = CallBack(s, args, kw) cb.apply(context) return res return wrap return dec scenario = that_with_context def within(**units): assert len(units) == 1, "use within(number=unit). e.g.: within(one=second)" word, unit = list(units.items())[0] value = word_to_number(word) convert_from, convert_to = UNITS[unit] timeout = convert_from(value) exc = [] def dec(func): def wrap(*args, **kw): start = datetime.utcnow() try: func(start, *args, **kw) except TypeError as e: if PY2: # PY2 has different error message fmt = "{0}() takes no arguments" else: fmt = "{0}() takes 0 positional arguments but 1 was given" err = text_type(e) if fmt.format(func.__name__) in err: func(*args, **kw) else: exc.append(traceback.format_exc()) except Exception as e: exc.append(traceback.format_exc()) end = datetime.utcnow() delta = end - start took = convert_to(delta.microseconds) print(took, timeout) assert took < timeout, "%s did not run within %s %s" % ( func.__name__, word, unit, ) if exc: raise AssertionError(exc.pop(0)) wrap.__name__ = func.__name__ wrap.__doc__ = func.__doc__ wrap.__dict__ = func.__dict__ return wrap return dec UNITS = { "minutes": ( lambda from_num: from_num / 60.0, lambda to_num: to_num * 6000000, ), "seconds": ( lambda from_num: from_num, lambda to_num: to_num / 100000, ), "miliseconds": ( lambda from_num: from_num * 1000, lambda to_num: to_num / 100, ), "microseconds": ( lambda from_num: from_num * 100000, lambda to_num: to_num, ), } milisecond = miliseconds = "miliseconds" microsecond = microseconds = "microseconds" second = seconds = "seconds" minute = minutes = "minutes" def word_to_number(word): basic = { "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12, } try: return basic[word] except KeyError: raise AssertionError( "sure supports only literal numbers from one to twelve, " 'you tried the word "twenty"' ) def action_for(context, provides=None, depends_on=None): if not provides: provides = [] if not depends_on: depends_on = [] def register_providers(func, attr): if re.search(r"^[{]\d+[}]$", attr): return # ignore dynamically declared provides if not attr in context.__sure_providers_of__: context.__sure_providers_of__[attr] = [] context.__sure_providers_of__[attr].append(func) def register_dynamic_providers(func, attr, args, kwargs): found = re.search(r"^[{](\d+)[}]$", attr) if not found: return # ignore dynamically declared provides index = int(found.group(1)) assert index < len(args), ( "the dynamic provider index: {%d} is bigger than %d, which is " "the length of the positional arguments passed to %s" % (index, len(args), func.__name__) ) attr = args[index] if not attr in context.__sure_providers_of__: context.__sure_providers_of__[attr] = [] context.__sure_providers_of__[attr].append(func) def ensure_providers(func, attr, args, kwargs): found = re.search(r"^[{](\d+)[}]$", attr) if found: index = int(found.group(1)) attr = args[index] assert attr in context, ( 'the action "%s" was supposed to provide the attribute "%s" ' "into the context, but it did not. Please double check its " "implementation" % (func.__name__, attr) ) dependency_error_lonely = ( 'the action "%s" defined at %s:%d ' 'depends on the attribute "%s" to be available in the' " context. It turns out that there are no actions providing " "that. Please double-check the implementation" ) dependency_error_hints = ( 'the action "%s" defined at %s:%d ' 'depends on the attribute "%s" to be available in the context.' " You need to call one of the following actions beforehand:\n" ) def check_dependencies(func): action = func.__name__ filename = _get_file_name(func) lineno = _get_line_number(func) for dependency in depends_on: if dependency in context.__sure_providers_of__: providers = context.__sure_providers_of__[dependency] err = dependency_error_hints % ( action, filename, lineno, dependency, ) err += "\n".join( [ " -> %s at %s:%d" % (p.__name__, _get_file_name(p), _get_line_number(p)) for p in providers ] ) else: err = dependency_error_lonely % ( action, filename, lineno, dependency, ) assert dependency in context, err def decorate_and_absorb(func): [register_providers(func, attr) for attr in provides] @wraps(func) def wrapper(*args, **kw): [register_dynamic_providers(func, attr, args, kw) for attr in provides] context.__sure_actions_ran__.append((func, args, kw)) check_dependencies(func) result = func(*args, **kw) [ensure_providers(func, attr, args, kw) for attr in provides] context.__sure_action_results__.append(result) return context setattr(context, func.__name__, wrapper) return wrapper return decorate_and_absorb def work_in_progress(func): @wraps(func) def wrapper(*args, **kwargs): _registry["is_running"] = True ret = func(*args, **kwargs) _registry["is_running"] = False return ret return wrapper def assertionmethod(func): @wraps(func) def wrapper(self, *args, **kw): try: value = func(self, *args, **kw) except AssertionError as e: raise AssertionError(e) msg = "{0}({1}) failed".format( func.__name__, ", ".join(map(safe_repr, args)), ", ".join(["{0}={1}".format(k, safe_repr(kw[k])) for k in kw]), ) if PY2: msg = text_type(msg) assert value, msg return value return wrapper def assertionproperty(func): return builtins.property(assertionmethod(func)) POSITIVES = [ "should", "does", "do", "must", "when", ] NEGATIVES = [ "shouldnt", "dont", "do_not", "doesnt", "does_not", "doesnot", "should_not", "shouldnot", ] class IdentityAssertion(object): def __init__(self, assertion_builder): self._ab = assertion_builder def __call__(self, other): if self._ab.negative: assert ( self._ab.obj is not other ), "{0} should not be the same object as {1}, but it is".format( self._ab.obj, other ) return True assert ( self._ab.obj is other ), "{0} should be the same object as {1}, but it is not".format( self._ab.obj, other ) return True def __getattr__(self, name): return getattr(self._ab, name) class AssertionBuilder(object): def __init__( self, name=None, negative=False, obj=None, callable_args=None, callable_kw=None ): self._name = name self.negative = negative self.obj = obj self._callable_args = callable_args or [] self._callable_kw = callable_kw or {} self._that = AssertionHelper(self.obj) def __call__(self, obj): self.obj = obj if isinstance(obj, self.__class__): self.obj = obj.obj self._callable_args = obj._callable_args self._callable_kw = obj._callable_kw self._that = AssertionHelper(self.obj) return self def __getattr__(self, attr): special_case = False special_case = attr in (POSITIVES + NEGATIVES) negative = attr in NEGATIVES if special_case: return AssertionBuilder( attr, negative=negative, obj=self.obj, callable_args=self._callable_args, callable_kw=self._callable_kw, ) return super(AssertionBuilder, self).__getattribute__(attr) @assertionproperty def callable(self): if self.negative: assert not callable( self.obj ), "expected `{0}` to not be callable but it is".format(safe_repr(self.obj)) else: assert callable(self.obj), "expected {0} to be callable".format( safe_repr(self.obj) ) return True @assertionproperty def be(self): return IdentityAssertion(self) being = be @assertionproperty def not_be(self): return IdentityAssertion(self.should_not) not_being = not_be @assertionproperty def not_have(self): return self.should_not @assertionproperty def to_not(self): return self.should_not @assertionproperty def to(self): return self which = to @assertionproperty def when(self): return self @assertionproperty def have(self): return self @assertionproperty def with_value(self): return self def property(self, name): has_it = hasattr(self.obj, name) if self.negative: assert ( not has_it ), "%r should not have the property `%s`, " "but it is %r" % ( self.obj, name, getattr(self.obj, name), ) return True assert has_it, "%r should have the property `%s` but does not" % ( self.obj, name, ) return expect(getattr(self.obj, name)) def key(self, name): has_it = name in self.obj if self.negative: assert not has_it, "%r should not have the key `%s`, " "but it is %r" % ( self.obj, name, self.obj[name], ) return True assert has_it, "%r should have the key `%s` but does not" % (self.obj, name) return expect(self.obj[name]) @assertionproperty def empty(self): representation = safe_repr(self.obj) length = len(self.obj) if self.negative: assert length > 0, "expected `{0}` to not be empty".format(representation) else: assert ( length == 0 ), "expected `{0}` to be empty but it has {1} items".format( representation, length ) return True @assertionproperty def ok(self): if self.negative: msg = "expected `{0}` to be falsy".format(self.obj) assert not bool(self.obj), msg else: msg = "expected `{0}` to be truthy".format(self.obj) assert bool(self.obj), msg return True truthy = ok true = ok @assertionproperty def falsy(self): if self.negative: msg = "expected `{0}` to be truthy".format(self.obj) assert bool(self.obj), msg else: msg = "expected `{0}` to be falsy".format(self.obj) assert not bool(self.obj), msg return True false = falsy @assertionproperty def none(self): if self.negative: assert self.obj is not None, r"expected `{0}` to not be None".format( self.obj ) else: assert self.obj is None, r"expected `{0}` to be None".format(self.obj) return True @assertionmethod def within_range(self, start, end): start = ensure_type("within_range", int, start) end = ensure_type("within_range", int, end) subject = ensure_type("within_range", int, self.obj) is_within_range = subject >= start and subject <= end if self.negative: if is_within_range: raise AssertionError( "expected {0} to NOT be within {1} and {2}".format( subject, start, end ) ) return not is_within_range else: if not is_within_range: raise AssertionError( "expected {0} to be within {1} and {2}".format(subject, start, end) ) return is_within_range @assertionmethod def within(self, first, *rest): if isinstance(first, Iterable): collection_should = AssertionHelper(first) if self.negative: return collection_should.does_not_contain(self.obj) else: return collection_should.contains(self.obj) elif len(rest) == 1: return self.within_range(first, rest[0]) else: if self.negative: ppath = "{0}.should_not.be.within".format(self.obj) else: ppath = "{0}.should.be.within".format(self.obj) raise AssertionError( ( "{0}({1}, {2}) must be called with either a iterable:\n" "{0}([1, 2, 3, 4])\n" "or with a range of numbers:" "{0}(1, 3000)" ).format(ppath, first, ", ".join([repr(x) for x in rest])) ) @assertionmethod def equal(self, what, epsilon=None): """compares given object ``X`` with an expected ``Y`` object. It primarily assures that the compared objects are absolute equal ``==``. :param what: the expected value :param epsilon: a delta to leverage upper-bound floating point permissiveness """ try: comparison = DeepComparison(self.obj, what, epsilon).compare() error = False except AssertionError as e: error = e comparison = None if isinstance(comparison, DeepExplanation): error = comparison.get_assertion(self.obj, what) if self.negative: if error: return True msg = "%s should differ from %s, but is the same thing" raise AssertionError(msg % (safe_repr(self.obj), safe_repr(what))) else: if not error: return True raise error eql = equal equals = equal equal_to = equal @assertionmethod def different_of(self, what): differ = difflib.Differ() source = self.obj.strip().splitlines(True) destination = what.strip().splitlines(True) result = differ.compare(source, destination) difference = "".join(result) if self.negative: if self.obj != what: assert not difference, "Difference:\n\n{0}".format(difference) else: if self.obj == what: raise AssertionError( "{0} should be different of {1}".format(self.obj, what) ) return True @assertionmethod def an(self, klass): if isinstance(klass, type): class_name = klass.__name__ elif isinstance(klass, string_types): class_name = klass.strip() else: class_name = text_type(klass) is_vowel = class_name[0] in "aeiou" if isinstance(klass, string_types): if "." in klass: items = klass.split(".") first = items.pop(0) if not items: items = [first] first = "_abcoll" else: if sys.version_info <= (3, 0, 0): first = "__builtin__" else: first = "builtins" items = [klass] klass = reduce(getattr, items, __import__(first)) suffix = is_vowel and "n" or "" if self.negative: assert not isinstance( self.obj, klass ), "expected `{0}` to not be a{1} {2}".format(self.obj, suffix, class_name) else: assert isinstance(self.obj, klass), "expected `{0}` to be a{1} {2}".format( self.obj, suffix, class_name ) return True a = an @assertionmethod def greater_than(self, dest): if self.negative: msg = "expected `{0}` to not be greater than `{1}`".format(self.obj, dest) assert not self.obj > dest, msg else: msg = "expected `{0}` to be greater than `{1}`".format(self.obj, dest) assert self.obj > dest, msg return True @assertionmethod def greater_than_or_equal_to(self, dest): if self.negative: msg = "expected `{0}` to not be greater than or equal to `{1}`".format( self.obj, dest ) assert not self.obj >= dest, msg else: msg = "expected `{0}` to be greater than or equal to `{1}`".format( self.obj, dest ) assert self.obj >= dest, msg return True @assertionmethod def lower_than(self, dest): if self.negative: msg = "expected `{0}` to not be lower than `{1}`".format(self.obj, dest) assert not self.obj < dest, msg else: msg = "expected `{0}` to be lower than `{1}`".format(self.obj, dest) assert self.obj < dest, msg return True @assertionmethod def lower_than_or_equal_to(self, dest): if self.negative: msg = "expected `{0}` to not be lower than or equal to `{1}`".format( self.obj, dest ) assert not self.obj <= dest, msg else: msg = "expected `{0}` to be lower than or equal to `{1}`".format( self.obj, dest ) assert self.obj <= dest, msg return True @assertionmethod def below(self, num): if self.negative: msg = "{0} should not be below {1}".format(self.obj, num) assert not self.obj < num, msg else: msg = "{0} should be below {1}".format(self.obj, num) assert self.obj < num, msg return True @assertionmethod def above(self, num): if self.negative: msg = "{0} should not be above {1}".format(self.obj, num) assert not self.obj > num, msg else: msg = "{0} should be above {1}".format(self.obj, num) assert self.obj > num, msg return True @assertionmethod def length_of(self, num): if self.negative: return self._that.len_is_not(num) return self._that.len_is(num) def called_with(self, *args, **kw): self._callable_args = args self._callable_kw = kw return self called = builtins.property(called_with) @assertionmethod def throw(self, *args, **kw): _that = AssertionHelper( self.obj, with_args=self._callable_args, and_kwargs=self._callable_kw ) if self.negative: msg = ( "{0} called with args {1} and kwargs {2} should " "not raise {3} but raised {4}" ) exc = args and args[0] or Exception try: self.obj(*self._callable_args, **self._callable_kw) return True except Exception as e: err = msg.format( self.obj, self._that._callable_args, self._that._callable_kw, exc, e, ) raise AssertionError(err) return _that.raises(*args, **kw) thrown = throw raised = thrown @assertionmethod def return_value(self, value): return_value = self.obj(*self._callable_args, **self._callable_kw) return this(return_value).should.equal(value) returned_the_value = return_value @assertionmethod def look_like(self, value): if self.negative: try: self._that.looks_like(value) except AssertionError: return True else: msg = "%r should not look like %r but does" raise AssertionError(msg % (self.obj, value)) return self._that.looks_like(value) @assertionmethod def contain(self, what): obj = self.obj if self.negative: return expect(what).to.not_be.within(obj) else: return expect(what).to.be.within(obj) @assertionmethod def match(self, regex, *args): obj_repr = repr(self.obj) assert isinstance( self.obj, basestring ), "{0} should be a string in order to compare using .match()".format(obj_repr) matched = re.search(regex, self.obj, *args) modifiers_map = { re.I: "i", re.L: "l", re.M: "m", re.S: "s", re.U: "u", } modifiers = "".join([modifiers_map.get(x, "") for x in args]) regex_representation = "/{0}/{1}".format(regex, modifiers) if self.negative: assert ( matched is None ), "{0} should not match the regular expression {1}".format( obj_repr, regex_representation ) else: assert ( matched is not None ), "{0} doesn't match the regular expression {1}".format( obj_repr, regex_representation ) return True this = AssertionBuilder("this") the = AssertionBuilder("the") it = AssertionBuilder("it") these = AssertionBuilder("these") those = AssertionBuilder("those") expect = AssertionBuilder("expect")
[docs]def assertion(func): """Extend sure with a custom assertion method.""" func = assertionmethod(func) setattr(AssertionBuilder, func.__name__, func) return func
[docs]def chain(func): """Extend sure with a custom chaining method.""" setattr(AssertionBuilder, func.__name__, func) return func
[docs]def chainproperty(func): """Extend sure with a custom chain property.""" func = assertionproperty(func) setattr(AssertionBuilder, func.fget.__name__, func) return func
[docs]class ensure(object): """ Contextmanager to ensure that the given assertion message is printed upon a raised ``AssertionError`` exception. The ``args`` and ``kwargs`` are used to format the message using ``format()``. """ def __init__(self, msg, *args, **kwargs): self.msg = msg self.args = args self.kwargs = kwargs def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): """ Catch all ``AsertionError`` exceptions and reraise them with the message provided to the context manager. """ if exc_type is not AssertionError: return msg = self.msg.format(*self.args, **self.kwargs) raise AssertionError(msg)
allows_new_syntax = not os.getenv("SURE_DISABLE_NEW_SYNTAX") def do_enable(): def make_safe_property(method, name, should_be_property=True): if not should_be_property: return method(None) def deleter(method, self, *args, **kw): if isinstance(self, type): # if the attribute has to be deleted from a class object # we cannot use ``del self.__dict__[name]`` directly because we cannot # modify a mappingproxy object. Thus, we have to delete it in our # proxy __dict__. overwritten_object_handlers.pop((id(self), method.__name__), None) else: # if the attribute has to be deleted from an instance object # we are able to directly delete it from the object's __dict__. self.__dict__.pop(name, None) def setter(method, self, other): if isinstance(self, type): # if the attribute has to be set to a class object # we cannot use ``self.__dict__[name] = other`` directly because we cannot # modify a mappingproxy object. Thus, we have to set it in our # proxy __dict__. overwritten_object_handlers[(id(self), method.__name__)] = other else: # if the attribute has to be set to an instance object # we are able to directly set it in the object's __dict__. self.__dict__[name] = other return builtins.property( fget=method, fset=partial(setter, method), fdel=partial(deleter, method), ) def build_assertion_property(name, is_negative, prop=True): """Build assertion property This is the assertion property which is usually patched to the built-in ``object`` and ``NoneType``. """ def method(self): # check if the given object already has an attribute with the # given name. If yes return it instead of patching it. try: if name in self.__dict__: return self.__dict__[name] except AttributeError: # we do not have an object with __dict__, thus # it's safe to just continue and patch the `name`. pass overwritten_object_handler = overwritten_object_handlers.get( (id(self), name), None ) if overwritten_object_handler: return overwritten_object_handler builder = AssertionBuilder(name, negative=is_negative) instance = builder(self) callable_args = getattr(self, "_callable_args", ()) if callable_args: instance._callable_args = callable_args callable_kw = getattr(self, "_callable_kw", {}) if callable_kw: instance._callable_kw = callable_kw return instance method.__name__ = str(name) return make_safe_property(method, name, prop) object_handler = patchable_builtin(object) # We have to keep track of all objects which # should overwrite a ``POSITIVES`` or ``NEGATIVES`` # property. If we wouldn't do that in the # make_safe_property.setter method we would loose # the newly assigned object reference. overwritten_object_handlers = {} # None does not have a tp_dict associated to its PyObject, so this # is the only way we could make it work like we expected. none = patchable_builtin(None.__class__) for name in POSITIVES: object_handler[name] = build_assertion_property(name, is_negative=False) none[name] = build_assertion_property(name, is_negative=False, prop=False) for name in NEGATIVES: object_handler[name] = build_assertion_property(name, is_negative=True) none[name] = build_assertion_property(name, is_negative=True, prop=False) old_dir = dir def enable(): @wraps(builtins.dir) def _new_dir(*obj): if not obj: frame = inspect.currentframe() return sorted(frame.f_back.f_locals.keys()) if len(obj) > 1: raise TypeError( "dir expected at most 1 arguments, got {0}".format(len(obj)) ) patched = [] try: import pytest except ImportError: pytest = None if not pytest: try: patched = [ x for x in old_dir(obj[0]) if isinstance(getattr(obj[0], x, None), AssertionBuilder) ] except Exception: pass return sorted(set(old_dir(obj[0])).difference(set())) builtins.dir = _new_dir do_enable() if is_cpython and allows_new_syntax: enable()