assertpy 2.35.0__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.
Files changed (63) hide show
  1. assertpy/__init__.py +87 -0
  2. assertpy/_clustering.py +511 -0
  3. assertpy/_dangling.py +505 -0
  4. assertpy/_engine/__init__.py +1 -0
  5. assertpy/_engine/_builder_check_typing.py +1365 -0
  6. assertpy/_engine/_capable_typing.py +772 -0
  7. assertpy/_engine/_check_typing.py +753 -0
  8. assertpy/_engine/_compare.py +983 -0
  9. assertpy/_engine/_compat.py +38 -0
  10. assertpy/_engine/_contract.py +1926 -0
  11. assertpy/_engine/_diff.py +1058 -0
  12. assertpy/_engine/_equality.py +843 -0
  13. assertpy/_engine/_introspection.py +729 -0
  14. assertpy/_engine/_membership.py +522 -0
  15. assertpy/_engine/_mixin_base.py +258 -0
  16. assertpy/_engine/_negated_typing.py +2176 -0
  17. assertpy/_engine/_operations.py +75 -0
  18. assertpy/_engine/_ordering.py +884 -0
  19. assertpy/_engine/_pairing.py +70 -0
  20. assertpy/_engine/_path.py +158 -0
  21. assertpy/_engine/_poll_typing.py +6039 -0
  22. assertpy/_engine/_require.py +312 -0
  23. assertpy/_engine/_size.py +31 -0
  24. assertpy/_engine/_text.py +39 -0
  25. assertpy/_engine/_typing.py +1226 -0
  26. assertpy/_hints.py +1213 -0
  27. assertpy/_inline.py +484 -0
  28. assertpy/_matcher_impls.py +1876 -0
  29. assertpy/_passes.py +249 -0
  30. assertpy/_renamed.py +64 -0
  31. assertpy/_satisfies.py +835 -0
  32. assertpy/_snapshot_codec.py +290 -0
  33. assertpy/assertpy.py +2640 -0
  34. assertpy/async_assertions.py +864 -0
  35. assertpy/base.py +862 -0
  36. assertpy/behave_matchers.py +96 -0
  37. assertpy/bytes_mixin.py +158 -0
  38. assertpy/collection.py +528 -0
  39. assertpy/contains.py +1068 -0
  40. assertpy/dataframe.py +200 -0
  41. assertpy/date.py +327 -0
  42. assertpy/dict.py +249 -0
  43. assertpy/dynamic.py +222 -0
  44. assertpy/errors.py +782 -0
  45. assertpy/exception.py +838 -0
  46. assertpy/extracting.py +266 -0
  47. assertpy/file.py +324 -0
  48. assertpy/helpers.py +747 -0
  49. assertpy/http_mixin.py +218 -0
  50. assertpy/json_mixin.py +1265 -0
  51. assertpy/matchers.py +639 -0
  52. assertpy/numeric.py +696 -0
  53. assertpy/outcome.py +163 -0
  54. assertpy/py.typed +0 -0
  55. assertpy/pytest_plugin.py +1650 -0
  56. assertpy/snapshot.py +1031 -0
  57. assertpy/string.py +803 -0
  58. assertpy/warning.py +137 -0
  59. assertpy-2.35.0.dist-info/METADATA +324 -0
  60. assertpy-2.35.0.dist-info/RECORD +63 -0
  61. assertpy-2.35.0.dist-info/WHEEL +4 -0
  62. assertpy-2.35.0.dist-info/entry_points.txt +2 -0
  63. assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
