assertpy2 2.26.0__tar.gz → 2.27.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.26.0 → assertpy2-2.27.0}/PKG-INFO +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_clustering.py +25 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_builder_check_typing.py +0 -10
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_check_typing.py +38 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_compare.py +125 -9
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_diff.py +15 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_equality.py +186 -40
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_introspection.py +54 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_membership.py +117 -16
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_mixin_base.py +1 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_negated_typing.py +501 -122
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_ordering.py +57 -8
- assertpy2-2.27.0/assertpy2/_engine/_pairing.py +59 -0
- assertpy2-2.27.0/assertpy2/_engine/_poll_typing.py +5492 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_require.py +62 -9
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_typing.py +91 -13
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_hints.py +8 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_inline.py +9 -5
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_matcher_impls.py +211 -78
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_satisfies.py +40 -51
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_snapshot_codec.py +23 -4
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/assertpy.py +39 -17
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/async_assertions.py +38 -9
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/base.py +26 -20
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/bytes_mixin.py +4 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/collection.py +23 -29
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/contains.py +59 -28
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/dataframe.py +9 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/date.py +27 -17
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/dict.py +5 -4
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/dynamic.py +7 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/errors.py +35 -8
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/exception.py +82 -59
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/file.py +9 -8
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/helpers.py +23 -39
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/http_mixin.py +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/json_mixin.py +31 -8
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/matchers.py +37 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/numeric.py +37 -28
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/pytest_plugin.py +2 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/snapshot.py +5 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/string.py +43 -22
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/concepts/stability.md +2 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/concepts/type-safety.md +8 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/extending/integrations.md +6 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/assertions.md +3 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/data.md +4 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/async.md +8 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/entry-points.md +18 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/errors.md +12 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/matchers.md +24 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/pyproject.toml +21 -4
- {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/generate_poll_protocols.py +200 -20
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/api_snapshot.json +489 -9
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/poll_parity_baseline.py +6 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/pyright_baseline.py +17 -19
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_async.py +150 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_bytes.py +6 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_capable_protocol.py +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_clustering.py +22 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_complexity.py +10 -7
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_core.py +49 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dataframe.py +39 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_datetime.py +72 -10
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dynamic.py +26 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_errors.py +31 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_evaluation_core.py +929 -5
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_extensions.py +160 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_file.py +11 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_hints.py +35 -0
- assertpy2-2.27.0/tests/test_hostile_values.py +238 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_inline_snapshot.py +27 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_json.py +15 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_matcher_registry.py +21 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_negated_protocols.py +19 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_numbers.py +116 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_openapi_contract.py +87 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pin_coverage.py +5 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_poll_protocols.py +101 -7
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_property_based.py +130 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_protocol_parity.py +20 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_public_surface.py +57 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pyright_baseline.py +1 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pytest_plugin.py +21 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare_config.py +189 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_satisfy.py +104 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_snapshots.py +89 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_soft.py +80 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_surface_conformance.py +102 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing.py +25 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_conformance.py +1 -1
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_negative.py +21 -7
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_promises.py +9 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_warn.py +15 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_cases.py +63 -2
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_harness.py +25 -3
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_negative_baseline.py +72 -10
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_promises.py +54 -0
- assertpy2-2.26.0/assertpy2/_engine/_poll_typing.py +0 -2724
- {assertpy2-2.26.0 → assertpy2-2.27.0}/.gitignore +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/LICENSE +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/README.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/SECURITY.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_dangling.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_capable_typing.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_operations.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_size.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/benchmarks/test_perf.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/extending/custom-assertions.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/getting-started/comparison.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/errors.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/fluent.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/matchers.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/guides/testing.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/index.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/logo.svg +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/recipes.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/core.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/files.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/http.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/json.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/mkdocs.yml +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/generate_check_protocols.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/mutation_report.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/scripts/regenerate.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/api_surface.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/conftest.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/docs_fixtures.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/group_compat.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/poll_parity_cases.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_api_compatibility.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_api_vocabulary.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_architecture_doc.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_async_predicates.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_attrs_support.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_bool.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_callable.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_check.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_check_protocols.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_class.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_collection.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dangling.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_declared_floors.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_description.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dict.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_docs_examples.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_docs_typing.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_equals.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_exception_context.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_expected_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_fail.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_http.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_in.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_inline_record.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_list.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_match_result.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_matcher_parity.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_message_elision.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_none.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_not.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_operation_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_optional_integration_contracts.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_overload_order.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_poll_parity.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_property_attrs.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_refusals.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_requirement.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_skip_guard.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_string.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_structural.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_sync_eventually.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_type.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_type_expression_failures.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_claims.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_completeness.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_from_a_wheel.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_http.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_typing_integrations.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/test_vacuity_contract.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_http.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_integrations.py +0 -0
- {assertpy2-2.26.0 → assertpy2-2.27.0}/tests/typing_integrations_baseline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.27.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
|
|
@@ -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
|
|
@@ -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,8 +26,9 @@ import uuid
|
|
|
24
26
|
from dataclasses import dataclass
|
|
25
27
|
from typing import TYPE_CHECKING, Any
|
|
26
28
|
|
|
27
|
-
from ._introspection import is_mapping_like, is_model_dump_object
|
|
28
|
-
from .
|
|
29
|
+
from ._introspection import is_attrs_instance, is_mapping_like, is_model_dump_object
|
|
30
|
+
from ._ordering import nan_operand
|
|
31
|
+
from ._require import raised_inside, verdict
|
|
29
32
|
|
|
30
33
|
if TYPE_CHECKING:
|
|
31
34
|
from collections.abc import Callable
|
|
@@ -225,16 +228,73 @@ def _is_real_number(value) -> bool:
|
|
|
225
228
|
def _within_tolerance(actual, expected, tolerance) -> bool:
|
|
226
229
|
"""Return whether two real numbers are within ``tolerance`` (absolute); ``NaN`` is never within.
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
Checked by type rather than through `math.isnan`, which overflows on an arbitrary-precision ``int``
|
|
232
|
+
and signals on a `Decimal` NaN.
|
|
233
|
+
|
|
234
|
+
The subtraction is tried as written, so two floats keep the arithmetic they always had. A `Decimal`
|
|
235
|
+
against a ``float`` refuses to subtract at all and a bignum ``int`` overflows one, and those two pairs
|
|
236
|
+
are measured exactly instead, through `fractions.Fraction`.
|
|
230
237
|
"""
|
|
231
|
-
if
|
|
232
|
-
return False
|
|
233
|
-
if isinstance(expected, float) and math.isnan(expected):
|
|
238
|
+
if nan_operand(actual) or nan_operand(expected):
|
|
234
239
|
return False
|
|
235
240
|
if actual == expected: # equal values (including inf == inf) are within any tolerance
|
|
236
241
|
return True
|
|
237
|
-
|
|
242
|
+
if _is_infinite(actual) or _is_infinite(expected):
|
|
243
|
+
return False # unequal, and no distance from an infinity is within a finite tolerance
|
|
244
|
+
try:
|
|
245
|
+
return abs(actual - expected) <= tolerance
|
|
246
|
+
except (TypeError, OverflowError) as error:
|
|
247
|
+
if raised_inside(error): # their own `__sub__` or `__abs__` raised: a bug in the value, not a refusal
|
|
248
|
+
raise
|
|
249
|
+
return abs(fractions.Fraction(actual) - fractions.Fraction(expected)) <= fractions.Fraction(tolerance)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_infinite(value) -> bool:
|
|
253
|
+
"""A `float` or `Decimal` infinity, asked by type so nothing else is converted to a float to answer.
|
|
254
|
+
|
|
255
|
+
Through `Decimal`'s own method rather than the value's: a subclass overriding `is_infinite` would
|
|
256
|
+
otherwise decide this, and run its code before the arithmetic it is being asked about.
|
|
257
|
+
"""
|
|
258
|
+
if isinstance(value, float):
|
|
259
|
+
return math.isinf(value)
|
|
260
|
+
return isinstance(value, decimal.Decimal) and decimal.Decimal.is_infinite(value)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class WindowRefusedError(TypeError):
|
|
264
|
+
"""The two operands form no window: neither the arithmetic nor an exact conversion takes them.
|
|
265
|
+
|
|
266
|
+
A `TypeError` still, so a caller that let the operand's own refusal out keeps letting this one out,
|
|
267
|
+
and a matcher, which may not raise, can tell it from a `__sub__` of somebody's own that raised.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def tolerance_window(middle: Any, tolerance: Any) -> tuple[Any, Any]:
|
|
272
|
+
"""The closed interval ``middle`` plus and minus ``tolerance``.
|
|
273
|
+
|
|
274
|
+
Typed as `Any` because the pairing is a run-time fact: the assertion has refused every combination
|
|
275
|
+
but two before it asks, a number against a number and a datetime against a timedelta, and a checker
|
|
276
|
+
reading the public signature alone is right to refuse the arithmetic. The matcher asks without
|
|
277
|
+
refusing anything first, which is what `WindowRefusedError` is for: it hands back the operands' own
|
|
278
|
+
refusal as something a matcher may answer "no match" to.
|
|
279
|
+
"""
|
|
280
|
+
if _is_infinite(middle) and _is_real_number(tolerance):
|
|
281
|
+
# its own window, the way a float infinity already gets one: no distance from it is finite. The
|
|
282
|
+
# tolerance is still read, or an infinity paired with anything at all would answer "close enough"
|
|
283
|
+
return middle, middle
|
|
284
|
+
try:
|
|
285
|
+
return middle - tolerance, middle + tolerance
|
|
286
|
+
except TypeError as refusal:
|
|
287
|
+
if raised_inside(refusal): # their own `__sub__` raised: that is a bug in the value
|
|
288
|
+
raise
|
|
289
|
+
if not all(isinstance(operand, numbers.Number) for operand in (middle, tolerance)):
|
|
290
|
+
raise WindowRefusedError(str(refusal)) from None
|
|
291
|
+
# a `Decimal` refuses arithmetic with a `float` or a `Fraction`, which both convert exactly, while
|
|
292
|
+
# an infinite or NaN one does not convert at all
|
|
293
|
+
try:
|
|
294
|
+
exact, span = fractions.Fraction(middle), fractions.Fraction(tolerance)
|
|
295
|
+
except (TypeError, ValueError, OverflowError) as failed:
|
|
296
|
+
raise WindowRefusedError(str(failed)) from None
|
|
297
|
+
return exact - span, exact + span
|
|
238
298
|
|
|
239
299
|
|
|
240
300
|
def _resolve_comparator(actual, config: _CompareConfig, *, field):
|
|
@@ -293,6 +353,51 @@ def _keyed_types_differ(actual, expected) -> bool:
|
|
|
293
353
|
return False
|
|
294
354
|
|
|
295
355
|
|
|
356
|
+
def _kinds_never_equal(actual, expected) -> bool:
|
|
357
|
+
"""Whether ``==`` rejects the pair by its kind alone, which a walk over the parts cannot see.
|
|
358
|
+
|
|
359
|
+
Under a compare config the walker decides by parts, so ``[1.0]`` against ``(1.0,)``, or two dataclasses
|
|
360
|
+
of different classes holding the same fields, had no differing part and passed, while ``==`` rejects
|
|
361
|
+
both outright. Asked only once ``==`` has already said no, so it never fails a pair ``==`` accepts.
|
|
362
|
+
|
|
363
|
+
The kind alone is not the answer: a list subclass may define ``__eq__`` to accept a tuple. So a pair
|
|
364
|
+
of different kinds counts only when both sides' own ``__eq__`` decline it, which leaves ``==`` at
|
|
365
|
+
identity whatever the parts hold. A pydantic model answers ``False`` rather than declining, so which of
|
|
366
|
+
class or fields decided cannot be told apart, and models are left to the walk.
|
|
367
|
+
"""
|
|
368
|
+
if isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple)):
|
|
369
|
+
kinds_differ = isinstance(actual, list) is not isinstance(expected, list)
|
|
370
|
+
else:
|
|
371
|
+
both_dataclasses = all(
|
|
372
|
+
dataclasses.is_dataclass(side) and not isinstance(side, type) for side in (actual, expected)
|
|
373
|
+
)
|
|
374
|
+
both_attrs = is_attrs_instance(actual) and is_attrs_instance(expected)
|
|
375
|
+
kinds_differ = (both_dataclasses or both_attrs) and type(actual) is not type(expected)
|
|
376
|
+
return kinds_differ and _both_decline(actual, expected)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _declines(value: Any, other: Any) -> bool:
|
|
380
|
+
"""Whether *value*'s own ``__eq__`` answers ``NotImplemented`` for *other*, asked as ``==`` asks it.
|
|
381
|
+
|
|
382
|
+
Read statically off the type, since `type(x).__eq__` is an ordinary read that a metaclass answers,
|
|
383
|
+
and bound through the descriptor protocol, since that is what turns a function, a `staticmethod` or
|
|
384
|
+
anything else on the class into the callable the operator uses.
|
|
385
|
+
"""
|
|
386
|
+
held: Any = inspect.getattr_static(type(value), "__eq__", None)
|
|
387
|
+
bind: Any = getattr(type(held), "__get__", None)
|
|
388
|
+
# measured against `==` itself: a callable that binds to nothing is called with the other side alone
|
|
389
|
+
asking = held if bind is None else bind(held, value, type(value))
|
|
390
|
+
return asking(other) is NotImplemented
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _both_decline(actual: Any, expected: Any) -> bool:
|
|
394
|
+
"""Whether each side's own ``__eq__`` answers ``NotImplemented`` for the other."""
|
|
395
|
+
try:
|
|
396
|
+
return _declines(actual, expected) and _declines(expected, actual)
|
|
397
|
+
except Exception: # an `__eq__` that raises decides nothing here, and the walk goes on as before
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
|
|
296
401
|
def _node_decision(actual, expected, config: _CompareConfig | None, *, field=None, at_root: bool = False) -> str:
|
|
297
402
|
"""Classify a node as ``"equal"``, ``"leaf"``, ``"recurse"`` or ``"strict"``.
|
|
298
403
|
|
|
@@ -327,7 +432,18 @@ def _node_decision(actual, expected, config: _CompareConfig | None, *, field=Non
|
|
|
327
432
|
return "strict"
|
|
328
433
|
if config.tolerance is not None and _is_real_number(actual) and _is_real_number(expected):
|
|
329
434
|
return "equal" if _within_tolerance(actual, expected, config.tolerance) else "leaf"
|
|
330
|
-
return
|
|
435
|
+
return _plain_decision(actual, expected, config, at_root=at_root)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _plain_decision(actual, expected, config: _CompareConfig | None, *, at_root: bool = False) -> str:
|
|
439
|
+
"""``==``'s answer as a decision, where under a config a pair ruled out by its kind alone is a leaf."""
|
|
440
|
+
if actual is expected and not at_root:
|
|
441
|
+
# the same rule a container's own `==` applies to its members, and the verdict came from that `==`:
|
|
442
|
+
# without it the diff listed a NaN both sides hold as differing, and the hint blamed it
|
|
443
|
+
return "equal"
|
|
444
|
+
if not _guarded_not_equal(actual, expected):
|
|
445
|
+
return "equal"
|
|
446
|
+
return "leaf" if config is not None and _kinds_never_equal(actual, expected) else "recurse"
|
|
331
447
|
|
|
332
448
|
|
|
333
449
|
def _spec_matches(key, value, specs) -> bool:
|
|
@@ -27,6 +27,7 @@ or answered for the wrong one. Ask the walker and read its answer instead: ``No
|
|
|
27
27
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
|
+
import collections
|
|
30
31
|
import dataclasses
|
|
31
32
|
import difflib
|
|
32
33
|
from typing import TYPE_CHECKING, TypeVar
|
|
@@ -48,7 +49,7 @@ def _field_dict(obj, is_model):
|
|
|
48
49
|
"""Field mapping of a pydantic-style model (``model_dump()``) or an attrs instance (shallow)."""
|
|
49
50
|
if is_model:
|
|
50
51
|
return obj.model_dump()
|
|
51
|
-
return {field.name: getattr(obj, field.name) for field in obj.__attrs_attrs__}
|
|
52
|
+
return {field.name: getattr(obj, field.name) for field in obj.__attrs_attrs__ if field.eq is not False}
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
def _child_entries(actual, expected, path: _Path, *, descended_for, _seen=None, config=None) -> list[DiffEntry]:
|
|
@@ -329,8 +330,9 @@ def _dataclass_diff_entries(actual, expected, prefix: _Path, seen, config=None)
|
|
|
329
330
|
so both report dataclass fields identically.
|
|
330
331
|
"""
|
|
331
332
|
entries: list[DiffEntry] = []
|
|
332
|
-
|
|
333
|
-
|
|
333
|
+
# a field declared `compare=False` is outside the dataclass's own `==`, so it stays outside here
|
|
334
|
+
actual_names = [field.name for field in dataclasses.fields(actual) if field.compare]
|
|
335
|
+
expected_names = [field.name for field in dataclasses.fields(expected) if field.compare]
|
|
334
336
|
in_actual, in_expected = set(actual_names), set(expected_names)
|
|
335
337
|
for field in _ordered_keys(actual_names, expected_names):
|
|
336
338
|
if field not in in_expected:
|
|
@@ -492,6 +494,7 @@ def _mapping_diff_entries(actual, expected, prefix: _Path, child_seen: set[int],
|
|
|
492
494
|
entries: list[DiffEntry] = []
|
|
493
495
|
actual_keys = set(kept)
|
|
494
496
|
expected_keys = set(kept_expected)
|
|
497
|
+
entries.extend(_order_entries(actual, expected, kept, kept_expected, prefix))
|
|
495
498
|
if config is not None and config.strict_types:
|
|
496
499
|
# `{True} & {1}` hands back whichever side the set drew from, losing the type that differs
|
|
497
500
|
stored = {key: key for key in kept_expected}
|
|
@@ -522,6 +525,15 @@ def _mapping_diff_entries(actual, expected, prefix: _Path, child_seen: set[int],
|
|
|
522
525
|
return entries
|
|
523
526
|
|
|
524
527
|
|
|
528
|
+
def _order_entries(actual, expected, kept, kept_expected, prefix: _Path) -> list[DiffEntry]:
|
|
529
|
+
"""The key order of two `OrderedDict` values holding the same keys in different places, which their `==` reads."""
|
|
530
|
+
if not (isinstance(actual, collections.OrderedDict) and isinstance(expected, collections.OrderedDict)):
|
|
531
|
+
return []
|
|
532
|
+
if set(kept) != set(kept_expected) or list(kept) == list(kept_expected):
|
|
533
|
+
return []
|
|
534
|
+
return [prefix.leaf_entry(actual=list(kept), expected=list(kept_expected))]
|
|
535
|
+
|
|
536
|
+
|
|
525
537
|
def _sub_diff_entries(
|
|
526
538
|
actual: object, expected: object, prefix: _Path = _ROOT, *, _seen: set[int] | None = None, config=None
|
|
527
539
|
) -> list[DiffEntry] | None:
|
|
@@ -14,13 +14,27 @@ calling `self._dict_not_equal(...)` still works, and the matcher calls the same
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import collections
|
|
17
18
|
import collections.abc
|
|
19
|
+
import dataclasses
|
|
20
|
+
import datetime
|
|
21
|
+
import enum
|
|
22
|
+
import numbers
|
|
23
|
+
import pathlib
|
|
18
24
|
import re
|
|
19
25
|
import types
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
import uuid
|
|
27
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
28
|
+
|
|
29
|
+
from ._compare import (
|
|
30
|
+
_guarded_not_equal,
|
|
31
|
+
_keyed_types_differ,
|
|
32
|
+
_kinds_never_equal,
|
|
33
|
+
_node_decision,
|
|
34
|
+
_spec_matches,
|
|
35
|
+
)
|
|
23
36
|
from ._diff import _sub_diff_entries
|
|
37
|
+
from ._introspection import is_attrs_instance, is_model_dump_object, is_namedtuple
|
|
24
38
|
from ._path import _ROOT
|
|
25
39
|
from ._require import refuse
|
|
26
40
|
|
|
@@ -46,6 +60,80 @@ def normalize_key_specs(specs: object, param: str) -> list:
|
|
|
46
60
|
refuse(specs, "a key, a nested-path tuple, or a list/set/frozenset of them", subject=param)
|
|
47
61
|
|
|
48
62
|
|
|
63
|
+
def key_specs_given(specs: object) -> bool:
|
|
64
|
+
"""Whether an ``ignore``/``include`` argument asks for anything, a falsy key such as ``0`` or ``""`` included.
|
|
65
|
+
|
|
66
|
+
Truthiness answered this and dropped a single falsy key in silence. An empty collection still asks
|
|
67
|
+
for nothing, as it always did.
|
|
68
|
+
"""
|
|
69
|
+
return specs is not None and not (isinstance(specs, (list, set, frozenset)) and not specs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def comparable_fields(obj: object) -> dict | None:
|
|
73
|
+
"""An object with introspectable fields as a dict of them, or ``None`` when it has no fields to compare.
|
|
74
|
+
|
|
75
|
+
Dataclasses are converted by reference rather than through `dataclasses.asdict`, which deep-copies and
|
|
76
|
+
crashes on a field that cannot be copied, and a field declared ``compare=False`` is left out, as the
|
|
77
|
+
dataclass's own ``==`` leaves it out. An attrs field declared ``eq=False`` is left out the same way.
|
|
78
|
+
|
|
79
|
+
A value of a builtin kind is not a bag of fields even when it carries a ``__dict__``: a subclass of
|
|
80
|
+
`Decimal` or `str` has an empty one, and reading it made every two such values compare equal.
|
|
81
|
+
"""
|
|
82
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
83
|
+
return cast("dict", _shallow_fields(obj))
|
|
84
|
+
if is_namedtuple(obj):
|
|
85
|
+
return dict(obj._asdict())
|
|
86
|
+
if is_model_dump_object(obj):
|
|
87
|
+
return obj.model_dump()
|
|
88
|
+
if is_attrs_instance(obj):
|
|
89
|
+
# deferred: at module level it cost 8.5 ms and 22 modules of a 39.8 ms import wherever attrs is installed
|
|
90
|
+
import attrs
|
|
91
|
+
|
|
92
|
+
return attrs.asdict(obj, filter=lambda attribute, _value: attribute.eq is not False)
|
|
93
|
+
builtin_kinds = (
|
|
94
|
+
type,
|
|
95
|
+
numbers.Number,
|
|
96
|
+
str,
|
|
97
|
+
bytes,
|
|
98
|
+
bytearray,
|
|
99
|
+
datetime.date,
|
|
100
|
+
datetime.time,
|
|
101
|
+
datetime.timedelta,
|
|
102
|
+
enum.Enum,
|
|
103
|
+
uuid.UUID,
|
|
104
|
+
pathlib.PurePath,
|
|
105
|
+
)
|
|
106
|
+
if hasattr(obj, "__dict__") and not isinstance(obj, builtin_kinds):
|
|
107
|
+
return dict(vars(obj))
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _shallow_fields(node: Any) -> Any:
|
|
112
|
+
"""`dataclasses.asdict` by reference, without the copy, and without the fields ``==`` leaves out."""
|
|
113
|
+
if dataclasses.is_dataclass(node) and not isinstance(node, type):
|
|
114
|
+
return {
|
|
115
|
+
field.name: _shallow_fields(getattr(node, field.name))
|
|
116
|
+
for field in dataclasses.fields(node)
|
|
117
|
+
if field.compare
|
|
118
|
+
}
|
|
119
|
+
if isinstance(node, tuple) and hasattr(node, "_fields"):
|
|
120
|
+
return type(node)(*[_shallow_fields(item) for item in node])
|
|
121
|
+
if isinstance(node, (list, tuple)):
|
|
122
|
+
return type(node)(_shallow_fields(item) for item in node)
|
|
123
|
+
if isinstance(node, dict):
|
|
124
|
+
return {_shallow_fields(key): _shallow_fields(value) for key, value in node.items()}
|
|
125
|
+
return node
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _as_fields(value: object) -> dict | None:
|
|
129
|
+
"""A plain dict as itself, anything else through `comparable_fields`."""
|
|
130
|
+
return value if isinstance(value, dict) else comparable_fields(value)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _plain_sequence(value: object) -> bool:
|
|
134
|
+
return isinstance(value, (list, tuple)) and not is_namedtuple(value)
|
|
135
|
+
|
|
136
|
+
|
|
49
137
|
def ignore_specs(ignore: object) -> list:
|
|
50
138
|
"""Ignore-specs, keeping nested paths whole: a one-element tuple is just that key."""
|
|
51
139
|
return [
|
|
@@ -193,6 +281,8 @@ def values_differ(value: object, other: object, config: _CompareConfig | None, *
|
|
|
193
281
|
return False
|
|
194
282
|
if config is None:
|
|
195
283
|
return _guarded_not_equal(value, other)
|
|
284
|
+
if _kinds_never_equal(value, other) and _guarded_not_equal(value, other):
|
|
285
|
+
return True
|
|
196
286
|
entries = _sub_diff_entries(value, other, _ROOT, config=config)
|
|
197
287
|
if entries is None:
|
|
198
288
|
# a leaf the walker does not decompose: `strict_types` asked here called two equal sets unequal
|
|
@@ -246,36 +336,32 @@ def mapping_differs(
|
|
|
246
336
|
return False
|
|
247
337
|
seen = seen | {pair}
|
|
248
338
|
|
|
249
|
-
|
|
339
|
+
ignoring, including = key_specs_given(ignore), key_specs_given(include)
|
|
340
|
+
if not (ignoring or including or config is not None):
|
|
250
341
|
return _guarded_not_equal(actual, expected)
|
|
251
342
|
|
|
252
|
-
ignores = ignore_specs(ignore) if
|
|
253
|
-
if
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
keys_in_expected = {
|
|
266
|
-
key
|
|
267
|
-
for key in right
|
|
268
|
-
if (not ignore or not _spec_matches(key, right[key], ignores))
|
|
269
|
-
and (not include or _spec_matches(key, right[key], includes))
|
|
270
|
-
}
|
|
271
|
-
else:
|
|
272
|
-
keys_in_actual = set(left)
|
|
273
|
-
keys_in_expected = set(right)
|
|
274
|
-
|
|
275
|
-
if keys_in_actual != keys_in_expected:
|
|
343
|
+
ignores = ignore_specs(ignore) if ignoring else []
|
|
344
|
+
includes = include_specs(include) if including else []
|
|
345
|
+
# read once for the whole mapping: asked per key, a five-hundred path include normalised itself five
|
|
346
|
+
# hundred times. After `include_specs`, so a one-shot iterable is still refused under its own name
|
|
347
|
+
nested_paths = ignore_specs(include) if including else []
|
|
348
|
+
if includes:
|
|
349
|
+
missing = missing_include_keys(left, includes)
|
|
350
|
+
if missing:
|
|
351
|
+
raise IncludeKeysMissingError(left, includes, missing)
|
|
352
|
+
keys_in_actual = _kept_keys(left, ignores, includes)
|
|
353
|
+
keys_in_expected = _kept_keys(right, ignores, includes)
|
|
354
|
+
if keys_in_actual != keys_in_expected or _order_differs(actual, expected, keys_in_actual):
|
|
276
355
|
return True
|
|
277
|
-
if
|
|
278
|
-
|
|
356
|
+
if (
|
|
357
|
+
config is not None
|
|
358
|
+
and config.strict_types
|
|
359
|
+
and _keyed_types_differ(
|
|
360
|
+
dict.fromkeys(key for key in left if key in keys_in_actual),
|
|
361
|
+
dict.fromkeys(key for key in right if key in keys_in_expected),
|
|
362
|
+
)
|
|
363
|
+
):
|
|
364
|
+
# `{True: "a"}` and `{1: "a"}` are equal to Python and not under strict types; only the keys still compared
|
|
279
365
|
return True
|
|
280
366
|
for key in keys_in_actual:
|
|
281
367
|
nested_left, nested_right = left[key], right[key]
|
|
@@ -285,18 +371,78 @@ def mapping_differs(
|
|
|
285
371
|
continue
|
|
286
372
|
if decision == "leaf":
|
|
287
373
|
return True
|
|
288
|
-
nested_ignore =
|
|
374
|
+
nested_ignore = (
|
|
375
|
+
[entry[1:] for entry in ignores if type(entry) is tuple and entry[0] == key] if ignoring else None
|
|
376
|
+
)
|
|
289
377
|
# the nested half of an include keeps whole paths, and the level above already consumed the first segment
|
|
290
378
|
nested_include = (
|
|
291
|
-
[entry[1:] for entry in
|
|
292
|
-
if include
|
|
293
|
-
else None
|
|
379
|
+
[entry[1:] for entry in nested_paths if type(entry) is tuple and entry[0] == key] if including else None
|
|
294
380
|
)
|
|
295
|
-
if
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
):
|
|
299
|
-
return True
|
|
300
|
-
elif values_differ(nested_left, nested_right, config):
|
|
381
|
+
if _nested_differs(
|
|
382
|
+
nested_left, nested_right, ignore=nested_ignore, include=nested_include, config=config, seen=seen
|
|
383
|
+
):
|
|
301
384
|
return True
|
|
302
385
|
return False
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _kept_keys(mapping: MappingLike, ignores: list, includes: list) -> set:
|
|
389
|
+
"""The keys a comparison looks at: those no ignore-spec names, and an include-spec does when there are any."""
|
|
390
|
+
return {
|
|
391
|
+
key
|
|
392
|
+
for key in mapping
|
|
393
|
+
if not (ignores and _spec_matches(key, mapping[key], ignores))
|
|
394
|
+
and (not includes or _spec_matches(key, mapping[key], includes))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _order_differs(actual: object, expected: object, kept: set) -> bool:
|
|
399
|
+
"""Whether two `OrderedDict` values hold the compared keys in different orders, which their `==` reads."""
|
|
400
|
+
if not (isinstance(actual, collections.OrderedDict) and isinstance(expected, collections.OrderedDict)):
|
|
401
|
+
return False
|
|
402
|
+
return [key for key in actual if key in kept] != [key for key in expected if key in kept]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _nested_differs(
|
|
406
|
+
left: object, right: object, *, ignore: object, include: object, config: _CompareConfig | None, seen: frozenset
|
|
407
|
+
) -> bool:
|
|
408
|
+
"""One value under a key, compared with the rest of the key path that reaches into it."""
|
|
409
|
+
if mapping_shaped(left, check_values=False) and mapping_shaped(right, check_values=False):
|
|
410
|
+
return mapping_differs(left, right, ignore=ignore, include=include, config=config, seen=seen)
|
|
411
|
+
if key_specs_given(ignore) or key_specs_given(include):
|
|
412
|
+
# a path that goes on into a dataclass, a model or an object is followed through its fields
|
|
413
|
+
left_fields, right_fields = comparable_fields(left), comparable_fields(right)
|
|
414
|
+
if left_fields is not None and right_fields is not None:
|
|
415
|
+
return mapping_differs(left_fields, right_fields, ignore=ignore, include=include, config=config, seen=seen)
|
|
416
|
+
return values_differ(left, right, config)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def filtered_differs(
|
|
420
|
+
actual: object, expected: object, *, ignore: object, include: object, config: _CompareConfig | None
|
|
421
|
+
) -> bool:
|
|
422
|
+
"""`is_equal_to`'s verdict under ``ignore``/``include`` for a pair that is not two mappings.
|
|
423
|
+
|
|
424
|
+
The builder takes a sequence pair element by element and anything else through its fields. A matcher
|
|
425
|
+
answering the same question takes the same route, or `match.equal_to(user, ignore="updated_at")`
|
|
426
|
+
compared ``updated_at`` anyway. An element with no fields is compared whole, as the builder compares
|
|
427
|
+
it. At the top a value with no fields is one the builder refuses with a `TypeError`, and a matcher,
|
|
428
|
+
which must not raise, answers that it differs.
|
|
429
|
+
"""
|
|
430
|
+
if _plain_sequence(actual) and _plain_sequence(expected):
|
|
431
|
+
sequence_actual = cast("list | tuple", actual)
|
|
432
|
+
sequence_expected = cast("list | tuple", expected)
|
|
433
|
+
if len(sequence_actual) != len(sequence_expected):
|
|
434
|
+
return True
|
|
435
|
+
return any(
|
|
436
|
+
_filtered_pair_differs(item, counterpart, ignore=ignore, include=include, config=config, at_root=False)
|
|
437
|
+
for item, counterpart in zip(sequence_actual, sequence_expected, strict=True)
|
|
438
|
+
)
|
|
439
|
+
return _filtered_pair_differs(actual, expected, ignore=ignore, include=include, config=config, at_root=True)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _filtered_pair_differs(
|
|
443
|
+
actual: object, expected: object, *, ignore: object, include: object, config: _CompareConfig | None, at_root: bool
|
|
444
|
+
) -> bool:
|
|
445
|
+
left, right = _as_fields(actual), _as_fields(expected)
|
|
446
|
+
if left is None or right is None:
|
|
447
|
+
return at_root or values_differ(actual, expected, config)
|
|
448
|
+
return mapping_differs(left, right, ignore=ignore, include=include, config=config)
|