assertpy2 2.11.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.11.0 → assertpy2-2.12.0}/.github/workflows/mutation.yml +2 -2
- {assertpy2-2.11.0 → assertpy2-2.12.0}/PKG-INFO +6 -7
- {assertpy2-2.11.0 → assertpy2-2.12.0}/README.md +5 -6
- assertpy2-2.12.0/assertpy2/_compare.py +136 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/_compat.py +1 -1
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/_mixin_base.py +11 -3
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/_typing.py +22 -4
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/assertpy.py +47 -46
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/async_assertions.py +1 -1
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/base.py +432 -128
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/collection.py +44 -13
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/contains.py +77 -25
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/dataframe.py +5 -4
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/date.py +23 -19
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/dict.py +16 -16
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/dynamic.py +3 -3
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/exception.py +21 -16
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/extracting.py +18 -18
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/file.py +16 -16
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/helpers.py +106 -79
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/json_mixin.py +6 -6
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/matchers.py +34 -7
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/numeric.py +40 -39
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/snapshot.py +38 -38
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/string.py +31 -30
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/warning.py +18 -16
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assertions.md +64 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/comparison.md +9 -5
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/errors.md +1 -1
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/extending.md +3 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/fluent.md +7 -1
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/getting-started.md +2 -1
- assertpy2-2.12.0/docs/index.md +103 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/integrations.md +3 -1
- {assertpy2-2.11.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.11.0 → assertpy2-2.12.0}/pyproject.toml +8 -4
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_bytes.py +20 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_chaining.py +3 -3
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_dataframe.py +13 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_datetime.py +120 -120
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_dict_compare.py +18 -18
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_dynamic.py +17 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_extracting.py +20 -0
- assertpy2-2.12.0/tests/test_iterable_cluster.py +201 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_matchers.py +3 -3
- {assertpy2-2.11.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.11.0 → assertpy2-2.12.0}/tests/test_typing.py +18 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/uv.lock +215 -157
- assertpy2-2.11.0/docs/index.md +0 -55
- assertpy2-2.11.0/mkdocs.yml +0 -82
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.codecov.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.gitattributes +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/dependabot.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/workflows/ci.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/workflows/codeql.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/workflows/docs.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/workflows/publish.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/workflows/scorecard.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.github/workflows/zizmor.yml +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/.gitignore +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/LICENSE +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/SECURITY.md +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/_introspection.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/data.md +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/logo.svg +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/matchers.md +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/testing.md +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/docs/type-safety.md +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_async.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_bool.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_callable.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_class.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_collection.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_core.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_description.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_dict.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_equals.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_errors.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_fail.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_file.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_in.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_json.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_list.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_none.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_not.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_readme.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_soft.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_string.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_structural.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.11.0 → assertpy2-2.12.0}/tests/test_type.py +0 -0
- {assertpy2-2.11.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
|
|
@@ -135,7 +135,7 @@ assert_that(response).is_equal_to(expected)
|
|
|
135
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
136
|
</p>
|
|
137
137
|
|
|
138
|
-
Recursive diffs work for dicts, dataclasses, namedtuples,
|
|
138
|
+
Recursive diffs work for dicts, dataclasses, namedtuples, and Pydantic models.
|
|
139
139
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
140
140
|
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
141
141
|
|
|
@@ -160,16 +160,16 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
160
160
|
|
|
161
161
|
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
162
162
|
|
|
163
|
-
---
|
|
164
|
-
|
|
165
163
|
<h2 align="center">Features</h2>
|
|
166
164
|
|
|
167
165
|
**Fluent API**
|
|
168
166
|
|
|
169
167
|
- [**Composable matchers**](https://solganis.github.io/assertpy2/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
170
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.
|
|
171
170
|
- [**Universal negation**](https://solganis.github.io/assertpy2/fluent/#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
172
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()`.
|
|
173
173
|
- [**Fluent chaining**](https://solganis.github.io/assertpy2/fluent/#chaining): write assertions as readable one-liners that chain naturally.
|
|
174
174
|
|
|
175
175
|
**Built-in types**
|
|
@@ -177,7 +177,8 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
177
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).
|
|
178
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`.
|
|
179
179
|
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
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).
|
|
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.
|
|
181
182
|
- [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
182
183
|
|
|
183
184
|
**Testing**
|
|
@@ -202,8 +203,6 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
202
203
|
|
|
203
204
|
See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
|
|
204
205
|
|
|
205
|
-
---
|
|
206
|
-
|
|
207
206
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
|
|
208
207
|
|
|
209
208
|
Optional adapters, each its own extra; full configuration and examples are in the
|
|
@@ -87,7 +87,7 @@ assert_that(response).is_equal_to(expected)
|
|
|
87
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
88
|
</p>
|
|
89
89
|
|
|
90
|
-
Recursive diffs work for dicts, dataclasses, namedtuples,
|
|
90
|
+
Recursive diffs work for dicts, dataclasses, namedtuples, and Pydantic models.
|
|
91
91
|
For responses with dynamic fields (IDs, timestamps), validate a subset with
|
|
92
92
|
[`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
|
|
93
93
|
|
|
@@ -112,16 +112,16 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
112
112
|
|
|
113
113
|
See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
|
|
114
114
|
|
|
115
|
-
---
|
|
116
|
-
|
|
117
115
|
<h2 align="center">Features</h2>
|
|
118
116
|
|
|
119
117
|
**Fluent API**
|
|
120
118
|
|
|
121
119
|
- [**Composable matchers**](https://solganis.github.io/assertpy2/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`. Also work with plain `assert ==`.
|
|
122
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.
|
|
123
122
|
- [**Universal negation**](https://solganis.github.io/assertpy2/fluent/#universal-negation): `.not_` inverts any assertion without dedicated `is_not_*` methods.
|
|
124
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()`.
|
|
125
125
|
- [**Fluent chaining**](https://solganis.github.io/assertpy2/fluent/#chaining): write assertions as readable one-liners that chain naturally.
|
|
126
126
|
|
|
127
127
|
**Built-in types**
|
|
@@ -129,7 +129,8 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
129
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).
|
|
130
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`.
|
|
131
131
|
- [**Dynamic assertions**](https://solganis.github.io/assertpy2/assertions/#dynamic-assertions-on-objects): `has_<name>()` for any attribute, property, or zero-argument method.
|
|
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).
|
|
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.
|
|
133
134
|
- [**Extracting**](https://solganis.github.io/assertpy2/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
134
135
|
|
|
135
136
|
**Testing**
|
|
@@ -154,8 +155,6 @@ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) gui
|
|
|
154
155
|
|
|
155
156
|
See the [full documentation](https://solganis.github.io/assertpy2/) for all assertion methods, examples, and advanced features.
|
|
156
157
|
|
|
157
|
-
---
|
|
158
|
-
|
|
159
158
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/integrations/">Integrations</a></h2>
|
|
160
159
|
|
|
161
160
|
Optional adapters, each its own extra; full configuration and examples are in the
|
|
@@ -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,6 +5,7 @@ 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
10
|
from .errors import DiffEntry, DiffResult
|
|
10
11
|
|
|
@@ -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,6 +93,7 @@ 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
|
|
@@ -98,7 +101,12 @@ class _MixinBase:
|
|
|
98
101
|
|
|
99
102
|
@staticmethod
|
|
100
103
|
def _sub_diff_entries(
|
|
101
|
-
actual: object,
|
|
104
|
+
actual: object,
|
|
105
|
+
expected: object,
|
|
106
|
+
prefix: str,
|
|
107
|
+
*,
|
|
108
|
+
_seen: set[int] | None = ...,
|
|
109
|
+
config: _CompareConfig | None = ...,
|
|
102
110
|
) -> list[DiffEntry] | None: ...
|
|
103
111
|
|
|
104
112
|
# NumericMixin class attrs used by HelpersMixin._validate_between_args
|
|
@@ -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
|
"""
|