assertpy2 2.22.0__tar.gz → 2.23.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 → assertpy2-2.23.0}/CONTRIBUTING.md +6 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/PKG-INFO +4 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/README.md +3 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_clustering.py +9 -15
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_dangling.py +106 -18
- assertpy2-2.23.0/assertpy2/_engine/_builder_check_typing.py +1322 -0
- assertpy2-2.23.0/assertpy2/_engine/_check_typing.py +662 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_compare.py +11 -13
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_contract.py +2 -4
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_diff.py +13 -16
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_equality.py +8 -13
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_introspection.py +8 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_membership.py +21 -22
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_mixin_base.py +2 -10
- assertpy2-2.23.0/assertpy2/_engine/_operations.py +82 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_ordering.py +5 -8
- assertpy2-2.23.0/assertpy2/_engine/_poll_typing.py +2782 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_require.py +7 -14
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_size.py +2 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_typing.py +448 -111
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_hints.py +19 -36
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_inline.py +1 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_matcher_impls.py +47 -72
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_satisfies.py +80 -53
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_snapshot_codec.py +2 -4
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/assertpy.py +211 -116
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/async_assertions.py +29 -55
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/base.py +30 -25
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/bytes_mixin.py +7 -5
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/collection.py +62 -26
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/contains.py +53 -37
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/dataframe.py +11 -7
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/date.py +21 -14
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/dict.py +4 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/dynamic.py +2 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/errors.py +24 -7
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/exception.py +29 -7
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/extracting.py +7 -13
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/file.py +7 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/helpers.py +15 -22
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/http_mixin.py +6 -11
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/json_mixin.py +9 -17
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/matchers.py +6 -18
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/numeric.py +46 -20
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/pytest_plugin.py +125 -40
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/snapshot.py +10 -23
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/string.py +57 -36
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/warning.py +5 -4
- {assertpy2-2.22.0 → assertpy2-2.23.0}/benchmarks/test_perf.py +17 -20
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/concepts/stability.md +41 -4
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/concepts/type-safety.md +62 -24
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/extending/custom-assertions.md +6 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/assertions.md +14 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/errors.md +15 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/matchers.md +51 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/pyproject.toml +15 -2
- assertpy2-2.23.0/scripts/generate_check_protocols.py +146 -0
- assertpy2-2.23.0/scripts/generate_poll_protocols.py +448 -0
- assertpy2-2.23.0/scripts/mutation_report.py +139 -0
- assertpy2-2.23.0/tests/api_snapshot.json +5967 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/api_surface.py +113 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/conftest.py +14 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/docs_fixtures.py +8 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/pyright_baseline.py +19 -21
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_api_compatibility.py +82 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_api_vocabulary.py +0 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_async.py +3 -15
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_boundary_cases.py +2 -25
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_bytes.py +2 -4
- assertpy2-2.23.0/tests/test_check_protocols.py +205 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_clustering.py +0 -26
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_core.py +13 -30
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_custom_dict.py +7 -14
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_custom_list.py +3 -6
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dangling.py +166 -10
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dataframe.py +27 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_datetime.py +7 -7
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dict_compare.py +0 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_docs_examples.py +1 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_docs_typing.py +33 -9
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dynamic.py +1 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_equals.py +1 -9
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_errors.py +39 -4
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_evaluation_core.py +52 -37
- assertpy2-2.23.0/tests/test_expected_contract.py +183 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_expected_exception.py +45 -21
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_expected_warning.py +2 -12
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_extensions.py +3 -5
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_extracting.py +1 -4
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_file.py +1 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_grouped_soft.py +7 -7
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_hints.py +13 -62
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_http.py +0 -12
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_inline_record.py +2 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_iterable_cluster.py +15 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_list.py +2 -10
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_match_result.py +69 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_matcher_parity.py +2 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_matchers.py +0 -8
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_message_elision.py +2 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_not.py +10 -13
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_numbers.py +0 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_openapi_contract.py +0 -5
- assertpy2-2.23.0/tests/test_operation_contract.py +410 -0
- assertpy2-2.23.0/tests/test_overload_order.py +170 -0
- assertpy2-2.23.0/tests/test_poll_protocols.py +235 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_property_attrs.py +1 -1
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_property_based.py +73 -165
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_protocol_parity.py +104 -10
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_public_surface.py +13 -7
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_pyright_baseline.py +22 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_pytest_plugin.py +200 -115
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare.py +0 -26
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare_config.py +1 -22
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_refusals.py +3 -5
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_rich_diff.py +28 -77
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_satisfy.py +0 -2
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_snapshots.py +16 -45
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_soft.py +2 -5
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_structural.py +26 -32
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_surface_conformance.py +4 -9
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_traceback.py +1 -3
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_typing.py +139 -84
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_typing_claims.py +0 -1
- assertpy2-2.23.0/tests/test_typing_conformance.py +440 -0
- assertpy2-2.23.0/tests/test_typing_from_a_wheel.py +101 -0
- assertpy2-2.23.0/tests/test_typing_http.py +78 -0
- assertpy2-2.23.0/tests/test_typing_integrations.py +111 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_typing_negative.py +57 -71
- assertpy2-2.23.0/tests/test_vacuity_contract.py +466 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_warn.py +24 -33
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/typing_cases.py +61 -10
- assertpy2-2.23.0/tests/typing_harness.py +80 -0
- assertpy2-2.23.0/tests/typing_http.py +75 -0
- assertpy2-2.23.0/tests/typing_integrations.py +64 -0
- assertpy2-2.23.0/tests/typing_integrations_baseline.py +93 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/typing_negative_baseline.py +84 -27
- assertpy2-2.22.0/tests/api_snapshot.json +0 -3724
- assertpy2-2.22.0/tests/test_vacuity_contract.py +0 -180
- {assertpy2-2.22.0 → assertpy2-2.23.0}/.gitignore +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/LICENSE +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/SECURITY.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/extending/integrations.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/getting-started/comparison.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/data.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/fluent.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/guides/testing.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/index.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/logo.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/recipes.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/async.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/core.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/files.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/http.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/json.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/mkdocs.yml +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/group_compat.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_attrs_support.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_bool.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_callable.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_check.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_class.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_collection.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_description.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_dict.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_exception_context.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_fail.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_in.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_inline_snapshot.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_json.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_none.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_optional_integration_contracts.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_string.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_sync_eventually.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.23.0}/tests/test_type.py +0 -0
|
@@ -49,10 +49,15 @@ run with them installed collects a different set:
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
|
|
52
|
-
uv run --group typecheck pyright tests/test_typing.py
|
|
52
|
+
uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
|
|
53
53
|
uv run --group typecheck pytest tests/test_pyright_baseline.py
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
`--pythonversion 3.14` is not decoration. Pyright reports against the interpreter it finds unless told
|
|
57
|
+
otherwise, and the count moves with it: 108 diagnostics for this package on 3.10 against 102 on 3.14.
|
|
58
|
+
Without it a contributor on the supported floor meets a red baseline that says nothing about their
|
|
59
|
+
change. The baseline test passes the same target itself, so it gives one answer everywhere.
|
|
60
|
+
|
|
56
61
|
Three details that cost time if you meet them the hard way:
|
|
57
62
|
|
|
58
63
|
- `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.23.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>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
|
|
|
@@ -209,7 +209,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
209
209
|
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
210
210
|
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
15 type-specific Protocols instead of one `Any`.<br>
|
|
213
213
|
Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
214
214
|
|
|
215
215
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
@@ -263,6 +263,7 @@ is open to anything else you build.
|
|
|
263
263
|
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
264
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.
|
|
265
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.
|
|
266
|
+
- [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
|
|
266
267
|
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
267
268
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
268
269
|
- [**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.
|
|
@@ -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>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
|
|
|
@@ -157,7 +157,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
157
157
|
- `assert_that(my_dict).` → dict methods: `contains_key`, `contains_entry`, `has_json_path`, ...
|
|
158
158
|
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
15 type-specific Protocols instead of one `Any`.<br>
|
|
161
161
|
Works in PyCharm, VS Code, and any LSP-compatible editor.
|
|
162
162
|
|
|
163
163
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
@@ -211,6 +211,7 @@ is open to anything else you build.
|
|
|
211
211
|
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
212
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.
|
|
213
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.
|
|
214
|
+
- [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
|
|
214
215
|
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
215
216
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
216
217
|
- [**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.
|
|
@@ -59,9 +59,7 @@ __tracebackhide__ = True
|
|
|
59
59
|
# steps that name a *place* in a value, as opposed to `item`, which names a member of a set
|
|
60
60
|
_COORDINATE: Final = frozenset({"key", "index", "attr", "line"})
|
|
61
61
|
|
|
62
|
-
# a position collapses to one placeholder: `users[0].role` and `users[7].role` are
|
|
63
|
-
# reported against two rows, and a reader chasing a common cause wants them in one line. a line number
|
|
64
|
-
# is incidental in the same way
|
|
62
|
+
# a position collapses to one placeholder: `users[0].role` and `users[7].role` are one difference on two rows
|
|
65
63
|
_POSITIONAL: Final = frozenset({"index", "line"})
|
|
66
64
|
_ANY_POSITION: Final = "[*]"
|
|
67
65
|
|
|
@@ -190,8 +188,8 @@ def stable_repr(value: object, _seen: frozenset[int] = frozenset()) -> str:
|
|
|
190
188
|
try:
|
|
191
189
|
if isinstance(value, (set, frozenset)):
|
|
192
190
|
members = sorted(stable_repr(member, inner) for member in value)
|
|
193
|
-
# an empty one falls back to `repr`, since `{}` is a dict
|
|
194
|
-
#
|
|
191
|
+
# an empty one falls back to `repr`, since `{}` is a dict, so `set()` and the `frozenset()` it equals
|
|
192
|
+
# read apart
|
|
195
193
|
return "{" + ", ".join(members) + "}" if value else _safe_repr(value)
|
|
196
194
|
if isinstance(value, dict):
|
|
197
195
|
pairs = (f"{stable_repr(key, inner)}: {stable_repr(item, inner)}" for key, item in value.items())
|
|
@@ -289,9 +287,8 @@ def is_well_formed(key: Signature) -> bool:
|
|
|
289
287
|
)
|
|
290
288
|
if key.steps:
|
|
291
289
|
return False
|
|
292
|
-
# exactly one of the two
|
|
293
|
-
#
|
|
294
|
-
# a `contains` payload would print the `None` that stands for a missing item as somebody's value
|
|
290
|
+
# exactly one of the two: a pair of values only for the kinds whose fields hold them, or a `contains` payload
|
|
291
|
+
# would print the `None` standing for a missing item
|
|
295
292
|
if key.label:
|
|
296
293
|
return not key.values
|
|
297
294
|
return len(key.values) == 2 and key.where in _VALUES_ARE_VALUES
|
|
@@ -330,17 +327,15 @@ def clusters(
|
|
|
330
327
|
"""
|
|
331
328
|
if total_failures <= 0:
|
|
332
329
|
return []
|
|
333
|
-
# keyed on the node id
|
|
334
|
-
#
|
|
335
|
-
# printed a cluster of six over two tests
|
|
330
|
+
# keyed on the node id: `pytest-rerunfailures` reports one test failing per retry, and counting attempts printed
|
|
331
|
+
# a cluster of six over two tests
|
|
336
332
|
grouped: dict[Signature, dict[str, None]] = {}
|
|
337
333
|
actuals: dict[Signature, dict[str, None]] = {}
|
|
338
334
|
expecteds: dict[Signature, dict[str, None]] = {}
|
|
339
335
|
for nodeid, found in recorded:
|
|
340
336
|
for one in found:
|
|
341
337
|
grouped.setdefault(one.signature, {})[nodeid] = None
|
|
342
|
-
# one past the limit, so a full side is distinguishable from a capped one by its length
|
|
343
|
-
# alone. The smallest value is always kept, whichever order the failures came in
|
|
338
|
+
# one past the limit, so a full side is distinguishable from a capped one by its length alone
|
|
344
339
|
for store, value in ((actuals, one.actual), (expecteds, one.expected)):
|
|
345
340
|
seen = store.setdefault(one.signature, {})
|
|
346
341
|
if len(seen) <= _EXAMPLE_LIMIT:
|
|
@@ -353,8 +348,7 @@ def clusters(
|
|
|
353
348
|
for key, nodeids in grouped.items()
|
|
354
349
|
if len(nodeids) >= minimum
|
|
355
350
|
]
|
|
356
|
-
# size first, then the signature
|
|
357
|
-
# runs, and the signature is the only thing about them that cannot vary
|
|
351
|
+
# size first, then the signature: two clusters of equal size must not swap places between runs
|
|
358
352
|
large.sort(key=lambda cluster: (-cluster.size, cluster.signature))
|
|
359
353
|
return large
|
|
360
354
|
|
|
@@ -18,11 +18,21 @@ runtime and cannot shift a frame.
|
|
|
18
18
|
What it deliberately does not flag:
|
|
19
19
|
|
|
20
20
|
* a builder bound to a name (`b = assert_that(x)`), because whether `b` is used later is not a
|
|
21
|
-
question about this statement;
|
|
22
|
-
* a chain that ends on a pivot (`.described_as(...)`, `.extracting(...)`), because the set of
|
|
23
|
-
assertions is a runtime property of the builder and hard-coding it here would rot;
|
|
21
|
+
question about this statement, and ruff's F841 answers it when the answer is never;
|
|
24
22
|
* `assert_conforms()`, `fail()`, `soft_fail()`, which assert on their own rather than returning a
|
|
25
23
|
builder, so a bare call to them is correct usage.
|
|
24
|
+
|
|
25
|
+
A chain ending on a pivot or a configurer is the same defect wearing a longer tail:
|
|
26
|
+
``assert_that(load).raises(ValueError)`` never calls anything, and ``assert_that(rows).extracting("id")``
|
|
27
|
+
throws the extracted value away. Both were left out while the set of operations that reach no verdict
|
|
28
|
+
was only a runtime property; `assertpy2/_engine/_operations.py` states it now, and a gate keeps it in
|
|
29
|
+
step with the source, so reading it here cannot rot.
|
|
30
|
+
|
|
31
|
+
An ``assert`` in front reads the chain instead of discarding it, and a builder and a bound method are
|
|
32
|
+
both truthy, so ``assert assert_that(items).is_empty`` is green on every value. Neither B018 nor
|
|
33
|
+
coverage sees that one: the value is consumed rather than dropped, and the line does run. What the
|
|
34
|
+
``assert`` reads is the whole question, since ``assert assert_that(x).val`` tests that value instead,
|
|
35
|
+
and the register names the members that hand it back.
|
|
26
36
|
"""
|
|
27
37
|
|
|
28
38
|
from __future__ import annotations
|
|
@@ -32,13 +42,22 @@ import io
|
|
|
32
42
|
import tokenize
|
|
33
43
|
from typing import TYPE_CHECKING, Final, NamedTuple
|
|
34
44
|
|
|
45
|
+
from ._engine._operations import (
|
|
46
|
+
ALSO_ASSERTS,
|
|
47
|
+
CONFIGURES,
|
|
48
|
+
DESCRIBES,
|
|
49
|
+
HANDS_THE_SUBJECT_BACK,
|
|
50
|
+
POLLS,
|
|
51
|
+
TRANSFORMS,
|
|
52
|
+
WITHOUT_A_VERDICT,
|
|
53
|
+
)
|
|
54
|
+
|
|
35
55
|
if TYPE_CHECKING:
|
|
36
56
|
from collections.abc import Iterator
|
|
37
57
|
|
|
38
58
|
__tracebackhide__ = True
|
|
39
59
|
|
|
40
|
-
# the builder factories
|
|
41
|
-
# asserts by itself or is not an entry point at all
|
|
60
|
+
# the builder factories and only those: everything else either asserts by itself or is not an entry point
|
|
42
61
|
_ENTRY: Final = frozenset({"assert_that", "assert_warn"})
|
|
43
62
|
_PACKAGE: Final = "assertpy2"
|
|
44
63
|
|
|
@@ -58,7 +77,27 @@ A project that registered its own wrapper reads `check(1)` on the offending line
|
|
|
58
77
|
`assert_that()` there costs a moment of "that is not what my code says" on every finding.
|
|
59
78
|
"""
|
|
60
79
|
_NOT_CALLED: Final = "assertion is looked up and never called (missing parentheses)"
|
|
80
|
+
_ENDS_ON: Final = {
|
|
81
|
+
CONFIGURES: "{name}() only sets an expectation here, and nothing calls it",
|
|
82
|
+
TRANSFORMS: "{name}() hands back another value here, and nothing asserts on it",
|
|
83
|
+
DESCRIBES: "{name}() only sets the failure description here, and asserts nothing",
|
|
84
|
+
POLLS: "{name}() starts a polling chain here, and no assertion follows it",
|
|
85
|
+
}
|
|
86
|
+
"""What to say about a chain ending on an operation that reaches no verdict, one sentence per kind."""
|
|
87
|
+
|
|
88
|
+
_NO_VERDICT: Final = {name: kind for name, kind in WITHOUT_A_VERDICT.items() if name not in ALSO_ASSERTS}
|
|
61
89
|
_NOT_AWAITED: Final = "eventually() chain is never awaited, so nothing is polled (missing await)"
|
|
90
|
+
_UNDER_ASSERT: Final = "nothing here asserts: assert reads the builder, whose truth says nothing about the value"
|
|
91
|
+
"""What to say about a dangling chain an `assert` reads.
|
|
92
|
+
|
|
93
|
+
The wrapper turns every shape below into the same defect and a worse one: a builder, a proxy and a
|
|
94
|
+
bound method are all truthy, so the line is green whatever the value is, and it reads as if it
|
|
95
|
+
asserted. Neither ruff nor coverage sees it, since the value is consumed and the line does run.
|
|
96
|
+
|
|
97
|
+
The sentence stops at "says nothing" rather than promising the line passes, because a few of the
|
|
98
|
+
builder's own fields are falsy by default and turn the line red instead. Green or red, neither
|
|
99
|
+
outcome was decided by the value under test.
|
|
100
|
+
"""
|
|
62
101
|
|
|
63
102
|
|
|
64
103
|
class Finding(NamedTuple):
|
|
@@ -106,9 +145,8 @@ def _rebound(tree: ast.Module) -> frozenset[str]:
|
|
|
106
145
|
names.add(node.id)
|
|
107
146
|
elif isinstance(node, ast.arg):
|
|
108
147
|
names.add(node.arg)
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
# and only the separate branch narrows it
|
|
148
|
+
# merging the identical branches is what ruff asks for and a type checker then rejects, since only the
|
|
149
|
+
# separate branch narrows `ExceptHandler.name`
|
|
112
150
|
elif isinstance(node, ast.ExceptHandler) and node.name: # noqa: SIM114
|
|
113
151
|
names.add(node.name)
|
|
114
152
|
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
@@ -181,12 +219,12 @@ def _reaches_entry(node: ast.expr, bindings: _Bindings) -> bool:
|
|
|
181
219
|
return False
|
|
182
220
|
|
|
183
221
|
|
|
184
|
-
def
|
|
185
|
-
"""True when an attribute chain calls
|
|
222
|
+
def _runs(node: ast.expr, name: str) -> bool:
|
|
223
|
+
"""True when an attribute chain calls *name* anywhere along it."""
|
|
186
224
|
current: ast.expr = node
|
|
187
225
|
while True:
|
|
188
226
|
if isinstance(current, ast.Call):
|
|
189
|
-
if isinstance(current.func, ast.Attribute) and current.func.attr ==
|
|
227
|
+
if isinstance(current.func, ast.Attribute) and current.func.attr == name:
|
|
190
228
|
return True
|
|
191
229
|
current = current.func
|
|
192
230
|
elif isinstance(current, ast.Attribute):
|
|
@@ -195,6 +233,49 @@ def _polls(node: ast.expr) -> bool:
|
|
|
195
233
|
return False
|
|
196
234
|
|
|
197
235
|
|
|
236
|
+
def _tail(node: ast.expr) -> str | None:
|
|
237
|
+
"""The name a statement ends on when that name reaches no verdict, else ``None``."""
|
|
238
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in _NO_VERDICT:
|
|
239
|
+
return node.func.attr
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _reads_a_verdict_field(node: ast.Attribute) -> bool:
|
|
244
|
+
"""Whether the attribute reads a field off a verdict rather than leaving an assertion uncalled.
|
|
245
|
+
|
|
246
|
+
Both sit behind `check()` and only the step before them tells them apart: `check().is_empty().passed`
|
|
247
|
+
reads a field of what the assertion decided, while `check().is_empty` is that assertion with its
|
|
248
|
+
parentheses missing, which is the shape this whole check exists for.
|
|
249
|
+
"""
|
|
250
|
+
if not _runs(node, "check") or not isinstance(node.value, ast.Call):
|
|
251
|
+
return False
|
|
252
|
+
called = node.value.func
|
|
253
|
+
return not (isinstance(called, ast.Attribute) and called.attr == "check")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _reads_a_truthy_chain(node: ast.expr, bindings: _Bindings) -> bool:
|
|
257
|
+
"""Whether `assert <node>` reads a chain that decided nothing.
|
|
258
|
+
|
|
259
|
+
An attribute handing the subject back is excluded, since `assert assert_that(x).val` tests that
|
|
260
|
+
value rather than leaving an assertion uncalled, and so is a verdict's own field. Nothing else is:
|
|
261
|
+
reading the builder's state asserts as little as reading a method does, and `logger` is truthy on
|
|
262
|
+
every subject there is.
|
|
263
|
+
|
|
264
|
+
A chain ending on a bare `check()` is left alone deliberately. It is truthy and asserts nothing,
|
|
265
|
+
but calling it a defect means deciding the name is ours, and a project may register an extension
|
|
266
|
+
called `check` that asserts by itself. A read attribute needs no such guess, since nothing was
|
|
267
|
+
called at all. Measured before deciding: no bare `check()` under an `assert` in 524 files across
|
|
268
|
+
this suite, the fixture projects and a consuming framework.
|
|
269
|
+
"""
|
|
270
|
+
if not _reaches_entry(node, bindings):
|
|
271
|
+
return False
|
|
272
|
+
if _entry_call(node, bindings) or _tail(node) or (_runs(node, "eventually") and not _closed(node)):
|
|
273
|
+
return True
|
|
274
|
+
if not isinstance(node, ast.Attribute):
|
|
275
|
+
return False
|
|
276
|
+
return node.attr not in HANDS_THE_SUBJECT_BACK and not _reads_a_verdict_field(node)
|
|
277
|
+
|
|
278
|
+
|
|
198
279
|
def _closed(node: ast.expr) -> bool:
|
|
199
280
|
"""True when the statement ends in a bare ``close()``, the discard the chain itself stays quiet about."""
|
|
200
281
|
return (
|
|
@@ -206,11 +287,12 @@ def _closed(node: ast.expr) -> bool:
|
|
|
206
287
|
)
|
|
207
288
|
|
|
208
289
|
|
|
209
|
-
def _statements(tree: ast.Module) -> Iterator[tuple[ast.Expr, tuple[str, ...]]]:
|
|
210
|
-
"""Every
|
|
290
|
+
def _statements(tree: ast.Module) -> Iterator[tuple[ast.Expr | ast.Assert, tuple[str, ...]]]:
|
|
291
|
+
"""Every statement that consumes a builder on the spot, with the scope it sits in.
|
|
211
292
|
|
|
212
|
-
An expression statement
|
|
213
|
-
|
|
293
|
+
Two do. An expression statement discards the value, and anywhere else it is bound, passed or
|
|
294
|
+
returned, where whether *that* asserts is not a question about this statement. An `assert` reads
|
|
295
|
+
it for its truth, which a builder and a bound method answer the same way whatever the value is.
|
|
214
296
|
|
|
215
297
|
The scope is a chain rather than one name because a bare name does not identify a test: two classes
|
|
216
298
|
in one file may each define `test_same`, and matching on the name alone handed both findings to
|
|
@@ -224,7 +306,7 @@ def _statements(tree: ast.Module) -> Iterator[tuple[ast.Expr, tuple[str, ...]]]:
|
|
|
224
306
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
225
307
|
stack.append((child, (*scope, child.name)))
|
|
226
308
|
else:
|
|
227
|
-
if isinstance(child, ast.Expr):
|
|
309
|
+
if isinstance(child, ast.Expr | ast.Assert):
|
|
228
310
|
yield child, scope
|
|
229
311
|
stack.append((child, scope))
|
|
230
312
|
|
|
@@ -247,7 +329,7 @@ def _marked_lines(source: str) -> frozenset[int]:
|
|
|
247
329
|
return frozenset()
|
|
248
330
|
|
|
249
331
|
|
|
250
|
-
def _silenced(statement: ast.
|
|
332
|
+
def _silenced(statement: ast.stmt, marked: frozenset[int]) -> bool:
|
|
251
333
|
"""Whether the statement carries the marker on any of its own lines.
|
|
252
334
|
|
|
253
335
|
Every line the statement spans counts, so the marker can sit at the end of a call broken over
|
|
@@ -279,6 +361,10 @@ def findings(source: str, path: str, extra_entries: frozenset[str] = frozenset()
|
|
|
279
361
|
for statement, scope in _statements(tree):
|
|
280
362
|
if _silenced(statement, marked):
|
|
281
363
|
continue
|
|
364
|
+
if isinstance(statement, ast.Assert):
|
|
365
|
+
if _reads_a_truthy_chain(statement.test, bindings):
|
|
366
|
+
found.append(Finding(path, statement.lineno, _UNDER_ASSERT, scope))
|
|
367
|
+
continue
|
|
282
368
|
value = statement.value
|
|
283
369
|
awaited = isinstance(value, ast.Await)
|
|
284
370
|
while isinstance(value, ast.Await): # `await assert_that(x).eventually()...` unwraps to the chain
|
|
@@ -287,6 +373,8 @@ def findings(source: str, path: str, extra_entries: frozenset[str] = frozenset()
|
|
|
287
373
|
found.append(Finding(path, statement.lineno, _NO_ASSERTION.format(name=name), scope))
|
|
288
374
|
elif isinstance(value, ast.Attribute) and _reaches_entry(value, bindings):
|
|
289
375
|
found.append(Finding(path, statement.lineno, _NOT_CALLED, scope))
|
|
290
|
-
elif not awaited and
|
|
376
|
+
elif not awaited and _runs(value, "eventually") and not _closed(value) and _reaches_entry(value, bindings):
|
|
291
377
|
found.append(Finding(path, statement.lineno, _NOT_AWAITED, scope))
|
|
378
|
+
elif (tail := _tail(value)) and _reaches_entry(value, bindings):
|
|
379
|
+
found.append(Finding(path, statement.lineno, _ENDS_ON[_NO_VERDICT[tail]].format(name=tail), scope))
|
|
292
380
|
return sorted(found, key=lambda finding: finding.lineno)
|