sarj-python-lint 1.1.0__tar.gz → 1.1.1__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 (146) hide show
  1. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/__main__.py +14 -11
  4. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_file_context.py +5 -0
  5. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_ast_index.py +15 -2
  6. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +15 -15
  7. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +6 -9
  8. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +6 -10
  9. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_select_star.py +5 -3
  10. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +9 -11
  11. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +6 -6
  12. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +3 -5
  13. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +6 -4
  14. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +8 -10
  15. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +6 -6
  16. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +8 -6
  17. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +9 -8
  18. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +5 -4
  19. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +5 -4
  20. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +7 -5
  21. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/.gitignore +0 -0
  22. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/LICENSE +0 -0
  23. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/README.md +0 -0
  24. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/__init__.py +0 -0
  25. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  26. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_filesystem.py +0 -0
  27. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_python_target.py +0 -0
  28. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  29. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_secret_names.py +0 -0
  30. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_version.py +0 -0
  31. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/benchmark.py +0 -0
  32. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/json_boundary.py +0 -0
  33. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/py.typed +0 -0
  34. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/ratchet.py +0 -0
  35. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rule_base.py +0 -0
  36. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  37. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  38. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  39. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  40. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  41. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  42. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  43. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  44. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  45. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  46. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  47. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  48. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  49. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  50. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  51. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  52. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  53. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  54. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  55. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  56. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  57. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  58. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  59. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  60. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  61. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  62. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  63. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  64. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  65. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  66. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  67. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  68. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  69. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  70. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  71. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  72. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  73. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  74. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  75. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  76. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  77. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  78. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  79. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  80. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  81. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  82. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  83. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  84. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  85. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  86. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  87. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  88. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  89. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  90. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  91. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  92. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  93. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  94. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  95. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  96. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  97. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  98. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  99. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  100. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  101. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  102. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  103. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  104. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  105. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  106. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  107. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  108. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  109. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  110. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  111. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  112. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  113. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  114. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  115. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  116. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  117. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  118. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  119. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  120. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  121. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  122. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  123. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  124. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  125. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  126. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  127. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  128. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  129. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  130. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  131. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  132. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  133. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  134. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  135. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  136. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  137. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  138. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  139. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  140. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  141. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  142. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  143. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  144. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  145. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  146. {sarj_python_lint-1.1.0 → sarj_python_lint-1.1.1}/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.1.0
3
+ Version: 1.1.1
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.1.0"
3
+ version = "1.1.1"
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,6 +3,7 @@ from __future__ import annotations
3
3
  import ast
4
4
  from collections import Counter
5
5
  from dataclasses import dataclass
6
+ from fnmatch import fnmatch
6
7
  import json
7
8
  from pathlib import Path
8
9
  import sys
@@ -66,19 +67,21 @@ def expand_paths(paths: list[Path]) -> list[Path]:
66
67
 
67
68
 
68
69
  def _python_files(p: Path) -> list[Path]:
70
+ if not SKIP_DIR_NAMES.isdisjoint(p.parts):
71
+ return []
69
72
  out: list[Path] = []
70
- for child in p.rglob("*.py"):
71
- if not child.is_file():
72
- continue
73
- if any(part in SKIP_DIR_NAMES for part in child.parts):
74
- continue
75
- try:
76
- if child.stat().st_size > _MAX_FILE_BYTES:
73
+ for directory, directories, filenames in p.walk():
74
+ directories[:] = [name for name in directories if name not in SKIP_DIR_NAMES]
75
+ for name in filenames:
76
+ if not fnmatch(name, "*.py"):
77
77
  continue
78
- except OSError:
79
- continue
80
- out.append(child)
81
- return out
78
+ child = directory / name
79
+ try:
80
+ if child.is_file() and child.stat().st_size <= _MAX_FILE_BYTES:
81
+ out.append(child)
82
+ except OSError:
83
+ continue
84
+ return sorted(out)
82
85
 
83
86
 
84
87
  def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
@@ -12,6 +12,7 @@ from sarj_python_lint.rules._paths import is_generated
12
12
 
13
13
 
14
14
  if TYPE_CHECKING:
15
+ from collections.abc import Mapping
15
16
  from pathlib import Path
16
17
 
17
18
 
