Skip to content

Commit

Permalink
presents more improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Jan 1, 2024
1 parent aadc315 commit 16effc5
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 78 deletions.
43 changes: 30 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,34 @@ Installing

.. code:: bash
$ pip install sure
pip install sure
Running tests
-------------

.. code:: bash
sure tests
.. code:: bash
sure --help
Documentation
-------------

Available in the `website <https://sure.readthedocs.io/en/latest/>`__ or under the
``docs`` directory.
Available on the `website <https://sure.readthedocs.io/en/latest/>`_.

You can also build the documentation locally using sphinx:
To build locally run:

.. code:: bash
make docs
Here is a tease
---------------
Quick Library Showcase
----------------------

Equality
~~~~~~~~
Expand All @@ -72,24 +84,29 @@ Equality

.. code:: python
import sure
from sure import expect
(4).should.be.equal(2 + 2)
(7.5).should.eql(3.5 + 4)
expect(4).to.be.equal(2 + 2)
expect(7.5).to.be.eql(3.5 + 4)
expect(3).to.not_be.equal(5)
expect(9).to_not.be.equal(11)
(3).shouldnt.be.equal(5)
Assert dictionary and its contents
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: python
{'foo': 'bar'}.should.equal({'foo': 'bar'})
{'foo': 'bar'}.should.have.key('foo').which.should.equal('bar')
from sure import expect
expect({'foo': 'bar'}).to.equal({'foo': 'bar'})
expect({'foo': 'bar'}).to.have.key('foo').being.equal('bar')
"A string".lower().should.equal("a string") also works
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: python
"Awesome ASSERTIONS".lower().split().should.equal(['awesome', 'assertions'])
"Awesome ASSERTIONS".lower().split().should.equal(['awesome', 'assertions'])
49 changes: 30 additions & 19 deletions sure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from sure.core import DeepComparison
from sure.core import DeepExplanation
from sure.errors import SpecialSyntaxDisabledError
from sure.errors import WrongUsageError, SpecialSyntaxDisabledError
from sure.errors import InternalRuntimeError
from sure.doubles.dummies import anything
from sure.loader import get_file_name
Expand Down Expand Up @@ -596,6 +597,10 @@ def to(self):
def when(self):
return self

@assertionproperty
def which(self):
return self

@assertionproperty
def have(self):
return self
Expand Down Expand Up @@ -691,7 +696,7 @@ def none(self):

return True

def __contains__(self, what):
def __contains__(self, expectation):
if isinstance(self.obj, dict):
items = self.obj.keys()

Expand All @@ -700,14 +705,14 @@ def __contains__(self, what):
else:
items = dir(self.obj)

return what in items
return expectation in items

@assertionmethod
def contains(self, what):
if what in self.obj:
def contains(self, expectation):
if expectation in self.obj:
return True
else:
raise AssertionError('%r should be in %r' % (what, self.obj))
raise AssertionError('%r should be in %r' % (expectation, self.obj))

@assertionmethod
def within_range(self, start, end):
Expand Down Expand Up @@ -759,32 +764,32 @@ def within(self, first, *rest):
)

