sarj-python-lint 1.64.0__tar.gz → 1.66.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 (241) hide show
  1. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/.gitignore +3 -0
  2. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/PKG-INFO +1 -1
  3. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/pyproject.toml +1 -1
  4. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_file_context.py +19 -0
  5. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/cast_access.py +3 -2
  6. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_annotation_semantics.py +4 -1
  7. sarj_python_lint-1.66.1/src/sarj_python_lint/rules/_ast_names.py +63 -0
  8. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_closed_helpers.py +10 -0
  9. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_comments.py +30 -4
  10. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_copy_on_write.py +2 -4
  11. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_data_union_discriminator.py +1 -0
  12. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_docstrings.py +19 -0
  13. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +2 -2
  14. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_resource_provenance.py +39 -28
  15. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_sql.py +25 -15
  16. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_store_writes.py +3 -3
  17. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +2 -2
  18. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +2 -2
  19. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -8
  20. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +2 -23
  21. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +2 -21
  22. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +3 -3
  23. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +2 -2
  24. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +1 -1
  25. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +37 -36
  26. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +3 -3
  27. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +2 -8
  28. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_discarded_validation_result.py +3 -7
  29. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +1 -1
  30. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +1 -8
  31. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -9
  32. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +2 -2
  33. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +2 -2
  34. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +2 -2
  35. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_mutating_container_transform.py +1 -2
  36. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +1 -6
  37. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_partial_type_is.py +2 -8
  38. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -8
  39. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +32 -33
  40. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +1 -7
  41. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +2 -2
  42. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_select_star.py +74 -38
  43. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +4 -10
  44. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +1 -2
  45. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +1 -8
  46. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +2 -2
  47. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +1 -6
  48. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/over_mocked_test.py +1 -12
  49. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +2 -2
  50. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_class_row.py +1 -6
  51. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +2 -12
  52. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +6 -24
  53. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +1 -2
  54. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +2 -2
  55. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +20 -11
  56. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +2 -4
  57. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +2 -4
  58. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +2 -2
  59. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +1 -6
  60. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +2 -4
  61. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +1 -1
  62. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +1 -8
  63. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +2 -2
  64. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +3 -3
  65. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +2 -2
  66. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +1 -6
  67. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +2 -2
  68. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +1 -8
  69. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +1 -2
  70. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +2 -11
  71. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +1 -0
  72. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +1 -15
  73. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +1 -8
  74. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +3 -3
  75. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -9
  76. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +2 -21
  77. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +2 -18
  78. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1 -13
  79. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +4 -6
  80. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +1 -6
  81. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/restated_test_docstring.py +11 -25
  82. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +1 -10
  83. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +2 -2
  84. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/typed_error_reasons.py +1 -8
  85. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +1 -15
  86. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/unused_mock_setup.py +3 -14
  87. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +2 -2
  88. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/LICENSE +0 -0
  89. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/README.md +0 -0
  90. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/__init__.py +0 -0
  91. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/__main__.py +0 -0
  92. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  93. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_filesystem.py +0 -0
  94. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_python_target.py +0 -0
  95. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  96. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_secret_names.py +0 -0
  97. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_source.py +0 -0
  98. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/_version.py +0 -0
  99. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/benchmark.py +0 -0
  100. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  101. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/json_boundary.py +0 -0
  102. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/py.typed +0 -0
  103. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/ratchet.py +0 -0
  104. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rule_base.py +0 -0
  105. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  106. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  107. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  108. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  109. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  110. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  111. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  112. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  113. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  114. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  115. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  116. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  117. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  118. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  119. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  120. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  121. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  122. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  123. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  124. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  125. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  126. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  127. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  128. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  129. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  130. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  131. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  132. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  133. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  134. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  135. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  136. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  137. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  138. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  139. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  140. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  141. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  142. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  143. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  144. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  145. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  146. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  147. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  148. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  149. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  150. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  151. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  152. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  153. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  154. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  155. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  156. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  157. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  158. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  159. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  160. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  161. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  162. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  163. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  164. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  165. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  166. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  167. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  168. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  169. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  170. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  171. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  172. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  173. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  174. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  175. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  176. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  177. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  178. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  179. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  180. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  181. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  182. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  183. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  184. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  185. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  186. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  187. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  188. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  189. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  190. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  191. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  192. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  193. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  194. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  195. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  196. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  197. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  198. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  199. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  200. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  201. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  202. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  203. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  204. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  205. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  206. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  207. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  208. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  209. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  210. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  211. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  212. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  213. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  214. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  215. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  216. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  217. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  218. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  219. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  220. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  221. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  222. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  223. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  224. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  225. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  226. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  227. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  228. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  229. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  230. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  231. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  232. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  233. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  234. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  235. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  236. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  237. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  238. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  239. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  240. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  241. {sarj_python_lint-1.64.0 → sarj_python_lint-1.66.1}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -44,6 +44,9 @@ Thumbs.db
