assertpy2 2.20.1__tar.gz → 2.22.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.22.0/CONTRIBUTING.md +83 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/PKG-INFO +107 -42
- {assertpy2-2.20.1 → assertpy2-2.22.0}/README.md +105 -40
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/__init__.py +2 -0
- assertpy2-2.22.0/assertpy2/_clustering.py +499 -0
- assertpy2-2.22.0/assertpy2/_dangling.py +292 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_compare.py +31 -10
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_diff.py +76 -15
- assertpy2-2.22.0/assertpy2/_engine/_equality.py +205 -0
- assertpy2-2.22.0/assertpy2/_engine/_membership.py +312 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_mixin_base.py +16 -0
- assertpy2-2.22.0/assertpy2/_engine/_ordering.py +128 -0
- assertpy2-2.22.0/assertpy2/_engine/_require.py +142 -0
- assertpy2-2.22.0/assertpy2/_engine/_size.py +33 -0
- assertpy2-2.22.0/assertpy2/_engine/_text.py +39 -0
- assertpy2-2.22.0/assertpy2/_engine/_typing.py +632 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_hints.py +90 -31
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_matcher_impls.py +377 -87
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_satisfies.py +44 -36
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/assertpy.py +226 -89
- assertpy2-2.22.0/assertpy2/async_assertions.py +756 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/base.py +33 -16
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/bytes_mixin.py +4 -3
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/collection.py +66 -56
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/contains.py +43 -49
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/dataframe.py +3 -6
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/date.py +9 -9
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/dict.py +3 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/errors.py +18 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/exception.py +181 -13
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/extracting.py +15 -12
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/file.py +5 -8
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/helpers.py +75 -196
- assertpy2-2.22.0/assertpy2/http_mixin.py +224 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/json_mixin.py +21 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/matchers.py +95 -23
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/numeric.py +27 -22
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/pytest_plugin.py +413 -18
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/snapshot.py +4 -8
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/string.py +43 -71
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/warning.py +6 -3
- {assertpy2-2.20.1 → assertpy2-2.22.0}/benchmarks/test_perf.py +137 -10
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/concepts/stability.md +31 -7
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/concepts/type-safety.md +36 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/extending/custom-assertions.md +3 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/extending/integrations.md +21 -10
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/getting-started/comparison.md +3 -3
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/getting-started/migration.md +4 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/assertions.md +113 -10
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/data.md +18 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/errors.md +163 -8
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/fluent.md +15 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/matchers.md +9 -4
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/guides/testing.md +121 -3
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/index.md +18 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/recipes.md +32 -6
- assertpy2-2.22.0/docs/reference/http.md +6 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/overview.md +2 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/mkdocs.yml +1 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/pyproject.toml +48 -6
- assertpy2-2.22.0/tests/api_snapshot.json +3724 -0
- assertpy2-2.22.0/tests/api_surface.py +381 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/docs_fixtures.py +1 -0
- assertpy2-2.22.0/tests/group_compat.py +31 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/pyright_baseline.py +12 -13
- assertpy2-2.22.0/tests/test_api_compatibility.py +386 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_api_vocabulary.py +2 -0
- assertpy2-2.22.0/tests/test_async.py +1490 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_attrs_support.py +15 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_bytes.py +7 -7
- assertpy2-2.22.0/tests/test_clustering.py +1578 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_collection.py +49 -4
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_core.py +3 -3
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_custom_dict.py +5 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_custom_list.py +2 -2
- assertpy2-2.22.0/tests/test_dangling.py +906 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_datetime.py +57 -33
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dict.py +52 -8
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_docs_typing.py +4 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_equals.py +23 -0
- assertpy2-2.22.0/tests/test_evaluation_core.py +1211 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_exception_context.py +33 -15
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_expected_exception.py +189 -10
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_expected_warning.py +1 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_extensions.py +66 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_extracting.py +5 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_file.py +2 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_hints.py +372 -1
- assertpy2-2.22.0/tests/test_http.py +599 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_inline_record.py +72 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_inline_snapshot.py +51 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_iterable_cluster.py +17 -13
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_json.py +2 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_list.py +11 -11
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_matcher_registry.py +2 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_matchers.py +46 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_not.py +187 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_numbers.py +78 -47
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_pipeline.py +7 -7
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_property_based.py +981 -23
- assertpy2-2.22.0/tests/test_protocol_parity.py +603 -0
- assertpy2-2.22.0/tests/test_public_surface.py +325 -0
- assertpy2-2.22.0/tests/test_pyright_baseline.py +102 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_pytest_plugin.py +635 -26
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_assertion.py +6 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare_config.py +221 -5
- assertpy2-2.22.0/tests/test_refusals.py +425 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_regex_groups.py +2 -2
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_rich_diff.py +778 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_satisfy.py +183 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_snapshots.py +411 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_soft.py +261 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_string.py +40 -36
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_structural.py +10 -5
- assertpy2-2.22.0/tests/test_surface_conformance.py +396 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_sync_eventually.py +19 -1
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_type.py +8 -5
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_typing.py +88 -16
- assertpy2-2.22.0/tests/test_typing_negative.py +204 -0
- assertpy2-2.22.0/tests/typing_cases.py +370 -0
- assertpy2-2.22.0/tests/typing_negative_baseline.py +257 -0
- assertpy2-2.20.1/.codecov.yml +0 -10
- assertpy2-2.20.1/.gitattributes +0 -2
- assertpy2-2.20.1/.github/dependabot.yml +0 -31
- assertpy2-2.20.1/.github/workflows/ci.yml +0 -181
- assertpy2-2.20.1/.github/workflows/codeql.yml +0 -27
- assertpy2-2.20.1/.github/workflows/codspeed.yml +0 -41
- assertpy2-2.20.1/.github/workflows/docs.yml +0 -59
- assertpy2-2.20.1/.github/workflows/mutation.yml +0 -78
- assertpy2-2.20.1/.github/workflows/publish.yml +0 -75
- assertpy2-2.20.1/.github/workflows/scorecard.yml +0 -35
- assertpy2-2.20.1/.github/workflows/zizmor.yml +0 -26
- assertpy2-2.20.1/CONTRIBUTING.md +0 -67
- assertpy2-2.20.1/assertpy2/_engine/_typing.py +0 -430
- assertpy2-2.20.1/assertpy2/async_assertions.py +0 -388
- assertpy2-2.20.1/scripts/mutation_report.py +0 -142
- assertpy2-2.20.1/tests/test_async.py +0 -628
- assertpy2-2.20.1/tests/test_protocol_parity.py +0 -219
- assertpy2-2.20.1/tests/test_public_surface.py +0 -168
- assertpy2-2.20.1/tests/test_pyright_baseline.py +0 -54
- assertpy2-2.20.1/uv.lock +0 -2785
- {assertpy2-2.20.1 → assertpy2-2.22.0}/.gitignore +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/LICENSE +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/SECURITY.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_contract.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_introspection.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_inline.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/_snapshot_codec.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/logo.svg +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/async.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/core.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/files.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/json.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/conftest.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_bool.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_callable.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_check.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_class.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dataframe.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_description.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_docs_examples.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_errors.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_fail.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_in.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_match_result.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_matcher_parity.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_message_elision.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_none.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_openapi_contract.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_optional_integration_contracts.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_property_attrs.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_typing_claims.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_vacuity_contract.py +0 -0
- {assertpy2-2.20.1 → assertpy2-2.22.0}/tests/test_warn.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions of docs, tests, or code are welcome.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
1. For a new assertion or matcher, open an issue first. The API grows from demand, and a working
|
|
8
|
+
implementation on its own is not enough to land one
|
|
9
|
+
2. Fork the repo
|
|
10
|
+
3. Clone your fork (`git clone <your_fork_url>`)
|
|
11
|
+
4. Create a branch (`git checkout -b my_branch`)
|
|
12
|
+
5. Install dependencies with the [one sync command](#verification-pipeline) below, not a bare `uv sync`
|
|
13
|
+
6. Make your changes
|
|
14
|
+
7. Run the [verification pipeline](#verification-pipeline) and fix any issues
|
|
15
|
+
8. Commit using [Conventional Commits](#commit-style)
|
|
16
|
+
9. Push your branch (`git push origin my_branch`)
|
|
17
|
+
10. Open a [Pull Request](http://github.com/Solganis/assertpy2/pulls)
|
|
18
|
+
|
|
19
|
+
Read more about how pulls work on GitHub's [About pull requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) page.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Python 3.10+
|
|
24
|
+
- [uv](https://docs.astral.sh/uv/) as the package manager
|
|
25
|
+
|
|
26
|
+
## Verification pipeline
|
|
27
|
+
|
|
28
|
+
Run all checks before submitting a PR. Every step must pass.
|
|
29
|
+
|
|
30
|
+
Install exactly this set. A bare `uv sync` leaves out the optional libraries several tests import, and
|
|
31
|
+
those tests fail rather than skip:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv sync --extra json --extra data --extra inline --group integrations --group docs-examples
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run ruff check .
|
|
39
|
+
uv run ruff format --check .
|
|
40
|
+
uv run ty check assertpy2/ tests/test_typing.py
|
|
41
|
+
uv run pytest --cov=assertpy2 --cov-fail-under=100 --ignore=tests/test_docs_examples.py tests
|
|
42
|
+
uv run pytest tests/test_docs_examples.py tests/test_typing_claims.py
|
|
43
|
+
uv run --group docs mkdocs build --strict
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The public typing surface is checked by three checkers. Run them after the block above, not before:
|
|
47
|
+
`uv run --group typecheck` adds the checkers to the same `.venv` and leaves them there, and a coverage
|
|
48
|
+
run with them installed collects a different set:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
|
|
52
|
+
uv run --group typecheck pyright tests/test_typing.py
|
|
53
|
+
uv run --group typecheck pytest tests/test_pyright_baseline.py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Three details that cost time if you meet them the hard way:
|
|
57
|
+
|
|
58
|
+
- `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
|
|
59
|
+
`tests/`, which is full of deliberate negative typing cases: calls that must not type-check.
|
|
60
|
+
- `--all-extras` is not a shortcut for the sync line. Installing `allure` or `behave` makes 100%
|
|
61
|
+
coverage unreachable by construction, because the `except ImportError` branch guarding them stops
|
|
62
|
+
being executed. CI never installs them for the coverage job either.
|
|
63
|
+
- `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
|
|
64
|
+
needs the `docs-examples` group and a different collection.
|
|
65
|
+
|
|
66
|
+
CI requires 100% code coverage.
|
|
67
|
+
|
|
68
|
+
## Commit style
|
|
69
|
+
|
|
70
|
+
Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
Write tests for every new feature or bug fix. Use `assertpy2` assertions in tests.
|
|
75
|
+
|
|
76
|
+
## Documentation examples
|
|
77
|
+
|
|
78
|
+
Guide code blocks are executed and type-checked in CI, so an example you add has to run. A block that
|
|
79
|
+
cannot (pseudo-context, a deliberate failure, a rejected counter-example) is marked with an HTML comment
|
|
80
|
+
above the fence: `tests/test_docs_examples.py` lists the markers and what each one exempts.
|
|
81
|
+
|
|
82
|
+
Setup a page assumes (a domain class, a repository, an HTTP response) goes in
|
|
83
|
+
`tests/docs_fixtures.py`, not into an extra block on the page.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.22.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
|
|
@@ -57,7 +57,7 @@ Description-Content-Type: text/markdown
|
|
|
57
57
|
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
58
58
|
</picture>
|
|
59
59
|
<br>
|
|
60
|
-
<b>
|
|
60
|
+
<b>A fully typed fluent assertion library for Python</b><br>
|
|
61
61
|
A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
|
|
62
62
|
</p>
|
|
63
63
|
|
|
@@ -95,23 +95,44 @@ def test_user():
|
|
|
95
95
|
|
|
96
96
|
The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
|
|
97
97
|
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
<table>
|
|
99
|
+
<tr>
|
|
100
|
+
<td valign="top" width="50%">
|
|
101
|
+
<a href="#why-fluent-assertions"><b>Failures that point at the difference</b></a><br>
|
|
102
|
+
A recursive diff names the exact path that differs, in color, instead of dumping both structures.
|
|
103
|
+
</td>
|
|
104
|
+
<td valign="top" width="50%">
|
|
105
|
+
<a href="#type-aware-autocomplete"><b>Type-aware autocomplete</b></a><br>
|
|
106
|
+
<code>assert_that()</code> returns a protocol per value type, so your IDE offers the methods that fit.
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<tr>
|
|
110
|
+
<td valign="top">
|
|
111
|
+
<a href="#typed-narrowing"><b>Typed narrowing</b></a><br>
|
|
112
|
+
An assertion hands the value back statically narrowed, with no <code>cast</code> and no bare <code>assert</code>.
|
|
113
|
+
</td>
|
|
114
|
+
<td valign="top">
|
|
115
|
+
<a href="#features"><b>Composable matchers</b></a><br>
|
|
116
|
+
45 matchers that combine with <code>&</code>, <code>|</code>, <code>~</code> and nest inside the expected structure itself.
|
|
117
|
+
</td>
|
|
118
|
+
</tr>
|
|
119
|
+
<tr>
|
|
120
|
+
<td valign="top">
|
|
121
|
+
<a href="#features"><b>Built for test suites</b></a><br>
|
|
122
|
+
Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains.
|
|
123
|
+
</td>
|
|
124
|
+
<td valign="top">
|
|
125
|
+
<a href="#integrations"><b>Integrations</b></a><br>
|
|
126
|
+
Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts.
|
|
127
|
+
</td>
|
|
128
|
+
</tr>
|
|
129
|
+
</table>
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
```python
|
|
105
|
-
# bare - three statements, no autocomplete help
|
|
106
|
-
assert isinstance(items, list)
|
|
107
|
-
assert len(items) == 3
|
|
108
|
-
assert "admin" in items
|
|
131
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
|
|
109
132
|
|
|
110
|
-
|
|
111
|
-
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
112
|
-
```
|
|
133
|
+
`assert` states a condition well, and pytest reports it well.
|
|
113
134
|
|
|
114
|
-
|
|
135
|
+
What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
|
|
115
136
|
|
|
116
137
|
```text
|
|
117
138
|
assert response == expected
|
|
@@ -122,7 +143,7 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
|
|
|
122
143
|
E {'status': 'active'} != {'status': 'disabled'}
|
|
123
144
|
```
|
|
124
145
|
|
|
125
|
-
assertpy2
|
|
146
|
+
assertpy2 names the [exact path](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
|
|
126
147
|
|
|
127
148
|
<!-- docs-guard: skip -->
|
|
128
149
|
```python
|
|
@@ -133,24 +154,45 @@ assert_that(response).is_equal_to(expected)
|
|
|
133
154
|
<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">
|
|
134
155
|
</p>
|
|
135
156
|
|
|
136
|
-
|
|
157
|
+
It recurses through nested containers, and matcher predicates get the same treatment.
|
|
137
158
|
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
For dynamic fields like IDs, assert a subset with
|
|
160
|
+
[`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
|
|
161
|
+
|
|
162
|
+
The chain is the other half: one statement carries the whole intent, and your IDE offers only the
|
|
163
|
+
[methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
|
|
164
|
+
|
|
165
|
+
<!-- docs-guard: skip -->
|
|
166
|
+
```python
|
|
167
|
+
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
|
|
171
|
+
|
|
172
|
+
Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:
|
|
140
173
|
|
|
141
174
|
```python
|
|
142
175
|
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
|
|
143
176
|
|
|
144
177
|
assert_that(response).is_equal_to(
|
|
145
|
-
{
|
|
178
|
+
{
|
|
179
|
+
"id": match.greater_than(0),
|
|
180
|
+
"user": {"name": "Alice", "age": match.between(18, 120)},
|
|
181
|
+
"tags": ["a", "b"],
|
|
182
|
+
}
|
|
146
183
|
)
|
|
147
184
|
|
|
148
185
|
# or keep the bare `assert`, and pytest's own rewriting reports it
|
|
149
|
-
assert response == {
|
|
186
|
+
assert response == {
|
|
187
|
+
"id": match.greater_than(0),
|
|
188
|
+
"user": match.ignore(),
|
|
189
|
+
"tags": ["a", "b"],
|
|
190
|
+
}
|
|
150
191
|
```
|
|
151
192
|
|
|
152
|
-
The fluent form keeps the path-level diff, the bare form keeps pytest's.
|
|
153
|
-
|
|
193
|
+
The fluent form keeps the path-level diff, the bare form keeps pytest's.
|
|
194
|
+
|
|
195
|
+
There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/), combining with `&`, `|` and `~`.
|
|
154
196
|
|
|
155
197
|
<p align="center">
|
|
156
198
|
<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">
|
|
@@ -167,13 +209,14 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
167
209
|
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
168
210
|
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
169
211
|
|
|
170
|
-
|
|
212
|
+
11 type-specific Protocols instead of one `Any`.<br>
|
|
171
213
|
Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
172
214
|
|
|
173
215
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
174
216
|
|
|
175
|
-
An assertion hands the value back, statically narrowed.
|
|
176
|
-
|
|
217
|
+
An assertion hands the value back, statically narrowed.
|
|
218
|
+
|
|
219
|
+
`is_not_none()` strips `None`, `is_instance_of()` narrows to the class, and `.value` returns it:
|
|
177
220
|
|
|
178
221
|
<!-- docs-guard: skip -->
|
|
179
222
|
```python
|
|
@@ -181,31 +224,52 @@ order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
|
181
224
|
order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
182
225
|
```
|
|
183
226
|
|
|
184
|
-
For API tests,
|
|
185
|
-
|
|
227
|
+
For API tests,
|
|
228
|
+
[`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms)
|
|
229
|
+
validates a payload against a Pydantic model and narrows to it. `exact=True` catches contract drift:
|
|
186
230
|
|
|
187
231
|
<!-- docs-guard: skip -->
|
|
188
232
|
```python
|
|
189
233
|
data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
190
234
|
```
|
|
191
235
|
|
|
236
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
|
|
237
|
+
|
|
238
|
+
An exception is the right default, and a dead end for anything that wants to read the result.
|
|
239
|
+
|
|
240
|
+
`check()` runs the next assertion for its verdict instead:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
|
|
244
|
+
expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}
|
|
245
|
+
|
|
246
|
+
outcome = assert_that(response).check().is_equal_to(expected)
|
|
247
|
+
|
|
248
|
+
if not outcome and outcome.diff:
|
|
249
|
+
print(outcome.diff.entries[0].path) # user.role
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
|
|
253
|
+
and a walkable `.diff`, and so does `AssertionFailure`.
|
|
254
|
+
|
|
255
|
+
So a reporter reads structure instead of parsing a string. That is how the
|
|
256
|
+
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works, and it
|
|
257
|
+
is open to anything else you build.
|
|
258
|
+
|
|
192
259
|
<h2 align="center">Features</h2>
|
|
193
260
|
|
|
194
261
|
**Fluent API**
|
|
195
262
|
|
|
196
|
-
- [**Composable matchers**](https://solganis.github.io/assertpy2/guides/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`, usable in plain `assert ==`.
|
|
197
263
|
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
198
264
|
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
|
|
199
265
|
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
|
|
200
266
|
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
201
267
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
202
268
|
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.
|
|
203
|
-
- [**Fluent chaining**](https://solganis.github.io/assertpy2/guides/fluent/#chaining): chain assertions into readable one-liners.
|
|
204
269
|
|
|
205
270
|
**Type safety**
|
|
206
271
|
|
|
207
|
-
- [**
|
|
208
|
-
- [**Typed narrowing**](https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value): `.value` hands the checked value back, statically narrowed by `is_not_none()`, `is_instance_of()`, and a [`satisfies()` `TypeIs` predicate](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced).
|
|
272
|
+
- [**Refinement predicates**](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced): `satisfies()` takes a `TypeIs` predicate, so a domain check narrows the chain too.
|
|
209
273
|
- [**Contract testing**](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms): `assert_conforms()` validates a raw payload against a Pydantic model and narrows to it. [`exact=True`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-drift-with-exacttrue) catches contract drift, `each=True` validates list endpoints.
|
|
210
274
|
|
|
211
275
|
**Built-in types**
|
|
@@ -221,18 +285,19 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
221
285
|
|
|
222
286
|
- [**Soft assertions**](https://solganis.github.io/assertpy2/guides/testing/#soft-assertions): thread-safe and async-safe via `contextvars`, each failure reported with its `file:line`. Group with `sa.group()` or `assert_all()`.
|
|
223
287
|
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
|
|
224
|
-
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`),
|
|
225
|
-
- [**
|
|
226
|
-
- [**
|
|
227
|
-
- [**
|
|
228
|
-
- [**
|
|
229
|
-
- [**
|
|
288
|
+
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
|
|
289
|
+
- [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
|
|
290
|
+
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
|
|
291
|
+
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
|
|
292
|
+
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
|
|
293
|
+
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
|
|
294
|
+
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.
|
|
230
295
|
|
|
231
296
|
**Extensibility**
|
|
232
297
|
|
|
233
|
-
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()`
|
|
298
|
+
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
|
|
299
|
+
- [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
|
|
234
300
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
235
|
-
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
236
301
|
|
|
237
302
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
|
|
238
303
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<img src="docs/logo.svg" alt="assertpy2" width="280">
|
|
6
6
|
</picture>
|
|
7
7
|
<br>
|
|
8
|
-
<b>
|
|
8
|
+
<b>A fully typed fluent assertion library for Python</b><br>
|
|
9
9
|
A modern, batteries-included fork of <a href="https://github.com/assertpy/assertpy">assertpy</a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
@@ -43,23 +43,44 @@ def test_user():
|
|
|
43
43
|
|
|
44
44
|
The [full documentation](https://solganis.github.io/assertpy2/) covers every assertion, matcher, and integration.
|
|
45
45
|
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
<table>
|
|
47
|
+
<tr>
|
|
48
|
+
<td valign="top" width="50%">
|
|
49
|
+
<a href="#why-fluent-assertions"><b>Failures that point at the difference</b></a><br>
|
|
50
|
+
A recursive diff names the exact path that differs, in color, instead of dumping both structures.
|
|
51
|
+
</td>
|
|
52
|
+
<td valign="top" width="50%">
|
|
53
|
+
<a href="#type-aware-autocomplete"><b>Type-aware autocomplete</b></a><br>
|
|
54
|
+
<code>assert_that()</code> returns a protocol per value type, so your IDE offers the methods that fit.
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<td valign="top">
|
|
59
|
+
<a href="#typed-narrowing"><b>Typed narrowing</b></a><br>
|
|
60
|
+
An assertion hands the value back statically narrowed, with no <code>cast</code> and no bare <code>assert</code>.
|
|
61
|
+
</td>
|
|
62
|
+
<td valign="top">
|
|
63
|
+
<a href="#features"><b>Composable matchers</b></a><br>
|
|
64
|
+
45 matchers that combine with <code>&</code>, <code>|</code>, <code>~</code> and nest inside the expected structure itself.
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td valign="top">
|
|
69
|
+
<a href="#features"><b>Built for test suites</b></a><br>
|
|
70
|
+
Soft assertions, polling for eventual consistency, snapshots, and expected-exception chains.
|
|
71
|
+
</td>
|
|
72
|
+
<td valign="top">
|
|
73
|
+
<a href="#integrations"><b>Integrations</b></a><br>
|
|
74
|
+
Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI response contracts.
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
</table>
|
|
50
78
|
|
|
51
|
-
|
|
52
|
-
```python
|
|
53
|
-
# bare - three statements, no autocomplete help
|
|
54
|
-
assert isinstance(items, list)
|
|
55
|
-
assert len(items) == 3
|
|
56
|
-
assert "admin" in items
|
|
79
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
60
|
-
```
|
|
81
|
+
`assert` states a condition well, and pytest reports it well.
|
|
61
82
|
|
|
62
|
-
|
|
83
|
+
What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
|
|
63
84
|
|
|
64
85
|
```text
|
|
65
86
|
assert response == expected
|
|
@@ -70,7 +91,7 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
|
|
|
70
91
|
E {'status': 'active'} != {'status': 'disabled'}
|
|
71
92
|
```
|
|
72
93
|
|
|
73
|
-
assertpy2
|
|
94
|
+
assertpy2 names the [exact path](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs), in color:
|
|
74
95
|
|
|
75
96
|
<!-- docs-guard: skip -->
|
|
76
97
|
```python
|
|
@@ -81,24 +102,45 @@ assert_that(response).is_equal_to(expected)
|
|
|
81
102
|
<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">
|
|
82
103
|
</p>
|
|
83
104
|
|
|
84
|
-
|
|
105
|
+
It recurses through nested containers, and matcher predicates get the same treatment.
|
|
85
106
|
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
For dynamic fields like IDs, assert a subset with
|
|
108
|
+
[`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
|
|
109
|
+
|
|
110
|
+
The chain is the other half: one statement carries the whole intent, and your IDE offers only the
|
|
111
|
+
[methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
|
|
112
|
+
|
|
113
|
+
<!-- docs-guard: skip -->
|
|
114
|
+
```python
|
|
115
|
+
assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
|
|
119
|
+
|
|
120
|
+
Nothing is patched, so a matcher can sit inside the expected structure itself, at any depth:
|
|
88
121
|
|
|
89
122
|
```python
|
|
90
123
|
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
|
|
91
124
|
|
|
92
125
|
assert_that(response).is_equal_to(
|
|
93
|
-
{
|
|
126
|
+
{
|
|
127
|
+
"id": match.greater_than(0),
|
|
128
|
+
"user": {"name": "Alice", "age": match.between(18, 120)},
|
|
129
|
+
"tags": ["a", "b"],
|
|
130
|
+
}
|
|
94
131
|
)
|
|
95
132
|
|
|
96
133
|
# or keep the bare `assert`, and pytest's own rewriting reports it
|
|
97
|
-
assert response == {
|
|
134
|
+
assert response == {
|
|
135
|
+
"id": match.greater_than(0),
|
|
136
|
+
"user": match.ignore(),
|
|
137
|
+
"tags": ["a", "b"],
|
|
138
|
+
}
|
|
98
139
|
```
|
|
99
140
|
|
|
100
|
-
The fluent form keeps the path-level diff, the bare form keeps pytest's.
|
|
101
|
-
|
|
141
|
+
The fluent form keeps the path-level diff, the bare form keeps pytest's.
|
|
142
|
+
|
|
143
|
+
There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/), combining with `&`, `|` and `~`.
|
|
102
144
|
|
|
103
145
|
<p align="center">
|
|
104
146
|
<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">
|
|
@@ -115,13 +157,14 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
115
157
|
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
116
158
|
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
117
159
|
|
|
118
|
-
|
|
160
|
+
11 type-specific Protocols instead of one `Any`.<br>
|
|
119
161
|
Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
120
162
|
|
|
121
163
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
122
164
|
|
|
123
|
-
An assertion hands the value back, statically narrowed.
|
|
124
|
-
|
|
165
|
+
An assertion hands the value back, statically narrowed.
|
|
166
|
+
|
|
167
|
+
`is_not_none()` strips `None`, `is_instance_of()` narrows to the class, and `.value` returns it:
|
|
125
168
|
|
|
126
169
|
<!-- docs-guard: skip -->
|
|
127
170
|
```python
|
|
@@ -129,31 +172,52 @@ order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
|
129
172
|
order.refund() # statically PaidOrder - verified by ty, mypy, and pyright
|
|
130
173
|
```
|
|
131
174
|
|
|
132
|
-
For API tests,
|
|
133
|
-
|
|
175
|
+
For API tests,
|
|
176
|
+
[`assert_conforms()`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms)
|
|
177
|
+
validates a payload against a Pydantic model and narrows to it. `exact=True` catches contract drift:
|
|
134
178
|
|
|
135
179
|
<!-- docs-guard: skip -->
|
|
136
180
|
```python
|
|
137
181
|
data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
138
182
|
```
|
|
139
183
|
|
|
184
|
+
<h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
|
|
185
|
+
|
|
186
|
+
An exception is the right default, and a dead end for anything that wants to read the result.
|
|
187
|
+
|
|
188
|
+
`check()` runs the next assertion for its verdict instead:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
response = {"user": {"name": "Alice", "role": "superadmin"}, "status": "active"}
|
|
192
|
+
expected = {"user": {"name": "Alice", "role": "admin"}, "status": "active"}
|
|
193
|
+
|
|
194
|
+
outcome = assert_that(response).check().is_equal_to(expected)
|
|
195
|
+
|
|
196
|
+
if not outcome and outcome.diff:
|
|
197
|
+
print(outcome.diff.entries[0].path) # user.role
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
|
|
201
|
+
and a walkable `.diff`, and so does `AssertionFailure`.
|
|
202
|
+
|
|
203
|
+
So a reporter reads structure instead of parsing a string. That is how the
|
|
204
|
+
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works, and it
|
|
205
|
+
is open to anything else you build.
|
|
206
|
+
|
|
140
207
|
<h2 align="center">Features</h2>
|
|
141
208
|
|
|
142
209
|
**Fluent API**
|
|
143
210
|
|
|
144
|
-
- [**Composable matchers**](https://solganis.github.io/assertpy2/guides/matchers/): `match.greater_than(5)`, `match.is_uuid()`, combine with `&`, `|`, `~`, usable in plain `assert ==`.
|
|
145
211
|
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
146
212
|
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
|
|
147
213
|
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
|
|
148
214
|
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
149
215
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
150
216
|
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.
|
|
151
|
-
- [**Fluent chaining**](https://solganis.github.io/assertpy2/guides/fluent/#chaining): chain assertions into readable one-liners.
|
|
152
217
|
|
|
153
218
|
**Type safety**
|
|
154
219
|
|
|
155
|
-
- [**
|
|
156
|
-
- [**Typed narrowing**](https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value): `.value` hands the checked value back, statically narrowed by `is_not_none()`, `is_instance_of()`, and a [`satisfies()` `TypeIs` predicate](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced).
|
|
220
|
+
- [**Refinement predicates**](https://solganis.github.io/assertpy2/concepts/type-safety/#refinement-narrowing-with-a-typeis-predicate-advanced): `satisfies()` takes a `TypeIs` predicate, so a domain check narrows the chain too.
|
|
157
221
|
- [**Contract testing**](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-narrowing-with-assert_conforms): `assert_conforms()` validates a raw payload against a Pydantic model and narrows to it. [`exact=True`](https://solganis.github.io/assertpy2/concepts/type-safety/#contract-drift-with-exacttrue) catches contract drift, `each=True` validates list endpoints.
|
|
158
222
|
|
|
159
223
|
**Built-in types**
|
|
@@ -169,18 +233,19 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
169
233
|
|
|
170
234
|
- [**Soft assertions**](https://solganis.github.io/assertpy2/guides/testing/#soft-assertions): thread-safe and async-safe via `contextvars`, each failure reported with its `file:line`. Group with `sa.group()` or `assert_all()`.
|
|
171
235
|
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
|
|
172
|
-
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`),
|
|
173
|
-
- [**
|
|
174
|
-
- [**
|
|
175
|
-
- [**
|
|
176
|
-
- [**
|
|
177
|
-
- [**
|
|
236
|
+
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
|
|
237
|
+
- [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
|
|
238
|
+
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
|
|
239
|
+
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
|
|
240
|
+
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
|
|
241
|
+
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
|
|
242
|
+
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.
|
|
178
243
|
|
|
179
244
|
**Extensibility**
|
|
180
245
|
|
|
181
|
-
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()`
|
|
246
|
+
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
|
|
247
|
+
- [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
|
|
182
248
|
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
183
|
-
- [**Extensions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` for custom assertion methods.
|
|
184
249
|
|
|
185
250
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
|
|
186
251
|
|
|
@@ -17,6 +17,7 @@ from .assertpy import (
|
|
|
17
17
|
from .async_assertions import AsyncAssertionBuilder, SyncAssertionBuilder
|
|
18
18
|
from .errors import (
|
|
19
19
|
AssertionFailure,
|
|
20
|
+
DanglingAssertionWarning,
|
|
20
21
|
DiffEntry,
|
|
21
22
|
DiffResult,
|
|
22
23
|
PollSample,
|
|
@@ -48,6 +49,7 @@ __all__ = [
|
|
|
48
49
|
"AsyncAssertionBuilder",
|
|
49
50
|
"BaseMatcher",
|
|
50
51
|
"CheckBuilder",
|
|
52
|
+
"DanglingAssertionWarning",
|
|
51
53
|
"DiffEntry",
|
|
52
54
|
"DiffResult",
|
|
53
55
|
"MatchResult",
|