assertpy2 2.23.0__tar.gz → 2.25.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.23.0 → assertpy2-2.25.0}/CONTRIBUTING.md +54 -11
- {assertpy2-2.23.0 → assertpy2-2.25.0}/PKG-INFO +30 -20
- {assertpy2-2.23.0 → assertpy2-2.25.0}/README.md +27 -17
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_clustering.py +7 -13
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_dangling.py +156 -53
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_builder_check_typing.py +92 -120
- assertpy2-2.25.0/assertpy2/_engine/_capable_typing.py +433 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_check_typing.py +38 -13
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_compare.py +10 -13
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_compat.py +14 -1
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_diff.py +54 -49
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_equality.py +111 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_introspection.py +89 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_membership.py +29 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_mixin_base.py +19 -7
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_operations.py +4 -11
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_ordering.py +5 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_poll_typing.py +183 -241
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_require.py +29 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_size.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_typing.py +98 -49
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_hints.py +10 -21
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_matcher_impls.py +157 -61
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_satisfies.py +15 -17
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_snapshot_codec.py +4 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/assertpy.py +110 -91
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/async_assertions.py +10 -18
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/base.py +29 -18
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/behave_matchers.py +16 -10
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/collection.py +17 -27
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/contains.py +4 -5
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/dict.py +14 -11
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/dynamic.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/errors.py +26 -4
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/exception.py +216 -3
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/extracting.py +19 -18
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/file.py +7 -4
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/helpers.py +88 -48
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/http_mixin.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/json_mixin.py +3 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/matchers.py +6 -6
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/numeric.py +8 -5
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/pytest_plugin.py +170 -117
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/snapshot.py +42 -31
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/string.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/benchmarks/test_perf.py +171 -41
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/concepts/stability.md +77 -40
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/concepts/type-safety.md +100 -78
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/extending/custom-assertions.md +58 -8
- assertpy2-2.25.0/docs/getting-started/comparison.md +280 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/getting-started/migration.md +1 -1
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/assertions.md +76 -24
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/data.md +20 -11
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/errors.md +125 -60
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/matchers.md +22 -7
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/testing.md +28 -13
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/recipes.md +34 -3
- {assertpy2-2.23.0 → assertpy2-2.25.0}/pyproject.toml +50 -7
- {assertpy2-2.23.0 → assertpy2-2.25.0}/scripts/generate_check_protocols.py +26 -11
- assertpy2-2.25.0/scripts/generate_poll_protocols.py +907 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/scripts/mutation_report.py +12 -6
- assertpy2-2.25.0/scripts/regenerate.py +45 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/api_snapshot.json +492 -145
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/api_surface.py +32 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/conftest.py +50 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/docs_fixtures.py +37 -13
- assertpy2-2.25.0/tests/pyright_baseline.py +108 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_api_compatibility.py +42 -39
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_api_vocabulary.py +5 -10
- assertpy2-2.25.0/tests/test_architecture_doc.py +112 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_async.py +13 -26
- assertpy2-2.25.0/tests/test_async_predicates.py +195 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_bytes.py +2 -4
- assertpy2-2.25.0/tests/test_capable_protocol.py +629 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_check.py +3 -6
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_check_protocols.py +41 -19
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_class.py +35 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_clustering.py +31 -37
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_collection.py +9 -14
- assertpy2-2.25.0/tests/test_complexity.py +120 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_core.py +5 -12
- assertpy2-2.25.0/tests/test_custom_dict.py +1010 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dangling.py +134 -20
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dataframe.py +3 -7
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_datetime.py +1 -2
- assertpy2-2.25.0/tests/test_declared_floors.py +190 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dict.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dict_compare.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_docs_examples.py +45 -13
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_docs_typing.py +4 -6
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_dynamic.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_equals.py +7 -15
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_errors.py +2 -5
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_evaluation_core.py +107 -19
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_exception_context.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_expected_contract.py +92 -5
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_expected_exception.py +205 -8
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_expected_warning.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_extensions.py +17 -11
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_extracting.py +3 -6
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_hints.py +7 -15
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_http.py +6 -12
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_inline_record.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_inline_snapshot.py +4 -8
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_iterable_cluster.py +8 -8
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_json.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_list.py +4 -8
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_matcher_parity.py +49 -30
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_matcher_registry.py +2 -5
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_matchers.py +142 -21
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_message_elision.py +2 -3
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_not.py +4 -7
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_openapi_contract.py +6 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_operation_contract.py +6 -12
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_optional_integration_contracts.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_overload_order.py +2 -7
- assertpy2-2.25.0/tests/test_pin_coverage.py +480 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_pipeline.py +8 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_poll_protocols.py +43 -23
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_property_attrs.py +2 -4
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_property_based.py +327 -82
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_protocol_parity.py +33 -52
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_public_surface.py +48 -24
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_pyright_baseline.py +45 -21
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_pytest_plugin.py +147 -24
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare.py +1 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare_config.py +46 -44
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_refusals.py +30 -13
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_rich_diff.py +17 -33
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_satisfy.py +2 -4
- assertpy2-2.25.0/tests/test_skip_guard.py +115 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_snapshots.py +99 -15
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_soft.py +6 -12
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_structural.py +10 -19
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_surface_conformance.py +23 -28
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_sync_eventually.py +1 -2
- assertpy2-2.25.0/tests/test_type_expression_failures.py +148 -0
- assertpy2-2.25.0/tests/test_typing.py +621 -0
- assertpy2-2.25.0/tests/test_typing_claims.py +194 -0
- assertpy2-2.25.0/tests/test_typing_completeness.py +142 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_conformance.py +64 -15
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_from_a_wheel.py +5 -9
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_http.py +2 -2
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_integrations.py +3 -4
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_typing_negative.py +19 -24
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_vacuity_contract.py +9 -12
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_cases.py +100 -31
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_harness.py +28 -1
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_http.py +7 -7
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_integrations.py +7 -8
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_integrations_baseline.py +3 -4
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/typing_negative_baseline.py +84 -43
- assertpy2-2.23.0/docs/getting-started/comparison.md +0 -248
- assertpy2-2.23.0/scripts/generate_poll_protocols.py +0 -448
- assertpy2-2.23.0/tests/pyright_baseline.py +0 -66
- assertpy2-2.23.0/tests/test_custom_dict.py +0 -234
- assertpy2-2.23.0/tests/test_typing.py +0 -386
- assertpy2-2.23.0/tests/test_typing_claims.py +0 -80
- {assertpy2-2.23.0 → assertpy2-2.25.0}/.gitignore +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/LICENSE +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/SECURITY.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_contract.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/_inline.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/dataframe.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/date.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/extending/integrations.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/guides/fluent.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/index.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/logo.svg +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/async.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/core.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/files.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/http.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/json.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/mkdocs.yml +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/group_compat.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_attrs_support.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_bool.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_callable.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_description.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_fail.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_file.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_in.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_match_result.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_none.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_string.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_type.py +0 -0
- {assertpy2-2.23.0 → assertpy2-2.25.0}/tests/test_warn.py +0 -0
|
@@ -23,17 +23,26 @@ Read more about how pulls work on GitHub's [About pull requests](https://help.gi
|
|
|
23
23
|
- Python 3.10+
|
|
24
24
|
- [uv](https://docs.astral.sh/uv/) as the package manager
|
|
25
25
|
|
|
26
|
+
## Before you change a public signature
|
|
27
|
+
|
|
28
|
+
Read [ARCHITECTURE.md](ARCHITECTURE.md). Four files under `assertpy2/_engine/` are generated, the public
|
|
29
|
+
surface is pinned by a recorded snapshot, and neither is discoverable from the files themselves.
|
|
30
|
+
|
|
26
31
|
## Verification pipeline
|
|
27
32
|
|
|
28
33
|
Run all checks before submitting a PR. Every step must pass.
|
|
29
34
|
|
|
30
|
-
Install
|
|
31
|
-
|
|
35
|
+
### Install
|
|
36
|
+
|
|
37
|
+
A bare `uv sync` leaves out the optional libraries several tests import, and those tests then fail
|
|
38
|
+
rather than skip. Install exactly this set:
|
|
32
39
|
|
|
33
40
|
```bash
|
|
34
41
|
uv sync --extra json --extra data --extra inline --group integrations --group docs-examples
|
|
35
42
|
```
|
|
36
43
|
|
|
44
|
+
### The main gate
|
|
45
|
+
|
|
37
46
|
```bash
|
|
38
47
|
uv run ruff check .
|
|
39
48
|
uv run ruff format --check .
|
|
@@ -43,22 +52,58 @@ uv run pytest tests/test_docs_examples.py tests/test_typing_claims.py
|
|
|
43
52
|
uv run --group docs mkdocs build --strict
|
|
44
53
|
```
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
CI requires 100% code coverage.
|
|
56
|
+
|
|
57
|
+
### Then once on the supported floor
|
|
58
|
+
|
|
59
|
+
Run the suite on Python 3.10 as well. This is not optional, and the block above does not cover it.
|
|
60
|
+
|
|
61
|
+
A union carries `__name__` from 3.14 onwards and not before. `logging.LoggerAdapter` became
|
|
62
|
+
subscriptable in 3.11. Neither difference is visible to a type checker told to target 3.10, and both
|
|
63
|
+
shipped as defects that every other gate passed.
|
|
64
|
+
|
|
65
|
+
Use a throwaway environment. `uv run --python 3.10` inside the project rebuilds `.venv` for that
|
|
66
|
+
version, and on Windows it can fail partway and leave you without pytest:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv venv /tmp/py310 --python 3.10
|
|
70
|
+
|
|
71
|
+
export VIRTUAL_ENV=/tmp/py310
|
|
72
|
+
uv pip install -e ".[json,data,inline]"
|
|
73
|
+
uv pip install pytest pytest-cov hypothesis ruff
|
|
74
|
+
uv pip install attrs pydantic requests httpx flask
|
|
75
|
+
|
|
76
|
+
/tmp/py310/bin/python -m pytest tests --ignore=tests/test_docs_examples.py -q
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`ruff` belongs in that list: the tests that compare a generated file against its generator shell out
|
|
80
|
+
to it, and without it they fail for a reason that has nothing to do with your change.
|
|
81
|
+
|
|
82
|
+
### The four type checkers
|
|
83
|
+
|
|
84
|
+
Run these after the main gate, not before. `uv run --group typecheck` adds the checkers to the same
|
|
85
|
+
`.venv` and leaves them there, and a coverage run with them installed collects a different set:
|
|
49
86
|
|
|
50
87
|
```bash
|
|
51
88
|
uv run --group typecheck mypy --strict --follow-imports=silent tests/test_typing.py
|
|
52
|
-
uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
|
|
89
|
+
PYRIGHT_PYTHON_FORCE_VERSION=1.1.413 uv run --group typecheck pyright --pythonversion 3.14 tests/test_typing.py
|
|
90
|
+
uv run --group typecheck pyrefly check tests/test_typing.py
|
|
53
91
|
uv run --group typecheck pytest tests/test_pyright_baseline.py
|
|
54
92
|
```
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
`PYRIGHT_PYTHON_FORCE_VERSION` picks the engine rather than the launcher. The `pyright` distribution on
|
|
95
|
+
PyPI stopped at 1.1.411 on 25 June while npm has shipped 1.1.412 and 1.1.413 since, so the launcher and
|
|
96
|
+
the engine move apart. The baseline is recorded against one engine, and `tests/typing_harness.py` pins
|
|
97
|
+
the same build, so only this direct invocation needs the variable.
|
|
98
|
+
|
|
99
|
+
`--pythonversion 3.14` is not decoration. Pyright reports against the interpreter it finds unless
|
|
100
|
+
told otherwise, and the count moves with it: 174 diagnostics for this package on 3.10 against 169 on
|
|
101
|
+
3.14.
|
|
102
|
+
|
|
58
103
|
Without it a contributor on the supported floor meets a red baseline that says nothing about their
|
|
59
104
|
change. The baseline test passes the same target itself, so it gives one answer everywhere.
|
|
60
105
|
|
|
61
|
-
Three details that cost time if you meet them the hard way
|
|
106
|
+
### Three details that cost time if you meet them the hard way
|
|
62
107
|
|
|
63
108
|
- `ty check` is scoped on purpose. Run over the whole tree it reports hundreds of diagnostics from
|
|
64
109
|
`tests/`, which is full of deliberate negative typing cases: calls that must not type-check.
|
|
@@ -68,8 +113,6 @@ Three details that cost time if you meet them the hard way:
|
|
|
68
113
|
- `tests/test_docs_examples.py` is run separately because it executes the snippets in `docs/`, which
|
|
69
114
|
needs the `docs-examples` group and a different collection.
|
|
70
115
|
|
|
71
|
-
CI requires 100% code coverage.
|
|
72
|
-
|
|
73
116
|
## Commit style
|
|
74
117
|
|
|
75
118
|
Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, etc.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.25.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
|
|
@@ -30,13 +30,13 @@ Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
|
|
|
30
30
|
Provides-Extra: allure
|
|
31
31
|
Requires-Dist: allure-pytest>=2.13; extra == 'allure'
|
|
32
32
|
Provides-Extra: behave
|
|
33
|
-
Requires-Dist: behave>=1.
|
|
33
|
+
Requires-Dist: behave>=1.3.0; extra == 'behave'
|
|
34
34
|
Provides-Extra: data
|
|
35
35
|
Requires-Dist: numpy>=1.26; extra == 'data'
|
|
36
36
|
Requires-Dist: pandas>=2.0; extra == 'data'
|
|
37
37
|
Requires-Dist: polars>=1.0; extra == 'data'
|
|
38
38
|
Provides-Extra: inline
|
|
39
|
-
Requires-Dist: asttokens>=2.0; extra == 'inline'
|
|
39
|
+
Requires-Dist: asttokens>=2.0.4; extra == 'inline'
|
|
40
40
|
Requires-Dist: executing>=2.2; extra == 'inline'
|
|
41
41
|
Provides-Extra: json
|
|
42
42
|
Requires-Dist: jsonpath-ng>=1.8; extra == 'json'
|
|
@@ -68,7 +68,7 @@ Description-Content-Type: text/markdown
|
|
|
68
68
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
69
69
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
70
70
|
<br>
|
|
71
|
-
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/
|
|
71
|
+
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
|
|
72
72
|
<a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
73
73
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
74
74
|
</p>
|
|
@@ -130,7 +130,7 @@ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI respons
|
|
|
130
130
|
|
|
131
131
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
|
|
132
132
|
|
|
133
|
-
`assert` states a condition well
|
|
133
|
+
`assert` states a condition well. pytest reports it well.
|
|
134
134
|
|
|
135
135
|
What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
|
|
136
136
|
|
|
@@ -154,12 +154,12 @@ assert_that(response).is_equal_to(expected)
|
|
|
154
154
|
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
|
|
155
155
|
</p>
|
|
156
156
|
|
|
157
|
-
It recurses through nested containers
|
|
157
|
+
It recurses through nested containers. Matcher predicates get the same treatment.
|
|
158
158
|
|
|
159
159
|
For dynamic fields like IDs, assert a subset with
|
|
160
160
|
[`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
|
|
161
161
|
|
|
162
|
-
The chain is the other half
|
|
162
|
+
The chain is the other half. Your IDE offers only the
|
|
163
163
|
[methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
|
|
164
164
|
|
|
165
165
|
<!-- docs-guard: skip -->
|
|
@@ -169,7 +169,7 @@ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
|
169
169
|
|
|
170
170
|
Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
|
|
171
171
|
|
|
172
|
-
Nothing is patched, so a matcher can sit inside the expected structure
|
|
172
|
+
Nothing is patched, so a matcher can sit inside the expected structure at any depth:
|
|
173
173
|
|
|
174
174
|
```python
|
|
175
175
|
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
|
|
@@ -201,7 +201,7 @@ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/),
|
|
|
201
201
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
|
|
202
202
|
|
|
203
203
|
`assert_that()` uses `@overload` to return type-specific Protocols.<br>
|
|
204
|
-
Your IDE shows only methods relevant to the value you're testing, not
|
|
204
|
+
Your IDE shows only methods relevant to the value you're testing, not the whole surface:
|
|
205
205
|
|
|
206
206
|
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
207
207
|
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
@@ -210,7 +210,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
210
210
|
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
211
211
|
|
|
212
212
|
15 type-specific Protocols instead of one `Any`.<br>
|
|
213
|
-
Works in PyCharm, VS Code, and any
|
|
213
|
+
Works in PyCharm, VS Code, and any editor that runs a type checker.
|
|
214
214
|
|
|
215
215
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
216
216
|
|
|
@@ -221,7 +221,7 @@ An assertion hands the value back, statically narrowed.
|
|
|
221
221
|
<!-- docs-guard: skip -->
|
|
222
222
|
```python
|
|
223
223
|
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
224
|
-
order.refund() # statically PaidOrder - verified by ty, mypy, and
|
|
224
|
+
order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
|
|
225
225
|
```
|
|
226
226
|
|
|
227
227
|
For API tests,
|
|
@@ -235,7 +235,7 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
235
235
|
|
|
236
236
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
|
|
237
237
|
|
|
238
|
-
An exception is the right default
|
|
238
|
+
An exception is the right default. Reading the result means catching it.
|
|
239
239
|
|
|
240
240
|
`check()` runs the next assertion for its verdict instead:
|
|
241
241
|
|
|
@@ -250,11 +250,11 @@ if not outcome and outcome.diff:
|
|
|
250
250
|
```
|
|
251
251
|
|
|
252
252
|
It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
|
|
253
|
-
and a walkable `.diff
|
|
253
|
+
and a walkable `.diff`. So does `AssertionFailure`.
|
|
254
254
|
|
|
255
255
|
So a reporter reads structure instead of parsing a string. That is how the
|
|
256
|
-
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works
|
|
257
|
-
|
|
256
|
+
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works.
|
|
257
|
+
Anything else you build can too.
|
|
258
258
|
|
|
259
259
|
<h2 align="center">Features</h2>
|
|
260
260
|
|
|
@@ -262,8 +262,6 @@ is open to anything else you build.
|
|
|
262
262
|
|
|
263
263
|
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
264
264
|
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
|
|
265
|
-
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
|
|
266
|
-
- [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
|
|
267
265
|
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
268
266
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
269
267
|
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.
|
|
@@ -281,6 +279,7 @@ is open to anything else you build.
|
|
|
281
279
|
- [**Dict comparison**](https://solganis.github.io/assertpy2/guides/assertions/#selective-comparison-ignore--include): `is_equal_to(ignore=..., include=...)` for selective key/field matching by name, regex, or type.
|
|
282
280
|
- [**Recursive comparison**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance`, `comparators`, or `ignore_null` for nested structures.
|
|
283
281
|
- [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
282
|
+
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
284
283
|
|
|
285
284
|
**Testing**
|
|
286
285
|
|
|
@@ -288,17 +287,28 @@ is open to anything else you build.
|
|
|
288
287
|
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
|
|
289
288
|
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
|
|
290
289
|
- [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
|
|
290
|
+
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
|
|
291
|
+
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.
|
|
292
|
+
|
|
293
|
+
**Failure reporting**
|
|
294
|
+
|
|
291
295
|
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
|
|
292
296
|
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
|
|
293
297
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
|
|
294
|
-
|
|
295
|
-
|
|
298
|
+
|
|
299
|
+
**Plugin for pytest**
|
|
300
|
+
|
|
301
|
+
Set from the command line or from `[tool.pytest.ini_options]`, not from a call.
|
|
302
|
+
|
|
303
|
+
- [**Failure clustering**](https://solganis.github.io/assertpy2/guides/errors/#what-the-failures-had-in-common): forty failing tests are usually not forty problems. Where three or more differ at the same place, the run ends with a line saying where. On by default, `assertpy2_failure_clusters = "off"` turns it off.
|
|
304
|
+
- [**Diagnostic profiles**](https://solganis.github.io/assertpy2/guides/errors/#configuration): `assertpy2_profile` turns the guards below on in one line. `compatible` (default) leaves them off, `safe` warns, `strict` fails the tests they find. A setting you name yourself still wins.
|
|
305
|
+
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
|
|
306
|
+
- [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
|
|
296
307
|
|
|
297
308
|
**Extensibility**
|
|
298
309
|
|
|
299
310
|
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
|
|
300
311
|
- [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
|
|
301
|
-
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
302
312
|
|
|
303
313
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
|
|
304
314
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
17
17
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
18
18
|
<br>
|
|
19
|
-
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/
|
|
19
|
+
<a href="https://solganis.github.io/assertpy2/concepts/type-safety/"><img src="https://img.shields.io/badge/overloads-ty%20%7C%20mypy%20%7C%20pyright%20%7C%20pyrefly-2ea043" alt="every public overload checked by ty, mypy --strict, pyright and pyrefly with zero suppressions"></a>
|
|
20
20
|
<a href="https://solganis.github.io/assertpy2/"><img src="https://img.shields.io/badge/docs-online-black" alt="Documentation"></a>
|
|
21
21
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://img.shields.io/ossf-scorecard/github.com/Solganis/assertpy2?label=OpenSSF%20Scorecard" alt="OpenSSF Scorecard"></a>
|
|
22
22
|
</p>
|
|
@@ -78,7 +78,7 @@ Allure, Behave, JSON Path and Schema, pandas, polars, numpy, and OpenAPI respons
|
|
|
78
78
|
|
|
79
79
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/getting-started/comparison/">Why fluent assertions?</a></h2>
|
|
80
80
|
|
|
81
|
-
`assert` states a condition well
|
|
81
|
+
`assert` states a condition well. pytest reports it well.
|
|
82
82
|
|
|
83
83
|
What it cannot say is *where* two structures differ. It prints both and leaves the reading to you:
|
|
84
84
|
|
|
@@ -102,12 +102,12 @@ assert_that(response).is_equal_to(expected)
|
|
|
102
102
|
<img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: user.role shown with its path, removal in red and addition in green">
|
|
103
103
|
</p>
|
|
104
104
|
|
|
105
|
-
It recurses through nested containers
|
|
105
|
+
It recurses through nested containers. Matcher predicates get the same treatment.
|
|
106
106
|
|
|
107
107
|
For dynamic fields like IDs, assert a subset with
|
|
108
108
|
[`matches_structure()`](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching).
|
|
109
109
|
|
|
110
|
-
The chain is the other half
|
|
110
|
+
The chain is the other half. Your IDE offers only the
|
|
111
111
|
[methods that fit the value](https://solganis.github.io/assertpy2/concepts/type-safety/).
|
|
112
112
|
|
|
113
113
|
<!-- docs-guard: skip -->
|
|
@@ -117,7 +117,7 @@ assert_that(items).is_instance_of(list).is_length(3).contains("admin")
|
|
|
117
117
|
|
|
118
118
|
Matchers are ordinary values that answer `==`, the way `unittest.mock.ANY` does.
|
|
119
119
|
|
|
120
|
-
Nothing is patched, so a matcher can sit inside the expected structure
|
|
120
|
+
Nothing is patched, so a matcher can sit inside the expected structure at any depth:
|
|
121
121
|
|
|
122
122
|
```python
|
|
123
123
|
response = {"id": 7, "user": {"name": "Alice", "age": 30}, "tags": ["a", "b"]}
|
|
@@ -149,7 +149,7 @@ There are [45 matchers](https://solganis.github.io/assertpy2/guides/matchers/),
|
|
|
149
149
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/">Type-aware autocomplete</a></h2>
|
|
150
150
|
|
|
151
151
|
`assert_that()` uses `@overload` to return type-specific Protocols.<br>
|
|
152
|
-
Your IDE shows only methods relevant to the value you're testing, not
|
|
152
|
+
Your IDE shows only methods relevant to the value you're testing, not the whole surface:
|
|
153
153
|
|
|
154
154
|
- `assert_that("hello").` → string methods: `starts_with`, `matches`, `is_alpha`, ...
|
|
155
155
|
- `assert_that(42).` → numeric methods: `is_positive`, `is_between`, `is_close_to`, ...
|
|
@@ -158,7 +158,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
|
|
|
158
158
|
- `assert_that(b"\x89PNG").` → bytes methods: `starts_with_bytes`, `is_valid_utf8`, `decoded_as`, ...
|
|
159
159
|
|
|
160
160
|
15 type-specific Protocols instead of one `Any`.<br>
|
|
161
|
-
Works in PyCharm, VS Code, and any
|
|
161
|
+
Works in PyCharm, VS Code, and any editor that runs a type checker.
|
|
162
162
|
|
|
163
163
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/concepts/type-safety/#typed-narrowing-with-value">Typed narrowing</a></h2>
|
|
164
164
|
|
|
@@ -169,7 +169,7 @@ An assertion hands the value back, statically narrowed.
|
|
|
169
169
|
<!-- docs-guard: skip -->
|
|
170
170
|
```python
|
|
171
171
|
order = assert_that(repo.find(42)).is_not_none().is_instance_of(PaidOrder).value
|
|
172
|
-
order.refund() # statically PaidOrder - verified by ty, mypy, and
|
|
172
|
+
order.refund() # statically PaidOrder - verified by ty, mypy, pyright and pyrefly
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
For API tests,
|
|
@@ -183,7 +183,7 @@ data = assert_conforms(response.json(), OrderModel).value # data: OrderModel
|
|
|
183
183
|
|
|
184
184
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting">A failure you can read from code</a></h2>
|
|
185
185
|
|
|
186
|
-
An exception is the right default
|
|
186
|
+
An exception is the right default. Reading the result means catching it.
|
|
187
187
|
|
|
188
188
|
`check()` runs the next assertion for its verdict instead:
|
|
189
189
|
|
|
@@ -198,11 +198,11 @@ if not outcome and outcome.diff:
|
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
It is truthy when the assertion held. When it did not, it carries `.message`, `.actual`, `.expected`
|
|
201
|
-
and a walkable `.diff
|
|
201
|
+
and a walkable `.diff`. So does `AssertionFailure`.
|
|
202
202
|
|
|
203
203
|
So a reporter reads structure instead of parsing a string. That is how the
|
|
204
|
-
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works
|
|
205
|
-
|
|
204
|
+
[Allure integration](https://solganis.github.io/assertpy2/extending/integrations/#allure) works.
|
|
205
|
+
Anything else you build can too.
|
|
206
206
|
|
|
207
207
|
<h2 align="center">Features</h2>
|
|
208
208
|
|
|
@@ -210,8 +210,6 @@ is open to anything else you build.
|
|
|
210
210
|
|
|
211
211
|
- [**Structural matching**](https://solganis.github.io/assertpy2/guides/matchers/#structural-matching): `matches_structure()` for declarative dict/API-response validation.
|
|
212
212
|
- [**Recursive field assertions**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-field-assertions): `all_fields_satisfy()` / `has_no_none_fields()` apply a predicate to every leaf of an object graph.
|
|
213
|
-
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
|
|
214
|
-
- [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
|
|
215
213
|
- [**Universal negation**](https://solganis.github.io/assertpy2/guides/fluent/#universal-negation): `.not_` inverts any assertion, no dedicated `is_not_*` methods.
|
|
216
214
|
- [**Collection pipeline**](https://solganis.github.io/assertpy2/guides/fluent/#collection-pipeline): `filtered_on()`, `mapped()`, `flat_mapped()`, `first()`, `last()`, `element()`, `single()`.
|
|
217
215
|
- [**Positional & pairwise checks**](https://solganis.github.io/assertpy2/guides/assertions/#lists): `satisfies_exactly()`, `zip_satisfies()`, `contains_only_once()`, `has_same_size_as()`, plus `*_in_any_order` variants.
|
|
@@ -229,6 +227,7 @@ is open to anything else you build.
|
|
|
229
227
|
- [**Dict comparison**](https://solganis.github.io/assertpy2/guides/assertions/#selective-comparison-ignore--include): `is_equal_to(ignore=..., include=...)` for selective key/field matching by name, regex, or type.
|
|
230
228
|
- [**Recursive comparison**](https://solganis.github.io/assertpy2/guides/assertions/#recursive-comparison-tolerance--custom-comparators): `is_equal_to()` with `tolerance`, `comparators`, or `ignore_null` for nested structures.
|
|
231
229
|
- [**Extracting**](https://solganis.github.io/assertpy2/guides/assertions/#extracting-attributes-from-objects): flatten collections on attributes with `filter` and `sort` support.
|
|
230
|
+
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
232
231
|
|
|
233
232
|
**Testing**
|
|
234
233
|
|
|
@@ -236,17 +235,28 @@ is open to anything else you build.
|
|
|
236
235
|
- [**Polling assertions**](https://solganis.github.io/assertpy2/guides/testing/#async-assertions): `eventually()` (async) / `eventually_sync()` (blocking) retry for eventual consistency, with a convergence trace on timeout.
|
|
237
236
|
- [**Expected exceptions**](https://solganis.github.io/assertpy2/guides/errors/#expected-exceptions): `raises().when_called_with()`, walk the cause chain (`caused_by()`, `has_root_cause()`), search an `ExceptionGroup` (`contains_error()`, `errors()`, `error_of()`), or pivot to the object (`raised()`).
|
|
238
237
|
- [**HTTP responses**](https://solganis.github.io/assertpy2/recipes/#test-an-http-api-response): assert on the response itself and every failure names the request it came from, with `decoded_as_json()` to step into the body. No client library is a dependency.
|
|
238
|
+
- [**Snapshot testing**](https://solganis.github.io/assertpy2/guides/testing/#snapshot-testing): an external JSON file, an [inline](https://solganis.github.io/assertpy2/guides/testing/#inline-snapshots) value recorded into the test source, or a [value-tolerant contract](https://solganis.github.io/assertpy2/guides/testing/#contract-snapshots), all updated with `--assertpy2-snapshot-update`.
|
|
239
|
+
- [**OpenAPI response contracts**](https://solganis.github.io/assertpy2/reference/json/#assertpy2.json_mixin.JsonMixin.conforms_to_openapi): `conforms_to_openapi()` checks a JSON body against an operation's response schema, reporting every violation with its JSON path.
|
|
240
|
+
|
|
241
|
+
**Failure reporting**
|
|
242
|
+
|
|
239
243
|
- [**Structured errors**](https://solganis.github.io/assertpy2/guides/errors/#structured-errors): `AssertionFailure` carries `.actual`, `.expected` and `.diff`, and the diff renders into the message, so it shows off pytest too.
|
|
240
244
|
- [**Assertions as values**](https://solganis.github.io/assertpy2/guides/errors/#asking-instead-of-asserting): `check()` runs the next assertion for its verdict instead of raising, handing back an `AssertionOutcome`.
|
|
241
245
|
- [**Rich pytest diffs**](https://solganis.github.io/assertpy2/guides/errors/#rich-pytest-diffs): recursive diffs across containers, dataclasses, attrs and Pydantic models, with intra-line carets for strings.
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
|
|
247
|
+
**Plugin for pytest**
|
|
248
|
+
|
|
249
|
+
Set from the command line or from `[tool.pytest.ini_options]`, not from a call.
|
|
250
|
+
|
|
251
|
+
- [**Failure clustering**](https://solganis.github.io/assertpy2/guides/errors/#what-the-failures-had-in-common): forty failing tests are usually not forty problems. Where three or more differ at the same place, the run ends with a line saying where. On by default, `assertpy2_failure_clusters = "off"` turns it off.
|
|
252
|
+
- [**Diagnostic profiles**](https://solganis.github.io/assertpy2/guides/errors/#configuration): `assertpy2_profile` turns the guards below on in one line. `compatible` (default) leaves them off, `safe` warns, `strict` fails the tests they find. A setting you name yourself still wins.
|
|
253
|
+
- [**Vacuous-assertion guard**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-checked-nothing): `--assertpy2-vacuous` warns when a universal assertion passes over an empty collection, having checked nothing.
|
|
254
|
+
- [**Dangling-assertion detector**](https://solganis.github.io/assertpy2/guides/assertions/#assertions-that-never-ran): `--assertpy2-dangling` warns when a chain builds an assertion and never runs it. `assert assert_that(x).is_positive` passes on any value, and neither ruff nor coverage sees it.
|
|
244
255
|
|
|
245
256
|
**Extensibility**
|
|
246
257
|
|
|
247
258
|
- [**Custom matchers**](https://solganis.github.io/assertpy2/guides/matchers/#custom-matchers): `register_matcher()` composes existing ones, `BaseMatcher` carries its own predicate. Both compose with `&`, `|`, `~`.
|
|
248
259
|
- [**Custom assertions**](https://solganis.github.io/assertpy2/extending/custom-assertions/): `add_extension()` adds a method to the builder.
|
|
249
|
-
- [**Regex group extraction**](https://solganis.github.io/assertpy2/guides/data/#regex-group-extraction): `extracting_group()` and `matches_with_groups()` for regex captures.
|
|
250
260
|
|
|
251
261
|
<h2 align="center"><a href="https://solganis.github.io/assertpy2/extending/integrations/">Integrations</a></h2>
|
|
252
262
|
|
|
@@ -188,8 +188,7 @@ def stable_repr(value: object, _seen: frozenset[int] = frozenset()) -> str:
|
|
|
188
188
|
try:
|
|
189
189
|
if isinstance(value, (set, frozenset)):
|
|
190
190
|
members = sorted(stable_repr(member, inner) for member in value)
|
|
191
|
-
# an empty one falls back to `repr
|
|
192
|
-
# read apart
|
|
191
|
+
# an empty one falls back to `repr`: `{}` is a dict, so `set()` and the `frozenset()` it equals read apart
|
|
193
192
|
return "{" + ", ".join(members) + "}" if value else _safe_repr(value)
|
|
194
193
|
if isinstance(value, dict):
|
|
195
194
|
pairs = (f"{stable_repr(key, inner)}: {stable_repr(item, inner)}" for key, item in value.items())
|
|
@@ -287,8 +286,7 @@ def is_well_formed(key: Signature) -> bool:
|
|
|
287
286
|
)
|
|
288
287
|
if key.steps:
|
|
289
288
|
return False
|
|
290
|
-
#
|
|
291
|
-
# would print the `None` standing for a missing item
|
|
289
|
+
# a pair of values only for the kinds whose fields hold them, or a `contains` payload prints a `None`
|
|
292
290
|
if key.label:
|
|
293
291
|
return not key.values
|
|
294
292
|
return len(key.values) == 2 and key.where in _VALUES_ARE_VALUES
|
|
@@ -327,8 +325,7 @@ def clusters(
|
|
|
327
325
|
"""
|
|
328
326
|
if total_failures <= 0:
|
|
329
327
|
return []
|
|
330
|
-
# keyed on the node id: `pytest-rerunfailures`
|
|
331
|
-
# a cluster of six over two tests
|
|
328
|
+
# keyed on the node id: `pytest-rerunfailures` retries printed a cluster of six over two tests
|
|
332
329
|
grouped: dict[Signature, dict[str, None]] = {}
|
|
333
330
|
actuals: dict[Signature, dict[str, None]] = {}
|
|
334
331
|
expecteds: dict[Signature, dict[str, None]] = {}
|
|
@@ -473,21 +470,18 @@ def render(
|
|
|
473
470
|
lines.append(f"{collect_errors} collection error{'' if one else 's'}, not counted below")
|
|
474
471
|
for cluster, where in zip(shown, _headings(shown), strict=True):
|
|
475
472
|
lines.append(_headline(cluster, where, total_failures))
|
|
476
|
-
# a cluster keyed on its diagnostic
|
|
477
|
-
# it are the ones that vary between its failures rather than the thing they share
|
|
473
|
+
# a cluster keyed on its diagnostic says the difference in words; the values are what varies inside it
|
|
478
474
|
if cluster.signature.label:
|
|
479
475
|
lines.append(f" {cluster.signature.label}")
|
|
480
476
|
else:
|
|
481
477
|
lines.append(f" actual: {_side(cluster.actuals)}")
|
|
482
478
|
lines.append(f" expected: {_side(cluster.expecteds)}")
|
|
483
479
|
if omitted:
|
|
484
|
-
# said rather than dropped: five of eleven
|
|
485
|
-
#
|
|
480
|
+
# said rather than dropped: "five of eleven" silently reads as "these are the causes". "more" rather
|
|
481
|
+
# than "smaller", since equal-sized clusters are ordered by signature
|
|
486
482
|
lines.append(f"{len(omitted)} more cluster{'' if len(omitted) == 1 else 's'} not shown")
|
|
487
483
|
covered = len({nodeid for cluster in found for nodeid in cluster.nodeids})
|
|
488
484
|
if covered < total_failures:
|
|
489
|
-
# what the number measures,
|
|
490
|
-
# a floor of three are related, and the summary declined to print them, which is not the same as
|
|
491
|
-
# having found nothing about them
|
|
485
|
+
# what the number measures, not "not clustered": the summary declined to print a pair under the floor
|
|
492
486
|
lines.append(f"{total_failures - covered} of {total_failures} outside any cluster of {minimum}")
|
|
493
487
|
return lines
|