sarj-python-lint 1.7.2__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.2 → sarj_python_lint-1.7.4}/PKG-INFO +1 -1
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/pyproject.toml +1 -1
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/__main__.py +1 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_imports.py +4 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_project_index.py +18 -2
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_registry.py +6 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +28 -7
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +8 -8
- sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +333 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +1 -1
- sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_explicit_service_contract.py +343 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/.gitignore +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/LICENSE +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/README.md +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.7.2 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.7.2 → 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.2 → 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,
|
|
@@ -17,7 +17,6 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
Severity,
|
|
18
18
|
is_suppressed,
|
|
19
19
|
)
|
|
20
|
-
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
21
20
|
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
22
21
|
|
|
23
22
|
|
|
@@ -66,7 +65,7 @@ class NoServiceBehaviorInSettings(Rule):
|
|
|
66
65
|
autofix=AutofixPolicy.NONE,
|
|
67
66
|
limitations=(
|
|
68
67
|
"Only direct module classes named *Settings, *Config, *Configuration, or *Options are checked.",
|
|
69
|
-
"A finding requires a typed collaborator field and an ordinary public method that calls that field; pure computations, properties, validators, serializers, factories, generated files, and tests are excluded.",
|
|
68
|
+
"A finding requires a typed collaborator field and an ordinary public method that calls that field; pure computations, properties, validators, serializers, factories, generated files, and tests are excluded. Nested scopes, containers of collaborators, class objects, and methods that rebind self are not inferred.",
|
|
70
69
|
"The rule does not require every data object to use a particular record library and does not infer behavior from a filename alone.",
|
|
71
70
|
),
|
|
72
71
|
examples=(
|
|
@@ -180,10 +179,19 @@ def _collaborator_fields(node: ast.ClassDef) -> frozenset[str]:
|
|
|
180
179
|
def _is_collaborator_annotation(annotation: ast.expr | None) -> bool:
|
|
181
180
|
if annotation is None:
|
|
182
181
|
return False
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
|
|
183
|
+
return _is_collaborator_annotation(annotation.left) or _is_collaborator_annotation(annotation.right)
|
|
184
|
+
if isinstance(annotation, ast.Subscript):
|
|
185
|
+
wrapper = _dotted_tail(annotation.value)
|
|
186
|
+
if wrapper in {"Optional", "Union"}:
|
|
187
|
+
members = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
|
|
188
|
+
return any(_is_collaborator_annotation(member) for member in members)
|
|
189
|
+
if wrapper == "Annotated" and isinstance(annotation.slice, ast.Tuple) and annotation.slice.elts:
|
|
190
|
+
return _is_collaborator_annotation(annotation.slice.elts[0])
|
|
191
|
+
return _is_collaborator_annotation(annotation.value)
|
|
192
|
+
return (
|
|
193
|
+
isinstance(annotation, (ast.Name, ast.Attribute))
|
|
194
|
+
and _COLLABORATOR_RE.search(_dotted_tail(annotation) or "") is not None
|
|
187
195
|
)
|
|
188
196
|
|
|
189
197
|
|
|
@@ -198,9 +206,22 @@ def _is_behavioral_method(method: ast.FunctionDef | ast.AsyncFunctionDef) -> boo
|
|
|
198
206
|
|
|
199
207
|
|
|
200
208
|
def _calls_collaborator(method: ast.FunctionDef | ast.AsyncFunctionDef, fields: frozenset[str]) -> bool:
|
|
209
|
+
nodes: list[ast.AST] = []
|
|
210
|
+
pending: list[ast.AST] = list(method.body)
|
|
211
|
+
while pending:
|
|
212
|
+
node = pending.pop()
|
|
213
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
214
|
+
continue
|
|
215
|
+
nodes.append(node)
|
|
216
|
+
pending.extend(ast.iter_child_nodes(node))
|
|
217
|
+
if any(
|
|
218
|
+
isinstance(node, ast.Name) and node.id == "self" and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
219
|
+
for node in nodes
|
|
220
|
+
):
|
|
221
|
+
return False
|
|
201
222
|
return any(
|
|
202
223
|
(field := _self_field(call.func)) is not None and field in fields
|
|
203
|
-
for call in
|
|
224
|
+
for call in nodes
|
|
204
225
|
if isinstance(call, ast.Call)
|
|
205
226
|
)
|
|
206
227
|
|
|
@@ -219,14 +219,14 @@ def _is_syntax_required(owner: ast.Module | ast.ClassDef | ast.FunctionDef | ast
|
|
|
219
219
|
def _owner_keys(tree: ast.Module) -> dict[int, str]:
|
|
220
220
|
keys = {id(tree): "__doc__"}
|
|
221
221
|
|
|
222
|
-
def visit(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
visit(tree
|
|
222
|
+
def visit(node: ast.AST, prefix: tuple[str, ...]) -> None:
|
|
223
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
224
|
+
prefix = (*prefix, node.name)
|
|
225
|
+
keys[id(node)] = ".".join(prefix)
|
|
226
|
+
for child in ast.iter_child_nodes(node):
|
|
227
|
+
visit(child, prefix)
|
|
228
|
+
|
|
229
|
+
visit(tree, ())
|
|
230
230
|
return keys
|
|
231
231
|
|
|
232
232
|
|
|
@@ -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
|
|
@@ -55,7 +55,7 @@ class RequireExplicitPsycopgTransaction(Rule):
|
|
|
55
55
|
"Inspects literal SQL, simple local/module constants, and psycopg.sql.SQL wrappers. Dynamic SQL and unsupported SQL syntax are excluded.",
|
|
56
56
|
"A locking SELECT followed by a write on the same connection outside transaction() is reported; a single statement containing a mutation is treated as atomic. Multiple writes on a reachable local path are a warning because independent writes can be intentional.",
|
|
57
57
|
"Loops, exception handlers, match statements, comprehensions, and nested closures are not analyzed as execution paths. Manual BEGIN/commit protocols are not inferred. Tests, support fixtures, and generated files are excluded.",
|
|
58
|
-
"The rule cannot prove isolation level, database constraints, or application-level atomicity; fault-injection integration tests remain necessary.",
|
|
58
|
+
"Non-autocommit connections can already provide implicit transaction protection. A manual commit is not itself evidence of a defect or of safety in autocommit mode; document the proven connection configuration when retaining that protocol. The rule cannot prove isolation level, database constraints, or application-level atomicity; fault-injection integration tests remain necessary.",
|
|
59
59
|
),
|
|
60
60
|
examples=(
|
|
61
61
|
RuleExample(
|