assertpy2 2.10.0__tar.gz → 2.11.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.11.0}/PKG-INFO +29 -61
- {assertpy2-2.10.0 → assertpy2-2.11.0}/README.md +18 -60
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_mixin_base.py +6 -1
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/assertpy.py +3 -1
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/base.py +62 -12
- assertpy2-2.11.0/assertpy2/dataframe.py +153 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/helpers.py +3 -54
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/comparison.md +1 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/errors.md +2 -2
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/integrations.md +62 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/pyproject.toml +6 -2
- assertpy2-2.11.0/tests/test_dataframe.py +163 -0
- assertpy2-2.11.0/tests/test_equals.py +138 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_rich_diff.py +121 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_snapshots.py +1 -1
- {assertpy2-2.10.0 → assertpy2-2.11.0}/uv.lock +468 -28
- assertpy2-2.10.0/tests/test_equals.py +0 -66
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.codecov.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.gitattributes +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/mutation.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/.gitignore +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/LICENSE +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/SECURITY.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_compat.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_introspection.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/_typing.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/contains.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/date.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/file.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/matchers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/string.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assertions.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/data.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/extending.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/fluent.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/getting-started.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/index.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/logo.svg +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/matchers.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/migration.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/testing.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/docs/type-safety.md +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/mkdocs.yml +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_async.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_bool.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_callable.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_class.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_collection.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_core.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_description.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_dict.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_errors.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_fail.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_file.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_in.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_json.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_list.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_mutation_hardening.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_none.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_not.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_readme.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_soft.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_string.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_structural.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_type.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_typing.py +0 -0
- {assertpy2-2.10.0 → assertpy2-2.11.0}/tests/test_warn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.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,7 +131,9 @@ 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
138
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
127
139
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
@@ -129,9 +141,11 @@ For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
|
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+:
|
|
@@ -148,7 +162,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
148
162
|
|
|
149
163
|
---
|
|
150
164
|
|
|
151
|
-
|
|
165
|
+
<h2 align="center">Features</h2>
|
|
152
166
|
|
|
153
167
|
**Fluent API**
|
|
154
168
|
|
|
@@ -162,7 +176,6 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
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
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).
|
|
168
181
|
- [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
@@ -178,7 +191,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
178
191
|
|
|
179
192
|
**Type safety**
|
|
180
193
|
|
|
181
|
-
- [**Type-aware autocomplete**](
|
|
194
|
+
- [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
|
|
182
195
|
- **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
183
196
|
|
|
184
197
|
**Extensibility**
|
|
@@ -187,64 +200,19 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
187
200
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
188
201
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/): `add_extension()` for custom assertion methods.
|
|
189
202
|
|
|
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
203
|
See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
|
|
196
204
|
|
|
197
205
|
---
|
|
198
206
|
|
|
199
|
-
|
|
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
|
-
```
|
|
207
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
|
|
244
208
|
|
|
245
|
-
|
|
209
|
+
Optional adapters, each its own extra; full configuration and examples are in the
|
|
210
|
+
[Integrations guide](https://solganis.github.io/assertpy2/integrations/).
|
|
246
211
|
|
|
247
|
-
|
|
212
|
+
- [**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.
|
|
213
|
+
- [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
|
|
214
|
+
- [**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()`).
|
|
215
|
+
- [**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
216
|
|
|
249
217
|
---
|
|
250
218
|
|
|
@@ -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,7 +83,9 @@ 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
90
|
Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
|
|
89
91
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
@@ -91,9 +93,11 @@ For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
|
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+:
|
|
@@ -110,7 +114,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
110
114
|
|
|
111
115
|
---
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
<h2 align="center">Features</h2>
|
|
114
118
|
|
|
115
119
|
**Fluent API**
|
|
116
120
|
|
|
@@ -124,7 +128,6 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
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
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).
|
|
130
133
|
- [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
@@ -140,7 +143,7 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
140
143
|
|
|
141
144
|
**Type safety**
|
|
142
145
|
|
|
143
|
-
- [**Type-aware autocomplete**](
|
|
146
|
+
- [**Type-aware autocomplete**](https://solganis.github.io/assertpy2/type-safety/): 9 Protocols, IDE shows only relevant methods per type.
|
|
144
147
|
- **py.typed**: `Self` return types, PEP 561 compliant ([PEP 561](https://peps.python.org/pep-0561/)).
|
|
145
148
|
|
|
146
149
|
**Extensibility**
|
|
@@ -149,64 +152,19 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
149
152
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
150
153
|
- [**Extensions**](https://solganis.github.io/assertpy2/extending/): `add_extension()` for custom assertion methods.
|
|
151
154
|
|
|
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
155
|
See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
|
|
158
156
|
|
|
159
157
|
---
|
|
160
158
|
|
|
161
|
-
|
|
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
|
-
```
|
|
159
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
|
|
206
160
|
|
|
207
|
-
|
|
161
|
+
Optional adapters, each its own extra; full configuration and examples are in the
|
|
162
|
+
[Integrations guide](https://solganis.github.io/assertpy2/integrations/).
|
|
208
163
|
|
|
209
|
-
|
|
164
|
+
- [**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.
|
|
165
|
+
- [**Behave**](https://solganis.github.io/assertpy2/integrations/#behave) (`pip install assertpy2[behave]`): ready-made parameter types (`PositiveInt`, `NonEmptyString`, ...) for step definitions like `{age:PositiveInt}`.
|
|
166
|
+
- [**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()`).
|
|
167
|
+
- [**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
168
|
|
|
211
169
|
---
|
|
212
170
|
|
|
@@ -6,7 +6,7 @@ if TYPE_CHECKING:
|
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
from ._compat import Self
|
|
9
|
-
from .errors import DiffResult
|
|
9
|
+
from .errors import DiffEntry, DiffResult
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class _MixinBase:
|
|
@@ -96,6 +96,11 @@ class _MixinBase:
|
|
|
96
96
|
@staticmethod
|
|
97
97
|
def _to_comparable_dict(obj: object) -> dict[str, object] | None: ...
|
|
98
98
|
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _sub_diff_entries(
|
|
101
|
+
actual: object, expected: object, prefix: str, *, _seen: set[int] | None = ...
|
|
102
|
+
) -> list[DiffEntry] | None: ...
|
|
103
|
+
|
|
99
104
|
# NumericMixin class attrs used by HelpersMixin._validate_between_args
|
|
100
105
|
_NUMERIC_COMPAREABLE: frozenset[type]
|
|
101
106
|
_NUMERIC_NON_COMPAREABLE: frozenset[type]
|
|
@@ -34,6 +34,7 @@ from .base import BaseMixin
|
|
|
34
34
|
from .bytes_mixin import BytesMixin
|
|
35
35
|
from .collection import CollectionMixin
|
|
36
36
|
from .contains import ContainsMixin
|
|
37
|
+
from .dataframe import DataFrameMixin
|
|
37
38
|
from .date import DateMixin
|
|
38
39
|
from .dict import DictMixin
|
|
39
40
|
from .dynamic import DynamicMixin
|
|
@@ -48,7 +49,7 @@ from .snapshot import SnapshotMixin
|
|
|
48
49
|
from .string import StringMixin
|
|
49
50
|
from .warning import WarningMixin
|
|
50
51
|
|
|
51
|
-
__version__ = "2.
|
|
52
|
+
__version__ = "2.11.0"
|
|
52
53
|
|
|
53
54
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
54
55
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
|
@@ -548,6 +549,7 @@ class AssertionBuilder(
|
|
|
548
549
|
ContainsMixin,
|
|
549
550
|
CollectionMixin,
|
|
550
551
|
BytesMixin,
|
|
552
|
+
DataFrameMixin,
|
|
551
553
|
BaseMixin,
|
|
552
554
|
):
|
|
553
555
|
"""The main assertion class. Never call the constructor directly, always use the
|
|
@@ -4,7 +4,7 @@ import collections.abc
|
|
|
4
4
|
import dataclasses
|
|
5
5
|
from typing import TYPE_CHECKING, Any, Final
|
|
6
6
|
|
|
7
|
-
from ._introspection import is_model_dump_object, is_namedtuple
|
|
7
|
+
from ._introspection import is_mapping_like, is_model_dump_object, is_namedtuple
|
|
8
8
|
from ._mixin_base import _MixinBase
|
|
9
9
|
from .errors import DiffEntry, DiffResult
|
|
10
10
|
from .matchers import Matcher, StructureMatcher
|
|
@@ -17,6 +17,34 @@ __tracebackhide__ = True
|
|
|
17
17
|
_SENTINEL: Final = object()
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _ambiguous_array_operand(value: object, other: object) -> object | None:
|
|
21
|
+
"""Return the array/frame-like operand whose ``==`` has no single truth value, else ``None``.
|
|
22
|
+
|
|
23
|
+
numpy/pandas/polars containers expose ``__array__`` and compare element-wise, so ``bool(a == b)``
|
|
24
|
+
raises rather than yielding one bool (and a ``DataFrame`` also quacks dict-like, which would otherwise
|
|
25
|
+
mis-dispatch the comparison). The ``__array__`` gate keeps the extra comparison off the hot path; the
|
|
26
|
+
truth test is actually attempted, so 0-d / scalar array values (which *are* truth-testable) pass
|
|
27
|
+
through unchanged.
|
|
28
|
+
"""
|
|
29
|
+
for candidate, counterpart in ((value, other), (other, value)):
|
|
30
|
+
if hasattr(candidate, "__array__"):
|
|
31
|
+
try:
|
|
32
|
+
bool(candidate == counterpart)
|
|
33
|
+
except (ValueError, TypeError):
|
|
34
|
+
return candidate
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _array_equality_error(method: str, operand: object) -> TypeError:
|
|
39
|
+
"""Build the actionable error raised when ``method`` is given an element-wise array/frame-like."""
|
|
40
|
+
return TypeError(
|
|
41
|
+
f"{method}() cannot directly compare <{type(operand).__name__}>: its '==' is element-wise and has"
|
|
42
|
+
" no single truth value. Compare the value's own equality (e.g."
|
|
43
|
+
" assert_that(actual.equals(expected)).is_true()), assert on extracted scalars (columns, shape,"
|
|
44
|
+
" length), or use satisfies(...) with an explicit predicate."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
20
48
|
class BaseMixin(_MixinBase):
|
|
21
49
|
"""Base mixin."""
|
|
22
50
|
|
|
@@ -115,7 +143,9 @@ class BaseMixin(_MixinBase):
|
|
|
115
143
|
Raises:
|
|
116
144
|
AssertionError: if actual is **not** equal to expected
|
|
117
145
|
TypeError: if ``ignore``/``include`` is a one-shot or otherwise unsupported iterable, or is
|
|
118
|
-
used on a value that is neither dict-like nor has introspectable fields
|
|
146
|
+
used on a value that is neither dict-like nor has introspectable fields; or if val or
|
|
147
|
+
other is an element-wise array/frame-like (numpy/pandas/polars) whose ``==`` has no single
|
|
148
|
+
truth value (compare the value's own equality, e.g. ``actual.equals(expected)``, instead)
|
|
119
149
|
|
|
120
150
|
Tip:
|
|
121
151
|
Using :meth:`is_equal_to` with a ``float`` val is just asking for trouble. Instead, you'll
|
|
@@ -128,6 +158,10 @@ class BaseMixin(_MixinBase):
|
|
|
128
158
|
ignore = kwargs.get("ignore")
|
|
129
159
|
include = kwargs.get("include")
|
|
130
160
|
|
|
161
|
+
operand = _ambiguous_array_operand(self.val, other)
|
|
162
|
+
if operand is not None:
|
|
163
|
+
raise _array_equality_error("is_equal_to", operand)
|
|
164
|
+
|
|
131
165
|
if self._is_dict_like(self.val, check_values=False) and self._is_dict_like(other, check_values=False):
|
|
132
166
|
if self._dict_not_equal(self.val, other, ignore=ignore, include=include):
|
|
133
167
|
self._dict_err(self.val, other, ignore=ignore, include=include)
|
|
@@ -335,26 +369,36 @@ class BaseMixin(_MixinBase):
|
|
|
335
369
|
def _sub_diff_entries(
|
|
336
370
|
actual: object, expected: object, prefix: str, *, _seen: set[int] | None = None
|
|
337
371
|
) -> list[DiffEntry] | None:
|
|
372
|
+
"""Canonical recursive diff for a value, returning path-level entries (or ``None`` for a leaf).
|
|
373
|
+
|
|
374
|
+
Recurses into mappings, dataclasses, namedtuples, model-dump objects and sequences; returns
|
|
375
|
+
``None`` for anything else so the caller renders a single leaf entry. This is the single nested
|
|
376
|
+
engine shared by the top-level paths: :meth:`_build_equality_diff` (lists, dataclasses, ...) and
|
|
377
|
+
the dict path (:meth:`HelpersMixin._dict_err`), which calls it with an empty ``prefix`` so the
|
|
378
|
+
top-level dict keys render bare (``b``) and nested keys render dotted (``u.b``).
|
|
379
|
+
"""
|
|
338
380
|
if _seen is None:
|
|
339
381
|
_seen = set()
|
|
340
382
|
if id(actual) in _seen or id(expected) in _seen:
|
|
341
383
|
return [DiffEntry(path=prefix, actual="<circular ref>", expected="<circular ref>")]
|
|
342
384
|
|
|
343
|
-
if
|
|
385
|
+
if is_mapping_like(actual) and is_mapping_like(expected):
|
|
344
386
|
child_seen = _seen | {id(actual), id(expected)}
|
|
345
387
|
entries: list[DiffEntry] = []
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
entries.append(DiffEntry(path=path, actual=
|
|
352
|
-
elif
|
|
353
|
-
|
|
388
|
+
actual_keys = set(actual)
|
|
389
|
+
expected_keys = set(expected)
|
|
390
|
+
for key in sorted(actual_keys | expected_keys, key=repr):
|
|
391
|
+
path = f"{prefix}.{key}" if prefix else str(key)
|
|
392
|
+
if key not in expected_keys:
|
|
393
|
+
entries.append(DiffEntry(path=path, actual=actual[key], expected=None))
|
|
394
|
+
elif key not in actual_keys:
|
|
395
|
+
entries.append(DiffEntry(path=path, actual=None, expected=expected[key]))
|
|
396
|
+
elif actual[key] != expected[key]:
|
|
397
|
+
sub_entries = BaseMixin._sub_diff_entries(actual[key], expected[key], path, _seen=child_seen)
|
|
354
398
|
if sub_entries is not None:
|
|
355
399
|
entries.extend(sub_entries)
|
|
356
400
|
else:
|
|
357
|
-
entries.append(DiffEntry(path=path, actual=actual[key], expected=expected[key]))
|
|
401
|
+
entries.append(DiffEntry(path=path, actual=actual[key], expected=expected[key]))
|
|
358
402
|
return entries or None
|
|
359
403
|
if (
|
|
360
404
|
dataclasses.is_dataclass(actual)
|
|
@@ -715,7 +759,13 @@ class BaseMixin(_MixinBase):
|
|
|
715
759
|
|
|
716
760
|
Raises:
|
|
717
761
|
AssertionError: if actual **is** equal to expected
|
|
762
|
+
TypeError: if val or other is an element-wise array/frame-like (numpy/pandas/polars) whose
|
|
763
|
+
``==`` has no single truth value; compare the value's own equality instead
|
|
718
764
|
"""
|
|
765
|
+
operand = _ambiguous_array_operand(self.val, other)
|
|
766
|
+
if operand is not None:
|
|
767
|
+
raise _array_equality_error("is_not_equal_to", operand)
|
|
768
|
+
|
|
719
769
|
if self.val == other:
|
|
720
770
|
return self.error(f"Expected <{self.val}> to be not equal to <{other}>, but was.")
|
|
721
771
|
return self
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Optional fluent assertions for data-science containers (pandas / polars / numpy).
|
|
2
|
+
|
|
3
|
+
This is an *integration* layer in the same spirit as the Allure and Behave adapters: each library is its
|
|
4
|
+
own optional extra (``pip install assertpy2[pandas]`` / ``[polars]`` / ``[numpy]``, or ``[data]`` for all
|
|
5
|
+
three), imported lazily by name, so the core stays free of runtime dependencies. Comparison
|
|
6
|
+
**semantics** are delegated entirely to each library's own testing
|
|
7
|
+
utilities (``assert_frame_equal`` / ``assert_series_equal`` / ``assert_array_equal`` / ``assert_allclose``),
|
|
8
|
+
so dtype, tolerance and NaN handling match the library exactly. This layer only adds the fluent entry
|
|
9
|
+
point and routes failures through the standard assertpy2 error model.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from ._mixin_base import _MixinBase
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ._compat import Self
|
|
21
|
+
|
|
22
|
+
__tracebackhide__ = True
|
|
23
|
+
|
|
24
|
+
_FRAME_ROOTS = ("pandas", "polars")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _ensure_module(name: str) -> Any:
|
|
28
|
+
"""Import optional library *name* by string, or raise a clear ImportError pointing at its extra."""
|
|
29
|
+
try:
|
|
30
|
+
return importlib.import_module(name)
|
|
31
|
+
except ImportError:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
f"{name} is required for these assertions. Install it with: pip install assertpy2[{name}]"
|
|
34
|
+
) from None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load(root: str) -> tuple[Any, Any]:
|
|
38
|
+
"""Return ``(library, library.testing)`` for *root* (``pandas``/``polars``/``numpy``)."""
|
|
39
|
+
library = _ensure_module(root)
|
|
40
|
+
return library, importlib.import_module(f"{root}.testing")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DataFrameMixin(_MixinBase):
|
|
44
|
+
"""Fluent assertions for pandas/polars frames and numpy arrays (optional ``[data]`` extra)."""
|
|
45
|
+
|
|
46
|
+
def is_frame_equal(self, expected: object, **options: Any) -> Self:
|
|
47
|
+
"""Asserts that a pandas/polars ``DataFrame`` or ``Series`` equals *expected*.
|
|
48
|
+
|
|
49
|
+
Delegates to the owning library's own ``assert_frame_equal`` / ``assert_series_equal``, so all
|
|
50
|
+
comparison semantics (dtype strictness, row/column order, tolerance, categoricals, ...) are the
|
|
51
|
+
library's. Any keyword options are passed straight through.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
expected: the expected frame/series (same library as val)
|
|
55
|
+
**options: keyword options forwarded to the library's ``assert_frame_equal`` /
|
|
56
|
+
``assert_series_equal`` (e.g. ``check_dtype=False``, ``check_exact=False``, ``rtol=1e-3``)
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
Usage::
|
|
60
|
+
|
|
61
|
+
import pandas as pd
|
|
62
|
+
|
|
63
|
+
assert_that(pd.DataFrame({"a": [1, 2]})).is_frame_equal(pd.DataFrame({"a": [1, 2]}))
|
|
64
|
+
assert_that(actual).is_frame_equal(expected, check_dtype=False)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
AssertionError: if the frames/series are not equal (carrying the library's own diff message)
|
|
71
|
+
TypeError: if val is not a pandas or polars ``DataFrame``/``Series``
|
|
72
|
+
ImportError: if the owning library is not installed
|
|
73
|
+
"""
|
|
74
|
+
actual = self.val
|
|
75
|
+
root = type(actual).__module__.split(".", 1)[0]
|
|
76
|
+
if root not in _FRAME_ROOTS:
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"is_frame_equal() expects a pandas or polars DataFrame/Series, but was <{type(actual).__name__}>."
|
|
79
|
+
)
|
|
80
|
+
library, testing = _load(root)
|
|
81
|
+
if isinstance(actual, library.Series):
|
|
82
|
+
assert_equal, label = testing.assert_series_equal, "Series"
|
|
83
|
+
else:
|
|
84
|
+
assert_equal, label = testing.assert_frame_equal, "DataFrame"
|
|
85
|
+
try:
|
|
86
|
+
assert_equal(actual, expected, **options)
|
|
87
|
+
except AssertionError as exc:
|
|
88
|
+
return self.error(f"Expected the {label} to equal the expected one, but they differ:\n{exc}")
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def is_array_equal(self, expected: object) -> Self:
|
|
92
|
+
"""Asserts that val equals *expected* element-wise, via numpy's ``assert_array_equal``.
|
|
93
|
+
|
|
94
|
+
Works on any array-likes numpy can coerce (``ndarray``, nested lists, ...); shape and every
|
|
95
|
+
element must match exactly (with ``NaN`` treated as equal, per numpy).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
expected: the expected array-like
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
Usage::
|
|
102
|
+
|
|
103
|
+
import numpy as np
|
|
104
|
+
|
|
105
|
+
assert_that(np.array([1, 2, 3])).is_array_equal(np.array([1, 2, 3]))
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
AssertionError: if the arrays are not equal (carrying numpy's own diff message)
|
|
112
|
+
ImportError: if numpy is not installed
|
|
113
|
+
"""
|
|
114
|
+
_, testing = _load("numpy")
|
|
115
|
+
try:
|
|
116
|
+
testing.assert_array_equal(self.val, expected)
|
|
117
|
+
except AssertionError as exc:
|
|
118
|
+
return self.error(f"Expected the arrays to be equal, but they differ:\n{exc}")
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def is_array_close_to(
|
|
122
|
+
self, expected: object, *, rtol: float = 1e-05, atol: float = 1e-08, equal_nan: bool = False
|
|
123
|
+
) -> Self:
|
|
124
|
+
"""Asserts that val is element-wise close to *expected*, via numpy's ``assert_allclose``.
|
|
125
|
+
|
|
126
|
+
The float-tolerant counterpart to :meth:`is_array_equal`, for comparing computed arrays.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
expected: the expected array-like
|
|
130
|
+
rtol: relative tolerance (numpy default ``1e-05``)
|
|
131
|
+
atol: absolute tolerance (numpy default ``1e-08``)
|
|
132
|
+
equal_nan: whether ``NaN`` in the same position compares equal
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
Usage::
|
|
136
|
+
|
|
137
|
+
import numpy as np
|
|
138
|
+
|
|
139
|
+
assert_that(np.array([1.0, 2.0])).is_array_close_to(np.array([1.0, 2.0000001]))
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
AssertionError: if the arrays are not close (carrying numpy's own diff message)
|
|
146
|
+
ImportError: if numpy is not installed
|
|
147
|
+
"""
|
|
148
|
+
_, testing = _load("numpy")
|
|
149
|
+
try:
|
|
150
|
+
testing.assert_allclose(self.val, expected, rtol=rtol, atol=atol, equal_nan=equal_nan)
|
|
151
|
+
except AssertionError as exc:
|
|
152
|
+
return self.error(f"Expected the arrays to be close, but they differ:\n{exc}")
|
|
153
|
+
return self
|