sarj-python-lint 1.66.0__tar.gz → 1.66.2__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 (241) hide show
  1. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_annotation_semantics.py +59 -26
  4. sarj_python_lint-1.66.2/src/sarj_python_lint/rules/_ast_names.py +63 -0
  5. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_closed_helpers.py +10 -0
  6. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_comments.py +30 -4
  7. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_docstrings.py +19 -0
  8. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_imports.py +16 -1
  9. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_resource_provenance.py +30 -13
  10. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -8
  11. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +2 -23
  12. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +2 -21
  13. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +1 -12
  14. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +1 -6
  15. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_async_function_cache.py +1 -11
  16. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +2 -8
  17. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +1 -8
  18. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -9
  19. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +1 -11
  20. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +1 -6
  21. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_partial_type_is.py +2 -8
  22. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -8
  23. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +1 -7
  24. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_select_star.py +1 -8
  25. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +2 -8
  26. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +1 -8
  27. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +1 -6
  28. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/over_mocked_test.py +1 -12
  29. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_class_row.py +1 -6
  30. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +2 -12
  31. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +6 -24
  32. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +87 -35
  33. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +2 -4
  34. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +3 -10
  35. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +1 -6
  36. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +1 -6
  37. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +2 -4
  38. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +1 -8
  39. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +1 -12
  40. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +5 -3
  41. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +1 -6
  42. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +1 -8
  43. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_str_enum.py +1 -12
  44. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +2 -11
  45. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +1 -15
  46. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +2 -14
  47. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -9
  48. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +2 -20
  49. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +2 -17
  50. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +10 -16
  51. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +1 -6
  52. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/restated_test_docstring.py +11 -25
  53. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +1 -10
  54. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/typed_error_reasons.py +1 -8
  55. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +1 -15
  56. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/unused_mock_setup.py +1 -12
  57. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/.gitignore +0 -0
  58. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/LICENSE +0 -0
  59. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/README.md +0 -0
  60. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/__init__.py +0 -0
  61. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/__main__.py +0 -0
  62. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  63. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_file_context.py +0 -0
  64. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_filesystem.py +0 -0
  65. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_python_target.py +0 -0
  66. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  67. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_secret_names.py +0 -0
  68. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_source.py +0 -0
  69. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/_version.py +0 -0
  70. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/benchmark.py +0 -0
  71. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/cast_access.py +0 -0
  72. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  73. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/json_boundary.py +0 -0
  74. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/py.typed +0 -0
  75. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/ratchet.py +0 -0
  76. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rule_base.py +0 -0
  77. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  78. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  79. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  80. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  81. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  82. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  83. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  84. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  85. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  86. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  87. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  88. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  89. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  90. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  91. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  92. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  93. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  94. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  95. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  96. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  97. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  98. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  99. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  100. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  101. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  102. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  103. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  104. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  105. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  106. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  107. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  108. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  109. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  110. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  111. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  112. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  113. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  114. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  115. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  116. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  117. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  118. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  119. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  120. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  121. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  122. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  123. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  124. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  125. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  126. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  127. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  128. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  129. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  130. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  131. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  132. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  133. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  134. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  135. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  136. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  137. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  138. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  139. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  140. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  141. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  142. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  143. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  144. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  145. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  146. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  147. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  148. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  149. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  150. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  151. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  152. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  153. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  154. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  155. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  156. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  157. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  158. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  159. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  160. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  161. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  162. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  163. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  164. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  165. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  166. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  167. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  168. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  169. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  170. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  171. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  172. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  173. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  174. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  175. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  176. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  177. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  178. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  179. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  180. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  181. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  182. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  183. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  184. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  185. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  186. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  187. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  188. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  189. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  190. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  191. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  192. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  193. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  194. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  195. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  196. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  197. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  198. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  199. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  200. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  201. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  202. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  203. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  204. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  205. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  206. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  207. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  208. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  209. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  210. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  211. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  212. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  213. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  214. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  215. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  216. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  217. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  218. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  219. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  220. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  221. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  222. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  223. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  224. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  225. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  226. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  227. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  228. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  229. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  230. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  231. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  232. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  233. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  234. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  235. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  236. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  237. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  238. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  239. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  240. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  241. {sarj_python_lint-1.66.0 → sarj_python_lint-1.66.2}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.66.0
