assertpy2 2.22.0__tar.gz → 2.24.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.24.0}/CONTRIBUTING.md +57 -9
- {assertpy2-2.22.0 → assertpy2-2.24.0}/PKG-INFO +7 -6
- {assertpy2-2.22.0 → assertpy2-2.24.0}/README.md +6 -5
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_clustering.py +10 -22
- assertpy2-2.24.0/assertpy2/_dangling.py +483 -0
- assertpy2-2.24.0/assertpy2/_engine/_builder_check_typing.py +1290 -0
- assertpy2-2.24.0/assertpy2/_engine/_capable_typing.py +402 -0
- assertpy2-2.24.0/assertpy2/_engine/_check_typing.py +686 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_compare.py +11 -16
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_compat.py +14 -1
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_contract.py +2 -4
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_diff.py +8 -17
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_equality.py +5 -13
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_introspection.py +51 -3
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_membership.py +46 -27
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_mixin_base.py +19 -15
- assertpy2-2.24.0/assertpy2/_engine/_operations.py +75 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_ordering.py +6 -13
- assertpy2-2.24.0/assertpy2/_engine/_poll_typing.py +2718 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_require.py +36 -14
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_size.py +1 -3
- assertpy2-2.24.0/assertpy2/_engine/_typing.py +997 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_hints.py +19 -47
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_inline.py +1 -2
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_matcher_impls.py +88 -102
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_satisfies.py +85 -60
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_snapshot_codec.py +6 -6
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/assertpy.py +275 -161
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/async_assertions.py +24 -58
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/base.py +44 -32
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/behave_matchers.py +16 -10
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/bytes_mixin.py +7 -5
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/collection.py +69 -43
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/contains.py +55 -40
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/dataframe.py +11 -7
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/date.py +21 -14
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/dict.py +18 -13
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/dynamic.py +1 -3
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/errors.py +48 -9
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/exception.py +32 -9
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/extracting.py +24 -29
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/file.py +14 -6
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/helpers.py +10 -22
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/http_mixin.py +5 -11
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/json_mixin.py +10 -17
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/matchers.py +11 -23
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/numeric.py +49 -20
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/pytest_plugin.py +250 -112
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/snapshot.py +46 -48
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/string.py +56 -36
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/warning.py +5 -4
- {assertpy2-2.22.0 → assertpy2-2.24.0}/benchmarks/test_perf.py +155 -59
- assertpy2-2.24.0/docs/concepts/stability.md +145 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/concepts/type-safety.md +124 -64
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/extending/custom-assertions.md +8 -4
- assertpy2-2.24.0/docs/getting-started/comparison.md +280 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/getting-started/migration.md +1 -1
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/guides/assertions.md +78 -22
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/guides/data.md +20 -11
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/guides/errors.md +107 -53
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/guides/matchers.md +52 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/guides/testing.md +23 -13
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/recipes.md +5 -3
- {assertpy2-2.22.0 → assertpy2-2.24.0}/pyproject.toml +44 -3
- assertpy2-2.24.0/scripts/generate_check_protocols.py +146 -0
- assertpy2-2.24.0/scripts/generate_poll_protocols.py +863 -0
- assertpy2-2.24.0/scripts/mutation_report.py +145 -0
- assertpy2-2.24.0/tests/api_snapshot.json +5975 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/api_surface.py +115 -7
- assertpy2-2.24.0/tests/conftest.py +99 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/docs_fixtures.py +43 -11
- assertpy2-2.24.0/tests/pyright_baseline.py +108 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_api_compatibility.py +113 -29
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_api_vocabulary.py +5 -13
- assertpy2-2.24.0/tests/test_architecture_doc.py +133 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_async.py +16 -41
- assertpy2-2.24.0/tests/test_async_predicates.py +157 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_boundary_cases.py +2 -25
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_bytes.py +4 -8
- assertpy2-2.24.0/tests/test_capable_protocol.py +578 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_check.py +3 -6
- assertpy2-2.24.0/tests/test_check_protocols.py +227 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_class.py +35 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_clustering.py +31 -63
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_collection.py +9 -14
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_core.py +18 -42
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_custom_dict.py +9 -19
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_custom_list.py +3 -6
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_dangling.py +300 -30
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_dataframe.py +27 -7
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_datetime.py +8 -9
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_dict.py +1 -2
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_dict_compare.py +1 -5
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_docs_examples.py +46 -14
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_docs_typing.py +37 -15
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_dynamic.py +2 -4
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_equals.py +8 -24
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_errors.py +41 -9
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_evaluation_core.py +159 -56
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_exception_context.py +1 -2
- assertpy2-2.24.0/tests/test_expected_contract.py +181 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_expected_exception.py +49 -29
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_expected_warning.py +3 -14
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_extensions.py +20 -16
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_extracting.py +4 -10
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_file.py +1 -1
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_grouped_soft.py +7 -7
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_hints.py +16 -73
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_http.py +6 -24
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_inline_record.py +3 -4
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_inline_snapshot.py +4 -8
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_iterable_cluster.py +23 -11
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_json.py +1 -2
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_list.py +6 -18
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_match_result.py +69 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_matcher_parity.py +51 -32
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_matcher_registry.py +2 -5
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_matchers.py +25 -20
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_message_elision.py +4 -5
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_not.py +14 -20
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_numbers.py +0 -1
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_openapi_contract.py +6 -7
- assertpy2-2.24.0/tests/test_operation_contract.py +404 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_optional_integration_contracts.py +1 -2
- assertpy2-2.24.0/tests/test_overload_order.py +165 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_pipeline.py +8 -9
- assertpy2-2.24.0/tests/test_poll_protocols.py +254 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_property_attrs.py +3 -5
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_property_based.py +197 -193
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_protocol_parity.py +115 -40
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_public_surface.py +61 -31
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_pyright_baseline.py +55 -11
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_pytest_plugin.py +324 -116
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_recursive_compare.py +1 -28
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_recursive_compare_config.py +47 -66
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_refusals.py +33 -18
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_rich_diff.py +29 -94
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_satisfy.py +2 -6
- assertpy2-2.24.0/tests/test_skip_guard.py +115 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_snapshots.py +115 -60
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_soft.py +8 -17
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_structural.py +36 -51
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_surface_conformance.py +27 -37
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_sync_eventually.py +1 -2
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_traceback.py +1 -3
- assertpy2-2.24.0/tests/test_type_expression_failures.py +148 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_typing.py +183 -96
- assertpy2-2.24.0/tests/test_typing_claims.py +194 -0
- assertpy2-2.24.0/tests/test_typing_completeness.py +142 -0
- assertpy2-2.24.0/tests/test_typing_conformance.py +489 -0
- assertpy2-2.24.0/tests/test_typing_from_a_wheel.py +97 -0
- assertpy2-2.24.0/tests/test_typing_http.py +78 -0
- assertpy2-2.24.0/tests/test_typing_integrations.py +110 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_typing_negative.py +59 -78
- assertpy2-2.24.0/tests/test_vacuity_contract.py +463 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_warn.py +24 -33
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/typing_cases.py +137 -33
- assertpy2-2.24.0/tests/typing_harness.py +107 -0
- assertpy2-2.24.0/tests/typing_http.py +75 -0
- assertpy2-2.24.0/tests/typing_integrations.py +63 -0
- assertpy2-2.24.0/tests/typing_integrations_baseline.py +92 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/typing_negative_baseline.py +131 -40
- assertpy2-2.22.0/assertpy2/_dangling.py +0 -292
- assertpy2-2.22.0/assertpy2/_engine/_typing.py +0 -632
- assertpy2-2.22.0/docs/concepts/stability.md +0 -87
- assertpy2-2.22.0/docs/getting-started/comparison.md +0 -248
- assertpy2-2.22.0/tests/api_snapshot.json +0 -3724
- assertpy2-2.22.0/tests/conftest.py +0 -35
- assertpy2-2.22.0/tests/pyright_baseline.py +0 -68
- assertpy2-2.22.0/tests/test_typing_claims.py +0 -81
- assertpy2-2.22.0/tests/test_vacuity_contract.py +0 -180
- {assertpy2-2.22.0 → assertpy2-2.24.0}/.gitignore +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/LICENSE +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/SECURITY.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/extending/integrations.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/guides/fluent.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/index.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/logo.svg +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/async.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/core.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/files.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/http.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/json.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/mkdocs.yml +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/group_compat.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_attrs_support.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_bool.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_callable.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_description.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_fail.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_in.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_none.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_string.py +0 -0
- {assertpy2-2.22.0 → assertpy2-2.24.0}/tests/test_type.py +0 -0
|
@@ -23,17 +23,26 @@ Read more about how pulls work on GitHub's [About pull requests](https://help.gi
|
|
|
23
23
|
- Python 3.10+
|
|
24
24
|
- [uv](https://docs.astral.sh/uv/) as the package manager
|
|
25
25
|
|
|
26
|
+
## Before you change a public signature
|
|
27
|
+
|
|
28
|
+
Read [ARCHITECTURE.md](ARCHITECTURE.md). Four files under `assertpy2/_engine/` are generated, the public
|
|
29
|
+
surface is pinned by a recorded snapshot, and neither is discoverable from the files themselves.
|
|
30
|
+
|
|
26
31
|
## Verification pipeline
|
|
27
32
|
|
|
28
33
|
Run all checks before submitting a PR. Every step must pass.
|
|
29
34
|
|
|
30
|
-
Install
|
|
31
|
-
|
|
35
|
+
### Install
|
|
36
|
+
|
|
37
|
+
A bare `uv sync` leaves out the optional libraries several tests import, and those tests then fail
|
|
38
|
+
rather than skip. Install exactly this set:
|
|
32
39
|
|
|
33
40
|
```bash
|
|
34
41
|
uv sync --extra json --extra data --extra inline --group integrations --group docs-examples
|
|
35
42
|
```
|
|
36
43
|
|
|
44
|
+
### The main gate
|
|
45
|
+
|
|
37
46
|
```bash
|
|
38
47
|
uv run ruff check .
|
|
39
48
|
uv run ruff format --check .
|
|
@@ -43,17 +52,58 @@ uv run pytest tests/test_docs_examples.py tests/test_typing_claims.py
|
|
|
43
52
|
uv run --group docs mkdocs build --strict
|
|
44
53
|
```
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
CI requires 100% code coverage.
|
|
56
|
+
|
|
57
|
+
### Then once on the supported floor
|
|
58
|
+
|
|
59
|
+
Run the suite on Python 3.10 as well. This is not optional, and the block above does not cover it.
|
|
60
|
+
|
|
61
|
+
A union carries `__name__` from 3.14 onwards and not before. `logging.LoggerAdapter` became
|
|
62
|
+
subscriptable in 3.11. Neither difference is visible to a type checker told to target 3.10, and both
|
|
63
|
+
shipped as defects that every other gate passed.
|
|
64
|
+
|
|
65
|
+
Use a throwaway environment. `uv run --python 3.10` inside the project rebuilds `.venv` for that
|
|
66
|
+
version, and on Windows it can fail partway and leave you without pytest:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv venv /tmp/py310 --python 3.10
|
|
70
|
+
|
|
71
|
+
export VIRTUAL_ENV=/tmp/py310
|
|
72
|
+
uv pip install -e ".[json,data,inline]"
|
|
73
|
+
uv pip install pytest pytest-cov hypothesis ruff
|
|
74
|
+
uv pip install attrs pydantic requests httpx flask
|
|
75
|
+
|
|
76
|
+
/tmp/py310/bin/python -m pytest tests --ignore=tests/test_docs_examples.py -q
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`ruff` belongs in that list: the tests that compare a generated file against its generator shell out
|
|
80
|
+
to it, and without it they fail for a reason that has nothing to do with your change.
|
|
81
|
+
|
|
82
|
+
### The four type checkers
|
|
83
|
+
|
|
84
|
+
Run these after the main gate, not before. `uv run --group typecheck` adds the checkers to the same
|
|
85
|
+
`.venv` and leaves them there, and a coverage run with them installed collects a different set:
|
|
49
86
|
|
|
50
87
|
```bash
|
|
51
88
|
uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
|
|
52
|
-
uv run --group typecheck pyright tests/test_typing.py
|
|
89
|
+
PYRIGHT_PYTHON_FORCE_VERSION=1.1.413 uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
|
|
90
|
+
uv run --group typecheck pyrefly check tests/test_typing.py
|
|
53
91
|
uv run --group typecheck pytest tests/test_pyright_baseline.py
|
|
54
92
|
```
|
|
55
93
|
|
|
56
|
-
|
|
94
|
+
`PYRIGHT_PYTHON_FORCE_VERSION` picks the engine rather than the launcher. The `pyright` distribution on
|
|
95
|
+
PyPI stopped at 1.1.411 on 25 June while npm has shipped 1.1.412 and 1.1.413 since, so the launcher and
|
|
96
|
+
the engine move apart. The baseline is recorded against one engine, and `tests/typing_harness.py` pins
|
|
97
|
+
the same build, so only this direct invocation needs the variable.
|
|
98
|
+
|
|
99
|
+
`--pythonversion 3.14` is not decoration. Pyright reports against the interpreter it finds unless
|
|
100
|
+
told otherwise, and the count moves with it: 174 diagnostics for this package on 3.10 against 169 on
|
|
101
|
+
3.14.
|
|
102
|
+
|
|
103
|
+
Without it a contributor on the supported floor meets a red baseline that says nothing about their
|
|
104
|
+
change. The baseline test passes the same target itself, so it gives one answer everywhere.
|
|
105
|
+
|
|
106
|
+
### Three details that cost time if you meet them the hard way
|
|
57
107
|
|
|
58
108
|
- `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
|
|
59
109
|
`tests/`, which is full of deliberate negative typing cases: calls that must not type-check.
|
|
@@ -63,8 +113,6 @@ Three details that cost time if you meet them the hard way:
|
|
|
63
113
|
- `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
|
|
64
114
|
needs the `docs-examples` group and a different collection.
|
|
65
115
|
|
|
66
|
-
CI requires 100% code coverage.
|
|
67
|
-
|
|
68
116
|
## Commit style
|
|
69
117
|
|
|
70
118
|
Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.24.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
|
|
|
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
|
|
|
68
68
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
69
69
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
70
70
|
<br>
|
|
71
|
-
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/
|
|
71
|
+
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
|
|
72
72
|
<a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
73
73
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
74
74
|
</p>
|
|
@@ -201,7 +201,7 @@ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/),
|
|
|
201
201
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
|
|
202
202
|
|
|
203
203
|
`assert_that()` uses `@overload` to return type-specific Protocols.<br>
|
|
204
|
-
Your IDE shows only methods relevant to the value you're testing, not
|
|
204
|
+
Your IDE shows only methods relevant to the value you're testing, not the whole surface:
|
|
205
205
|
|
|
206
206
|
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
207
207
|
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
@@ -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>
|
|
@@ -221,7 +221,7 @@ An assertion hands the value back, statically narrowed.
|
|
|
221
221
|
<!-- docs-guard: skip -->
|
|
222
222
|
```python
|
|
223
223
|
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
224
|
-
order.refund() # statically PaidOrder - verified by ty, mypy, and
|
|
224
|
+
order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
|
|
225
225
|
```
|
|
226
226
|
|
|
227
227
|
For API tests,
|
|
@@ -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
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
17
17
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
18
18
|
<br>
|
|
19
|
-
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/
|
|
19
|
+
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
|
|
20
20
|
<a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
21
21
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
22
22
|
</p>
|
|
@@ -149,7 +149,7 @@ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/),
|
|
|
149
149
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
|
|
150
150
|
|
|
151
151
|
`assert_that()` uses `@overload` to return type-specific Protocols.<br>
|
|
152
|
-
Your IDE shows only methods relevant to the value you're testing, not
|
|
152
|
+
Your IDE shows only methods relevant to the value you're testing, not the whole surface:
|
|
153
153
|
|
|
154
154
|
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
155
155
|
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
@@ -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>
|
|
@@ -169,7 +169,7 @@ An assertion hands the value back, statically narrowed.
|
|
|
169
169
|
<!-- docs-guard: skip -->
|
|
170
170
|
```python
|
|
171
171
|
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
172
|
-
order.refund() # statically PaidOrder - verified by ty, mypy, and
|
|
172
|
+
order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
For API tests,
|
|
@@ -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,7 @@ 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
|
|
194
|
-
# and the `frozenset()` it equals read apart, where two non-empty equal ones read alike
|
|
191
|
+
# an empty one falls back to `repr`: `{}` is a dict, so `set()` and the `frozenset()` it equals read apart
|
|
195
192
|
return "{" + ", ".join(members) + "}" if value else _safe_repr(value)
|
|
196
193
|
if isinstance(value, dict):
|
|
197
194
|
pairs = (f"{stable_repr(key, inner)}: {stable_repr(item, inner)}" for key, item in value.items())
|
|
@@ -289,9 +286,7 @@ def is_well_formed(key: Signature) -> bool:
|
|
|
289
286
|
)
|
|
290
287
|
if key.steps:
|
|
291
288
|
return False
|
|
292
|
-
#
|
|
293
|
-
# never joins them. A pair of values only for the kinds whose fields hold the compared values, or
|
|
294
|
-
# a `contains` payload would print the `None` that stands for a missing item as somebody's value
|
|
289
|
+
# a pair of values only for the kinds whose fields hold them, or a `contains` payload prints a `None`
|
|
295
290
|
if key.label:
|
|
296
291
|
return not key.values
|
|
297
292
|
return len(key.values) == 2 and key.where in _VALUES_ARE_VALUES
|
|
@@ -330,17 +325,14 @@ def clusters(
|
|
|
330
325
|
"""
|
|
331
326
|
if total_failures <= 0:
|
|
332
327
|
return []
|
|
333
|
-
# keyed on the node id
|
|
334
|
-
# what `pytest-rerunfailures` does on every retry, and counting its attempts as separate failures
|
|
335
|
-
# printed a cluster of six over two tests
|
|
328
|
+
# keyed on the node id: `pytest-rerunfailures` retries printed a cluster of six over two tests
|
|
336
329
|
grouped: dict[Signature, dict[str, None]] = {}
|
|
337
330
|
actuals: dict[Signature, dict[str, None]] = {}
|
|
338
331
|
expecteds: dict[Signature, dict[str, None]] = {}
|
|
339
332
|
for nodeid, found in recorded:
|
|
340
333
|
for one in found:
|
|
341
334
|
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
|
|
335
|
+
# one past the limit, so a full side is distinguishable from a capped one by its length alone
|
|
344
336
|
for store, value in ((actuals, one.actual), (expecteds, one.expected)):
|
|
345
337
|
seen = store.setdefault(one.signature, {})
|
|
346
338
|
if len(seen) <= _EXAMPLE_LIMIT:
|
|
@@ -353,8 +345,7 @@ def clusters(
|
|
|
353
345
|
for key, nodeids in grouped.items()
|
|
354
346
|
if len(nodeids) >= minimum
|
|
355
347
|
]
|
|
356
|
-
# size first, then the signature
|
|
357
|
-
# runs, and the signature is the only thing about them that cannot vary
|
|
348
|
+
# size first, then the signature: two clusters of equal size must not swap places between runs
|
|
358
349
|
large.sort(key=lambda cluster: (-cluster.size, cluster.signature))
|
|
359
350
|
return large
|
|
360
351
|
|
|
@@ -479,21 +470,18 @@ def render(
|
|
|
479
470
|
lines.append(f"{collect_errors} collection error{'' if one else 's'}, not counted below")
|
|
480
471
|
for cluster, where in zip(shown, _headings(shown), strict=True):
|
|
481
472
|
lines.append(_headline(cluster, where, total_failures))
|
|
482
|
-
# a cluster keyed on its diagnostic
|
|
483
|
-
# it are the ones that vary between its failures rather than the thing they share
|
|
473
|
+
# a cluster keyed on its diagnostic says the difference in words; the values are what varies inside it
|
|
484
474
|
if cluster.signature.label:
|
|
485
475
|
lines.append(f" {cluster.signature.label}")
|
|
486
476
|
else:
|
|
487
477
|
lines.append(f" actual: {_side(cluster.actuals)}")
|
|
488
478
|
lines.append(f" expected: {_side(cluster.expecteds)}")
|
|
489
479
|
if omitted:
|
|
490
|
-
# said rather than dropped: five of eleven
|
|
491
|
-
#
|
|
480
|
+
# said rather than dropped: "five of eleven" silently reads as "these are the causes". "more" rather
|
|
481
|
+
# than "smaller", since equal-sized clusters are ordered by signature
|
|
492
482
|
lines.append(f"{len(omitted)} more cluster{'' if len(omitted) == 1 else 's'} not shown")
|
|
493
483
|
covered = len({nodeid for cluster in found for nodeid in cluster.nodeids})
|
|
494
484
|
if covered < total_failures:
|
|
495
|
-
# what the number measures,
|
|
496
|
-
# a floor of three are related, and the summary declined to print them, which is not the same as
|
|
497
|
-
# having found nothing about them
|
|
485
|
+
# what the number measures, not "not clustered": the summary declined to print a pair under the floor
|
|
498
486
|
lines.append(f"{total_failures - covered} of {total_failures} outside any cluster of {minimum}")
|
|
499
487
|
return lines
|