Merge pull request #3776 from alysivji/attrs-n-dataclasses
Detailed assert failure introspection for attrs and dataclasses objects
This commit is contained in:
commit
f987b368e8
|
@ -44,3 +44,4 @@ coverage.xml
|
|||
.pydevproject
|
||||
.project
|
||||
.settings
|
||||
.vscode
|
||||
|
|
1
AUTHORS
1
AUTHORS
|
@ -11,6 +11,7 @@ Alan Velasco
|
|||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Allan Feldman
|
||||
Aly Sivji
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs <http://www.attrs.org/en/stable/>`_ or `dataclasses <https://docs.python.org/3/library/dataclasses.html>`_ (Python 3.7+, `backported to 3.6 <https://pypi.org/project/dataclasses>`_).
|
|
@ -98,6 +98,30 @@ class TestSpecialisedExplanations(object):
|
|||
text = "head " * 50 + "f" * 70 + "tail " * 20
|
||||
assert "f" * 70 not in text
|
||||
|
||||
def test_eq_dataclass(self):
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Foo(object):
|
||||
a: int
|
||||
b: str
|
||||
|
||||
left = Foo(1, "b")
|
||||
right = Foo(1, "c")
|
||||
assert left == right
|
||||
|
||||
def test_eq_attrs(self):
|
||||
import attr
|
||||
|
||||
@attr.s
|
||||
class Foo(object):
|
||||
a = attr.ib()
|
||||
b = attr.ib()
|
||||
|
||||
left = Foo(1, "b")
|
||||
right = Foo(1, "c")
|
||||
assert left == right
|
||||
|
||||
|
||||
def test_attribute():
|
||||
class Foo(object):
|
||||
|
|
|
@ -9,5 +9,5 @@ def test_failure_demo_fails_properly(testdir):
|
|||
failure_demo.copy(target)
|
||||
failure_demo.copy(testdir.tmpdir.join(failure_demo.basename))
|
||||
result = testdir.runpytest(target, syspathinsert=True)
|
||||
result.stdout.fnmatch_lines(["*42 failed*"])
|
||||
result.stdout.fnmatch_lines(["*44 failed*"])
|
||||
assert result.ret != 0
|
||||
|
|
|
@ -122,6 +122,12 @@ def assertrepr_compare(config, op, left, right):
|
|||
def isset(x):
|
||||
return isinstance(x, (set, frozenset))
|
||||
|
||||
def isdatacls(obj):
|
||||
return getattr(obj, "__dataclass_fields__", None) is not None
|
||||
|
||||
def isattrs(obj):
|
||||
return getattr(obj, "__attrs_attrs__", None) is not None
|
||||
|
||||
def isiterable(obj):
|
||||
try:
|
||||
iter(obj)
|
||||
|
@ -142,6 +148,9 @@ def assertrepr_compare(config, op, left, right):
|
|||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
elif type(left) == type(right) and (isdatacls(left) or isattrs(left)):
|
||||
type_fn = (isdatacls, isattrs)
|
||||
explanation = _compare_eq_cls(left, right, verbose, type_fn)
|
||||
if isiterable(left) and isiterable(right):
|
||||
expl = _compare_eq_iterable(left, right, verbose)
|
||||
if explanation is not None:
|
||||
|
@ -315,6 +324,38 @@ def _compare_eq_dict(left, right, verbose=False):
|
|||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_cls(left, right, verbose, type_fns):
|
||||
isdatacls, isattrs = type_fns
|
||||
if isdatacls(left):
|
||||
all_fields = left.__dataclass_fields__
|
||||
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
||||
elif isattrs(left):
|
||||
all_fields = left.__attrs_attrs__
|
||||
fields_to_check = [field.name for field in all_fields if field.cmp]
|
||||
|
||||
same = []
|
||||
diff = []
|
||||
for field in fields_to_check:
|
||||
if getattr(left, field) == getattr(right, field):
|
||||
same.append(field)
|
||||
else:
|
||||
diff.append(field)
|
||||
|
||||
explanation = []
|
||||
if same and verbose < 2:
|
||||
explanation.append(u"Omitting %s identical items, use -vv to show" % len(same))
|
||||
elif same:
|
||||
explanation += [u"Matching attributes:"]
|
||||
explanation += pprint.pformat(same).splitlines()
|
||||
if diff:
|
||||
explanation += [u"Differing attributes:"]
|
||||
for field in diff:
|
||||
explanation += [
|
||||
(u"%s: %r != %r") % (field, getattr(left, field), getattr(right, field))
|
||||
]
|
||||
return explanation
|
||||
|
||||
|
||||
def _notin_text(term, text, verbose=False):
|
||||
index = text.find(term)
|
||||
head = text[:index]
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
|
||||
def test_dataclasses():
|
||||
@dataclass
|
||||
class SimpleDataObject(object):
|
||||
field_a: int = field()
|
||||
field_b: int = field()
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
assert left == right
|
|
@ -0,0 +1,14 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
|
||||
def test_dataclasses_with_attribute_comparison_off():
|
||||
@dataclass
|
||||
class SimpleDataObject(object):
|
||||
field_a: int = field()
|
||||
field_b: int = field(compare=False)
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
assert left == right
|
|
@ -0,0 +1,14 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
|
||||
def test_dataclasses_verbose():
|
||||
@dataclass
|
||||
class SimpleDataObject(object):
|
||||
field_a: int = field()
|
||||
field_b: int = field()
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
assert left == right
|
|
@ -0,0 +1,19 @@
|
|||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
|
||||
def test_comparing_two_different_data_classes():
|
||||
@dataclass
|
||||
class SimpleDataObjectOne(object):
|
||||
field_a: int = field()
|
||||
field_b: int = field()
|
||||
|
||||
@dataclass
|
||||
class SimpleDataObjectTwo(object):
|
||||
field_a: int = field()
|
||||
field_b: int = field()
|
||||
|
||||
left = SimpleDataObjectOne(1, "b")
|
||||
right = SimpleDataObjectTwo(1, "c")
|
||||
|
||||
assert left != right
|
|
@ -6,6 +6,7 @@ from __future__ import print_function
|
|||
import sys
|
||||
import textwrap
|
||||
|
||||
import attr
|
||||
import py
|
||||
import six
|
||||
|
||||
|
@ -548,6 +549,115 @@ class TestAssert_reprcompare(object):
|
|||
assert msg
|
||||
|
||||
|
||||
class TestAssert_reprcompare_dataclass(object):
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses(self, testdir):
|
||||
p = testdir.copy_example("dataclasses/test_compare_dataclasses.py")
|
||||
result = testdir.runpytest(p)
|
||||
result.assert_outcomes(failed=1, passed=0)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Omitting 1 identical items, use -vv to show*",
|
||||
"*Differing attributes:*",
|
||||
"*field_b: 'b' != 'c'*",
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses_verbose(self, testdir):
|
||||
p = testdir.copy_example("dataclasses/test_compare_dataclasses_verbose.py")
|
||||
result = testdir.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=1, passed=0)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Matching attributes:*",
|
||||
"*['field_a']*",
|
||||
"*Differing attributes:*",
|
||||
"*field_b: 'b' != 'c'*",
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_dataclasses_with_attribute_comparison_off(self, testdir):
|
||||
p = testdir.copy_example(
|
||||
"dataclasses/test_compare_dataclasses_field_comparison_off.py"
|
||||
)
|
||||
result = testdir.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+")
|
||||
def test_comparing_two_different_data_classes(self, testdir):
|
||||
p = testdir.copy_example(
|
||||
"dataclasses/test_compare_two_different_dataclasses.py"
|
||||
)
|
||||
result = testdir.runpytest(p, "-vv")
|
||||
result.assert_outcomes(failed=0, passed=1)
|
||||
|
||||
|
||||
class TestAssert_reprcompare_attrsclass(object):
|
||||
def test_attrs(self):
|
||||
@attr.s
|
||||
class SimpleDataObject(object):
|
||||
field_a = attr.ib()
|
||||
field_b = attr.ib()
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
lines = callequal(left, right)
|
||||
assert lines[1].startswith("Omitting 1 identical item")
|
||||
assert "Matching attributes" not in lines
|
||||
for line in lines[1:]:
|
||||
assert "field_a" not in line
|
||||
|
||||
def test_attrs_verbose(self):
|
||||
@attr.s
|
||||
class SimpleDataObject(object):
|
||||
field_a = attr.ib()
|
||||
field_b = attr.ib()
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "c")
|
||||
|
||||
lines = callequal(left, right, verbose=2)
|
||||
assert lines[1].startswith("Matching attributes:")
|
||||
assert "Omitting" not in lines[1]
|
||||
assert lines[2] == "['field_a']"
|
||||
|
||||
def test_attrs_with_attribute_comparison_off(self):
|
||||
@attr.s
|
||||
class SimpleDataObject(object):
|
||||
field_a = attr.ib()
|
||||
field_b = attr.ib(cmp=False)
|
||||
|
||||
left = SimpleDataObject(1, "b")
|
||||
right = SimpleDataObject(1, "b")
|
||||
|
||||
lines = callequal(left, right, verbose=2)
|
||||
assert lines[1].startswith("Matching attributes:")
|
||||
assert "Omitting" not in lines[1]
|
||||
assert lines[2] == "['field_a']"
|
||||
for line in lines[2:]:
|
||||
assert "field_b" not in line
|
||||
|
||||
def test_comparing_two_different_attrs_classes(self):
|
||||
@attr.s
|
||||
class SimpleDataObjectOne(object):
|
||||
field_a = attr.ib()
|
||||
field_b = attr.ib()
|
||||
|
||||
@attr.s
|
||||
class SimpleDataObjectTwo(object):
|
||||
field_a = attr.ib()
|
||||
field_b = attr.ib()
|
||||
|
||||
left = SimpleDataObjectOne(1, "b")
|
||||
right = SimpleDataObjectTwo(1, "c")
|
||||
|
||||
lines = callequal(left, right)
|
||||
assert lines is None
|
||||
|
||||
|
||||
class TestFormatExplanation(object):
|
||||
def test_special_chars_full(self, testdir):
|
||||
# Issue 453, for the bug this would raise IndexError
|
||||
|
|
Loading…
Reference in New Issue