sarj-python-lint 1.28.2__tar.gz → 1.28.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 (192) hide show
  1. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_file_context.py +10 -0
  4. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_docstrings.py +90 -0
  5. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +4 -3
  6. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +1 -8
  7. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +1 -10
  8. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +1 -9
  9. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +1 -9
  10. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +1 -89
  11. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/redundant_docstring.py +3 -90
  12. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +4 -1
  13. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +2 -0
  14. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/.gitignore +0 -0
  15. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/LICENSE +0 -0
  16. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/README.md +0 -0
  17. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/__init__.py +0 -0
  18. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/__main__.py +0 -0
  19. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_analysis_session.py +0 -0
  20. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_filesystem.py +0 -0
  21. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_python_target.py +0 -0
  22. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  23. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_secret_names.py +0 -0
  24. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_source.py +0 -0
  25. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/_version.py +0 -0
  26. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/benchmark.py +0 -0
  27. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/interpreter_argv.py +0 -0
  28. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/json_boundary.py +0 -0
  29. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/py.typed +0 -0
  30. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/ratchet.py +0 -0
  31. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rule_base.py +0 -0
  32. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/__init__.py +0 -0
  33. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  34. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  35. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  36. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  37. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_comments.py +0 -0
  38. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  39. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  40. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  41. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  42. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
  43. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  44. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_imports.py +0 -0
  45. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_local_source.py +0 -0
  46. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_logging.py +0 -0
  47. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  48. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_paths.py +0 -0
  49. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_project_index.py +0 -0
  50. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  51. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
  52. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_registry.py +0 -0
  53. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  54. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  55. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_sql.py +0 -0
  56. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  57. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  58. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  59. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  60. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  61. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  62. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  63. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  64. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  65. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  66. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  67. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  68. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  69. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  70. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  71. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  72. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  73. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  74. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  75. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  76. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  77. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  78. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  79. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  80. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  81. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  82. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  83. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  84. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  85. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  86. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  87. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  88. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  89. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  90. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  91. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  92. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  93. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  94. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  95. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  96. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  97. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  98. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  99. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  100. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  101. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  102. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  103. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  104. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  105. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  106. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  107. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  108. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  109. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  110. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  111. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  112. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  113. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  114. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  115. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  116. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  117. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  118. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  119. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  120. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  121. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  122. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  123. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  124. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  125. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  126. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  127. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  128. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  129. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  130. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  131. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  132. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  133. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  134. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  135. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  136. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  137. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  138. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  139. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  140. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  141. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  142. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  143. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  144. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  145. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  146. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  147. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  148. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  149. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  150. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  151. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  152. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  153. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  154. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  155. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  156. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  157. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  158. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  159. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  160. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  161. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  162. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  163. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  164. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  165. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  166. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  167. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  168. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  169. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  170. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  171. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  172. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  173. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  174. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  175. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  176. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  177. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  178. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  179. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  180. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  181. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  182. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  183. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
  184. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  185. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  186. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  187. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  188. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  189. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  190. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  191. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  192. {sarj_python_lint-1.28.2 → sarj_python_lint-1.28.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.28.2
3
+ Version: 1.28.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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.16.10
22
- Requires-Dist: sarj-rule-contracts==0.2.2
22
+ Requires-Dist: sarj-rule-contracts==0.2.3
23
23
  Requires-Dist: sqlglot==30.19.0
24
24
  Requires-Dist: typer==0.27.2
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.28.2"
3
+ version = "1.28.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.2.2", "packaging==26.3", "ruff>=0.16.10", "sqlglot==30.19.0", "typer==0.27.2"]
18
+ dependencies = ["sarj-rule-contracts==0.2.3", "packaging==26.3", "ruff>=0.16.10", "sqlglot==30.19.0", "typer==0.27.2"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -57,6 +57,16 @@ class PythonFileContext:
57
57
  def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
58
58
  return [node for node in self.node_index.query(types) if isinstance(node, types)]
59
59
 
60
+ @cached_property
61
+ def has_wildcard_import(self) -> bool:
62
+ return any(alias.name == "*" for node in self.nodes(ast.ImportFrom) for alias in node.names)
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
+
60
70
  @property
61
71
  def parents(self) -> Mapping[ast.AST, ast.AST]:
62
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)
@@ -131,11 +131,12 @@ def _pytest_plugin_test_targets(tree: ast.Module) -> list[_PluginTarget]:
131
131
  for statement in tree.body:
