assertpy2 2.10.0__tar.gz → 2.12.0__tar.gz
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.
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/mutation.yml +2 -2
- {assertpy2-2.10.0 → assertpy2-2.12.0}/PKG-INFO +34 -67
- {assertpy2-2.10.0 → assertpy2-2.12.0}/README.md +23 -66
- assertpy2-2.12.0/assertpy2/_compare.py +136 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/_compat.py +1 -1
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/_mixin_base.py +16 -3
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/_typing.py +22 -4
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/assertpy.py +49 -46
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/async_assertions.py +1 -1
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/base.py +484 -130
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/collection.py +44 -13
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/contains.py +77 -25
- assertpy2-2.12.0/assertpy2/dataframe.py +154 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/date.py +23 -19
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/dict.py +16 -16
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/dynamic.py +3 -3
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/exception.py +21 -16
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/extracting.py +18 -18
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/file.py +16 -16
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/helpers.py +107 -131
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/json_mixin.py +6 -6
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/matchers.py +34 -7
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/numeric.py +40 -39
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/snapshot.py +38 -38
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/string.py +31 -30
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/warning.py +18 -16
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assertions.md +64 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/comparison.md +10 -5
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/errors.md +3 -3
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/extending.md +3 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/fluent.md +7 -1
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/getting-started.md +2 -1
- assertpy2-2.12.0/docs/index.md +103 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/integrations.md +65 -1
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/migration.md +1 -1
- assertpy2-2.12.0/docs/reference/async.md +13 -0
- assertpy2-2.12.0/docs/reference/bytes.md +5 -0
- assertpy2-2.12.0/docs/reference/collections.md +5 -0
- assertpy2-2.12.0/docs/reference/containment.md +5 -0
- assertpy2-2.12.0/docs/reference/core.md +6 -0
- assertpy2-2.12.0/docs/reference/dataframes.md +5 -0
- assertpy2-2.12.0/docs/reference/dates.md +5 -0
- assertpy2-2.12.0/docs/reference/dicts.md +5 -0
- assertpy2-2.12.0/docs/reference/dynamic.md +6 -0
- assertpy2-2.12.0/docs/reference/entry-points.md +47 -0
- assertpy2-2.12.0/docs/reference/errors.md +23 -0
- assertpy2-2.12.0/docs/reference/exceptions.md +5 -0
- assertpy2-2.12.0/docs/reference/extracting.md +5 -0
- assertpy2-2.12.0/docs/reference/files.md +11 -0
- assertpy2-2.12.0/docs/reference/json.md +5 -0
- assertpy2-2.12.0/docs/reference/matchers.md +7 -0
- assertpy2-2.12.0/docs/reference/numbers.md +5 -0
- assertpy2-2.12.0/docs/reference/overview.md +21 -0
- assertpy2-2.12.0/docs/reference/snapshots.md +5 -0
- assertpy2-2.12.0/docs/reference/strings.md +6 -0
- assertpy2-2.12.0/docs/reference/warnings.md +5 -0
- assertpy2-2.12.0/docs/stylesheets/extra.css +13 -0
- assertpy2-2.12.0/mkdocs.yml +141 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/pyproject.toml +13 -5
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_bytes.py +20 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_chaining.py +3 -3
- assertpy2-2.12.0/tests/test_dataframe.py +176 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_datetime.py +120 -120
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_dict_compare.py +18 -18
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_dynamic.py +17 -0
- assertpy2-2.12.0/tests/test_equals.py +138 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_extracting.py +20 -0
- assertpy2-2.12.0/tests/test_iterable_cluster.py +201 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_matchers.py +3 -3
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_mutation_hardening.py +32 -0
- assertpy2-2.12.0/tests/test_recursive_assertion.py +194 -0
- assertpy2-2.12.0/tests/test_recursive_compare_config.py +274 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_rich_diff.py +121 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_snapshots.py +1 -1
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_typing.py +18 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/uv.lock +623 -125
- assertpy2-2.10.0/docs/index.md +0 -55
- assertpy2-2.10.0/mkdocs.yml +0 -82
- assertpy2-2.10.0/tests/test_equals.py +0 -66
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.codecov.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.gitattributes +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/.gitignore +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/LICENSE +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/SECURITY.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/_introspection.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/data.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/logo.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/matchers.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/testing.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/docs/type-safety.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_async.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_bool.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_callable.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_class.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_collection.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_core.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_description.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_dict.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_errors.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_fail.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_file.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_in.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_json.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_list.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_none.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_not.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_readme.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_soft.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_string.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_structural.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_type.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.12.0}/tests/test_warn.py +0 -0
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
strategy:
|
|
20
20
|
fail-fast: false
|
|
21
21
|
matrix:
|
|
22
|
-
module: [base, numeric, string, contains, collection, dict, date, matchers, helpers]
|
|
22
|
+
module: [base, numeric, string, contains, collection, dict, date, matchers, helpers, _compare, dataframe, json_mixin, snapshot, bytes_mixin, dynamic, extracting, warning, exception, async_assertions, file, behave_matchers]
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
25
25
|
with:
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
python-version: "3.14"
|
|
35
35
|
|
|
36
36
|
- name: Install dependencies
|
|
37
|
-
run: uv sync --extra json --group mutation
|
|
37
|
+
run: uv sync --extra json --extra behave --group mutation
|
|
38
38
|
|
|
39
39
|
- name: Run mutation testing
|
|
40
40
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.0
|
|
4
4
|
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -31,9 +31,19 @@ Provides-Extra: allure
|
|
|
31
31
|
Requires-Dist: allure-pytest>=2.13; extra == 'allure'
|
|
32
32
|
Provides-Extra: behave
|
|
33
33
|
Requires-Dist: behave>=1.2.6; 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'
|
|
34
38
|
Provides-Extra: json
|
|
35
39
|
Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
|
|
36
40
|
Requires-Dist: jsonschema>=4.0; extra == 'json'
|
|
41
|
+
Provides-Extra: numpy
|
|
42
|
+
Requires-Dist: numpy>=1.26; extra == 'numpy'
|
|
43
|
+
Provides-Extra: pandas
|
|
44
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
45
|
+
Provides-Extra: polars
|
|
46
|
+
Requires-Dist: polars>=1.0; extra == 'polars'
|
|
37
47
|
Description-Content-Type: text/markdown
|
|
38
48
|
|
|
39
49
|
<p align="center">
|
|
@@ -68,7 +78,7 @@ Description-Content-Type: text/markdown
|
|
|
68
78
|
|
|
69
79
|
---
|
|
70
80
|
|
|
71
|
-
|
|
81
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/">Quick start</a></h2>
|
|
72
82
|
|
|
73
83
|
```bash
|
|
74
84
|
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
@@ -88,7 +98,7 @@ def test_user():
|
|
|
88
98
|
|
|
89
99
|
Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/comparison/">Why fluent assertions?</a></h2>
|
|
92
102
|
|
|
93
103
|
A fluent chain reads as one intent and replaces several bare asserts - and your IDE
|
|
94
104
|
offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
|
|
@@ -121,17 +131,21 @@ assertpy2 reports the [exact path to every difference](https://solganis.github.i
|
|
|
121
131
|
assert_that(response).is_equal_to(expected)
|
|
122
132
|
```
|
|
123
133
|
|
|
124
|
-
<
|
|
134
|
+
<p align="center">
|
|
135
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/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">
|
|
136
|
+
</p>
|
|
125
137
|
|
|
126
|
-
Recursive diffs work for dicts, dataclasses, namedtuples,
|
|
138
|
+
Recursive diffs work for dicts, dataclasses, namedtuples, and Pydantic models.
|
|
127
139
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
128
140
|
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
129
141
|
|
|
130
142
|
The same path-level treatment for dicts, lists, sets, and matcher predicates:
|
|
131
143
|
|
|
132
|
-
<
|
|
144
|
+
<p align="center">
|
|
145
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/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">
|
|
146
|
+
</p>
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/type-safety/">Type-aware autocomplete</a></h2>
|
|
135
149
|
|
|
136
150
|
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
137
151
|
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
@@ -146,25 +160,25 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
146
160
|
|
|
147
161
|
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
## Features
|
|
163
|
+
<h2 align="center">Features</h2>
|
|
152
164
|
|
|
153
165
|
**Fluent API**
|
|
154
166
|
|
|
155
167
|
- [**Composable matchers**](https://solganis.github.io/assertpy2/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
156
168
|
- [**Structural matching**](https://solganis.github.io/assertpy2/matchers/#structural-matching): `matches_structure()` for declarative dict/API response validation, reporting the exact path to each mismatch on failure.
|
|
169
|
+
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph, reporting the exact path.
|
|
157
170
|
- [**Universal negation**](https://solganis.github.io/assertpy2/fluent/#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
158
171
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
172
|
+
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`.
|
|
159
173
|
- [**Fluent chaining**](https://solganis.github.io/assertpy2/fluent/#chaining): write assertions as readable one-liners that chain naturally.
|
|
160
174
|
|
|
161
175
|
**Built-in types**
|
|
162
176
|
|
|
163
177
|
- [Strings](https://solganis.github.io/assertpy2/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/errors/#expected-exceptions).
|
|
164
178
|
- [**Bytes assertions**](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
|
|
165
|
-
- [**JSON assertions**](https://solganis.github.io/assertpy2/data/#json-path--schema): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
|
|
166
179
|
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
167
|
-
- [**Dict comparison**](https://solganis.github.io/assertpy2/assertions/#selective-comparison-ignore--include): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects).
|
|
180
|
+
- [**Dict comparison**](https://solganis.github.io/assertpy2/assertions/#selective-comparison-ignore--include): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects), including by regex or type.
|
|
181
|
+
- [**Recursive comparison**](https://solganis.github.io/assertpy2/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance` (absolute float tolerance at any depth) or `comparators` (per-type / per-field predicates) for nested structures.
|
|
168
182
|
- [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
169
183
|
|
|
170
184
|
**Testing**
|
|
@@ -178,7 +192,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
178
192
|
|
|
179
193
|
**Type safety**
|
|
180
194
|
|
|
181
|
-
- [**Type-aware autocomplete**](
|
|
195
|
+
- [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
|
|
182
196
|
- **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
183
197
|
|
|
184
198
|
**Extensibility**
|
|
@@ -187,64 +201,17 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
187
201
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
188
202
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/): `add_extension()` for custom assertion methods.
|
|
189
203
|
|
|
190
|
-
**Integrations**
|
|
191
|
-
|
|
192
|
-
- [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
|
|
193
|
-
- [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
|
|
194
|
-
|
|
195
204
|
See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
## [Integrations](https://solganis.github.io/assertpy2/integrations/)
|
|
200
|
-
|
|
201
|
-
### Allure
|
|
202
|
-
|
|
203
|
-
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
204
|
-
|
|
205
|
-
```bash
|
|
206
|
-
pip install assertpy2[allure]
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
210
|
-
|
|
211
|
-
| Mode | What is attached |
|
|
212
|
-
|---|---|
|
|
213
|
-
| `diff` (default) | Structured Diff JSON (path-level breakdown) |
|
|
214
|
-
| `full` | Structured Diff + actual/expected JSON |
|
|
215
|
-
| `off` | Nothing |
|
|
216
|
-
|
|
217
|
-
```toml
|
|
218
|
-
# pyproject.toml
|
|
219
|
-
[tool.pytest.ini_options]
|
|
220
|
-
assertpy2_allure = "full"
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Behave
|
|
224
|
-
|
|
225
|
-
Ready-made parameter types for Behave step definitions:
|
|
226
|
-
|
|
227
|
-
```bash
|
|
228
|
-
pip install assertpy2[behave]
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
```py
|
|
232
|
-
# in environment.py or steps/conftest.py
|
|
233
|
-
from assertpy2.behave_matchers import register_assertpy_types
|
|
234
|
-
register_assertpy_types()
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
Then use in step definitions:
|
|
238
|
-
|
|
239
|
-
```py
|
|
240
|
-
@given('a user aged {age:PositiveInt}')
|
|
241
|
-
def step_impl(context, age):
|
|
242
|
-
context.age = age # already validated as int > 0
|
|
243
|
-
```
|
|
206
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
|
|
244
207
|
|
|
245
|
-
|
|
208
|
+
Optional adapters, each its own extra; full configuration and examples are in the
|
|
209
|
+
[Integrations guide](https://solganis.github.io/assertpy2/integrations/).
|
|
246
210
|
|
|
247
|
-
|
|
211
|
+
- [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
|
|
212
|
+
- [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
|
|
213
|
+
- [**JSON**](https://solganis.github.io/assertpy2/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
|
|
214
|
+
- [**Data frames**](https://solganis.github.io/assertpy2/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.
|
|
248
215
|
|
|
249
216
|
---
|
|
250
217
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/">Quick start</a></h2>
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
pip install assertpy2 # drop-in replacement for assertpy, just change the import
|
|
@@ -50,7 +50,7 @@ def test_user():
|
|
|
50
50
|
|
|
51
51
|
Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/comparison/">Why fluent assertions?</a></h2>
|
|
54
54
|
|
|
55
55
|
A fluent chain reads as one intent and replaces several bare asserts - and your IDE
|
|
56
56
|
offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
|
|
@@ -83,17 +83,21 @@ assertpy2 reports the [exact path to every difference](https://solganis.github.i
|
|
|
83
83
|
assert_that(response).is_equal_to(expected)
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
<
|
|
86
|
+
<p align="center">
|
|
87
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/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">
|
|
88
|
+
</p>
|
|
87
89
|
|
|
88
|
-
Recursive diffs work for dicts, dataclasses, namedtuples,
|
|
90
|
+
Recursive diffs work for dicts, dataclasses, namedtuples, and Pydantic models.
|
|
89
91
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
90
92
|
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
91
93
|
|
|
92
94
|
The same path-level treatment for dicts, lists, sets, and matcher predicates:
|
|
93
95
|
|
|
94
|
-
<
|
|
96
|
+
<p align="center">
|
|
97
|
+
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/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">
|
|
98
|
+
</p>
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/type-safety/">Type-aware autocomplete</a></h2>
|
|
97
101
|
|
|
98
102
|
`assert_that()` uses `@overload` to return type-specific Protocols.
|
|
99
103
|
Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
@@ -108,25 +112,25 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
108
112
|
|
|
109
113
|
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
## Features
|
|
115
|
+
<h2 align="center">Features</h2>
|
|
114
116
|
|
|
115
117
|
**Fluent API**
|
|
116
118
|
|
|
117
119
|
- [**Composable matchers**](https://solganis.github.io/assertpy2/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
118
120
|
- [**Structural matching**](https://solganis.github.io/assertpy2/matchers/#structural-matching): `matches_structure()` for declarative dict/API response validation, reporting the exact path to each mismatch on failure.
|
|
121
|
+
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph, reporting the exact path.
|
|
119
122
|
- [**Universal negation**](https://solganis.github.io/assertpy2/fluent/#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
120
123
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
124
|
+
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`.
|
|
121
125
|
- [**Fluent chaining**](https://solganis.github.io/assertpy2/fluent/#chaining): write assertions as readable one-liners that chain naturally.
|
|
122
126
|
|
|
123
127
|
**Built-in types**
|
|
124
128
|
|
|
125
129
|
- [Strings](https://solganis.github.io/assertpy2/assertions/#strings), [numbers](https://solganis.github.io/assertpy2/assertions/#numbers), [lists](https://solganis.github.io/assertpy2/assertions/#lists), [tuples](https://solganis.github.io/assertpy2/assertions/#tuples), [sets](https://solganis.github.io/assertpy2/assertions/#sets), [dicts](https://solganis.github.io/assertpy2/assertions/#dicts), [dates](https://solganis.github.io/assertpy2/assertions/#dates), [booleans](https://solganis.github.io/assertpy2/assertions/#booleans), [objects](https://solganis.github.io/assertpy2/assertions/#objects), [bytes](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray), [files](https://solganis.github.io/assertpy2/assertions/#files), [exceptions](https://solganis.github.io/assertpy2/errors/#expected-exceptions).
|
|
126
130
|
- [**Bytes assertions**](https://solganis.github.io/assertpy2/assertions/#bytes--bytearray): `is_valid_utf8()`, `starts_with_bytes()`, `is_hex_equal_to()`, `decoded_as()` for `bytes`/`bytearray`.
|
|
127
|
-
- [**JSON assertions**](https://solganis.github.io/assertpy2/data/#json-path--schema): JSONPath navigation and JSON Schema validation. `pip install assertpy2[json]`.
|
|
128
131
|
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
129
|
-
- [**Dict comparison**](https://solganis.github.io/assertpy2/assertions/#selective-comparison-ignore--include): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects).
|
|
132
|
+
- [**Dict comparison**](https://solganis.github.io/assertpy2/assertions/#selective-comparison-ignore--include): `is_equal_to()` with `ignore` and `include` for selective key/field matching (dicts, dataclasses, namedtuples, Pydantic models, attrs, plain objects), including by regex or type.
|
|
133
|
+
- [**Recursive comparison**](https://solganis.github.io/assertpy2/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance` (absolute float tolerance at any depth) or `comparators` (per-type / per-field predicates) for nested structures.
|
|
130
134
|
- [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
131
135
|
|
|
132
136
|
**Testing**
|
|
@@ -140,7 +144,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
140
144
|
|
|
141
145
|
**Type safety**
|
|
142
146
|
|
|
143
|
-
- [**Type-aware autocomplete**](
|
|
147
|
+
- [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
|
|
144
148
|
- **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
145
149
|
|
|
146
150
|
**Extensibility**
|
|
@@ -149,64 +153,17 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
149
153
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
150
154
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/): `add_extension()` for custom assertion methods.
|
|
151
155
|
|
|
152
|
-
**Integrations**
|
|
153
|
-
|
|
154
|
-
- [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure): auto-attach structured diff and actual/expected data to reports. `pip install assertpy2[allure]`.
|
|
155
|
-
- [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave): ready-made parameter types for step definitions. `pip install assertpy2[behave]`.
|
|
156
|
-
|
|
157
156
|
See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
## [Integrations](https://solganis.github.io/assertpy2/integrations/)
|
|
162
|
-
|
|
163
|
-
### Allure
|
|
164
|
-
|
|
165
|
-
When `allure-pytest` is installed, the pytest plugin auto-attaches structured failure data to Allure reports as JSON attachments.
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
pip install assertpy2[allure]
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Three modes controlled via `pytest.ini` (or `pyproject.toml`):
|
|
172
|
-
|
|
173
|
-
| Mode | What is attached |
|
|
174
|
-
|---|---|
|
|
175
|
-
| `diff` (default) | Structured Diff JSON (path-level breakdown) |
|
|
176
|
-
| `full` | Structured Diff + actual/expected JSON |
|
|
177
|
-
| `off` | Nothing |
|
|
178
|
-
|
|
179
|
-
```toml
|
|
180
|
-
# pyproject.toml
|
|
181
|
-
[tool.pytest.ini_options]
|
|
182
|
-
assertpy2_allure = "full"
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Behave
|
|
186
|
-
|
|
187
|
-
Ready-made parameter types for Behave step definitions:
|
|
188
|
-
|
|
189
|
-
```bash
|
|
190
|
-
pip install assertpy2[behave]
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
```py
|
|
194
|
-
# in environment.py or steps/conftest.py
|
|
195
|
-
from assertpy2.behave_matchers import register_assertpy_types
|
|
196
|
-
register_assertpy_types()
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Then use in step definitions:
|
|
200
|
-
|
|
201
|
-
```py
|
|
202
|
-
@given('a user aged {age:PositiveInt}')
|
|
203
|
-
def step_impl(context, age):
|
|
204
|
-
context.age = age # already validated as int > 0
|
|
205
|
-
```
|
|
158
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
|
|
206
159
|
|
|
207
|
-
|
|
160
|
+
Optional adapters, each its own extra; full configuration and examples are in the
|
|
161
|
+
[Integrations guide](https://solganis.github.io/assertpy2/integrations/).
|
|
208
162
|
|
|
209
|
-
|
|
163
|
+
- [**Allure**](https://solganis.github.io/assertpy2/integrations/#allure) (`pip install assertpy2[allure]`): the pytest plugin auto-attaches structured diff and actual/expected data to Allure reports, in three configurable modes.
|
|
164
|
+
- [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
|
|
165
|
+
- [**JSON**](https://solganis.github.io/assertpy2/data/#json-path--schema) (`pip install assertpy2[json]`): JSONPath navigation (`at_json_path()`, `has_json_path()`) and JSON Schema validation (`matches_json_schema()`).
|
|
166
|
+
- [**Data frames**](https://solganis.github.io/assertpy2/integrations/#data-frames-and-arrays) (`pip install assertpy2[pandas]` / `[polars]` / `[numpy]`): fluent equality for pandas/polars frames and numpy arrays, carrying each library's own diff.
|
|
210
167
|
|
|
211
168
|
---
|
|
212
169
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Per-call configuration for tolerant / custom-comparator equality, shared by the equality and diff code.
|
|
2
|
+
|
|
3
|
+
``is_equal_to`` builds a `_CompareConfig` from its ``tolerance``/``comparators`` kwargs and threads it
|
|
4
|
+
through both the boolean comparison (`HelpersMixin._dict_not_equal()`) and the diff/message rendering
|
|
5
|
+
(`BaseMixin._sub_diff_entries()`, `HelpersMixin._dict_err()`). `_node_decision()` is the single
|
|
6
|
+
switch both sides consult, so a tolerated or comparator-equal leaf is reported in neither. With ``config is
|
|
7
|
+
None`` every helper reproduces the engine's historical ``actual != expected`` behavior exactly.
|
|
8
|
+
|
|
9
|
+
Following the package convention, the impl helpers take unannotated args (the typed public surface lives in
|
|
10
|
+
`assertpy2._typing`); they operate on arbitrary user values whose operators ``numbers.Number`` cannot
|
|
11
|
+
express to the type checker.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
import numbers
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
27
|
+
class _CompareConfig:
|
|
28
|
+
"""Tolerance and custom comparators for a single ``is_equal_to`` call.
|
|
29
|
+
|
|
30
|
+
``tolerance`` is an absolute tolerance applied to real-number leaves; ``comparators`` maps a ``type`` or
|
|
31
|
+
an immediate field name to a ``(actual, expected) -> bool`` predicate that owns matching leaves.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
tolerance: float | None = None
|
|
35
|
+
comparators: dict[object, Callable[[object, object], bool]] | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_compare_config(tolerance, comparators) -> _CompareConfig | None:
|
|
39
|
+
"""Validate the ``is_equal_to`` ``tolerance``/``comparators`` kwargs and build a config (``None`` if neither).
|
|
40
|
+
|
|
41
|
+
``tolerance`` must be a non-negative real number (not ``bool``/``complex``/``NaN``); ``comparators`` must be a
|
|
42
|
+
dict of ``(actual, expected) -> bool`` callables keyed by ``type`` or field name.
|
|
43
|
+
"""
|
|
44
|
+
if tolerance is None and comparators is None:
|
|
45
|
+
return None
|
|
46
|
+
if tolerance is not None:
|
|
47
|
+
if isinstance(tolerance, bool) or not isinstance(tolerance, numbers.Number) or isinstance(tolerance, complex):
|
|
48
|
+
raise TypeError("given tolerance arg must be a real number")
|
|
49
|
+
if math.isnan(tolerance):
|
|
50
|
+
raise ValueError("given tolerance arg must not be NaN")
|
|
51
|
+
if tolerance < 0:
|
|
52
|
+
raise ValueError("given tolerance arg must not be negative")
|
|
53
|
+
if comparators is not None:
|
|
54
|
+
if not isinstance(comparators, dict):
|
|
55
|
+
raise TypeError("given comparators arg must be a dict")
|
|
56
|
+
for comparator in comparators.values():
|
|
57
|
+
if not callable(comparator):
|
|
58
|
+
raise TypeError("each comparator must be callable")
|
|
59
|
+
return _CompareConfig(tolerance=tolerance, comparators=comparators)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_real_number(value) -> bool:
|
|
63
|
+
"""Return whether ``value`` is a real number eligible for tolerance (excludes ``bool`` and ``complex``).
|
|
64
|
+
|
|
65
|
+
Array/frame-likes are not `numbers.Number`, so they are excluded too - tolerance never triggers
|
|
66
|
+
their element-wise ``==`` that has no single truth value.
|
|
67
|
+
"""
|
|
68
|
+
return isinstance(value, numbers.Number) and not isinstance(value, (bool, complex))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _within_tolerance(actual, expected, tolerance) -> bool:
|
|
72
|
+
"""Return whether two real numbers are within ``tolerance`` (absolute); ``NaN`` is never within.
|
|
73
|
+
|
|
74
|
+
Only actual ``float`` operands are checked for ``NaN`` (other reals cannot be ``NaN``), which also keeps
|
|
75
|
+
``math.isnan`` off arbitrary-precision ``int`` values that would overflow it.
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(actual, float) and math.isnan(actual):
|
|
78
|
+
return False
|
|
79
|
+
if isinstance(expected, float) and math.isnan(expected):
|
|
80
|
+
return False
|
|
81
|
+
return abs(actual - expected) <= tolerance
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _resolve_comparator(actual, config: _CompareConfig, *, field):
|
|
85
|
+
"""Resolve the comparator owning a leaf: immediate field name first, then exact type, then ``isinstance``.
|
|
86
|
+
|
|
87
|
+
``field`` is the leaf's immediate key/field name (``None`` for sequence elements and bare scalars, which
|
|
88
|
+
have no name). Returns ``None`` when no comparator applies, so the caller falls back to tolerance / ``==``.
|
|
89
|
+
"""
|
|
90
|
+
comparators = config.comparators
|
|
91
|
+
if comparators is None:
|
|
92
|
+
return None
|
|
93
|
+
if field is not None and not isinstance(field, type) and field in comparators:
|
|
94
|
+
return comparators[field]
|
|
95
|
+
if type(actual) in comparators:
|
|
96
|
+
return comparators[type(actual)]
|
|
97
|
+
for key, comparator in comparators.items():
|
|
98
|
+
if isinstance(key, type) and isinstance(actual, key):
|
|
99
|
+
return comparator
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _node_decision(actual, expected, config: _CompareConfig | None, *, field=None) -> str:
|
|
104
|
+
"""Classify a node as ``"equal"``, ``"leaf"`` or ``"recurse"``.
|
|
105
|
+
|
|
106
|
+
With ``config is None`` this is exactly the engine's historical behavior: differing values ``"recurse"``
|
|
107
|
+
(to decompose into a sub-diff), equal values are ``"equal"`` (skipped); ``"leaf"`` never occurs. With a
|
|
108
|
+
config, a matching comparator or tolerance owns the node - it is classified ``"equal"`` or ``"leaf"`` and
|
|
109
|
+
never recursed into.
|
|
110
|
+
"""
|
|
111
|
+
if config is not None:
|
|
112
|
+
comparator = _resolve_comparator(actual, config, field=field)
|
|
113
|
+
if comparator is not None:
|
|
114
|
+
return "equal" if comparator(actual, expected) else "leaf"
|
|
115
|
+
if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
|
|
116
|
+
return "equal" if _within_tolerance(actual, expected, config.tolerance) else "leaf"
|
|
117
|
+
return "recurse" if actual != expected else "equal"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _spec_matches(key, value, specs) -> bool:
|
|
121
|
+
"""Return whether a dict ``key``/``value`` matches any ignore/include ``spec``.
|
|
122
|
+
|
|
123
|
+
A spec matches by exact key equality (today's behavior), by a compiled `re.Pattern` searched
|
|
124
|
+
against ``str(key)``, or by a ``type`` the ``value`` is an instance of. Nested-path tuples never match
|
|
125
|
+
here - they are expanded by the recursion in `HelpersMixin._dict_not_equal()`.
|
|
126
|
+
"""
|
|
127
|
+
for spec in specs:
|
|
128
|
+
if isinstance(spec, re.Pattern):
|
|
129
|
+
if spec.search(str(key)):
|
|
130
|
+
return True
|
|
131
|
+
elif isinstance(spec, type):
|
|
132
|
+
if isinstance(value, spec):
|
|
133
|
+
return True
|
|
134
|
+
elif spec == key:
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Internal compatibility shims.
|
|
2
2
|
|
|
3
|
-
``Self`` entered the standard library's
|
|
3
|
+
``Self`` entered the standard library's `typing` in Python 3.11; on 3.10 it is provided by
|
|
4
4
|
``typing_extensions``. Re-exporting it from one place lets every mixin import ``Self`` without repeating
|
|
5
5
|
the version gate, and lets ``typing_extensions`` be dropped as a runtime dependency on Python 3.11+.
|
|
6
6
|
"""
|
|
@@ -5,8 +5,9 @@ from typing import TYPE_CHECKING, Any
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
|
+
from ._compare import _CompareConfig
|
|
8
9
|
from ._compat import Self
|
|
9
|
-
from .errors import DiffResult
|
|
10
|
+
from .errors import DiffEntry, DiffResult
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class _MixinBase:
|
|
@@ -60,7 +61,7 @@ class _MixinBase:
|
|
|
60
61
|
|
|
61
62
|
def _is_dict_like(
|
|
62
63
|
self,
|
|
63
|
-
|
|
64
|
+
candidate: object,
|
|
64
65
|
check_keys: bool = ...,
|
|
65
66
|
check_values: bool = ...,
|
|
66
67
|
check_getitem: bool = ...,
|
|
@@ -68,7 +69,7 @@ class _MixinBase:
|
|
|
68
69
|
|
|
69
70
|
def _require_dict_like(
|
|
70
71
|
self,
|
|
71
|
-
|
|
72
|
+
candidate: object,
|
|
72
73
|
check_keys: bool = ...,
|
|
73
74
|
check_values: bool = ...,
|
|
74
75
|
check_getitem: bool = ...,
|
|
@@ -83,6 +84,7 @@ class _MixinBase:
|
|
|
83
84
|
other: object,
|
|
84
85
|
ignore: object = ...,
|
|
85
86
|
include: object = ...,
|
|
87
|
+
config: _CompareConfig | None = ...,
|
|
86
88
|
) -> bool: ...
|
|
87
89
|
|
|
88
90
|
def _dict_err(
|
|
@@ -91,11 +93,22 @@ class _MixinBase:
|
|
|
91
93
|
other: object,
|
|
92
94
|
ignore: object = ...,
|
|
93
95
|
include: object = ...,
|
|
96
|
+
config: _CompareConfig | None = ...,
|
|
94
97
|
) -> None: ...
|
|
95
98
|
|
|
96
99
|
@staticmethod
|
|
97
100
|
def _to_comparable_dict(obj: object) -> dict[str, object] | None: ...
|
|
98
101
|
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _sub_diff_entries(
|
|
104
|
+
actual: object,
|
|
105
|
+
expected: object,
|
|
106
|
+
prefix: str,
|
|
107
|
+
*,
|
|
108
|
+
_seen: set[int] | None = ...,
|
|
109
|
+
config: _CompareConfig | None = ...,
|
|
110
|
+
) -> list[DiffEntry] | None: ...
|
|
111
|
+
|
|
99
112
|
# NumericMixin class attrs used by HelpersMixin._validate_between_args
|
|
100
113
|
_NUMERIC_COMPAREABLE: frozenset[type]
|
|
101
114
|
_NUMERIC_NON_COMPAREABLE: frozenset[type]
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
-
from collections.abc import Callable, Hashable
|
|
6
|
+
from collections.abc import Callable, Hashable, Iterable
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, Protocol
|
|
9
9
|
|
|
@@ -21,7 +21,13 @@ if TYPE_CHECKING:
|
|
|
21
21
|
# BaseMixin
|
|
22
22
|
def described_as(self, description: str) -> Self: ...
|
|
23
23
|
def is_equal_to(
|
|
24
|
-
self,
|
|
24
|
+
self,
|
|
25
|
+
other: object,
|
|
26
|
+
*,
|
|
27
|
+
ignore: _KeySpecs | None = ...,
|
|
28
|
+
include: _KeySpecs | None = ...,
|
|
29
|
+
tolerance: float | None = ...,
|
|
30
|
+
comparators: dict[object, Callable[..., bool]] | None = ...,
|
|
25
31
|
) -> Self: ...
|
|
26
32
|
def is_not_equal_to(self, other: object) -> Self: ...
|
|
27
33
|
def is_same_as(self, other: object) -> Self: ...
|
|
@@ -36,6 +42,8 @@ if TYPE_CHECKING:
|
|
|
36
42
|
def is_callable(self) -> Self: ...
|
|
37
43
|
def is_not_callable(self) -> Self: ...
|
|
38
44
|
def satisfies(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
45
|
+
def all_fields_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
46
|
+
def has_no_none_fields(self) -> Self: ...
|
|
39
47
|
# ContainsMixin - universal
|
|
40
48
|
def is_in(self, *items: object) -> Self: ...
|
|
41
49
|
def is_not_in(self, *items: object) -> Self: ...
|
|
@@ -73,8 +81,11 @@ if TYPE_CHECKING:
|
|
|
73
81
|
def does_not_contain_duplicates(self) -> Self: ...
|
|
74
82
|
def contains_exactly(self, *items: object) -> Self: ...
|
|
75
83
|
def contains_in_order(self, *items: object) -> Self: ...
|
|
84
|
+
def contains_only_once(self, *items: object) -> Self: ...
|
|
85
|
+
def has_same_size_as(self, other: object) -> Self: ...
|
|
76
86
|
def is_empty(self) -> Self: ...
|
|
77
87
|
def is_not_empty(self) -> Self: ...
|
|
88
|
+
def is_subset_of(self, *supersets: object) -> Self: ...
|
|
78
89
|
# StringMixin - regex
|
|
79
90
|
def extracting_group(self, pattern: str, group: int | str = ...) -> Self: ...
|
|
80
91
|
def matches_with_groups(self, pattern: str) -> Self: ...
|
|
@@ -138,6 +149,10 @@ if TYPE_CHECKING:
|
|
|
138
149
|
def any_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
139
150
|
def all_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
140
151
|
def none_satisfy(self, matcher: Matcher | Callable[..., bool]) -> Self: ...
|
|
152
|
+
def satisfies_exactly(self, *matchers: Matcher | Callable[..., bool]) -> Self: ...
|
|
153
|
+
def zip_satisfies(self, other: Iterable[object], predicate: Callable[..., bool]) -> Self: ...
|
|
154
|
+
def contains_only_once(self, *items: object) -> Self: ...
|
|
155
|
+
def has_same_size_as(self, other: object) -> Self: ...
|
|
141
156
|
# CollectionMixin - pipeline
|
|
142
157
|
def filtered_on(self, predicate: Callable[..., bool]) -> Self: ...
|
|
143
158
|
def mapped(self, func: Callable[..., object]) -> Self: ...
|
|
@@ -166,6 +181,7 @@ if TYPE_CHECKING:
|
|
|
166
181
|
# ContainsMixin
|
|
167
182
|
def contains(self, *items: object) -> Self: ...
|
|
168
183
|
def does_not_contain(self, *items: object) -> Self: ...
|
|
184
|
+
def has_same_size_as(self, other: object) -> Self: ...
|
|
169
185
|
def is_empty(self) -> Self: ...
|
|
170
186
|
def is_not_empty(self) -> Self: ...
|
|
171
187
|
# BaseMixin
|
|
@@ -209,6 +225,7 @@ if TYPE_CHECKING:
|
|
|
209
225
|
|
|
210
226
|
# ContainsMixin
|
|
211
227
|
def contains(self, *items: object) -> Self: ...
|
|
228
|
+
def has_same_size_as(self, other: object) -> Self: ...
|
|
212
229
|
def is_empty(self) -> Self: ...
|
|
213
230
|
def is_not_empty(self) -> Self: ...
|
|
214
231
|
# BytesMixin
|
|
@@ -223,8 +240,9 @@ if TYPE_CHECKING:
|
|
|
223
240
|
class _InvokedAssertion(_StringAssertion, Protocol):
|
|
224
241
|
"""Assertions available after ``when_called_with()`` captured an exception/warning message.
|
|
225
242
|
|
|
226
|
-
The captured message is a ``str`` (hence the string assertions);
|
|
227
|
-
|
|
243
|
+
The captured message is a ``str`` (hence the string assertions);
|
|
244
|
+
[`returned()`][assertpy2.exception.ExceptionMixin.returned] pivots to the value the callable
|
|
245
|
+
returned. Its static type is unknown, so ``returned()`` exposes the
|
|
228
246
|
type-agnostic core assertions (``is_equal_to``, ``is_instance_of``, ``satisfies``, ...) -
|
|
229
247
|
type-safe by construction, never advertising methods that may not apply.
|
|
230
248
|
"""
|