assertpy/warning.py ADDED
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ from ._engine._mixin_base import _MixinBase
7
+ from ._engine._require import refuse
8
+ from .errors import _callable_name, _type_expression_name
9
+ from .exception import _InertBuilder, _require_synchronous_result, named_classes
10
+
11
+ if TYPE_CHECKING:
12
+ from ._engine._compat import Self
13
+ from ._matcher_impls import WarningInfo
14
+
15
+ __tracebackhide__ = True
16
+
17
+
18
+ class WarningMixin(_MixinBase):
19
+ """Expected warning mixin (a ``pytest.warns``-style assertion).
20
+
21
+ The front verbs ([`warns()`][assertpy.warning.WarningMixin.warns] /
22
+ [`does_not_warn()`][assertpy.warning.WarningMixin.does_not_warn]) only record the expected
23
+ warning
24
+ category; the actual invocation happens in
25
+ [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with], which dispatches to the private
26
+ handlers below.
27
+ """
28
+
29
+ def warns(self, warning: type[Warning] | WarningInfo = Warning) -> Self:
30
+ """Asserts that val is callable and sets the expected warning category.
31
+
32
+ Just records the expectation, but never calls val. You must chain to
33
+ [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with] to invoke ``val()`` inside a
34
+ warning-capturing context. On success, the matched warning's message becomes the new val so
35
+ you can chain further assertions on it.
36
+
37
+ Subclasses of the given category match, and the category defaults to the base `Warning`
38
+ (which matches any warning).
39
+
40
+ Args:
41
+ warning: the expected warning category (a `Warning` subclass), or a union or a tuple of
42
+ them any one of which will do
43
+
44
+ Examples:
45
+ Usage:
46
+
47
+ assert_that(deprecated_func).warns(DeprecationWarning).when_called_with("foo")
48
+ assert_that(deprecated_func).warns(DeprecationWarning).when_called_with("foo").matches("since 2.6")
49
+
50
+ Returns:
51
+ AssertionBuilder: returns a new instance (now with the expected warning) to chain to
52
+ [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with]
53
+
54
+ Note:
55
+ Capturing warnings mutates process-global state, so this is **not** thread-safe across OS
56
+ threads (the same caveat as ``pytest.warns``). It is safe within a single thread,
57
+ including multiple ``asyncio`` tasks on one event loop.
58
+ """
59
+ return self._warning_builder(warning)
60
+
61
+ def does_not_warn(self, warning: type[Warning] | WarningInfo = Warning) -> Self:
62
+ """Asserts that val is callable and sets the not-expected warning category.
63
+
64
+ Just records the expectation, but never calls val. You must chain to
65
+ [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with] to invoke ``val()`` and assert
66
+ that no warning of the given category is emitted. The category defaults to the base
67
+ `Warning` (which forbids any warning).
68
+
69
+ Args:
70
+ warning: the warning category that should **not** be emitted (a `Warning` subclass), or a
71
+ union or a tuple of them
72
+
73
+ Examples:
74
+ Usage:
75
+
76
+ assert_that(safe_func).does_not_warn(DeprecationWarning).when_called_with("foo")
77
+
78
+ Returns:
79
+ AssertionBuilder: returns a new instance to chain to
80
+ [`when_called_with()`][assertpy.exception.ExceptionMixin.when_called_with]
81
+ """
82
+ new_builder = self._warning_builder(warning)
83
+ new_builder._not_expected = True
84
+ return new_builder
85
+
86
+ def _warning_builder(self, warning: type[Warning] | WarningInfo) -> Self:
87
+ """Validate args and build a new instance carrying the expected warning category."""
88
+ if not callable(self.val):
89
+ refuse(self.val, "callable")
90
+ # every member asked here: `issubclass` on a non-class raises with a message about the builtin instead
91
+ named_classes(warning, Warning, "a warning type, or a union or a non-empty tuple of them", subject="warning")
92
+ new_builder = self.builder(self.val, self.description, self.kind, logger=self.logger)
93
+ new_builder._expected_warning = warning
94
+ return new_builder
95
+
96
+ def _when_called_with_warning(self, expected: type[Warning] | WarningInfo, *some_args, **some_kwargs) -> Self:
97
+ with warnings.catch_warnings(record=True) as caught:
98
+ warnings.simplefilter("always") # bypass __warningregistry__ "show once" dedup and filterwarnings=error
99
+ result = self.val(*some_args, **some_kwargs)
100
+ _require_synchronous_result(result, self.val)
101
+ matched = [warning for warning in caught if issubclass(warning.category, expected)]
102
+ if matched:
103
+ captured = self.builder(str(matched[0].message), self.description, self.kind, logger=self.logger)
104
+ captured._return_value = result
105
+ return captured
106
+ if caught:
107
+ seen = ", ".join(sorted({warning.category.__name__ for warning in caught}))
108
+ self.error(
109
+ f"Expected <{_callable_name(self.val)}> to warn <{_type_expression_name(expected)}>"
110
+ f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
111
+ f" but warned <{seen}>.",
112
+ expected=expected,
113
+ )
114
+ return cast("Self", _InertBuilder(self._value_taint_reason))
115
+ self.error(
116
+ f"Expected <{_callable_name(self.val)}> to warn <{_type_expression_name(expected)}>"
117
+ f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}).",
118
+ expected=expected,
119
+ )
120
+ return cast("Self", _InertBuilder(self._value_taint_reason))
121
+
122
+ def _when_called_with_not_warning(self, expected: type[Warning] | WarningInfo, *some_args, **some_kwargs) -> Self:
123
+ with warnings.catch_warnings(record=True) as caught:
124
+ warnings.simplefilter("always") # bypass __warningregistry__ "show once" dedup and filterwarnings=error
125
+ result = self.val(*some_args, **some_kwargs)
126
+ _require_synchronous_result(result, self.val)
127
+ matched = [warning for warning in caught if issubclass(warning.category, expected)]
128
+ if matched:
129
+ seen = ", ".join(sorted({warning.category.__name__ for warning in matched}))
130
+ self.error(
131
+ f"Expected <{_callable_name(self.val)}> to not warn <{_type_expression_name(expected)}>"
132
+ f" when called with ({self._fmt_args_kwargs(*some_args, **some_kwargs)}),"
133
+ f" but did warn <{seen}>."
134
+ )
135
+ return cast("Self", _InertBuilder(self._value_taint_reason))
136
+ self._return_value = result
137
+ return self
@@ -0,0 +1,324 @@
1
+ Metadata-Version: 2.5
2
+ Name: assertpy
3
+ Version: 2.35.0
4
+ Summary: Fluent, fully typed assertion library for Python: your type checker sees only the assertions that fit the value, and every failure carries a structured diff. Soft assertions, polling, inline snapshots, OpenAPI contracts.
5
+ Project-URL: Homepage, https://github.com/assertpy/assertpy
6
+ Project-URL: Repository, https://github.com/assertpy/assertpy
7
+ Project-URL: Issues, https://github.com/assertpy/assertpy/issues
8
+ Author-email: Justin Shacklette <justin@saturnboy.com>
9
+ Maintainer-email: Solganis <solganis.dev@gmail.com>
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ Keywords: assert,assert_that,assertion,assertpy,assertthat,fluent,matchers,pytest,test,testing,type-safe,unittest
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
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 :: 3.15
26
+ Classifier: Topic :: Software Development
27
+ Classifier: Topic :: Software Development :: Testing
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
30
+ Provides-Extra: allure
31
+ Requires-Dist: allure-pytest>=2.13; extra == 'allure'
32
+ Provides-Extra: behave
33
+ Requires-Dist: behave>=1.3.0; extra == 'behave'
34
+ Provides-Extra: data
35
+ Requires-Dist: numpy>=1.26; extra == 'data'
36
+ Requires-Dist: pandas>=2.0; extra == 'data'
37
+ Requires-Dist: polars>=1.0; extra == 'data'
38
+ Provides-Extra: inline
39
+ Requires-Dist: asttokens>=2.0.4; extra == 'inline'
40
+ Requires-Dist: executing>=2.2; extra == 'inline'
41
+ Provides-Extra: json
42
+ Requires-Dist: idna>=3.7; extra == 'json'
43
+ Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
44
+ Requires-Dist: jsonschema>=4.18; extra == 'json'
45
+ Requires-Dist: referencing>=0.28.4; extra == 'json'
46
+ Provides-Extra: numpy
47
+ Requires-Dist: numpy>=1.26; extra == 'numpy'
48
+ Provides-Extra: pandas
49
+ Requires-Dist: pandas>=2.0; extra == 'pandas'
50
+ Provides-Extra: polars
51
+ Requires-Dist: polars>=1.0; extra == 'polars'
52
+ Description-Content-Type: text/markdown
53
+
54
+ <p align="center">
55
+ <picture>
56
+ <source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
57
+ <source media="(prefers-color-scheme: light)" srcset="docs/logo.svg">
58
+ <img src="docs/logo.svg" alt="assertpy" width="280">
59
+ </picture>
60
+ <br>
61
+ <b>Fully typed fluent assertion library for Python</b>
62
+ </p>
63
+
64
+ <p align="center">
65
+ <a href="https://github.com/assertpy/assertpy/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/assertpy/assertpy/ci.yml?branch=main&amp;label=CI" alt="CI"></a>
66
+ <a href="https://codecov.io/gh/assertpy/assertpy"><img src="https://codecov.io/gh/assertpy/assertpy/graph/badge.svg" alt="Coverage"></a>
67
+ <a href="https://pypi.org/project/assertpy/"><img src="https://img.shields.io/pypi/v/assertpy" alt="PyPI version"></a>
68
+ <a href="https://pypi.org/project/assertpy/"><img src="https://img.shields.io/pypi/pyversions/assertpy" alt="Python"></a>
69
+ <a href="https://pepy.tech/projects/assertpy"><img src="https://static.pepy.tech/badge/assertpy/month" alt="Downloads"></a>
70
+ <br>
71
+ <a href="https://assertpy.github.io/assertpy/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
72
+ <a href="https://assertpy.github.io/assertpy/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
73
+ <a href="https://scorecard.dev/viewer/?uri=github.com/assertpy/assertpy"><img src="https://img.shields.io/ossf-scorecard/github.com/assertpy/assertpy?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
74
+ </p>
75
+
76
+ <h2 align="center"><a href="https://assertpy.github.io/assertpy/getting-started/quickstart/">Quick start</a></h2>
77
+
78
+ ```bash
79
+ pip install assertpy
80
+ ```
81
+
82
+ ```python
83
+ from assertpy import assert_that
84
+
85
+ def test_user():
86
+ user = {"name": "Alice", "age": 30, "roles": ["viewer", "editor"]}
87
+
88
+ assert_that(user).contains_key("name", "age")
89
+ assert_that(user).contains_entry({"name": "Alice"})
90
+ assert_that(user["age"]).is_between(18, 120)
91
+ assert_that(user["roles"]).contains("viewer").does_not_contain("admin")
92
+ ```
93
+
94
+ The [full documentation](https://assertpy.github.io/assertpy/) covers every assertion, matcher, and integration.
95
+
96
+ <table>
97
+ <tr>
98
+ <td valign="top" width="50%">
99
+ <a href="#why-fluent-assertions"><b>Failures that point at the difference</b></a><br>
100
+ A recursive diff names the exact path that differs, in color, instead of dumping both structures
101
+ </td>
102
+ <td valign="top" width="50%">
103
+ <a href="#type-aware-autocomplete"><b>Type-aware autocomplete</b></a><br>
104
+ <code>assert_that()</code> returns a protocol per value type, so your IDE offers the methods that fit
105
+ </td>
106
+ </tr>
107
+ <tr>
108
+ <td valign="top">
109
+ <a href="#typed-narrowing"><b>Typed narrowing</b></a><br>
110
+ An assertion hands the value back statically narrowed, with no <code>cast</code> and no bare <code>assert</code>
111
+ </td>
112
+ <td valign="top">
113
+ <a href="#features"><b>Composable matchers</b></a><br>
114
+ 45 matchers that combine with <code>&amp;</code>, <code>|</code>, <code>~</code> and nest inside the expected structure itself
115
+ </td>
116
+ </tr>
117
+ <tr>
118
+ <td valign="top">
119
+ <a href="#features"><b>Built for test suites</b></a><br>
120
+ Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains
121
+ </td>
122
+ <td valign="top">
123
+ <a href="#integrations"><b>Integrations</b></a><br>
124
+ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts
125
+ </td>
126
+ </tr>
127
+ </table>
128
+
129
+ <h2 align="center"><a href="https://assertpy.github.io/assertpy/getting-started/comparison/">Why fluent assertions?</a></h2>
130
+
131
+ `assert` states a condition well. pytest reports it well.
132
+
133
+ What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
134
+
135
+ ```text
136
+ assert response == expected
137
+ E AssertionError: assert {'id': 1, ...} == {'id': 1, ...}
138
+ E Omitting 1 identical items, use -vv to show
139
+ E Differing items:
140
+ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Alice', 'role': 'admin'}}
141
+ E {'status': 'active'} != {'status': 'disabled'}
142
+ ```
143
+
144
+ assertpy names the [exact path](https://assertpy.github.io/assertpy/guides/errors/#rich-pytest-diffs), in color:
145
+
146
+ <!-- docs-guard: skip -->
147
+ ```python
148
+ assert_that(response).is_equal_to(expected)
149
+ ```
150
+
151
+ <p align="center">
152
+ <img src="https://raw.githubusercontent.com/assertpy/assertpy/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
153
+ </p>
154
+
155
+ It recurses through nested containers. Matcher predicates get the same treatment.
156
+
157
+ For dynamic fields like IDs, assert a subset with
158
+ [`matches_structure()`](https://assertpy.github.io/assertpy/guides/matchers/#structural-matching).
159
+
160
+ The chain is the other half. Your IDE offers only the
161
+ [methods that fit the value](https://assertpy.github.io/assertpy/concepts/type-safety/).
162
+
163
+ <!-- docs-guard: skip -->
164
+ ```python
165
+ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
166
+ ```
167
+
168
+ Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
169
+
170
+ Nothing is patched, so a matcher can sit inside the expected structure at any depth:
171
+
172
+ ```python
173
+ response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
174
+
175
+ assert_that(response).is_equal_to(
176
+ {
177
+ "id": match.greater_than(0),
178
+ "user": {"name": "Alice", "age": match.between(18, 120)},
179
+ "tags": ["a", "b"],
180
+ }
181
+ )
182
+
183
+ # or keep the bare `assert`, and pytest's own rewriting reports it
184
+ assert response == {
185
+ "id": match.greater_than(0),
186
+ "user": match.ignore(),
187
+ "tags": ["a", "b"],
188
+ }
189
+ ```
190
+
191
+ The fluent form keeps the path-level diff, the bare form keeps pytest's.
192
+
193
+ There are [45 matchers](https://assertpy.github.io/assertpy/guides/matchers/), combining with `&`, `|` and `~`.
194
+
195
+ <p align="center">
196
+ <img src="https://raw.githubusercontent.com/assertpy/assertpy/main/docs/assets/diff-gallery.png" width="640" alt="Structured diffs in the terminal: dict path, list element, set extra/missing, and structural-matcher predicate diffs, side by side">
197
+ </p>
198
+
199
+ <h2 align="center"><a href="https://assertpy.github.io/assertpy/concepts/type-safety/">Type-aware autocomplete</a></h2>
200
+
201
+ `assert_that()` uses `@overload` to return type-specific Protocols.<br>
202
+ Your IDE shows only methods relevant to the value you're testing, not the whole surface:
203
+
204
+ - `assert_that("hello").` &rarr; string methods: `starts_with`, `matches`, `is_alpha`, ...
205
+ - `assert_that(42).` &rarr; numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
206
+ - `assert_that(Path("/tmp")).` &rarr; path methods: `exists`, `is_file`, `is_readable`, ...
207
+ - `assert_that(my_dict).` &rarr; dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
208
+ - `assert_that(b"\x89PNG").` &rarr; bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
209
+
210
+ 15 type-specific Protocols instead of one `Any`<br>
211
+ Works in PyCharm, VS Code, and any editor that runs a type checker
212
+
213
+ <h2 align="center"><a href="https://assertpy.github.io/assertpy/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
214
+
215
+ An assertion hands the value back, statically narrowed.
216
+
217
+ `is_not_none()` strips `None`, `is_instance_of()` narrows to the class, and `.value` returns it:
218
+
219
+ <!-- docs-guard: skip -->
220
+ ```python
221
+ order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
222
+ order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
223
+ ```
224
+
225
+ For API tests,
226
+ [`assert_conforms()`](https://assertpy.github.io/assertpy/concepts/type-safety/#contract-narrowing-with-assert_conforms)
227
+ validates a payload against a Pydantic model and narrows to it. `exact=True` catches contract drift:
228
+
229
+ <!-- docs-guard: skip -->
230
+ ```python
231
+ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
232
+ ```
233
+
234
+ <h2 align="center"><a href="https://assertpy.github.io/assertpy/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
235
+
236
+ An exception is the right default. Reading the result means catching it.
237
+
238
+ `check()` runs the next assertion for its verdict instead:
239
+
240
+ ```python
241
+ response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
242
+ expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}
243
+
244
+ outcome = assert_that(response).check().is_equal_to(expected)
245
+
246
+ if not outcome and outcome.diff:
247
+ print(outcome.diff.entries[0].path) # user.role
248
+ ```
249
+
250
+ It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
251
+ and a walkable `.diff`. So does `AssertionFailure`.
252
+
253
+ So a reporter reads structure instead of parsing a string. That is how the
254
+ [Allure integration](https://assertpy.github.io/assertpy/extending/integrations/#allure) works.
255
+ Anything else you build can too.
256
+
257
+ <h2 align="center">Features</h2>
258
+
259
+ **Fluent API**
260
+
261
+ - [**Structural matching**](https://assertpy.github.io/assertpy/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation
262
+ - [**Recursive field assertions**](https://assertpy.github.io/assertpy/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph
263
+ - [**Universal negation**](https://assertpy.github.io/assertpy/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods
264
+ - [**Collection pipeline**](https://assertpy.github.io/assertpy/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`
265
+ - [**Positional & pairwise checks**](https://assertpy.github.io/assertpy/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants
266
+
267
+ **Type safety**
268
+
269
+ - [**Refinement predicates**](https://assertpy.github.io/assertpy/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced): `satisfies()` takes a `TypeIs` predicate, so a domain check narrows the chain too
270
+ - [**Contract testing**](https://assertpy.github.io/assertpy/concepts/type-safety/#contract-narrowing-with-assert_conforms): `assert_conforms()` validates a raw payload against a Pydantic model and narrows to it. [`exact=True`](https://assertpy.github.io/assertpy/concepts/type-safety/#contract-drift-with-exacttrue) catches contract drift, `each=True` validates list endpoints
271
+
272
+ **Built-in types**
273
+
274
+ - [Strings](https://assertpy.github.io/assertpy/guides/assertions/#strings), [numbers](https://assertpy.github.io/assertpy/guides/assertions/#numbers), [lists](https://assertpy.github.io/assertpy/guides/assertions/#lists), [tuples](https://assertpy.github.io/assertpy/guides/assertions/#tuples), [sets](https://assertpy.github.io/assertpy/guides/assertions/#sets), [dicts](https://assertpy.github.io/assertpy/guides/assertions/#dicts), [dates](https://assertpy.github.io/assertpy/guides/assertions/#dates), [booleans](https://assertpy.github.io/assertpy/guides/assertions/#booleans), [objects](https://assertpy.github.io/assertpy/guides/assertions/#objects), [bytes](https://assertpy.github.io/assertpy/guides/assertions/#bytes--bytearray), [files](https://assertpy.github.io/assertpy/guides/assertions/#files), [exceptions](https://assertpy.github.io/assertpy/guides/errors/#expected-exceptions)
275
+ - [**Bytes assertions**](https://assertpy.github.io/assertpy/guides/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`
276
+ - [**Dynamic assertions**](https://assertpy.github.io/assertpy/guides/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method
277
+ - [**Dict comparison**](https://assertpy.github.io/assertpy/guides/assertions/#selective-comparison-ignore--include): `is_equal_to(ignore=..., include=...)` for selective key/field matching by name, regex, or type
278
+ - [**Recursive comparison**](https://assertpy.github.io/assertpy/guides/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance`, `comparators`, or `ignore_null` for nested structures
279
+ - [**Extracting**](https://assertpy.github.io/assertpy/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support
280
+ - [**Regex group extraction**](https://assertpy.github.io/assertpy/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures
281
+
282
+ **Testing**
283
+
284
+ - [**Soft assertions**](https://assertpy.github.io/assertpy/guides/testing/#soft-assertions): thread-safe and async-safe via `contextvars`, each failure reported with its `file:line`. Group with `sa.group()` or `assert_all()`
285
+ - [**Polling assertions**](https://assertpy.github.io/assertpy/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout
286
+ - [**Expected exceptions**](https://assertpy.github.io/assertpy/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`)
287
+ - [**HTTP responses**](https://assertpy.github.io/assertpy/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency
288
+ - [**Snapshot testing**](https://assertpy.github.io/assertpy/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://assertpy.github.io/assertpy/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://assertpy.github.io/assertpy/guides/testing/#contract-snapshots), all updated with `--assertpy-snapshot-update`
289
+ - [**OpenAPI response contracts**](https://assertpy.github.io/assertpy/reference/json/#assertpy.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path
290
+
291
+ **Failure reporting**
292
+
293
+ - [**Structured errors**](https://assertpy.github.io/assertpy/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too
294
+ - [**Assertions as values**](https://assertpy.github.io/assertpy/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`
295
+ - [**Rich pytest diffs**](https://assertpy.github.io/assertpy/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings
296
+ - [**Failures that say why**](https://assertpy.github.io/assertpy/guides/errors/#some-failures-say-why-not-only-what): where one fact explains a failure, a line under the message says it. An id the payload holds as text (`7` against `"7"`), a NaN, two instances compared by identity, surrounding whitespace. A failed `contains()` or `is_in()` names the closest row, and forty rows that differ in one field are named as one place
297
+
298
+ **Plugin for pytest**
299
+
300
+ Set from the command line or from `[tool.pytest.ini_options]`, not from a call
301
+
302
+ - [**Failure clustering**](https://assertpy.github.io/assertpy/guides/errors/#what-the-failures-had-in-common): forty failing tests are usually not forty problems. Where three or more differ at the same place, the run ends with a line saying where. On by default, `assertpy_failure_clusters = "off"` turns it off
303
+ - [**IDE comparison window**](https://assertpy.github.io/assertpy/guides/errors/#the-comparison-window-of-an-ide): a failed `is_equal_to()` or `has_<name>()` gets PyCharm's "Click to see difference" link, as a plain `assert` does, and the window shows the two values it compared. Under `ignore=` or `include=` those go without the keys left out. The plugin hands them to `pytest_assertrepr_compare`, the hook the IDE's runner listens to. On by default, `assertpy_comparison = "off"` turns it off
304
+ - [**Diagnostic profiles**](https://assertpy.github.io/assertpy/guides/errors/#configuration): `assertpy_profile` turns the guards below on in one line. `compatible` (default) leaves them off, `safe` warns, `strict` fails the tests they find. A setting you name yourself still wins
305
+ - [**Vacuous-assertion guard**](https://assertpy.github.io/assertpy/guides/assertions/#assertions-that-checked-nothing): `--assertpy-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing
306
+ - [**Dangling-assertion detector**](https://assertpy.github.io/assertpy/guides/assertions/#assertions-that-never-ran): `--assertpy-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it
307
+
308
+ **Extensibility**
309
+
310
+ - [**Custom matchers**](https://assertpy.github.io/assertpy/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`
311
+ - [**Custom assertions**](https://assertpy.github.io/assertpy/extending/custom-assertions/): `add_extension()` adds a method to the builder
312
+
313
+ <h2 align="center"><a href="https://assertpy.github.io/assertpy/extending/integrations/">Integrations</a></h2>
314
+
315
+ - [**Allure**](https://assertpy.github.io/assertpy/extending/integrations/#allure) (`pip install assertpy[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes
316
+ - [**Behave**](https://assertpy.github.io/assertpy/extending/integrations/#behave) (`pip install assertpy[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`
317
+ - [**JSON**](https://assertpy.github.io/assertpy/guides/data/#json-path--schema) (`pip install assertpy[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`)
318
+ - [**Data frames**](https://assertpy.github.io/assertpy/extending/integrations/#data-frames-and-arrays) (`pip install assertpy[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff
319
+
320
+ ---
321
+
322
+ <p align="center">
323
+ <a href="https://github.com/assertpy/assertpy/blob/main/LICENSE">BSD 3-Clause License</a>
324
+ </p>
@@ -0,0 +1,63 @@
1
+ assertpy/__init__.py,sha256=Rf_S0hzUuzp1n6oRAqoHhCSLS6TzJ8YcB7TmU-74mmg,1835
2
+ assertpy/_clustering.py,sha256=VJRziYs35uNSRSW3jCw3aoMws5YQiysCzpshg9k7GX0,25569
3
+ assertpy/_dangling.py,sha256=0tERUBRQJhhPWF55WAFgkM7sz78T4ncOUQYcrGZSdxw,24916
4
+ assertpy/_hints.py,sha256=Xofscs0ZXzrrJMqThn_xqFqC5V3VrRy9i_KbN556zNo,59046
5
+ assertpy/_inline.py,sha256=QhNY-O4wVko1tqood9XlC82I-_HRa8Ld4dTOkpOdaog,21951
6
+ assertpy/_matcher_impls.py,sha256=GtUcIcOFxqfZkz75kRRYn-EQNpceqiFdAjFg2zoMldA,81000
7
+ assertpy/_passes.py,sha256=lKILSfdxO3b4vWLq4KMwd1MBUUMoWFCnsBKLVq_brP8,10283
8
+ assertpy/_renamed.py,sha256=1n2UEJ70BFqQbvaIu6A6KOiMF2A7f3BhrxmPBbQipoE,1849
9
+ assertpy/_satisfies.py,sha256=sm25E4PjxAAEbaCn9TlIDqSE4NorxVFYGk36wdUm4P8,36055
10
+ assertpy/_snapshot_codec.py,sha256=L3uJnWFB3m20IT4UAyLOScStq0y873hSSdakIBKPOfs,13247
11
+ assertpy/assertpy.py,sha256=ETv6uDjBz_q8nQ3hHCoskAHNo96U81ruqvsJpVsctpw,121672
12
+ assertpy/async_assertions.py,sha256=HIg7PbZ3lqovBQAfaBnwmT_KxDwd4ofSJyIz_AdVFLM,41947
13
+ assertpy/base.py,sha256=Hr9_WEXzNPUr5202i7W3FZ-p7iUb9NcVWYlUeZ7RYLw,39155
14
+ assertpy/behave_matchers.py,sha256=46bFFVa31oXK9edvcCqk4JUaAeVU1DGWhV9l8fIDCic,2865
15
+ assertpy/bytes_mixin.py,sha256=hoin-zxlMN31L0kpWYLwUWwMnO77tacTQy8HM_ppQJo,5972
16
+ assertpy/collection.py,sha256=-fBHGBCCxVdBEQ0GltNqQjH349pF9Odu2u3GiR9ghsc,20683
17
+ assertpy/contains.py,sha256=H62NmHmOVT12Cv8UkbmQ9jtDU4JyIIPh73MAPlgJB5E,47433
18
+ assertpy/dataframe.py,sha256=e4LC8_rhlGcBBBcdrRMPpgT-lhn_e6sCssjM3wFvy3s,9647
19
+ assertpy/date.py,sha256=FEE7d2EFw518CIdYy6V3yppamSL_-EiLkMMrGcAjwGc,13102
20
+ assertpy/dict.py,sha256=CaXDMD72hPTJm2ClrmXWRsVD4iscqcFx8v826cqOQII,10006
21
+ assertpy/dynamic.py,sha256=kjFMJgklJ4vzNStvj3AqpWrDnO_H9ULvGFB0XyBMiqg,11705
22
+ assertpy/errors.py,sha256=7ZJD4C251iP-rrdXa-QbAiggbP3bCahshcPNhi8_Qdc,37119
23
+ assertpy/exception.py,sha256=QbFuOGRC2bfdCr6Oi1f02HjxdRh_Y1z4eT1MaMJp80s,38916
24
+ assertpy/extracting.py,sha256=3YOdyngjSjwTTJ6KemM1xnucp1aXHjU7w02ulUDToTY,12173
25
+ assertpy/file.py,sha256=QE0bEYsJNmzEmGxVqsFzY639QwcWL3XsiYYMl6Ecmpw,11869
26
+ assertpy/helpers.py,sha256=h5VfNtyPBCasO0Ouqiq9AnOIFrhSYaogBE5NcuXTLPg,35609
27
+ assertpy/http_mixin.py,sha256=IyEEc8S833rTqqTZkeAaLM_Fj-mTg7buV7FflRwsCMU,10363
28
+ assertpy/json_mixin.py,sha256=m1hebcT3Yi6onDFcHmDusebc_LAtEyrtx04PZ9a6taM,59463
29
+ assertpy/matchers.py,sha256=pB4PkVnOzUSVyUXoM9cVIKWm8sxPuStFffPl3GLQk7U,25364
30
+ assertpy/numeric.py,sha256=b5ZgxH_EiVupEYr5gVL0Xn7z1XJNxR4djUNKMU_BGgY,26434
31
+ assertpy/outcome.py,sha256=TcB5POBCobIH7aaoP8m5O-vW_qelpN6og-pl7dCX7oU,7152
32
+ assertpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ assertpy/pytest_plugin.py,sha256=G8qwCLX2Pdp62NbH_8n9Bqmg7bz9cjcZx5rZiND-hkc,74962
34
+ assertpy/snapshot.py,sha256=Kr6K-vRBbzhuVARGqCIcIPP5IvPdwSazF42CqJw-ZSQ,49850
35
+ assertpy/string.py,sha256=qrfoiYvx_L4cAyVOCVfAvz8v99Vd7dfwq4XNY0K6b1k,32804
36
+ assertpy/warning.py,sha256=htSduXVFuFPHs5eRmM8O9gq2oUowVDAgLQooQ0NMr9Y,6762
37
+ assertpy/_engine/__init__.py,sha256=xvRm6rIBsDmUvMEVWciNeVHw3I_AiVCUUyya_75nIlw,91
38
+ assertpy/_engine/_builder_check_typing.py,sha256=CxS-ojY2PYnshBhuuvnuSl0yyvrYGXnR3KDwmRdDWGM,55705
39
+ assertpy/_engine/_capable_typing.py,sha256=NSYZgncn-Xal-5lsOnFftbmAA_LZDom0l8P8lu-p1FA,43304
40
+ assertpy/_engine/_check_typing.py,sha256=x6GHpvMGb7CQKdOdwFxWqEheS6qZUOqYYfwxD6MkQ8c,38146
41
+ assertpy/_engine/_compare.py,sha256=4zsxtujLT0R3MGwTJjRUrkR1xnHiHpBTnH7Y70Ky1rg,49026
42
+ assertpy/_engine/_compat.py,sha256=P2W8_P7c4WNiAF4P0f_t7KmpJ85fk6-z_j_IKpF57xQ,1742
43
+ assertpy/_engine/_contract.py,sha256=YmhmZoIT53AZeyOB9xoQGDFumXPOpMaXn8g-0uDnuI0,91115
44
+ assertpy/_engine/_diff.py,sha256=Rj3SGm-AW2FjJMP_rn1AhWqQla_2U78Kctu8L1Hp3-k,55669
45
+ assertpy/_engine/_equality.py,sha256=W_LEf2K6lmRXiwV45yTOhBqbG_FW6ZOQbKipuCoOKFc,40647
46
+ assertpy/_engine/_introspection.py,sha256=A2S_FvoynFGZGYyKnF1yM44pTrbIYx7bEAqYgbv6OHo,33572
47
+ assertpy/_engine/_membership.py,sha256=cq2Roes8tq1kXH9VtyK4Wr4GZ51ZWPgpF3ZwOB4NoRY,24267
48
+ assertpy/_engine/_mixin_base.py,sha256=mSyIybzvnbhO02a6BRsAvRfVsTI-o3wjqkBDQRwZaSE,10296
49
+ assertpy/_engine/_negated_typing.py,sha256=UpveYN_KBj8va_Yd51SbrmiygrkjxXkmiq8YXHzuTrM,123905
50
+ assertpy/_engine/_operations.py,sha256=TncowBzkbVGhl5UK-jBDNewPzm4dywguzwyOo_EweAs,3037
51
+ assertpy/_engine/_ordering.py,sha256=wOKyH-MO2sGDGa5x66_BBzm3a5OGBwljF9Wmo6FCDuA,44498
52
+ assertpy/_engine/_pairing.py,sha256=4YuylWUS3uCBOXzMu9Rd_VIzGItdRqhRLjw1RTdWoKo,2700
53
+ assertpy/_engine/_path.py,sha256=rKggawa5iiOxaXl4jITlbqbeZZ7sIVYocWe8w2BCTOg,7565
54
+ assertpy/_engine/_poll_typing.py,sha256=KiXF-PFTM_xP7Mea8PW5pvXmQNJEmRORUHPBJPbfNXI,253721
55
+ assertpy/_engine/_require.py,sha256=SiNmreJZ9tjDfGEyaJiF9zMIIRQlZqQkLzoMRGoPX4M,14897
56
+ assertpy/_engine/_size.py,sha256=D1ezRB7LA6UsuoyIQvwHbt0wStDefFlx4vukRxe46do,1519
57
+ assertpy/_engine/_text.py,sha256=HMhlBLHGRO5mClvtXHVAEbwjYpRiNGKGRppPl69o3A4,1737
58
+ assertpy/_engine/_typing.py,sha256=ZGc6qbCY6BIeHcrBzzq-CPDE5ZwfYnFi5ZFGDEID-PM,63766
59
+ assertpy-2.35.0.dist-info/METADATA,sha256=G40ygiMryOkSTpCr55gal9DQdLGx3MazsNcKppIEu2U,21103
60
+ assertpy-2.35.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
61
+ assertpy-2.35.0.dist-info/entry_points.txt,sha256=0bXpJmVXTIHw3qgPiN62hfbYztMJidYdYsPUvkJZq74,45
62
+ assertpy-2.35.0.dist-info/licenses/LICENSE,sha256=ep-e6x7HTPIdDUdZXDzKJIpOUZJg5Y1Q6kI56qYjPbM,1547
63
+ assertpy-2.35.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ assertpy = assertpy.pytest_plugin
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2015-2019, Activision Publishing, Inc.
2
+ Copyright (c) 2026, Solganis
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification,
6
+ are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software without
17
+ specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
23
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.