sarj-python-lint 1.28.3__tar.gz → 1.28.5__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 (192) hide show
  1. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_file_context.py +6 -0
  4. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_docstrings.py +90 -0
  5. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +4 -1
  6. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +5 -2
  7. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +4 -2
  8. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_class_docstring.py +1 -89
  9. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_docstring.py +3 -90
  10. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_precise_factory_signature.py +45 -14
  11. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/.gitignore +0 -0
  12. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/LICENSE +0 -0
  13. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/README.md +0 -0
  14. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/__main__.py +0 -0
  16. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_analysis_session.py +0 -0
  17. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_filesystem.py +0 -0
  18. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_python_target.py +0 -0
  19. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  20. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_secret_names.py +0 -0
  21. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_source.py +0 -0
  22. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_version.py +0 -0
  23. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/benchmark.py +0 -0
  24. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/interpreter_argv.py +0 -0
  25. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/json_boundary.py +0 -0
  26. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/py.typed +0 -0
  27. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/ratchet.py +0 -0
  28. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rule_base.py +0 -0
  29. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/__init__.py +0 -0
  30. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  31. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  32. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  34. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_comments.py +0 -0
  35. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  36. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  37. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  38. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  39. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_first_party.py +0 -0
  40. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  41. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_imports.py +0 -0
  42. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_local_source.py +0 -0
  43. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_logging.py +0 -0
  44. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  45. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_paths.py +0 -0
  46. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_project_index.py +0 -0
  47. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  48. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_pytest.py +0 -0
  49. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_registry.py +0 -0
  50. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  51. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  52. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_sql.py +0 -0
  53. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  54. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  55. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  56. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  57. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  58. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  59. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  60. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  61. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  62. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  63. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  64. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  65. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  66. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  67. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  68. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  69. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  70. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  71. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  72. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  73. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  74. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  75. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  76. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  77. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  78. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  79. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  80. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  81. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  82. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  83. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  84. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  85. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  86. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  87. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  88. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  89. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  90. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  91. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  92. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  93. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  94. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  95. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  96. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  97. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  98. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  99. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  100. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  101. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  102. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  103. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  104. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  105. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  106. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  107. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  108. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  109. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  110. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  111. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  112. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  113. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  114. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  115. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  116. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  117. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  118. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  119. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  120. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  121. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  122. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  123. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  124. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  125. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  126. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  127. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  128. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  129. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  130. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  131. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  132. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  133. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  134. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  135. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  136. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  137. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  138. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  139. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  140. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  141. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  142. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  143. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  144. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  145. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  146. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  147. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  148. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  149. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  150. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  151. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  152. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  153. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  154. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  155. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  156. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  157. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  158. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  159. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  160. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  161. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  162. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  163. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  164. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  165. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  166. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  167. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  168. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  169. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  170. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  171. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  172. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  173. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  174. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  175. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  176. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  177. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  178. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  179. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  180. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  181. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  182. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  183. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/stepdown.py +0 -0
  184. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  185. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  186. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  187. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  188. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  189. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  190. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  191. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  192. {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/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.28.3
3
+ Version: 1.28.5
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.28.3"
3
+ version = "1.28.5"
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" }]
@@ -61,6 +61,12 @@ class PythonFileContext:
61
61
  def has_wildcard_import(self) -> bool:
62
62
  return any(alias.name == "*" for node in self.nodes(ast.ImportFrom) for alias in node.names)
63
63
 
64
+ @cached_property
65
+ def consumed_docstring_names(self) -> frozenset[str]:
66
+ from sarj_python_lint.rules._docstrings import consumed_docstring_names # ruff: ignore[import-outside-top-level] -- share runtime docstring analysis only when requested.
67
+
68
+ return consumed_docstring_names(self._valid_tree(), node_index=self.node_index)
69
+
64
70
  @property
65
71
  def parents(self) -> Mapping[ast.AST, ast.AST]:
66
72
  return self.node_index.parents
@@ -5,12 +5,17 @@ from itertools import starmap
5
5
  import re
6
6
  from typing import TYPE_CHECKING, NamedTuple
7
7
 
8
+ from sarj_python_lint.rules._ast_index import walk as walk_ast
8
9
  from sarj_python_lint.rules._comments import split_identifier, stem
9
10
 
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from collections.abc import Iterable
13
14
 
15
+ from sarj_python_lint.rules._ast_index import NodeIndex
16
+
17
+
18
+ _GETATTR_MIN_ARGS = 2
14
19
 
15
20
  # Docstring filler that says nothing about *which* thing is being described.
16
21
  _BASE_STOPWORDS = frozenset(
@@ -325,3 +330,88 @@ def restates(text: str, known: Iterable[str]) -> bool:
325
330
  if not content:
326
331
  return False
327
332
  return all(stem(word) in known_stems for word in content)
333
+
334
+
335
+ def consumed_docstring_names(tree: ast.Module, *, node_index: NodeIndex | None = None) -> frozenset[str]:
336
+ consumed: set[str] = set()
337
+ aliases: dict[str, set[str]] = {}
338
+ for node in walk_ast(tree, index=node_index):
339
+ _record_docstring_use(node, consumed, aliases)
340
+ changed = True
341
+ while changed:
342
+ changed = False
343
+ for alias, originals in aliases.items():
344
+ if alias not in consumed:
345
+ continue
346
+ before = len(consumed)
347
+ consumed.update(originals)
348
+ changed |= len(consumed) != before
349
+ return frozenset(consumed)
350
+
351
+
352
+ def _terminal_name(node: ast.expr) -> str | None:
353
+ match node:
354
+ case ast.Name():
355
+ return node.id
356
+ case ast.Attribute():
357
+ return node.attr
358
+ case _:
359
+ return None
360
+
361
+
362
+ def _record_docstring_use(node: ast.AST, consumed: set[str], aliases: dict[str, set[str]]) -> None:
363
+ if isinstance(node, ast.Attribute) and node.attr == "__doc__":
364
+ if name := _terminal_name(node.value):
365
+ consumed.add(name)
366
+ elif isinstance(node, ast.Call) and _is_docstring_reader(node):
367
+ if name := _terminal_name(node.args[0]):
368
+ consumed.add(name)
369
+ elif isinstance(node, ast.Subscript):
370
+ if name := _subscripted_docstring_owner(node):
371
+ consumed.add(name)
372
+ else:
373
+ _record_docstring_alias(node, aliases)
374
+
375
+
376
+ def _subscripted_docstring_owner(node: ast.Subscript) -> str | None:
377
+ if not isinstance(node.slice, ast.Constant) or node.slice.value != "__doc__":
378
+ return None
379
+ value = node.value
380
+ if isinstance(value, ast.Attribute) and value.attr == "__dict__":
381
+ return _terminal_name(value.value)
382
+ if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "vars" and value.args:
383
+ return _terminal_name(value.args[0])
384
+ return None
385
+
386
+
387
+ def _is_docstring_reader(node: ast.Call) -> bool:
388
+ if not node.args:
389
+ return False
390
+ function = node.func
391
+ if isinstance(function, ast.Name) and function.id == "help":
392
+ return True
393
+ if isinstance(function, ast.Attribute) and function.attr in {"getdoc", "render_doc"}:
394
+ return True
395
+ return (
396
+ isinstance(function, ast.Name)
397
+ and function.id == "getattr"
398
+ and len(node.args) >= _GETATTR_MIN_ARGS
399
+ and isinstance(node.args[1], ast.Constant)
400
+ and node.args[1].value == "__doc__"
401
+ )
402
+
403
+
404
+ def _record_docstring_alias(node: ast.AST, aliases: dict[str, set[str]]) -> None:
405
+ if isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Attribute)):
406
+ original = _terminal_name(node.value)
407
+ for target in node.targets:
408
+ if isinstance(target, ast.Name) and original is not None:
409
+ aliases.setdefault(target.id, set()).add(original)
410
+ elif (
411
+ isinstance(node, ast.AnnAssign)
412
+ and isinstance(node.target, ast.Name)
413
+ and isinstance(node.value, (ast.Name, ast.Attribute))
414
+ ):
415
+ original = _terminal_name(node.value)
416
+ if original is not None:
417
+ aliases.setdefault(node.target.id, set()).add(original)
@@ -64,13 +64,16 @@ class DefectXfailRequiresExplicitStrict(Rule):
64
64
  ),
