assertpy2 2.30.0__tar.gz → 2.32.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.30.0 → assertpy2-2.32.0}/PKG-INFO +1 -1
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_builder_check_typing.py +7 -4
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_capable_typing.py +19 -8
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_check_typing.py +5 -4
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_contract.py +199 -71
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_negated_typing.py +21 -16
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_poll_typing.py +23 -16
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_require.py +17 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_typing.py +5 -4
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_matcher_impls.py +4 -6
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_snapshot_codec.py +3 -2
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/assertpy.py +232 -40
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/errors.py +18 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/json_mixin.py +9 -7
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/matchers.py +3 -2
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/pytest_plugin.py +391 -58
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/snapshot.py +2 -2
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/string.py +32 -34
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/concepts/stability.md +1 -1
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/concepts/type-safety.md +10 -2
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/extending/integrations.md +4 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/guides/assertions.md +11 -6
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/guides/errors.md +63 -4
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/guides/matchers.md +1 -1
- {assertpy2-2.30.0 → assertpy2-2.32.0}/pyproject.toml +43 -4
- {assertpy2-2.30.0 → assertpy2-2.32.0}/scripts/generate_check_protocols.py +1 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/scripts/generate_poll_protocols.py +2 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/api_snapshot.json +36 -29
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/conftest.py +4 -0
- assertpy2-2.32.0/tests/test_compiled_patterns.py +209 -0
- assertpy2-2.32.0/tests/test_exact_oracle.py +180 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_pytest_plugin.py +782 -8
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_refusal_frames.py +4 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_snapshots.py +52 -4
- assertpy2-2.32.0/tests/test_soft_summary_blocks.py +356 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_string.py +11 -2
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_structural.py +445 -27
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing.py +14 -2
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_chain_corpus.py +2 -2
- assertpy2-2.32.0/tests/test_warn_on_failure.py +158 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_cases.py +6 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_chain_corpus_baseline.json +0 -198
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_negative_baseline.py +4 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/.gitignore +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/CONTRIBUTING.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/LICENSE +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/README.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/SECURITY.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/__init__.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_clustering.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_dangling.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/__init__.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_compare.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_compat.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_diff.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_equality.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_introspection.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_membership.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_mixin_base.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_operations.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_ordering.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_pairing.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_path.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_size.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_engine/_text.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_hints.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_inline.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/_satisfies.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/async_assertions.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/base.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/behave_matchers.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/bytes_mixin.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/collection.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/contains.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/dataframe.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/date.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/dict.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/dynamic.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/exception.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/extracting.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/file.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/helpers.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/http_mixin.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/numeric.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/outcome.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/py.typed +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/assertpy2/warning.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/benchmarks/test_perf.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-equal.png +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-equal.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-gallery.png +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-match.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-sequence.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-set.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/assets/diff-string.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/extending/custom-assertions.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/getting-started/comparison.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/getting-started/migration.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/getting-started/quickstart.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/guides/data.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/guides/fluent.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/guides/testing.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/index.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/logo-dark.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/logo.svg +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/recipes.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/async.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/bytes.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/collections.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/containment.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/core.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/dataframes.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/dates.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/dicts.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/dynamic.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/entry-points.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/errors.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/exceptions.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/extracting.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/files.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/http.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/json.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/matchers.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/numbers.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/overview.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/snapshots.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/strings.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/reference/warnings.md +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/docs/stylesheets/extra.css +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/mkdocs.yml +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/mkdocs_hooks.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/scripts/mutation_report.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/scripts/regenerate.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/api_surface.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/chain_corpus.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/chain_model.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/chain_surfaces.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/LICENSE +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/date-time.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/date.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/duration.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/email.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/hostname.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/multipleOf.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/time.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/data/json_schema_test_suite/uri.json +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/docs_fixtures.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/format_corpus.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/group_compat.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/poll_parity_baseline.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/poll_parity_cases.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/pyright_baseline.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_api_compatibility.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_api_vocabulary.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_architecture_doc.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_async.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_async_predicates.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_attrs_support.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_behave_matchers.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_bool.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_boundary_cases.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_bytes.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_call_awaitables.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_callable.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_capable_protocol.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_chain_machine.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_chaining.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_check.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_check_protocols.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_class.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_class_in_spec.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_clustering.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_collection.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_complexity.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_core.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_custom_dict.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_custom_list.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_dangling.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_dataframe.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_datetime.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_declared_floors.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_deep_nesting.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_description.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_dict.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_dict_compare.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_diff_laws.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_difference_places.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_docs_examples.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_docs_typing.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_duality.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_dynamic.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_equality_signals.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_equals.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_errors.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_evaluation_core.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_exception_context.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_exception_fields.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_expected_contract.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_expected_exception.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_expected_warning.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_extensions.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_extracting.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_fail.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_file.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_fixed_width_integers.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_grouped_soft.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_hints.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_hostile_values.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_http.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_import_cost.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_in.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_inline_record.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_inline_snapshot.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_integer_arguments.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_iterable_cluster.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_json.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_list.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_match_result.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_matcher_algebra.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_matcher_parity.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_matcher_registry.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_matchers.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_membership_failures.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_membership_family.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_message_elision.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_message_values.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_missing_names.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_moment_relations.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_namedtuple.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_negated_protocols.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_none.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_not.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_number_zoo.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_numbers.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_numeric_questions.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_one_shot_chains.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_30_keywords.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_contract.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_format_suite.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_formats.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_multiple_of.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_oracle.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_openapi_part_read.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_operation_contract.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_optional_integration_contracts.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_overload_order.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_overloads.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_pin_coverage.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_pipeline.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_poll_parity.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_poll_protocols.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_prerequisites.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_property_attrs.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_property_based.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_protocol_parity.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_public_surface.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_pyright_baseline.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_recursive_assertion.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_recursive_compare.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_recursive_compare_attrs.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_recursive_compare_config.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_recursive_compare_pydantic.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_refusals.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_regex_groups.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_render_laws.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_requirement.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_rich_diff.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_same_as.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_satisfy.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_skip_guard.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_soft.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_soft_fail.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_strict_reach.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_structure_sentence.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_surface_conformance.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_sync_eventually.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_text_relations.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_traceback.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_type.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_type_expression_failures.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_claims.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_completeness.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_conformance.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_from_a_wheel.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_harness.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_http.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_integrations.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_negative.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_typing_promises.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_unrenderable_values.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_vacuity_contract.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_verdict_corpus.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_verdicts_held_to_python.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/test_warn.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_harness.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_http.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_integrations.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_integrations_baseline.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/typing_promises.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/verdict_corpus.py +0 -0
- {assertpy2-2.30.0 → assertpy2-2.32.0}/tests/verdict_golden.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.32.0
|
|
4
4
|
Summary: Fluent, fully typed assertion library for Python: your type checker sees only the assertions that fit the value, and every failure carries a structured diff. Soft assertions, polling, inline snapshots, OpenAPI contracts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
import datetime
|
|
26
26
|
import pathlib
|
|
27
|
+
import re
|
|
27
28
|
from collections.abc import Callable, Hashable, Iterable, Mapping, Sized
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
from typing import Any, Protocol, SupportsFloat, SupportsIndex, TypeVar, overload
|
|
@@ -155,9 +156,9 @@ if TYPE_CHECKING:
|
|
|
155
156
|
|
|
156
157
|
def ends_with_ignoring_case(self: _CheckAnyValue[str], suffix: str) -> AssertionOutcome: ...
|
|
157
158
|
|
|
158
|
-
def matches(self: _CheckAnyValue[str], pattern: str) -> AssertionOutcome: ...
|
|
159
|
+
def matches(self: _CheckAnyValue[str], pattern: str | re.Pattern[str]) -> AssertionOutcome: ...
|
|
159
160
|
|
|
160
|
-
def does_not_match(self: _CheckAnyValue[str], pattern: str) -> AssertionOutcome: ...
|
|
161
|
+
def does_not_match(self: _CheckAnyValue[str], pattern: str | re.Pattern[str]) -> AssertionOutcome: ...
|
|
161
162
|
|
|
162
163
|
def is_alpha(self: _CheckAnyValue[str]) -> AssertionOutcome: ...
|
|
163
164
|
|
|
@@ -571,9 +572,11 @@ if TYPE_CHECKING:
|
|
|
571
572
|
allow_empty: bool = ...,
|
|
572
573
|
) -> AssertionOutcome: ...
|
|
573
574
|
|
|
574
|
-
def extracting_group(
|
|
575
|
+
def extracting_group(
|
|
576
|
+
self: _CheckAnyValue[str], pattern: str | re.Pattern[str], group: int | str = ...
|
|
577
|
+
) -> AssertionOutcome: ...
|
|
575
578
|
|
|
576
|
-
def matches_with_groups(self: _CheckAnyValue[str], pattern: str) -> AssertionOutcome: ...
|
|
579
|
+
def matches_with_groups(self: _CheckAnyValue[str], pattern: str | re.Pattern[str]) -> AssertionOutcome: ...
|
|
577
580
|
|
|
578
581
|
@overload
|
|
579
582
|
def exists(self: _CheckAnyValue[str] | _CheckAnyValue[pathlib.Path]) -> AssertionOutcome: ...
|
|
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
import datetime
|
|
26
26
|
import logging
|
|
27
|
+
import re
|
|
27
28
|
from collections.abc import Callable, Iterable, Mapping, Sized
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
from typing import Any, Protocol, SupportsFloat, SupportsIndex, TypeVar, overload
|
|
@@ -297,7 +298,9 @@ if TYPE_CHECKING:
|
|
|
297
298
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
298
299
|
def does_not_exist(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
299
300
|
def does_not_have_json_path(self, path: str) -> Self: ...
|
|
300
|
-
def does_not_match(
|
|
301
|
+
def does_not_match(
|
|
302
|
+
self: _CapableAssertion[str], pattern: str | re.Pattern[str]
|
|
303
|
+
) -> _CapableAssertion[_CapableT_co]: ...
|
|
301
304
|
def each(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = ...) -> Self: ...
|
|
302
305
|
def ends_with(self, suffix: object) -> Self: ...
|
|
303
306
|
def ends_with_ignoring_case(self: _CapableAssertion[str], suffix: str) -> _CapableAssertion[_CapableT_co]: ...
|
|
@@ -306,7 +309,7 @@ if TYPE_CHECKING:
|
|
|
306
309
|
def exists(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
307
310
|
def extracting(self, *names: object, **kwargs: object) -> _ListAssertion[Any]: ...
|
|
308
311
|
def extracting_group(
|
|
309
|
-
self: _CapableAssertion[str], pattern: str, group: int | str = ...
|
|
312
|
+
self: _CapableAssertion[str], pattern: str | re.Pattern[str], group: int | str = ...
|
|
310
313
|
) -> AssertionBuilder[Any]: ...
|
|
311
314
|
def filtered_on(self, predicate: Matcher[Any] | Callable[[Any], bool]) -> _ListAssertion[Any]: ...
|
|
312
315
|
def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) -> _ListAssertion[Any]: ...
|
|
@@ -427,7 +430,9 @@ if TYPE_CHECKING:
|
|
|
427
430
|
def is_whitespace(self: _CapableAssertion[str]) -> _CapableAssertion[_CapableT_co]: ...
|
|
428
431
|
def is_writable(self: _CapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
429
432
|
def is_zero(self) -> Self: ...
|
|
430
|
-
def matches(
|
|
433
|
+
def matches(
|
|
434
|
+
self: _CapableAssertion[str], pattern: str | re.Pattern[str]
|
|
435
|
+
) -> _CapableAssertion[_CapableT_co]: ...
|
|
431
436
|
def matches_contract_snapshot(self, id: str | None = ..., path: str = ...) -> Self: ...
|
|
432
437
|
def matches_error_tree(self, *expected: type | list[Any]) -> Self: ...
|
|
433
438
|
def matches_inline(
|
|
@@ -443,7 +448,9 @@ if TYPE_CHECKING:
|
|
|
443
448
|
def matches_json_schema(self, schema: dict[str, Any]) -> Self: ...
|
|
444
449
|
def matches_json_schema_from_file(self, path: str | Path) -> Self: ...
|
|
445
450
|
def matches_structure(self, spec: dict[Any, Any]) -> Self: ...
|
|
446
|
-
def matches_with_groups(
|
|
451
|
+
def matches_with_groups(
|
|
452
|
+
self: _CapableAssertion[str], pattern: str | re.Pattern[str]
|
|
453
|
+
) -> AssertionBuilder[Any]: ...
|
|
447
454
|
def none_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self: ...
|
|
448
455
|
def raised(self) -> AssertionBuilder[Any]: ...
|
|
449
456
|
def returned(self) -> AssertionBuilder[Any]: ...
|
|
@@ -548,7 +555,9 @@ if TYPE_CHECKING:
|
|
|
548
555
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
549
556
|
def does_not_exist(self: _NegatedCapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
550
557
|
def does_not_have_json_path(self, path: str) -> _CapableAssertion[_CapableT_co]: ...
|
|
551
|
-
def does_not_match(
|
|
558
|
+
def does_not_match(
|
|
559
|
+
self: _NegatedCapableAssertion[str], pattern: str | re.Pattern[str]
|
|
560
|
+
) -> _CapableAssertion[_CapableT_co]: ...
|
|
552
561
|
def each(
|
|
553
562
|
self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = ...
|
|
554
563
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
@@ -559,7 +568,7 @@ if TYPE_CHECKING:
|
|
|
559
568
|
def error_of(self, ex: type) -> _CapableAssertion[_CapableT_co]: ...
|
|
560
569
|
def exists(self: _NegatedCapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
561
570
|
def extracting_group(
|
|
562
|
-
self: _NegatedCapableAssertion[str], pattern: str, group: int | str = ...
|
|
571
|
+
self: _NegatedCapableAssertion[str], pattern: str | re.Pattern[str], group: int | str = ...
|
|
563
572
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
564
573
|
def has_byte_at(
|
|
565
574
|
self: _NegatedCapableAssertion[bytes | bytearray], index: SupportsIndex, expected: SupportsIndex
|
|
@@ -694,7 +703,9 @@ if TYPE_CHECKING:
|
|
|
694
703
|
def is_whitespace(self: _NegatedCapableAssertion[str]) -> _CapableAssertion[_CapableT_co]: ...
|
|
695
704
|
def is_writable(self: _NegatedCapableAssertion[_PathLike]) -> _CapableAssertion[_CapableT_co]: ...
|
|
696
705
|
def is_zero(self) -> _CapableAssertion[_CapableT_co]: ...
|
|
697
|
-
def matches(
|
|
706
|
+
def matches(
|
|
707
|
+
self: _NegatedCapableAssertion[str], pattern: str | re.Pattern[str]
|
|
708
|
+
) -> _CapableAssertion[_CapableT_co]: ...
|
|
698
709
|
def matches_contract_snapshot(
|
|
699
710
|
self, id: str | None = ..., path: str = ...
|
|
700
711
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
@@ -713,7 +724,7 @@ if TYPE_CHECKING:
|
|
|
713
724
|
def matches_json_schema_from_file(self, path: str | Path) -> _CapableAssertion[_CapableT_co]: ...
|
|
714
725
|
def matches_structure(self, spec: dict[Any, Any]) -> _CapableAssertion[_CapableT_co]: ...
|
|
715
726
|
def matches_with_groups(
|
|
716
|
-
self: _NegatedCapableAssertion[str], pattern: str
|
|
727
|
+
self: _NegatedCapableAssertion[str], pattern: str | re.Pattern[str]
|
|
717
728
|
) -> _CapableAssertion[_CapableT_co]: ...
|
|
718
729
|
def none_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> _CapableAssertion[_CapableT_co]: ...
|
|
719
730
|
def satisfies_exactly(
|
|
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING
|
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
import datetime
|
|
17
|
+
import re
|
|
17
18
|
from collections.abc import Callable, Hashable, Iterable, Mapping, Sized
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import Any, Protocol, SupportsFloat, SupportsIndex, TypeVar, overload
|
|
@@ -401,8 +402,8 @@ if TYPE_CHECKING:
|
|
|
401
402
|
def starts_with_ignoring_case(self, prefix: str) -> AssertionOutcome: ...
|
|
402
403
|
def ends_with(self, suffix: str) -> AssertionOutcome: ...
|
|
403
404
|
def ends_with_ignoring_case(self, suffix: str) -> AssertionOutcome: ...
|
|
404
|
-
def matches(self, pattern: str) -> AssertionOutcome: ...
|
|
405
|
-
def does_not_match(self, pattern: str) -> AssertionOutcome: ...
|
|
405
|
+
def matches(self, pattern: str | re.Pattern[str]) -> AssertionOutcome: ...
|
|
406
|
+
def does_not_match(self, pattern: str | re.Pattern[str]) -> AssertionOutcome: ...
|
|
406
407
|
def is_alpha(self) -> AssertionOutcome: ...
|
|
407
408
|
def is_digit(self) -> AssertionOutcome: ...
|
|
408
409
|
def is_lower(self) -> AssertionOutcome: ...
|
|
@@ -439,8 +440,8 @@ if TYPE_CHECKING:
|
|
|
439
440
|
def zip_satisfies(
|
|
440
441
|
self, other: Iterable[_Other], predicate: Callable[[str, _Other], object], *, allow_empty: bool = ...
|
|
441
442
|
) -> AssertionOutcome: ...
|
|
442
|
-
def extracting_group(self, pattern: str, group: int | str = ...) -> AssertionOutcome: ...
|
|
443
|
-
def matches_with_groups(self, pattern: str) -> AssertionOutcome: ...
|
|
443
|
+
def extracting_group(self, pattern: str | re.Pattern[str], group: int | str = ...) -> AssertionOutcome: ...
|
|
444
|
+
def matches_with_groups(self, pattern: str | re.Pattern[str]) -> AssertionOutcome: ...
|
|
444
445
|
@property
|
|
445
446
|
def not_(self) -> Self: ...
|
|
446
447
|
|
|
@@ -36,6 +36,7 @@ from typing import (
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
from ..errors import DiffEntry, Step
|
|
39
|
+
from ._introspection import definition_of
|
|
39
40
|
|
|
40
41
|
if TYPE_CHECKING:
|
|
41
42
|
from collections.abc import Callable
|
|
@@ -52,15 +53,15 @@ def _alias_sources(alias: object) -> list[tuple[object, ...]]:
|
|
|
52
53
|
return [tuple(path)] if path else []
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
def
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return keys
|
|
56
|
+
def _spellings(name: str, info: Any) -> list[object]:
|
|
57
|
+
"""Every key that spells a field: its name, the first key of each alias validation may read it by, and the alias
|
|
58
|
+
it is dumped under. Which of them validation reads is the config's to say (`_field_sources`)."""
|
|
59
|
+
keys: list[object] = [name]
|
|
60
|
+
for alias in (getattr(info, "alias", None), getattr(info, "validation_alias", None)):
|
|
61
|
+
if alias is not None:
|
|
62
|
+
keys += [source[0] for source in _alias_sources(alias)]
|
|
63
|
+
shown = getattr(info, "serialization_alias", None)
|
|
64
|
+
return [*keys, shown] if isinstance(shown, str) else keys
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def _field_sources(name: str, info: Any, config: Any) -> tuple[tuple[object, ...], ...]:
|
|
@@ -215,8 +216,9 @@ and how it was taken (the keys followed for a field or a key, ``None`` for a pos
|
|
|
215
216
|
collection that keeps no positions). A hop into a mapping's key, and one into what JSON text decodes to, is named
|
|
216
217
|
by nothing. Read out only where something is found (`_placed`)."""
|
|
217
218
|
|
|
218
|
-
_Found = tuple[_Hop, object]
|
|
219
|
-
"""An undeclared field: where it stands and the value sent there
|
|
219
|
+
_Found = tuple[_Hop, object] | tuple[_Hop, object, str]
|
|
220
|
+
"""An undeclared field: where it stands and the value sent there, and, where the key spells a field of the model, why
|
|
221
|
+
validation did not read it."""
|
|
220
222
|
|
|
221
223
|
_DECLARED: dict[tuple[object, object], tuple[Any, Any]] = {}
|
|
222
224
|
"""What a declaration says a container built from it holds, per declaration and kind of container: worked out once,
|
|
@@ -348,6 +350,20 @@ def _item_nodes(node: object, owner: object) -> tuple[tuple[Any, ...] | None, An
|
|
|
348
350
|
return None, _either([*every, *unknown])
|
|
349
351
|
|
|
350
352
|
|
|
353
|
+
def _missing_is_its_own(cls: Any) -> bool:
|
|
354
|
+
"""Whether the enum *cls* answers a value it does not name by a `_missing_` other than `Enum`'s.
|
|
355
|
+
|
|
356
|
+
Its constructor calls that method, and a schema names it only where pydantic calls it in the constructor's
|
|
357
|
+
place. A class that cannot be read says yes.
|
|
358
|
+
"""
|
|
359
|
+
try:
|
|
360
|
+
found = definition_of(cls, "_missing_")
|
|
361
|
+
# no class: its tree is read off the slots of `type`, so nothing the class or its metaclass says is asked
|
|
362
|
+
except TypeError:
|
|
363
|
+
return True
|
|
364
|
+
return found is None or found[0] is not enum.Enum
|
|
365
|
+
|
|
366
|
+
|
|
351
367
|
_OWN_CODE: dict[type, tuple[object, bool]] = {}
|
|
352
368
|
"""Per model class, beside the validator it was read with: whether validating by it runs code that is not pydantic's.
|
|
353
369
|
At most 256 classes (`_remember`)."""
|
|
@@ -355,10 +371,12 @@ At most 256 classes (`_remember`)."""
|
|
|
355
371
|
|
|
356
372
|
def runs_own_code(model: Any) -> bool:
|
|
357
373
|
"""Whether validation by *model* runs code of its own anywhere in its schema: a validator, a `model_post_init`
|
|
358
|
-
(private attributes bring one), a custom `__init__`, a dataclass `__post_init__`, a called class
|
|
374
|
+
(private attributes bring one), a custom `__init__`, a dataclass `__post_init__`, a called class, a named tuple,
|
|
375
|
+
whose class is called as well, an enum's own `_missing_`.
|
|
359
376
|
|
|
360
377
|
Pydantic's own validation never changes the payload it is given, so only such a model can leave it other than
|
|
361
|
-
it was sent. A
|
|
378
|
+
it was sent. A default factory that takes no data is not counted: validation hands it nothing of the payload.
|
|
379
|
+
A schema that cannot be read says yes.
|
|
362
380
|
"""
|
|
363
381
|
validator = getattr(model, "__pydantic_validator__", None)
|
|
364
382
|
known = _OWN_CODE.get(model)
|
|
@@ -374,11 +392,12 @@ def runs_own_code(model: Any) -> bool:
|
|
|
374
392
|
kind = node.get("type")
|
|
375
393
|
answer = isinstance(kind, str) and bool(
|
|
376
394
|
kind.startswith("function-")
|
|
377
|
-
or kind
|
|
395
|
+
or kind in ("call", "named-tuple")
|
|
378
396
|
or node.get("post_init")
|
|
379
397
|
or node.get("custom_init")
|
|
380
398
|
or node.get("default_factory_takes_data")
|
|
381
399
|
or node.get("missing")
|
|
400
|
+
or (kind == "enum" and _missing_is_its_own(node.get("cls")))
|
|
382
401
|
or callable(node.get("discriminator"))
|
|
383
402
|
)
|
|
384
403
|
# a serialiser runs at dump time, metadata is never validated, a default may hold itself
|
|
@@ -719,8 +738,37 @@ def _adapter(declared: _Declared) -> Any:
|
|
|
719
738
|
|
|
720
739
|
_FieldReads = tuple[tuple[str, tuple[tuple[object, ...], ...], _Declared], ...]
|
|
721
740
|
|
|
722
|
-
|
|
723
|
-
|
|
741
|
+
_Sources = tuple[tuple[object, ...], ...]
|
|
742
|
+
_KeyReads = tuple[frozenset[object], tuple[_Sources, ...], dict[object, _Sources]]
|
|
743
|
+
"""Which keys of a payload a model or a record reads: the keys it reads wherever they are sent, the sources of each
|
|
744
|
+
field that may be read from another place, and every key that spells a field beside where validation reads that
|
|
745
|
+
field."""
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _key_reads(fields: collections.abc.Iterable[tuple[_Sources, collections.abc.Iterable[object]]]) -> _KeyReads:
|
|
749
|
+
"""The keys the *fields* are read from, each given as its sources in the order validation tries them and the
|
|
750
|
+
keys that spell it.
|
|
751
|
+
|
|
752
|
+
A field is read from the first of its sources a payload holds whole. The first of them, where it is a key and
|
|
753
|
+
no path, is read wherever it is sent. A later one is read only where those before it are absent, and a path only
|
|
754
|
+
where it leads somewhere, so those are told per payload (`_unread`): of two spellings sent validation drops the
|
|
755
|
+
later, and the key a path starts at is dropped where the path leads nowhere.
|
|
756
|
+
"""
|
|
757
|
+
sure: set[object] = set()
|
|
758
|
+
chosen: list[_Sources] = []
|
|
759
|
+
spelled: dict[object, _Sources] = {}
|
|
760
|
+
for sources, spellings in fields:
|
|
761
|
+
if sources and len(sources[0]) == 1:
|
|
762
|
+
sure.add(sources[0][0])
|
|
763
|
+
if len(sources) != 1 or len(sources[0]) != 1:
|
|
764
|
+
chosen.append(sources)
|
|
765
|
+
for key in (*(source[0] for source in sources), *spellings):
|
|
766
|
+
spelled.setdefault(key, sources)
|
|
767
|
+
return frozenset(sure), tuple(chosen), spelled
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
_READS: dict[type, tuple[object, _KeyReads, _FieldReads, _Declared | None]] = {}
|
|
771
|
+
"""Per model class, with the validator they were read beside: the keys it reads, where each field is read, and
|
|
724
772
|
what a typed `__pydantic_extra__` declares its extra values as.
|
|
725
773
|
|
|
726
774
|
Worked out per level of every payload, it was most of the walk: a list of 1000 nested models asked for it 1000 times.
|
|
@@ -729,7 +777,7 @@ A rebuild replaces the validator, and with it what the class reads, so an entry
|
|
|
729
777
|
was made with. At most 256 classes are kept, so models made on the fly cannot grow it without bound."""
|
|
730
778
|
|
|
731
779
|
|
|
732
|
-
def _reads_of(model: Any) -> tuple[
|
|
780
|
+
def _reads_of(model: Any) -> tuple[_KeyReads, _FieldReads, _Declared | None]:
|
|
733
781
|
validator = getattr(model, "__pydantic_validator__", None)
|
|
734
782
|
known = _READS.get(model)
|
|
735
783
|
if known is None or known[0] is not validator:
|
|
@@ -742,7 +790,10 @@ def _reads_of(model: Any) -> tuple[frozenset[str], _FieldReads, _Declared | None
|
|
|
742
790
|
if _may_hold_model(getattr(info, "annotation", None)) or _keyed_under(nodes.get(name), model)
|
|
743
791
|
)
|
|
744
792
|
extras = _extra_values(model, nodes.get("__pydantic_extra__")) if config.get("extra") == "allow" else None
|
|
745
|
-
|
|
793
|
+
keyed = _key_reads(
|
|
794
|
+
(_field_sources(name, info, config), _spellings(name, info)) for name, info in model.model_fields.items()
|
|
795
|
+
)
|
|
796
|
+
known = (validator, keyed, fields, extras)
|
|
746
797
|
if model in _READS or len(_READS) < 256:
|
|
747
798
|
_READS[model] = known
|
|
748
799
|
return known[1], known[2], known[3]
|
|
@@ -789,10 +840,10 @@ def _extra_values(model: Any, node: object) -> _Declared | None:
|
|
|
789
840
|
|
|
790
841
|
|
|
791
842
|
_RecordReads = tuple[
|
|
792
|
-
frozenset[str], tuple[tuple[str, tuple[tuple[object, ...], ...], _Declared | None], ...], _Declared
|
|
843
|
+
_KeyReads, frozenset[str], bool, tuple[tuple[str, tuple[tuple[object, ...], ...], _Declared | None], ...], _Declared
|
|
793
844
|
]
|
|
794
|
-
"""For a dataclass or a `TypedDict`: the payload keys it
|
|
795
|
-
what it declares, and the class as a declaration of its own."""
|
|
845
|
+
"""For a dataclass or a `TypedDict`: the payload keys it reads, the names of its fields, whether it keeps extras, the
|
|
846
|
+
fields worth walking with where each is read and what it declares, and the class as a declaration of its own."""
|
|
796
847
|
|
|
797
848
|
_RECORDS: dict[tuple[object, object], tuple[object, _RecordReads]] = {}
|
|
798
849
|
"""Per dataclass or `TypedDict`, by its schema or its class, and the class whose schema holds it, beside the schema or
|
|
@@ -804,9 +855,9 @@ def _record_reads(record: _Record, owner: object = None) -> _RecordReads:
|
|
|
804
855
|
|
|
805
856
|
The keys it takes and where each field is read come from the schema pydantic built, the one the walk came to it
|
|
806
857
|
by, the record's own, or the one of the class that declared it (*owner*). It holds what no annotation tells: an
|
|
807
|
-
alias written as text, the one of several `Field`s that wins, a field ``init=False`` leaves out
|
|
808
|
-
|
|
809
|
-
|
|
858
|
+
alias written as text, the one of several `Field`s that wins, a field ``init=False`` leaves out, and the config
|
|
859
|
+
that says whether a name is read, which reaches a `TypedDict` from the model above. Where no schema holds the
|
|
860
|
+
record, the annotations are read as written (`_written_fields`), and a name counts as read after its aliases.
|
|
810
861
|
|
|
811
862
|
What each field declares below it is the schema of that field, beside its annotation as pydantic resolved it or
|
|
812
863
|
as written where that can be read.
|
|
@@ -824,29 +875,30 @@ def _record_reads(record: _Record, owner: object = None) -> _RecordReads:
|
|
|
824
875
|
# only later pydantic names the class in the schema of a `TypedDict`
|
|
825
876
|
told = _written_fields(kind) if isinstance(kind, type) else ()
|
|
826
877
|
written = {name: (sources, annotation, held) for name, sources, annotation, held in told}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
878
|
+
found = _record_node(_Record(node, kind), holder)
|
|
879
|
+
said = found if isinstance(found, dict) else {}
|
|
880
|
+
# an older pydantic says it in the config alone, and a `TypedDict` that declares its extras in the schema alone
|
|
881
|
+
keeps = said.get("extra_behavior", (said.get("config") or {}).get("extra_fields_behavior")) == "allow"
|
|
882
|
+
reads = _schema_fields(_Record(found, kind), holder)
|
|
883
|
+
reads = reads or [(name, sources, held, None, ()) for name, (sources, _, held) in written.items()]
|
|
884
|
+
keyed, fields = [], []
|
|
885
|
+
for name, sources, held, schema, spellings in reads:
|
|
886
|
+
keyed.append((sources, (name, *spellings)))
|
|
833
887
|
annotation = written[name][1] if name in written else None
|
|
834
888
|
if held and (annotation is None or _may_hold_model(annotation) or _keyed_under(schema, holder)):
|
|
835
889
|
fields.append(
|
|
836
890
|
(name, sources, None if annotation is None and schema is None else (annotation, holder, schema))
|
|
837
891
|
)
|
|
838
|
-
|
|
892
|
+
names = frozenset(name for name, *_ in reads)
|
|
893
|
+
answer = (_key_reads(keyed), names, keeps, tuple(fields), (kind, None, None))
|
|
839
894
|
_remember(_RECORDS, key, read_with, answer, 256)
|
|
840
895
|
return answer
|
|
841
896
|
|
|
842
897
|
|
|
843
|
-
def
|
|
844
|
-
"""
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
A `TypedDict` is told by the id its ``ref`` ends in, since only later pydantic names its class there.
|
|
849
|
-
"""
|
|
898
|
+
def _record_node(record: _Record, holder: object) -> Any:
|
|
899
|
+
"""The schema of a record: the one the walk came to it by, else the one the schema of *holder* has for its class,
|
|
900
|
+
or ``None``. A `TypedDict` is told by the id its ``ref`` ends in, since only later pydantic names its class
|
|
901
|
+
there."""
|
|
850
902
|
found, kind = record
|
|
851
903
|
pending: list[Any] = [] if found is not None else [getattr(holder, "__pydantic_core_schema__", None)]
|
|
852
904
|
while pending and found is None:
|
|
@@ -858,7 +910,20 @@ def _schema_fields(record: _Record, holder: object) -> list[tuple[str, tuple[tup
|
|
|
858
910
|
pending.extend(value for key, value in node.items() if key not in ("serialization", "metadata", "default"))
|
|
859
911
|
elif isinstance(node, (list, tuple)):
|
|
860
912
|
pending.extend(node)
|
|
861
|
-
|
|
913
|
+
return found
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _schema_fields(
|
|
917
|
+
record: _Record, holder: object
|
|
918
|
+
) -> list[tuple[str, tuple[tuple[object, ...], ...], bool, Any, tuple[object, ...]]]:
|
|
919
|
+
"""Each key the schema says a record takes: its name, where it is read (its alias paths, then its name, each
|
|
920
|
+
where the record's config reads by it), whether the record holds it, which an `InitVar` is not, the schema of its
|
|
921
|
+
value, and the other keys that spell it. Empty where no schema holds the record (`_record_node`).
|
|
922
|
+
"""
|
|
923
|
+
found = _record_node(record, holder)
|
|
924
|
+
config = (found.get("config") or {}) if isinstance(found, dict) else {}
|
|
925
|
+
by_alias, by_name = config.get("validate_by_alias") is not False, config.get("validate_by_name")
|
|
926
|
+
by_name = by_name if by_name is not None else config.get("populate_by_name")
|
|
862
927
|
while isinstance(found, dict) and found.get("type") not in ("typed-dict", "dataclass-args"):
|
|
863
928
|
found = found.get("schema")
|
|
864
929
|
if not isinstance(found, dict):
|
|
@@ -870,11 +935,11 @@ def _schema_fields(record: _Record, holder: object) -> list[tuple[str, tuple[tup
|
|
|
870
935
|
if field.get("init") is not False:
|
|
871
936
|
alias = field.get("validation_alias")
|
|
872
937
|
paths = [alias] if isinstance(alias, str) or not alias or not isinstance(alias[0], list) else alias
|
|
873
|
-
|
|
874
|
-
|
|
938
|
+
aliases = [(path,) if isinstance(path, str) else tuple(path) for path in paths if path]
|
|
939
|
+
read_at = (*(aliases if by_alias else ()), *([(name,)] if by_name or not aliases else ()))
|
|
875
940
|
shown = field.get("serialization_alias")
|
|
876
|
-
|
|
877
|
-
reads.append((name, read_at, not field.get("init_only"), field.get("schema")))
|
|
941
|
+
spellings = (*(path[0] for path in aliases), *([shown] if isinstance(shown, str) else ()))
|
|
942
|
+
reads.append((name, read_at, not field.get("init_only"), field.get("schema"), spellings))
|
|
878
943
|
return reads
|
|
879
944
|
|
|
880
945
|
|
|
@@ -952,6 +1017,33 @@ def _raw_field(
|
|
|
952
1017
|
return None, None
|
|
953
1018
|
|
|
954
1019
|
|
|
1020
|
+
def _unread(left: list[object], payload: collections.abc.Mapping, reads: _KeyReads) -> list[object]:
|
|
1021
|
+
"""Those of the keys *left*, which no field is sure to read, that no field was read from in *payload*: the ones
|
|
1022
|
+
validation dropped, or kept as extras."""
|
|
1023
|
+
read = {source[0] for sources in reads[1] for source in [_raw_field(payload, sources)[0]] if source}
|
|
1024
|
+
return [key for key in left if key not in read]
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _undeclared(payload: collections.abc.Mapping, key: object, path: _Hop, reads: _KeyReads) -> _Found:
|
|
1028
|
+
"""The find for a *key* of *payload* validation dropped, with the reason where the key spells a field: the
|
|
1029
|
+
source its field was read from in its place, or the ones validation reads it from where the payload sent none."""
|
|
1030
|
+
sources = reads[2].get(key)
|
|
1031
|
+
if not sources:
|
|
1032
|
+
return (path, key, (key,)), payload[key]
|
|
1033
|
+
read, _ = _raw_field(payload, sources)
|
|
1034
|
+
reason = (
|
|
1035
|
+
f"its field was read from <{_spelt(read)}>"
|
|
1036
|
+
if read is not None
|
|
1037
|
+
else f"validation reads its field from {' or '.join(f'<{_spelt(source)}>' for source in sources)}"
|
|
1038
|
+
)
|
|
1039
|
+
return (path, key, (key,)), payload[key], reason
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _spelt(source: tuple[object, ...]) -> str:
|
|
1043
|
+
"""A source as a failure names it: a key, and the keys and positions a path goes on by."""
|
|
1044
|
+
return "".join(f"[{step}]" if isinstance(step, int) else f".{step}" for step in source).removeprefix(".")
|
|
1045
|
+
|
|
1046
|
+
|
|
955
1047
|
def _placed(hop: _Hop) -> tuple[str, tuple[Step, ...]]:
|
|
956
1048
|
"""Where *hop* stands, as the text a failure names it by and as the steps that reach it in the payload."""
|
|
957
1049
|
hops = []
|
|
@@ -1023,7 +1115,7 @@ def contract_drift(
|
|
|
1023
1115
|
reads those items by position.
|
|
1024
1116
|
"""
|
|
1025
1117
|
model = type(instance)
|
|
1026
|
-
|
|
1118
|
+
reads, fields, extras = _reads_of(model)
|
|
1027
1119
|
replayed = _REPLAYED in _seen
|
|
1028
1120
|
if getattr(model, "__pydantic_root_model__", False):
|
|
1029
1121
|
return _declared_drift(payload, instance.root, path, _seen, fields[0][2], replayed) if fields else []
|
|
@@ -1034,10 +1126,14 @@ def contract_drift(
|
|
|
1034
1126
|
return []
|
|
1035
1127
|
seen = _seen | {(id(payload), id(instance))}
|
|
1036
1128
|
drift: list[_Found] = []
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1129
|
+
sure = reads[0]
|
|
1130
|
+
left = [key for key in payload if key not in sure]
|
|
1131
|
+
if left:
|
|
1132
|
+
left = _unread(left, payload, reads)
|
|
1133
|
+
if getattr(model, "model_config", {}).get("extra") == "allow":
|
|
1134
|
+
drift += _extras_drift(payload, instance, path, seen, extras, replayed, left)
|
|
1135
|
+
elif left:
|
|
1136
|
+
drift += [_undeclared(payload, key, path, reads) for key in left]
|
|
1041
1137
|
for name, sources, annotated in fields:
|
|
1042
1138
|
source, raw = _raw_field(payload, sources)
|
|
1043
1139
|
if source is None:
|
|
@@ -1060,15 +1156,15 @@ def _extras_drift(
|
|
|
1060
1156
|
seen: frozenset[tuple[int, int]],
|
|
1061
1157
|
declared: _Declared | None,
|
|
1062
1158
|
replayed: bool,
|
|
1063
|
-
|
|
1159
|
+
left: list[object],
|
|
1064
1160
|
) -> list[_Found]:
|
|
1065
1161
|
"""Drift under the extras of a model that allows them: built through a typed `__pydantic_extra__` and walked as it
|
|
1066
1162
|
*declared* them; untyped ones are the payload's own objects.
|
|
1067
1163
|
|
|
1068
1164
|
An extra the payload sent that the model no longer holds was built as that type and removed afterwards, by a
|
|
1069
1165
|
validator. What it was built into is gone, so it is validated again as the declared type and walked beside what
|
|
1070
|
-
that builds, or refused where that cannot tell (`_replayed`). *
|
|
1071
|
-
|
|
1166
|
+
that builds, or refused where that cannot tell (`_replayed`). *left* are the keys no field was read from, which
|
|
1167
|
+
are the extras the model was sent.
|
|
1072
1168
|
"""
|
|
1073
1169
|
held = getattr(instance, "__pydantic_extra__", None)
|
|
1074
1170
|
# a validator can leave anything there: what is no dict holds no extra
|
|
@@ -1080,9 +1176,7 @@ def _extras_drift(
|
|
|
1080
1176
|
for entry in _declared_drift(payload[key], value, (path, key, (key,)), seen, declared, replayed)
|
|
1081
1177
|
]
|
|
1082
1178
|
if declared is not None:
|
|
1083
|
-
gone: list[tuple[_Hop, object]] = [
|
|
1084
|
-
((path, key, (key,)), payload[key]) for key in payload if key not in own and key not in built
|
|
1085
|
-
]
|
|
1179
|
+
gone: list[tuple[_Hop, object]] = [((path, key, (key,)), payload[key]) for key in left if key not in built]
|
|
1086
1180
|
if any(_holds(part, _is_mapping, read_text=True) for _, part in gone):
|
|
1087
1181
|
reason = f"the model no longer holds {len(gone)} of the extras it was sent"
|
|
1088
1182
|
drift += _replayed(payload, gone, path, seen, reason, _listed(declared))
|
|
@@ -1154,10 +1248,11 @@ def exactness_failure(pairs: Any, *, carrier: str) -> tuple[str, list[DiffEntry]
|
|
|
1154
1248
|
return f"<the payload> {reason}", [DiffEntry(path=where or ".", steps=steps, actual=payload, expected=reason)]
|
|
1155
1249
|
if not drift:
|
|
1156
1250
|
return None
|
|
1157
|
-
placed = sorted(((*_placed(hop), sent) for hop, sent in drift), key=operator.itemgetter(0))
|
|
1158
|
-
entries = [DiffEntry(path=where, steps=steps, actual=sent, absent="expected") for where, steps, sent in placed]
|
|
1251
|
+
placed = sorted(((*_placed(hop), sent, why) for hop, sent, *why in drift), key=operator.itemgetter(0))
|
|
1252
|
+
entries = [DiffEntry(path=where, steps=steps, actual=sent, absent="expected") for where, steps, sent, _ in placed]
|
|
1159
1253
|
paths = [entry.path for entry in entries]
|
|
1160
|
-
|
|
1254
|
+
told = "".join(f"\n<{where}> is not read: {why[0]}" for where, _, _, why in placed if why)
|
|
1255
|
+
return f"{carrier}{len(entries)} undeclared field(s) the model does not declare: {paths}{told}", entries
|
|
1161
1256
|
|
|
1162
1257
|
|
|
1163
1258
|
def _value_drift(
|
|
@@ -1227,11 +1322,23 @@ def _record_drift(
|
|
|
1227
1322
|
owner: object,
|
|
1228
1323
|
) -> list[_Found]:
|
|
1229
1324
|
"""Drift under a dataclass or a `TypedDict`, which drop an undeclared key as a model does: the keys *raw* holds
|
|
1230
|
-
that the record neither
|
|
1231
|
-
fields it *built*.
|
|
1232
|
-
|
|
1325
|
+
that the record neither read a field from nor keeps among its *extras*, where it allows them, and what lies under
|
|
1326
|
+
the fields it *built*.
|
|
1327
|
+
|
|
1328
|
+
A key that is the name of a field is no extra by being in what was built, where every field is: it is kept only
|
|
1329
|
+
by a record that keeps extras, which puts what it was sent there over the field.
|
|
1330
|
+
"""
|
|
1331
|
+
reads, names, keeps, fields, _ = _record_reads(record, owner)
|
|
1233
1332
|
replayed = _REPLAYED in seen
|
|
1234
|
-
|
|
1333
|
+
sure = reads[0]
|
|
1334
|
+
left = [key for key in raw if key not in sure]
|
|
1335
|
+
drift: list[_Found] = []
|
|
1336
|
+
if left:
|
|
1337
|
+
drift = [
|
|
1338
|
+
_undeclared(raw, key, path, reads)
|
|
1339
|
+
for key in _unread(left, raw, reads)
|
|
1340
|
+
if (not keeps if key in names else key not in extras)
|
|
1341
|
+
]
|
|
1235
1342
|
for name, sources, declared in fields:
|
|
1236
1343
|
source, part = _raw_field(raw, sources)
|
|
1237
1344
|
if source is not None and name in built:
|
|
@@ -1250,7 +1357,8 @@ def _unsure_drift(
|
|
|
1250
1357
|
"""Drift under a dict one of several declared `TypedDict`s may have built.
|
|
1251
1358
|
|
|
1252
1359
|
One whose `Literal` fields do not hold what the dict holds did not build it, where no code of the owner's could
|
|
1253
|
-
have rewritten them since
|
|
1360
|
+
have rewritten them since and no extra was put over them (`_put_over`). Where that leaves one, and nothing
|
|
1361
|
+
declared beside them may build a dict, the dict is
|
|
1254
1362
|
read as that record. Else nothing tells the rest apart, and the walk does not choose as validation did. A key the
|
|
1255
1363
|
payload sent that the dict holds neither under its name nor as the field one of them reads from it could have been
|
|
1256
1364
|
dropped by any, so it refuses; so does a field two of them read from different parts of the payload. What the
|
|
@@ -1258,12 +1366,7 @@ def _unsure_drift(
|
|
|
1258
1366
|
"""
|
|
1259
1367
|
records = unsure.records
|
|
1260
1368
|
if not runs_own_code(owner):
|
|
1261
|
-
|
|
1262
|
-
record
|
|
1263
|
-
for record in records
|
|
1264
|
-
if all(name not in built or built[name] in allowed for name, allowed in _literals(record, owner))
|
|
1265
|
-
]
|
|
1266
|
-
records = tuple(fitting)
|
|
1369
|
+
records = tuple(record for record in records if _may_have_built(raw, built, record, owner))
|
|
1267
1370
|
if len(records) == 1 and not unsure.beside:
|
|
1268
1371
|
return _record_drift(raw, built, built, path, seen, _Record(records[0], None), owner)
|
|
1269
1372
|
read_as, fields, other = _shared_reads(records, unsure.beside, owner)
|
|
@@ -1288,6 +1391,31 @@ def _unsure_drift(
|
|
|
1288
1391
|
return drift
|
|
1289
1392
|
|
|
1290
1393
|
|
|
1394
|
+
def _may_have_built(raw: collections.abc.Mapping, built: dict, record: Any, owner: object) -> bool:
|
|
1395
|
+
"""Whether the `TypedDict` schema *record* may have built the dict *built* from *raw*: each of its `Literal` fields
|
|
1396
|
+
holds a value it takes, or one a key of the payload was put over."""
|
|
1397
|
+
over = _put_over(raw, record, owner)
|
|
1398
|
+
return all(
|
|
1399
|
+
name not in built or name in over or built[name] in allowed for name, allowed in _literals(record, owner)
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
def _put_over(raw: collections.abc.Mapping, record: Any, owner: object) -> collections.abc.Collection[object]:
|
|
1404
|
+
"""The fields of the `TypedDict` schema *record* that a key of *raw* was put over, if it built the dict.
|
|
1405
|
+
|
|
1406
|
+
One that keeps extras keeps a key it read no field from, and where that key is the name of a field, as beside the
|
|
1407
|
+
alias the field was read from, it lands on what validation built there. What the dict holds under that name is
|
|
1408
|
+
then what was sent, and does not say that this one did not build it. One that drops extras had nothing put over
|
|
1409
|
+
its fields, so what another would have kept is nothing to it.
|
|
1410
|
+
"""
|
|
1411
|
+
reads, names, keeps, _, _ = _record_reads(_Record(record, None), owner)
|
|
1412
|
+
if not keeps:
|
|
1413
|
+
return ()
|
|
1414
|
+
sure = reads[0]
|
|
1415
|
+
left = [key for key in raw if key in names and key not in sure]
|
|
1416
|
+
return _unread(left, raw, reads) if left else left
|
|
1417
|
+
|
|
1418
|
+
|
|
1291
1419
|
def _literals(record: Any, owner: object) -> list[tuple[str, list[object]]]:
|
|
1292
1420
|
"""The fields of the `TypedDict` schema *record* that take nothing but the values a `Literal` lists, with those
|
|
1293
1421
|
values, ``None`` among them where the field takes it too."""
|
|
@@ -1296,7 +1424,7 @@ def _literals(record: Any, owner: object) -> list[tuple[str, list[object]]]:
|
|
|
1296
1424
|
if known is not None and known[0] is record:
|
|
1297
1425
|
return known[1]
|
|
1298
1426
|
found = []
|
|
1299
|
-
for name, _, _, schema in _schema_fields(_Record(record, None), owner):
|
|
1427
|
+
for name, _, _, schema, _ in _schema_fields(_Record(record, None), owner):
|
|
1300
1428
|
allowed: list[object] = []
|
|
1301
1429
|
while _schema_kind(schema) == "nullable":
|
|
1302
1430
|
allowed.append(None)
|
|
@@ -1326,7 +1454,7 @@ def _shared_reads(records: tuple[Any, ...], beside: tuple[Any, ...], owner: obje
|
|
|
1326
1454
|
read_at: dict[str, list[tuple[tuple[object, ...], ...]]] = {}
|
|
1327
1455
|
schemas: dict[str, list[Any]] = {}
|
|
1328
1456
|
for record in records:
|
|
1329
|
-
for name, sources, _, schema in _schema_fields(_Record(record, None), owner):
|
|
1457
|
+
for name, sources, _, schema, _ in _schema_fields(_Record(record, None), owner):
|
|
1330
1458
|
for source in sources:
|
|
1331
1459
|
read_as.setdefault(source[0], []).append(name)
|
|
1332
1460
|
read_at.setdefault(name, []).append(sources)
|
|
@@ -1579,7 +1707,7 @@ def _revalidated(
|
|
|
1579
1707
|
validate = getattr(model, "model_validate", None)
|
|
1580
1708
|
validate_json = getattr(model, "model_validate_json", None)
|
|
1581
1709
|
if validate is None and hasattr(model, "__dataclass_fields__"):
|
|
1582
|
-
adapter = _adapter(_record_reads(_Record(None, model))[
|
|
1710
|
+
adapter = _adapter(_record_reads(_Record(None, model))[4])
|
|
1583
1711
|
validate, validate_json = getattr(adapter, "validate_python", None), getattr(adapter, "validate_json", None)
|
|
1584
1712
|
if validate is None or validate_json is None:
|
|
1585
1713
|
raise UncheckableDriftError(path, reason, raw)
|