@assertionmethod
def equal(self, what, epsilon=None):
def equal(self, expectation, 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 expectation: the expected value
:param epsilon: a delta to leverage upper-bound floating point permissiveness
"""
obj = self.obj

try:
comparison = DeepComparison(obj, what, epsilon).compare()
comparison = DeepComparison(obj, expectation, epsilon).compare()
error = False
except AssertionError as e:
error = e
comparison = None

if isinstance(comparison, DeepExplanation):
error = comparison.get_assertion(obj, what)
error = comparison.get_assertion(obj, expectation)

if self.negative:
if error:
return True

msg = "%s should differ from %s"
raise AssertionError(msg % (repr(obj), repr(what)))
raise AssertionError(msg % (repr(obj), repr(expectation)))

else:
if not error:
Expand All @@ -796,22 +801,27 @@ def equal(self, what, epsilon=None):
equal_to = equal

@assertionmethod
def different_of(self, what):
def different_of(self, expectation):
differ = difflib.Differ()

obj = isinstance(self.obj, AssertionHelper) and self.obj.src or self.obj
if not isinstance(expectation, str):
raise WrongUsageError(f".different_of only works for string comparison but in this case is expecting {repr(expectation)} ({type(expectation)}) instead")

if not isinstance(self.obj, str):
raise WrongUsageError(f".different_of only works for string comparison but in this case the actual source comparison object is {repr(self.obj)} ({type(self.obj)}) instead")

source = obj.strip().splitlines(True)
destination = what.strip().splitlines(True)
destination = expectation.strip().splitlines(True)
result = differ.compare(source, destination)
difference = "".join(result)
if self.negative:
if obj != what:
if obj != expectation:
assert not difference, "Difference:\n\n{0}".format(difference)
else:
if obj == what:
if obj == expectation:
raise AssertionError(
"{0} should be different of {1}".format(obj, what)
"{0} should be different of {1}".format(obj, expectation)
)

return True
Expand Down Expand Up @@ -1006,12 +1016,12 @@ def look_like(self, value):
return self._that.looks_like(value)

@assertionmethod
def contain(self, what):
def contain(self, expectation):
obj = self.obj
if self.negative:
return expect(what).to.not_be.within(obj)
return expect(expectation).to.not_be.within(obj)
else:
return expect(what).to.be.within(obj)
return expect(expectation).to.be.within(obj)

@assertionmethod
def match(self, regex, *args):
Expand All @@ -1028,7 +1038,7 @@ def match(self, regex, *args):
re.S: "s",
re.U: "u",
}
modifiers = "".join([modifiers_map.get(x, "") for x in args])
modifiers = "".join(filter(bool, [modifiers_map.get(x, "") for x in args]))
regex_representation = "/{0}/{1}".format(regex, modifiers)

if self.negative:
Expand All @@ -1051,6 +1061,7 @@ def match(self, regex, *args):
assert_that = AssertionBuilder("assert_that")
it = AssertionBuilder("it")
expect = AssertionBuilder("expect")
expects = AssertionBuilder("expect")
that = AssertionBuilder("that")
the = AssertionBuilder("the")
these = AssertionBuilder("these")
Expand Down
67 changes: 36 additions & 31 deletions sure/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ def as_assertion(self, X, Y):


class DeepComparison(object):
"""Performs a deep comparison between Python objects in the sense
that complex or nested data-structures - such as mappings of
sequences, sequences of mappings, mappings of sequences of
mappings, sequences of mappings of sequences containing et cetera
- are recursively compared and reaching farthest accessible edges.
"""
def __init__(self, X, Y, epsilon=None, parent=None):
self.complex_cmp_funcs = {
float: self.compare_floats,
Expand All @@ -64,10 +70,34 @@ def is_simple(self, obj):
string_types, integer_types, binary_type, Anything
))

@cache
def get_context(self):
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(repr, i)))

class ComparisonContext:
current_X_keys = get_keys(X_keys)
current_Y_keys = get_keys(Y_keys)
parent = comp

return ComparisonContext()

def is_complex(self, obj):
return isinstance(obj, tuple(self.complex_cmp_funcs.keys()))

def compare_complex_stuff(self, X, Y):
def compare_complex_instances(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}'):
Expand Down Expand Up @@ -138,43 +168,18 @@ def compare_ordered_dicts(self, X, Y):
for i, j in zip(X.items(), Y.items()):
if i[0] != j[0]:
c = self.get_context()
msg = "X{0} and Y{1} appear have keys in different order".format(
red(c.current_X_keys), green(c.current_Y_keys)
)
msg = f"X{red(c.current_X_keys)} and Y{green(c.current_Y_keys)} appear have keys in different order"
return DeepExplanation(msg)
return True

@cache
def get_context(self):
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(repr, i)))

class ComparisonContext:
current_X_keys = get_keys(X_keys)
current_Y_keys = get_keys(Y_keys)
parent = comp

return ComparisonContext()

def compare_iterables(self, X, Y):
c = self.get_context()
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)
msg = f"X{red(c.current_X_keys)} has {len_X} items whereas Y{green(c.current_Y_keys)} has only {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)
msg = f"Y{green(c.current_Y_keys)} has {len_Y} items whereas X{red(c.current_X_keys)} has only {len_X}"
return DeepExplanation(msg)
elif X == Y:
return True
Expand All @@ -201,7 +206,7 @@ def compare(self):

c = self.get_context()
if self.is_complex(X) and type(X) is type(Y):
return self.compare_complex_stuff(X, Y)
return self.compare_complex_instances(X, Y)

def safe_format_repr(string):
"Escape '{' and '}' in string for use with str.format()"
Expand Down
10 changes: 10 additions & 0 deletions sure/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ def __init__(self, context, exception: Exception):
context.reporter.on_internal_runtime_error(context, self)


class WrongUsageError(Exception):
"""raised when :class:`~sure.AssertionBuilder` is used
incorrectly, such as passing a value of the wrong type as argument
to an assertion method or as source of comparison.
This exception should be clearly indicated by reporters so that
the offending action can be understood and corrected quickly.
"""


class SpecialSyntaxDisabledError(Exception):
"""raised when a :class:`AttributeError` occurs and the traceback
contains evidence indicating that the probable cause is an attempt
Expand Down

0 comments on commit 16effc5

Please sign in to comment.