sarj-python-lint 1.22.1__tar.gz → 1.24.0__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 (182) hide show
  1. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +27 -14
  4. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +103 -14
  5. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +95 -4
  6. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/.gitignore +0 -0
  7. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/LICENSE +0 -0
  8. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/README.md +0 -0
  9. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  29. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  30. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  31. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  32. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  33. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  34. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  35. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  36. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  37. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  38. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  39. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  40. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  41. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  42. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  43. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  44. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  45. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  46. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  47. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  48. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  49. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  50. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  51. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  52. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  53. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  54. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  55. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  56. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  57. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  58. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  59. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  60. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  61. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  62. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  63. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  64. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  65. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  66. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  67. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  68. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  69. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  70. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  71. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  72. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  73. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  74. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  75. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  76. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  77. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  78. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  79. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  80. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  81. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  82. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  83. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  84. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  85. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  86. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  87. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  88. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  89. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  90. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  91. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  92. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  93. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  94. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  95. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  96. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  97. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  98. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  99. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  100. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  101. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  102. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  103. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  104. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  105. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  106. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  107. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  108. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  109. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  110. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  111. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  112. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  113. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  114. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  115. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  116. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  117. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  118. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  119. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  120. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  121. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  122. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  123. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  124. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  125. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  126. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  127. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  128. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  129. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  130. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  131. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  132. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  133. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  134. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  135. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  136. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  137. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  138. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  139. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  140. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  141. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  142. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  143. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  144. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  145. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  146. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  147. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  148. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  149. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  150. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  151. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  152. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  153. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  154. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  155. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  156. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  157. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  158. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  159. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  160. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  161. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  162. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  163. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  164. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  165. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  166. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  167. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  168. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  169. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  170. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  171. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  172. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  173. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  174. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  175. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  176. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  177. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  178. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  179. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  180. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  181. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  182. {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/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.22.1
3
+ Version: 1.24.0
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.22.1"
3
+ version = "1.24.0"
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" }]
@@ -133,18 +133,8 @@ class NoPsycopgExecutionOutsideInjectedOwner(Rule):
133
133
  or _is_operational_path(path)
134
134
  ):
135
135
  return []
136
- tree = context.tree
137
- if not isinstance(tree, ast.Module):
138
- return []
139
- types = _type_index(tree)
140
- ownership = {
141
- node: _class_ownership(node, types) for node in context.nodes(ast.AST) if isinstance(node, ast.ClassDef)
142
- }
143
136
  source_lines = context.source_lines
144
- calls: list[ast.Call] = []
145
- for scope in _function_scopes(tree):
146
- owned = ownership[scope.owner] if scope.owner is not None else _ClassOwnership(frozenset(), frozenset())
147
- calls.extend(_FunctionAnalyzer(types, owned).analyze(scope.function))
137
+ calls = psycopg_execution_calls(context, include_injected=False, include_probes=False)
148
138
  unique = {(call.lineno, call.col_offset): call for call in calls}