132
132
  value: ast.expr | None = None
133
133
  match statement:
134
- case (
135
- ast.Assign(targets=[ast.Name(id="pytest_plugins")])
136
- | ast.AnnAssign(target=ast.Name(id="pytest_plugins"))
134
+ case ast.Assign(targets=targets) if any(
135
+ isinstance(target, ast.Name) and target.id == "pytest_plugins" for target in targets
137
136
  ):
138
137
  value = statement.value
138
+ case ast.AnnAssign(target=ast.Name(id="pytest_plugins")):
139
+ value = statement.value
139
140
  case _:
140
141
  continue
141
142
  findings.extend(
@@ -177,7 +177,7 @@ class PreferMatchExceptionDispatch(Rule):
177
177
  imports = context.imports
178
178
  if (
179
179
  not imports.builtin_is_unshadowed("isinstance")
180
- or _has_wildcard_import(tree, node_index=context.node_index)
180
+ or context.has_wildcard_import
181
181
  or _has_local_import(tree, node_index=context.node_index)
182
182
  ):
183
183
  return []
@@ -412,13 +412,6 @@ def _body_terminates(body: list[ast.stmt]) -> bool:
412
412
  return False
413
413
 
414
414
 
415
- def _has_wildcard_import(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
416
- return any(
417
- isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names)
418
- for node in walk_ast(tree, index=node_index)
419
- )
420
-
421
-
422
415
  def _has_local_import(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
423
416
  module_imports = {id(statement) for statement in tree.body if isinstance(statement, (ast.Import, ast.ImportFrom))}
424
417
  return any(
@@ -16,12 +16,10 @@ from sarj_python_lint.rule_base import (
16
16
  Severity,
17
17
  parse_or_none,
18
18
  )
19
- from sarj_python_lint.rules._ast_index import walk as walk_ast
20
19
 
21
20
 
22
21
  if TYPE_CHECKING:
23
22
  from sarj_python_lint._file_context import PythonFileContext
24
- from sarj_python_lint.rules._ast_index import NodeIndex
25
23
  from sarj_python_lint.rules._imports import ImportIndex
26
24
 
27
25
 
@@ -101,7 +99,7 @@ class PreferNamedtupleOverTupleReturn(Rule):
101
99
  if context.generated or _is_documentation_path(path):
102
100
  return []
103
101
  tree = context.tree
104
- if tree is None or _has_wildcard_import(tree, node_index=context.node_index):
102
+ if tree is None or context.has_wildcard_import:
105
103
  return []
106
104
  imports = context.imports
107
105
  diagnostics: list[Diagnostic] = []
@@ -127,13 +125,6 @@ def _is_documentation_path(path: Path) -> bool:
127
125
  return any(part.lower() in _DOCUMENTATION_DIR_NAMES for part in path.parts)
128
126
 
129
127
 
130
- def _has_wildcard_import(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
131
- return any(
132
- isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names)
133
- for node in walk_ast(tree, index=node_index)
134
- )
135
-
136
-
137
128
  def _is_public_record_tuple(annotation: ast.expr, imports: ImportIndex) -> bool:
138
129
  annotation = _unwrap_return_annotation(annotation, imports)
139
130
  if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.slice, ast.Tuple):
@@ -27,7 +27,6 @@ if TYPE_CHECKING:
27
27
  from pathlib import Path
28
28
 
29
29
  from sarj_python_lint._file_context import PythonFileContext
30
- from sarj_python_lint.rules._ast_index import NodeIndex
31
30
  from sarj_python_lint.rules._imports import ImportIndex
32
31
 
33
32
 
@@ -221,7 +220,7 @@ class PreferNominalIdTypes(Rule):
221
220
  if _is_excluded_path(path) or context.generated:
222
221
  return []
223
222
  tree = context.tree
224
- if tree is None or _has_wildcard_import(tree, node_index=context.node_index):
223
+ if tree is None or context.has_wildcard_import:
225
224
  return []
226
225
 
227
226
  imports = context.imports
@@ -311,13 +310,6 @@ def _is_excluded_path(path: Path) -> bool:
311
310
  )
312
311
 
313
312
 
314
- def _has_wildcard_import(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
315
- return any(
316
- isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names)
317
- for node in walk_ast(tree, index=node_index)
318
- )
319
-
320
-
321
313
  def _boundary_nodes(
322
314
  tree: ast.Module,
323
315
  imports: ImportIndex,
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
23
23
  from collections.abc import Iterator
24
24
 
25
25
  from sarj_python_lint._file_context import PythonFileContext
26
- from sarj_python_lint.rules._ast_index import NodeIndex
27
26
  from sarj_python_lint.rules._imports import ImportIndex
28
27
 
29
28
 
@@ -99,7 +98,7 @@ class PreferNonNullableCollection(Rule):
99
98
  if is_test_path(path) or is_test_support_path(path) or context.generated:
100
99
  return []
101
100
  tree = context.tree
102
- if tree is None or _has_wildcard_import(tree, node_index=context.node_index):
101
+ if tree is None or context.has_wildcard_import:
103
102
  return []
104
103
 
105
104
  imports = context.imports
@@ -125,13 +124,6 @@ class PreferNonNullableCollection(Rule):
125
124
  return sorted(diagnostics, key=lambda diagnostic: (diagnostic.line, diagnostic.col))
126
125
 
127
126
 
128
- def _has_wildcard_import(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
129
- return any(
130
- isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names)
131
- for node in walk_ast(tree, index=node_index)
132
- )
133
-
134
-
135
127
  def _eligible_functions(tree: ast.Module, imports: ImportIndex) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
136
128
  functions: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
137
129
  for statement in tree.body:
@@ -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)
@@ -61,7 +61,10 @@ class RedundantTestConstructorForwarder(ProjectRule):
61
61
  example_id=name,
62
62
  title=title,
63
63
  outcome=outcome,
64
- files=(ExampleFile.python("tests/test_record.py", source),),
64
+ files=(
65
+ ExampleFile.python("tests/__init__.py", "# Example test package.\n"),
66
+ ExampleFile.python("tests/test_record.py", source),
67
+ ),
65
68
  focus_path=PurePosixPath("tests/test_record.py"),
66
69
  expected_count=count,
67
70
  public=True,
@@ -60,6 +60,7 @@ class RepeatedUnimplementedTestContract(ProjectRule):
60
60
  title="Two test fakes repeat one unconfigured operation",
61
61
  outcome=ExampleOutcome.MATCH,
62
62
  files=(
63
+ ExampleFile.python("tests/__init__.py", "# Example test package.\n"),
63
64
  ExampleFile.python(
64
65
  "tests/test_store.py",
65
66
  "from abc import ABC, abstractmethod\nclass Store(ABC):\n @abstractmethod\n def read(self, key: str) -> str:\n pass\nclass First(Store):\n def read(self, key: str) -> str:\n raise NotImplementedError\nclass Second(Store):\n def read(self, key: str) -> str:\n raise NotImplementedError\n",
@@ -74,6 +75,7 @@ class RepeatedUnimplementedTestContract(ProjectRule):
74
75
  title="Configured fake behavior belongs to the leaf",
75
76
  outcome=ExampleOutcome.NO_MATCH,
76
77
  files=(
78
+ ExampleFile.python("tests/__init__.py", "# Example test package.\n"),
77
79
  ExampleFile.python(
78
80
  "tests/test_store.py",
79
81
  "from abc import ABC, abstractmethod\nclass Store(ABC):\n @abstractmethod\n def read(self, key: str) -> str:\n pass\nclass First(Store):\n def read(self, key: str) -> str:\n raise NotImplementedError\nclass Second(Store):\n def read(self, key: str) -> str:\n return key\n",