3
+ Version: 1.66.2
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.66.0"
3
+ version = "1.66.2"
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" }]
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import ast
4
4
  from dataclasses import dataclass
5
5
  from types import MappingProxyType
6
- from typing import Self
6
+ from typing import NamedTuple, Self
7
7
 
8
8
  from sarj_python_lint.rules._imports import ImportIndex
9
9
 
@@ -12,6 +12,23 @@ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
12
12
  _TRANSPARENT_WRAPPERS = frozenset({"Annotated", "ClassVar", "Final", "NotRequired", "Optional", "ReadOnly", "Required"})
13
13
 
14
14
 
15
+ def bit_or_members(expression: ast.expr) -> list[ast.expr]:
16
+ members: list[ast.expr] = []
17
+ pending = [expression]
18
+ while pending:
19
+ member = pending.pop()
20
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
21
+ pending.extend((member.right, member.left))
22
+ else:
23
+ members.append(member)
24
+ return members
25
+
26
+
27
+ class _AnnotationMember(NamedTuple):
28
+ node: ast.expr | None
29
+ seen: frozenset[str]
30
+
31
+
15
32
  @dataclass(frozen=True, slots=True)
16
33
  class AnnotationSemantics:
17
34
  imports: ImportIndex
@@ -75,35 +92,51 @@ class AnnotationSemantics:
75
92
  if resolved is None:
76
93
  return ()
77
94
  if isinstance(resolved, ast.Name) and resolved.id in self.aliases:
