sarj-python-lint 1.4.0__tar.gz → 1.6.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 (156) hide show
  1. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-1.6.0/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +373 -0
  5. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/stepdown.py +469 -120
  6. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/.gitignore +0 -0
  7. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/LICENSE +0 -0
  8. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/README.md +0 -0
  9. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  29. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  30. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  31. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  35. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  36. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  37. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  38. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  39. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  40. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  41. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  42. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  43. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  44. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  45. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  46. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  47. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  48. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  49. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  50. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  51. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  52. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  53. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  54. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  55. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  56. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  57. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  58. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  59. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  60. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  61. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  62. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  63. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  64. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  65. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  66. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  67. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  68. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  69. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  70. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  71. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  72. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  73. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  74. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  75. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  76. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  77. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  78. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  79. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  80. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  81. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  82. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  83. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  84. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  85. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  86. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  87. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  88. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  89. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  90. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  91. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  92. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  93. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  94. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  95. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  96. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  97. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  98. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  99. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  100. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  101. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  102. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  103. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  104. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  105. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  106. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  107. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  108. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  109. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  110. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  111. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  112. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  113. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  114. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  115. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  116. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  117. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  118. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  119. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  120. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  121. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  122. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  123. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  124. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  125. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  126. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  127. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  128. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  129. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  130. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  131. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  132. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  133. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  134. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  135. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  136. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  137. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  138. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  139. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  140. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  141. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  142. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  143. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  144. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  145. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  146. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  147. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  148. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  149. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  150. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  151. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  152. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  153. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  154. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  155. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  156. {sarj_python_lint-1.4.0 → sarj_python_lint-1.6.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.4.0
3
+ Version: 1.6.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.4.0"
3
+ version = "1.6.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" }]
@@ -149,6 +149,7 @@ from sarj_python_lint.rules.redundant_module_docstring import RedundantModuleDoc
149
149
  from sarj_python_lint.rules.repeated_kwarg_heavy_call_in_test import RepeatedKwargHeavyCallInTest
150
150
  from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCallCases
151
151
  from sarj_python_lint.rules.repeated_test_composition import RepeatedTestComposition
152
+ from sarj_python_lint.rules.require_explicit_psycopg_transaction import RequireExplicitPsycopgTransaction
152
153
  from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
153
154
  RequireKeywordOnlySwapProneParams,
154
155
  )
