assertpy 1.1__tar.gz → 2.36.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.
- assertpy-2.36.0/.github/workflows/ci.yml +465 -0
- assertpy-2.36.0/.gitignore +34 -0
- assertpy-2.36.0/ARCHITECTURE.md +167 -0
- assertpy-2.36.0/CONTRIBUTING.md +163 -0
- {assertpy-1.1 → assertpy-2.36.0}/LICENSE +2 -1
- assertpy-2.36.0/PKG-INFO +324 -0
- assertpy-2.36.0/README.md +271 -0
- assertpy-2.36.0/SECURITY.md +24 -0
- assertpy-2.36.0/assertpy/__init__.py +87 -0
- assertpy-2.36.0/assertpy/_clustering.py +511 -0
- assertpy-2.36.0/assertpy/_dangling.py +505 -0
- assertpy-2.36.0/assertpy/_engine/__init__.py +1 -0
- assertpy-2.36.0/assertpy/_engine/_builder_check_typing.py +1369 -0
- assertpy-2.36.0/assertpy/_engine/_capable_typing.py +778 -0
- assertpy-2.36.0/assertpy/_engine/_check_typing.py +756 -0
- assertpy-2.36.0/assertpy/_engine/_compare.py +983 -0
- assertpy-2.36.0/assertpy/_engine/_compat.py +38 -0
- assertpy-2.36.0/assertpy/_engine/_contract.py +2003 -0
- assertpy-2.36.0/assertpy/_engine/_diff.py +1058 -0
- assertpy-2.36.0/assertpy/_engine/_equality.py +843 -0
- assertpy-2.36.0/assertpy/_engine/_introspection.py +729 -0
- assertpy-2.36.0/assertpy/_engine/_membership.py +522 -0
- assertpy-2.36.0/assertpy/_engine/_mixin_base.py +258 -0
- assertpy-2.36.0/assertpy/_engine/_negated_typing.py +2200 -0
- assertpy-2.36.0/assertpy/_engine/_operations.py +75 -0
- assertpy-2.36.0/assertpy/_engine/_ordering.py +884 -0
- assertpy-2.36.0/assertpy/_engine/_pairing.py +70 -0
- assertpy-2.36.0/assertpy/_engine/_path.py +158 -0
- assertpy-2.36.0/assertpy/_engine/_poll_typing.py +6055 -0
- assertpy-2.36.0/assertpy/_engine/_require.py +312 -0
- assertpy-2.36.0/assertpy/_engine/_size.py +31 -0
- assertpy-2.36.0/assertpy/_engine/_text.py +39 -0
- assertpy-2.36.0/assertpy/_engine/_typing.py +1227 -0
- assertpy-2.36.0/assertpy/_hints.py +1213 -0
- assertpy-2.36.0/assertpy/_html.py +439 -0
- assertpy-2.36.0/assertpy/_inline.py +484 -0
- assertpy-2.36.0/assertpy/_matcher_impls.py +1911 -0
- assertpy-2.36.0/assertpy/_passes.py +249 -0
- assertpy-2.36.0/assertpy/_renamed.py +64 -0
- assertpy-2.36.0/assertpy/_satisfies.py +835 -0
- assertpy-2.36.0/assertpy/_snapshot_codec.py +537 -0
- assertpy-2.36.0/assertpy/assertpy.py +2641 -0
- assertpy-2.36.0/assertpy/async_assertions.py +864 -0
- assertpy-2.36.0/assertpy/base.py +862 -0
- assertpy-2.36.0/assertpy/behave_matchers.py +96 -0
- assertpy-2.36.0/assertpy/bytes_mixin.py +158 -0
- assertpy-2.36.0/assertpy/collection.py +528 -0
- assertpy-2.36.0/assertpy/contains.py +1068 -0
- assertpy-2.36.0/assertpy/dataframe.py +200 -0
- assertpy-2.36.0/assertpy/date.py +327 -0
- assertpy-2.36.0/assertpy/dict.py +249 -0
- assertpy-2.36.0/assertpy/dynamic.py +222 -0
- assertpy-2.36.0/assertpy/errors.py +782 -0
- assertpy-2.36.0/assertpy/exception.py +838 -0
- assertpy-2.36.0/assertpy/extracting.py +266 -0
- assertpy-2.36.0/assertpy/file.py +324 -0
- assertpy-2.36.0/assertpy/helpers.py +747 -0
- assertpy-2.36.0/assertpy/http_mixin.py +218 -0
- assertpy-2.36.0/assertpy/json_mixin.py +1265 -0
- assertpy-2.36.0/assertpy/matchers.py +651 -0
- assertpy-2.36.0/assertpy/numeric.py +696 -0
- assertpy-2.36.0/assertpy/outcome.py +163 -0
- assertpy-2.36.0/assertpy/py.typed +0 -0
- assertpy-2.36.0/assertpy/pytest_plugin.py +1679 -0
- assertpy-2.36.0/assertpy/snapshot.py +1116 -0
- assertpy-2.36.0/assertpy/string.py +877 -0
- assertpy-2.36.0/assertpy/warning.py +137 -0
- assertpy-2.36.0/benchmarks/test_perf.py +492 -0
- assertpy-2.36.0/bridge/LICENSE +28 -0
- assertpy-2.36.0/bridge/README.md +17 -0
- assertpy-2.36.0/bridge/assertpy2/__init__.py +27 -0
- assertpy-2.36.0/bridge/assertpy2/assertpy.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/async_assertions.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/base.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/behave_matchers.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/bytes_mixin.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/collection.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/contains.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/dataframe.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/date.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/dict.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/dynamic.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/errors.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/exception.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/extracting.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/file.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/helpers.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/http_mixin.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/json_mixin.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/matchers.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/numeric.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/outcome.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/py.typed +0 -0
- assertpy-2.36.0/bridge/assertpy2/pytest_plugin.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/snapshot.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/string.py +8 -0
- assertpy-2.36.0/bridge/assertpy2/warning.py +8 -0
- assertpy-2.36.0/bridge/pyproject.toml +39 -0
- assertpy-2.36.0/docs/assets/diff-equal.png +0 -0
- assertpy-2.36.0/docs/assets/diff-equal.svg +51 -0
- assertpy-2.36.0/docs/assets/diff-gallery.png +0 -0
- assertpy-2.36.0/docs/assets/diff-match.svg +50 -0
- assertpy-2.36.0/docs/assets/diff-sequence.svg +51 -0
- assertpy-2.36.0/docs/assets/diff-set.svg +47 -0
- assertpy-2.36.0/docs/assets/diff-string.svg +59 -0
- assertpy-2.36.0/docs/concepts/stability.md +190 -0
- assertpy-2.36.0/docs/concepts/type-safety.md +574 -0
- assertpy-2.36.0/docs/extending/custom-assertions.md +186 -0
- assertpy-2.36.0/docs/extending/integrations.md +487 -0
- assertpy-2.36.0/docs/getting-started/comparison.md +263 -0
- assertpy-2.36.0/docs/getting-started/migration.md +209 -0
- assertpy-2.36.0/docs/getting-started/quickstart.md +86 -0
- assertpy-2.36.0/docs/guides/assertions.md +1228 -0
- assertpy-2.36.0/docs/guides/data.md +254 -0
- assertpy-2.36.0/docs/guides/errors.md +1370 -0
- assertpy-2.36.0/docs/guides/fluent.md +199 -0
- assertpy-2.36.0/docs/guides/matchers.md +534 -0
- assertpy-2.36.0/docs/guides/testing.md +690 -0
- assertpy-2.36.0/docs/index.md +189 -0
- assertpy-2.36.0/docs/logo-dark.svg +5 -0
- assertpy-2.36.0/docs/logo.svg +5 -0
- assertpy-2.36.0/docs/recipes.md +324 -0
- assertpy-2.36.0/docs/reference/async.md +30 -0
- assertpy-2.36.0/docs/reference/bytes.md +5 -0
- assertpy-2.36.0/docs/reference/collections.md +5 -0
- assertpy-2.36.0/docs/reference/containment.md +5 -0
- assertpy-2.36.0/docs/reference/core.md +12 -0
- assertpy-2.36.0/docs/reference/dataframes.md +22 -0
- assertpy-2.36.0/docs/reference/dates.md +5 -0
- assertpy-2.36.0/docs/reference/dicts.md +5 -0
- assertpy-2.36.0/docs/reference/dynamic.md +6 -0
- assertpy-2.36.0/docs/reference/entry-points.md +105 -0
- assertpy-2.36.0/docs/reference/errors.md +54 -0
- assertpy-2.36.0/docs/reference/exceptions.md +5 -0
- assertpy-2.36.0/docs/reference/extracting.md +5 -0
- assertpy-2.36.0/docs/reference/files.md +11 -0
- assertpy-2.36.0/docs/reference/http.md +6 -0
- assertpy-2.36.0/docs/reference/json.md +5 -0
- assertpy-2.36.0/docs/reference/matchers.md +45 -0
- assertpy-2.36.0/docs/reference/numbers.md +5 -0
- assertpy-2.36.0/docs/reference/overview.md +31 -0
- assertpy-2.36.0/docs/reference/snapshots.md +29 -0
- assertpy-2.36.0/docs/reference/strings.md +6 -0
- assertpy-2.36.0/docs/reference/warnings.md +5 -0
- assertpy-2.36.0/docs/stylesheets/extra.css +56 -0
- assertpy-2.36.0/mkdocs.yml +164 -0
- assertpy-2.36.0/mkdocs_hooks.py +16 -0
- assertpy-2.36.0/pyproject.toml +443 -0
- assertpy-2.36.0/pyrefly.toml +14 -0
- assertpy-2.36.0/scripts/check_sdist.py +236 -0
- assertpy-2.36.0/scripts/generate_check_protocols.py +169 -0
- assertpy-2.36.0/scripts/generate_poll_protocols.py +1749 -0
- assertpy-2.36.0/scripts/mutation_report.py +145 -0
- assertpy-2.36.0/scripts/regenerate.py +49 -0
- assertpy-2.36.0/tests/api_snapshot.json +7249 -0
- assertpy-2.36.0/tests/api_surface.py +514 -0
- assertpy-2.36.0/tests/chain_corpus.py +337 -0
- assertpy-2.36.0/tests/chain_model.py +633 -0
- assertpy-2.36.0/tests/chain_surfaces.py +226 -0
- assertpy-2.36.0/tests/conftest.py +111 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/LICENSE +19 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/date-time.json +226 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/date.json +423 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/duration.json +273 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/email.json +368 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/hostname.json +376 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/multipleOf.json +102 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/time.json +297 -0
- assertpy-2.36.0/tests/data/json_schema_test_suite/uri.json +248 -0
- assertpy-2.36.0/tests/docs_fixtures.py +251 -0
- assertpy-2.36.0/tests/format_corpus.py +137 -0
- assertpy-2.36.0/tests/group_compat.py +31 -0
- assertpy-2.36.0/tests/poll_parity_baseline.py +919 -0
- assertpy-2.36.0/tests/poll_parity_cases.py +3331 -0
- assertpy-2.36.0/tests/pyright_baseline.py +112 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-custom-rich-payload.json +70 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-custom-tuple-payload.json +8 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-datetime.json +4 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-marker-value.json +13 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-nested-records.json +27 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-shaped-pair.json +9 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-shaped-tagged.json +12 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-enum-tuple.json +10 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-instance-tuple.json +11 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-keyed.json +7 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-nested.json +11 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-registered-namedtuple.json +4 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-set-of-tuples.json +15 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-tuple-keys.json +22 -0
- assertpy-2.36.0/tests/snapshot_files_2_35/snap-tuple.json +4 -0
- assertpy-2.36.0/tests/snapshot_types_2_35.py +66 -0
- assertpy-2.36.0/tests/test_api_compatibility.py +471 -0
- assertpy-2.36.0/tests/test_api_vocabulary.py +147 -0
- assertpy-2.36.0/tests/test_architecture_doc.py +107 -0
- assertpy-2.36.0/tests/test_async.py +1705 -0
- assertpy-2.36.0/tests/test_async_predicates.py +195 -0
- assertpy-2.36.0/tests/test_attrs_support.py +480 -0
- assertpy-2.36.0/tests/test_behave_matchers.py +111 -0
- assertpy-2.36.0/tests/test_bool.py +33 -0
- assertpy-2.36.0/tests/test_boundary_cases.py +112 -0
- assertpy-2.36.0/tests/test_bridge.py +94 -0
- assertpy-2.36.0/tests/test_bytes.py +243 -0
- assertpy-2.36.0/tests/test_call_awaitables.py +163 -0
- assertpy-2.36.0/tests/test_callable.py +48 -0
- assertpy-2.36.0/tests/test_capable_protocol.py +1076 -0
- assertpy-2.36.0/tests/test_chain_machine.py +664 -0
- assertpy-2.36.0/tests/test_chaining.py +22 -0
- assertpy-2.36.0/tests/test_check.py +351 -0
- assertpy-2.36.0/tests/test_check_protocols.py +230 -0
- assertpy-2.36.0/tests/test_class.py +145 -0
- assertpy-2.36.0/tests/test_class_in_spec.py +293 -0
- assertpy-2.36.0/tests/test_clustering.py +1587 -0
- assertpy-2.36.0/tests/test_collection.py +321 -0
- assertpy-2.36.0/tests/test_compiled_patterns.py +209 -0
- assertpy-2.36.0/tests/test_complexity.py +121 -0
- assertpy-2.36.0/tests/test_core.py +194 -0
- assertpy-2.36.0/tests/test_custom_dict.py +1013 -0
- assertpy-2.36.0/tests/test_custom_list.py +47 -0
- assertpy-2.36.0/tests/test_dangling.py +1196 -0
- assertpy-2.36.0/tests/test_dataframe.py +299 -0
- assertpy-2.36.0/tests/test_datetime.py +1024 -0
- assertpy-2.36.0/tests/test_declared_floors.py +190 -0
- assertpy-2.36.0/tests/test_deep_nesting.py +438 -0
- assertpy-2.36.0/tests/test_description.py +29 -0
- assertpy-2.36.0/tests/test_dict.py +422 -0
- assertpy-2.36.0/tests/test_dict_compare.py +1086 -0
- assertpy-2.36.0/tests/test_diff_laws.py +1772 -0
- assertpy-2.36.0/tests/test_difference_places.py +223 -0
- assertpy-2.36.0/tests/test_docs_examples.py +334 -0
- assertpy-2.36.0/tests/test_docs_typing.py +122 -0
- assertpy-2.36.0/tests/test_duality.py +1018 -0
- assertpy-2.36.0/tests/test_dynamic.py +460 -0
- assertpy-2.36.0/tests/test_equality_signals.py +1206 -0
- assertpy-2.36.0/tests/test_equals.py +487 -0
- assertpy-2.36.0/tests/test_errors.py +464 -0
- assertpy-2.36.0/tests/test_evaluation_core.py +3291 -0
- assertpy-2.36.0/tests/test_exact_oracle.py +343 -0
- assertpy-2.36.0/tests/test_exception_context.py +90 -0
- assertpy-2.36.0/tests/test_exception_fields.py +267 -0
- assertpy-2.36.0/tests/test_expected_contract.py +303 -0
- assertpy-2.36.0/tests/test_expected_exception.py +1305 -0
- assertpy-2.36.0/tests/test_expected_warning.py +288 -0
- assertpy-2.36.0/tests/test_extensions.py +639 -0
- assertpy-2.36.0/tests/test_extracting.py +494 -0
- assertpy-2.36.0/tests/test_fail.py +15 -0
- assertpy-2.36.0/tests/test_file.py +297 -0
- assertpy-2.36.0/tests/test_fixed_width_integers.py +717 -0
- assertpy-2.36.0/tests/test_grouped_soft.py +148 -0
- assertpy-2.36.0/tests/test_hints.py +1025 -0
- assertpy-2.36.0/tests/test_hostile_values.py +238 -0
- assertpy-2.36.0/tests/test_html_equality.py +492 -0
- assertpy-2.36.0/tests/test_http.py +599 -0
- assertpy-2.36.0/tests/test_import_cost.py +44 -0
- assertpy-2.36.0/tests/test_in.py +66 -0
- assertpy-2.36.0/tests/test_inline_record.py +1075 -0
- assertpy-2.36.0/tests/test_inline_snapshot.py +280 -0
- assertpy-2.36.0/tests/test_integer_arguments.py +113 -0
- assertpy-2.36.0/tests/test_iterable_cluster.py +426 -0
- assertpy-2.36.0/tests/test_json.py +209 -0
- assertpy-2.36.0/tests/test_list.py +1012 -0
- assertpy-2.36.0/tests/test_log_passes.py +1957 -0
- assertpy-2.36.0/tests/test_match_result.py +256 -0
- assertpy-2.36.0/tests/test_matcher_algebra.py +230 -0
- assertpy-2.36.0/tests/test_matcher_parity.py +210 -0
- assertpy-2.36.0/tests/test_matcher_registry.py +292 -0
- assertpy-2.36.0/tests/test_matchers.py +1630 -0
- assertpy-2.36.0/tests/test_membership_failures.py +270 -0
- assertpy-2.36.0/tests/test_membership_family.py +523 -0
- assertpy-2.36.0/tests/test_message_elision.py +278 -0
- assertpy-2.36.0/tests/test_message_values.py +345 -0
- assertpy-2.36.0/tests/test_missing_names.py +488 -0
- assertpy-2.36.0/tests/test_moment_relations.py +866 -0
- assertpy-2.36.0/tests/test_namedtuple.py +79 -0
- assertpy-2.36.0/tests/test_negated_protocols.py +422 -0
- assertpy-2.36.0/tests/test_none.py +27 -0
- assertpy-2.36.0/tests/test_not.py +421 -0
- assertpy-2.36.0/tests/test_number_zoo.py +491 -0
- assertpy-2.36.0/tests/test_numbers.py +2208 -0
- assertpy-2.36.0/tests/test_numeric_questions.py +322 -0
- assertpy-2.36.0/tests/test_one_shot_chains.py +572 -0
- assertpy-2.36.0/tests/test_one_shot_matchers.py +422 -0
- assertpy-2.36.0/tests/test_openapi_30_keywords.py +576 -0
- assertpy-2.36.0/tests/test_openapi_contract.py +1048 -0
- assertpy-2.36.0/tests/test_openapi_format_suite.py +118 -0
- assertpy-2.36.0/tests/test_openapi_formats.py +1007 -0
- assertpy-2.36.0/tests/test_openapi_multiple_of.py +322 -0
- assertpy-2.36.0/tests/test_openapi_oracle.py +328 -0
- assertpy-2.36.0/tests/test_openapi_part_read.py +441 -0
- assertpy-2.36.0/tests/test_operation_contract.py +404 -0
- assertpy-2.36.0/tests/test_optional_integration_contracts.py +86 -0
- assertpy-2.36.0/tests/test_overload_order.py +174 -0
- assertpy-2.36.0/tests/test_overloads.py +58 -0
- assertpy-2.36.0/tests/test_pin_coverage.py +784 -0
- assertpy-2.36.0/tests/test_pipeline.py +187 -0
- assertpy-2.36.0/tests/test_poll_parity.py +388 -0
- assertpy-2.36.0/tests/test_poll_protocols.py +489 -0
- assertpy-2.36.0/tests/test_prerequisites.py +376 -0
- assertpy-2.36.0/tests/test_property_attrs.py +123 -0
- assertpy-2.36.0/tests/test_property_based.py +3140 -0
- assertpy-2.36.0/tests/test_protocol_parity.py +698 -0
- assertpy-2.36.0/tests/test_public_surface.py +423 -0
- assertpy-2.36.0/tests/test_pyright_baseline.py +156 -0
- assertpy-2.36.0/tests/test_pytest_plugin.py +4458 -0
- assertpy-2.36.0/tests/test_recursive_assertion.py +227 -0
- assertpy-2.36.0/tests/test_recursive_compare.py +275 -0
- assertpy-2.36.0/tests/test_recursive_compare_attrs.py +81 -0
- assertpy-2.36.0/tests/test_recursive_compare_config.py +1509 -0
- assertpy-2.36.0/tests/test_recursive_compare_pydantic.py +466 -0
- assertpy-2.36.0/tests/test_refusal_frames.py +292 -0
- assertpy-2.36.0/tests/test_refusals.py +440 -0
- assertpy-2.36.0/tests/test_regex_groups.py +114 -0
- assertpy-2.36.0/tests/test_relative_tolerance.py +298 -0
- assertpy-2.36.0/tests/test_renamed.py +196 -0
- assertpy-2.36.0/tests/test_render_laws.py +651 -0
- assertpy-2.36.0/tests/test_requirement.py +673 -0
- assertpy-2.36.0/tests/test_rich_diff.py +2664 -0
- assertpy-2.36.0/tests/test_same_as.py +42 -0
- assertpy-2.36.0/tests/test_satisfy.py +651 -0
- assertpy-2.36.0/tests/test_skip_guard.py +240 -0
- assertpy-2.36.0/tests/test_snapshot_layout.py +295 -0
- assertpy-2.36.0/tests/test_snapshot_tuples.py +604 -0
- assertpy-2.36.0/tests/test_snapshots.py +1973 -0
- assertpy-2.36.0/tests/test_soft.py +645 -0
- assertpy-2.36.0/tests/test_soft_fail.py +61 -0
- assertpy-2.36.0/tests/test_soft_summary_blocks.py +356 -0
- assertpy-2.36.0/tests/test_strict_reach.py +466 -0
- assertpy-2.36.0/tests/test_string.py +747 -0
- assertpy-2.36.0/tests/test_structural.py +5605 -0
- assertpy-2.36.0/tests/test_structure_sentence.py +539 -0
- assertpy-2.36.0/tests/test_surface_conformance.py +488 -0
- assertpy-2.36.0/tests/test_sync_eventually.py +312 -0
- assertpy-2.36.0/tests/test_text_relations.py +404 -0
- assertpy-2.36.0/tests/test_traceback.py +84 -0
- assertpy-2.36.0/tests/test_type.py +311 -0
- assertpy-2.36.0/tests/test_type_expression_failures.py +148 -0
- assertpy-2.36.0/tests/test_typing.py +928 -0
- assertpy-2.36.0/tests/test_typing_chain_corpus.py +38 -0
- assertpy-2.36.0/tests/test_typing_claims.py +195 -0
- assertpy-2.36.0/tests/test_typing_completeness.py +142 -0
- assertpy-2.36.0/tests/test_typing_conformance.py +489 -0
- assertpy-2.36.0/tests/test_typing_from_a_wheel.py +97 -0
- assertpy-2.36.0/tests/test_typing_harness.py +79 -0
- assertpy-2.36.0/tests/test_typing_http.py +98 -0
- assertpy-2.36.0/tests/test_typing_integrations.py +110 -0
- assertpy-2.36.0/tests/test_typing_negative.py +208 -0
- assertpy-2.36.0/tests/test_typing_promises.py +76 -0
- assertpy-2.36.0/tests/test_unrenderable_values.py +244 -0
- assertpy-2.36.0/tests/test_vacuity_contract.py +686 -0
- assertpy-2.36.0/tests/test_verdict_corpus.py +137 -0
- assertpy-2.36.0/tests/test_verdicts_held_to_python.py +294 -0
- assertpy-2.36.0/tests/test_warn.py +158 -0
- assertpy-2.36.0/tests/test_warn_on_failure.py +158 -0
- assertpy-2.36.0/tests/typing_cases.py +705 -0
- assertpy-2.36.0/tests/typing_chain_corpus_baseline.json +256 -0
- assertpy-2.36.0/tests/typing_harness.py +166 -0
- assertpy-2.36.0/tests/typing_http.py +81 -0
- assertpy-2.36.0/tests/typing_integrations.py +65 -0
- assertpy-2.36.0/tests/typing_integrations_baseline.py +94 -0
- assertpy-2.36.0/tests/typing_negative_baseline.py +564 -0
- assertpy-2.36.0/tests/typing_promises.py +288 -0
- assertpy-2.36.0/tests/verdict_corpus.py +632 -0
- assertpy-2.36.0/tests/verdict_golden.txt +127 -0
- assertpy-1.1/PKG-INFO +0 -62
- assertpy-1.1/assertpy/__init__.py +0 -3
- assertpy-1.1/assertpy/assertpy.py +0 -433
- assertpy-1.1/assertpy/base.py +0 -427
- assertpy-1.1/assertpy/collection.py +0 -196
- assertpy-1.1/assertpy/contains.py +0 -362
- assertpy-1.1/assertpy/date.py +0 -185
- assertpy-1.1/assertpy/dict.py +0 -236
- assertpy-1.1/assertpy/dynamic.py +0 -116
- assertpy-1.1/assertpy/exception.py +0 -113
- assertpy-1.1/assertpy/extracting.py +0 -231
- assertpy-1.1/assertpy/file.py +0 -225
- assertpy-1.1/assertpy/helpers.py +0 -250
- assertpy-1.1/assertpy/numeric.py +0 -513
- assertpy-1.1/assertpy/snapshot.py +0 -208
- assertpy-1.1/assertpy/string.py +0 -413
- assertpy-1.1/setup.cfg +0 -8
- assertpy-1.1/setup.py +0 -68
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
# every suite runs on four workers by file: split by test, a pair in test_snapshots.py races on the cwd __snapshots
|
|
17
|
+
jobs:
|
|
18
|
+
test:
|
|
19
|
+
timeout-minutes: 15
|
|
20
|
+
runs-on: ${{ matrix.os }}
|
|
21
|
+
# mapped at job level so the upload step can ask whether it has a token before it runs: the secrets
|
|
22
|
+
# context is not readable from a step `if`, and step-level env is not in scope for its own condition
|
|
23
|
+
env:
|
|
24
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
25
|
+
strategy:
|
|
26
|
+
matrix:
|
|
27
|
+
os: [ubuntu-latest]
|
|
28
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
29
|
+
include:
|
|
30
|
+
# the Codecov cell also installs every optional integration (pandas/polars/numpy extras plus
|
|
31
|
+
# pydantic/attrs/requests), so the tests the other minimal-env cells skip run here for real
|
|
32
|
+
- os: ubuntu-latest
|
|
33
|
+
python-version: "3.14"
|
|
34
|
+
full-deps: true
|
|
35
|
+
- os: windows-latest
|
|
36
|
+
python-version: "3.14"
|
|
37
|
+
- os: macos-latest
|
|
38
|
+
python-version: "3.14"
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
41
|
+
with:
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
|
|
44
|
+
- name: Install uv
|
|
45
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
46
|
+
|
|
47
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
48
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
49
|
+
with:
|
|
50
|
+
python-version: ${{ matrix.python-version }}
|
|
51
|
+
allow-prereleases: true
|
|
52
|
+
|
|
53
|
+
- name: Install dependencies
|
|
54
|
+
if: '!matrix.full-deps'
|
|
55
|
+
run: uv sync --locked --extra json
|
|
56
|
+
|
|
57
|
+
- name: Install dependencies (all integrations)
|
|
58
|
+
if: matrix.full-deps
|
|
59
|
+
run: uv sync --locked --extra json --extra data --extra inline --group integrations --group docs-examples --group oracle
|
|
60
|
+
|
|
61
|
+
- name: Test
|
|
62
|
+
# no coverage: only the full-deps cell reports it, and on 3.12 its tracer made this the slowest cell of the run
|
|
63
|
+
if: '!matrix.full-deps'
|
|
64
|
+
run: uv run pytest -v -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
|
|
65
|
+
|
|
66
|
+
- name: The oracle is installed
|
|
67
|
+
# its module skips where a package is absent, so an absence here would pass for a green run
|
|
68
|
+
if: matrix.full-deps
|
|
69
|
+
run: uv run python -c "import openapi_schema_validator, fqdn, isoduration, rfc3986_validator"
|
|
70
|
+
|
|
71
|
+
- name: Test with coverage (enforce 100%)
|
|
72
|
+
# Only the full-deps cell has every integration installed and 0 skips, so it is
|
|
73
|
+
# the only cell that can reach 100%. This is the hard local gate; Codecov's status
|
|
74
|
+
# is reporting on top, not the sole guard (it uploads async and can fail open).
|
|
75
|
+
# The doc-example guard is excluded here so its runs never pad the coverage number.
|
|
76
|
+
if: matrix.full-deps
|
|
77
|
+
run: uv run pytest -v -n 4 --dist loadfile --cov=assertpy --cov-report=term-missing --cov-report=xml --cov-fail-under=100 --ignore=tests/test_docs_examples.py tests
|
|
78
|
+
|
|
79
|
+
- name: Guard doc examples
|
|
80
|
+
# Runs once (docs are platform-independent) and without coverage, so executing the
|
|
81
|
+
# README/guide snippets can't mask a real coverage gap in the test suite.
|
|
82
|
+
if: matrix.full-deps
|
|
83
|
+
run: uv run pytest tests/test_docs_examples.py
|
|
84
|
+
|
|
85
|
+
- name: Upload coverage to Codecov
|
|
86
|
+
# Only the full-deps cell. The bare 3.14/ubuntu cell skips every integration test and used
|
|
87
|
+
# to upload too: Codecov merges reports so the number stayed right, but one of the two was
|
|
88
|
+
# always a partial report sent at a 100% target.
|
|
89
|
+
# A run without repository secrets gets an empty token, and Codecov rejects an untokened upload
|
|
90
|
+
# to a protected branch, so the job went red on the upload while every test passed. The gate is
|
|
91
|
+
# the token rather than who opened the pull request: dependabot pushes its branches into this
|
|
92
|
+
# repository, so a fork test reads false for it and it still runs without secrets.
|
|
93
|
+
if: matrix.full-deps && env.CODECOV_TOKEN != ''
|
|
94
|
+
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
|
95
|
+
with:
|
|
96
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
97
|
+
files: coverage.xml
|
|
98
|
+
# A failed upload is a broken report, not a passing build. The 100% gate above is the hard
|
|
99
|
+
# guard either way, so this cannot turn a real coverage drop into a green run.
|
|
100
|
+
fail_ci_if_error: true
|
|
101
|
+
|
|
102
|
+
declared-floors:
|
|
103
|
+
# Every lower bound on a published extra is a promise, and the lockfile only ever resolves the newest
|
|
104
|
+
# of each, so the bottom of every range was a claim nobody checked. This job checks them, and each
|
|
105
|
+
# widening of it found a real defect: first the data libraries, where the refusal gate keyed on
|
|
106
|
+
# numpy's wording for an unusable operand and that wording is different below numpy 2, then
|
|
107
|
+
# `asttokens>=2.0` and `behave>=1.2.6`, which were simply wrong, then `starlette>=0.40`, whose test
|
|
108
|
+
# client reaches a function deprecated in 3.14.
|
|
109
|
+
#
|
|
110
|
+
# Scope, said exactly: every extra in `[project.optional-dependencies]`, plus the `dev` and
|
|
111
|
+
# `integrations` groups, which is what running the suite needs. The `docs`, `typecheck`, `mutation`,
|
|
112
|
+
# `docs-examples` and `benchmark` groups are not here: each drives a tool with its own command rather
|
|
113
|
+
# than this suite, and none of them is a promise to anyone installing the package.
|
|
114
|
+
#
|
|
115
|
+
# Its own job rather than a cell in the matrix above. An `include` entry naming an `os` and a
|
|
116
|
+
# `python-version` that already exist is merged into that combination instead of adding one, so a
|
|
117
|
+
# cell written that way would have replaced the ordinary runs rather than joining them.
|
|
118
|
+
timeout-minutes: 15
|
|
119
|
+
strategy:
|
|
120
|
+
fail-fast: false
|
|
121
|
+
matrix:
|
|
122
|
+
include:
|
|
123
|
+
- python-version: "3.10"
|
|
124
|
+
extras: "allure,behave,data,inline,json,numpy,pandas,polars"
|
|
125
|
+
groups: "--group dev --group integrations"
|
|
126
|
+
imports: "typing_extensions, numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
|
|
127
|
+
- python-version: "3.11"
|
|
128
|
+
extras: "allure,behave,data,inline,json,numpy,pandas,polars"
|
|
129
|
+
groups: "--group dev --group integrations"
|
|
130
|
+
imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
|
|
131
|
+
- python-version: "3.12"
|
|
132
|
+
extras: "allure,behave,data,inline,json,numpy,pandas,polars"
|
|
133
|
+
groups: "--group dev --group integrations"
|
|
134
|
+
imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
|
|
135
|
+
- python-version: "3.13"
|
|
136
|
+
extras: "allure,behave,data,inline,json,numpy,pandas,polars"
|
|
137
|
+
groups: "--group dev --group integrations"
|
|
138
|
+
imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
|
|
139
|
+
- python-version: "3.14"
|
|
140
|
+
extras: "allure,behave,data,inline,json,numpy,pandas,polars"
|
|
141
|
+
groups: "--group dev --group integrations"
|
|
142
|
+
imports: "numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave, attr, attrs, django, flask, httpx, httpx2, pydantic, requests, starlette"
|
|
143
|
+
# The one dependency every user gets, at its own floor, and it needs a cell to itself: pydantic
|
|
144
|
+
# 2.0 requires typing-extensions>=4.6.1, so every cell installing `integrations` resolves it
|
|
145
|
+
# above the declared bound. Measured 4.0.0 here against 4.6.1 in each of those.
|
|
146
|
+
- python-version: "3.10"
|
|
147
|
+
extras: "allure,behave,data,inline,json,numpy,pandas,polars"
|
|
148
|
+
groups: "--group dev"
|
|
149
|
+
imports: "typing_extensions, numpy, pandas, polars, asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave"
|
|
150
|
+
reduced: "no integrations, so pydantic cannot lift typing-extensions off its declared floor"
|
|
151
|
+
# 3.15 carries what is installable and green there. No combination of the data libraries
|
|
152
|
+
# publishes a wheel for it yet. The `integrations` group installs since pydantic 2.14, and stays
|
|
153
|
+
# out for one test: this cell resolves anyio 4.15.1 on 3.15 where the cells below resolve
|
|
154
|
+
# 4.14.2, and starlette at its floor 0.46.0 imports a name that anyio deprecates, which the
|
|
155
|
+
# suite's `-W error` refuses. Move this cell up to the others when both are settled.
|
|
156
|
+
- python-version: "3.15"
|
|
157
|
+
extras: "allure,behave,inline,json"
|
|
158
|
+
groups: "--group dev"
|
|
159
|
+
imports: "asttokens, executing, idna, jsonschema, jsonpath_ng, referencing, allure_commons, behave"
|
|
160
|
+
reduced: "no data-library wheel for 3.15 yet, and starlette at its floor warns under the anyio resolved here"
|
|
161
|
+
runs-on: ubuntu-latest
|
|
162
|
+
steps:
|
|
163
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
164
|
+
with:
|
|
165
|
+
persist-credentials: false
|
|
166
|
+
|
|
167
|
+
- name: Install uv
|
|
168
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
169
|
+
|
|
170
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
171
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
172
|
+
with:
|
|
173
|
+
python-version: ${{ matrix.python-version }}
|
|
174
|
+
allow-prereleases: true
|
|
175
|
+
|
|
176
|
+
- name: Install every declared dependency at its lower bound
|
|
177
|
+
# `lowest-direct` resolves every direct dependency to the oldest its specifier allows, which is
|
|
178
|
+
# what the metadata promises. Hand-pinning `numpy==1.26.0` did the same for three of them and
|
|
179
|
+
# covered none of the rest.
|
|
180
|
+
#
|
|
181
|
+
# `--only-binary` because the oldest allowed release is not installable everywhere: pandas 2.0.0
|
|
182
|
+
# publishes no wheel above 3.11 and building it fails on a missing `pkg_resources`. With it the
|
|
183
|
+
# resolver lands on the oldest release that has a wheel for this interpreter, which is the oldest
|
|
184
|
+
# a user of it can actually get. Measured: pandas 2.0.0 on 3.10 and 3.11, 2.1.1 on 3.12, 2.2.3 on
|
|
185
|
+
# 3.13, 2.3.3 on 3.14.
|
|
186
|
+
#
|
|
187
|
+
# `uv pip install` and not `uv sync`, which was measured rewriting `uv.lock` wholesale: the tests
|
|
188
|
+
# that shell out then read a lockfile that is not the committed one, and six of them failed on
|
|
189
|
+
# exactly that.
|
|
190
|
+
#
|
|
191
|
+
# Every extra by name, `data` and the three it aggregates alike, because
|
|
192
|
+
# `tests/test_declared_floors.py` reads these lines and asks for each of them.
|
|
193
|
+
run: |
|
|
194
|
+
uv venv --python ${{ matrix.python-version }}
|
|
195
|
+
uv pip install --only-binary=:all: --resolution lowest-direct -e ".[${{ matrix.extras }}]" ${{ matrix.groups }}
|
|
196
|
+
|
|
197
|
+
- name: Every installed floor must import
|
|
198
|
+
# A release old enough to install and too old to import would otherwise leave the cell green:
|
|
199
|
+
# its tests answer `pytest.importorskip` with a skip, and a skip carrying its own reason is
|
|
200
|
+
# invisible to the missing-module guard in `tests/conftest.py`.
|
|
201
|
+
#
|
|
202
|
+
# The `integrations` group is in the list too, not only the published extras: it is installed by
|
|
203
|
+
# the same command and its tests skip the same way. The `dev` group is not, because nothing in it
|
|
204
|
+
# skips: the suite imports pytest and hypothesis to run at all, and the gates that shell out to
|
|
205
|
+
# ruff and ty refuse rather than skip when the tool does not answer.
|
|
206
|
+
run: uv run --no-sync python -c "import ${{ matrix.imports }}"
|
|
207
|
+
|
|
208
|
+
- name: What this cell resolved
|
|
209
|
+
# Just what is installed and at which version. Which of those are declared floors, and whether
|
|
210
|
+
# each was reached, is decided once in `floors-reached` below, where `tomllib` is available and
|
|
211
|
+
# a requirement carrying an environment marker parses rather than being mangled by a regex.
|
|
212
|
+
run: |
|
|
213
|
+
uv run --no-sync python - <<'PROBE'
|
|
214
|
+
import importlib.metadata as md
|
|
215
|
+
import pathlib
|
|
216
|
+
|
|
217
|
+
with pathlib.Path("resolved.txt").open("w", encoding="utf-8") as verdict:
|
|
218
|
+
for distribution in sorted(md.distributions(), key=lambda item: item.metadata["Name"] or ""):
|
|
219
|
+
name = distribution.metadata["Name"]
|
|
220
|
+
if name:
|
|
221
|
+
print(f"{name} {distribution.version}", file=verdict)
|
|
222
|
+
print(pathlib.Path("resolved.txt").read_text(encoding="utf-8"))
|
|
223
|
+
PROBE
|
|
224
|
+
|
|
225
|
+
- name: Keep this cell's resolution for the aggregate
|
|
226
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
227
|
+
with:
|
|
228
|
+
name: floors-resolved-${{ matrix.python-version }}-${{ strategy.job-index }}
|
|
229
|
+
path: resolved.txt
|
|
230
|
+
retention-days: 1
|
|
231
|
+
|
|
232
|
+
- name: Suite at the floor
|
|
233
|
+
# `--no-sync` is load bearing here and nowhere else: a bare `uv run` restores the environment
|
|
234
|
+
# from the lockfile, which would put the newest of everything back and leave this job testing
|
|
235
|
+
# what the matrix above already tests
|
|
236
|
+
run: uv run --no-sync pytest -q -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
|
|
237
|
+
|
|
238
|
+
floors-reached:
|
|
239
|
+
# The cross-cell half of the floor job, and it has to be cross-cell: `typing-extensions` reaches its
|
|
240
|
+
# floor only where pydantic is absent, and `attrs` only where it is installed by name rather than
|
|
241
|
+
# arriving through jsonschema. No cell reaches everything, and between them they do.
|
|
242
|
+
#
|
|
243
|
+
# Every declaration is read here rather than in the cells, with `tomllib` rather than a regex: a
|
|
244
|
+
# requirement carrying an environment marker holds quotes of its own, and reading it by pattern
|
|
245
|
+
# silently truncated `typing-extensions>=4.0; python_version < '3.11'` to nothing.
|
|
246
|
+
timeout-minutes: 5
|
|
247
|
+
needs: declared-floors
|
|
248
|
+
runs-on: ubuntu-latest
|
|
249
|
+
steps:
|
|
250
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
251
|
+
with:
|
|
252
|
+
persist-credentials: false
|
|
253
|
+
|
|
254
|
+
- name: Install uv
|
|
255
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
256
|
+
|
|
257
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
258
|
+
with:
|
|
259
|
+
pattern: floors-resolved-*
|
|
260
|
+
path: resolutions
|
|
261
|
+
|
|
262
|
+
- name: Every declared floor is reached somewhere
|
|
263
|
+
run: |
|
|
264
|
+
uv run --no-project --with packaging python - <<'AGGREGATE'
|
|
265
|
+
import pathlib
|
|
266
|
+
import tomllib
|
|
267
|
+
from packaging.requirements import Requirement
|
|
268
|
+
from packaging.version import Version
|
|
269
|
+
|
|
270
|
+
# keyed by name and floor, because two extras may declare two different bounds on one
|
|
271
|
+
# distribution and reaching either says nothing about the other
|
|
272
|
+
UNREACHED: dict[tuple[str, str], str] = {}
|
|
273
|
+
|
|
274
|
+
declared = {}
|
|
275
|
+
project = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
|
|
276
|
+
sources = [project["project"]["dependencies"]]
|
|
277
|
+
sources += list(project["project"]["optional-dependencies"].values())
|
|
278
|
+
sources += [project["dependency-groups"][name] for name in ("dev", "integrations")]
|
|
279
|
+
for group in sources:
|
|
280
|
+
for line in group:
|
|
281
|
+
requirement = Requirement(line)
|
|
282
|
+
floors = [spec.version for spec in requirement.specifier if spec.operator == ">="]
|
|
283
|
+
if floors and requirement.name != "assertpy":
|
|
284
|
+
declared.setdefault(requirement.name, set()).add(floors[0])
|
|
285
|
+
|
|
286
|
+
installed = {}
|
|
287
|
+
files = sorted(pathlib.Path("resolutions").rglob("resolved.txt"))
|
|
288
|
+
if not files:
|
|
289
|
+
raise SystemExit("no cell reported a resolution, so this job proves nothing")
|
|
290
|
+
for path in files:
|
|
291
|
+
for row in path.read_text(encoding="utf-8").splitlines():
|
|
292
|
+
name, version = row.rsplit(" ", 1)
|
|
293
|
+
installed.setdefault(name.lower().replace("_", "-"), set()).add(version)
|
|
294
|
+
|
|
295
|
+
never, stale = [], []
|
|
296
|
+
for name, floors in sorted(declared.items()):
|
|
297
|
+
key = name.lower().replace("_", "-")
|
|
298
|
+
for floor in sorted(floors, key=Version):
|
|
299
|
+
reached = any(Version(seen) == Version(floor) for seen in installed.get(key, ()))
|
|
300
|
+
excused = (key, floor) in UNREACHED
|
|
301
|
+
print(f"{name:18} floor {floor:10} {'reached' if reached else 'NEVER REACHED'}")
|
|
302
|
+
if not reached and not excused:
|
|
303
|
+
never.append(f"{name}>={floor}")
|
|
304
|
+
if reached and excused:
|
|
305
|
+
stale.append(f"{name}>={floor}")
|
|
306
|
+
if never:
|
|
307
|
+
raise SystemExit(f"declared floors no cell installs: {', '.join(never)}")
|
|
308
|
+
if stale:
|
|
309
|
+
raise SystemExit(f"recorded as unreachable and reached anyway, so the note is stale: {', '.join(stale)}")
|
|
310
|
+
AGGREGATE
|
|
311
|
+
|
|
312
|
+
optional-integrations:
|
|
313
|
+
# allure and behave get their own job because installing them is incompatible with the coverage
|
|
314
|
+
# gate: with allure present the `except ImportError` fallback in pytest_plugin.py never runs, and
|
|
315
|
+
# without it the import itself never runs. The gating cell keeps the second state, this one holds
|
|
316
|
+
# the first. Everywhere else the two are tested against mocks, which accept any call and would
|
|
317
|
+
# wave through the renamed parameter or moved attribute a Dependabot bump can bring.
|
|
318
|
+
timeout-minutes: 10
|
|
319
|
+
runs-on: ubuntu-latest
|
|
320
|
+
steps:
|
|
321
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
322
|
+
with:
|
|
323
|
+
persist-credentials: false
|
|
324
|
+
|
|
325
|
+
- name: Install uv
|
|
326
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
327
|
+
|
|
328
|
+
- name: Set up Python
|
|
329
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
330
|
+
with:
|
|
331
|
+
python-version: "3.14"
|
|
332
|
+
|
|
333
|
+
- name: Install dependencies
|
|
334
|
+
run: uv sync --locked --extra json --extra allure --extra behave
|
|
335
|
+
|
|
336
|
+
- name: Contract tests against the real libraries
|
|
337
|
+
run: uv run pytest -v tests/test_optional_integration_contracts.py
|
|
338
|
+
|
|
339
|
+
- name: The rest of the suite must also pass with both installed
|
|
340
|
+
# Installing allure flips `_HAS_ALLURE`, so every failing assertion in the suite takes the
|
|
341
|
+
# real attach path. That must not change any other test's outcome.
|
|
342
|
+
run: uv run pytest -q -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
|
|
343
|
+
|
|
344
|
+
lint:
|
|
345
|
+
timeout-minutes: 10
|
|
346
|
+
runs-on: ubuntu-latest
|
|
347
|
+
steps:
|
|
348
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
349
|
+
with:
|
|
350
|
+
persist-credentials: false
|
|
351
|
+
|
|
352
|
+
- name: Install uv
|
|
353
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
354
|
+
|
|
355
|
+
- name: Set up Python
|
|
356
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
357
|
+
with:
|
|
358
|
+
python-version: "3.14"
|
|
359
|
+
|
|
360
|
+
- name: Install dependencies
|
|
361
|
+
# the whole `data` stack, because the overloads narrow on *structure* and the structures are
|
|
362
|
+
# pandas', polars' and numpy's to change. A stub release that stops declaring the member a
|
|
363
|
+
# shape keys on moves those values back to the generic builder, silently: it type-checks, and
|
|
364
|
+
# the narrowed methods just stop being offered. The integration matrix below is what notices
|
|
365
|
+
run: uv sync --locked --extra json --extra data --extra inline --group typecheck --group integrations
|
|
366
|
+
|
|
367
|
+
- name: Ruff check
|
|
368
|
+
run: uv run ruff check .
|
|
369
|
+
|
|
370
|
+
- name: Ruff format check
|
|
371
|
+
run: uv run ruff format --check .
|
|
372
|
+
|
|
373
|
+
- name: Type check (ty)
|
|
374
|
+
run: uv run ty check assertpy/ tests/test_typing.py
|
|
375
|
+
|
|
376
|
+
- name: Type check (mypy --strict, public typing surface)
|
|
377
|
+
run: uv run mypy --strict --follow-imports=silent tests/test_typing.py
|
|
378
|
+
|
|
379
|
+
# the engine, not the launcher: `pyright` on PyPI and the npm engine it launches are released apart
|
|
380
|
+
# and have drifted before, and every recorded number here was recorded against one build. The
|
|
381
|
+
# gates that call pyright from Python pin the same one, in `tests/typing_harness.py`
|
|
382
|
+
- name: Type check (pyright, public typing surface)
|
|
383
|
+
env:
|
|
384
|
+
PYRIGHT_PYTHON_FORCE_VERSION: "1.1.414"
|
|
385
|
+
run: uv run pyright --pythonversion 3.14 tests/test_typing.py
|
|
386
|
+
|
|
387
|
+
# a fourth overload-resolution engine on the same file, which is what the badge names. Its
|
|
388
|
+
# settings are in `pyrefly.toml`, and the preset there is deliberate: the fallback one answers
|
|
389
|
+
# green while looking at much less
|
|
390
|
+
- name: Type check (pyrefly, public typing surface)
|
|
391
|
+
run: uv run pyrefly check tests/test_typing.py
|
|
392
|
+
|
|
393
|
+
# the fourth checker over the promises too: the harness reads three, and both defects the file
|
|
394
|
+
# was written for were accepted by all four
|
|
395
|
+
- name: Type check (pyrefly, the object matches the promise)
|
|
396
|
+
run: uv run pyrefly check tests/typing_promises.py
|
|
397
|
+
|
|
398
|
+
# the gates that need the checkers this job installs: each file's docstring says what it holds
|
|
399
|
+
- name: Type check (pytest gates, all four checkers)
|
|
400
|
+
run: >
|
|
401
|
+
uv run pytest -q -n 4 --dist loadfile
|
|
402
|
+
tests/test_pyright_baseline.py
|
|
403
|
+
tests/test_typing_completeness.py
|
|
404
|
+
tests/test_typing_promises.py
|
|
405
|
+
tests/test_typing_negative.py
|
|
406
|
+
tests/test_typing_chain_corpus.py
|
|
407
|
+
tests/test_typing_integrations.py
|
|
408
|
+
tests/test_typing_http.py
|
|
409
|
+
tests/test_typing_from_a_wheel.py
|
|
410
|
+
tests/test_check_protocols.py
|
|
411
|
+
tests/test_operation_contract.py
|
|
412
|
+
tests/test_typing_conformance.py
|
|
413
|
+
|
|
414
|
+
sdist:
|
|
415
|
+
# the suite as a distribution runs it, from the sdist: `scripts/check_sdist.py` says how and why
|
|
416
|
+
timeout-minutes: 20
|
|
417
|
+
runs-on: ubuntu-latest
|
|
418
|
+
strategy:
|
|
419
|
+
fail-fast: false
|
|
420
|
+
matrix:
|
|
421
|
+
include:
|
|
422
|
+
- mode: packager
|
|
423
|
+
options: '--export="--extra json"'
|
|
424
|
+
# the lint job's set, for the type-checker gates
|
|
425
|
+
- mode: developer
|
|
426
|
+
options: '--developer --export="--extra json --extra data --extra inline --group typecheck --group integrations"'
|
|
427
|
+
steps:
|
|
428
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
429
|
+
with:
|
|
430
|
+
persist-credentials: false
|
|
431
|
+
|
|
432
|
+
- name: Install uv
|
|
433
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
434
|
+
|
|
435
|
+
- name: Set up Python
|
|
436
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
437
|
+
with:
|
|
438
|
+
python-version: "3.14"
|
|
439
|
+
|
|
440
|
+
- name: The suite from the sdist and from the checkout (${{ matrix.mode }})
|
|
441
|
+
run: >
|
|
442
|
+
python scripts/check_sdist.py ${{ matrix.options }} --work "$RUNNER_TEMP/check_sdist"
|
|
443
|
+
-- -q -n 4 --dist loadfile --ignore=tests/test_docs_examples.py tests
|
|
444
|
+
|
|
445
|
+
ci-ok:
|
|
446
|
+
timeout-minutes: 5
|
|
447
|
+
needs: [test, declared-floors, floors-reached, optional-integrations, lint, sdist]
|
|
448
|
+
if: always()
|
|
449
|
+
runs-on: ubuntu-latest
|
|
450
|
+
steps:
|
|
451
|
+
- name: Verify required jobs succeeded
|
|
452
|
+
env:
|
|
453
|
+
TEST_RESULT: ${{ needs.test.result }}
|
|
454
|
+
FLOOR_RESULT: ${{ needs.declared-floors.result }}
|
|
455
|
+
REACHED_RESULT: ${{ needs.floors-reached.result }}
|
|
456
|
+
INTEGRATIONS_RESULT: ${{ needs.optional-integrations.result }}
|
|
457
|
+
LINT_RESULT: ${{ needs.lint.result }}
|
|
458
|
+
SDIST_RESULT: ${{ needs.sdist.result }}
|
|
459
|
+
run: |
|
|
460
|
+
[ "$TEST_RESULT" = "success" ] &&
|
|
461
|
+
[ "$FLOOR_RESULT" = "success" ] &&
|
|
462
|
+
[ "$REACHED_RESULT" = "success" ] &&
|
|
463
|
+
[ "$INTEGRATIONS_RESULT" = "success" ] &&
|
|
464
|
+
[ "$LINT_RESULT" = "success" ] &&
|
|
465
|
+
[ "$SDIST_RESULT" = "success" ]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# .gitignore
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.cache
|
|
4
|
+
.pytest_cache
|
|
5
|
+
__pycache__
|
|
6
|
+
|
|
7
|
+
# testing output
|
|
8
|
+
*.xml
|
|
9
|
+
*.log
|
|
10
|
+
cover
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov
|
|
13
|
+
.hypothesis
|
|
14
|
+
__snapshots
|
|
15
|
+
mycustompath
|
|
16
|
+
|
|
17
|
+
# packaging
|
|
18
|
+
MANIFEST
|
|
19
|
+
dist
|
|
20
|
+
|
|
21
|
+
# mac
|
|
22
|
+
.DS_Store
|
|
23
|
+
|
|
24
|
+
# venv
|
|
25
|
+
.venv
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
/.idea
|
|
29
|
+
.vscode
|
|
30
|
+
|
|
31
|
+
# docs output
|
|
32
|
+
docs/build
|
|
33
|
+
docs/out
|
|
34
|
+
/site/
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# The typed surface
|
|
2
|
+
|
|
3
|
+
`assert_that()` hands back a different set of methods depending on what you gave it, and four checkers have to
|
|
4
|
+
agree on which set. Read this before changing a public signature. None of it is discoverable from the files.
|
|
5
|
+
|
|
6
|
+
## Two halves
|
|
7
|
+
|
|
8
|
+
The runtime is a stack of mixins. `AssertionBuilder` inherits `StringMixin`, `NumericMixin`, `DictMixin` and a
|
|
9
|
+
dozen more, so every assertion exists on every value and refuses what it cannot handle.
|
|
10
|
+
|
|
11
|
+
The typed surface is Protocols in `assertpy/_engine/_typing.py`, one per kind of value, picked by an overload
|
|
12
|
+
ladder on `assert_that` in `assertpy/assertpy.py`. A checker sees only the protocol, so
|
|
13
|
+
`assert_that({}).is_positive()` is a type error and `assert_that(1).is_positive()` is not.
|
|
14
|
+
|
|
15
|
+
Neither half knows about the other. Everything below keeps them from drifting apart.
|
|
16
|
+
|
|
17
|
+
Most of it is not yours to edit:
|
|
18
|
+
|
|
19
|
+
| file | protocols | declarations |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `_engine/_typing.py` | 43 | 415 |
|
|
22
|
+
| `_engine/_check_typing.py` | 34 | 326 |
|
|
23
|
+
| `_engine/_builder_check_typing.py` | 1 | 320 |
|
|
24
|
+
| `_engine/_capable_typing.py` | 10 | 325 |
|
|
25
|
+
| `_engine/_poll_typing.py` | 22 | 1505 |
|
|
26
|
+
| `_engine/_negated_typing.py` | 23 | 1053 |
|
|
27
|
+
|
|
28
|
+
One is written by hand. `tests/test_architecture_doc.py` recomputes this table. No line counts, which
|
|
29
|
+
move on every edit to a generated file.
|
|
30
|
+
|
|
31
|
+
## What is generated
|
|
32
|
+
|
|
33
|
+
| file | written by | built from |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `_engine/_typing.py` | hand | the protocols themselves |
|
|
36
|
+
| `assertpy.py` overload ladder | hand | which protocol each subject resolves to |
|
|
37
|
+
| `_engine/_poll_typing.py` | `scripts/generate_poll_protocols.py` | `_typing.py`, `assertpy.py`, `_engine/_operations.py` |
|
|
38
|
+
| `_engine/_builder_check_typing.py` | `scripts/generate_poll_protocols.py` | the same three |
|
|
39
|
+
| `_engine/_capable_typing.py` | `scripts/generate_poll_protocols.py` | the **runtime mixins** under `assertpy/*.py`, `_engine/_operations.py` |
|
|
40
|
+
| `_engine/_negated_typing.py` | `scripts/generate_poll_protocols.py` | `_typing.py` and `_engine/_operations.py` |
|
|
41
|
+
| `_engine/_check_typing.py` | `scripts/generate_check_protocols.py` | `_typing.py`, `_engine/_operations.py` |
|
|
42
|
+
|
|
43
|
+
One script writes four of them. That is the part people miss.
|
|
44
|
+
|
|
45
|
+
- **polling twins** answer `eventually()` and `eventually_sync()`, so every assertion hands back the chain.
|
|
46
|
+
- **verdict twins** answer `check()`, returning an `AssertionOutcome` instead of `Self`.
|
|
47
|
+
- **the capability façade** stands in for the builder itself, so it reads the mixins rather than the views:
|
|
48
|
+
reading the views would narrow it to what one value type answers. It skips its own hand-written list, and
|
|
49
|
+
takes which mixin methods pivot from `_engine/_operations.py`, since the mixins write those `-> Self` too.
|
|
50
|
+
- **negation twins** answer `.not_`, where one assertion hands back the view the chain had before it.
|
|
51
|
+
|
|
52
|
+
`_engine/_operations.py` decides what the first two leave out: `NOT_AN_OPERATION`, `WITHOUT_A_VERDICT`.
|
|
53
|
+
|
|
54
|
+
## Changing a signature
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv run python scripts/regenerate.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That runs both generators, re-records `tests/api_snapshot.json`, which pins the public surface, and
|
|
61
|
+
re-records the table of sizes above. Read the diff it leaves.
|
|
62
|
+
|
|
63
|
+
**The generators carry hard-coded import lines.** A signature naming something the generated file did not
|
|
64
|
+
already import produces a file that will not compile, and ruff reports `F821 Undefined name` against a file you
|
|
65
|
+
never edited. Add the name to the import template in the generator.
|
|
66
|
+
|
|
67
|
+
## Adding a type of value
|
|
68
|
+
|
|
69
|
+
The runtime comes first, because the rest is generated or recorded from it.
|
|
70
|
+
|
|
71
|
+
1. Write the mixin, compose it into `AssertionBuilder`'s bases.
|
|
72
|
+
2. Add it to `_COVERAGE` in `tests/test_protocol_parity.py`, which has its own gate.
|
|
73
|
+
3. Declare the protocol in `_engine/_typing.py`.
|
|
74
|
+
4. Add the arm to the `assert_that` ladder in `assertpy.py`.
|
|
75
|
+
5. Pin it with `assert_type` in `tests/test_typing.py`, the file the README badge is about.
|
|
76
|
+
6. Regenerate.
|
|
77
|
+
7. Re-record the snapshot, last.
|
|
78
|
+
|
|
79
|
+
Regenerating before step 1 produces a stale façade. Recording the snapshot before the mixin is composed makes
|
|
80
|
+
its gate red again.
|
|
81
|
+
|
|
82
|
+
## Adding an assertion to a type that exists
|
|
83
|
+
|
|
84
|
+
The skipped step is the third.
|
|
85
|
+
|
|
86
|
+
1. Write the method on its mixin.
|
|
87
|
+
2. Declare it on the protocols it applies to in `_engine/_typing.py`, and on those only. A declaration
|
|
88
|
+
on `_CoreAssertion` offers it to every value, which is the thing the typed surface exists to avoid.
|
|
89
|
+
3. Register it in `_engine/_operations.py` unless it is a plain assertion. No verdict goes in
|
|
90
|
+
`WITHOUT_A_VERDICT` under what it does instead, since `check()` and `not_` mean nothing on it and
|
|
91
|
+
both used to accept them. A pivot that also asserts goes in `ALSO_ASSERTS`, since reaching
|
|
92
|
+
`self.error()` does not separate a verdict from a precondition.
|
|
93
|
+
4. If the polling twin is meant to leave it out, add the name to `_AFTER_A_CALL` in
|
|
94
|
+
`tests/test_poll_protocols.py`. The whole caught-exception family is absent from that twin, and the
|
|
95
|
+
list is what says so on purpose rather than by accident.
|
|
96
|
+
5. Regenerate. The generators emit what they find, so a missing declaration surfaces not here but in
|
|
97
|
+
`test_protocol_parity.py::test_mixin_methods_are_declared_on_its_protocols`.
|
|
98
|
+
|
|
99
|
+
A signature naming a type the generated files do not import produces a file that does not compile. The
|
|
100
|
+
import lines are templates inside the generators.
|
|
101
|
+
|
|
102
|
+
## The gates
|
|
103
|
+
|
|
104
|
+
| gate | the question it answers |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `test_typing.py` | does each subject resolve to the protocol it should, under all four checkers |
|
|
107
|
+
| `test_pin_coverage.py` | does every declaration that changes the type have a pin above |
|
|
108
|
+
| `test_protocol_parity.py` | does every declared method exist on the runtime builder |
|
|
109
|
+
| `test_typing_conformance.py` | do the declaration and the runtime agree parameter by parameter |
|
|
110
|
+
| `test_poll_protocols.py` | are the polling twins what the generator produces today |
|
|
111
|
+
| `test_check_protocols.py` | are the verdict twins |
|
|
112
|
+
| `test_capable_protocol.py` | is the façade |
|
|
113
|
+
| `test_api_compatibility.py` | has the public surface moved without the snapshot being re-recorded |
|
|
114
|
+
| `test_typing_completeness.py` | can a checker name a type for every exported symbol |
|
|
115
|
+
| `test_pyright_baseline.py` | has a new pyright diagnostic appeared in the package |
|
|
116
|
+
| `test_typing_promises.py` | is the object at the end of a chain the type the checker was promised |
|
|
117
|
+
| `test_negated_protocols.py` | does every reachable view have a negation twin that hands the view back, and is the file what the generator produces |
|
|
118
|
+
| `test_architecture_doc.py` | does this document still describe the tree, counts included |
|
|
119
|
+
| `test_typing_negative.py` | do the checkers still refuse what they should |
|
|
120
|
+
| `test_typing_chain_corpus.py` | do the chains the chain model takes, to two pivots, still type the way they were recorded |
|
|
121
|
+
| `test_typing_from_a_wheel.py` | does the typed surface survive packaging |
|
|
122
|
+
| `test_typing_integrations.py`, `test_typing_http.py` | do real pandas, polars, numpy and HTTP values still resolve |
|
|
123
|
+
| `test_typing_harness.py` | is pyright's report read off its output alone, and refused where that is not one report |
|
|
124
|
+
| `test_overload_order.py` | is the frame overload still above every other shape-keyed one |
|
|
125
|
+
| `test_operation_contract.py` | is the register of operations that reach no verdict still the true one |
|
|
126
|
+
| `test_typing_claims.py` | are the four checkers still run with zero suppressions, as the badge says |
|
|
127
|
+
| `test_docs_typing.py` | do the snippets in the README and the guides type-check |
|
|
128
|
+
| `test_public_surface.py` | does `import assertpy` still give what it gave |
|
|
129
|
+
|
|
130
|
+
Which one goes red tells you what you did:
|
|
131
|
+
|
|
132
|
+
- a declaration the runtime does not have: `test_protocol_parity.py`
|
|
133
|
+
- a declaration whose parameters the runtime does not match: `test_typing_conformance.py`
|
|
134
|
+
- a generated file edited by hand: that file's own gate, one of the four
|
|
135
|
+
- a protocol changed without regenerating: the polling, verdict and negation gates, never the facade one,
|
|
136
|
+
which reads the mixins instead
|
|
137
|
+
- a subject resolving to the wrong protocol: `test_typing.py`, and only under whichever checker sees it
|
|
138
|
+
- a public name added or moved: `test_api_compatibility.py`, which is a snapshot rather than a rule
|
|
139
|
+
|
|
140
|
+
The four checkers are ty, mypy `--strict`, Pyright and Pyrefly. Where they disagree it is recorded, in
|
|
141
|
+
`tests/typing_negative_baseline.py`, `tests/typing_integrations_baseline.py` and
|
|
142
|
+
`tests/typing_chain_corpus_baseline.json`.
|
|
143
|
+
|
|
144
|
+
Pyright's engine is pinned in `tests/typing_harness.py`. The PyPI distribution is a launcher for a node package
|
|
145
|
+
and the two move at different speeds.
|
|
146
|
+
|
|
147
|
+
## True, though it looks wrong
|
|
148
|
+
|
|
149
|
+
- **`assert_that`'s implementation returns `Any`.** The real return made Pyright allocate four gigabytes against
|
|
150
|
+
a protocol carrying the whole surface and die. The overloads are what callers see.
|
|
151
|
+
- **Some overlapping overloads are deliberate**, recorded per method in `tests/pyright_baseline.py`.
|
|
152
|
+
- **Some variance suggestions are refused**, recorded there too, per TypeVar.
|
|
153
|
+
- **`ty` is scoped to `assertpy/` and `tests/test_typing.py`.** Over the whole tree it reports hundreds of
|
|
154
|
+
diagnostics from `tests/`, which is full of calls that must not type-check.
|
|
155
|
+
|
|
156
|
+
## Where the two halves may differ
|
|
157
|
+
|
|
158
|
+
**Which methods are offered.** At run time every assertion exists on every value; a typed view offers only the
|
|
159
|
+
ones that apply. That gap is the point of the typed surface.
|
|
160
|
+
|
|
161
|
+
**What an offered method accepts.** Here they must agree exactly, and both directions have shipped as defects:
|
|
162
|
+
|
|
163
|
+
- the type **refuses** what the runtime accepts, so working code is rejected
|
|
164
|
+
- the type **accepts** what the runtime refuses, so a checker approves a call that raises
|
|
165
|
+
|
|
166
|
+
The runtime is the fact and the declaration follows it. `ClassInfo` in `_matcher_impls.py` is the worked
|
|
167
|
+
example: recursive because `isinstance` accepts tuples nested to any depth.
|