149
139
  return [
150
140
  Diagnostic(
@@ -163,6 +153,25 @@ class NoPsycopgExecutionOutsideInjectedOwner(Rule):
163
153
  ]
164
154
 
165
155
 
156
+ def psycopg_execution_calls(
157
+ context: PythonFileContext, *, include_injected: bool, include_probes: bool
158
+ ) -> list[ast.Call]:
159
+ tree = context.tree
160
+ if tree is None:
161
+ return []
162
+ types = _type_index(tree)
163
+ ownership = {node: _class_ownership(node, types) for node in context.nodes(ast.ClassDef)}
164
+ calls: list[ast.Call] = []
165
+ for scope in _function_scopes(tree):
166
+ owned = ownership[scope.owner] if scope.owner is not None else _ClassOwnership(frozenset(), frozenset())
167
+ calls.extend(
168
+ _FunctionAnalyzer(types, owned, include_injected=include_injected, include_probes=include_probes).analyze(
169
+ scope.function
170
+ )
171
+ )
172
+ return calls
173
+
174
+
166
175
  def _type_index(tree: ast.Module) -> _TypeIndex:
167
176
  counts = _module_binding_counts(tree)
168
177
  aliases = _module_type_aliases(tree, counts)
@@ -416,9 +425,13 @@ def _function_scopes(tree: ast.Module) -> list[_FunctionScope]:
416
425
 
417
426
  @final
418
427
  class _FunctionAnalyzer:
419
- def __init__(self, types: _TypeIndex, ownership: _ClassOwnership) -> None:
428
+ def __init__(
429
+ self, types: _TypeIndex, ownership: _ClassOwnership, *, include_injected: bool, include_probes: bool
430
+ ) -> None:
420
431
  self._types = types
421
432
  self._ownership = ownership
433
+ self._allowed_origins = frozenset(_Origin) if include_injected else frozenset({_Origin.EXTERNAL})
434
+ self._include_probes = include_probes
422
435
  self._findings: list[ast.Call] = []
423
436
  self._shadowed: set[str] = set()
424
437
 
@@ -578,8 +591,8 @@ class _FunctionAnalyzer:
578
591
  if (
579
592
  isinstance(node.func, ast.Attribute)
580
593
  and node.func.attr in _EXECUTE_METHODS
581
- and analyzer._expression_origin(node.func.value, state) is _Origin.EXTERNAL
582
- and not _is_connection_probe(node)
594
+ and analyzer._expression_origin(node.func.value, state) in analyzer._allowed_origins
595
+ and (analyzer._include_probes or not _is_connection_probe(node))
583
596
  ):
584
597
  analyzer._findings.append(node)
585
598
  self.generic_visit(node)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from pathlib import PurePosixPath
5
+ import re
5
6
  from typing import TYPE_CHECKING, ClassVar, final, override
6
7
 
7
8
  from sarj_python_lint.rule_base import (
@@ -14,9 +15,12 @@ from sarj_python_lint.rule_base import (
14
15
  RuleDocumentation,
15
16
  RuleExample,
16
17
  Severity,
18
+ is_suppressed,
17
19
  )
18
20
  from sarj_python_lint.rules._ast_index import walk as walk_ast
19
21
  from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
22
+ from sarj_python_lint.rules._sql import sql_string_value, strip_sql_noise
23
+ from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import psycopg_execution_calls
20
24
 
21
25
 
22
26
  if TYPE_CHECKING:
@@ -26,6 +30,17 @@ if TYPE_CHECKING:
26
30
 
27
31
 
28
32
  _POOL_TYPES = frozenset({"AsyncConnectionPool", "ConnectionPool"})
33
+ _SQL_METHODS = frozenset(
34
+ {"execute", "executemany", "executescript", "exec_driver_sql", "fetch", "fetchrow", "fetchval", "query", "command"}
35
+ )
36
+ _QUERY_KEYWORDS = frozenset({"query", "sql", "statement", "command", "operation"})
37
+ _SQL_START = re.compile(
38
+ r"\A\s*(?:SELECT\b|WITH\s+\S+[\s\S]*?\bAS\s*\(|INSERT\s+INTO\b|"
39
+ r"UPDATE\s+\S+[\s\S]*?\bSET\b|DELETE\s+FROM\b|TRUNCATE\b|"
40
+ r"(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX|SCHEMA|VIEW|FUNCTION|TRIGGER)\b|"
41
+ r"LOCK\s+TABLE\b|SET\s+(?:LOCAL|TRANSACTION)\b)",
42
+ re.IGNORECASE,
43
+ )
29
44
 
30
45
 
31
46
  @final
@@ -34,34 +49,35 @@ class NoRawConnectionInTests(Rule):
34
49
  code = "SARJ429"
35
50
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
36
51
  default_level=Severity.ERROR,
37
- summary="Do not acquire raw database connections in tests.",
52
+ summary="Do not execute raw SQL or acquire raw database connections in tests.",
38
53
  rationale=(
39
54
  "Tests that reach through a pool couple assertions and setup to persistence internals, bypass the "
40
55
  "application boundary, and duplicate transaction ownership."
41
56
  ),
42
57
  remediation=(
43
- "Exercise the owning store/service API or expose a narrow test-support fixture. Use an exact SARJ429 "
44
- "suppression for a test whose purpose is explicitly connection or transaction behavior."
58
+ "Exercise the owning store/service API. Do not move SQL into a test-only probe to hide it. Use an exact "
59
+ "SARJ429 suppression for deliberate database bootstrap, migration or transaction fault testing."
45
60
  ),
46
61
  category=RuleCategory.TESTING,
47
62
  autofix=AutofixPolicy.NONE,
63
+ aliases=("no-raw-sql-in-tests",),
48
64
  limitations=(
49
- "Only collected test modules outside conftest.py, conventional shared test-support modules, and migration-test trees are inspected.",
50
- "A receiver is reported only when a parameter, annotated local, or constructor call proves it is a psycopg ConnectionPool or AsyncConnectionPool.",
51
- "Pytest fixtures may use a connection internally for setup and cleanup, but fixtures that return or yield the connection remain reportable.",
65
+ "Raw SQL execution is checked in tests, conftest.py, shared test-support helpers and migration tests; generated code is excluded. Standalone SQL examples and store/service calls are not execution.",
66
+ "Import-proven Psycopg connection/pool flows, including constructor-injected members, aliases and dynamic query arguments, are followed. SQL-shaped literals in execute/query/fetch-like calls are also checked across drivers, including concatenation, f-strings and import-proven SQL/text constructors.",
67
+ "Opaque non-Psycopg builders and interprocedural receiver flows are not inferred. Raw connection acquisition keeps its existing fixture/support exclusions when no SQL execution is found in that scope.",
52
68
  ),
53
69
  examples=(
54
70
  RuleExample(
55
- example_id="test-acquires-pool-connection",
56
- title="A test helper reaches directly through its pool",
71
+ example_id="test-probe-executes-sql",
72
+ title="An injected test probe still bypasses the owning store",
57
73
  outcome=ExampleOutcome.MATCH,
58
74
  files=(
59
75
  ExampleFile.python(
60
- "tests/test_orders.py",
61
- "async def load_rows(pool: AsyncConnectionPool):\n async with pool.connection() as conn:\n return await conn.execute('SELECT 1')\n",
76
+ "tests/fixtures/probe.py",
77
+ "from psycopg_pool import AsyncConnectionPool\n\nclass Probe:\n def __init__(self, pool: AsyncConnectionPool):\n self._pool = pool\n\n async def seed(self):\n async with self._pool.connection() as conn:\n await conn.execute('INSERT INTO jobs (id) VALUES (%s)', (job_id,))\n",
62
78
  ),
63
79
  ),
64
- focus_path=PurePosixPath("tests/test_orders.py"),
80
+ focus_path=PurePosixPath("tests/fixtures/probe.py"),
65
81
  expected_count=1,
66
82
  public=True,
67
83
  ),
@@ -86,6 +102,20 @@ class NoRawConnectionInTests(Rule):
86
102
  @override
87
103
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
88
104
  path = context.path
105
+ if context.generated or context.tree is None:
106
+ return []
107
+ sql_calls = _test_sql_calls(context)
108
+ diagnostics = [
109
+ Diagnostic(
110
+ path=path,
111
+ line=node.lineno,
112
+ col=node.col_offset + 1,
113
+ code=self.code,
114
+ message="test code executes raw SQL; use the owning store/service API or an exact SARJ429 exception for deliberate database-level testing",
115
+ )
116
+ for node in sql_calls
117
+ if not is_suppressed(context.source_lines, node.lineno, self.code)
118
+ ]
89
119
  excluded_path = any(
90
120
  (
91
121
  not is_test_path(path),
@@ -95,17 +125,19 @@ class NoRawConnectionInTests(Rule):
95
125
  _is_migration_test(path),
96
126
  )
97
127
  )
98
- if excluded_path or context.generated:
99
- return []
128
+ if excluded_path:
129
+ return sorted(diagnostics, key=lambda item: (item.line, item.col))
100
130
  tree = context.tree
101
131
  if tree is None:
102
132
  return []
103
- diagnostics: list[Diagnostic] = []
133
+ sql_scopes = {_containing_scope(context, node) for node in sql_calls}
104
134
  scopes: list[ast.Module | ast.FunctionDef | ast.AsyncFunctionDef] = [
105
135
  tree,
106
136
  *(node for node in context.nodes(ast.AST) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))),
107
137
  ]
108
138
  for scope in scopes:
139
+ if scope in sql_scopes:
140
+ continue
109
141
  pool_names = _proven_pool_names(scope)
110
142
  diagnostics.extend(
111
143
  Diagnostic(
@@ -129,6 +161,63 @@ class NoRawConnectionInTests(Rule):
129
161
  return sorted(diagnostics, key=lambda item: (item.line, item.col))
130
162
 
131
163
 
164
+ def _test_sql_calls(context: PythonFileContext) -> list[ast.Call]:
165
+ path = context.path
166
+ if not (is_test_path(path) or is_test_support_path(path) or "test_support" in path.parts):
167
+ return []
168
+ candidates = [
169
+ node
170
+ for node in context.nodes(ast.Call)
171
+ if isinstance(node.func, ast.Attribute) and node.func.attr in _SQL_METHODS
172
+ ]
173
+ if not candidates:
174
+ return []
175
+ calls: list[ast.Call] = []
176
+ for node in candidates:
177
+ value = _execution_sql(node, context)
178
+ if value is not None and _SQL_START.search(strip_sql_noise(value)):
179
+ calls.append(node)
180
+ if any(
181
+ isinstance(node.func, ast.Attribute) and node.func.attr in {"execute", "executemany"} and node not in calls
182
+ for node in candidates
183
+ ):
184
+ calls.extend(psycopg_execution_calls(context, include_injected=True, include_probes=True))
185
+ unique = {(call.lineno, call.col_offset): call for call in calls}
186
+ return sorted(unique.values(), key=lambda item: (item.lineno, item.col_offset))
187
+
188
+
189
+ def _execution_sql(call: ast.Call, context: PythonFileContext) -> str | None:
190
+ if call.args:
191
+ return _literal_sql(call.args[0], context)
192
+ for keyword in call.keywords:
193
+ if keyword.arg in _QUERY_KEYWORDS:
194
+ return _literal_sql(keyword.value, context)
195
+ return None
196
+
197
+
198
+ def _literal_sql(node: ast.expr | None, context: PythonFileContext) -> str | None:
199
+ if node is None:
200
+ return None
201
+ if isinstance(node, ast.Call):
202
+ if isinstance(node.func, ast.Attribute) and node.func.attr == "format":
203
+ return _literal_sql(node.func.value, context)
204
+ if node.args and (
205
+ context.imports.resolves(node.func, sources=frozenset({"psycopg.sql"}), symbol="SQL")
206
+ or context.imports.resolves(node.func, sources=frozenset({"sqlalchemy"}), symbol="text")
207
+ ):
208
+ return sql_string_value(node.args[0])
209
+ return sql_string_value(node)
210
+
211
+
212
+ def _containing_scope(context: PythonFileContext, node: ast.AST) -> ast.AST:
213
+ current = node
214
+ while (parent := context.parents.get(current)) is not None:
215
+ if isinstance(parent, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef)):
216
+ return parent
217
+ current = parent
218
+ return current
219
+
220
+
132
221
  def _is_non_collected_test_support(path: Path) -> bool:
133
222
  return "tests" in path.parts and not (path.name.startswith("test_") or path.name.endswith("_test.py"))
134
223
 
@@ -96,7 +96,7 @@ class PreferCollectionComprehension(Rule):
96
96
  "Only function-local, adjacent empty initializers and one synchronous single-purpose loop are checked.",
97
97
  (
98
98
  "The rule fills gaps left by Ruff PERF401, PERF403, and FURB142: derived dict projections, "
99
- "destructured list projections, narrowly filtered computed list candidates, and filtered set "
99
+ "constructor-valued dict projections, destructured list projections, narrowly filtered computed list candidates, and filtered set "
100
100
  "builders."
101
101
  ),
102
102
  (
@@ -115,6 +115,41 @@ class PreferCollectionComprehension(Rule):
115
115
  ),
116
116
  ),
117
117
  examples=(
118
+ RuleExample(
119
+ example_id="constructor-dict-projection-loop",
120
+ title="Build constructor-valued dictionaries directly",
121
+ outcome=ExampleOutcome.MATCH,
122
+ files=(
123
+ ExampleFile.python(
124
+ "app/views.py",
125
+ "def views(rows, labels):\n"
126
+ " result = {}\n"
127
+ " for row in rows:\n"
128
+ " result[row.id] = EntryView(id=row.id, label=labels.get(row.id))\n"
129
+ " return result\n",
130
+ ),
131
+ ),
132
+ focus_path=PurePosixPath("app/views.py"),
133
+ expected_count=1,
134
+ public=True,
135
+ scenario="constructor-dict",
136
+ ),
137
+ RuleExample(
138
+ example_id="constructor-dict-comprehension",
139
+ title="Keep constructor-valued dictionaries declarative",
140
+ outcome=ExampleOutcome.NO_MATCH,
141
+ files=(
142
+ ExampleFile.python(
143
+ "app/views.py",
144
+ "def views(rows, labels):\n"
145
+ " return {row.id: EntryView(id=row.id, label=labels.get(row.id)) for row in rows}\n",
146
+ ),
147
+ ),
148
+ focus_path=PurePosixPath("app/views.py"),
149
+ expected_count=0,
150
+ public=True,
151
+ scenario="constructor-dict",
152
+ ),
118
153
  RuleExample(
119
154
  example_id="derived-dict-projection-loop",
120
155
  title="Build a projected dictionary directly",
@@ -331,6 +366,7 @@ def _loop_replacement(
331
366
  _CollectionKind.DICT,
332
367
  f"{{{ast.unparse(key)}: {ast.unparse(value)} for {ast.unparse(loop.target)} "
333
368
  f"in {ast.unparse(loop.iter)}}}",
369
+ allow_multiline=isinstance(value, ast.Call),
334
370
  )
335
371
  case _:
336
372
  pass
@@ -520,8 +556,20 @@ def _valid_dict_projection(key: ast.expr, value: ast.expr, name: str, bound_name
520
556
  return False
521
557
  return (
522
558
  _simple_projection(key, bound_names)
523
- and _simple_projection(value, bound_names)
524
- and (_is_derived(key) or _is_derived(value))
559
+ and (_simple_projection(value, bound_names) or _constructor_projection(value))
560
+ and (_is_derived(key) or _is_derived(value) or isinstance(value, ast.Call))
561
+ )
562
+
563
+
564
+ def _constructor_projection(node: ast.expr) -> bool:
565
+ if not isinstance(node, ast.Call) or _contains_prohibited_expression(node):
566
+ return False
567
+ return all(
568
+ _dotted_name(call.func)
569
+ and not any(isinstance(argument, ast.Starred) for argument in call.args)
570
+ and all(keyword.arg is not None for keyword in call.keywords)
571
+ for call in walk_ast(node)
572
+ if isinstance(call, ast.Call)
525
573
  )
526
574
 
527
575
 
@@ -553,9 +601,12 @@ def _target_binding_is_observable(owner: _Callable, names: frozenset[str], loop:
553
601
  if _declares_external(owner, names) or any(_loads_name(loop.iter, name) for name in names):
554
602
  return True
555
603
  end_line = loop.end_lineno or loop.lineno
604
+ comprehension_locals = _comprehension_local_nodes(owner, names)
556
605
  for node in walk_ast(owner):
557
606
  if isinstance(node, ast.arg) and node.arg in names:
558
607
  return True
608
+ if id(node) in comprehension_locals:
609
+ continue
559
610
  if (
560
611
  isinstance(node, ast.Name)
561
612
  and node.id in names
@@ -568,6 +619,29 @@ def _target_binding_is_observable(owner: _Callable, names: frozenset[str], loop:
568
619
  return False
569
620
 
570
621
 
622
+ def _comprehension_local_nodes(owner: _Callable, names: frozenset[str]) -> set[int]:
623
+ local_nodes: set[int] = set()
624
+ for node in walk_ast(owner):
625
+ if not isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
626
+ continue
627
+ # Multiple generators can read an outer binding before a later generator shadows it.
628
+ if len(node.generators) != 1:
629
+ continue
630
+ bound = _bound_names(node.generators[0].target) & names
631
+ if not bound:
632
+ continue
633
+ # The first iterable runs in the surrounding scope; targets and remaining expressions are local.
634
+ scoped: list[ast.AST] = [node.generators[0].target, *node.generators[0].ifs]
635
+ scoped.extend((node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,))
636
+ local_nodes.update(
637
+ id(child)
638
+ for expression in scoped
639
+ for child in walk_ast(expression)
640
+ if isinstance(child, ast.Name) and child.id in bound
641
+ )
642
+ return local_nodes
643
+
644
+
571
645
  def _declares_external(owner: _Callable, names: frozenset[str]) -> bool:
572
646
  return any(
573
647
  isinstance(node, (ast.Global, ast.Nonlocal)) and not names.isdisjoint(node.names) for node in walk_ast(owner)
@@ -604,7 +678,24 @@ def _replacement_fits(init: _Init, name: str, expression: str, *, allow_multilin
604
678
  prefix = f"{name}: {ast.unparse(init.annotation)} = "
605
679
  if init.col_offset + len(prefix) + len(expression) <= _MAX_REPLACEMENT_WIDTH:
606
680
  return True
607
- if not allow_multiline or not (expression.startswith("[") and expression.endswith("]")):
681
+ if not allow_multiline:
682
+ return False
683
+ if expression.startswith("{"):
684
+ comprehension = ast.parse(expression, mode="eval").body
685
+ if not isinstance(comprehension, ast.DictComp) or not isinstance(comprehension.value, ast.Call):
686
+ return False
687
+ call = comprehension.value
688
+ generator = comprehension.generators[0]
689
+ lines = (
690
+ f"{ast.unparse(comprehension.key)}: {ast.unparse(call.func)}(",
691
+ *(f" {ast.unparse(argument)}," for argument in call.args),
692
+ *(f" {keyword.arg}={ast.unparse(keyword.value)}," for keyword in call.keywords),
693
+ f"for {ast.unparse(generator.target)} in {ast.unparse(generator.iter)}",
694
+ )
695
+ return init.col_offset + len(prefix) + 1 <= _MAX_REPLACEMENT_WIDTH and all(
696
+ init.col_offset + 4 + len(line) <= _MAX_REPLACEMENT_WIDTH for line in lines
697
+ )
698
+ if not (expression.startswith("[") and expression.endswith("]")):
608
699
  return False
609
700
  content = expression[1:-1]
610
701
  return (