sarj-python-lint 1.7.3__tar.gz → 1.7.4__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.
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/PKG-INFO +1 -1
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/pyproject.toml +1 -1
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/__main__.py +1 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_imports.py +4 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_project_index.py +18 -2
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_registry.py +6 -0
- sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +333 -0
- sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_explicit_service_contract.py +343 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_port_for_service.py +22 -20
- sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_public_dependency_contract.py +195 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/.gitignore +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/LICENSE +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/README.md +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.4
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -9,6 +9,10 @@ from typing import Self
|
|
|
9
9
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
ABC_SOURCES = frozenset({"abc"})
|
|
13
|
+
TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
14
|
+
|
|
15
|
+
|
|
12
16
|
@dataclass(frozen=True, slots=True)
|
|
13
17
|
class _ImportTarget:
|
|
14
18
|
module: str
|
{sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -77,8 +77,9 @@ class LoadedSource:
|
|
|
77
77
|
|
|
78
78
|
@final
|
|
79
79
|
class ProjectIndexSet:
|
|
80
|
-
def __init__(self, units: Mapping[Path, SourceUnit]) -> None:
|
|
80
|
+
def __init__(self, units: Mapping[Path, SourceUnit], roots: Sequence[Path] = ()) -> None:
|
|
81
81
|
self._units = MappingProxyType(dict(units))
|
|
82
|
+
self._roots = tuple(roots)
|
|
82
83
|
by_module = {unit.module: unit for unit in units.values() if unit.module is not None}
|
|
83
84
|
self._by_module = MappingProxyType(by_module)
|
|
84
85
|
classes: dict[SymbolRef, ClassSummary] = {}
|
|
@@ -104,7 +105,7 @@ class ProjectIndexSet:
|
|
|
104
105
|
continue
|
|
105
106
|
for root in roots:
|
|
106
107
|
_load_root_sources(root, sources)
|
|
107
|
-
return cls(_units(sources, roots))
|
|
108
|
+
return cls(_units(sources, roots), roots)
|
|
108
109
|
|
|
109
110
|
@classmethod
|
|
110
111
|
def single(cls, path: Path, source: str) -> Self:
|
|
@@ -119,6 +120,21 @@ class ProjectIndexSet:
|
|
|
119
120
|
except OSError:
|
|
120
121
|
return None
|
|
121
122
|
|
|
123
|
+
def unit_or_source(self, path: Path, source: str, tree: ast.Module) -> SourceUnit | None:
|
|
124
|
+
indexed = self.unit(path)
|
|
125
|
+
if indexed is not None:
|
|
126
|
+
return indexed
|
|
127
|
+
module = _module_name(path, self._roots)
|
|
128
|
+
if module is None:
|
|
129
|
+
return None
|
|
130
|
+
return SourceUnit(
|
|
131
|
+
path=path,
|
|
132
|
+
module=module,
|
|
133
|
+
source=source,
|
|
134
|
+
tree=tree,
|
|
135
|
+
imports=MappingProxyType(_imports(module, tree, is_package=path.name == "__init__.py")),
|
|
136
|
+
)
|
|
137
|
+
|
|
122
138
|
def nominal_for_field(self, name: str) -> SymbolRef | None:
|
|
123
139
|
matches = self._nominals.get(name)
|
|
124
140
|
return next(iter(matches)) if matches is not None and len(matches) == 1 else None
|
|
@@ -150,7 +150,9 @@ from sarj_python_lint.rules.redundant_module_docstring import RedundantModuleDoc
|
|
|
150
150
|
from sarj_python_lint.rules.repeated_kwarg_heavy_call_in_test import RepeatedKwargHeavyCallInTest
|
|
151
151
|
from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCallCases
|
|
152
152
|
from sarj_python_lint.rules.repeated_test_composition import RepeatedTestComposition
|
|
153
|
+
from sarj_python_lint.rules.require_explicit_contract_implementation import RequireExplicitContractImplementation
|
|
153
154
|
from sarj_python_lint.rules.require_explicit_psycopg_transaction import RequireExplicitPsycopgTransaction
|
|
155
|
+
from sarj_python_lint.rules.require_explicit_service_contract import RequireExplicitServiceContract
|
|
154
156
|
from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
|
|
155
157
|
RequireKeywordOnlySwapProneParams,
|
|
156
158
|
)
|
|
@@ -158,6 +160,7 @@ from sarj_python_lint.rules.require_nodecode_for_splitting_settings_field import
|
|
|
158
160
|
RequireNoDecodeForSplittingSettingsField,
|
|
159
161
|
)
|
|
160
162
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
163
|
+
from sarj_python_lint.rules.require_public_dependency_contract import RequirePublicDependencyContract
|
|
161
164
|
from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
162
165
|
RequirePydanticForExternalJson,
|
|
163
166
|
)
|
|
@@ -188,6 +191,9 @@ if TYPE_CHECKING:
|
|
|
188
191
|
|
|
189
192
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
190
193
|
{
|
|
194
|
+
RequireExplicitContractImplementation.id: RequireExplicitContractImplementation,
|
|
195
|
+
RequirePublicDependencyContract.id: RequirePublicDependencyContract,
|
|
196
|
+
RequireExplicitServiceContract.id: RequireExplicitServiceContract,
|
|
191
197
|
AsyncCleanupRegisteredSynchronously.id: AsyncCleanupRegisteredSynchronously,
|
|
192
198
|
SubprocessKillWithoutReap.id: SubprocessKillWithoutReap,
|
|
193
199
|
NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
ProjectRule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._imports import ABC_SOURCES, TYPING_SOURCES, ImportIndex
|
|
20
|
+
from sarj_python_lint.rules.require_port_for_service import annotation_tail
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
25
|
+
from sarj_python_lint.rules._project_index import ClassSummary, ProjectIndexSet, SourceUnit, SymbolRef
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@final
|
|
29
|
+
class RequireExplicitContractImplementation(ProjectRule):
|
|
30
|
+
id = "require-explicit-contract-implementation"
|
|
31
|
+
code = "SARJ467"
|
|
32
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
33
|
+
default_level=Severity.WARNING,
|
|
34
|
+
summary="Require owned substitutes to explicitly inherit their injected contract.",
|
|
35
|
+
rationale=(
|
|
36
|
+
"Structural matching only checks a fake where it is passed. Explicit inheritance makes its obligation "
|
|
37
|
+
"visible at the class declaration and catches missing abstract operations at instantiation."
|
|
38
|
+
),
|
|
39
|
+
remediation="Make the owned implementation inherit the exact ABC or Protocol expected by the consumer.",
|
|
40
|
+
category=RuleCategory.ARCHITECTURE,
|
|
41
|
+
autofix=AutofixPolicy.NONE,
|
|
42
|
+
limitations=(
|
|
43
|
+
"Only direct constructor arguments and unreassigned local constructor results supplied to a resolvable typed parameter are checked.",
|
|
44
|
+
"Dynamic factories, containers, casts, unresolved ancestry, and third-party implementations are excluded.",
|
|
45
|
+
),
|
|
46
|
+
examples=(
|
|
47
|
+
RuleExample(
|
|
48
|
+
example_id="structural-fake-at-injection",
|
|
49
|
+
title="A fake is supplied without declaring its contract",
|
|
50
|
+
outcome=ExampleOutcome.MATCH,
|
|
51
|
+
files=(
|
|
52
|
+
ExampleFile.python("app/__init__.py", "# package\n"),
|
|
53
|
+
ExampleFile.python(
|
|
54
|
+
"app/consumer.py",
|
|
55
|
+
"from typing import Protocol\nclass Publisher(Protocol):\n def publish(self) -> None: ...\nclass Consumer:\n def __init__(self, publisher: Publisher) -> None:\n self.publisher = publisher\n",
|
|
56
|
+
),
|
|
57
|
+
ExampleFile.python("app/fake.py", "class FakePublisher:\n def publish(self) -> None: ...\n"),
|
|
58
|
+
ExampleFile.python(
|
|
59
|
+
"app/usage.py",
|
|
60
|
+
"from app.consumer import Consumer\nfrom app.fake import FakePublisher\ndef setup() -> None:\n Consumer(publisher=FakePublisher())\n",
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
focus_path=PurePosixPath("app/usage.py"),
|
|
64
|
+
expected_count=1,
|
|
65
|
+
public=True,
|
|
66
|
+
),
|
|
67
|
+
RuleExample(
|
|
68
|
+
example_id="explicit-fake-at-injection",
|
|
69
|
+
title="The fake declares the actual contract",
|
|
70
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
71
|
+
files=(
|
|
72
|
+
ExampleFile.python("app/__init__.py", "# package\n"),
|
|
73
|
+
ExampleFile.python(
|
|
74
|
+
"app/consumer.py",
|
|
75
|
+
"from typing import Protocol\nclass Publisher(Protocol):\n def publish(self) -> None: ...\nclass Consumer:\n def __init__(self, publisher: Publisher) -> None:\n self.publisher = publisher\n",
|
|
76
|
+
),
|
|
77
|
+
ExampleFile.python(
|
|
78
|
+
"app/fake.py",
|
|
79
|
+
"from app.consumer import Publisher\nclass FakePublisher(Publisher):\n def publish(self) -> None: ...\n",
|
|
80
|
+
),
|
|
81
|
+
ExampleFile.python(
|
|
82
|
+
"app/usage.py",
|
|
83
|
+
"from app.consumer import Consumer\nfrom app.fake import FakePublisher\ndef setup() -> None:\n Consumer(publisher=FakePublisher())\n",
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
focus_path=PurePosixPath("app/usage.py"),
|
|
87
|
+
expected_count=0,
|
|
88
|
+
public=True,
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
description = documentation.summary
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
96
|
+
if context.generated or context.tree is None or context.session.project is None:
|
|
97
|
+
return []
|
|
98
|
+
project = context.session.project
|
|
99
|
+
unit = project.unit_or_source(context.path, context.source, context.tree)
|
|
100
|
+
if unit is None:
|
|
101
|
+
return []
|
|
102
|
+
visitor = _InjectionVisitor(context, unit, project, self.code)
|
|
103
|
+
visitor.visit(context.tree)
|
|
104
|
+
return visitor.findings
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@final
|
|
108
|
+
class _InjectionVisitor(ast.NodeVisitor):
|
|
109
|
+
def __init__(self, context: PythonFileContext, unit: SourceUnit, project: ProjectIndexSet, code: str) -> None:
|
|
110
|
+
self.context = context
|
|
111
|
+
self.unit = unit
|
|
112
|
+
self.project = project
|
|
113
|
+
self.code = code
|
|
114
|
+
self.findings: list[Diagnostic] = []
|
|
115
|
+
self.origins: dict[str, ClassSummary] = {}
|
|
116
|
+
self.local_any_names: frozenset[str] = frozenset()
|
|
117
|
+
|
|
118
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
119
|
+
self._visit_function(node)
|
|
120
|
+
|
|
121
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
122
|
+
self._visit_function(node)
|
|
123
|
+
|
|
124
|
+
def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
125
|
+
previous = self.origins
|
|
126
|
+
previous_any = self.local_any_names
|
|
127
|
+
self.origins = {}
|
|
128
|
+
self.local_any_names = frozenset(
|
|
129
|
+
alias.asname or alias.name
|
|
130
|
+
for statement in node.body
|
|
131
|
+
if isinstance(statement, ast.ImportFrom) and statement.module in {"typing", "typing_extensions"}
|
|
132
|
+
for alias in statement.names
|
|
133
|
+
if alias.name == "Any"
|
|
134
|
+
)
|
|
135
|
+
for statement in node.body:
|
|
136
|
+
self.visit(statement)
|
|
137
|
+
self.origins = previous
|
|
138
|
+
self.local_any_names = previous_any
|
|
139
|
+
|
|
140
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
|
141
|
+
self.visit(node.value)
|
|
142
|
+
origin = self._origin(node.value)
|
|
143
|
+
for target in node.targets:
|
|
144
|
+
if isinstance(target, ast.Name):
|
|
145
|
+
if origin is None:
|
|
146
|
+
self.origins.pop(target.id, None)
|
|
147
|
+
else:
|
|
148
|
+
self.origins[target.id] = origin
|
|
149
|
+
else:
|
|
150
|
+
for name in (item.id for item in ast.walk(target) if isinstance(item, ast.Name)):
|
|
151
|
+
self.origins.pop(name, None)
|
|
152
|
+
|
|
153
|
+
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
|
|
154
|
+
if node.value is None:
|
|
155
|
+
return
|
|
156
|
+
self.visit(node.value)
|
|
157
|
+
if isinstance(node.target, ast.Name):
|
|
158
|
+
if self.context.imports.resolves(node.annotation, sources=TYPING_SOURCES, symbol="Any") or (
|
|
159
|
+
isinstance(node.annotation, ast.Name) and node.annotation.id in self.local_any_names
|
|
160
|
+
):
|
|
161
|
+
self.origins.pop(node.target.id, None)
|
|
162
|
+
return
|
|
163
|
+
origin = self._origin(node.value)
|
|
164
|
+
if origin is None:
|
|
165
|
+
self.origins.pop(node.target.id, None)
|
|
166
|
+
else:
|
|
167
|
+
self.origins[node.target.id] = origin
|
|
168
|
+
|
|
169
|
+
def visit_If(self, node: ast.If) -> None:
|
|
170
|
+
self.visit(node.test)
|
|
171
|
+
before = self.origins.copy()
|
|
172
|
+
for statement in node.body:
|
|
173
|
+
self.visit(statement)
|
|
174
|
+
after_body = self.origins
|
|
175
|
+
self.origins = before.copy()
|
|
176
|
+
for statement in node.orelse:
|
|
177
|
+
self.visit(statement)
|
|
178
|
+
after_else = self.origins
|
|
179
|
+
self.origins = {name: origin for name, origin in after_body.items() if after_else.get(name) == origin}
|
|
180
|
+
|
|
181
|
+
def visit_For(self, node: ast.For) -> None:
|
|
182
|
+
self._visit_uncertain_flow(node)
|
|
183
|
+
|
|
184
|
+
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
|
|
185
|
+
self._visit_uncertain_flow(node)
|
|
186
|
+
|
|
187
|
+
def visit_While(self, node: ast.While) -> None:
|
|
188
|
+
self._visit_uncertain_flow(node)
|
|
189
|
+
|
|
190
|
+
def visit_Try(self, node: ast.Try) -> None:
|
|
191
|
+
self._visit_uncertain_flow(node)
|
|
192
|
+
|
|
193
|
+
def visit_TryStar(self, node: ast.TryStar) -> None:
|
|
194
|
+
self._visit_uncertain_flow(node)
|
|
195
|
+
|
|
196
|
+
def visit_Match(self, node: ast.Match) -> None:
|
|
197
|
+
self._visit_uncertain_flow(node)
|
|
198
|
+
|
|
199
|
+
def _visit_uncertain_flow(self, node: ast.stmt) -> None:
|
|
200
|
+
self.generic_visit(node)
|
|
201
|
+
for child in ast.walk(node):
|
|
202
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, (ast.Store, ast.Del)):
|
|
203
|
+
self.origins.pop(child.id, None)
|
|
204
|
+
|
|
205
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
206
|
+
target = self.project.class_for(self.unit, node.func)
|
|
207
|
+
if target is not None:
|
|
208
|
+
target_unit = self.project.source_unit(target.symbol.module)
|
|
209
|
+
if target_unit is not None and target_unit.tree is not None:
|
|
210
|
+
declaration = next(
|
|
211
|
+
(
|
|
212
|
+
item
|
|
213
|
+
for item in target_unit.tree.body
|
|
214
|
+
if isinstance(item, ast.ClassDef) and item.name == target.symbol.name
|
|
215
|
+
),
|
|
216
|
+
None,
|
|
217
|
+
)
|
|
218
|
+
if declaration is not None:
|
|
219
|
+
self._check_constructor(node, declaration, target_unit)
|
|
220
|
+
self.generic_visit(node)
|
|
221
|
+
|
|
222
|
+
def _check_constructor(self, call: ast.Call, declaration: ast.ClassDef, target_unit: SourceUnit) -> None:
|
|
223
|
+
constructor = next(
|
|
224
|
+
(item for item in declaration.body if isinstance(item, ast.FunctionDef) and item.name == "__init__"),
|
|
225
|
+
None,
|
|
226
|
+
)
|
|
227
|
+
if constructor is None:
|
|
228
|
+
return
|
|
229
|
+
positional = [*constructor.args.posonlyargs, *constructor.args.args][1:]
|
|
230
|
+
parameters = {argument.arg: argument for argument in (*positional, *constructor.args.kwonlyargs)}
|
|
231
|
+
supplied = [
|
|
232
|
+
(parameters.get(keyword.arg), keyword.value) for keyword in call.keywords if keyword.arg is not None
|
|
233
|
+
]
|
|
234
|
+
supplied.extend((parameter, value) for parameter, value in zip(positional, call.args, strict=False))
|
|
235
|
+
for parameter, value in supplied:
|
|
236
|
+
self._check_argument(parameter, value, target_unit)
|
|
237
|
+
|
|
238
|
+
def _check_argument(self, parameter: ast.arg | None, value: ast.expr, target_unit: SourceUnit) -> None:
|
|
239
|
+
if parameter is None or parameter.annotation is None:
|
|
240
|
+
return
|
|
241
|
+
name = annotation_tail(parameter.annotation)
|
|
242
|
+
if name is None:
|
|
243
|
+
return
|
|
244
|
+
contract = self.project.class_for(target_unit, ast.Name(id=name, ctx=ast.Load()))
|
|
245
|
+
if contract is None or not _is_direct_contract(contract, self.project):
|
|
246
|
+
return
|
|
247
|
+
origin = self._origin(value)
|
|
248
|
+
if origin is None:
|
|
249
|
+
return
|
|
250
|
+
inherits = _inherits_contract(origin, contract.symbol, self.project, set())
|
|
251
|
+
if inherits is not False or is_suppressed(self.context.source_lines, value.lineno, self.code):
|
|
252
|
+
return
|
|
253
|
+
self.findings.append(
|
|
254
|
+
Diagnostic(
|
|
255
|
+
path=self.context.path,
|
|
256
|
+
line=value.lineno,
|
|
257
|
+
col=value.col_offset + 1,
|
|
258
|
+
code=self.code,
|
|
259
|
+
severity=Severity.WARNING,
|
|
260
|
+
message=(
|
|
261
|
+
f"`{origin.symbol.name}` is supplied as `{contract.symbol.name}` without explicitly "
|
|
262
|
+
"inheriting that contract. Declare the relationship on the implementation class."
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _origin(self, value: ast.expr) -> ClassSummary | None:
|
|
268
|
+
if isinstance(value, ast.Name):
|
|
269
|
+
return self.origins.get(value.id)
|
|
270
|
+
if isinstance(value, ast.Call):
|
|
271
|
+
return self.project.class_for(self.unit, value.func)
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_direct_contract(summary: ClassSummary, project: ProjectIndexSet) -> bool:
|
|
276
|
+
protocol = any(
|
|
277
|
+
(base.module, base.name) in {("typing", "Protocol"), ("typing_extensions", "Protocol")}
|
|
278
|
+
for base in summary.bases
|
|
279
|
+
)
|
|
280
|
+
abc = any((base.module, base.name) == ("abc", "ABC") for base in summary.bases)
|
|
281
|
+
if not protocol and not abc:
|
|
282
|
+
return False
|
|
283
|
+
unit = project.source_unit(summary.symbol.module)
|
|
284
|
+
if unit is None or unit.tree is None:
|
|
285
|
+
return False
|
|
286
|
+
declaration = next(
|
|
287
|
+
(item for item in unit.tree.body if isinstance(item, ast.ClassDef) and item.name == summary.symbol.name),
|
|
288
|
+
None,
|
|
289
|
+
)
|
|
290
|
+
if declaration is None:
|
|
291
|
+
return False
|
|
292
|
+
imports = ImportIndex.from_tree(unit.tree, module_scope_only=True)
|
|
293
|
+
return any(
|
|
294
|
+
isinstance(member, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
295
|
+
and not member.name.startswith("_")
|
|
296
|
+
and (
|
|
297
|
+
protocol
|
|
298
|
+
or any(
|
|
299
|
+
imports.resolves(decorator, sources=ABC_SOURCES, symbol="abstractmethod")
|
|
300
|
+
for decorator in member.decorator_list
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
for member in declaration.body
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _inherits_contract(
|
|
308
|
+
implementation: ClassSummary,
|
|
309
|
+
target: SymbolRef,
|
|
310
|
+
project: ProjectIndexSet,
|
|
311
|
+
seen: set[SymbolRef],
|
|
312
|
+
) -> bool | None:
|
|
313
|
+
if implementation.symbol == target:
|
|
314
|
+
return True
|
|
315
|
+
if implementation.symbol in seen:
|
|
316
|
+
return None
|
|
317
|
+
seen.add(implementation.symbol)
|
|
318
|
+
unknown = False
|
|
319
|
+
for base in implementation.bases:
|
|
320
|
+
if base == target:
|
|
321
|
+
return True
|
|
322
|
+
if (base.module, base.name) in {("abc", "ABC"), ("typing", "Generic"), ("builtins", "object")}:
|
|
323
|
+
continue
|
|
324
|
+
unit = project.source_unit(base.module)
|
|
325
|
+
ancestor = project.class_for(unit, ast.Name(id=base.name, ctx=ast.Load())) if unit is not None else None
|
|
326
|
+
if ancestor is None:
|
|
327
|
+
unknown = True
|
|
328
|
+
continue
|
|
329
|
+
relationship = _inherits_contract(ancestor, target, project, seen)
|
|
330
|
+
if relationship is True:
|
|
331
|
+
return True
|
|
332
|
+
unknown |= relationship is None
|
|
333
|
+
return None if unknown else False
|