@@ -186,6 +187,7 @@ if TYPE_CHECKING:
186
187
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
187
188
  {
188
189
  NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
190
+ RequireExplicitPsycopgTransaction.id: RequireExplicitPsycopgTransaction,
189
191
  AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
190
192
  ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
191
193
  ExcessiveCommentary.id: ExcessiveCommentary,
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass, field
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, ClassVar, Literal, final, override
7
+
8
+ import sqlglot
9
+ from sqlglot import exp
10
+ from sqlglot.errors import SqlglotError
11
+
12
+ from sarj_python_lint.rule_base import (
13
+ AutofixPolicy,
14
+ Diagnostic,
15
+ ExampleFile,
16
+ ExampleOutcome,
17
+ Rule,
18
+ RuleCategory,
19
+ RuleDocumentation,
20
+ RuleExample,
21
+ Severity,
22
+ is_suppressed,
23
+ )
24
+ from sarj_python_lint.rules._imports import ImportIndex
25
+ from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
26
+ from sarj_python_lint.rules._sql import sql_string_value
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Iterator
31
+
32
+ from sarj_python_lint._file_context import PythonFileContext
33
+
34
+
35
+ type _Function = ast.FunctionDef | ast.AsyncFunctionDef
36
+ type _Kind = Literal["pool", "connection", "cursor"]
37
+ _MUTATIONS = (exp.Insert, exp.Update, exp.Delete, exp.Merge)
38
+ _BAD = 'from psycopg_pool import AsyncConnectionPool\nasync def save(pool: AsyncConnectionPool):\n async with pool.connection() as conn:\n await conn.execute("SELECT id FROM items FOR UPDATE")\n await conn.execute("UPDATE items SET n = 1")\n'
39
+ _GOOD = _BAD.replace("as conn:", "as conn, conn.transaction():")
40
+
41
+
42
+ @final
43
+ class RequireExplicitPsycopgTransaction(Rule):
44
+ id = "require-explicit-psycopg-transaction"
45
+ code = "SARJ462"
46
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
47
+ default_level=Severity.WARNING,
48
+ summary="Make Psycopg locking reads and related writes explicitly transactional.",
49
+ rationale="Pool checkout does not start a transaction in autocommit mode. Locks can expire before use and earlier writes can survive a later failure.",
50
+ remediation="Enclose the complete atomic operation in the same connection's transaction() context, or use one atomic SQL statement. Document intentionally independent writes with an exact suppression.",
51
+ category=RuleCategory.CORRECTNESS,
52
+ autofix=AutofixPolicy.NONE,
53
+ limitations=(
54
+ "Reports import-proven Psycopg connections and cursors from typed parameters or constructor-injected pools. Dynamic factories, interprocedural SQL, and untyped ownership are not inferred.",
55
+ "Inspects literal SQL, simple local/module constants, and psycopg.sql.SQL wrappers. Dynamic SQL and unsupported SQL syntax are excluded.",
56
+ "A locking SELECT followed by a write on the same connection outside transaction() is reported; a single statement containing a mutation is treated as atomic. Multiple writes on a reachable local path are a warning because independent writes can be intentional.",
57
+ "Loops, exception handlers, match statements, comprehensions, and nested closures are not analyzed as execution paths. Manual BEGIN/commit protocols are not inferred. Tests, support fixtures, and generated files are excluded.",
58
+ "The rule cannot prove isolation level, database constraints, or application-level atomicity; fault-injection integration tests remain necessary.",
59
+ ),
60
+ examples=(
61
+ RuleExample(
62
+ example_id="unprotected-lock",
63
+ title="A pool checkout alone does not retain a lock",
64
+ outcome=ExampleOutcome.MATCH,
65
+ files=(ExampleFile.python("repository.py", _BAD),),
66
+ focus_path=PurePosixPath("repository.py"),
67
+ expected_count=1,
68
+ public=True,
69
+ ),
70
+ RuleExample(
71
+ example_id="explicit-lock-transaction",
72
+ title="Retain locks for the atomic operation",
73
+ outcome=ExampleOutcome.NO_MATCH,
74
+ files=(ExampleFile.python("repository.py", _GOOD),),
75
+ focus_path=PurePosixPath("repository.py"),
76
+ expected_count=0,
77
+ public=True,
78
+ ),
79
+ ),
80
+ )
81
+ description = documentation.summary
82
+
83
+ @override
84
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
85
+ if (
86
+ "execute" not in context.source
87
+ or context.generated
88
+ or is_test_path(context.path)
89
+ or is_test_support_path(context.path)
90
+ ):
91
+ return []
92
+ tree = context.tree
93
+ if tree is None:
94
+ return []
95
+ imports = ImportIndex.from_tree(tree, module_scope_only=True)
96
+ constants = _constants(tree.body)
97
+ findings: dict[int, ast.Call] = {}
98
+ for function, attributes in _functions(tree.body, imports):
99
+ bindings = attributes | _parameters(function, imports)
100
+ parameter_names = {
101
+ arg.arg for arg in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
102
+ }
103
+ analysis = _Analysis(
104
+ imports, {name: value for name, value in constants.items() if name not in parameter_names}, findings
105
+ )
106
+ analysis.block(function.body, _State(bindings=bindings))
107
+ return [
108
+ Diagnostic(
109
+ path=context.path,
110
+ line=node.lineno,
111
+ col=node.col_offset + 1,
112
+ code=self.code,
113
+ severity=Severity.WARNING,
114
+ message="Locking read or multiple writes without a shared explicit transaction; wrap the atomic operation in this connection's transaction() context.",
115
+ )
116
+ for node in sorted(findings.values(), key=lambda item: (item.lineno, item.col_offset))
117
+ if not is_suppressed(context.source_lines, node.lineno, self.code)
118
+ ]
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class _Binding:
123
+ kind: _Kind
124
+ connection: int
125
+
126
+
127
+ @dataclass
128
+ class _State:
129
+ bindings: dict[str, _Binding] = field(default_factory=dict)
130
+ writes: set[int] = field(default_factory=set)
131
+ locks: dict[int, ast.Call] = field(default_factory=dict)
132
+ transactions: set[int] = field(default_factory=set)
133
+ constants: dict[str, ast.expr] = field(default_factory=dict)
134
+
135
+ def copy(self) -> _State:
136
+ return _State(
137
+ bindings=self.bindings.copy(),
138
+ writes=self.writes.copy(),
139
+ locks=self.locks.copy(),
140
+ transactions=self.transactions.copy(),
141
+ constants=self.constants.copy(),
142
+ )
143
+
144
+
145
+ def _key(node: ast.expr) -> str:
146
+ return ast.dump(node, include_attributes=False)
147
+
148
+
149
+ def _parameters(function: _Function, imports: ImportIndex) -> dict[str, _Binding]:
150
+ return {
151
+ _key(ast.Name(id=arg.arg, ctx=ast.Load())): _Binding(kind, id(arg))
152
+ for arg in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
153
+ if (kind := _annotation(arg.annotation, imports)) is not None
154
+ }
155
+
156
+
157
+ def _annotation(node: ast.expr | None, imports: ImportIndex) -> _Kind | None:
158
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
159
+ try:
160
+ node = ast.parse(node.value, mode="eval").body
161
+ except SyntaxError:
162
+ return None
163
+ if isinstance(node, ast.Subscript):
164
+ node = node.value
165
+ if node is None:
166
+ return None
167
+ if imports.resolved_symbol(node, sources=frozenset({"psycopg_pool"})) in {"AsyncConnectionPool", "ConnectionPool"}:
168
+ return "pool"
169
+ if imports.resolved_symbol(node, sources=frozenset({"psycopg"})) in {"AsyncConnection", "Connection"}:
170
+ return "connection"
171
+ return None
172
+
173
+
174
+ def _functions(statements: list[ast.stmt], imports: ImportIndex) -> Iterator[tuple[_Function, dict[str, _Binding]]]:
175
+ for statement in statements:
176
+ if isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef):
177
+ yield statement, {}
178
+ elif isinstance(statement, ast.ClassDef):
179
+ attributes = _class_bindings(statement, imports)
180
+ for method in statement.body:
181
+ if isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef):
182
+ yield method, attributes
183
+
184
+
185
+ def _class_bindings(owner: ast.ClassDef, imports: ImportIndex) -> dict[str, _Binding]:
186
+ constructor = next(
187
+ (method for method in owner.body if isinstance(method, ast.FunctionDef) and method.name == "__init__"), None
188
+ )
189
+ if constructor is None:
190
+ return {}
191
+ parameters = _parameters(constructor, imports)
192
+ attributes: dict[str, _Binding] = {}
193
+ for assignment in constructor.body:
194
+ if not isinstance(assignment, ast.Assign):
195
+ continue
196
+ binding = parameters.get(_key(assignment.value))
197
+ if binding is None:
198
+ continue
199
+ for target in assignment.targets:
200
+ if isinstance(target, ast.Attribute):
201
+ attributes[_load_key(target)] = binding
202
+ return attributes
203
+
204
+
205
+ def _load_key(node: ast.expr) -> str:
206
+ if isinstance(node, ast.Name):
207
+ return _key(ast.Name(id=node.id, ctx=ast.Load()))
208
+ if isinstance(node, ast.Attribute):
209
+ return _key(ast.Attribute(value=node.value, attr=node.attr, ctx=ast.Load()))
210
+ return _key(node)
211
+
212
+
213
+ def _constants(statements: list[ast.stmt]) -> dict[str, ast.expr]:
214
+ values: dict[str, ast.expr] = {}
215
+ for statement in statements:
216
+ if isinstance(statement, ast.Assign):
217
+ for target in statement.targets:
218
+ if isinstance(target, ast.Name):
219
+ values[target.id] = statement.value
220
+ elif isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name) and statement.value:
221
+ values[statement.target.id] = statement.value
222
+ return values
223
+
224
+
225
+ @dataclass
226
+ class _Analysis:
227
+ imports: ImportIndex
228
+ constants: dict[str, ast.expr]
229
+ findings: dict[int, ast.Call]
230
+
231
+ def block(self, statements: list[ast.stmt], state: _State) -> _State | None:
232
+ for statement in statements:
233
+ match statement:
234
+ case ast.With() | ast.AsyncWith():
235
+ result = self.with_block(statement, state)
236
+ case ast.If():
237
+ result = self.branches(statement, state)
238
+ case ast.Return() | ast.Raise():
239
+ self.executions(statement, state)
240
+ return None
241
+ case ast.Expr() | ast.Assign() | ast.AnnAssign():
242
+ self.simple(statement, state)
243
+ result = state
244
+ case ast.Pass():
245
+ result = state
246
+ case _:
247
+ # Unsupported control flow can rebind names or terminate; do not guess across it.
248
+ result = _State(transactions=state.transactions.copy())
249
+ if result is None:
250
+ return None
251
+ state = result
252
+ return state
253
+
254
+ def with_block(self, statement: ast.With | ast.AsyncWith, state: _State) -> _State | None:
255
+ prior_transactions = state.transactions.copy()
256
+ for item in statement.items:
257
+ call = item.context_expr
258
+ if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
259
+ binding = state.bindings.get(_key(call.func.value))
260
+ if binding and call.func.attr == "transaction" and binding.kind == "connection":
261
+ state.transactions.add(binding.connection)
262
+ if item.optional_vars:
263
+ self.bind(item.optional_vars, call, state)
264
+ result = self.block(statement.body, state)
265
+ if result is not None:
266
+ result.transactions = prior_transactions
267
+ return result
268
+
269
+ def branches(self, statement: ast.If, state: _State) -> _State | None:
270
+ if isinstance(statement.test, ast.Constant):
271
+ return self.block(statement.body if statement.test.value else statement.orelse, state)
272
+ left = self.block(statement.body, state.copy())
273
+ right = self.block(statement.orelse, state.copy())
274
+ if left is None:
275
+ return right
276
+ if right is None:
277
+ return left
278
+ return _State(
279
+ bindings={key: value for key, value in left.bindings.items() if right.bindings.get(key) == value},
280
+ writes=left.writes | right.writes,
281
+ locks=left.locks | right.locks,
282
+ transactions=left.transactions & right.transactions,
283
+ constants={key: value for key, value in left.constants.items() if right.constants.get(key) is value},
284
+ )
285
+
286
+ def simple(self, statement: ast.stmt, state: _State) -> None:
287
+ self.executions(statement, state)
288
+ if isinstance(statement, ast.Assign):
289
+ for target in statement.targets:
290
+ self.bind(target, statement.value, state)
291
+ elif isinstance(statement, ast.AnnAssign) and statement.value:
292
+ self.bind(statement.target, statement.value, state)
293
+
294
+ def bind(self, target: ast.expr, value: ast.expr, state: _State) -> None:
295
+ binding = state.bindings.get(_key(value))
296
+ if isinstance(value, ast.Call) and isinstance(value.func, ast.Attribute):
297
+ parent = state.bindings.get(_key(value.func.value))
298
+ if parent and parent.kind == "pool" and value.func.attr == "connection":
299
+ binding = _Binding("connection", id(value))
300
+ elif parent and parent.kind == "connection" and value.func.attr == "cursor":
301
+ binding = _Binding("cursor", parent.connection)
302
+ key = _load_key(target)
303
+ state.bindings.pop(key, None)
304
+ if binding:
305
+ state.bindings[key] = binding
306
+ if isinstance(target, ast.Name):
307
+ state.constants[target.id] = value
308
+
309
+ def executions(self, statement: ast.stmt, state: _State) -> None:
310
+ for node in _eager_nodes(statement):
311
+ if (
312
+ not isinstance(node, ast.Call)
313
+ or not isinstance(node.func, ast.Attribute)
314
+ or node.func.attr not in {"execute", "executemany"}
315
+ ):
316
+ continue
317
+ binding = state.bindings.get(_key(node.func.value))
318
+ if not binding or binding.kind == "pool" or binding.connection in state.transactions or not node.args:
319
+ continue
320
+ sql = self.sql(node.args[0], state, frozenset())
321
+ if sql is None:
322
+ continue
323
+ effects = _sql_effects(sql)
324
+ if effects.mutation and (binding.connection in state.writes or binding.connection in state.locks):
325
+ self.findings.setdefault(binding.connection, state.locks.get(binding.connection, node))
326
+ if effects.locking and not effects.mutation:
327
+ state.locks.setdefault(binding.connection, node)
328
+ if effects.mutation:
329
+ state.writes.add(binding.connection)
330
+
331
+ def sql(self, node: ast.expr, state: _State, seen: frozenset[str]) -> str | None:
332
+ if isinstance(node, ast.Name):
333
+ if node.id in seen:
334
+ return None
335
+ value = state.constants.get(node.id, self.constants.get(node.id))
336
+ return self.sql(value, state, seen | {node.id}) if value else None
337
+ if isinstance(node, ast.Call):
338
+ if self.imports.resolves(node.func, sources=frozenset({"psycopg.sql"}), symbol="SQL") and node.args:
339
+ return self.sql(node.args[0], state, seen)
340
+ if isinstance(node.func, ast.Attribute) and node.func.attr == "format":
341
+ return self.sql(node.func.value, state, seen)
342
+ return None
343
+ return sql_string_value(node)
344
+
345
+
346
+ def _eager_nodes(node: ast.AST) -> Iterator[ast.AST]:
347
+ if isinstance(
348
+ node, ast.Lambda | ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp | ast.IfExp | ast.BoolOp
349
+ ):
350
+ return
351
+ yield node
352
+ for child in ast.iter_child_nodes(node):
353
+ yield from _eager_nodes(child)
354
+
355
+
356
+ @dataclass(frozen=True)
357
+ class _SqlEffects:
358
+ mutation: bool
359
+ locking: bool
360
+
361
+
362
+ def _sql_effects(sql: str) -> _SqlEffects:
363
+ try:
364
+ queries = sqlglot.parse(sql, read="postgres")
365
+ except SqlglotError:
366
+ return _SqlEffects(mutation=False, locking=False)
367
+ mutation = False
368
+ locking = False
369
+ for query in queries:
370
+ if query is not None:
371
+ mutation = mutation or query.find(*_MUTATIONS) is not None
372
+ locking = locking or query.find(exp.Lock) is not None
373
+ return _SqlEffects(mutation=mutation, locking=locking)