From 9769bc05c626dd0401e691c2c9c68f774fb990c3 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Thu, 2 Aug 2018 02:54:15 -0500 Subject: [PATCH 01/14] moving plugin inside pytest first pass --- .gitignore | 2 ++ src/_pytest/assertion/util.py | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/.gitignore b/.gitignore index f5cd0145c..907876f1b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ coverage.xml .pydevproject .project .settings +.vscode +.envrc diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 451e45495..1f5a857b8 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -122,6 +122,12 @@ def assertrepr_compare(config, op, left, right): def isset(x): return isinstance(x, (set, frozenset)) + def isdatacls(obj): + return hasattr(obj, "__dataclass_fields__") + + def isattrs(obj): + return hasattr(obj, "__attrs_attrs__") + def isiterable(obj): try: iter(obj) @@ -142,6 +148,10 @@ 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) and isdatacls(right): + explanation = _compare_eq_class(left, right, verbose, type="data") + elif type(left) == type(right) and isattrs(left) and isattrs(right): + explanation = _compare_eq_class(left, right, verbose, type="attrs") if isiterable(left) and isiterable(right): expl = _compare_eq_iterable(left, right, verbose) if explanation is not None: @@ -315,6 +325,42 @@ def _compare_eq_dict(left, right, verbose=False): return explanation +def _compare_eq_class(left, right, verbose, type=None): + # TODO account for verbose + # TODO write tests + + if type == "data": + all_fields = left.__dataclass_fields__ + fields_to_check = [field for field, info in all_fields.items() if info.compare] + elif type == "attrs": + all_fields = left.__attrs_attrs__ + fields_to_check = [field.name for field in all_fields if field.cmp] + else: + raise RuntimeError # TODO figure out what to raise + + 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: + explanation += [("Common attributes:")] + explanation += pprint.pformat(same).splitlines() + if diff: + explanation += [("Differing attributes:")] + for k in diff: + class_name = left.__class__.__name__ + explanation += [ + u("%s(%s=%r) != %s(%s=%r)") + % (class_name, k, getattr(left, k), class_name, k, getattr(right, k)) + ] + return explanation + + def _notin_text(term, text, verbose=False): index = text.find(term) head = text[:index] From d42f1e87c3c4b55ef5faa1a11a4f3083860c864d Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Thu, 2 Aug 2018 17:16:14 -0500 Subject: [PATCH 02/14] Add tests for attrs and dataclasses --- src/_pytest/assertion/util.py | 11 ++- testing/test_assertion.py | 139 ++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 1f5a857b8..da5d5fe97 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -326,9 +326,6 @@ def _compare_eq_dict(left, right, verbose=False): def _compare_eq_class(left, right, verbose, type=None): - # TODO account for verbose - # TODO write tests - if type == "data": all_fields = left.__dataclass_fields__ fields_to_check = [field for field, info in all_fields.items() if info.compare] @@ -336,7 +333,7 @@ def _compare_eq_class(left, right, verbose, type=None): all_fields = left.__attrs_attrs__ fields_to_check = [field.name for field in all_fields if field.cmp] else: - raise RuntimeError # TODO figure out what to raise + raise RuntimeError same = [] diff = [] @@ -347,8 +344,10 @@ def _compare_eq_class(left, right, verbose, type=None): diff.append(field) explanation = [] - if same: - explanation += [("Common attributes:")] + if same and verbose < 2: + explanation += [u("Omitting %s identical items, use -vv to show") % len(same)] + elif same: + explanation += [u("Common items:")] explanation += pprint.pformat(same).splitlines() if diff: explanation += [("Differing attributes:")] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index b6c31aba2..87f7de2b5 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -6,6 +6,7 @@ from __future__ import print_function import sys import textwrap +import attr import py import six @@ -548,6 +549,144 @@ 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): + from dataclasses import dataclass + + @dataclass + class SimpleDataObject: + field_a: int + field_b: str + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + lines = callequal(left, right) + assert lines[1].startswith("Omitting 1 identical item") + assert "Common items" not in lines + for line in lines[1:]: + assert "field_a" not in line + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_dataclasses_verbose(self): + from dataclasses import dataclass + + @dataclass + class SimpleDataObject: + field_a: int + field_b: str + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + lines = callequal(left, right, verbose=2) + assert lines[1].startswith("Common items:") + assert "Omitting" not in lines[1] + assert lines[2] == "['field_a']" + + def test_dataclasses_with_attribute_comparison_off(self): + from dataclasses import dataclass, field + + @dataclass + class SimpleDataObject: + field_a: int + field_b: str = field(compare=False) + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "b") + + lines = callequal(left, right, verbose=2) + assert lines[1].startswith("Common items:") + 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_different_data_classes(self): + from dataclasses import dataclass + + @dataclass + class SimpleDataObjectOne: + field_a: int + field_b: str + + @dataclass + class SimpleDataObjectTwo: + field_a: int + field_b: str + + left = SimpleDataObjectOne(1, "b") + right = SimpleDataObjectTwo(1, "c") + + lines = callequal(left, right) + assert lines is None + + +class TestAssert_reprcompare_attrsclass(object): + def test_attrs(self): + @attr.s + class SimpleDataObject: + 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 "Common items" not in lines + for line in lines[1:]: + assert "field_a" not in line + + def test_attrs_verbose(self): + @attr.s + class SimpleDataObject: + 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("Common items:") + assert "Omitting" not in lines[1] + assert lines[2] == "['field_a']" + + def test_attrs_with_attribute_comparison_off(self): + @attr.s + class SimpleDataObject: + 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("Common items:") + 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_different_attrs(self): + @attr.s + class SimpleDataObjectOne: + field_a = attr.ib() + field_b = attr.ib() + + @attr.s + class SimpleDataObjectTwo: + 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 From a0ba881c2202b4b7d49505fb5d53088ea387db30 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Thu, 2 Aug 2018 18:08:07 -0500 Subject: [PATCH 03/14] Add change to log; name to AUTHORS --- AUTHORS | 1 + changelog/3632.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/3632.feature.rst diff --git a/AUTHORS b/AUTHORS index 777eda324..f5ba603c2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Alan Velasco Alexander Johnson Alexei Kozlenok Allan Feldman +Aly Sivji Anatoly Bubenkoff Anders Hovmöller Andras Tim diff --git a/changelog/3632.feature.rst b/changelog/3632.feature.rst new file mode 100644 index 000000000..bb7918ab7 --- /dev/null +++ b/changelog/3632.feature.rst @@ -0,0 +1 @@ +Provide richer comparison on ``AssertionError`` for objects created using `dataclasses `_ (Python 3.7+) or `attrs package `_. From 1184db827373822863fe936b3027b225cb77ed02 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Thu, 2 Aug 2018 18:22:15 -0500 Subject: [PATCH 04/14] cleaning up --- changelog/3632.feature.rst | 2 +- testing/test_assertion.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog/3632.feature.rst b/changelog/3632.feature.rst index bb7918ab7..a08a6dd61 100644 --- a/changelog/3632.feature.rst +++ b/changelog/3632.feature.rst @@ -1 +1 @@ -Provide richer comparison on ``AssertionError`` for objects created using `dataclasses `_ (Python 3.7+) or `attrs package `_. +Richer comparison information on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+). diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 87f7de2b5..e7f3dbaf3 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -585,6 +585,7 @@ class TestAssert_reprcompare_dataclass(object): assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_dataclasses_with_attribute_comparison_off(self): from dataclasses import dataclass, field @@ -603,6 +604,7 @@ class TestAssert_reprcompare_dataclass(object): for line in lines[2:]: assert "field_b" not in line + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_comparing_different_data_classes(self): from dataclasses import dataclass From 87b019d5f91f7fb74c8a80218b4d4cf1647e61e5 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Thu, 2 Aug 2018 18:24:46 -0500 Subject: [PATCH 05/14] fix gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 907876f1b..e2d59502c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,3 @@ coverage.xml .project .settings .vscode -.envrc From 1847cc74208a58efbfd1d8154e34b2bf18987b8c Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Fri, 3 Aug 2018 09:23:50 -0500 Subject: [PATCH 06/14] adding docs and cleaning up --- changelog/3632.feature.rst | 2 +- doc/en/example/assertion/failure_demo.py | 24 +++++++++++++++++++++++ doc/en/example/assertion/test_failures.py | 2 +- src/_pytest/assertion/util.py | 4 ++-- testing/test_assertion.py | 16 +++++++-------- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/changelog/3632.feature.rst b/changelog/3632.feature.rst index a08a6dd61..a715288e1 100644 --- a/changelog/3632.feature.rst +++ b/changelog/3632.feature.rst @@ -1 +1 @@ -Richer comparison information on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+). +Richer comparison introspection on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+). diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index 115fd3e22..10f8798f2 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -101,6 +101,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): diff --git a/doc/en/example/assertion/test_failures.py b/doc/en/example/assertion/test_failures.py index 9ffe31664..30ebc72dc 100644 --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -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 diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index da5d5fe97..ac83f6000 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -347,12 +347,12 @@ def _compare_eq_class(left, right, verbose, type=None): if same and verbose < 2: explanation += [u("Omitting %s identical items, use -vv to show") % len(same)] elif same: - explanation += [u("Common items:")] + explanation += [u("Common attributes:")] explanation += pprint.pformat(same).splitlines() if diff: + class_name = left.__class__.__name__ explanation += [("Differing attributes:")] for k in diff: - class_name = left.__class__.__name__ explanation += [ u("%s(%s=%r) != %s(%s=%r)") % (class_name, k, getattr(left, k), class_name, k, getattr(right, k)) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e7f3dbaf3..8ddd96b94 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -564,7 +564,7 @@ class TestAssert_reprcompare_dataclass(object): lines = callequal(left, right) assert lines[1].startswith("Omitting 1 identical item") - assert "Common items" not in lines + assert "Common attributes" not in lines for line in lines[1:]: assert "field_a" not in line @@ -581,7 +581,7 @@ class TestAssert_reprcompare_dataclass(object): right = SimpleDataObject(1, "c") lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common items:") + assert lines[1].startswith("Common attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" @@ -598,14 +598,14 @@ class TestAssert_reprcompare_dataclass(object): right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common items:") + assert lines[1].startswith("Common attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" for line in lines[2:]: assert "field_b" not in line @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_comparing_different_data_classes(self): + def test_comparing_two_different_data_classes(self): from dataclasses import dataclass @dataclass @@ -637,7 +637,7 @@ class TestAssert_reprcompare_attrsclass(object): lines = callequal(left, right) assert lines[1].startswith("Omitting 1 identical item") - assert "Common items" not in lines + assert "Common attributes" not in lines for line in lines[1:]: assert "field_a" not in line @@ -651,7 +651,7 @@ class TestAssert_reprcompare_attrsclass(object): right = SimpleDataObject(1, "c") lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common items:") + assert lines[1].startswith("Common attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" @@ -665,13 +665,13 @@ class TestAssert_reprcompare_attrsclass(object): right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common items:") + assert lines[1].startswith("Common 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_different_attrs(self): + def test_comparing_two_different_attrs_classes(self): @attr.s class SimpleDataObjectOne: field_a = attr.ib() From a3e388a73a4ef6b712c442c4147633322790b80d Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Fri, 3 Aug 2018 10:28:46 -0500 Subject: [PATCH 07/14] Improve changelog --- changelog/3632.feature.rst | 2 +- src/_pytest/assertion/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/3632.feature.rst b/changelog/3632.feature.rst index a715288e1..023fa3607 100644 --- a/changelog/3632.feature.rst +++ b/changelog/3632.feature.rst @@ -1 +1 @@ -Richer comparison introspection on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+). +Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+, `backported to Python 3.6 `_). diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index ac83f6000..4536cd0dd 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -333,7 +333,7 @@ def _compare_eq_class(left, right, verbose, type=None): all_fields = left.__attrs_attrs__ fields_to_check = [field.name for field in all_fields if field.cmp] else: - raise RuntimeError + raise RuntimeError("Unexpected value for `type` paramater") same = [] diff = [] From 025d160dfc0ce87482606b27762c47d47e4a2d5a Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Fri, 3 Aug 2018 11:29:45 -0500 Subject: [PATCH 08/14] Update tests to pass in py27 --- changelog/3632.feature.rst | 2 +- testing/test_assertion.py | 46 +++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/changelog/3632.feature.rst b/changelog/3632.feature.rst index 023fa3607..cb1d93750 100644 --- a/changelog/3632.feature.rst +++ b/changelog/3632.feature.rst @@ -1 +1 @@ -Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+, `backported to Python 3.6 `_). +Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs `_ or `dataclasses `_ (Python 3.7+, `backported to 3.6 `_). diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 8ddd96b94..fdcfccfed 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -552,12 +552,12 @@ class TestAssert_reprcompare(object): class TestAssert_reprcompare_dataclass(object): @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_dataclasses(self): - from dataclasses import dataclass + from dataclasses import dataclass, field @dataclass - class SimpleDataObject: - field_a: int - field_b: str + class SimpleDataObject(object): + field_a = field() + field_b = field() left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") @@ -570,12 +570,12 @@ class TestAssert_reprcompare_dataclass(object): @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_dataclasses_verbose(self): - from dataclasses import dataclass + from dataclasses import dataclass, field @dataclass - class SimpleDataObject: - field_a: int - field_b: str + class SimpleDataObject(object): + field_a = field() + field_b = field() left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") @@ -590,9 +590,9 @@ class TestAssert_reprcompare_dataclass(object): from dataclasses import dataclass, field @dataclass - class SimpleDataObject: - field_a: int - field_b: str = field(compare=False) + class SimpleDataObject(object): + field_a = field() + field_b = field(compare=False) left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "b") @@ -606,17 +606,17 @@ class TestAssert_reprcompare_dataclass(object): @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_comparing_two_different_data_classes(self): - from dataclasses import dataclass + from dataclasses import dataclass, field @dataclass - class SimpleDataObjectOne: - field_a: int - field_b: str + class SimpleDataObjectOne(object): + field_a = field() + field_b = field() @dataclass - class SimpleDataObjectTwo: - field_a: int - field_b: str + class SimpleDataObjectTwo(object): + field_a = field() + field_b = field() left = SimpleDataObjectOne(1, "b") right = SimpleDataObjectTwo(1, "c") @@ -628,7 +628,7 @@ class TestAssert_reprcompare_dataclass(object): class TestAssert_reprcompare_attrsclass(object): def test_attrs(self): @attr.s - class SimpleDataObject: + class SimpleDataObject(object): field_a = attr.ib() field_b = attr.ib() @@ -643,7 +643,7 @@ class TestAssert_reprcompare_attrsclass(object): def test_attrs_verbose(self): @attr.s - class SimpleDataObject: + class SimpleDataObject(object): field_a = attr.ib() field_b = attr.ib() @@ -657,7 +657,7 @@ class TestAssert_reprcompare_attrsclass(object): def test_attrs_with_attribute_comparison_off(self): @attr.s - class SimpleDataObject: + class SimpleDataObject(object): field_a = attr.ib() field_b = attr.ib(cmp=False) @@ -673,12 +673,12 @@ class TestAssert_reprcompare_attrsclass(object): def test_comparing_two_different_attrs_classes(self): @attr.s - class SimpleDataObjectOne: + class SimpleDataObjectOne(object): field_a = attr.ib() field_b = attr.ib() @attr.s - class SimpleDataObjectTwo: + class SimpleDataObjectTwo(object): field_a = attr.ib() field_b = attr.ib() From e1e81e315e41fd4674b9e10c2bd4074734d3692b Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 4 Aug 2018 08:29:55 -0500 Subject: [PATCH 09/14] code review 1/n -- change hasattr to getattr --- src/_pytest/assertion/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 4536cd0dd..b6867436e 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -123,10 +123,10 @@ def assertrepr_compare(config, op, left, right): return isinstance(x, (set, frozenset)) def isdatacls(obj): - return hasattr(obj, "__dataclass_fields__") + return getattr(obj, "__dataclass_fields__", None) is not None def isattrs(obj): - return hasattr(obj, "__attrs_attrs__") + return getattr(obj, "__attrs_attrs__", None) is not None def isiterable(obj): try: From a663f60b054fa791c1cb9729b68dbddedfdff26f Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Mon, 10 Sep 2018 15:33:37 -0500 Subject: [PATCH 10/14] cr 2/n -- refactor compare eq class --- src/_pytest/assertion/util.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index b6867436e..9984b5120 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -148,10 +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) and isdatacls(right): - explanation = _compare_eq_class(left, right, verbose, type="data") - elif type(left) == type(right) and isattrs(left) and isattrs(right): - explanation = _compare_eq_class(left, right, verbose, type="attrs") + 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: @@ -325,15 +324,14 @@ def _compare_eq_dict(left, right, verbose=False): return explanation -def _compare_eq_class(left, right, verbose, type=None): - if type == "data": +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 type == "attrs": + elif isattrs(left): all_fields = left.__attrs_attrs__ fields_to_check = [field.name for field in all_fields if field.cmp] - else: - raise RuntimeError("Unexpected value for `type` paramater") same = [] diff = [] From 4e99c80425024dfcf733e060c5ce9c53ff49c475 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Mon, 12 Nov 2018 11:24:15 -0600 Subject: [PATCH 11/14] have tests pass in python37; move to separate file --- src/_pytest/assertion/util.py | 8 ++++---- testing/test_assertion.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 9984b5120..4e10a3fbb 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -343,16 +343,16 @@ def _compare_eq_cls(left, right, verbose, type_fns): explanation = [] if same and verbose < 2: - explanation += [u("Omitting %s identical items, use -vv to show") % len(same)] + explanation.append(u"Omitting %s identical items, use -vv to show" % len(same)) elif same: - explanation += [u("Common attributes:")] + explanation += [u"Common attributes:"] explanation += pprint.pformat(same).splitlines() if diff: class_name = left.__class__.__name__ - explanation += [("Differing attributes:")] + explanation += [u"Differing attributes:"] for k in diff: explanation += [ - u("%s(%s=%r) != %s(%s=%r)") + (u"%s(%s=%r) != %s(%s=%r)") % (class_name, k, getattr(left, k), class_name, k, getattr(right, k)) ] return explanation diff --git a/testing/test_assertion.py b/testing/test_assertion.py index fdcfccfed..4a09187b7 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -556,8 +556,8 @@ class TestAssert_reprcompare_dataclass(object): @dataclass class SimpleDataObject(object): - field_a = field() - field_b = field() + field_a: int = field() + field_b: int = field() left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") @@ -574,8 +574,8 @@ class TestAssert_reprcompare_dataclass(object): @dataclass class SimpleDataObject(object): - field_a = field() - field_b = field() + field_a: int = field() + field_b: int = field() left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") @@ -591,8 +591,8 @@ class TestAssert_reprcompare_dataclass(object): @dataclass class SimpleDataObject(object): - field_a = field() - field_b = field(compare=False) + field_a: int = field() + field_b: int = field(compare=False) left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "b") @@ -610,13 +610,13 @@ class TestAssert_reprcompare_dataclass(object): @dataclass class SimpleDataObjectOne(object): - field_a = field() - field_b = field() + field_a: int = field() + field_b: int = field() @dataclass class SimpleDataObjectTwo(object): - field_a = field() - field_b = field() + field_a: int = field() + field_b: int = field() left = SimpleDataObjectOne(1, "b") right = SimpleDataObjectTwo(1, "c") From 2bffd6829eb38a44f9e7523d044dad7339830691 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Mon, 12 Nov 2018 15:36:16 -0600 Subject: [PATCH 12/14] Move dataclass tests for 3.7 to separate file --- .../dataclasses/test_compare_dataclasses.py | 14 +++ ...ompare_dataclasses_field_comparison_off.py | 14 +++ .../test_compare_dataclasses_verbose.py | 14 +++ .../test_compare_two_different_dataclasses.py | 19 ++++ testing/test_assertion.py | 101 ++++++------------ 5 files changed, 96 insertions(+), 66 deletions(-) create mode 100644 testing/example_scripts/dataclasses/test_compare_dataclasses.py create mode 100644 testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py create mode 100644 testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py create mode 100644 testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_dataclasses.py new file mode 100644 index 000000000..3bbebe2aa --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses.py @@ -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 diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py new file mode 100644 index 000000000..63b9f534e --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py @@ -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 diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py new file mode 100644 index 000000000..17835c0c3 --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py @@ -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 diff --git a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py new file mode 100644 index 000000000..24f185d8a --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py @@ -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 diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 4a09187b7..2a55f70bc 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -551,78 +551,47 @@ class TestAssert_reprcompare(object): class TestAssert_reprcompare_dataclass(object): @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_dataclasses(self): - from dataclasses import dataclass, field - - @dataclass - class SimpleDataObject(object): - field_a: int = field() - field_b: int = field() - - left = SimpleDataObject(1, "b") - right = SimpleDataObject(1, "c") - - lines = callequal(left, right) - assert lines[1].startswith("Omitting 1 identical item") - assert "Common attributes" not in lines - for line in lines[1:]: - assert "field_a" not in line + 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:*", + "*SimpleDataObject(field_b='b') != SimpleDataObject(field_b='c')*", + ] + ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_dataclasses_verbose(self): - from dataclasses import dataclass, field - - @dataclass - class SimpleDataObject(object): - field_a: int = field() - field_b: int = field() - - left = SimpleDataObject(1, "b") - right = SimpleDataObject(1, "c") - - lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common attributes:") - assert "Omitting" not in lines[1] - assert lines[2] == "['field_a']" + 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( + [ + "*Common attributes:*", + "*['field_a']*", + "*Differing attributes:*", + "*SimpleDataObject(field_b='b') != SimpleDataObject(field_b='c')*", + ] + ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_dataclasses_with_attribute_comparison_off(self): - from dataclasses import dataclass, field - - @dataclass - class SimpleDataObject(object): - field_a: int = field() - field_b: int = field(compare=False) - - left = SimpleDataObject(1, "b") - right = SimpleDataObject(1, "b") - - lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common 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_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): - from dataclasses import dataclass, field - - @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") - - lines = callequal(left, right) - assert lines is None + 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): From b83e97802e91ff74c6a437bb076de65bf2423fa1 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Tue, 13 Nov 2018 09:37:02 -0600 Subject: [PATCH 13/14] improve failure output --- src/_pytest/assertion/util.py | 8 +++----- testing/test_assertion.py | 12 ++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 4e10a3fbb..3ec9a365a 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -345,15 +345,13 @@ def _compare_eq_cls(left, right, verbose, type_fns): if same and verbose < 2: explanation.append(u"Omitting %s identical items, use -vv to show" % len(same)) elif same: - explanation += [u"Common attributes:"] + explanation += [u"Matching attributes:"] explanation += pprint.pformat(same).splitlines() if diff: - class_name = left.__class__.__name__ explanation += [u"Differing attributes:"] - for k in diff: + for field in diff: explanation += [ - (u"%s(%s=%r) != %s(%s=%r)") - % (class_name, k, getattr(left, k), class_name, k, getattr(right, k)) + (u"%s: %r != %r") % (field, getattr(left, field), getattr(right, field)) ] return explanation diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 2a55f70bc..bb54e394f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -559,7 +559,7 @@ class TestAssert_reprcompare_dataclass(object): [ "*Omitting 1 identical items, use -vv to show*", "*Differing attributes:*", - "*SimpleDataObject(field_b='b') != SimpleDataObject(field_b='c')*", + "*field_b: 'b' != 'c'*", ] ) @@ -570,10 +570,10 @@ class TestAssert_reprcompare_dataclass(object): result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Common attributes:*", + "*Matching attributes:*", "*['field_a']*", "*Differing attributes:*", - "*SimpleDataObject(field_b='b') != SimpleDataObject(field_b='c')*", + "*field_b: 'b' != 'c'*", ] ) @@ -606,7 +606,7 @@ class TestAssert_reprcompare_attrsclass(object): lines = callequal(left, right) assert lines[1].startswith("Omitting 1 identical item") - assert "Common attributes" not in lines + assert "Matching attributes" not in lines for line in lines[1:]: assert "field_a" not in line @@ -620,7 +620,7 @@ class TestAssert_reprcompare_attrsclass(object): right = SimpleDataObject(1, "c") lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common attributes:") + assert lines[1].startswith("Matching attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" @@ -634,7 +634,7 @@ class TestAssert_reprcompare_attrsclass(object): right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) - assert lines[1].startswith("Common attributes:") + assert lines[1].startswith("Matching attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" for line in lines[2:]: From d52ea4b6cf1b7a552d52a78acfd3b3b7f642bcc6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 19 Nov 2018 20:06:06 -0200 Subject: [PATCH 14/14] Use python 3 in 'doctesting' environment We some examples now use type annotations --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dbfd4eef5..b9e12983b 100644 --- a/tox.ini +++ b/tox.ini @@ -141,7 +141,7 @@ commands = sphinx-build -W -b html . _build [testenv:doctesting] -basepython = python +basepython = python3 skipsdist = True deps = PyYAML