assert-matcher 0.9__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ VERSION = '0.9'
@@ -0,0 +1,35 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import pytest
4
+ from _pytest.python_api import ApproxBase
5
+
6
+
7
+ def approx(expected, rel=None, abs=None, nan_ok=False):
8
+ if isinstance(expected, datetime):
9
+ if rel is not None:
10
+ raise TypeError("rel doesn't make sense with a datetime")
11
+ return ApproxDatetime(expected, abs=abs)
12
+
13
+ return pytest.approx(expected, rel=rel, abs=abs, nan_ok=nan_ok)
14
+
15
+
16
+ class ApproxDatetime(ApproxBase):
17
+ """Perform approximate comparisons between datetime."""
18
+ default_abs = timedelta(seconds=1)
19
+
20
+ def __init__(self, expected, abs=None):
21
+ assert isinstance(expected, datetime)
22
+
23
+ if abs is None:
24
+ abs = self.default_abs
25
+ elif isinstance(abs, int):
26
+ abs = timedelta(seconds=abs)
27
+
28
+ assert abs >= timedelta(0), f"absolute tolerance can't be negative: {abs}"
29
+ super().__init__(expected, abs=abs)
30
+
31
+ def __repr__(self):
32
+ return f"approx({self.expected} ± {self.abs})"
33
+
34
+ def __eq__(self, actual: object) -> bool:
35
+ return abs(self.expected - actual) <= self.abs
@@ -0,0 +1,244 @@
1
+ import pprint
2
+ from collections import defaultdict
3
+ from collections.abc import Mapping, Set
4
+ from functools import partial
5
+ from itertools import count
6
+
7
+ from _pytest._io.saferepr import safeformat, saferepr
8
+ from _pytest.assertion.util import issequence, assertrepr_compare
9
+
10
+ from .matchers import Matcher, MatcherDict
11
+
12
+
13
+ def pytest_assertrepr_compare(config, op: str, left, right):
14
+ import shutil
15
+ import _pytest._code
16
+
17
+ verbose = config.get_verbosity() if hasattr(config, "get_verbosity") \
18
+ else config.getoption("verbose")
19
+ if verbose > 1:
20
+ left_repr = safeformat(left)
21
+ right_repr = safeformat(right)
22
+ else:
23
+ hspace = max(80, shutil.get_terminal_size().columns - 10)
24
+ hspace -= len("E AssertionError: assert") + len(op) + 2 # 2 spaces
25
+ left_repr = saferepr(left, maxsize=hspace // 2)
26
+ right_repr = saferepr(right, maxsize=hspace - len(left_repr))
27
+
28
+ try:
29
+ explanation = to_lines(compare(config, op, left, right))
30
+ # NOTE: not calling _compare_eq_iterable() anywhere for now
31
+ except Exception:
32
+ crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
33
+ explanation = [
34
+ "assert_matcher comparison failed:",
35
+ f" {crash}",
36
+ "Probably an object has a faulty __repr__.",
37
+ ]
38
+
39
+ if not explanation:
40
+ return None
41
+
42
+ summary = f"{left_repr} {op} {right_repr}"
43
+ return [summary] + explanation
44
+
45
+
46
+ def compare(config, op, left, right):
47
+ explanation = []
48
+ for test, explain in EXPLAINERS[op]:
49
+ if test(left, right):
50
+ return explain(config, left, right)
51
+ return explanation
52
+
53
+
54
+ def compare_eq(config, left, right):
55
+ return compare(config, "==", left, right) or [f"{left!r} != {right!r}"]
56
+
57
+
58
+ # Explainers framework
59
+
60
+ EXPLAINERS = defaultdict(list)
61
+
62
+
63
+ def register_explainer(op, test, explain=None):
64
+ if explain is None:
65
+ return partial(register_explainer, op, test)
66
+ EXPLAINERS[op].append((test, explain))
67
+ return explain # Return the function when we use this as a decorator
68
+
69
+
70
+ def both(test):
71
+ return lambda left, right: test(left) and test(right)
72
+
73
+
74
+ def bothis(cls):
75
+ return lambda left, right: isinstance(left, cls) and isinstance(right, cls)
76
+
77
+
78
+ def anyis(cls):
79
+ return lambda left, right: isinstance(left, cls) or isinstance(right, cls)
80
+
81
+
82
+ @register_explainer("==", anyis(MatcherDict))
83
+ def _explain_m_dict(config, left, right):
84
+ if isinstance(left, MatcherDict) and isinstance(right, MatcherDict):
85
+ return ["Should not compare two M.dict() instances"]
86
+
87
+ if isinstance(left, MatcherDict):
88
+ matcher_side, val, matcher, left_d, right_d = "Left", right, left, left.d, right
89
+ else:
90
+ matcher_side, val, matcher, left_d, right_d = "Right", left, right, left, right.d
91
+
92
+ explanation = _dict_extra(matcher_side, matcher.d, val)
93
+
94
+ # NOTE: not using sets here to use the matchers keys order
95
+ diff = _dict_diff(config, [k for k in matcher.d if k in val], left_d, right_d)
96
+ return explanation + diff
97
+
98
+
99
+ @register_explainer("==", bothis(Mapping))
100
+ def _explain_dict(config, left, right):
101
+ explanation = []
102
+
103
+ for side, more, less in [("Left", left, right), ("Right", right, left)]:
104
+ explanation.extend(_dict_extra(side, more, less))
105
+
106
+ # NOTE: not using sets here to preserve keys order
107
+ diff = _dict_diff(config, [k for k in right if k in left], left, right)
108
+ return explanation + diff
109
+
110
+
111
+ def _dict_extra(side, more, less):
112
+ explanation = []
113
+ extra = {k: v for k, v in more.items() if k not in less}
114
+ if extra:
115
+ n = len(extra)
116
+ explanation.append(f"{side} contains {n} more item{'' if n == 1 else 's'}:")
117
+ explanation.extend(pprint.pformat(extra).splitlines())
118
+ return explanation
119
+
120
+
121
+ def _dict_diff(config, keys, left, right):
122
+ # NOTE: we use simple key with no ["..."] wrapping to make it cleaner
123
+ items = {k: compare_eq(config, left[k], right[k]) for k in keys if left[k] != right[k]}
124
+ return Explanation(prefix="Differing items:", items=items) if items else []
125
+
126
+
127
+ class Missing:
128
+ def __str__(self):
129
+ return "<missing>"
130
+
131
+ __repr__ = __str__
132
+
133
+
134
+ missing = Missing()
135
+
136
+
137
+ @register_explainer("==", anyis(Matcher))
138
+ def _explain_m(config, left, right):
139
+ if isinstance(left, Matcher) and isinstance(right, Matcher):
140
+ return ["Should not compare two M() instances"]
141
+
142
+ if isinstance(left, Matcher):
143
+ matcher, left_d = left, left.attrs
144
+ right_d = {k: getattr(right, k, missing) for k in left_d}
145
+ else:
146
+ matcher, right_d = right, right.attrs
147
+ left_d = {k: getattr(left, k, missing) for k in right_d}
148
+
149
+ return _dict_diff(config, matcher.attrs, left_d, right_d)
150
+
151
+
152
+ @register_explainer("==", bothis(Set))
153
+ def _explain_set(config, left, right):
154
+ return assertrepr_compare(config, "==", left, right)
155
+
156
+
157
+ @register_explainer("==", bothis(str))
158
+ def _explain_str(config, left, right):
159
+ return assertrepr_compare(config, "==", left, right)
160
+
161
+
162
+ # TODO: catch a situation when some item was removed in the middle?
163
+ # what about swap?
164
+ @register_explainer("==", both(issequence))
165
+ def _explain_seq(config, left, right):
166
+ if isinstance(left, bytes) and isinstance(right, bytes):
167
+ return assertrepr_compare(config, "==", left, right)
168
+
169
+ explanation = []
170
+ for i, l, r in zip(count(), left, right):
171
+ if l != r: # noqa
172
+ explanation = Explanation("First mismatch:", {f"[{i}]": compare_eq(config, l, r)})
173
+ # NOTE: we are ignoring other mismatches, and "First mismatch:" above won't be shown
174
+ # if the below section will yield nothing
175
+ break
176
+
177
+ len_diff = len(left) - len(right)
178
+ if len_diff:
179
+ if len_diff > 0:
180
+ side, extra = "Left", left[len(right)]
181
+ else:
182
+ side, extra = "Right", right[len(left)]
183
+ len_diff = -len_diff
184
+
185
+ if len_diff == 1:
186
+ explanation += [f"{side} contains one more item: {extra!r}"]
187
+ else:
188
+ explanation += [f"{side} contains {len_diff} more items, first one: {extra!r}"]
189
+ return explanation
190
+
191
+
192
+ class Explanation(dict):
193
+ """An explanation of a diff for dicts and objects.
194
+
195
+ Converted to lines of strings before returning to pytest. This delaying of rendering allows to
196
+ join keys when we find differences in nested structures.
197
+
198
+ Allows separating diff collection from rendering partially. We want to reuse some of
199
+ the _pytest.assertion.util code, so we work with both this and list[str].
200
+ """
201
+ def __init__(self, prefix, items):
202
+ self._prefix = prefix
203
+ self.update(items)
204
+
205
+ def add(self, other, reverse=False):
206
+ if not other:
207
+ return self
208
+ elif isinstance(other, list):
209
+ rendered = [self._prefix] + to_lines(self)
210
+ return other + rendered if reverse else rendered + other
211
+ raise NotImplementedError
212
+
213
+ def __add__(self, other):
214
+ return self.add(other)
215
+
216
+ def __radd__(self, other):
217
+ return self.add(other, reverse=True)
218
+
219
+
220
+ def to_lines(expl, path=""):
221
+ """Coerce explanation to lines of strings"""
222
+ if isinstance(expl, list):
223
+ return expl
224
+
225
+ # NOTE: we only escape string keys if they contain "." for readablity.
226
+ # This makes object attrs look the same as dict keys though.
227
+ def escape_key(k):
228
+ return repr(k) if not k.startswith("[") and "." in k else k
229
+
230
+ explanation = []
231
+ for k, lines in expl.items():
232
+ # full_path = f"{path}.{escape_key(k)}" if path else escape_key(k)
233
+ if path:
234
+ full_path = path + k if k.startswith("[") else f"{path}.{escape_key(k)}"
235
+ else:
236
+ full_path = escape_key(k)
237
+ if isinstance(lines, dict):
238
+ explanation.extend(to_lines(lines, path=full_path))
239
+ elif len(lines) == 1:
240
+ explanation.append(f"{full_path}: " + lines[0])
241
+ else:
242
+ explanation.append(f"{full_path}:")
243
+ explanation.extend([" " + l for l in lines]) # noqa
244
+ return explanation
@@ -0,0 +1,150 @@
1
+ import re
2
+
3
+
4
+ class MatcherRegex:
5
+ """Special class to eq by matching regex"""
6
+ def __init__(self, pattern, flags=0):
7
+ self.regex = re.compile(pattern, flags)
8
+
9
+ def __repr__(self):
10
+ flags = self.regex.flags & ~32 # 32 is default
11
+ if flags:
12
+ return f"M.re(r'{self.regex.pattern}', {flags})"
13
+ else:
14
+ return f"r'{self.regex.pattern}'"
15
+
16
+ def __eq__(self, other):
17
+ return self.regex.search(other)
18
+
19
+
20
+ class MatcherDict:
21
+ """Special class to eq by matching only presented dict keys"""
22
+ # NOTE: can't inherit from dict because that makes D() == M.dict() to not call our __eq__,
23
+ # if D is a subclass of a dict
24
+ def __init__(self, __d=None, **keys):
25
+ self.d = __d or {}
26
+ if keys:
27
+ self.d = self.d.copy()
28
+ self.d.update(keys)
29
+
30
+ def __repr__(self):
31
+ return f"M.dict({', '.join(f'{k}={repr(v)}' for k, v in self.d.items())})"
32
+
33
+ def __len__(self):
34
+ return len(self.d)
35
+
36
+ def __eq__(self, other):
37
+ missing = object()
38
+ return all(other.get(name, missing) == v for name, v in self.d.items())
39
+
40
+
41
+ class MatcherAny:
42
+ """Equals to anything, a way to ignore parts of data structures on comparison"""
43
+ def __repr__(self):
44
+ return "M.any"
45
+
46
+ def __eq__(self, other):
47
+ return True
48
+
49
+
50
+ class MatcherAnyOf:
51
+ def __init__(self, *items):
52
+ self.items = items
53
+
54
+ def __repr__(self) -> str:
55
+ inner = ", ".join(map(repr, self.items))
56
+ return f"M.any_of({inner})"
57
+
58
+ def __eq__(self, other: object) -> bool:
59
+ return other in self.items
60
+
61
+
62
+ class MatcherUnordered:
63
+ """Compare list contents, but do not care about ordering. (E.g. sort lists first,
64
+ then compare.) If you care about ordering, then just compare lists directly."""
65
+ def __init__(self, items):
66
+ self.items = sorted(items)
67
+
68
+ def __repr__(self):
69
+ inner = ", ".join(map(repr, self.items))
70
+ return f"M.unordered({inner})"
71
+
72
+ def __eq__(self, other):
73
+ return self.items == sorted(other)
74
+
75
+
76
+ class MatcherIsa:
77
+ def __init__(self, *types):
78
+ assert all(isinstance(t, type) for t in types)
79
+ self.types = types
80
+
81
+ def __repr__(self):
82
+ inner = ", ".join(t.__name__ for t in self.types)
83
+ return f"M.isa({inner})"
84
+
85
+ def __eq__(self, other):
86
+ return isinstance(other, self.types)
87
+
88
+
89
+ class Matcher:
90
+ """Special class to eq by existing attrs. Also provides contructors for other types of matching:
91
+ - regex
92
+ - dict
93
+ - unordered sequence
94
+ - by class
95
+ - "any of"
96
+ - any wildcard
97
+
98
+ The purpose is to simplify asserts containing objects and nested data structures, i.e.:
99
+
100
+ assert result.errors == [
101
+ M(message=M.re("^Smth went wrong:"), extensions=M.dict(code=523)),
102
+ M(message=M.any, tags=M.unordered("one", "two")),
103
+ ]
104
+
105
+ Here all the structures like lists and dicts are followed as usual both outside and inside
106
+ a mather object. These could be freely intermixed.
107
+ """
108
+ def __init__(self, **attrs):
109
+ self.attrs = attrs
110
+
111
+ def __repr__(self):
112
+ name = "M"
113
+ attrs = self.attrs
114
+ if "__class__" in attrs:
115
+ name += "." + attrs["__class__"].__name__
116
+ attrs = {k: v for k, v in attrs.items() if k != "__class__"}
117
+ return f"{name}({', '.join(f'{k}={repr(v)}' for k, v in attrs.items())})"
118
+
119
+ def __eq__(self, other):
120
+ # Unforturnately this doesn't work with classes with slots
121
+ # self.__class__ = other.__class__
122
+ missing = object()
123
+ return all(getattr(other, name, missing) == v for name, v in self.attrs.items())
124
+
125
+ any = MatcherAny()
126
+
127
+ @staticmethod
128
+ def any_of(*items):
129
+ return MatcherAnyOf(*items)
130
+
131
+ @staticmethod
132
+ def re(pattern, flags=0):
133
+ return MatcherRegex(pattern, flags)
134
+
135
+ @staticmethod
136
+ def dict(_d=None, **keys):
137
+ return MatcherDict(_d, **keys)
138
+
139
+ @staticmethod
140
+ def unordered(*elems):
141
+ return MatcherUnordered(elems)
142
+
143
+ @staticmethod
144
+ def isa(*elems):
145
+ return MatcherIsa(*elems)
146
+
147
+ def approx(expected, rel=None, abs=None, nan_ok=False):
148
+ from .approx import approx # Requires pytest
149
+
150
+ return approx(expected, rel=rel, abs=abs, nan_ok=nan_ok)
@@ -0,0 +1,9 @@
1
+ import pytest
2
+
3
+ from .matchers import Matcher
4
+ from .compare import pytest_assertrepr_compare # noqa
5
+
6
+
7
+ @pytest.fixture
8
+ def M():
9
+ return Matcher
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: assert-matcher
3
+ Version: 0.9
4
+ Summary: Makes comparing nested data structures easy and glancable
5
+ Author: Alexander Schepanovski
6
+ Author-email: suor.web@gmail.com
7
+ License: BSD-2-Clause
8
+ Keywords: tests
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: Pytest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Operating System :: Unix
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: Implementation :: CPython
26
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
27
+ Classifier: Topic :: Software Development :: Testing
28
+ Classifier: Topic :: Utilities
29
+ Requires-Python: >=3.7
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: license-file
35
+
36
+ # Assert Matcher [![Build Status](https://github.com/Suor/assert-matcher/actions/workflows/ci.yml/badge.svg)](https://github.com/Suor/assert-matcher/actions/workflows/test.yml?query=branch%3Amaster)
37
+
38
+ The purpose of this library is to simplify asserts containing objects and nested data structures, i.e.:
39
+
40
+ ```python
41
+ def test_smth(M): # A special Matcher fixture
42
+ # ...
43
+ assert result.errors == [
44
+ M(message=M.re("^Smth went wrong:"), extensions=M.dict(code=523)),
45
+ M(message=M.any, tags=M.unordered("one", "two")),
46
+ ]
47
+ ```
48
+
49
+ Here all the structures like lists and dicts are followed as usual both outside and inside
50
+ a matcher object, while the matcher object and other helpers provide their own equality. These could be freely intermixed.
51
+
52
+ Alternatively may import Matcher directly this is more verbose but also more universal, i.e. works outside of pytest:
53
+
54
+ ```python
55
+ from assert_matcher import Matcher as M
56
+ ```
57
+
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install git+https://github.com/Suor/assert-matcher.git@master
63
+ # pip install assert-matcher # not released yet
64
+ ```
65
+
66
+ ## Matchers
67
+
68
+ The `M` fixture provides a number of matchers to help you write more expressive assertions.
69
+
70
+ #### `M(**attrs)`
71
+
72
+ Matches an object's attributes.
73
+
74
+ ```python
75
+ User = namedtuple("User", ["name", "email", "is_active"])
76
+
77
+ users = [User("Alice", "alice@example.com", True), User("Bob", "bob@example.com", False)]
78
+
79
+ assert users == [
80
+ M(name="Alice", is_active=True),
81
+ M(name="Bob", is_active=False),
82
+ ]
83
+ ```
84
+
85
+ #### `M.re(pattern, flags=0)`
86
+
87
+ Matches a string against a regular expression using `re.search`.
88
+
89
+ ```python
90
+ assert "hello world" == M.re(r"h.*o")
91
+ ```
92
+
93
+ #### `M.dict(d=None, **kwargs)`
94
+
95
+ Matches a dictionary against a subset of keys. It checks that the keys exist and their values match, but ignores other keys.
96
+
97
+ ```python
98
+ assert {"a": 1, "b": 2} == M.dict(a=1)
99
+ ```
100
+
101
+ #### M.any
102
+
103
+ Matches any value. This is useful when you want to ignore a particular field value in a larger data structure, but still want to test for a key presence.
104
+
105
+ ```python
106
+ assert {"a": 1, "b": 2, "c": "hi"} == M.dict(a=1, b=M.any)
107
+ ```
108
+
109
+ #### `M.any_of(*items)`
110
+
111
+ Matches if the value is one of the provided items.
112
+
113
+ ```python
114
+ assert "a" == M.any_of("a", "b", "c")
115
+ ```
116
+
117
+ #### `M.unordered(*items)`
118
+
119
+ Compares a list to the provided items, ignoring the order.
120
+
121
+ ```python
122
+ assert [1, 2, 3] == M.unordered(3, 1, 2)
123
+ ```
124
+
125
+ #### `M.isa(*types)`
126
+
127
+ Checks if an object is an instance of one of the provided types.
128
+
129
+ ```python
130
+ assert "hello" == M.isa(str)
131
+ assert 42 == M.isa(int, float)
132
+ ```
133
+
134
+ #### `M.approx(expected, rel=None, abs=None, nan_ok=False)`
135
+
136
+ Performs approximate comparison of numbers. It's a wrapper around `pytest.approx`.
137
+
138
+ ```python
139
+ assert 0.1 + 0.2 == M.approx(0.3)
140
+ ```
141
+
142
+ ## Running tests
143
+
144
+ To run the tests using your default python:
145
+
146
+ ```
147
+ pip install -r test_requirements.txt
148
+ pytest
149
+ ```
150
+
151
+ To fully run `tox` you need all the supported pythons to be installed. These are
152
+ 3.7+ and PyPy3. You can run it for particular environment even in absense
153
+ of all of the above:
154
+
155
+ ```
156
+ tox -e py310
157
+ tox -e pypy3
158
+ tox -e lint
159
+ ```
160
+
161
+ ## Pytest Plugin
162
+
163
+ `assert-matcher` provides enhanced assertion representation for pytest, making diffs for nested data structures more readable when using `Matcher` objects.
164
+
165
+ When a test fails, pytest usually prints a diff of the two objects that were compared. However, these diffs can be hard to read. `assert-matcher` plugin overrides the default diff representation to pinpoint an exact path and difference.
166
+
167
+ For example, consider the following failing test:
168
+
169
+ ```python
170
+ def test_user(M):
171
+ user = User("Alice", "alice@example.com", True)
172
+ assert user == M(name="Bob", is_active=False)
173
+ ```
174
+
175
+ Without the plugin, the diff would look something like this:
176
+
177
+ ```
178
+ E AssertionError: assert User(name='Alice', email='alice@example.com', is_active=True) == M(name='Bob', is_active=False)
179
+ E + where M(name='Bob', is_active=False) = <class 'assert_matcher.matchers.Matcher'>(name='Bob', is_active=False)
180
+ ```
181
+
182
+ With the `assert-matcher` plugin enabled, the diff is much more readable:
183
+
184
+ ```
185
+ E AssertionError: assert User(name=...tive=True) == M(name='Bo...ive=False)
186
+ E name: 'Alice' != 'Bob'
187
+ E is_active: True != False
188
+ ```
189
+
190
+ This is even more useful when you compare nested data structures. For example, consider the following test:
191
+
192
+ ```python
193
+ def test_nested_structure(M):
194
+ result = {
195
+ "users": [alice, bob],
196
+ "meta": ...
197
+ }
198
+ expected = {
199
+ "users": [
200
+ M(name="Alice", status="active"),
201
+ M(name="Bob", email=M.re(r"@example\.com$")),
202
+ ],
203
+ "meta": M.any
204
+ }
205
+ assert result == expected
206
+ ```
207
+
208
+ Without the plugin, the diff is hard to read (imagine these objects having 10+ fields):
209
+
210
+ ```
211
+ E AssertionError: assert {'meta': {'er... 'inactive'}]} == {'meta': M.di...ample.com$')]}
212
+ E
213
+ E Omitting 1 identical items, use -vv to show
214
+ E Differing items:
215
+ E {'users': [{'email': 'alice@example.com', 'name': 'Alice', 'status': 'active'}, {'email': 'bob@invalid', 'name': 'Bob', 'status': 'inactive'}]} != {'users': [M.dict(name='Alice', status='active'), M.dict(name='Bob', email=r'@example.com$')]}
216
+ E Use -v to get more diff
217
+ ```
218
+
219
+ With the plugin, the diff is much more concise and points to the exact location of the error:
220
+
221
+ ```
222
+ E AssertionError: assert {'meta': {...active'}} == {'meta': M...e.com$')]}
223
+ E users[1].email: 'bob@invalid' != r'@example.com$'
224
+ ```
@@ -0,0 +1,11 @@
1
+ assert_matcher/__init__.py,sha256=OtKAcQua1NGDSb_UJWgSKQ6Und0i3CWwIdvGt96b28k,16
2
+ assert_matcher/approx.py,sha256=LrbDnmNy8erhEYEGkZtlBBjEceXlDocH0BHHCq1ClAI,1084
3
+ assert_matcher/compare.py,sha256=-tG7RWwC4wUlgZXqtKNcpkV1kUy1RH8ZczVzvBo2hXs,8029
4
+ assert_matcher/matchers.py,sha256=svL5bIxiM71hHogMzD6sA-32_dojQ17fmyq8WpD7QCw,4318
5
+ assert_matcher/plugin.py,sha256=1W-YbroJpko0stx0eywCWGUld0tbNoqXT9Rj_5r12IE,146
6
+ assert_matcher-0.9.dist-info/licenses/LICENSE,sha256=RDmD_AtTfAXUlLdoR9RTYntoB34bkQ2riQAtvXaFpjw,1335
7
+ assert_matcher-0.9.dist-info/METADATA,sha256=TcGbFHNDbvVW79tuuK23eNNt99Kwzu49ljLShAvSU-s,6782
8
+ assert_matcher-0.9.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
9
+ assert_matcher-0.9.dist-info/entry_points.txt,sha256=HlJ0VQCbODwgXH4mAvzAsGLzc0PNXvhJLSCvaR9EdtY,50
10
+ assert_matcher-0.9.dist-info/top_level.txt,sha256=z13M84xUaDjLpcq-slO9ulLr5iXa9g_oNws68BL6l-k,15
11
+ assert_matcher-0.9.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ assert_matcher = assert_matcher.plugin
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2022-2026, Alexander Schepanovski.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ assert_matcher