78
- return self._alias_members(resolved.id, seen=seen)
79
- if isinstance(resolved, ast.BinOp) and isinstance(resolved.op, ast.BitOr):
80
- return self.transparent_members(resolved.left, seen=seen) + self.transparent_members(
81
- resolved.right, seen=seen
82
- )
83
- if isinstance(resolved, ast.Subscript):
84
- return self._subscript_members(resolved, seen=seen)
85
- return (resolved,)
86
-
87
- def _alias_members(self, name: str, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
88
- if name in seen:
89
- return ()
90
- return self.transparent_members(self.aliases[name], seen=seen | {name})
95
+ pending = [_AnnotationMember(resolved, seen)]
96
+ else:
97
+ children = self._transparent_children(resolved)
98
+ if children is None:
99
+ return (resolved,)
100
+ pending = [_AnnotationMember(child, seen) for child in reversed(children)]
101
+ members: list[ast.expr] = []
102
+ while pending:
103
+ state = self._resolve_member(pending.pop())
104
+ if state.node is None:
105
+ continue
106
+ children = self._transparent_children(state.node)
107
+ if children is None:
108
+ members.append(state.node)
109
+ else:
110
+ pending.extend(_AnnotationMember(child, state.seen) for child in reversed(children))
111
+ return tuple(members)
91
112
 
92
- def _subscript_members(self, node: ast.Subscript, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
113
+ def _resolve_member(self, state: _AnnotationMember) -> _AnnotationMember:
114
+ node = self.parse(state.node)
115
+ if not isinstance(node, ast.Name) or node.id not in self.aliases:
116
+ return _AnnotationMember(node, state.seen)
117
+ seen = set(state.seen)
118
+ while isinstance(node, ast.Name) and node.id in self.aliases:
119
+ if node.id in seen:
120
+ return _AnnotationMember(None, frozenset(seen))
121
+ seen.add(node.id)
122
+ node = self.parse(self.aliases[node.id])
123
+ return _AnnotationMember(node, frozenset(seen))
124
+
125
+ def _transparent_children(self, node: ast.expr) -> list[ast.expr] | None:
126
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
127
+ return bit_or_members(node)
128
+ if not isinstance(node, ast.Subscript):
129
+ return None
93
130
  wrapper = self.imports.resolved_symbol(node.value, sources=_TYPING_SOURCES)
94
131
  if wrapper == "Union":
95
- members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
96
- return self._flatten_members(members, seen=seen)
132
+ return list(node.slice.elts) if isinstance(node.slice, ast.Tuple) else [node.slice]
97
133
  if wrapper not in _TRANSPARENT_WRAPPERS:
98
- return (node,)
99
- inner = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) else node.slice
100
- return self.transparent_members(inner, seen=seen)
101
-
102
- def _flatten_members(self, nodes: list[ast.expr], *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
103
- members: list[ast.expr] = []
104
- for node in nodes:
105
- members.extend(self.transparent_members(node, seen=seen))
106
- return tuple(members)
134
+ return None
135
+ if isinstance(node.slice, ast.Tuple):
136
+ if not node.slice.elts:
137
+ return None
138
+ return [node.slice.elts[0]]
139
+ return [node.slice]
107
140
 
108
141
 
109
142
  def _module_rebindings(tree: ast.Module) -> set[str]:
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+
5
+
6
+ def root_name(node: ast.expr) -> str | None:
7
+ while isinstance(node, ast.Attribute):
8
+ node = node.value
9
+ return node.id if isinstance(node, ast.Name) else None
10
+
11
+
12
+ def expression_root(node: ast.expr) -> str | None:
13
+ while isinstance(node, (ast.Attribute, ast.Subscript)):
14
+ node = node.value
15
+ return node.id if isinstance(node, ast.Name) else None
16
+
17
+
18
+ def terminal_name(node: ast.expr) -> str | None:
19
+ match node:
20
+ case ast.Name(id=name) | ast.Attribute(attr=name):
21
+ return name
22
+ case _:
23
+ return None
24
+
25
+
26
+ def unqualified_name(node: ast.expr) -> str:
27
+ match node:
28
+ case ast.Name(id=name) | ast.Attribute(attr=name):
29
+ return name
30
+ case _:
31
+ return ""
32
+
33
+
34
+ def dotted_parts(node: ast.expr) -> tuple[str, ...] | None:
35
+ parts: list[str] = []
36
+ current = node
37
+ while isinstance(current, ast.Attribute):
38
+ parts.append(current.attr)
39
+ current = current.value
40
+ if not isinstance(current, ast.Name):
41
+ return None
42
+ parts.append(current.id)
43
+ return tuple(reversed(parts))
44
+
45
+
46
+ def dotted_name(node: ast.expr) -> str | None:
47
+ parts = dotted_parts(node)
48
+ return ".".join(parts) if parts is not None else None
49
+
50
+
51
+ def assignment_names(node: ast.AST) -> set[str]:
52
+ match node:
53
+ case ast.Name(id=name):
54
+ return {name}
55
+ case ast.Tuple() | ast.List():
56
+ names: set[str] = set()
57
+ for element in node.elts:
58
+ names.update(assignment_names(element))
59
+ return names
60
+ case ast.Starred(value=value):
61
+ return assignment_names(value)
62
+ case _:
63
+ return set()
@@ -183,3 +183,13 @@ def _plain_decorator(
183
183
  and not (keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "wrap")
184
184
  for keyword in decorator.keywords
185
185
  )
186
+
187
+
188
+ def closed_owner(function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
189
+ if not isinstance(function, ast.FunctionDef) or not function.name.startswith("_"):
190
+ return False
191
+ references = closed_references(context, function, context.resource_provenance)
192
+ return bool(references) and all(
193
+ (call := direct_call(reference, context)) is not None and call_matches(function, call)
194
+ for reference in references
195
+ )
@@ -326,7 +326,13 @@ class _Scan(NamedTuple):
326
326
  ordered: _Ordered
327
327
 
328
328
 
329
- _last_scan: tuple[str, _Scan] | None = None
329
+ class _ScanMemo(NamedTuple):
330
+ source: str
331
+ scan: _Scan | None
332
+ comment_lines: frozenset[int] | None
333
+
334
+
335
+ _last_scan: _ScanMemo | None = None
330
336
 
331
337
 
332
338
  def all_comments(source: str) -> _CommentScan[_Ordered]:
@@ -334,15 +340,35 @@ def all_comments(source: str) -> _CommentScan[_Ordered]:
334
340
  return _CommentScan(scan.ordered, scan.first_code_line)
335
341
 
336
342
 
343
+ def comment_line_numbers(source: str) -> frozenset[int]:
344
+ global _last_scan # ruff: ignore[global-statement] -- retain the existing single-source memo for both comment projections.
345
+ cached = _last_scan
346
+ scan = cached.scan if cached is not None and cached.source is source else None
347
+ if cached is not None and cached.source is source and cached.comment_lines is not None:
348
+ return cached.comment_lines
349
+ lines: frozenset[int]
350
+ if scan is not None:
351
+ lines = frozenset(comment.line for comment in scan.ordered)
352
+ else:
353
+ try:
354
+ tokens = tokenize.generate_tokens(io.StringIO(source).readline)
355
+ lines = frozenset(token.start[0] for token in tokens if token.type == tokenize.COMMENT)
356
+ except IndentationError, tokenize.TokenError:
357
+ lines = frozenset()
358
+ _last_scan = _ScanMemo(source, scan, lines)
359
+ return lines
360
+
361
+
337
362
  def _scan_memo(source: str) -> _Scan:
338
363
  global _last_scan # ruff: ignore[global-statement] — single-slot memo; the CLI runs rules per file sequentially
339
- if _last_scan is not None and _last_scan[0] is source:
340
- return _last_scan[1]
364
+ cached = _last_scan if _last_scan is not None and _last_scan.source is source else None
365
+ if cached is not None and cached.scan is not None:
366
+ return cached.scan
341
367
  try:
342
368
  result = _scan(source)
343
369
  except UnicodeError:
344
370
  result = _Scan([], [], set(), 1 << 30, [])
345
- _last_scan = (source, result)
371
+ _last_scan = _ScanMemo(source, result, cached.comment_lines if cached is not None else None)
346
372
  return result
347
373
 
348
374
 
@@ -13,9 +13,16 @@ if TYPE_CHECKING:
13
13
  from collections.abc import Iterable
14
14
 
15
15
  from sarj_python_lint.rules._ast_index import NodeIndex
16
+ from sarj_python_lint.rules._imports import ImportIndex
16
17
 
17
18
 
18
19
  _GETATTR_MIN_ARGS = 2
20
+ _RUNTIME_DOC_DECORATORS = (
21
+ (frozenset({"agents"}), "function_tool"),
22
+ (frozenset({"click"}), "command"),
23
+ (frozenset({"typer"}), "command"),
24
+ )
25
+ _OVERLOAD_MODULES = frozenset({"typing", "typing_extensions"})
19
26
 
20
27
  # Docstring filler that says nothing about *which* thing is being described.
21
28
  _BASE_STOPWORDS = frozenset(
@@ -259,6 +266,18 @@ def decorator_markers(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDe
259
266
  return markers
260
267
 
261
268
 
269
+ def is_runtime_consumed_or_overload(node: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex) -> bool:
270
+ if decorator_markers(node) & PROMPT_DECORATOR_MARKERS:
271
+ return True
272
+ for decorator in node.decorator_list:
273
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
274
+ if imports.resolves(target, sources=_OVERLOAD_MODULES, symbol="overload"):
275
+ return True
276
+ if any(imports.resolves(target, sources=sources, symbol=symbol) for sources, symbol in _RUNTIME_DOC_DECORATORS):
277
+ return True
278
+ return False
279
+
280
+
262
281
  def docstring_expression(
263
282
  node: ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
264
283
  ) -> ast.Expr | None:
@@ -4,11 +4,15 @@ import ast
4
4
  from collections.abc import Mapping
5
5
  from dataclasses import dataclass
6
6
  from types import MappingProxyType
7
- from typing import Self
7
+ from typing import TYPE_CHECKING, Self
8
8
 
9
9
  from sarj_python_lint.rules._ast_index import comprehension_results, walk as walk_ast
10
10
 
11
11
 
12
+ if TYPE_CHECKING:
13
+ from sarj_python_lint._file_context import PythonFileContext
14
+
15
+
12
16
  ABC_SOURCES = frozenset({"abc"})
13
17
  TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
14
18
 
@@ -81,6 +85,17 @@ class ImportIndex:
81
85
  return None
82
86
 
83
87
 
88
+ def ambiguous_import_names(context: PythonFileContext) -> frozenset[str]:
89
+ return frozenset(
90
+ alias.asname or alias.name.partition(".")[0]
91
+ for node in context.nodes(ast.Import, ast.ImportFrom)
92
+ for alias in node.names
93
+ if context.parents[node] is not context.tree
94
+ or (isinstance(node, ast.ImportFrom) and node.level > 0)
95
+ or alias.name == "*"
96
+ )
97
+
98
+
84
99
  def _non_import_bindings(tree: ast.Module) -> set[str]:
85
100
  names: set[str] = set()
86
101
  for node in walk_ast(tree):
@@ -12,6 +12,7 @@ from sarj_python_lint.rules._runtime_bindings import RuntimeBindings
12
12
 
13
13
 
14
14
  if TYPE_CHECKING:
15
+ from collections.abc import Callable
15
16
  from pathlib import Path
16
17
 
17
18
  from sarj_python_lint._file_context import PythonFileContext
@@ -82,6 +83,8 @@ class ResourceProvenance:
82
83
  self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
83
84
  self.reads: dict[str, list[ast.Name]] = defaultdict(list)
84
85
  self._reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
86
+ self._runtime_reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
87
+ self._import_read_safety: dict[str, bool] = {}
85
88
  self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
86
89
  self.async_contract_cache: dict[ast.AsyncFunctionDef, bool] = {}
87
90
  self.import_contract_cache: dict[str, bool] = {}
@@ -172,10 +175,10 @@ class ResourceProvenance:
172
175
  if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
173
176
  return None
174
177
  if binding not in self.runtime_import_cache:
175
- self.runtime_import_cache[binding] = all(
176
- self.runtime_binding(root, read) is not binding or self._safe_import_use(read)
177
- for read in self.reads.get(root, ())
178
+ reads = self._indexed_reads(
179
+ root, binding, cache=self._runtime_reads_by_binding, resolve=self.runtime_binding
178
180
  )
181
+ self.runtime_import_cache[binding] = all(self._safe_import_use(read) for read in reads)
179
182
  return qualified if self.runtime_import_cache[binding] else None
180
183
 
181
184
  @cached_property
@@ -283,18 +286,22 @@ class ResourceProvenance:
283
286
  ):
284
287
  return False
285
288
  binding = self.binding(root, node)
286
- for read in self.reads.get(root, ()):
287
- parent = self.parents.get(read)
288
- if isinstance(parent, ast.Attribute | ast.ExceptHandler):
289
- continue
290
- if not self._safe_import_use(read):
291
- return False
289
+ if not self._import_reads_safe(root):
290
+ return False
292
291
  return (
293
292
  isinstance(binding, ast.alias)
294
293
  and isinstance(self.scope(binding), ast.Module)
295
294
  and self.context.imports.resolved_qualified_name(node) == qualified
296
295
  )
297
296
 
297
+ def _import_reads_safe(self, name: str) -> bool:
298
+ if name not in self._import_read_safety:
299
+ self._import_read_safety[name] = all(
300
+ isinstance(self.parents.get(read), ast.Attribute | ast.ExceptHandler) or self._safe_import_use(read)
301
+ for read in self.reads.get(name, ())
302
+ )
303
+ return self._import_read_safety[name]
304
+
298
305
  def _owner_namespace_untouched(self, module: str) -> bool:
299
306
  if module not in _RESOURCE_OWNERS:
300
307
  return True
@@ -459,13 +466,23 @@ class ResourceProvenance:
459
466
  )
460
467
 
461
468
  def _binding_reads(self, name: str, binding: ast.AST) -> list[ast.Name]:
462
- if name not in self._reads_by_binding:
469
+ return self._indexed_reads(name, binding, cache=self._reads_by_binding, resolve=self.binding)
470
+
471
+ def _indexed_reads(
472
+ self,
473
+ name: str,
474
+ binding: ast.AST,
475
+ *,
476
+ cache: dict[str, dict[ast.AST, list[ast.Name]]],
477
+ resolve: Callable[[str, ast.AST], ast.AST | None],
478
+ ) -> list[ast.Name]:
479
+ if name not in cache:
463
480
  grouped: dict[ast.AST, list[ast.Name]] = defaultdict(list)
464
481
  for read in self.reads.get(name, ()):
465
- if (owner := self.binding(read.id, read)) is not None:
482
+ if (owner := resolve(read.id, read)) is not None:
466
483
  grouped[owner].append(read)
467
- self._reads_by_binding[name] = grouped
468
- return self._reads_by_binding[name].get(binding, [])
484
+ cache[name] = grouped
485
+ return cache[name].get(binding, [])
469
486
 
470
487
  def unescaped(self, name: ast.Name, at: ast.AST, *, allow_context: bool = False) -> bool:
471
488
  binding = self.binding(name.id, at)
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
17
17
  Severity,
18
18
  )
19
19
  from sarj_python_lint.rules._ast_index import nodes, walk
20
+ from sarj_python_lint.rules._ast_names import unqualified_name as _call_name
20
21
  from sarj_python_lint.rules._paths import is_test_path
21
22
  from sarj_python_lint.rules._sql import is_store_module, sql_string_value, strip_sql_noise
22
23
  from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import analytical_signal
@@ -148,14 +149,6 @@ def _query_argument(call: ast.Call) -> ast.expr | None:
148
149
  return next((keyword.value for keyword in call.keywords if keyword.arg in _QUERY_KEYWORDS), None)
149
150
 
150
151
 
151
- def _call_name(function: ast.expr) -> str:
152
- match function:
153
- case ast.Name(id=name) | ast.Attribute(attr=name):
154
- return name
155
- case _:
156
- return ""
157
-
158
-
159
152
  def _postgres_sql_constructors(
160
153
  tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
161
154
  ) -> frozenset[_ConstructorImport]:
@@ -20,13 +20,12 @@ from sarj_python_lint.rule_base import (
20
20
  from sarj_python_lint.rules._ast_index import children
21
21
  from sarj_python_lint.rules._comments import is_protected, stem
22
22
  from sarj_python_lint.rules._docstrings import (
23
- PROMPT_DECORATOR_MARKERS,
24
23
  VALUE_MARKER_RE,
25
24
  annotation_tokens,
26
25
  arg_entries,
27
26
  arg_section,
28
- decorator_markers,
29
27
  identifier_stems,
28
+ is_runtime_consumed_or_overload,
30
29
  restates,
31
30
  )
32
31
 
@@ -40,14 +39,6 @@ if TYPE_CHECKING:
40
39
 
41
40
  _ARGUMENT_CONSTRAINT_RE = re.compile(r"\b(available|current|existing|optional|required|supported)\b", re.IGNORECASE)
42
41
 
43
- _RUNTIME_DOC_DECORATORS = (
44
- (frozenset({"agents"}), "function_tool"),
45
- (frozenset({"click"}), "command"),
46
- (frozenset({"typer"}), "command"),
47
- )
48
-
49
- _OVERLOAD_MODULES = frozenset({"typing", "typing_extensions"})
50
-
51
42
 
52
43
  @dataclass(frozen=True, slots=True)
53
44
  class _ParameterFacts:
@@ -151,7 +142,7 @@ class DocstringArgsRestateSignature(Rule):
151
142
  block = arg_section(docstring)
152
143
  if block is None or VALUE_MARKER_RE.search(block) or is_protected(block):
153
144
  return
154
- if _is_runtime_consumed_or_overload(node, imports):
145
+ if is_runtime_consumed_or_overload(node, imports):
155
146
  return
156
147
  entries = arg_entries(block)
157
148
  if not entries:
@@ -202,15 +193,3 @@ def _parameter_stems(
202
193
  known = callable_stems | identifier_stems(argument.arg) | annotation_stems
203
194
  parameters[argument.arg] = _ParameterFacts(frozenset(known), frozenset(annotation_stems))
204
195
  return parameters
205
-
206
-
207
- def _is_runtime_consumed_or_overload(node: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex) -> bool:
208
- if decorator_markers(node) & PROMPT_DECORATOR_MARKERS:
209
- return True
210
- for decorator in node.decorator_list:
211
- target = decorator.func if isinstance(decorator, ast.Call) else decorator
212
- if imports.resolves(target, sources=_OVERLOAD_MODULES, symbol="overload"):
213
- return True
214
- if any(imports.resolves(target, sources=sources, symbol=symbol) for sources, symbol in _RUNTIME_DOC_DECORATORS):
215
- return True
216
- return False
@@ -20,11 +20,10 @@ from sarj_python_lint.rule_base import (
20
20
  from sarj_python_lint.rules._ast_index import children
21
21
  from sarj_python_lint.rules._comments import is_protected, stem
22
22
  from sarj_python_lint.rules._docstrings import (
23
- PROMPT_DECORATOR_MARKERS,
24
23
  VALUE_MARKER_RE,
25
24
  annotation_tokens,
26
- decorator_markers,
27
25
  identifier_stems,
26
+ is_runtime_consumed_or_overload,
28
27
  restates,
29
28
  sections,
30
29
  )
@@ -51,12 +50,6 @@ _RESULT_SEMANTIC_RE = re.compile(
51
50
  re.IGNORECASE,
52
51
  )
53
52
 
54
- _RUNTIME_DOC_DECORATORS = (
55
- (frozenset({"agents"}), "function_tool"),
56
- (frozenset({"click"}), "command"),
57
- (frozenset({"typer"}), "command"),
58
- )
59
- _OVERLOAD_MODULES = frozenset({"typing", "typing_extensions"})
60
53
  _GENERATOR_TYPES = frozenset({"AsyncGenerator", "AsyncIterable", "AsyncIterator", "Generator", "Iterable", "Iterator"})
61
54
  _BROAD_RETURN_TYPES = frozenset({"Any", "object"})
62
55
 
@@ -228,7 +221,7 @@ class DocstringReturnsRestateSignature(Rule):
228
221
  or _TYPED_RESULT_RE.search(block)
229
222
  ):
230
223
  return
231
- if _is_runtime_consumed_or_overload(node, imports):
224
+ if is_runtime_consumed_or_overload(node, imports):
232
225
  return
233
226
  annotation = _result_annotation(node, section.name)
234
227
  if annotation is None or _is_broad_annotation(annotation):
@@ -313,15 +306,3 @@ def _contains_yield(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
313
306
 
314
307
  def _is_broad_annotation(annotation: ast.expr) -> bool:
315
308
  return _annotation_name(annotation) in _BROAD_RETURN_TYPES
316
-
317
-
318
- def _is_runtime_consumed_or_overload(node: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex) -> bool:
319
- if decorator_markers(node) & PROMPT_DECORATOR_MARKERS:
320
- return True
321
- for decorator in node.decorator_list:
322
- target = decorator.func if isinstance(decorator, ast.Call) else decorator
323
- if imports.resolves(target, sources=_OVERLOAD_MODULES, symbol="overload"):
324
- return True
325
- if any(imports.resolves(target, sources=sources, symbol=symbol) for sources, symbol in _RUNTIME_DOC_DECORATORS):
326
- return True
327
- return False
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleDocumentation,
17
17
  RuleExample,
18
18
  )
19
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _bit_union_members
19
20
  from sarj_python_lint.rules._imports import ImportIndex
20
21
  from sarj_python_lint.rules._paths import is_test_path
21
22
 
@@ -314,18 +315,6 @@ def _literal_key(value: object) -> _LiteralKey:
314
315
  return _LiteralKey(type(value), value)
315
316
 
316
317
 
317
- def _bit_union_members(node: ast.expr) -> list[ast.expr]:
318
- members: list[ast.expr] = []
319
- pending = [node]
320
- while pending:
321
- member = pending.pop()
322
- if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
323
- pending.extend((member.right, member.left))
324
- else:
325
- members.append(member)
326
- return members
327
-
328
-
329
318
  def _literal_domain(node: ast.expr, imports: ImportIndex) -> _LiteralDomain | None:
330
319
  node = _parse_forward_annotation(node)
331
320
  if isinstance(node, ast.Constant) and node.value is None:
@@ -21,6 +21,7 @@ from sarj_python_lint.rule_base import (
21
21
  )
22
22
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
23
23
  from sarj_python_lint.rules._ast_index import walk as walk_ast
24
+ from sarj_python_lint.rules._ast_names import expression_root as _expression_root
24
25
  from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
25
26
 
26
27
 
@@ -568,12 +569,6 @@ def _type_attribute_mutation(call: ast.Call, imports: ImportIndex, sensitive: se
568
569
  return known and _expression_root(call.args[0]) in sensitive
569
570
 
570
571
 
571
- def _expression_root(node: ast.expr) -> str | None:
572
- while isinstance(node, (ast.Attribute, ast.Subscript)):
573
- node = node.value
574
- return node.id if isinstance(node, ast.Name) else None
575
-
576
-
577
572
  def _unshadowed_type_expression(
578
573
  node: ast.expr, parents: Mapping[ast.AST, ast.AST], scope_cache: dict[int, frozenset[str]]
579
574
  ) -> bool:
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
17
17
  is_suppressed,
18
18
  )
19
19
  from sarj_python_lint.rules._ast_index import has_own_yield
20
+ from sarj_python_lint.rules._imports import ambiguous_import_names as _nested_import_names
20
21
 
21
22
 
22
23
  if TYPE_CHECKING:
@@ -114,17 +115,6 @@ class NoAsyncFunctionCache(Rule):
114
115
  return diagnostics
115
116
 
116
117
 
117
- def _nested_import_names(context: PythonFileContext) -> frozenset[str]:
118
- return frozenset(
119
- alias.asname or alias.name.partition(".")[0]
120
- for node in context.nodes(ast.Import, ast.ImportFrom)
121
- for alias in node.names
122
- if context.parents[node] is not context.tree
123
- or (isinstance(node, ast.ImportFrom) and node.level > 0)
124
- or alias.name == "*"
125
- )
126
-
127
-
128
118
  def _is_coroutine_cache(decorator: ast.expr, imports: ImportIndex, shadowed: frozenset[str]) -> bool:
129
119
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
130
120
  root = target
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  Severity,
17
17
  is_suppressed,
18
18
  )
19
+ from sarj_python_lint.rules._closed_helpers import executable_body
19
20
  from sarj_python_lint.rules._imports import TYPING_SOURCES
20
21
  from sarj_python_lint.rules._type_guard_symbols import (
21
22
  builtin_guard_symbol,
@@ -189,14 +190,7 @@ def _sole_parameter(function: ast.FunctionDef) -> ast.arg | None:
189
190
 
190
191
 
191
192
  def _single_return(function: ast.FunctionDef) -> ast.Return | None:
192
- body = function.body
193
- if (
194
- body
195
- and isinstance(body[0], ast.Expr)
196
- and isinstance(body[0].value, ast.Constant)
197
- and isinstance(body[0].value.value, str)
198
- ):
199
- body = body[1:]
193
+ body = executable_body(function)
200
194
  return body[0] if len(body) == 1 and isinstance(body[0], ast.Return) else None
201
195
 
202
196
 
@@ -15,6 +15,7 @@ from sarj_python_lint.rule_base import (
15
15
  RuleExample,
16
16
  )
17
17
  from sarj_python_lint.rules._ast_index import walk as walk_ast
18
+ from sarj_python_lint.rules._ast_names import unqualified_name as _annotation_name
18
19
  from sarj_python_lint.rules._exports import mentions_dunder_all
19
20
 
20
21
 
@@ -164,14 +165,6 @@ def _dunder_all_matches_primary(tree: ast.Module, primary_name: str) -> bool:
164
165
  return isinstance(entry, ast.Constant) and entry.value == primary_name
165
166
 
166
167
 
167
- def _annotation_name(node: ast.expr) -> str:
168
- match node:
169
- case ast.Name(id=name) | ast.Attribute(attr=name):
170
- return name
171
- case _:
172
- return ""
173
-
174
-
175
168
  def _is_skipped_path(path: Path) -> bool:
176
169
  return "tests" in path.parts
177
170