44
44
  !.claude/skills/test-value/
45
45
  .claude/skills/test-value/*
46
46
  !.claude/skills/test-value/SKILL.md
47
+ !.claude/skills/100-passes/
48
+ .claude/skills/100-passes/*
49
+ !.claude/skills/100-passes/SKILL.md
47
50
  apps/docs/.wrangler/
48
51
  apps/docs/.dev.vars*
49
52
  !apps/docs/.dev.vars.example
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.64.0
3
+ Version: 1.66.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.64.0"
3
+ version = "1.66.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" }]
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, final
6
6
 
7
7
  from sarj_python_lint._analysis_session import AnalysisSession
8
8
  from sarj_python_lint._source import symbol_prefilter_source
9
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
9
10
  from sarj_python_lint.rules._ast_index import NodeIndex
10
11
  from sarj_python_lint.rules._fastapi import FastapiIndex
11
12
  from sarj_python_lint.rules._imports import ImportIndex
@@ -18,6 +19,7 @@ if TYPE_CHECKING:
18
19
  from pathlib import Path
19
20
 
20
21
  from sarj_python_lint.rules._copy_on_write import CopyOnWriteAnalysis
22
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
21
23
  from sarj_python_lint.rules._test_provenance import TestProvenance
22
24
 
23
25
 
@@ -91,6 +93,10 @@ class PythonFileContext:
91
93
  def module_imports(self) -> ImportIndex:
92
94
  return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
93
95
 
96
+ @cached_property
97
+ def annotation_semantics(self) -> AnnotationSemantics:
98
+ return AnnotationSemantics.from_imports(self._valid_tree(), self.module_imports)
99
+
94
100
  @cached_property
95
101
  def dynamic_namespace(self) -> bool:
96
102
  return dynamic_namespace_access(self.nodes(ast.AST), self.imports, self.parents)
@@ -117,3 +123,16 @@ class PythonFileContext:
117
123
  from sarj_python_lint.rules._copy_on_write import CopyOnWriteAnalysis # ruff: ignore[import-outside-top-level] — share ownership analysis only when requested
118
124
 
119
125
  return CopyOnWriteAnalysis(self)
126
+
127
+ @cached_property
128
+ def resource_provenance(self) -> ResourceProvenance:
129
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance # ruff: ignore[import-outside-top-level] -- file-owned binding facts are lazy.
130
+
131
+ # The owner caches this analysis; borrowing prevents a reference cycle.
132
+ return ResourceProvenance(self, borrow_context=True)
133
+
134
+ @cached_property
135
+ def static_resource_provenance(self) -> ResourceProvenance:
136
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance # ruff: ignore[import-outside-top-level] -- keep static-namespace policy in a separate cached analysis.
137
+
138
+ return ResourceProvenance(self, require_static_namespace=True, borrow_context=True)
@@ -6,13 +6,14 @@ from typing import TYPE_CHECKING, Literal
6
6
 
7
7
  from sarj_python_lint._file_context import PythonFileContext
8
8
  from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
9
- from sarj_python_lint.rules._resource_provenance import ResourceProvenance
10
9
 
11
10
 
12
11
  if TYPE_CHECKING:
13
12
  from collections.abc import Iterator
14
13
  from pathlib import Path
15
14
 
15
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
16
+
16
17
 
17
18
  _TYPING = frozenset({"typing", "typing_extensions"})
18
19
  _GETATTR_ARITIES = frozenset({2, 3})
@@ -44,7 +45,7 @@ def indirect_typing_cast_accesses(path: Path, source: str) -> tuple[CastAccess,
44
45
 
45
46
 
46
47
  def _reflection_accesses(context: PythonFileContext) -> list[CastAccess]:
47
- provenance = ResourceProvenance(context)
48
+ provenance = context.resource_provenance
48
49
  candidates = [
49
50
  (node, receiver)
50
51
  for node in context.nodes(ast.Call, ast.Subscript)
@@ -19,7 +19,10 @@ class AnnotationSemantics:
19
19
 
20
20
  @classmethod
21
21
  def from_tree(cls, tree: ast.Module, *, module_scope_only: bool = True) -> Self:
22
- imports = ImportIndex.from_tree(tree, module_scope_only=module_scope_only)
22
+ return cls.from_imports(tree, ImportIndex.from_tree(tree, module_scope_only=module_scope_only))
23
+
24
+ @classmethod
25
+ def from_imports(cls, tree: ast.Module, imports: ImportIndex) -> Self:
23
26
  candidates: dict[str, ast.expr] = {}
24
27
  ambiguous: set[str] = set()
25
28
  for statement in tree.body:
@@ -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
 
@@ -110,9 +110,7 @@ class _Models:
110
110
  def validation_owner(self, unit: SourceUnit) -> _ValidationOwner:
111
111
  if unit.path not in self._validation_owners:
112
112
  context = PythonFileContext(unit.path, unit.source, self._validation_session)
113
- self._validation_owners[unit.path] = _ValidationOwner(
114
- context, ResourceProvenance(context, require_static_namespace=True)
115
- )
113
+ self._validation_owners[unit.path] = _ValidationOwner(context, context.static_resource_provenance)
116
114
  return self._validation_owners[unit.path]
117
115
 
118
116
  def annotations(self, unit: SourceUnit) -> AnnotationSemantics:
@@ -378,7 +376,7 @@ class CopyOnWriteAnalysis:
378
376
 
379
377
  @cached_property
380
378
  def _validator_provider(self) -> ResourceProvenance:
381
- return ResourceProvenance(self.context, require_static_namespace=True)
379
+ return self.context.static_resource_provenance
382
380
 
383
381
  def _function(self, function: _Function) -> None:
384
382
  parameters = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
@@ -72,6 +72,7 @@ def data_union_diagnostics(context: PythonFileContext, code: str) -> list[Diagno
72
72
  class _DataUnions:
73
73
  def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
74
74
  self.context = context
75
+ # This analysis refines mutation ownership; keep its provenance private.
75
76
  self.provenance = ResourceProvenance(context, require_static_namespace=True)
76
77
  self.provenance.mutated = _scoped_module_mutations(self.provenance, tree)
77
78
  bindings = self.provenance.bindings.get(tree, {})
@@ -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:
@@ -13,11 +13,11 @@ from sarj_python_lint.json_boundary import is_object_mapping
13
13
  from sarj_python_lint.rules._first_party import distribution_root
14
14
  from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
15
15
  from sarj_python_lint.rules._native_namespace_capability import native_namespace_capability_escapes
16
- from sarj_python_lint.rules._resource_provenance import ResourceProvenance
17
16
 
18
17
 
19
18
  if TYPE_CHECKING:
20
19
  from sarj_python_lint._file_context import PythonFileContext
20
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
21
21
 
22
22
 
23
23
  MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
@@ -43,7 +43,7 @@ def direct_models(context: PythonFileContext, *, methods: bool = False) -> tuple
43
43
  or dynamic_namespace_access(context.nodes(ast.AST), context.imports, context.parents)
44
44
  ):
45
45
  return ()
46
- provenance = ResourceProvenance(context, require_static_namespace=True)
46
+ provenance = context.static_resource_provenance
47
47
  return tuple(
48
48
  node
49
49
  for node in tree.body
@@ -5,9 +5,9 @@ from collections import defaultdict
5
5
  from functools import cached_property
6
6
  from types import MappingProxyType
7
7
  from typing import TYPE_CHECKING, final
8
+ from weakref import ReferenceType, ref
8
9
 
9
10
  from sarj_python_lint.rules._ast_index import has_own_yield
10
- from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
11
11
  from sarj_python_lint.rules._runtime_bindings import RuntimeBindings
12
12
 
13
13
 
@@ -71,8 +71,13 @@ def builtin_namespace_untouched(context: PythonFileContext) -> bool:
71
71
 
72
72
  @final
73
73
  class ResourceProvenance:
74
- def __init__(self, context: PythonFileContext, *, require_static_namespace: bool = False) -> None:
75
- self.context = context
74
+ def __init__(
75
+ self, context: PythonFileContext, *, require_static_namespace: bool = False, borrow_context: bool = False
76
+ ) -> None:
77
+ self._context: PythonFileContext | ReferenceType[PythonFileContext] = (
78
+ ref(context) if borrow_context else context
79
+ )
80
+ self.parents = context.parents
76
81
  self.require_static_namespace = require_static_namespace
77
82
  self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
78
83
  self.reads: dict[str, list[ast.Name]] = defaultdict(list)
@@ -98,15 +103,23 @@ class ResourceProvenance:
98
103
  if root is not None:
99
104
  self.mutated.add(root)
100
105
 
106
+ @property
107
+ def context(self) -> PythonFileContext:
108
+ context = self._context() if isinstance(self._context, ReferenceType) else self._context
109
+ if context is None:
110
+ msg = "The provenance file context has been released"
111
+ raise ReferenceError(msg)
112
+ return context
113
+
101
114
  @cached_property
102
115
  def builtin_namespace_untouched(self) -> bool:
103
116
  return builtin_namespace_untouched(self.context)
104
117
 
105
118
  def scope(self, node: ast.AST) -> ast.AST | None:
106
- parent = self.context.parents.get(node)
119
+ parent = self.parents.get(node)
107
120
  while parent is not None:
108
121
  if isinstance(parent, ast.arguments) and (node in parent.defaults or node in parent.kw_defaults):
109
- parent = self.context.parents.get(parent)
122
+ parent = self.parents.get(parent)
110
123
  elif isinstance(parent, _SCOPES) and not (
111
124
  (
112
125
  isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
@@ -118,12 +131,12 @@ class ResourceProvenance:
118
131
  if parent is None:
119
132
  return None
120
133
  node = parent
121
- parent = self.context.parents.get(parent)
134
+ parent = self.parents.get(parent)
122
135
  return None
123
136
 
124
137
  def _binding_scope(self, node: ast.AST) -> ast.AST | None:
125
138
  scope = self.scope(node)
126
- parent = self.context.parents.get(node)
139
+ parent = self.parents.get(node)
127
140
  if isinstance(parent, ast.NamedExpr) and parent.target is node:
128
141
  while isinstance(scope, ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp):
129
142
  scope = self.scope(scope)
@@ -155,7 +168,7 @@ class ResourceProvenance:
155
168
  binding = self.runtime_binding(root, node)
156
169
  if not isinstance(binding, ast.alias):
157
170
  return None
158
- qualified = import_name_for_binding(node, binding, self.context.parents.get(binding))
171
+ qualified = import_name_for_binding(node, binding, self.parents.get(binding))
159
172
  if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
160
173
  return None
161
174
  if binding not in self.runtime_import_cache:
@@ -195,7 +208,7 @@ class ResourceProvenance:
195
208
  def _namespace_aliases(self) -> dict[str, tuple[ast.alias, ...]]:
196
209
  aliases: dict[str, list[ast.alias]] = defaultdict(list)
197
210
  for binding in self.context.nodes(ast.alias):
198
- parent = self.context.parents.get(binding)
211
+ parent = self.parents.get(binding)
199
212
  name = binding.asname or (
200
213
  binding.name.partition(".")[0] if isinstance(parent, ast.Import) else binding.name
201
214
  )
@@ -231,7 +244,7 @@ class ResourceProvenance:
231
244
  return paths
232
245
 
233
246
  def _namespace_module_path(self, binding: ast.alias, attributes: tuple[str, ...]) -> Path | None:
234
- parent = self.context.parents.get(binding)
247
+ parent = self.parents.get(binding)
235
248
  match parent:
236
249
  case ast.Import():
237
250
  prefix = binding.name if binding.asname else binding.name.partition(".")[0]
@@ -250,7 +263,7 @@ class ResourceProvenance:
250
263
  for node in nodes:
251
264
  if not isinstance(node, ast.alias):
252
265
  return False
253
- parent = self.context.parents.get(node)
266
+ parent = self.parents.get(node)
254
267
  if isinstance(parent, ast.Import):
255
268
  targets.add(("import", None, 0, node.name))
256
269
  elif isinstance(parent, ast.ImportFrom):
@@ -271,7 +284,7 @@ class ResourceProvenance:
271
284
  return False
272
285
  binding = self.binding(root, node)
273
286
  for read in self.reads.get(root, ()):
274
- parent = self.context.parents.get(read)
287
+ parent = self.parents.get(read)
275
288
  if isinstance(parent, ast.Attribute | ast.ExceptHandler):
276
289
  continue
277
290
  if not self._safe_import_use(read):
@@ -317,7 +330,7 @@ class ResourceProvenance:
317
330
  )
318
331
 
319
332
  def _safe_import_use(self, node: ast.Name | ast.Attribute) -> bool:
320
- parent = self.context.parents.get(node)
333
+ parent = self.parents.get(node)
321
334
  return (
322
335
  (isinstance(parent, ast.Attribute) and parent.value is node)
323
336
  or (isinstance(parent, ast.Call) and parent.func is node)
@@ -337,9 +350,9 @@ class ResourceProvenance:
337
350
  binding = self.binding(name.id, at)
338
351
  if not isinstance(binding, ast.Name):
339
352
  return None
340
- statement = self.context.parents.get(binding)
353
+ statement = self.parents.get(binding)
341
354
  if isinstance(statement, ast.withitem):
342
- owner = self.context.parents.get(statement)
355
+ owner = self.parents.get(statement)
343
356
  if (
344
357
  binding is statement.optional_vars
345
358
  and self.contains(owner, at)
@@ -351,12 +364,12 @@ class ResourceProvenance:
351
364
  return value if isinstance(value, ast.Call) else None
352
365
 
353
366
  def assignment_value(self, binding: ast.Name, at: ast.AST) -> ast.expr | None:
354
- statement = self.context.parents.get(binding)
367
+ statement = self.parents.get(binding)
355
368
  if not isinstance(statement, ast.Assign | ast.AnnAssign):
356
369
  return None
357
370
  if isinstance(statement, ast.Assign) and len(statement.targets) != 1:
358
371
  return None
359
- if not isinstance(self.context.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
372
+ if not isinstance(self.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
360
373
  return None
361
374
  return statement.value if statement.lineno < getattr(at, "lineno", 0) else None
362
375
 
@@ -381,12 +394,10 @@ class ResourceProvenance:
381
394
 
382
395
  @cached_property
383
396
  def dynamic_namespace(self) -> bool:
384
- return self.require_static_namespace and dynamic_namespace_access(
385
- self.context.nodes(ast.AST), self.context.imports, self.context.parents
386
- )
397
+ return self.require_static_namespace and self.context.dynamic_namespace
387
398
 
388
399
  def _stable_async_definition(self, function: ast.AsyncFunctionDef) -> ast.AsyncFunctionDef | None:
389
- owner = self.context.parents.get(function)
400
+ owner = self.parents.get(function)
390
401
  if not isinstance(owner, ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
391
402
  return None
392
403
  if function.decorator_list or has_own_yield(function):
@@ -415,10 +426,10 @@ class ResourceProvenance:
415
426
  return True
416
427
 
417
428
  def _function_alias(self, read: ast.Name) -> ast.Name | None:
418
- parent = self.context.parents.get(read)
429
+ parent = self.parents.get(read)
419
430
  if not isinstance(parent, ast.Assign | ast.AnnAssign) or parent.value is not read:
420
431
  return None
421
- if not isinstance(self.context.parents.get(parent), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
432
+ if not isinstance(self.parents.get(parent), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
422
433
  return None
423
434
  targets = parent.targets if isinstance(parent, ast.Assign) else [parent.target]
424
435
  if len(targets) != 1 or not isinstance(targets[0], ast.Name):
@@ -427,7 +438,7 @@ class ResourceProvenance:
427
438
  return target if self.binding(target.id, target) is target else None
428
439
 
429
440
  def _safe_async_read(self, read: ast.Name) -> bool:
430
- parent = self.context.parents.get(read)
441
+ parent = self.parents.get(read)
431
442
  if not isinstance(parent, ast.Call):
432
443
  return False
433
444
  if parent.func is read:
@@ -469,7 +480,7 @@ class ResourceProvenance:
469
480
  return self.escape_cache[key]
470
481
 
471
482
  def _method_or_context_read(self, read: ast.Name, *, allow_context: bool) -> bool:
472
- parent = self.context.parents.get(read)
483
+ parent = self.parents.get(read)
473
484
  if allow_context and isinstance(parent, ast.withitem) and parent.context_expr is read:
474
485
  return True
475
486
  if (
@@ -478,12 +489,12 @@ class ResourceProvenance:
478
489
  or parent.attr in {"__setattr__", "__delattr__", "__getattribute__"}
479
490
  ):
480
491
  return False
481
- call = self.context.parents.get(parent)
492
+ call = self.parents.get(parent)
482
493
  return isinstance(call, ast.Call) and call.func is parent
483
494
 
484
495
  def contains(self, owner: ast.AST | None, node: ast.AST) -> bool:
485
- while node in self.context.parents:
486
- node = self.context.parents[node]
496
+ while node in self.parents:
497
+ node = self.parents[node]
487
498
  if node is owner:
488
499
  return True
489
500
  return False
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ import re
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from sarj_python_lint.rules._paths import is_test_path
@@ -10,6 +11,15 @@ if TYPE_CHECKING:
10
11
  from pathlib import Path
11
12
 
12
13
 
14
+ _SQL_LINE_END = re.compile(r"[\r\n]")
15
+ _CONTINUED_QUOTE = re.compile(r"[ \t\f\v]*(?:--[^\r\n]*)?[\r\n](?:[ \t\f\v\r\n]|--[^\r\n]*[\r\n])*'")
16
+ POSTGRES_IDENTIFIER_CONTINUATION = r"[A-Za-z0-9_$\u0080-\U0010ffff]"
17
+ POSTGRES_UNQUOTED_IDENTIFIER = rf"[A-Za-z_\u0080-\U0010ffff]{POSTGRES_IDENTIFIER_CONTINUATION}*"
18
+ SQL_WHITESPACE = " \t\r\n\f\v"
19
+ _IDENTIFIER_CONTINUATION = re.compile(POSTGRES_IDENTIFIER_CONTINUATION)
20
+ _DOLLAR_DELIMITER = re.compile(r"\$(?:[A-Za-z_\u0080-\U0010ffff][A-Za-z0-9_\u0080-\U0010ffff]*)?\$")
21
+
22
+
13
23
  def is_store_module(path: Path) -> bool:
14
24
  if is_test_path(path):
15
25
  return False
@@ -44,6 +54,7 @@ def strip_sql_noise(
44
54
  *,
45
55
  mask_dollar_quotes: bool = True,
46
56
  mask_double_quotes: bool = True,
57
+ preserve_quoted_tokens: bool = False,
47
58
  ) -> str:
48
59
  out = list(text)
49
60
  i = 0
@@ -56,8 +67,10 @@ def strip_sql_noise(
56
67
  i += 1
57
68
  continue
58
69
  for offset in range(i, end):
59
- if text[offset] != "\n":
70
+ if text[offset] not in {"\n", "\r"}:
60
71
  out[offset] = " "
72
+ if preserve_quoted_tokens and text[i] in {"'", '"', "$"}:
73
+ out[i] = "?"
61
74
  i = end
62
75
  return "".join(out)
63
76
 
@@ -68,7 +81,7 @@ def _quoted_sql_end(text: str, start: int) -> int:
68
81
  quote == "'"
69
82
  and start > 0
70
83
  and text[start - 1] in {"E", "e"}
71
- and (start == 1 or not (text[start - 2].isalnum() or text[start - 2] in {"_", "$"}))
84
+ and (start == 1 or _IDENTIFIER_CONTINUATION.match(text, start - 2) is None)
72
85
  )
73
86
  i = start + 1
74
87
  while i < len(text):
@@ -82,6 +95,9 @@ def _quoted_sql_end(text: str, start: int) -> int:
82
95
  if i < len(text) and text[i] == quote:
83
96
  i += 1
84
97
  continue
98
+ if escape_string and (continuation := _CONTINUED_QUOTE.match(text, i)) is not None:
99
+ i = continuation.end()
100
+ continue
85
101
  break
86
102
  return i
87
103
 
@@ -91,8 +107,8 @@ def _sql_noise_end(text: str, start: int, *, mask_dollar_quotes: bool) -> int |
91
107
  if ch in {"'", '"'}:
92
108
  return _quoted_sql_end(text, start)
93
109
  if text.startswith("--", start) or (ch == "#" and (start == 0 or text[start - 1].isspace())):
94
- end = text.find("\n", start)
95
- return len(text) if end == -1 else end
110
+ end = _SQL_LINE_END.search(text, start)
111
+ return len(text) if end is None else end.start()
96
112
  if text.startswith("/*", start):
97
113
  return _block_comment_end(text, start)
98
114
  if mask_dollar_quotes and ch == "$":
@@ -116,17 +132,11 @@ def _block_comment_end(text: str, start: int) -> int:
116
132
 
117
133
 
118
134
  def _dollar_quote_end(text: str, start: int) -> int | None:
119
- if start > 0 and (text[start - 1].isalnum() or text[start - 1] in {"_", "$"}):
135
+ if start > 0 and _IDENTIFIER_CONTINUATION.match(text, start - 1) is not None:
120
136
  return None
121
- delimiter_end = text.find("$", start + 1)
122
- if delimiter_end == -1:
123
- return None
124
- tag = text[start + 1 : delimiter_end]
125
- valid_tag = not tag or (
126
- (tag[0].isalpha() or tag[0] == "_") and all(char.isalnum() or char == "_" for char in tag[1:])
127
- )
128
- if not valid_tag:
137
+ match = _DOLLAR_DELIMITER.match(text, start)
138
+ if match is None:
129
139
  return None
130
- delimiter = text[start : delimiter_end + 1]
131
- close = text.find(delimiter, delimiter_end + 1)
140
+ delimiter = match[0]
141
+ close = text.find(delimiter, match.end())
132
142
  return None if close == -1 else close + len(delimiter)
@@ -12,7 +12,6 @@ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, sc
12
12
  from sarj_python_lint.rules._ast_index import walk
13
13
  from sarj_python_lint.rules._paths import is_generated
14
14
  from sarj_python_lint.rules._project_index import ProjectIndexSet
15
- from sarj_python_lint.rules._resource_provenance import ResourceProvenance
16
15
 
17
16
 
18
17
  if TYPE_CHECKING:
@@ -20,6 +19,7 @@ if TYPE_CHECKING:
20
19
 
21
20
  from sarj_python_lint._file_context import PythonFileContext
22
21
  from sarj_python_lint.rules._imports import ImportIndex
22
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
23
23
 
24
24
 
25
25
  type Function = ast.FunctionDef | ast.AsyncFunctionDef
@@ -187,7 +187,7 @@ def _comprehension_scopes(
187
187
  class StoreOwners:
188
188
  def __init__(self, context: PythonFileContext) -> None:
189
189
  self.context = context
190
- self.provenance = ResourceProvenance(context, require_static_namespace=True)
190
+ self.provenance = context.static_resource_provenance
191
191
  self.project = context.session.project
192
192
 
193
193
  def receiver(self, expression: ast.expr, at: ast.AST) -> tuple[ast.ClassDef, ImportIndex] | None:
@@ -290,7 +290,7 @@ class StoreOwners:
290
290
  from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer to avoid the rule registry's context import cycle.
291
291
 
292
292
  definition = PythonFileContext(owner.path, owner.source, self.context.session)
293
- provenance = ResourceProvenance(definition, require_static_namespace=True)
293
+ provenance = definition.static_resource_provenance
294
294
  if (
295
295
  provenance.dynamic_namespace
296
296
  or provenance.wildcard