@@ -48,6 +49,10 @@ class PythonFileContext:
48
49
  def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
49
50
  return [node for node in self.node_index.query(types) if isinstance(node, types)]
50
51
 
52
+ @property
53
+ def parents(self) -> Mapping[ast.AST, ast.AST]:
54
+ return self.node_index.parents
55
+
51
56
  @cached_property
52
57
  def imports(self) -> ImportIndex:
53
58
  return ImportIndex.from_tree(self._valid_tree())
@@ -3,11 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
+ from types import MappingProxyType
6
7
  from typing import TYPE_CHECKING, TypeIs, final
7
8
 
8
9
 
9
10
  if TYPE_CHECKING:
10
- from collections.abc import Iterator
11
+ from collections.abc import Iterator, Mapping
11
12
 
12
13
 
13
14
  _AST = ast.AST
@@ -43,7 +44,7 @@ def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
43
44
 
44
45
  @final
45
46
  class NodeIndex:
46
- __slots__ = ("_buckets", "_flat", "_queries", "tree")
47
+ __slots__ = ("_buckets", "_flat", "_parents", "_queries", "tree")
47
48
 
48
49
  def __init__(self, tree: ast.AST) -> None:
49
50
  self.tree = tree
@@ -62,8 +63,15 @@ class NodeIndex:
62
63
  flat += children(current)
63
64
  self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
64
65
  self._flat: list[ast.AST] = flat
66
+ self._parents: Mapping[ast.AST, ast.AST] | None = None
65
67
  self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
66
68
 
69
+ @property
70
+ def parents(self) -> Mapping[ast.AST, ast.AST]:
71
+ if self._parents is None:
72
+ self._parents = MappingProxyType({child: parent for parent in self._flat for child in children(parent)})
73
+ return self._parents
74
+
67
75
  def query(self, types: tuple[type[ast.AST], ...]) -> list[ast.AST]:
68
76
  hit = self._queries.get(types)
69
77
  if hit is not None:
@@ -87,3 +95,8 @@ class NodeIndex:
87
95
  def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT], index: NodeIndex | None = None) -> list[NodeT]:
88
96
  selected = index if index is not None and index.tree is tree else NodeIndex(tree)
89
97
  return [node for node in selected.query(types) if isinstance(node, types)]
98
+
99
+
100
+ def parent_map(tree: ast.AST, *, index: NodeIndex | None = None) -> Mapping[ast.AST, ast.AST]:
101
+ selected = index if index is not None and index.tree is tree else NodeIndex(tree)
102
+ return selected.parents
@@ -23,7 +23,7 @@ from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import a
23
23
 
24
24
 
25
25
  if TYPE_CHECKING:
26
- from collections.abc import Callable
26
+ from collections.abc import Callable, Mapping
27
27
  from pathlib import Path
28
28
 
29
29
  from sqlglot import exp
@@ -83,18 +83,18 @@ def _docstring_node_ids(tree: ast.AST, *, node_index: NodeIndex | None = None) -
83
83
  return result
84
84
 
85
85
 
86
- def _is_query_context(node: ast.expr, parents: dict[int, ast.AST], roots: frozenset[int]) -> bool:
86
+ def _is_query_context(node: ast.expr, parents: Mapping[ast.AST, ast.AST], roots: frozenset[int]) -> bool:
87
87
  current: ast.AST = node
88
88
  while True:
89
89
  if id(current) in roots:
90
90
  return True
91
- if (parent := parents.get(id(current))) is None:
91
+ if (parent := parents.get(current)) is None:
92
92
  return False
93
93
  current = parent
94
94
 
95
95
 
96
96
  def _query_roots(
97
- tree: ast.Module, parents: dict[int, ast.AST], *, node_index: NodeIndex | None = None
97
+ tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
98
98
  ) -> frozenset[int]:
99
99
  binding_counts: dict[tuple[int, str], int] = {}
100
100
  binding_values: dict[tuple[int, str], ast.expr] = {}
