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.
Files changed (162) hide show
  1. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/__main__.py +1 -0
  4. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_imports.py +4 -0
  5. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_project_index.py +18 -2
  6. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_registry.py +6 -0
  7. sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +333 -0
  8. sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_explicit_service_contract.py +343 -0
  9. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_port_for_service.py +22 -20
  10. sarj_python_lint-1.7.4/src/sarj_python_lint/rules/require_public_dependency_contract.py +195 -0
  11. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/.gitignore +0 -0
  12. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/LICENSE +0 -0
  13. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/README.md +0 -0
  14. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_analysis_session.py +0 -0
  16. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_file_context.py +0 -0
  17. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_filesystem.py +0 -0
  18. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_python_target.py +0 -0
  19. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  20. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_secret_names.py +0 -0
  21. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/_version.py +0 -0
  22. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/benchmark.py +0 -0
  23. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/json_boundary.py +0 -0
  24. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/py.typed +0 -0
  25. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/ratchet.py +0 -0
  26. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rule_base.py +0 -0
  27. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  29. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  30. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  31. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  33. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  34. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  35. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
  36. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  37. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_logging.py +0 -0
  38. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  39. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_paths.py +0 -0
  40. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  41. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
  42. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  43. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  44. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_sql.py +0 -0
  45. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  46. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  47. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  48. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  49. {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
  50. {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
  51. {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
  52. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  53. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  54. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  55. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  56. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  57. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  58. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  59. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  60. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  61. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  62. {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
  63. {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
  64. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  65. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  66. {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
  67. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  68. {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
  69. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  70. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  71. {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
  72. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  73. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  74. {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
  75. {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
  76. {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
  77. {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
  78. {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
  79. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  80. {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
  81. {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
  82. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  83. {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
  84. {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
  85. {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
  86. {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
  87. {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
  88. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  89. {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
  90. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  91. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  92. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  93. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  94. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  95. {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
  96. {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
  97. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  98. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  99. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  100. {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
  101. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  102. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  103. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  104. {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
  105. {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
  106. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  107. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  108. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  109. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  110. {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
  111. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  112. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  113. {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
  114. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  115. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  116. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  117. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  118. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  119. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  120. {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
  121. {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
  122. {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
  123. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  124. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  125. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  126. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  127. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  128. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  129. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  130. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  131. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  132. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  133. {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
  134. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  135. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  136. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  137. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  138. {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
  139. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  140. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  141. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  142. {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
  143. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  144. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  145. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  146. {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
  147. {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
  148. {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
  149. {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
  150. {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
  151. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  152. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  153. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
  154. {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
  155. {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
  156. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  157. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  158. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  159. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  160. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  161. {sarj_python_lint-1.7.3 → sarj_python_lint-1.7.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  162. {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
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.7.3"
3
+ version = "1.7.4"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -161,6 +161,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
161
161
  # an all-rules run emits one actionable diagnostic for this overlap.
162
162
  "SARJ447": frozenset({"SARJ008"}),
163
163
  "SARJ457": frozenset({"SARJ045"}),
164
+ "SARJ465": frozenset({"SARJ071"}),
164
165
  }
165
166
  )
166
167
 
@@ -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
@@ -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