65
65
  remediation=(
66
66
  "Set literal `strict=True` so an unexpected pass fails. If only some parameter cases carry the defect, "
67
- "move the marker to those `pytest.param(...)` cases before making it strict."
67
+ "move the marker to those `pytest.param(...)` cases before making it strict. When the defect's exception "
68
+ "type or group shape is independently known, native `raises=...`, where supported, can narrow unrelated failures; "
69
+ "same-type failures still need an origin oracle. Keep intentional broad expected-failure policies."
68
70
  ),
69
71
  category=RuleCategory.TESTING,
70
72
  aliases=("defect-xfail-requires-strict", "xfail-requires-strict"),
71
73
  limitations=(
72
74
  "Only markers resolved through an unambiguous pytest or pytest.mark import are analyzed.",
73
75
  "Only xfail reasons that explicitly identify a defect are analyzed.",
76
+ "Exception narrowing and defect identity remain manual review; strictness or a typed marker alone does not establish failure origin.",
74
77
  "Nondeterministic, recognized Hypothesis, network, environment-gated, run=False, and statically disabled pins are excluded.",
75
78
  "Repository-level strict_xfail / xfail_strict configuration is intentionally not consulted: defect pins remain locally explicit.",
76
79
  "Default pytest test_/Test collection names are recognized; repositories with custom collection patterns may need a local suppression.",
@@ -124,13 +124,16 @@ class PreferFstringOverConcat(Rule):
124
124
  summary="Prefer f-strings for short human-readable interpolation when they make the result clearer.",
125
125
  rationale="For prose-like strings, f-strings keep interpolated values and surrounding text visible together.",
126
126
  remediation=(
127
- "Replace the interpolation with one f-string. Preserve an explicit `str(value)` conversion as `{value!s}` "
128
- "and `repr(value)` as `{value!r}` when those conversion semantics matter."
127
+ "Use one f-string when it preserves the required conversion and operator semantics. Preserve an explicit "
128
+ "`str(value)` conversion as `{value!s}` "
129
+ "and `repr(value)` as `{value!r}` when those conversion semantics matter. Retain required custom "
130
+ "concatenation or formatting behavior; a `str` annotation alone does not prove equivalence."
129
131
  ),
130
132
  category=RuleCategory.STYLE,
131
133
  autofix=AutofixPolicy.NONE,
132
134
  limitations=(
133
135
  "Runtime operands must have concrete string evidence.",
136
+ "String evidence does not prove that custom concatenation operators and `__format__` agree; this remains manual review.",
134
137
  (
135
138
  "Logging, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long "
136
139
  "literals, and punctuation-oriented fragment composition are excluded."
@@ -55,7 +55,9 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
55
55
  "fake when rebuilding the fixture would add wiring without improving the boundary. Keep attribute-existence "
56
56
  "checks enabled and the tested implementation intact. Suppress SARJ445 only at that call, naming the "
57
57
  "owned instance and injection seam. For inherently global boundaries or lookup interception, explain why "
58
- "injection would invalidate the test. Do not switch to bare assignment merely to avoid this rule."
58
+ "injection would invalidate the test. Establish the binding the exercised consumer actually reads, "
59
+ "including already-bound import aliases, and show that the oracle rejects a wrong consumer. "
60
+ "Do not switch to bare assignment merely to avoid this rule."
59
61
  ),
60
62
  category=RuleCategory.TESTING,
61
63
  autofix=AutofixPolicy.NONE,
@@ -65,7 +67,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
65
67
  "Environment, mapping, import-path, and working-directory mutations are intentionally allowed.",
66
68
  "Untyped handles are recognized only on pytest tests or fixtures; directly parametrized values are excluded.",
67
69
  "A nested function that captures a monkeypatch handle from an outer scope is not inferred.",
68
- "Instance ownership and collaborator contracts are not inferred; justified scoped patches need a local suppression.",
70
+ "Instance ownership, collaborator contracts, and active consumer lookups are not inferred; justified scoped patches need a local suppression.",
69
71
  ),
70
72
  examples=(
71
73
  RuleExample(
@@ -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._comments import is_protected, stem
22
21
  from sarj_python_lint.rules._docstrings import (
23
22
  VALUE_MARKER_RE,
@@ -27,11 +26,9 @@ from sarj_python_lint.rules._docstrings import (
27
26
 
28
27
  if TYPE_CHECKING:
29
28
  from sarj_python_lint._file_context import PythonFileContext
30
- from sarj_python_lint.rules._ast_index import NodeIndex
31
29
 
32
30
 
33
31
  _GRAMMATICAL_FILLER = frozenset({"a", "an", "class", "the"})
34
- _GETATTR_MIN_ARGS = 2
35
32
  _DOCSTRING_TOKEN_RE = re.compile(r"[^\W\d_]+(?:'[^\W\d_]+)?|\d+")
36
33
 
37
34
 
@@ -93,7 +90,7 @@ class RedundantClassDocstring(Rule):
93
90
  if tree is None:
94
91
  return []
95
92
  source_lines = context.source_lines
96
- consumed_docstrings = _consumed_docstring_names(tree, node_index=context.node_index)
93
+ consumed_docstrings = context.consumed_docstring_names
97
94
  diags: list[Diagnostic] = []
98
95
  for node in context.nodes(ast.ClassDef):
99
96
  if node.name not in consumed_docstrings and self._is_ceremony(node):
@@ -128,88 +125,3 @@ class RedundantClassDocstring(Rule):
128
125
  if (word := match.group(0).lower()) not in _GRAMMATICAL_FILLER
129
126
  ]
130
127
  return bool(content) and all(stem(word) in known for word in content)
131
-
132
-
133
- def _consumed_docstring_names(tree: ast.Module, *, node_index: NodeIndex | None = None) -> set[str]:
134
- consumed: set[str] = set()
135
- aliases: dict[str, set[str]] = {}
136
- for node in walk_ast(tree, index=node_index):
137
- _record_docstring_use(node, consumed, aliases)
138
- changed = True
139
- while changed:
140
- changed = False
141
- for alias, originals in aliases.items():
142
- if alias not in consumed:
143
- continue
144
- before = len(consumed)
145
- consumed.update(originals)
146
- changed |= len(consumed) != before
147
- return consumed
148
-
149
-
150
- def _terminal_name(node: ast.expr) -> str | None:
151
- match node:
152
- case ast.Name():
153
- return node.id
154
- case ast.Attribute():
155
- return node.attr
156
- case _:
157
- return None
158
-
159
-
160
- def _record_docstring_use(node: ast.AST, consumed: set[str], aliases: dict[str, set[str]]) -> None:
161
- if isinstance(node, ast.Attribute) and node.attr == "__doc__":
162
- if name := _terminal_name(node.value):
163
- consumed.add(name)
164
- elif isinstance(node, ast.Call) and _is_docstring_reader(node):
165
- if name := _terminal_name(node.args[0]):
166
- consumed.add(name)
167
- elif isinstance(node, ast.Subscript):
168
- if name := _subscripted_docstring_owner(node):
169
- consumed.add(name)
170
- else:
171
- _record_docstring_alias(node, aliases)
172
-
173
-
174
- def _subscripted_docstring_owner(node: ast.Subscript) -> str | None:
175
- if not isinstance(node.slice, ast.Constant) or node.slice.value != "__doc__":
176
- return None
177
- value = node.value
178
- if isinstance(value, ast.Attribute) and value.attr == "__dict__":
179
- return _terminal_name(value.value)
180
- if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "vars" and value.args:
181
- return _terminal_name(value.args[0])
182
- return None
183
-
184
-
185
- def _is_docstring_reader(node: ast.Call) -> bool:
186
- if not node.args:
187
- return False
188
- function = node.func
189
- if isinstance(function, ast.Name) and function.id == "help":
190
- return True
191
- if isinstance(function, ast.Attribute) and function.attr in {"getdoc", "render_doc"}:
192
- return True
193
- return (
194
- isinstance(function, ast.Name)
195
- and function.id == "getattr"
196
- and len(node.args) >= _GETATTR_MIN_ARGS
197
- and isinstance(node.args[1], ast.Constant)
198
- and node.args[1].value == "__doc__"
199
- )
200
-
201
-
202
- def _record_docstring_alias(node: ast.AST, aliases: dict[str, set[str]]) -> None:
203
- if isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Attribute)):
204
- original = _terminal_name(node.value)
205
- for target in node.targets:
206
- if isinstance(target, ast.Name) and original is not None:
207
- aliases.setdefault(target.id, set()).add(original)
208
- elif (
209
- isinstance(node, ast.AnnAssign)
210
- and isinstance(node.target, ast.Name)
211
- and isinstance(node.value, (ast.Name, ast.Attribute))
212
- ):
213
- original = _terminal_name(node.value)
214
- if original is not None:
215
- aliases.setdefault(node.target.id, set()).add(original)
@@ -18,7 +18,7 @@ from sarj_python_lint.rule_base import (
18
18
  Severity,
19
19
  is_suppressed,
20
20
  )
21
- from sarj_python_lint.rules._ast_index import children, walk as walk_ast
21
+ from sarj_python_lint.rules._ast_index import children
22
22
  from sarj_python_lint.rules._comments import is_protected, split_identifier, stem
23
23
  from sarj_python_lint.rules._docstrings import (
24
24
  VALUE_MARKER_RE,
@@ -28,12 +28,10 @@ from sarj_python_lint.rules._docstrings import (
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from sarj_python_lint._file_context import PythonFileContext
31
- from sarj_python_lint.rules._ast_index import NodeIndex
32
31
 
33
32
 
34
33
  _DOCSTRING_TOKEN_RE = re.compile(r"[^\W\d_]+(?:'[^\W\d_]+)?|\d+")
35
34
  _GRAMMATICAL_FILLER = frozenset({"a", "an", "function", "method", "the"})
36
- _GETATTR_MIN_ARGS = 2
37
35
  _MULTI_PARAMETER_MIN = 2
38
36
 
39
37
 
@@ -41,7 +39,7 @@ _MULTI_PARAMETER_MIN = 2
41
39
  class _ScanContext:
42
40
  path: Path
43
41
  source_lines: list[str]
44
- consumed_docstrings: set[str]
42
+ consumed_docstrings: frozenset[str]
45
43
  diagnostics: list[Diagnostic]
46
44
 
47
45
 
@@ -105,7 +103,7 @@ class RedundantDocstring(Rule):
105
103
  if tree is None:
106
104
  return []
107
105
  source_lines = context.source_lines
108
- consumed_docstrings = _consumed_docstring_names(tree, node_index=context.node_index)
106
+ consumed_docstrings = context.consumed_docstring_names
109
107
  diags: list[Diagnostic] = []
110
108
  analysis_context = _ScanContext(path, source_lines, consumed_docstrings, diags)
111
109
  self._walk(tree, analysis_context)
@@ -190,88 +188,3 @@ def _parameters_are_distinguished(groups: list[set[str]], content: set[str]) ->
190
188
  if not distinguishing or distinguishing.isdisjoint(content):
191
189
  return False
192
190
  return True
193
-
194
-
195
- def _consumed_docstring_names(tree: ast.Module, *, node_index: NodeIndex | None = None) -> set[str]:
196
- consumed: set[str] = set()
197
- aliases: dict[str, set[str]] = {}
198
- for node in walk_ast(tree, index=node_index):
199
- _record_docstring_use(node, consumed, aliases)
200
- changed = True
201
- while changed:
202
- changed = False
203
- for alias, originals in aliases.items():
204
- if alias not in consumed:
205
- continue
206
- before = len(consumed)
207
- consumed.update(originals)
208
- changed |= len(consumed) != before
209
- return consumed
210
-
211
-
212
- def _terminal_name(node: ast.expr) -> str | None:
213
- match node:
214
- case ast.Name():
215
- return node.id
216
- case ast.Attribute():
217
- return node.attr
218
- case _:
219
- return None
220
-
221
-
222
- def _record_docstring_use(node: ast.AST, consumed: set[str], aliases: dict[str, set[str]]) -> None:
223
- if isinstance(node, ast.Attribute) and node.attr == "__doc__":
224
- if name := _terminal_name(node.value):
225
- consumed.add(name)
226
- elif isinstance(node, ast.Call) and _is_docstring_reader(node):
227
- if name := _terminal_name(node.args[0]):
228
- consumed.add(name)
229
- elif isinstance(node, ast.Subscript):
230
- if name := _subscripted_docstring_owner(node):
231
- consumed.add(name)
232
- else:
233
- _record_docstring_alias(node, aliases)
234
-
235
-
236
- def _subscripted_docstring_owner(node: ast.Subscript) -> str | None:
237
- if not isinstance(node.slice, ast.Constant) or node.slice.value != "__doc__":
238
- return None
239
- value = node.value
240
- if isinstance(value, ast.Attribute) and value.attr == "__dict__":
241
- return _terminal_name(value.value)
242
- if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "vars" and value.args:
243
- return _terminal_name(value.args[0])
244
- return None
245
-
246
-
247
- def _is_docstring_reader(node: ast.Call) -> bool:
248
- if not node.args:
249
- return False
250
- function = node.func
251
- if isinstance(function, ast.Name) and function.id == "help":
252
- return True
253
- if isinstance(function, ast.Attribute) and function.attr in {"getdoc", "render_doc"}:
254
- return True
255
- return (
256
- isinstance(function, ast.Name)
257
- and function.id == "getattr"
258
- and len(node.args) >= _GETATTR_MIN_ARGS
259
- and isinstance(node.args[1], ast.Constant)
260
- and node.args[1].value == "__doc__"
261
- )
262
-
263
-
264
- def _record_docstring_alias(node: ast.AST, aliases: dict[str, set[str]]) -> None:
265
- if isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Attribute)):
266
- original = _terminal_name(node.value)
267
- for target in node.targets:
268
- if isinstance(target, ast.Name) and original is not None:
269
- aliases.setdefault(target.id, set()).add(original)
270
- elif (
271
- isinstance(node, ast.AnnAssign)
272
- and isinstance(node.target, ast.Name)
273
- and isinstance(node.value, (ast.Name, ast.Attribute))
274
- ):
275
- original = _terminal_name(node.value)
276
- if original is not None:
277
- aliases.setdefault(node.target.id, set()).add(original)
@@ -35,7 +35,7 @@ class RequirePreciseFactorySignature(ProjectRule):
35
35
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
36
36
  default_level=Severity.WARNING,
37
37
  summary="Keep fixed keyword factory calls behind a precise callable signature.",
38
- rationale="Callable[..., T] erases argument checking, so misspelled keywords and incompatible injected factories pass strict typing and can fail at runtime.",
38
+ rationale="Callable[..., T] and functools.partial[T] erase argument checking at injected boundaries, so misspelled keywords and incompatible factories pass strict typing and can fail at runtime.",
39
39
  remediation=(
40
40
  "If factory substitution is unnecessary, call the existing constructor directly. Otherwise reuse an "
41
41
  "existing precise callable contract; use type[T] when the dependency requires a class, accounting "
@@ -46,7 +46,7 @@ class RequirePreciseFactorySignature(ProjectRule):
46
46
  category=RuleCategory.ARCHITECTURE,
47
47
  autofix=AutofixPolicy.NONE,
48
48
  limitations=(
49
- "Only undecorated module-level functions with proven standard Callable[..., OwnedClass] parameters directly called with at least one named keyword are considered. Imported classes require scanned first-party declarations.",
49
+ "Only undecorated module-level functions with proven standard Callable[..., OwnedClass] or functools.partial[OwnedClass] parameters directly called with at least one named keyword are considered. Imported classes require scanned first-party declarations; direct partial construction and calls remain native type-checker ownership.",
50
50
  "Tests, test support, generated code, unresolved/string/generic annotations, positional-only calls, unpacking, forwarding, nested captures, rebinding and ambiguous imports are excluded. Import capture checks are conservative across the file, so an unrelated local pattern, exception or walrus binding can prevent a warning.",
51
51
  "A fixed call demonstrates signature erasure, not a current runtime defect. Genuine dynamic APIs can use a reasoned suppression. No autofix: the correct contract depends on whether classes or function factories are intended.",
52
52
  ),
@@ -65,6 +65,36 @@ class RequirePreciseFactorySignature(ProjectRule):
65
65
  expected_count=1,
66
66
  public=True,
67
67
  ),
68
+ RuleExample(
69
+ example_id="erased-partial-factory",
70
+ title="An injected partial retains its result type but loses argument checking",
71
+ outcome=ExampleOutcome.MATCH,
72
+ files=(
73
+ ExampleFile.python(
74
+ "app/service.py",
75
+ "from functools import partial\nclass Result: pass\ndef build(factory: partial[Result]):\n return factory(size=4)\n",
76
+ ),
77
+ ),
78
+ focus_path=PurePosixPath("app/service.py"),
79
+ expected_count=1,
80
+ scenario="partial-erasure",
81
+ public=True,
82
+ ),
83
+ RuleExample(
84
+ example_id="direct-partial-factory",
85
+ title="Direct partial construction keeps native argument checking",
86
+ outcome=ExampleOutcome.NO_MATCH,
87
+ files=(
88
+ ExampleFile.python(
89
+ "app/service.py",
90
+ "from functools import partial\nclass Result:\n def __init__(self, *, size: int):\n self.size = size\ndef build():\n factory = partial(Result)\n return factory(size=4)\n",
91
+ ),
92
+ ),
93
+ focus_path=PurePosixPath("app/service.py"),
94
+ expected_count=0,
95
+ scenario="partial-erasure",
96
+ public=True,
97
+ ),
68
98
  RuleExample(
69
99
  example_id="exact-factory",
70
100
  title="Factory declares its keyword contract",
@@ -113,7 +143,7 @@ class RequirePreciseFactorySignature(ProjectRule):
113
143
  col=argument.col_offset + 1,
114
144
  code=self.code,
115
145
  message=(
116
- f"`{argument.arg}` is called with fixed keywords but Callable[..., T] erases its "
146
+ f"`{argument.arg}` is called with fixed keywords but its factory annotation erases its "
117
147
  "argument contract; remove unnecessary factory indirection or reuse a precise callable "
118
148
  "contract that preserves the accepted keywords."
119
149
  ),
@@ -124,19 +154,20 @@ class RequirePreciseFactorySignature(ProjectRule):
124
154
 
125
155
 
126
156
  def _factory_result(annotation: ast.expr | None, context: PythonFileContext) -> ast.Name | ast.Attribute | None:
127
- if (
128
- not isinstance(annotation, ast.Subscript)
129
- or context.module_imports.resolved_qualified_name(annotation.value) not in _CALLABLES
130
- ):
157
+ if not isinstance(annotation, ast.Subscript):
131
158
  return None
132
- if not isinstance(annotation.slice, ast.Tuple) or len(annotation.slice.elts) != _CALLABLE_ARITY:
159
+ carrier = context.module_imports.resolved_qualified_name(annotation.value)
160
+ if carrier in _CALLABLES:
161
+ if not isinstance(annotation.slice, ast.Tuple) or len(annotation.slice.elts) != _CALLABLE_ARITY:
162
+ return None
163
+ parameters, result = annotation.slice.elts
164
+ if not isinstance(parameters, ast.Constant) or parameters.value is not Ellipsis:
165
+ return None
166
+ elif carrier == "functools.partial":
167
+ result = annotation.slice
168
+ else:
133
169
  return None
134
- parameters, result = annotation.slice.elts
135
- if (
136
- not isinstance(parameters, ast.Constant)
137
- or parameters.value is not Ellipsis
138
- or not isinstance(result, (ast.Name, ast.Attribute))
139
- ):
170
+ if not isinstance(result, (ast.Name, ast.Attribute)):
140
171
  return None
141
172
  root = _root(annotation.value)
142
173
  if (root is not None and _captured_import_root(root, context)) or any(