@@ -133,10 +133,10 @@ def _query_roots(
133
133
  return frozenset(roots)
134
134
 
135
135
 
136
- def _scope(node: ast.AST, parents: dict[int, ast.AST]) -> ast.AST:
136
+ def _scope(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> ast.AST:
137
137
  current = node
138
138
  while not isinstance(current, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)):
139
- parent = parents.get(id(current))
139
+ parent = parents.get(current)
140
140
  if parent is None:
141
141
  return current
142
142
  current = parent
@@ -169,7 +169,7 @@ def _call_name(function: ast.expr) -> str:
169
169
 
170
170
 
171
171
  def _postgres_sql_constructors(
172
- tree: ast.Module, parents: dict[int, ast.AST], *, node_index: NodeIndex | None = None
172
+ tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
173
173
  ) -> frozenset[_ConstructorImport]:
174
174
  constructors: set[_ConstructorImport] = set()
175
175
  for node in nodes(tree, ast.Import, ast.ImportFrom, index=node_index):
@@ -204,7 +204,7 @@ def _qualified_name(node: ast.expr) -> str:
204
204
  def _visible_constructors(
205
205
  owner: ast.AST,
206
206
  imports: frozenset[_ConstructorImport],
207
- parents: dict[int, ast.AST],
207
+ parents: Mapping[ast.AST, ast.AST],
208
208
  binding_counts: dict[tuple[int, str], int],
209
209
  wildcard_scopes: set[int],
210
210
  ) -> frozenset[str]:
@@ -212,7 +212,7 @@ def _visible_constructors(
212
212
  current = owner
213
213
  while True:
214
214
  scope_chain.append(id(current))
215
- parent = parents.get(id(current))
215
+ parent = parents.get(current)
216
216
  if parent is None:
217
217
  break
218
218
  current = _scope(parent, parents)
@@ -380,7 +380,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
380
380
  return []
381
381
 
382
382
  docstrings = _docstring_node_ids(tree, node_index=context.node_index)
383
- parents = {id(child): parent for parent in context.nodes(ast.AST) for child in ast.iter_child_nodes(parent)}
383
+ parents = context.parents
384
384
  query_roots = _query_roots(tree, parents, node_index=context.node_index)
385
385
  diagnostics: list[Diagnostic] = []
386
386
  consumed: set[int] = set()
@@ -417,7 +417,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
417
417
 
418
418
  def _count_query_bindings(
419
419
  tree: ast.Module,
420
- parents: dict[int, ast.AST],
420
+ parents: Mapping[ast.AST, ast.AST],
421
421
  count: Callable[[ast.AST, str], None],
422
422
  wildcard_scopes: set[int],
423
423
  *,
@@ -430,7 +430,7 @@ def _count_query_bindings(
430
430
  count(_scope(argument, parents), argument.arg)
431
431
  _count_import_bindings(tree, parents, count, wildcard_scopes, node_index=node_index)
432
432
  for definition in nodes(tree, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, index=node_index):
433
- parent = parents.get(id(definition))
433
+ parent = parents.get(definition)
434
434
  if parent is not None:
435
435
  count(_scope(parent, parents), definition.name)
436
436
  for handler in nodes(tree, ast.ExceptHandler, index=node_index):
@@ -447,7 +447,7 @@ def _count_query_bindings(
447
447
 
448
448
  def _query_binding_values(
449
449
  tree: ast.Module,
450
- parents: dict[int, ast.AST],
450
+ parents: Mapping[ast.AST, ast.AST],
451
451
  binding_values: dict[tuple[int, str], ast.expr],
452
452
  *,
453
453
  node_index: NodeIndex | None = None,
@@ -461,7 +461,7 @@ def _query_binding_values(
461
461
  if not isinstance(target, ast.Name) or assignment.value is None:
462
462
  continue
463
463
  owner = _scope(assignment, parents)
464
- if parents.get(id(assignment)) is owner:
464
+ if parents.get(assignment) is owner:
465
465
  binding_values[id(owner), target.id] = assignment.value
466
466
 
467
467
 
@@ -591,7 +591,7 @@ def _maximum_query_joins(statement: exp.Query) -> int:
591
591
 
592
592
  def _count_import_bindings(
593
593
  tree: ast.Module,
594
- parents: dict[int, ast.AST],
594
+ parents: Mapping[ast.AST, ast.AST],
595
595
  count: Callable[[ast.AST, str], None],
596
596
  wildcard_scopes: set[int],
597
597
  *,
@@ -20,6 +20,7 @@ from sarj_python_lint.rules._paths import is_test_path
20
20
 
21
21
 
22
22
  if TYPE_CHECKING:
23
+ from collections.abc import Mapping
23
24
  from pathlib import Path
24
25
 
25
26
  from sarj_python_lint._file_context import PythonFileContext
@@ -92,7 +93,7 @@ class FastapiClassRouterContract(Rule):
92
93
  if tree is None:
93
94
  return []
94
95
  index = context.fastapi
95
- parents = _parents(tree, node_index=context.node_index)
96
+ parents = context.parents
96
97
  findings = _router_construction_findings(path, tree, index, parents, node_index=context.node_index)
97
98
  for function in (
98
99
  node for node in context.nodes(ast.AST) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
@@ -109,7 +110,7 @@ def _router_construction_findings(
109
110
  path: Path,
110
111
  tree: ast.Module,
111
112
  index: FastapiIndex,
112
- parents: dict[int, ast.AST],
113
+ parents: Mapping[ast.AST, ast.AST],
113
114
  *,
114
115
  node_index: NodeIndex | None = None,
115
116
  ) -> list[Diagnostic]:
@@ -187,16 +188,12 @@ def _is_collection_or_scalar(annotation: ast.expr) -> bool:
187
188
  return flat_name(target) in _TOP_LEVEL_COLLECTIONS | _SCALARS
188
189
 
189
190
 
190
- def _parents(tree: ast.Module, *, node_index: NodeIndex | None = None) -> dict[int, ast.AST]:
191
- return {id(child): parent for parent in walk_ast(tree, index=node_index) for child in ast.iter_child_nodes(parent)}
192
-
193
-
194
191
  def _ancestor(
195
- node: ast.AST, parents: dict[int, ast.AST], kind: type[ast.AST] | tuple[type[ast.AST], ...]
192
+ node: ast.AST, parents: Mapping[ast.AST, ast.AST], kind: type[ast.AST] | tuple[type[ast.AST], ...]
196
193
  ) -> ast.AST | None:
197
- current = parents.get(id(node))
194
+ current = parents.get(node)
198
195
  while current is not None:
199
196
  if isinstance(current, kind):
200
197
  return current
201
- current = parents.get(id(current))
198
+ current = parents.get(current)
202
199
  return None
@@ -14,14 +14,14 @@ from sarj_python_lint.rule_base import (
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
16
  )
17
- from sarj_python_lint.rules._ast_index import walk as walk_ast
18
17
  from sarj_python_lint.rules._imports import ImportIndex
19
18
  from sarj_python_lint.rules._paths import is_test_path
20
19
 
21
20
 
22
21
  if TYPE_CHECKING:
22
+ from collections.abc import Mapping
23
+
23
24
  from sarj_python_lint._file_context import PythonFileContext
24
- from sarj_python_lint.rules._ast_index import NodeIndex
25
25
 
26
26
 
27
27
  _PYDANTIC_BASE_MODEL_SOURCES = frozenset({"pydantic", "pydantic.main", "pydantic.v1", "pydantic.v1.main"})
@@ -108,7 +108,7 @@ class NoNestedPydanticFieldValidator(Rule):
108
108
  if tree is None:
109
109
  return []
110
110
  imports = _module_scope_imports(tree)
111
- parents = _parent_index(tree, node_index=context.node_index)
111
+ parents = context.parents
112
112
  diagnostics: list[Diagnostic] = []
113
113
 
114
114
  def collect_nested_validators(outer: ast.ClassDef, nested: ast.ClassDef, outer_fields: frozenset[str]) -> None:
@@ -148,7 +148,7 @@ class NoNestedPydanticFieldValidator(Rule):
148
148
  return diagnostics
149
149
 
150
150
 
151
- def _is_direct_model(node: ast.ClassDef, imports: ImportIndex, parents: dict[ast.AST, ast.AST]) -> bool:
151
+ def _is_direct_model(node: ast.ClassDef, imports: ImportIndex, parents: Mapping[ast.AST, ast.AST]) -> bool:
152
152
  return any(
153
153
  (
154
154
  imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel")
@@ -174,7 +174,7 @@ def _direct_fields(node: ast.ClassDef, imports: ImportIndex) -> frozenset[str]:
174
174
  def _field_validators(
175
175
  node: ast.FunctionDef | ast.AsyncFunctionDef,
176
176
  imports: ImportIndex,
177
- parents: dict[ast.AST, ast.AST],
177
+ parents: Mapping[ast.AST, ast.AST],
178
178
  ) -> tuple[tuple[ast.Call, frozenset[str]], ...]:
179
179
  validators: list[tuple[ast.Call, frozenset[str]]] = []
180
180
  for decorator in node.decorator_list:
@@ -221,14 +221,10 @@ def _module_scope_imports(tree: ast.Module) -> ImportIndex:
221
221
  return ImportIndex.from_tree(ast.Module(body=body, type_ignores=[]))
222
222
 
223
223
 
224
- def _parent_index(tree: ast.Module, *, node_index: NodeIndex | None = None) -> dict[ast.AST, ast.AST]:
225
- return {child: owner for owner in walk_ast(tree, index=node_index) for child in ast.iter_child_nodes(owner)}
226
-
227
-
228
224
  def _shadowed_in_enclosing_function(
229
225
  reference: ast.expr,
230
226
  owner: ast.AST,
231
- parents: dict[ast.AST, ast.AST],
227
+ parents: Mapping[ast.AST, ast.AST],
232
228
  ) -> bool:
233
229
  root = _root_name(reference)
234
230
  if root is None:
@@ -21,6 +21,8 @@ from sarj_python_lint.rules._sql import sql_string_value, strip_sql_noise
21
21
 
22
22
 
23
23
  if TYPE_CHECKING:
24
+ from collections.abc import Mapping
25
+
24
26
  from sarj_python_lint._file_context import PythonFileContext
25
27
  from sarj_python_lint.rules._ast_index import NodeIndex
26
28
 
@@ -176,7 +178,7 @@ class NoSelectStar(Rule):
176
178
  return []
177
179
 
178
180
  diags: list[Diagnostic] = []
179
- parents = {id(child): parent for parent in context.nodes(ast.AST) for child in ast.iter_child_nodes(parent)}
181
+ parents = context.parents
180
182
  docstrings = _docstring_value_ids(tree, node_index=context.node_index)
181
183
  consumed: set[int] = set()
182
184
  for node in context.nodes(ast.Constant, ast.BinOp, ast.JoinedStr):
@@ -221,9 +223,9 @@ def _docstring_value_ids(tree: ast.AST, *, node_index: NodeIndex | None = None)
221
223
  }
222
224
 
223
225
 
224
- def _is_query_context(node: ast.expr, parents: dict[int, ast.AST]) -> bool:
226
+ def _is_query_context(node: ast.expr, parents: Mapping[ast.AST, ast.AST]) -> bool:
225
227
  current: ast.AST = node
226
- while (parent := parents.get(id(current))) is not None:
228
+ while (parent := parents.get(current)) is not None:
227
229
  match parent:
228
230
  case ast.Assign() | ast.AnnAssign() | ast.NamedExpr():
229
231
  return any(_is_query_binding(target) for target in _assignment_targets(parent))
@@ -14,14 +14,12 @@ from sarj_python_lint.rule_base import (
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
16
  Severity,
17
- parse_or_none,
18
17
  )
19
- from sarj_python_lint.rules._ast_index import walk as walk_ast
20
18
  from sarj_python_lint.rules._prose_budget import groups
21
19
 
22
20
 
23
21
  if TYPE_CHECKING:
24
- from pathlib import Path
22
+ from collections.abc import Mapping
25
23
 
26
24
  from sarj_python_lint._file_context import PythonFileContext
27
25
 
@@ -87,7 +85,7 @@ class NoTypedDocSections(Rule):
87
85
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
88
86
  path = context.path
89
87
  source = context.source
90
- excluded_lines = _public_contract_docstring_lines(path, source)
88
+ excluded_lines = _public_contract_docstring_lines(context)
91
89
  return [
92
90
  Diagnostic(
93
91
  path,
@@ -110,14 +108,14 @@ _PUBLIC_CONTRACT_DECORATORS = frozenset({"abstractmethod", "cached_property", "o
110
108
  _PUBLIC_CONTRACT_BASES = frozenset({"Protocol"})
111
109
 
112
110
 
113
- def _public_contract_docstring_lines(path: Path, source: str) -> frozenset[int]:
114
- tree = parse_or_none(path, source)
111
+ def _public_contract_docstring_lines(context: PythonFileContext) -> frozenset[int]:
112
+ tree = context.tree
115
113
  if tree is None:
116
114
  return frozenset()
117
- parents = {id(child): parent for parent in walk_ast(tree) for child in ast.iter_child_nodes(parent)}
115
+ parents = context.parents
118
116
  excluded: set[int] = set()
119
- for node in walk_ast(tree):
120
- if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) or not node.body:
117
+ for node in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
118
+ if not node.body:
121
119
  continue
122
120
  first = node.body[0]
123
121
  if not (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)):
@@ -127,9 +125,9 @@ def _public_contract_docstring_lines(path: Path, source: str) -> frozenset[int]:
127
125
  return frozenset(excluded)
128
126
 
129
127
 
130
- def _is_public_contract(node: ast.FunctionDef | ast.AsyncFunctionDef, parents: dict[int, ast.AST]) -> bool:
128
+ def _is_public_contract(node: ast.FunctionDef | ast.AsyncFunctionDef, parents: Mapping[ast.AST, ast.AST]) -> bool:
131
129
  decorators = {_terminal_name(item.func if isinstance(item, ast.Call) else item) for item in node.decorator_list}
132
- owner = parents.get(id(node))
130
+ owner = parents.get(node)
133
131
  protocol_method = isinstance(owner, ast.ClassDef) and any(
134
132
  _terminal_name(base) in _PUBLIC_CONTRACT_BASES for base in owner.bases
135
133
  )
@@ -22,7 +22,7 @@ from sarj_python_lint.rules._paths import is_test_path
22
22
 
23
23
 
24
24
  if TYPE_CHECKING:
25
- from collections.abc import Iterator
25
+ from collections.abc import Iterator, Mapping
26
26
 
27
27
  from sarj_python_lint._file_context import PythonFileContext
28
28
 
@@ -108,7 +108,7 @@ class NoUniqueViolationMessageMatch(Rule):
108
108
  if tree is None:
109
109
  return []
110
110
  imports = _module_import_index(tree)
111
- parents = {id(child): parent for parent in context.nodes(ast.AST) for child in ast.iter_child_nodes(parent)}
111
+ parents = context.parents
112
112
  findings: list[tuple[ast.expr, _HandlerContext]] = []
113
113
  for handler in (node for node in context.nodes(ast.AST) if isinstance(node, ast.ExceptHandler)):
114
114
  driver = _caught_driver(handler.type, imports)
@@ -194,11 +194,11 @@ def _statement_bound_names(statement: ast.stmt) -> frozenset[str]:
194
194
  def _locally_shadows_exception(
195
195
  caught: ast.expr | None,
196
196
  handler: ast.ExceptHandler,
197
- parents: dict[int, ast.AST],
197
+ parents: Mapping[ast.AST, ast.AST],
198
198
  ) -> bool:
199
199
  roots = _exception_root_names(caught)
200
200
  owner: ast.AST = handler
201
- while (parent := parents.get(id(owner))) is not None:
201
+ while (parent := parents.get(owner)) is not None:
202
202
  owner = parent
203
203
  if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)):
204
204
  return any(
@@ -244,10 +244,10 @@ def _walk_same_scope(node: ast.AST) -> Iterator[ast.AST]:
244
244
  def _builtins_are_available(
245
245
  tree: ast.Module,
246
246
  handler: ast.ExceptHandler,
247
- parents: dict[int, ast.AST],
247
+ parents: Mapping[ast.AST, ast.AST],
248
248
  ) -> bool:
249
249
  owner: ast.AST = handler
250
- while (parent := parents.get(id(owner))) is not None:
250
+ while (parent := parents.get(owner)) is not None:
251
251
  owner = parent
252
252
  if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)):
253
253
  return not any(
@@ -16,7 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleExample,
17
17
  Severity,
18
18
  )
19
- from sarj_python_lint.rules._ast_index import walk as walk_ast
19
+ from sarj_python_lint.rules._ast_index import parent_map, walk as walk_ast
20
20
  from sarj_python_lint.rules._docstrings import docstring_expression
21
21
 
22
22
 
@@ -365,15 +365,13 @@ def _owner_index(owners: frozenset[str]) -> dict[str, tuple[str, ...]]:
365
365
 
366
366
 
367
367
  def _bare_module_docstring_consumed(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
368
- parents = {
369
- id(child): parent for parent in walk_ast(tree, index=node_index) for child in ast.iter_child_nodes(parent)
370
- }
368
+ parents = parent_map(tree, index=node_index)
371
369
  for node in walk_ast(tree, index=node_index):
372
370
  if not (isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id == "__doc__"):
373
371
  continue
374
372
  current: ast.AST = node
375
373
  inside_function = False
376
- while (parent := parents.get(id(current))) is not None:
374
+ while (parent := parents.get(current)) is not None:
377
375
  if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
378
376
  inside_function = True
379
377
  if _function_binds_docstring_name(parent):
@@ -20,6 +20,8 @@ from sarj_python_lint.rules._ast_index import walk as walk_ast
20
20
 
21
21
 
22
22
  if TYPE_CHECKING:
23
+ from collections.abc import Mapping
24
+
23
25
  from sarj_python_lint._file_context import PythonFileContext
24
26
 
25
27
 
@@ -136,8 +138,8 @@ class NoUnusedValueMarker(Rule):
136
138
  ]
137
139
  if not markers:
138
140
  return []
139
- parents = (
140
- {child: parent for parent in context.nodes(ast.AST) for child in ast.iter_child_nodes(parent)}
141
+ parents: Mapping[ast.AST, ast.AST] = (
142
+ context.parents
141
143
  if any(isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Tuple)) for node in markers)
142
144
  else {}
143
145
  )
@@ -166,7 +168,7 @@ def _is_unused_value_marker(node: ast.Assign | ast.AnnAssign) -> bool:
166
168
 
167
169
  def _is_pure_parameter_marker(
168
170
  node: ast.Assign | ast.AnnAssign,
169
- parents: dict[ast.AST, ast.AST],
171
+ parents: Mapping[ast.AST, ast.AST],
170
172
  ) -> bool:
171
173
  if not isinstance(node, ast.Assign) or len(node.targets) != 1:
172
174
  return False
@@ -193,7 +195,7 @@ def _marker_reference_names(value: ast.expr) -> set[str] | None:
193
195
 
194
196
 
195
197
  def _enclosing_function(
196
- node: ast.AST, parents: dict[ast.AST, ast.AST]
198
+ node: ast.AST, parents: Mapping[ast.AST, ast.AST]
197
199
  ) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
198
200
  current = node
199
201
  while current in parents:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from collections import Counter
5
+ from collections.abc import Mapping
5
6
  from dataclasses import dataclass
6
7
  from pathlib import PurePosixPath
7
8
  from typing import TYPE_CHECKING, final, override
@@ -26,7 +27,6 @@ if TYPE_CHECKING:
26
27
  from pathlib import Path
27
28
 
28
29
  from sarj_python_lint._file_context import PythonFileContext
29
- from sarj_python_lint.rules._ast_index import NodeIndex
30
30
 
31
31
 
32
32
  _PAYLOAD_TERMINALS = frozenset({"body", "bodies", "content", "data", "json", "payload", "payloads", "text"})
@@ -60,7 +60,7 @@ class _SuiteFacts:
60
60
  class _AnalysisFacts:
61
61
  suites: dict[int, _SuiteFacts]
62
62
  statement_suites: dict[int, int]
63
- parents: dict[int, ast.AST]
63
+ parents: Mapping[ast.AST, ast.AST]
64
64
 
65
65
 
66
66
  def _reference_tokens(value: ast.expr) -> tuple[str, ...]:
@@ -171,7 +171,7 @@ class NoWholeRequestResponsePayloadInLog(Rule):
171
171
  tree = context.tree
172
172
  if tree is None:
173
173
  return []
174
- facts = _analysis_facts(tree, node_index=context.node_index)
174
+ facts = _analysis_facts(context)
175
175
  diagnostics: list[Diagnostic] = []
176
176
  for node in context.nodes(ast.Call):
177
177
  if not _is_logging_call(node):
@@ -295,13 +295,11 @@ def _is_payload_reference(value: ast.expr, *, label: str | None = None) -> bool:
295
295
  return bool(_REQUEST_RESPONSE.intersection(tokens) and _PAYLOAD_TERMINALS.intersection(tokens))
296
296
 
297
297
 
298
- def _analysis_facts(tree: ast.Module, *, node_index: NodeIndex | None = None) -> _AnalysisFacts:
298
+ def _analysis_facts(context: PythonFileContext) -> _AnalysisFacts:
299
299
  suites: dict[int, _SuiteFacts] = {}
300
300
  statement_suites: dict[int, int] = {}
301
- parents = {
302
- id(child): parent for parent in walk_ast(tree, index=node_index) for child in ast.iter_child_nodes(parent)
303
- }
304
- for owner in walk_ast(tree, index=node_index):
301
+ parents = context.parents
302
+ for owner in context.nodes(ast.AST):
305
303
  for field_name in owner._fields:
306
304
  statements = _statement_list(getattr(owner, field_name, None))
307
305
  if statements is None:
@@ -387,10 +385,10 @@ def _root_name(node: ast.expr) -> str | None:
387
385
  return node.id if isinstance(node, ast.Name) else None
388
386
 
389
387
 
390
- def _containing_statement(node: ast.AST, parents: dict[int, ast.AST]) -> ast.stmt | None:
388
+ def _containing_statement(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> ast.stmt | None:
391
389
  current: ast.AST | None = node
392
390
  while current is not None and not isinstance(current, ast.stmt):
393
- current = parents.get(id(current))
391
+ current = parents.get(current)
394
392
  return current
395
393
 
396
394
 
@@ -22,7 +22,7 @@ from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
22
22
 
23
23
 
24
24
  if TYPE_CHECKING:
25
- from collections.abc import Iterable, Iterator
25
+ from collections.abc import Iterable, Iterator, Mapping
26
26
 
27
27
  from sarj_python_lint._file_context import PythonFileContext
28
28
  from sarj_python_lint.rules._ast_index import NodeIndex
@@ -156,7 +156,7 @@ class PreferConstantTimeSecretCompare(Rule):
156
156
  tree = context.tree
157
157
  if tree is None:
158
158
  return []
159
- parents = {id(child): parent for parent in context.nodes(ast.AST) for child in ast.iter_child_nodes(parent)}
159
+ parents = context.parents
160
160
  role_cache: dict[int, _Roles] = {}
161
161
  environment_names = _environment_names(tree)
162
162
  dunder_compares = _equality_dunder_compares(tree, source, node_index=context.node_index)
@@ -427,13 +427,13 @@ def _bound_names(scope: ast.FunctionDef | ast.AsyncFunctionDef) -> frozenset[str
427
427
 
428
428
  def _dominating_walrus_roles(
429
429
  comparison: ast.Compare,
430
- parents: dict[int, ast.AST],
430
+ parents: Mapping[ast.AST, ast.AST],
431
431
  roles: _Roles,
432
432
  position: tuple[int, int],
433
433
  ) -> dict[str, _Role]:
434
434
  result: dict[str, _Role] = {}
435
435
  current: ast.AST = comparison
436
- while (parent := parents.get(id(current))) is not None:
436
+ while (parent := parents.get(current)) is not None:
437
437
  if isinstance(parent, ast.If) and current in parent.body:
438
438
  for named in nodes(parent.test, ast.NamedExpr):
439
439
  result[named.target.id] = _operand_role(named.value, roles, position, result)
@@ -459,11 +459,11 @@ def _terminal_name(node: ast.expr) -> str | None:
459
459
 
460
460
  def _enclosing_scope(
461
461
  node: ast.AST,
462
- parents: dict[int, ast.AST],
462
+ parents: Mapping[ast.AST, ast.AST],
463
463
  module: ast.Module,
464
464
  ) -> ast.Module | ast.FunctionDef | ast.AsyncFunctionDef:
465
465
  current: ast.AST = node
466
- while (parent := parents.get(id(current))) is not None:
466
+ while (parent := parents.get(current)) is not None:
467
467
  if isinstance(parent, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef)):
468
468
  return parent
469
469
  current = parent