assertpy2 2.26.0__tar.gz → 2.27.1__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.26.0 → assertpy2-2.27.1}/CONTRIBUTING.md +14 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/PKG-INFO +2 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_clustering.py +25 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_builder_check_typing.py +0 -10
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_capable_typing.py +37 -31
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_check_typing.py +38 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_compare.py +184 -32
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_diff.py +61 -16
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_equality.py +231 -41
- assertpy2-2.27.1/assertpy2/_engine/_introspection.py +410 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_membership.py +117 -16
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_mixin_base.py +1 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_negated_typing.py +501 -122
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_ordering.py +57 -8
- assertpy2-2.27.1/assertpy2/_engine/_pairing.py +59 -0
- assertpy2-2.27.1/assertpy2/_engine/_poll_typing.py +5492 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_require.py +62 -9
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_typing.py +91 -13
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_hints.py +22 -10
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_inline.py +9 -5
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_matcher_impls.py +211 -78
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_satisfies.py +44 -53
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_snapshot_codec.py +122 -9
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/assertpy.py +39 -17
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/async_assertions.py +38 -9
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/base.py +26 -20
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/bytes_mixin.py +4 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/collection.py +23 -29
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/contains.py +59 -28
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/dataframe.py +9 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/date.py +27 -17
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/dict.py +5 -4
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/dynamic.py +7 -3
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/errors.py +35 -8
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/exception.py +82 -59
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/file.py +9 -8
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/helpers.py +39 -40
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/http_mixin.py +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/json_mixin.py +31 -8
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/matchers.py +37 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/numeric.py +71 -43
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/pytest_plugin.py +7 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/snapshot.py +5 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/string.py +43 -22
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/concepts/stability.md +2 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/concepts/type-safety.md +8 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/extending/integrations.md +6 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/assertions.md +6 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/data.md +4 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/errors.md +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/async.md +8 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/entry-points.md +18 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/errors.md +12 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/matchers.md +24 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/mkdocs.yml +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/pyproject.toml +22 -5
- {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/generate_poll_protocols.py +258 -24
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/api_snapshot.json +506 -30
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/poll_parity_baseline.py +6 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/pyright_baseline.py +17 -19
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_async.py +150 -0
- assertpy2-2.27.1/tests/test_attrs_support.py +478 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_bytes.py +6 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_capable_protocol.py +414 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_clustering.py +22 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_complexity.py +12 -10
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_core.py +49 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dataframe.py +39 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_datetime.py +72 -10
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dynamic.py +26 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_equals.py +12 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_errors.py +31 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_evaluation_core.py +929 -5
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_extensions.py +160 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_file.py +11 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_hints.py +55 -0
- assertpy2-2.27.1/tests/test_hostile_values.py +238 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_inline_snapshot.py +27 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_json.py +15 -3
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_matcher_registry.py +21 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_negated_protocols.py +19 -3
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_numbers.py +199 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_openapi_contract.py +87 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pin_coverage.py +9 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_poll_protocols.py +101 -7
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_property_attrs.py +43 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_property_based.py +159 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_protocol_parity.py +20 -3
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_public_surface.py +57 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pyright_baseline.py +1 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pytest_plugin.py +29 -7
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_compare_config.py +224 -1
- assertpy2-2.27.1/tests/test_recursive_compare_pydantic.py +466 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_satisfy.py +104 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_snapshots.py +89 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_soft.py +80 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_surface_conformance.py +102 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing.py +43 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_conformance.py +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_negative.py +21 -7
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_promises.py +9 -0
- assertpy2-2.27.1/tests/test_verdict_corpus.py +137 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_warn.py +15 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_cases.py +64 -2
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_harness.py +25 -3
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_negative_baseline.py +74 -10
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_promises.py +89 -0
- assertpy2-2.27.1/tests/verdict_corpus.py +632 -0
- assertpy2-2.27.1/tests/verdict_golden.txt +127 -0
- assertpy2-2.26.0/assertpy2/_engine/_introspection.py +0 -214
- assertpy2-2.26.0/assertpy2/_engine/_poll_typing.py +0 -2724
- assertpy2-2.26.0/tests/test_attrs_support.py +0 -170
- assertpy2-2.26.0/tests/test_recursive_compare_pydantic.py +0 -83
- {assertpy2-2.26.0 → assertpy2-2.27.1}/.gitignore +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/LICENSE +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/README.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/SECURITY.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/__init__.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_dangling.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_operations.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_size.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/extracting.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/outcome.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/py.typed +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/assertpy2/warning.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/benchmarks/test_perf.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/extending/custom-assertions.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/getting-started/comparison.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/fluent.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/matchers.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/guides/testing.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/index.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/logo-dark.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/logo.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/recipes.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/bytes.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/collections.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/containment.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/core.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dates.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dicts.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/extracting.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/files.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/http.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/json.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/numbers.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/overview.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/strings.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/reference/warnings.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/mkdocs_hooks.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/generate_check_protocols.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/mutation_report.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/scripts/regenerate.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/api_surface.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/conftest.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/docs_fixtures.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/group_compat.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/poll_parity_cases.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_api_compatibility.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_api_vocabulary.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_architecture_doc.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_async_predicates.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_bool.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_callable.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_chaining.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_check.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_check_protocols.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_class.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_collection.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_custom_list.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dangling.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_declared_floors.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_description.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dict.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_docs_examples.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_docs_typing.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_exception_context.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_expected_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_extracting.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_fail.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_http.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_in.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_inline_record.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_list.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_match_result.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_matcher_parity.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_matchers.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_message_elision.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_none.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_not.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_operation_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_optional_integration_contracts.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_overload_order.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_overloads.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_pipeline.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_poll_parity.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_refusals.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_requirement.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_same_as.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_skip_guard.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_string.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_structural.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_sync_eventually.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_traceback.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_type.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_type_expression_failures.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_claims.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_completeness.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_from_a_wheel.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_http.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_typing_integrations.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/test_vacuity_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_http.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_integrations.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.1}/tests/typing_integrations_baseline.py +0 -0
|
@@ -114,6 +114,20 @@ change. The baseline test passes the same target itself, so it gives one answer
|
|
|
114
114
|
- `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
|
|
115
115
|
needs the `docs-examples` group and a different collection.
|
|
116
116
|
|
|
117
|
+
### When a gate says an answer moved
|
|
118
|
+
|
|
119
|
+
Two gates record what the library currently decides rather than what it should. Both fail on a change
|
|
120
|
+
and neither is a failure to paper over: read the diff, decide whether the change is meant to ship, then
|
|
121
|
+
re-record it in the same commit so the diff carries the decision into review.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
ASSERTPY2_UPDATE_API=1 uv run pytest tests/test_api_compatibility.py # the public surface
|
|
125
|
+
ASSERTPY2_UPDATE_VERDICTS=1 uv run pytest tests/test_verdict_corpus.py # what each input decides
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The second one's diff is the draft of the release's Behaviour changes section, so read it before
|
|
129
|
+
writing that section rather than after.
|
|
130
|
+
|
|
117
131
|
## Commit style
|
|
118
132
|
|
|
119
133
|
Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary: Fluent assertion library for Python
|
|
3
|
+
Version: 2.27.1
|
|
4
|
+
Summary: Fluent, fully typed assertion library for Python: your type checker sees only the assertions that fit the value, and every failure carries a structured diff. Soft assertions, polling, inline snapshots, OpenAPI contracts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
7
7
|
Project-URL: Issues, https://github.com/Solganis/assertpy2/issues
|
|
@@ -239,6 +239,25 @@ def _shown(value: object) -> str:
|
|
|
239
239
|
return _bounded(stable_repr(value))
|
|
240
240
|
|
|
241
241
|
|
|
242
|
+
class _Absent:
|
|
243
|
+
"""A side that held nothing at all.
|
|
244
|
+
|
|
245
|
+
An absent key carries `None`, and read as a value it printed as ``None`` and keyed with every
|
|
246
|
+
failure whose value really is `None`.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __repr__(self) -> str:
|
|
250
|
+
return "<absent>"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
_ABSENT = _Absent()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _side_of(entry: DiffEntry, side: str) -> object:
|
|
257
|
+
"""One side of an entry, or the absent marker where that side held nothing."""
|
|
258
|
+
return _ABSENT if entry.absent == side else (entry.actual if side == "actual" else entry.expected)
|
|
259
|
+
|
|
260
|
+
|
|
242
261
|
def _identity(value: object) -> str:
|
|
243
262
|
"""A value as a cluster key: the whole of it, in a fixed number of characters.
|
|
244
263
|
|
|
@@ -265,7 +284,9 @@ def signature(diff: DiffResult, entry: DiffEntry, label: str | None = None) -> S
|
|
|
265
284
|
return Signature(False, diff.kind, label=label)
|
|
266
285
|
if diff.kind not in _VALUES_ARE_VALUES:
|
|
267
286
|
return None
|
|
268
|
-
return Signature(
|
|
287
|
+
return Signature(
|
|
288
|
+
False, diff.kind, values=(_identity(_side_of(entry, "actual")), _identity(_side_of(entry, "expected")))
|
|
289
|
+
)
|
|
269
290
|
|
|
270
291
|
|
|
271
292
|
def is_well_formed(key: Signature) -> bool:
|
|
@@ -305,7 +326,9 @@ def observations_of(diff: DiffResult | None, label: str | None = None) -> list[O
|
|
|
305
326
|
for entry in diff.entries:
|
|
306
327
|
key = signature(diff, entry, label)
|
|
307
328
|
if key is not None:
|
|
308
|
-
seen.setdefault(
|
|
329
|
+
seen.setdefault(
|
|
330
|
+
key, Observation(key, _shown(_side_of(entry, "actual")), _shown(_side_of(entry, "expected")))
|
|
331
|
+
)
|
|
309
332
|
return list(seen.values())
|
|
310
333
|
|
|
311
334
|
|
|
@@ -27,7 +27,6 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from ..matchers import Matcher
|
|
28
28
|
from ..outcome import AssertionOutcome
|
|
29
29
|
from ._capable_typing import (
|
|
30
|
-
_Callable,
|
|
31
30
|
_Indexed,
|
|
32
31
|
_Keyed,
|
|
33
32
|
_KeyedWithItems,
|
|
@@ -1136,15 +1135,6 @@ if TYPE_CHECKING:
|
|
|
1136
1135
|
**options: Any,
|
|
1137
1136
|
) -> AssertionOutcome: ...
|
|
1138
1137
|
|
|
1139
|
-
@overload
|
|
1140
|
-
def when_called_with(
|
|
1141
|
-
self: _CheckAnyValue[Callable[..., _P]], *some_args: object, **some_kwargs: object
|
|
1142
|
-
) -> AssertionOutcome: ...
|
|
1143
|
-
@overload
|
|
1144
|
-
def when_called_with(
|
|
1145
|
-
self: _CheckAnyValue[_Callable], *some_args: object, **some_kwargs: object
|
|
1146
|
-
) -> AssertionOutcome: ...
|
|
1147
|
-
|
|
1148
1138
|
@overload
|
|
1149
1139
|
def is_not_none(self: _CheckAnyValue[str | None]) -> AssertionOutcome: ...
|
|
1150
1140
|
@overload
|
|
@@ -35,8 +35,18 @@ if TYPE_CHECKING:
|
|
|
35
35
|
from ..matchers import Matcher
|
|
36
36
|
from ._builder_check_typing import _CheckAnyValue
|
|
37
37
|
from ._compat import Self
|
|
38
|
-
from ._poll_typing import _AsyncPoll, _SyncPoll
|
|
39
|
-
from ._typing import
|
|
38
|
+
from ._poll_typing import _AsyncPoll, _NoExpectationOnAChain, _SyncPoll
|
|
39
|
+
from ._typing import (
|
|
40
|
+
_E,
|
|
41
|
+
_K,
|
|
42
|
+
_R,
|
|
43
|
+
_U,
|
|
44
|
+
_V,
|
|
45
|
+
_ExpectedCompletionAssertion,
|
|
46
|
+
_ExpectedRaiseAssertion,
|
|
47
|
+
_ExpectedWarningAssertion,
|
|
48
|
+
_ListAssertion,
|
|
49
|
+
)
|
|
40
50
|
|
|
41
51
|
# covariant: the façade only ever hands the subject back, through `value`
|
|
42
52
|
_CapableT_co = TypeVar("_CapableT_co", covariant=True)
|
|
@@ -194,6 +204,18 @@ if TYPE_CHECKING:
|
|
|
194
204
|
ignoring: type[Exception] | tuple[type[Exception], ...] = ...,
|
|
195
205
|
trace: bool = ...,
|
|
196
206
|
) -> _SyncPoll[Any]: ...
|
|
207
|
+
# where the callable view lands: flattened, the runtime's `-> Self` kept a caught message typed as the callable
|
|
208
|
+
def raises(self: _CapableAssertion[_Callable], ex: type) -> _ExpectedRaiseAssertion[Any]: ...
|
|
209
|
+
def does_not_raise(self: _CapableAssertion[_Callable], ex: type) -> _ExpectedCompletionAssertion[Any]: ...
|
|
210
|
+
def warns(
|
|
211
|
+
self: _CapableAssertion[_Callable], warning: type[Warning] = ...
|
|
212
|
+
) -> _ExpectedWarningAssertion[Any]: ...
|
|
213
|
+
def does_not_warn(
|
|
214
|
+
self: _CapableAssertion[_Callable], warning: type[Warning] = ...
|
|
215
|
+
) -> _ExpectedCompletionAssertion[Any]: ...
|
|
216
|
+
|
|
217
|
+
when_called_with: _NoExpectationOnAChain
|
|
218
|
+
|
|
197
219
|
# the builder's own helpers: left to `__getattr__`, `builder()` read as this facade over the value already here
|
|
198
220
|
def builder(
|
|
199
221
|
self,
|
|
@@ -220,8 +242,8 @@ if TYPE_CHECKING:
|
|
|
220
242
|
) -> Self: ...
|
|
221
243
|
def all_satisfy(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = ...) -> Self: ...
|
|
222
244
|
def any_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self: ...
|
|
223
|
-
def at_json_path(self, path: str) ->
|
|
224
|
-
def caused_by(self, ex: type) ->
|
|
245
|
+
def at_json_path(self, path: str) -> AssertionBuilder[Any]: ...
|
|
246
|
+
def caused_by(self, ex: type) -> AssertionBuilder[Any]: ...
|
|
225
247
|
def conforms_to_openapi(
|
|
226
248
|
self,
|
|
227
249
|
spec: dict[str, Any],
|
|
@@ -255,9 +277,7 @@ if TYPE_CHECKING:
|
|
|
255
277
|
def contains_value(
|
|
256
278
|
self: _CapableAssertion[_KeyedWithValues], *values: object
|
|
257
279
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
258
|
-
def decoded_as(
|
|
259
|
-
self: _CapableAssertion[bytes | bytearray], encoding: str = ...
|
|
260
|
-
) -> _CapableAssertion[_CapableT_co]: ...
|
|
280
|
+
def decoded_as(self: _CapableAssertion[bytes | bytearray], encoding: str = ...) -> AssertionBuilder[Any]: ...
|
|
261
281
|
def decoded_as_json(self) -> AssertionBuilder[object]: ...
|
|
262
282
|
def described_as(self, description: str) -> Self: ...
|
|
263
283
|
def does_not_contain(self, *items: object) -> Self: ...
|
|
@@ -273,28 +293,24 @@ if TYPE_CHECKING:
|
|
|
273
293
|
def does_not_exist(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
274
294
|
def does_not_have_json_path(self, path: str) -> Self: ...
|
|
275
295
|
def does_not_match(self: _CapableAssertion[str], pattern: str) -> _CapableAssertion[_CapableT_co]: ...
|
|
276
|
-
def does_not_raise(self: _CapableAssertion[_Callable], ex: type) -> _CapableAssertion[_CapableT_co]: ...
|
|
277
|
-
def does_not_warn(
|
|
278
|
-
self: _CapableAssertion[_Callable], warning: type[Warning] = ...
|
|
279
|
-
) -> _CapableAssertion[_CapableT_co]: ...
|
|
280
296
|
def each(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = ...) -> Self: ...
|
|
281
297
|
def ends_with(self, suffix: object) -> Self: ...
|
|
282
298
|
def ends_with_ignoring_case(self: _CapableAssertion[str], suffix: str) -> _CapableAssertion[_CapableT_co]: ...
|
|
283
|
-
def error_of(self, ex: type) ->
|
|
284
|
-
def errors(self) ->
|
|
299
|
+
def error_of(self, ex: type) -> AssertionBuilder[Any]: ...
|
|
300
|
+
def errors(self) -> AssertionBuilder[Any]: ...
|
|
285
301
|
def exists(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
286
|
-
def extracting(self, *names: object, **kwargs: object) ->
|
|
302
|
+
def extracting(self, *names: object, **kwargs: object) -> _ListAssertion[Any]: ...
|
|
287
303
|
def extracting_group(
|
|
288
304
|
self: _CapableAssertion[str], pattern: str, group: int | str = ...
|
|
289
|
-
) ->
|
|
290
|
-
def filtered_on(self, predicate: Matcher[Any] | Callable[[Any], bool]) ->
|
|
291
|
-
def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) ->
|
|
305
|
+
) -> AssertionBuilder[Any]: ...
|
|
306
|
+
def filtered_on(self, predicate: Matcher[Any] | Callable[[Any], bool]) -> _ListAssertion[Any]: ...
|
|
307
|
+
def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) -> _ListAssertion[Any]: ...
|
|
292
308
|
def has_byte_at(
|
|
293
309
|
self: _CapableAssertion[bytes | bytearray], index: int, expected: int
|
|
294
310
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
295
311
|
def has_json_path(self, path: str) -> Self: ...
|
|
296
312
|
def has_no_none_fields(self, *, allow_empty: bool = ...) -> Self: ...
|
|
297
|
-
def has_root_cause(self, ex: type) ->
|
|
313
|
+
def has_root_cause(self, ex: type) -> AssertionBuilder[Any]: ...
|
|
298
314
|
def has_same_size_as(self, other: Sized) -> Self: ...
|
|
299
315
|
def has_size_between(self, low: int, high: int) -> Self: ...
|
|
300
316
|
def has_size_greater_than(self, size: int) -> Self: ...
|
|
@@ -420,11 +436,10 @@ if TYPE_CHECKING:
|
|
|
420
436
|
def matches_json_schema(self, schema: dict[str, Any]) -> Self: ...
|
|
421
437
|
def matches_json_schema_from_file(self, path: str | Path) -> Self: ...
|
|
422
438
|
def matches_structure(self, spec: dict[Any, Any]) -> Self: ...
|
|
423
|
-
def matches_with_groups(self: _CapableAssertion[str], pattern: str) ->
|
|
439
|
+
def matches_with_groups(self: _CapableAssertion[str], pattern: str) -> AssertionBuilder[Any]: ...
|
|
424
440
|
def none_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self: ...
|
|
425
|
-
def raised(self) ->
|
|
426
|
-
def
|
|
427
|
-
def returned(self) -> Self: ...
|
|
441
|
+
def raised(self) -> AssertionBuilder[Any]: ...
|
|
442
|
+
def returned(self) -> AssertionBuilder[Any]: ...
|
|
428
443
|
def satisfies_exactly(self, *matchers: Matcher[Any] | Callable[..., bool]) -> Self: ...
|
|
429
444
|
def satisfies_exactly_in_any_order(self, *matchers: Matcher[Any] | Callable[..., bool]) -> Self: ...
|
|
430
445
|
def snapshot(
|
|
@@ -443,12 +458,6 @@ if TYPE_CHECKING:
|
|
|
443
458
|
self: _CapableAssertion[bytes | bytearray], prefix: bytes | bytearray
|
|
444
459
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
445
460
|
def starts_with_ignoring_case(self: _CapableAssertion[str], prefix: str) -> _CapableAssertion[_CapableT_co]: ...
|
|
446
|
-
def warns(
|
|
447
|
-
self: _CapableAssertion[_Callable], warning: type[Warning] = ...
|
|
448
|
-
) -> _CapableAssertion[_CapableT_co]: ...
|
|
449
|
-
def when_called_with(
|
|
450
|
-
self: _CapableAssertion[_Callable], *some_args: object, **some_kwargs: object
|
|
451
|
-
) -> _CapableAssertion[_CapableT_co]: ...
|
|
452
461
|
def zip_satisfies(
|
|
453
462
|
self, other: Iterable[object], predicate: Callable[..., bool], *, allow_empty: bool = ...
|
|
454
463
|
) -> Self: ...
|
|
@@ -721,9 +730,6 @@ if TYPE_CHECKING:
|
|
|
721
730
|
def starts_with_ignoring_case(
|
|
722
731
|
self: _NegatedCapableAssertion[str], prefix: str
|
|
723
732
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
724
|
-
def when_called_with(
|
|
725
|
-
self: _NegatedCapableAssertion[_Callable], *some_args: object, **some_kwargs: object
|
|
726
|
-
) -> _CapableAssertion[_CapableT_co]: ...
|
|
727
733
|
def zip_satisfies(
|
|
728
734
|
self, other: Iterable[object], predicate: Callable[..., bool], *, allow_empty: bool = ...
|
|
729
735
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
@@ -150,6 +150,12 @@ if TYPE_CHECKING:
|
|
|
150
150
|
@property
|
|
151
151
|
def not_(self) -> Self: ...
|
|
152
152
|
|
|
153
|
+
class _CheckReturningAssertion(Protocol):
|
|
154
|
+
"""The verdict twin of `_ReturningAssertion`."""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def not_(self) -> Self: ...
|
|
158
|
+
|
|
153
159
|
class _CheckZeroAssertion(Protocol):
|
|
154
160
|
"""The verdict twin of `_ZeroAssertion`."""
|
|
155
161
|
|
|
@@ -679,9 +685,41 @@ if TYPE_CHECKING:
|
|
|
679
685
|
@property
|
|
680
686
|
def not_(self) -> Self: ...
|
|
681
687
|
|
|
688
|
+
class _CheckWarnedAssertion(_CheckTextAssertion, _CheckReturningAssertion, Protocol):
|
|
689
|
+
"""The verdict twin of `_WarnedAssertion`."""
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def not_(self) -> Self: ...
|
|
693
|
+
|
|
682
694
|
class _CheckCallableAssertion(_CheckCoreAssertion, Protocol[_P_co]):
|
|
683
695
|
"""The verdict twin of `_CallableAssertion`."""
|
|
684
696
|
|
|
697
|
+
@property
|
|
698
|
+
def not_(self) -> Self: ...
|
|
699
|
+
|
|
700
|
+
class _CheckCompletedAssertion(_CheckCallableAssertion[_P_co], _CheckReturningAssertion, Protocol[_P_co]):
|
|
701
|
+
"""The verdict twin of `_CompletedAssertion`."""
|
|
702
|
+
|
|
703
|
+
@property
|
|
704
|
+
def not_(self) -> Self: ...
|
|
705
|
+
|
|
706
|
+
class _CheckExpectedRaiseAssertion(_CheckCallableAssertion[_P_co], Protocol[_P_co]):
|
|
707
|
+
"""The verdict twin of `_ExpectedRaiseAssertion`."""
|
|
708
|
+
|
|
709
|
+
def when_called_with(self, *some_args: object, **some_kwargs: object) -> AssertionOutcome: ...
|
|
710
|
+
@property
|
|
711
|
+
def not_(self) -> Self: ...
|
|
712
|
+
|
|
713
|
+
class _CheckExpectedWarningAssertion(_CheckCallableAssertion[_P_co], Protocol[_P_co]):
|
|
714
|
+
"""The verdict twin of `_ExpectedWarningAssertion`."""
|
|
715
|
+
|
|
716
|
+
def when_called_with(self, *some_args: object, **some_kwargs: object) -> AssertionOutcome: ...
|
|
717
|
+
@property
|
|
718
|
+
def not_(self) -> Self: ...
|
|
719
|
+
|
|
720
|
+
class _CheckExpectedCompletionAssertion(_CheckCallableAssertion[_P_co], Protocol[_P_co]):
|
|
721
|
+
"""The verdict twin of `_ExpectedCompletionAssertion`."""
|
|
722
|
+
|
|
685
723
|
def when_called_with(self, *some_args: object, **some_kwargs: object) -> AssertionOutcome: ...
|
|
686
724
|
@property
|
|
687
725
|
def not_(self) -> Self: ...
|
|
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|
|
16
16
|
import dataclasses
|
|
17
17
|
import datetime
|
|
18
18
|
import decimal
|
|
19
|
+
import fractions
|
|
20
|
+
import inspect
|
|
19
21
|
import math
|
|
20
22
|
import numbers
|
|
21
23
|
import pathlib
|
|
@@ -24,11 +26,21 @@ import uuid
|
|
|
24
26
|
from dataclasses import dataclass
|
|
25
27
|
from typing import TYPE_CHECKING, Any
|
|
26
28
|
|
|
27
|
-
from ._introspection import
|
|
28
|
-
|
|
29
|
+
from ._introspection import (
|
|
30
|
+
KeyedValue,
|
|
31
|
+
as_held,
|
|
32
|
+
eq_keyed,
|
|
33
|
+
is_attrs_instance,
|
|
34
|
+
is_mapping_like,
|
|
35
|
+
is_model_dump_object,
|
|
36
|
+
kind_of,
|
|
37
|
+
model_field_values,
|
|
38
|
+
)
|
|
39
|
+
from ._ordering import nan_operand
|
|
40
|
+
from ._require import raised_inside, verdict
|
|
29
41
|
|
|
30
42
|
if TYPE_CHECKING:
|
|
31
|
-
from collections.abc import Callable
|
|
43
|
+
from collections.abc import Callable, Iterable
|
|
32
44
|
|
|
33
45
|
|
|
34
46
|
_EQ_ATOMIC = frozenset(
|
|
@@ -161,33 +173,56 @@ def _find_ambiguous_operand(actual, expected, _seen=None):
|
|
|
161
173
|
operand = _ambiguous_array_operand(actual, expected)
|
|
162
174
|
if operand is not None:
|
|
163
175
|
return operand
|
|
176
|
+
for left, right in _members_compared(actual, expected):
|
|
177
|
+
found = _find_ambiguous_operand(left, right, _seen)
|
|
178
|
+
if found is not None:
|
|
179
|
+
return found
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _members_compared(actual: Any, expected: Any) -> Iterable[tuple[Any, Any]]:
|
|
184
|
+
"""The pairs ``==`` walks into for two values of one container kind, and none for any other pair."""
|
|
164
185
|
if is_mapping_like(actual) and is_mapping_like(expected):
|
|
165
186
|
expected_keys = set(expected)
|
|
166
|
-
for key in actual
|
|
167
|
-
if key in expected_keys:
|
|
168
|
-
found = _find_ambiguous_operand(actual[key], expected[key], _seen)
|
|
169
|
-
if found is not None:
|
|
170
|
-
return found
|
|
171
|
-
return None
|
|
187
|
+
return ((actual[key], expected[key]) for key in actual if key in expected_keys)
|
|
172
188
|
if (
|
|
173
189
|
dataclasses.is_dataclass(actual)
|
|
174
190
|
and not isinstance(actual, type)
|
|
175
191
|
and dataclasses.is_dataclass(expected)
|
|
176
192
|
and not isinstance(expected, type)
|
|
177
193
|
):
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
return (
|
|
195
|
+
(getattr(actual, one.name), getattr(expected, one.name, None))
|
|
196
|
+
for one in dataclasses.fields(actual)
|
|
197
|
+
if one.compare
|
|
198
|
+
)
|
|
199
|
+
if is_attrs_instance(actual) and is_attrs_instance(expected):
|
|
200
|
+
return _keyed_members(actual, expected)
|
|
183
201
|
if is_model_dump_object(actual) and is_model_dump_object(expected):
|
|
184
|
-
return
|
|
202
|
+
return ((model_field_values(actual), model_field_values(expected)),)
|
|
185
203
|
if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
return zip(actual, expected, strict=False)
|
|
205
|
+
return ()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _keyed_members(actual: Any, expected: Any) -> tuple[tuple[Any, Any], ...]:
|
|
209
|
+
"""The attrs fields ``==`` compares, each read through its key as ``==`` reads it, or none.
|
|
210
|
+
|
|
211
|
+
Only what ``==`` compares can have broken it. attrs reads every key before it compares a field, so a key
|
|
212
|
+
that raises is the error being explained and no array is to blame: read again here, it raised a second
|
|
213
|
+
error while the first was handled, and skipped, it let a later array take the blame.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
return tuple(
|
|
217
|
+
(
|
|
218
|
+
eq_keyed(one, getattr(actual, one.name)),
|
|
219
|
+
eq_keyed(one, getattr(expected, one.name)) if hasattr(expected, one.name) else None,
|
|
220
|
+
)
|
|
221
|
+
for one in actual.__attrs_attrs__
|
|
222
|
+
if one.eq is not False
|
|
223
|
+
)
|
|
224
|
+
except (TypeError, ValueError): # the two errors `_guarded_not_equal` hands to this search
|
|
225
|
+
return ()
|
|
191
226
|
|
|
192
227
|
|
|
193
228
|
def _guarded_not_equal(actual, expected, *, method="is_equal_to") -> bool:
|
|
@@ -225,16 +260,73 @@ def _is_real_number(value) -> bool:
|
|
|
225
260
|
def _within_tolerance(actual, expected, tolerance) -> bool:
|
|
226
261
|
"""Return whether two real numbers are within ``tolerance`` (absolute); ``NaN`` is never within.
|
|
227
262
|
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
Checked by type rather than through `math.isnan`, which overflows on an arbitrary-precision ``int``
|
|
264
|
+
and signals on a `Decimal` NaN.
|
|
265
|
+
|
|
266
|
+
The subtraction is tried as written, so two floats keep the arithmetic they always had. A `Decimal`
|
|
267
|
+
against a ``float`` refuses to subtract at all and a bignum ``int`` overflows one, and those two pairs
|
|
268
|
+
are measured exactly instead, through `fractions.Fraction`.
|
|
230
269
|
"""
|
|
231
|
-
if
|
|
232
|
-
return False
|
|
233
|
-
if isinstance(expected, float) and math.isnan(expected):
|
|
270
|
+
if nan_operand(actual) or nan_operand(expected):
|
|
234
271
|
return False
|
|
235
272
|
if actual == expected: # equal values (including inf == inf) are within any tolerance
|
|
236
273
|
return True
|
|
237
|
-
|
|
274
|
+
if _is_infinite(actual) or _is_infinite(expected):
|
|
275
|
+
return False # unequal, and no distance from an infinity is within a finite tolerance
|
|
276
|
+
try:
|
|
277
|
+
return abs(actual - expected) <= tolerance
|
|
278
|
+
except (TypeError, OverflowError) as error:
|
|
279
|
+
if raised_inside(error): # their own `__sub__` or `__abs__` raised: a bug in the value, not a refusal
|
|
280
|
+
raise
|
|
281
|
+
return abs(fractions.Fraction(actual) - fractions.Fraction(expected)) <= fractions.Fraction(tolerance)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _is_infinite(value) -> bool:
|
|
285
|
+
"""A `float` or `Decimal` infinity, asked by type so nothing else is converted to a float to answer.
|
|
286
|
+
|
|
287
|
+
Through `Decimal`'s own method rather than the value's: a subclass overriding `is_infinite` would
|
|
288
|
+
otherwise decide this, and run its code before the arithmetic it is being asked about.
|
|
289
|
+
"""
|
|
290
|
+
if isinstance(value, float):
|
|
291
|
+
return math.isinf(value)
|
|
292
|
+
return isinstance(value, decimal.Decimal) and decimal.Decimal.is_infinite(value)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class WindowRefusedError(TypeError):
|
|
296
|
+
"""The two operands form no window: neither the arithmetic nor an exact conversion takes them.
|
|
297
|
+
|
|
298
|
+
A `TypeError` still, so a caller that let the operand's own refusal out keeps letting this one out,
|
|
299
|
+
and a matcher, which may not raise, can tell it from a `__sub__` of somebody's own that raised.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def tolerance_window(middle: Any, tolerance: Any) -> tuple[Any, Any]:
|
|
304
|
+
"""The closed interval ``middle`` plus and minus ``tolerance``.
|
|
305
|
+
|
|
306
|
+
Typed as `Any` because the pairing is a run-time fact: the assertion has refused every combination
|
|
307
|
+
but two before it asks, a number against a number and a datetime against a timedelta, and a checker
|
|
308
|
+
reading the public signature alone is right to refuse the arithmetic. The matcher asks without
|
|
309
|
+
refusing anything first, which is what `WindowRefusedError` is for: it hands back the operands' own
|
|
310
|
+
refusal as something a matcher may answer "no match" to.
|
|
311
|
+
"""
|
|
312
|
+
if _is_infinite(middle) and _is_real_number(tolerance):
|
|
313
|
+
# its own window, the way a float infinity already gets one: no distance from it is finite. The
|
|
314
|
+
# tolerance is still read, or an infinity paired with anything at all would answer "close enough"
|
|
315
|
+
return middle, middle
|
|
316
|
+
try:
|
|
317
|
+
return middle - tolerance, middle + tolerance
|
|
318
|
+
except TypeError as refusal:
|
|
319
|
+
if raised_inside(refusal): # their own `__sub__` raised: that is a bug in the value
|
|
320
|
+
raise
|
|
321
|
+
if not all(isinstance(operand, numbers.Number) for operand in (middle, tolerance)):
|
|
322
|
+
raise WindowRefusedError(str(refusal)) from None
|
|
323
|
+
# a `Decimal` refuses arithmetic with a `float` or a `Fraction`, which both convert exactly, while
|
|
324
|
+
# an infinite or NaN one does not convert at all
|
|
325
|
+
try:
|
|
326
|
+
exact, span = fractions.Fraction(middle), fractions.Fraction(tolerance)
|
|
327
|
+
except (TypeError, ValueError, OverflowError) as failed:
|
|
328
|
+
raise WindowRefusedError(str(failed)) from None
|
|
329
|
+
return exact - span, exact + span
|
|
238
330
|
|
|
239
331
|
|
|
240
332
|
def _resolve_comparator(actual, config: _CompareConfig, *, field):
|
|
@@ -246,6 +338,7 @@ def _resolve_comparator(actual, config: _CompareConfig, *, field):
|
|
|
246
338
|
comparators = config.comparators
|
|
247
339
|
if comparators is None:
|
|
248
340
|
return None
|
|
341
|
+
actual = as_held(actual)
|
|
249
342
|
if field is not None and not isinstance(field, type) and field in comparators:
|
|
250
343
|
return comparators[field]
|
|
251
344
|
if type(actual) in comparators:
|
|
@@ -266,7 +359,7 @@ def _types_differ(actual, expected) -> bool:
|
|
|
266
359
|
``_is_matcher`` is imported here rather than at module scope because ``_matcher_impls`` imports
|
|
267
360
|
``_guarded_not_equal`` from this module, so the module-level import would be a cycle.
|
|
268
361
|
"""
|
|
269
|
-
if
|
|
362
|
+
if kind_of(actual) is kind_of(expected):
|
|
270
363
|
return False
|
|
271
364
|
from .._matcher_impls import _is_matcher
|
|
272
365
|
|
|
@@ -293,6 +386,51 @@ def _keyed_types_differ(actual, expected) -> bool:
|
|
|
293
386
|
return False
|
|
294
387
|
|
|
295
388
|
|
|
389
|
+
def _kinds_never_equal(actual, expected) -> bool:
|
|
390
|
+
"""Whether ``==`` rejects the pair by its kind alone, which a walk over the parts cannot see.
|
|
391
|
+
|
|
392
|
+
Under a compare config the walker decides by parts, so ``[1.0]`` against ``(1.0,)``, or two dataclasses
|
|
393
|
+
of different classes holding the same fields, had no differing part and passed, while ``==`` rejects
|
|
394
|
+
both outright. Asked only once ``==`` has already said no, so it never fails a pair ``==`` accepts.
|
|
395
|
+
|
|
396
|
+
The kind alone is not the answer: a list subclass may define ``__eq__`` to accept a tuple. So a pair
|
|
397
|
+
of different kinds counts only when both sides' own ``__eq__`` decline it, which leaves ``==`` at
|
|
398
|
+
identity whatever the parts hold. A pydantic model answers ``False`` rather than declining, so which of
|
|
399
|
+
class or fields decided cannot be told apart, and models are left to the walk.
|
|
400
|
+
"""
|
|
401
|
+
if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
|
|
402
|
+
kinds_differ = isinstance(actual, list) is not isinstance(expected, list)
|
|
403
|
+
else:
|
|
404
|
+
both_dataclasses = all(
|
|
405
|
+
dataclasses.is_dataclass(side) and not isinstance(side, type) for side in (actual, expected)
|
|
406
|
+
)
|
|
407
|
+
both_attrs = is_attrs_instance(actual) and is_attrs_instance(expected)
|
|
408
|
+
kinds_differ = (both_dataclasses or both_attrs) and type(actual) is not type(expected)
|
|
409
|
+
return kinds_differ and _both_decline(actual, expected)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _declines(value: Any, other: Any) -> bool:
|
|
413
|
+
"""Whether *value*'s own ``__eq__`` answers ``NotImplemented`` for *other*, asked as ``==`` asks it.
|
|
414
|
+
|
|
415
|
+
Read statically off the type, since `type(x).__eq__` is an ordinary read that a metaclass answers,
|
|
416
|
+
and bound through the descriptor protocol, since that is what turns a function, a `staticmethod` or
|
|
417
|
+
anything else on the class into the callable the operator uses.
|
|
418
|
+
"""
|
|
419
|
+
held: Any = inspect.getattr_static(type(value), "__eq__", None)
|
|
420
|
+
bind: Any = getattr(type(held), "__get__", None)
|
|
421
|
+
# measured against `==` itself: a callable that binds to nothing is called with the other side alone
|
|
422
|
+
asking = held if bind is None else bind(held, value, type(value))
|
|
423
|
+
return asking(other) is NotImplemented
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _both_decline(actual: Any, expected: Any) -> bool:
|
|
427
|
+
"""Whether each side's own ``__eq__`` answers ``NotImplemented`` for the other."""
|
|
428
|
+
try:
|
|
429
|
+
return _declines(actual, expected) and _declines(expected, actual)
|
|
430
|
+
except Exception: # an `__eq__` that raises decides nothing here, and the walk goes on as before
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
|
|
296
434
|
def _node_decision(actual, expected, config: _CompareConfig | None, *, field=None, at_root: bool = False) -> str:
|
|
297
435
|
"""Classify a node as ``"equal"``, ``"leaf"``, ``"recurse"`` or ``"strict"``.
|
|
298
436
|
|
|
@@ -307,11 +445,12 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
|
|
|
307
445
|
`assertpy2._engine._diff._child_entries()` is the single place to know.
|
|
308
446
|
"""
|
|
309
447
|
if config is not None:
|
|
310
|
-
if config.ignore_null and field is not None and expected is None:
|
|
448
|
+
if config.ignore_null and field is not None and as_held(expected) is None:
|
|
311
449
|
return "equal" # a named field the expected side leaves None is not compared
|
|
312
450
|
comparator = _resolve_comparator(actual, config, field=field)
|
|
313
451
|
if comparator is not None:
|
|
314
|
-
|
|
452
|
+
agreed = verdict(comparator(as_held(actual), as_held(expected)), subject="the comparator")
|
|
453
|
+
return "equal" if agreed else "leaf"
|
|
315
454
|
if config.strict_types:
|
|
316
455
|
if actual is expected and not at_root:
|
|
317
456
|
# identity, free from `PyObject_RichCompareBool`. Not at the root, where it made `strict_types` weaker
|
|
@@ -325,9 +464,22 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
|
|
|
325
464
|
if type(actual) not in _EQ_ATOMIC and not _guarded_not_equal(actual, expected):
|
|
326
465
|
# `[True] == [1]`: a container says nothing about the types inside it, so the walk keeps going
|
|
327
466
|
return "strict"
|
|
328
|
-
if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
|
|
329
|
-
|
|
330
|
-
|
|
467
|
+
if config.tolerance is not None and _is_real_number(as_held(actual)) and _is_real_number(as_held(expected)):
|
|
468
|
+
# a keyed field's key still holds equal what the tolerance would not: it only ever loosens `==`
|
|
469
|
+
within = _within_tolerance(as_held(actual), as_held(expected), config.tolerance)
|
|
470
|
+
return "equal" if within or (type(actual) is KeyedValue and actual == expected) else "leaf"
|
|
471
|
+
return _plain_decision(actual, expected, config, at_root=at_root)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _plain_decision(actual, expected, config: _CompareConfig | None, *, at_root: bool = False) -> str:
|
|
475
|
+
"""``==``'s answer as a decision, where under a config a pair ruled out by its kind alone is a leaf."""
|
|
476
|
+
if actual is expected and not at_root:
|
|
477
|
+
# the same rule a container's own `==` applies to its members, and the verdict came from that `==`:
|
|
478
|
+
# without it the diff listed a NaN both sides hold as differing, and the hint blamed it
|
|
479
|
+
return "equal"
|
|
480
|
+
if not _guarded_not_equal(actual, expected):
|
|
481
|
+
return "equal"
|
|
482
|
+
return "leaf" if config is not None and _kinds_never_equal(actual, expected) else "recurse"
|
|
331
483
|
|
|
332
484
|
|
|
333
485
|
def _spec_matches(key, value, specs) -> bool:
|