assertpy2 2.21.0__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.21.0 → assertpy2-2.22.0}/PKG-INFO +1 -1
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_typing.py +5 -3
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/assertpy.py +60 -36
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/getting-started/comparison.md +2 -2
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/guides/assertions.md +9 -10
- {assertpy2-2.21.0 → assertpy2-2.22.0}/pyproject.toml +2 -2
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_not.py +187 -2
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/typing_cases.py +10 -3
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/typing_negative_baseline.py +22 -6
- assertpy2-2.21.0/CONTRIBUTING.md +0 -61
- {assertpy2-2.21.0 → assertpy2-2.22.0}/.gitignore +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/LICENSE +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/README.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/SECURITY.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_clustering.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_dangling.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_compare.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_contract.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_diff.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_equality.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_introspection.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_membership.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_mixin_base.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_ordering.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_require.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_size.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_hints.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_inline.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_matcher_impls.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_satisfies.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/_snapshot_codec.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/base.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/contains.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/dataframe.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/date.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/errors.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/file.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/helpers.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/http_mixin.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/json_mixin.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/matchers.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/pytest_plugin.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/snapshot.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/string.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/benchmarks/test_perf.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/concepts/stability.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/concepts/type-safety.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/extending/custom-assertions.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/extending/integrations.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/guides/data.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/guides/errors.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/guides/fluent.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/guides/matchers.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/guides/testing.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/index.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/logo.svg +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/recipes.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/async.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/core.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/files.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/http.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/json.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/mkdocs.yml +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/api_snapshot.json +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/api_surface.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/conftest.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/docs_fixtures.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/group_compat.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/pyright_baseline.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_api_compatibility.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_api_vocabulary.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_async.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_attrs_support.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_bool.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_callable.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_check.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_class.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_clustering.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_collection.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_core.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_dangling.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_dataframe.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_description.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_dict.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_docs_examples.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_docs_typing.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_equals.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_errors.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_evaluation_core.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_exception_context.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_fail.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_file.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_hints.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_http.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_in.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_inline_record.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_inline_snapshot.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_json.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_list.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_match_result.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_matcher_parity.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_message_elision.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_none.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_openapi_contract.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_optional_integration_contracts.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_property_attrs.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_protocol_parity.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_public_surface.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_pyright_baseline.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_pytest_plugin.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_recursive_compare_config.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_refusals.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_snapshots.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_soft.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_string.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_structural.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_surface_conformance.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_sync_eventually.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_type.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_typing.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_typing_claims.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_typing_negative.py +0 -0
- {assertpy2-2.21.0 → assertpy2-2.22.0}/tests/test_vacuity_contract.py +0 -0
- {assertpy2-2.21.0 → 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
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
|
|
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from typing_extensions import TypeIs
|
|
12
12
|
|
|
13
13
|
from .._engine._introspection import MappingLike
|
|
14
|
-
from ..assertpy import AssertionBuilder, CheckBuilder
|
|
14
|
+
from ..assertpy import AssertionBuilder, CheckBuilder
|
|
15
15
|
from ..async_assertions import AsyncAssertionBuilder, SyncAssertionBuilder
|
|
16
16
|
from ..matchers import Matcher
|
|
17
17
|
from ._compat import Self
|
|
@@ -251,9 +251,11 @@ if TYPE_CHECKING:
|
|
|
251
251
|
comparators: dict[object, Callable[[Any, Any], object]] | None = ...,
|
|
252
252
|
placeholders: dict[Hashable, Matcher[Any] | Callable[[Any], object]] | None = ...,
|
|
253
253
|
) -> Self: ...
|
|
254
|
-
# NegatedBuilder
|
|
254
|
+
# NegatedBuilder, which a checker sees as this very protocol: the proxy accepts what the
|
|
255
|
+
# value accepts and hands the chain back, so describing it any other way is what let
|
|
256
|
+
# `assert_that(1).not_.starts_with("x")` type-check
|
|
255
257
|
@property
|
|
256
|
-
def not_(self) ->
|
|
258
|
+
def not_(self) -> Self: ...
|
|
257
259
|
# CheckBuilder - run the next assertion for its verdict instead of for its failure
|
|
258
260
|
def check(self) -> CheckBuilder: ...
|
|
259
261
|
# AssertionBuilder - typed extract-and-continue
|
|
@@ -62,7 +62,7 @@ from .snapshot import SnapshotMixin
|
|
|
62
62
|
from .string import StringMixin
|
|
63
63
|
from .warning import WarningMixin
|
|
64
64
|
|
|
65
|
-
__version__ = "2.
|
|
65
|
+
__version__ = "2.22.0"
|
|
66
66
|
|
|
67
67
|
# the tracked value type of the generic AssertionBuilder fallback (_U appears only in narrowing stubs)
|
|
68
68
|
_T = TypeVar("_T")
|
|
@@ -956,37 +956,65 @@ class NegatedBuilder(Generic[_S]):
|
|
|
956
956
|
# unbound TypeVar has no attributes, so the code above could not read `kind` off it.
|
|
957
957
|
return _negated # ty: ignore[invalid-return-type] # see above
|
|
958
958
|
|
|
959
|
-
def _make_msg(self, name: str) -> str:
|
|
959
|
+
def _make_msg(self, name: str, *args: object, **kwargs: object) -> str:
|
|
960
|
+
"""The negated failure, naming the call that held when it should not have.
|
|
961
|
+
|
|
962
|
+
The arguments are in it because without them the message says which relation was asked for and
|
|
963
|
+
not what it was asked about, which is the one thing the reader needs.
|
|
964
|
+
|
|
965
|
+
Rendered here rather than by `HelpersMixin._fmt_args_kwargs()`, which spells a keyword as
|
|
966
|
+
``'key': value``: that is the shape `when_called_with()` has printed since assertpy and its
|
|
967
|
+
messages are pinned to it, but this line reads as a call and ``key=value`` is what a call looks
|
|
968
|
+
like. Keyword order is the caller's, which Python preserves, so it is stable without sorting.
|
|
969
|
+
"""
|
|
960
970
|
desc = f"[{self._builder.description}] " if self._builder.description else ""
|
|
961
|
-
|
|
971
|
+
rendered = ", ".join(
|
|
972
|
+
[_safe_repr(arg) for arg in args] + [f"{key}={_safe_repr(value)}" for key, value in kwargs.items()]
|
|
973
|
+
)
|
|
974
|
+
return f"{desc}Expected <{self._builder.val}> to NOT satisfy: {name}({rendered})"
|
|
975
|
+
|
|
976
|
+
def _verdict(self, attr: Callable[..., object], *args: object, **kwargs: object) -> AssertionOutcome | None:
|
|
977
|
+
"""What the underlying assertion decided, or ``None`` when it held.
|
|
978
|
+
|
|
979
|
+
Run in check mode, the one mode that hands a verdict back instead of delivering it. That is
|
|
980
|
+
what separates a verdict from an accident: anything still raised by the call came out of the
|
|
981
|
+
value or out of an assertion nested inside it, and inverting *that* turns a break into a pass.
|
|
982
|
+
|
|
983
|
+
Catching an exception class cannot make the distinction, whatever class is chosen. A comparator
|
|
984
|
+
that asserts with this library raises this library's own failure, and it is still not the
|
|
985
|
+
verdict of the assertion being negated. `_negated_check()` has always read the sink instead;
|
|
986
|
+
the other three modes now do the same.
|
|
987
|
+
"""
|
|
988
|
+
builder = self._builder
|
|
989
|
+
kind, sink = builder.kind, builder._check_sink
|
|
990
|
+
builder.kind = "check"
|
|
991
|
+
builder._check_sink = None
|
|
992
|
+
try:
|
|
993
|
+
attr(*args, **kwargs)
|
|
994
|
+
return builder._check_sink
|
|
995
|
+
finally:
|
|
996
|
+
builder.kind, builder._check_sink = kind, sink
|
|
962
997
|
|
|
963
998
|
def _negated_strict(
|
|
964
999
|
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
965
1000
|
) -> AssertionBuilder:
|
|
966
|
-
|
|
967
|
-
attr(*args, **kwargs)
|
|
968
|
-
except (AssertionError, AssertionFailure):
|
|
1001
|
+
if self._verdict(attr, *args, **kwargs) is not None:
|
|
969
1002
|
return self._builder
|
|
970
1003
|
# the message is composed here rather than by `error()`, which would prefix the description a
|
|
971
1004
|
# second time, but the exception is the builder's own: a negated failure is a failure
|
|
972
|
-
raise AssertionBuilder._failure(
|
|
1005
|
+
raise AssertionBuilder._failure(
|
|
1006
|
+
AssertionOutcome(message=self._make_msg(name, *args, **kwargs), actual=self._builder.val)
|
|
1007
|
+
)
|
|
973
1008
|
|
|
974
1009
|
def _negated_soft(
|
|
975
1010
|
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
976
1011
|
) -> AssertionBuilder:
|
|
1012
|
+
if self._verdict(attr, *args, **kwargs) is not None:
|
|
1013
|
+
return self._builder # the assertion failed, so the negation held
|
|
977
1014
|
block = _collecting()
|
|
978
1015
|
err_list = block.failures if block is not None else []
|
|
979
|
-
before = len(err_list)
|
|
980
|
-
taint_before = self._builder._value_taint_reason
|
|
981
|
-
attr(*args, **kwargs)
|
|
982
|
-
if len(err_list) > before:
|
|
983
|
-
# underlying assertion failed, so the negation passed: roll back the collected soft
|
|
984
|
-
# error and the taint that failure set, keeping any pre-existing taint
|
|
985
|
-
del err_list[before:]
|
|
986
|
-
self._builder._value_taint_reason = taint_before
|
|
987
|
-
return self._builder
|
|
988
1016
|
# underlying assertion passed, so the negation failed: collect it and taint .value
|
|
989
|
-
msg = self._make_msg(name)
|
|
1017
|
+
msg = self._make_msg(name, *args, **kwargs)
|
|
990
1018
|
if self._builder._value_taint_reason is None:
|
|
991
1019
|
self._builder._value_taint_reason = msg
|
|
992
1020
|
err_list.append(
|
|
@@ -1002,28 +1030,21 @@ class NegatedBuilder(Generic[_S]):
|
|
|
1002
1030
|
def _negated_check(
|
|
1003
1031
|
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
1004
1032
|
) -> AssertionBuilder:
|
|
1005
|
-
|
|
1006
|
-
# and reading the sink is how this tells which way it went
|
|
1007
|
-
self._builder._check_sink = None
|
|
1008
|
-
attr(*args, **kwargs)
|
|
1009
|
-
if self._builder._check_sink is not None:
|
|
1033
|
+
if self._verdict(attr, *args, **kwargs) is not None:
|
|
1010
1034
|
self._builder._check_sink = None # it failed, so the negation held
|
|
1011
1035
|
return self._builder
|
|
1012
|
-
self._builder._check_sink = AssertionOutcome(
|
|
1036
|
+
self._builder._check_sink = AssertionOutcome(
|
|
1037
|
+
message=self._make_msg(name, *args, **kwargs), actual=self._builder.val
|
|
1038
|
+
)
|
|
1013
1039
|
return self._builder
|
|
1014
1040
|
|
|
1015
1041
|
def _negated_warn(
|
|
1016
1042
|
self, name: str, attr: Callable[..., object], *args: object, **kwargs: object
|
|
1017
1043
|
) -> AssertionBuilder:
|
|
1018
|
-
self.
|
|
1019
|
-
|
|
1020
|
-
attr(*args, **kwargs)
|
|
1021
|
-
except (AssertionError, AssertionFailure):
|
|
1022
|
-
return self._builder
|
|
1023
|
-
finally:
|
|
1024
|
-
self._builder.kind = "warn"
|
|
1044
|
+
if self._verdict(attr, *args, **kwargs) is not None:
|
|
1045
|
+
return self._builder # the assertion failed, so the negation held
|
|
1025
1046
|
# underlying assertion passed, so the negation failed: taint .value like error() does
|
|
1026
|
-
msg = self._make_msg(name)
|
|
1047
|
+
msg = self._make_msg(name, *args, **kwargs)
|
|
1027
1048
|
if self._builder._value_taint_reason is None:
|
|
1028
1049
|
self._builder._value_taint_reason = msg
|
|
1029
1050
|
self._builder.logger.warning(msg)
|
|
@@ -1273,8 +1294,10 @@ class AssertionBuilder(
|
|
|
1273
1294
|
If an error description is set by [`described_as()`][assertpy2.base.BaseMixin.described_as], then that
|
|
1274
1295
|
description is prepended to the error message.
|
|
1275
1296
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1297
|
+
Always raises [`AssertionFailure`][assertpy2.errors.AssertionFailure], which is an
|
|
1298
|
+
``AssertionError``. Structured data (``actual``, ``expected``, ``diff``) is carried on it when
|
|
1299
|
+
given, and the class is the same either way, which is why nothing reads the class to learn who
|
|
1300
|
+
raised it: `NegatedBuilder._verdict()` asks in check mode instead.
|
|
1278
1301
|
|
|
1279
1302
|
Args:
|
|
1280
1303
|
msg: the error message
|
|
@@ -1288,11 +1311,12 @@ class AssertionBuilder(
|
|
|
1288
1311
|
it alone when the caught exception is the caller's, which is context they want.
|
|
1289
1312
|
|
|
1290
1313
|
Raises:
|
|
1291
|
-
|
|
1314
|
+
AssertionFailure: unless ``kind`` is ``warn``, ``soft`` or ``check``, which log it, collect
|
|
1315
|
+
it and record it respectively.
|
|
1292
1316
|
|
|
1293
1317
|
Returns:
|
|
1294
|
-
AssertionBuilder:
|
|
1295
|
-
|
|
1318
|
+
AssertionBuilder: this instance, to chain the next assertion, whenever the failure was
|
|
1319
|
+
delivered some other way than by raising.
|
|
1296
1320
|
"""
|
|
1297
1321
|
failure = self._deliver(self._compose(msg, actual=actual, expected=expected, diff=diff, trace=trace))
|
|
1298
1322
|
if failure is None:
|
|
@@ -154,8 +154,8 @@ whole differing container. assertpy2 keeps a path-level diff on either.
|
|
|
154
154
|
| | pytest assert | PyHamcrest | assertpy | dirty-equals | **assertpy2** |
|
|
155
155
|
|---|:---:|:---:|:---:|:---:|:---:|
|
|
156
156
|
| Paradigm | rewritten `assert` | matchers | fluent chain | `==` objects | **[fluent + matchers + `==`](#all-three-styles-one-import)** |
|
|
157
|
-
|
|
|
158
|
-
| Static typing
|
|
157
|
+
| All three styles from one import | No | No | No | No | **[Yes](#all-three-styles-one-import)** |
|
|
158
|
+
| Static typing | n/a | **generic matchers** | No | **Typed** | **[Typed + per-type overloads](../concepts/type-safety.md)** |
|
|
159
159
|
| Autocomplete filtered by value type | No | No | No | No | **[Yes](../concepts/type-safety.md#type-aware-autocomplete)** |
|
|
160
160
|
| Typed narrowing (the assertion returns the value, narrowed) | No | No | No | No | **[Yes](../concepts/type-safety.md#typed-narrowing-with-value)** |
|
|
161
161
|
| Contract testing (validate a payload and narrow to the model) | No | No | No | No | **[Yes](../concepts/type-safety.md#contract-narrowing-with-assert_conforms)** |
|
|
@@ -486,16 +486,15 @@ both sides is matched by identity and not walked again, exactly as Python does i
|
|
|
486
486
|
a config object placed in two expected blocks stays cheap and a container holding the same `NaN` on
|
|
487
487
|
both sides keeps comparing equal.
|
|
488
488
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
0.3 ms against 29 ms, which matters only if you are comparing large dumps in a loop.
|
|
489
|
+
Anything matched **by hash** is covered too, which means dictionary keys and set members. `1`, `1.0`
|
|
490
|
+
and `True` hash alike and compare equal, so a mapping finds the pair before anything looks at its
|
|
491
|
+
type, and reaching it takes a second pass keyed on the type. `{True: "a"}` against `{1: "a"}` and
|
|
492
|
+
`{1}` against `{1.0}` both fail a strict comparison.
|
|
493
|
+
|
|
494
|
+
The one limit is cost. Strictness turns off the fast path: a container's own `==` says nothing about
|
|
495
|
+
the types inside it, so every comparison walks the whole structure in Python. On a list of 20 000
|
|
496
|
+
dicts that is about 0.3 ms against 41 ms, which matters only if you are comparing large dumps in a
|
|
497
|
+
loop.
|
|
499
498
|
|
|
500
499
|
Inside a [structural spec](matchers.md#structural-matching) the same relation is spelled
|
|
501
500
|
`match.equal_to(value, strict_types=True)`, one matcher covering value and type together:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "assertpy2"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.22.0"
|
|
4
4
|
description = "Fluent assertion library for Python with composable matchers, structural matching, and full type safety"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "BSD-3-Clause"
|
|
@@ -85,7 +85,7 @@ dev = [
|
|
|
85
85
|
# real worker processes instead of a fake node
|
|
86
86
|
"pytest-xdist>=3.8.0",
|
|
87
87
|
"ruff>=0.16.1",
|
|
88
|
-
"ty>=0.0.
|
|
88
|
+
"ty>=0.0.73",
|
|
89
89
|
"typing_extensions>=4.0",
|
|
90
90
|
]
|
|
91
91
|
docs = [
|
|
@@ -3,7 +3,16 @@ from io import StringIO
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from assertpy2 import
|
|
6
|
+
from assertpy2 import (
|
|
7
|
+
AssertionFailure,
|
|
8
|
+
WarningLoggingAdapter,
|
|
9
|
+
add_extension,
|
|
10
|
+
assert_that,
|
|
11
|
+
assert_warn,
|
|
12
|
+
match,
|
|
13
|
+
remove_extension,
|
|
14
|
+
soft_assertions,
|
|
15
|
+
)
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
class TestNotBasic:
|
|
@@ -170,6 +179,182 @@ class TestNotWarnMode:
|
|
|
170
179
|
assert_that(out).is_empty()
|
|
171
180
|
|
|
172
181
|
|
|
182
|
+
class TestNotNamesWhatItWasAskedAbout:
|
|
183
|
+
"""A negated failure carries the arguments, or it names the relation and not the subject.
|
|
184
|
+
|
|
185
|
+
``starts_with()`` says which relation held; ``starts_with('a')`` says what held. The direct
|
|
186
|
+
spellings have always named the operand, and the proxy is the one place that did not.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def test_a_positional_argument(self):
|
|
190
|
+
with pytest.raises(AssertionError) as caught:
|
|
191
|
+
assert_that("abc").not_.starts_with("a")
|
|
192
|
+
assert_that(str(caught.value)).is_equal_to("Expected <abc> to NOT satisfy: starts_with('a')")
|
|
193
|
+
|
|
194
|
+
def test_several_arguments(self):
|
|
195
|
+
with pytest.raises(AssertionError) as caught:
|
|
196
|
+
assert_that(3).not_.is_between(1, 5)
|
|
197
|
+
assert_that(str(caught.value)).is_equal_to("Expected <3> to NOT satisfy: is_between(1, 5)")
|
|
198
|
+
|
|
199
|
+
def test_a_keyword_argument(self):
|
|
200
|
+
with pytest.raises(AssertionError) as caught:
|
|
201
|
+
assert_that({"n": 1}).not_.is_equal_to({"n": 1}, strict_types=True)
|
|
202
|
+
assert_that(str(caught.value)).is_equal_to(
|
|
203
|
+
"Expected <{'n': 1}> to NOT satisfy: is_equal_to({'n': 1}, strict_types=True)"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def test_an_assertion_that_takes_none_reads_as_before(self):
|
|
207
|
+
with pytest.raises(AssertionError) as caught:
|
|
208
|
+
assert_that(5).not_.is_positive()
|
|
209
|
+
assert_that(str(caught.value)).is_equal_to("Expected <5> to NOT satisfy: is_positive()")
|
|
210
|
+
|
|
211
|
+
def test_the_soft_block_collects_the_same_sentence(self):
|
|
212
|
+
with pytest.raises(AssertionFailure) as caught, soft_assertions():
|
|
213
|
+
assert_that("abc").not_.starts_with("a")
|
|
214
|
+
assert_that(str(caught.value)).contains("starts_with('a')")
|
|
215
|
+
|
|
216
|
+
def test_check_mode_records_the_same_sentence(self):
|
|
217
|
+
outcome = assert_that("abc").check().not_.starts_with("a")
|
|
218
|
+
assert_that(outcome.message).is_equal_to("Expected <abc> to NOT satisfy: starts_with('a')")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestNotDoesNotInvertAnErrorFromTheValue:
|
|
222
|
+
"""Negation inverts this library's verdict, and an ``AssertionError`` from your code is not one.
|
|
223
|
+
|
|
224
|
+
The proxy used to catch every ``AssertionError``, so a comparison that broke while being negated
|
|
225
|
+
read as "the assertion failed, so the negation held" and the test went green over the break. The
|
|
226
|
+
four sources below are the four places user code runs inside an assertion.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _foreign(caught):
|
|
231
|
+
"""The error travelled out as it was raised, rather than being read as a verdict."""
|
|
232
|
+
# spelled without `not_`, which is the thing under test here
|
|
233
|
+
assert_that(isinstance(caught.value, AssertionFailure)).is_false()
|
|
234
|
+
return assert_that(str(caught.value))
|
|
235
|
+
|
|
236
|
+
def test_an_equality_that_raises(self):
|
|
237
|
+
class Broken:
|
|
238
|
+
__hash__ = None
|
|
239
|
+
|
|
240
|
+
def __eq__(self, other):
|
|
241
|
+
raise AssertionError("comparison implementation broke")
|
|
242
|
+
|
|
243
|
+
with pytest.raises(AssertionError) as caught:
|
|
244
|
+
assert_that(Broken()).not_.is_equal_to(1)
|
|
245
|
+
self._foreign(caught).is_equal_to("comparison implementation broke")
|
|
246
|
+
|
|
247
|
+
def test_a_comparator_that_raises(self):
|
|
248
|
+
def broken(left, right):
|
|
249
|
+
raise AssertionError("comparator broke")
|
|
250
|
+
|
|
251
|
+
with pytest.raises(AssertionError) as caught:
|
|
252
|
+
assert_that({"n": 1}).not_.is_equal_to({"n": 2}, comparators={int: broken})
|
|
253
|
+
self._foreign(caught).is_equal_to("comparator broke")
|
|
254
|
+
|
|
255
|
+
def test_a_property_that_raises(self):
|
|
256
|
+
# this one never reached the catch even before it was narrowed: a dynamic `has_*` reads the
|
|
257
|
+
# property while the proxy is resolving the attribute, which is before there is anything to
|
|
258
|
+
# invert. Pinned so a refactor that moves resolution inside the try does not lose it silently
|
|
259
|
+
class Record:
|
|
260
|
+
@property
|
|
261
|
+
def name(self):
|
|
262
|
+
raise AssertionError("property broke")
|
|
263
|
+
|
|
264
|
+
with pytest.raises(AssertionError) as caught:
|
|
265
|
+
assert_that(Record()).not_.has_name("ada")
|
|
266
|
+
self._foreign(caught).is_equal_to("property broke")
|
|
267
|
+
|
|
268
|
+
def test_an_extension_that_raises_on_its_own(self, _broken_extension):
|
|
269
|
+
with pytest.raises(AssertionError) as caught:
|
|
270
|
+
assert_that(1).not_.breaks_outright()
|
|
271
|
+
self._foreign(caught).is_equal_to("extension broke")
|
|
272
|
+
|
|
273
|
+
def test_warn_mode_does_not_invert_it_either(self):
|
|
274
|
+
class Broken:
|
|
275
|
+
__hash__ = None
|
|
276
|
+
|
|
277
|
+
def __eq__(self, other):
|
|
278
|
+
raise AssertionError("comparison implementation broke")
|
|
279
|
+
|
|
280
|
+
with pytest.raises(AssertionError) as caught:
|
|
281
|
+
assert_warn(Broken()).not_.is_equal_to(1)
|
|
282
|
+
self._foreign(caught).is_equal_to("comparison implementation broke")
|
|
283
|
+
|
|
284
|
+
def test_a_nested_assertion_inside_a_comparator(self):
|
|
285
|
+
# the class of the exception cannot answer this one: a comparator that asserts with this
|
|
286
|
+
# library raises this library's own failure, and it is still not the verdict being negated
|
|
287
|
+
def asserts_inside(left, right):
|
|
288
|
+
assert_that(left).is_equal_to(right)
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
with pytest.raises(AssertionFailure) as caught:
|
|
292
|
+
assert_that({"n": 1}).not_.is_equal_to({"n": 2}, comparators={int: asserts_inside})
|
|
293
|
+
assert_that(str(caught.value)).starts_with("Expected <1> to be equal to <2>")
|
|
294
|
+
|
|
295
|
+
def test_a_nested_assertion_inside_a_comparator_in_warn_mode(self):
|
|
296
|
+
def asserts_inside(left, right):
|
|
297
|
+
assert_that(left).is_equal_to(right)
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
with pytest.raises(AssertionFailure) as caught:
|
|
301
|
+
assert_warn({"n": 1}).not_.is_equal_to({"n": 2}, comparators={int: asserts_inside})
|
|
302
|
+
assert_that(str(caught.value)).starts_with("Expected <1> to be equal to <2>")
|
|
303
|
+
|
|
304
|
+
def test_a_nested_assertion_in_a_soft_block_is_collected_rather_than_lost(self):
|
|
305
|
+
# soft collects rather than raises, so the honest outcome here is both failures, not silence
|
|
306
|
+
def asserts_inside(left, right):
|
|
307
|
+
assert_that(left).is_equal_to(right)
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
with pytest.raises(AssertionFailure) as caught, soft_assertions():
|
|
311
|
+
assert_that({"n": 1}).not_.is_equal_to({"n": 2}, comparators={int: asserts_inside})
|
|
312
|
+
collected = [outcome.message.splitlines()[0] for outcome in caught.value.failures]
|
|
313
|
+
assert_that(collected).is_length(2)
|
|
314
|
+
assert_that(collected[0]).is_equal_to("Expected <1> to be equal to <2>, but was not.")
|
|
315
|
+
|
|
316
|
+
def test_the_library_own_failure_still_inverts(self):
|
|
317
|
+
# the other half of the same rule: a real verdict is what negation is for
|
|
318
|
+
assert_that(5).not_.is_equal_to(6)
|
|
319
|
+
assert_warn(5).not_.is_equal_to(6)
|
|
320
|
+
|
|
321
|
+
def test_an_extension_reporting_a_verdict_still_inverts(self, _five_extension):
|
|
322
|
+
# the documented way an extension fails is `self.error(...)`, and that is a verdict
|
|
323
|
+
assert_that(6).not_.is_five()
|
|
324
|
+
with pytest.raises(AssertionFailure, match=r"to NOT satisfy: is_five\(\)"):
|
|
325
|
+
assert_that(5).not_.is_five()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.fixture
|
|
329
|
+
def _five_extension():
|
|
330
|
+
"""The extension from the guide, which reports its verdict with `self.error(...)`."""
|
|
331
|
+
|
|
332
|
+
def is_five(self):
|
|
333
|
+
if self.val != 5:
|
|
334
|
+
return self.error(f"{self.val} is NOT 5!")
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
add_extension(is_five)
|
|
338
|
+
try:
|
|
339
|
+
yield
|
|
340
|
+
finally:
|
|
341
|
+
remove_extension(is_five)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@pytest.fixture
|
|
345
|
+
def _broken_extension():
|
|
346
|
+
"""An extension that raises rather than reporting a verdict, taken back off the global registry."""
|
|
347
|
+
|
|
348
|
+
def breaks_outright(self):
|
|
349
|
+
raise AssertionError("extension broke")
|
|
350
|
+
|
|
351
|
+
add_extension(breaks_outright)
|
|
352
|
+
try:
|
|
353
|
+
yield
|
|
354
|
+
finally:
|
|
355
|
+
remove_extension(breaks_outright)
|
|
356
|
+
|
|
357
|
+
|
|
173
358
|
class TestNotAttributes:
|
|
174
359
|
def test_non_callable_attr_passthrough(self):
|
|
175
360
|
builder = assert_that(5).described_as("test")
|
|
@@ -207,7 +392,7 @@ def test_not_rejects_described_as_with_clear_error():
|
|
|
207
392
|
|
|
208
393
|
|
|
209
394
|
def test_described_as_before_not_keeps_working():
|
|
210
|
-
with pytest.raises(AssertionError, match=r"\[desc\] Expected <1> to NOT satisfy: is_equal_to\(\)"):
|
|
395
|
+
with pytest.raises(AssertionError, match=r"\[desc\] Expected <1> to NOT satisfy: is_equal_to\(1\)"):
|
|
211
396
|
assert_that(1).described_as("desc").not_.is_equal_to(1)
|
|
212
397
|
|
|
213
398
|
|
|
@@ -153,10 +153,17 @@ def _chaining_must_not_widen_what_the_value_offers() -> None:
|
|
|
153
153
|
"""
|
|
154
154
|
assert_that(1 + 2j).is_not_zero().is_positive() # case: complex-widened-by-chaining
|
|
155
155
|
assert_that(True).is_greater_than(0).is_even() # case: bool-widened-by-chaining
|
|
156
|
-
# `.not_`
|
|
157
|
-
#
|
|
158
|
-
#
|
|
156
|
+
# `.not_` used to be the exception: the proxy resolves any name through `__getattr__`, so the
|
|
157
|
+
# negated branch accepted what the protocol does not. It is declared as the protocol it was
|
|
158
|
+
# reached from instead, which costs nothing and refuses the same calls the un-negated chain does
|
|
159
159
|
assert_that(1 + 2j).not_.is_greater_than(0) # case: negation-widens-the-protocol
|
|
160
|
+
# what that declaration does allow and the runtime refuses: the fourteen names that transform or
|
|
161
|
+
# configure rather than assert. The runtime says so by name, which is the tolerable direction
|
|
162
|
+
assert_that(1).not_.described_as("x") # case: negation-allows-a-non-negatable-name
|
|
163
|
+
# an ordering matcher takes and judges anything its `<` accepts, and neither half is typed. A
|
|
164
|
+
# boundary of the wrong type is accepted, and so is a matcher built for another subject entirely
|
|
165
|
+
assert_that(1).satisfies(match.greater_than("x")) # case: ordering-matcher-takes-any-boundary
|
|
166
|
+
assert_that("x").satisfies(match.greater_than(0)) # case: ordering-matcher-judges-any-subject
|
|
160
167
|
|
|
161
168
|
|
|
162
169
|
def _takes_repeats_of_derived(assertion: _RepeatableAssertion[_Derived]) -> None:
|
|
@@ -15,9 +15,13 @@ and the argument is missing. `ty` sometimes answers `no-matching-overload` wher
|
|
|
15
15
|
argument: same family, different route through an overload set.
|
|
16
16
|
|
|
17
17
|
**What stays uncaught, and why.** `assert_that(Person()).is_positive()` type-checks, because a value
|
|
18
|
-
with no protocol of its own gets the generic builder, which carries every method there is.
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
with no protocol of its own gets the generic builder, which carries every method there is. The
|
|
19
|
+
ordering matchers take and judge anything, and the entries below say why every spelling that closes
|
|
20
|
+
that trades a correct call for an incorrect one.
|
|
21
|
+
|
|
22
|
+
`.not_` used to belong here for the same reason as the first: it returns a proxy resolving any name.
|
|
23
|
+
It is declared as the protocol it was reached from instead, which costs nothing, and what remains is
|
|
24
|
+
the other direction: the fourteen names the proxy refuses at runtime are still spelled as available.
|
|
21
25
|
|
|
22
26
|
**The trap this file exists to catch.** A numeric comparison is bound to `SupportsFloat` rather than to
|
|
23
27
|
a list of types: the first attempt named `float | Decimal | Fraction` and rejected `numpy.int64`, a
|
|
@@ -136,10 +140,22 @@ CAUGHT: dict[str, dict[str, frozenset[str]]] = {
|
|
|
136
140
|
# --- a step hands back `Self`, so the narrowing holds for the whole chain ----------------------
|
|
137
141
|
"complex-widened-by-chaining": _MISSING,
|
|
138
142
|
"bool-widened-by-chaining": _MISSING,
|
|
139
|
-
#
|
|
140
|
-
#
|
|
143
|
+
# `.not_` is declared as the protocol it was reached from, so the proxy accepts what the value
|
|
144
|
+
# accepts. What it still allows and the runtime refuses is the handful of non-negatable names
|
|
145
|
+
"negation-widens-the-protocol": _MISSING,
|
|
146
|
+
# --- still open: the generic builder carries every method there is ------------------------------
|
|
141
147
|
"numeric-assertion-on-an-object": {},
|
|
142
|
-
|
|
148
|
+
# and the negation proxy, which is declared as the protocol and so cannot describe the fourteen
|
|
149
|
+
# names it refuses at runtime
|
|
150
|
+
"negation-allows-a-non-negatable-name": {},
|
|
151
|
+
# the ordering matchers, deliberately. `Matcher` is contravariant and `satisfies()` already refuses
|
|
152
|
+
# `match.starts_with("a")` over an `int`, so the shape works; what cannot be spelled is the ordering
|
|
153
|
+
# relation itself. Typing the boundary as a number refuses `match.greater_than("a")`, which orders
|
|
154
|
+
# strings at runtime, and typing it as a plain TypeVar refuses `match.greater_than(0)` over a
|
|
155
|
+
# `float`, since contravariance then asks for `float` to be a subtype of `int`. Both trade a
|
|
156
|
+
# correct call for an incorrect one, so the boundary stays `object` and the gap stays recorded
|
|
157
|
+
"ordering-matcher-takes-any-boundary": {},
|
|
158
|
+
"ordering-matcher-judges-any-subject": {},
|
|
143
159
|
# --- and the third: `SupportsFloat` is wider than the runtime's `numbers.Number` ----------------
|
|
144
160
|
# both are refused at runtime by name, which is the tolerable direction for an approximation to err
|
|
145
161
|
"convertible-but-not-a-number": {},
|