sarj-python-lint 0.80.5__tar.gz → 0.80.6__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 (143) hide show
  1. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/PKG-INFO +2 -1
  2. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/pyproject.toml +2 -2
  3. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/__main__.py +3 -5
  4. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_fastapi.py +12 -0
  5. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_imports.py +6 -0
  6. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_prose_budget.py +121 -11
  7. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_registry.py +8 -14
  8. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_sql.py +9 -4
  9. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +505 -0
  10. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +144 -54
  11. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/docstring_args_restate_signature.py +215 -0
  12. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +326 -0
  13. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +64 -13
  14. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +99 -21
  15. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +14 -6
  16. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +219 -14
  17. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/mock_without_spec.py +255 -35
  18. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +103 -42
  19. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +187 -0
  20. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +33 -21
  21. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +270 -0
  22. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_offset_pagination.py +167 -0
  23. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +124 -49
  24. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +83 -41
  25. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +41 -50
  26. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_restated_comment.py +56 -18
  27. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_secret_in_log.py +183 -0
  28. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_select_star.py +258 -0
  29. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +206 -193
  30. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_tautological_expect.py +218 -0
  31. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_typed_doc_sections.py +134 -0
  32. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_unique_violation_message_match.py +399 -0
  33. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +202 -0
  34. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +181 -54
  35. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +273 -0
  36. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/source_coupled_test.py +27 -11
  37. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +103 -22
  38. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +68 -15
  39. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -140
  40. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -201
  41. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -212
  42. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -299
  43. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_long_comment.py +0 -114
  44. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -171
  45. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_offset_pagination.py +0 -118
  46. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -202
  47. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -155
  48. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_secret_in_log.py +0 -171
  49. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_select_star.py +0 -138
  50. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -580
  51. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -167
  52. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_tautological_expect.py +0 -279
  53. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -86
  54. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -170
  55. sarj_python_lint-0.80.5/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -206
  56. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/.gitignore +0 -0
  57. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/LICENSE +0 -0
  58. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/README.md +0 -0
  59. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/__init__.py +0 -0
  60. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_analysis_session.py +0 -0
  61. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_filesystem.py +0 -0
  62. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  63. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_secret_names.py +0 -0
  64. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_version.py +0 -0
  65. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/py.typed +0 -0
  66. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/ratchet.py +0 -0
  67. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rule_base.py +0 -0
  68. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/__init__.py +0 -0
  69. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  70. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_comments.py +0 -0
  71. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  72. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
  73. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_logging.py +0 -0
  74. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_paths.py +0 -0
  75. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
  76. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
  77. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  78. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  79. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  80. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  81. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  82. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  83. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  84. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  85. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  86. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  87. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  88. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  89. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  90. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  91. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  92. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  93. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  94. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  95. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  96. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  97. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  98. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  99. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  100. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  101. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  102. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  103. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  104. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  105. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  106. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  107. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  108. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  109. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
  110. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  111. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  112. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  113. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  114. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  115. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  116. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  117. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  118. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  119. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  120. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  121. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  122. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  123. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  124. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  125. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  126. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  127. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  128. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  129. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  130. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  131. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  132. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  133. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  134. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  135. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  136. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  137. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
  138. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  139. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  140. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  141. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  142. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  143. {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.80.5
3
+ Version: 0.80.6
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/
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: ruff>=0.16.5
21
+ Requires-Dist: sqlglot==30.18.0
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  <!-- Generated by `code-standards maintain docs sync`; do not edit. -->
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.80.5"
3
+ version = "0.80.6"
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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["ruff>=0.16.5"]
18
+ dependencies = ["ruff>=0.16.5", "sqlglot==30.18.0"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -125,14 +125,12 @@ def analyze(
125
125
 
126
126
  _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
127
127
  {
128
- "SARJ003": frozenset({"SARJ080"}),
129
128
  "SARJ050": frozenset({"SARJ420"}),
130
- "SARJ084": frozenset({"SARJ050", "SARJ091", "SARJ420"}),
129
+ "SARJ084": frozenset({"SARJ050", "SARJ420"}),
131
130
  "SARJ085": frozenset({"SARJ420"}),
132
131
  "SARJ086": frozenset({"SARJ420"}),
133
132
  "SARJ087": frozenset({"SARJ420"}),
134
- "SARJ088": frozenset({"SARJ050", "SARJ085", "SARJ091", "SARJ420"}),
135
- "SARJ091": frozenset({"SARJ420"}),
133
+ "SARJ088": frozenset({"SARJ050", "SARJ085", "SARJ420"}),
136
134
  "SARJ092": frozenset({"SARJ086", "SARJ087", "SARJ420"}),
137
135
  "SARJ093": frozenset({"SARJ034"}),
138
136
  "SARJ099": frozenset({"SARJ420"}),
@@ -140,7 +138,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
140
138
  )
141
139
 
142
140
  _DOCSTRING_PRECEDENCE_CODES = frozenset(
143
- {"SARJ050", "SARJ084", "SARJ085", "SARJ086", "SARJ087", "SARJ088", "SARJ091", "SARJ092", "SARJ099"}
141
+ {"SARJ050", "SARJ084", "SARJ085", "SARJ086", "SARJ087", "SARJ088", "SARJ092", "SARJ099"}
144
142
  )
145
143
 
146
144
 
@@ -41,6 +41,8 @@ class Route:
41
41
  path: str | None
42
42
  receiver_kind: Literal["FastAPI", "APIRouter"]
43
43
  inherited_hidden: bool = False
44
+ inherited_responses: ast.expr | None = None
45
+ inherited_response_class: ast.expr | None = None
44
46
 
45
47
  @property
46
48
  def keywords(self) -> dict[str, ast.expr]:
@@ -119,6 +121,7 @@ class FastapiIndex:
119
121
  self.receiver_origins: dict[_ReceiverKey, _ReceiverKey] = {}
120
122
  self.receiver_kinds: dict[_ReceiverKey, Literal["FastAPI", "APIRouter"]] = {}
121
123
  self.hidden_receivers: set[_ReceiverKey] = set()
124
+ self.receiver_defaults: dict[_ReceiverKey, dict[str, ast.expr]] = {}
122
125
  self.decorators: dict[_ReceiverKey, _DecoratorBinding] = {}
123
126
  self.bound_names: set[_ReceiverKey] = set()
124
127
  self._node_scopes: dict[int, int] = {}
@@ -243,6 +246,12 @@ class FastapiIndex:
243
246
  self.receivers.add(key)
244
247
  self.receiver_origins[key] = key
245
248
  self.receiver_kinds[key] = constructor_kind
249
+ if isinstance(value, ast.Call):
250
+ self.receiver_defaults[key] = {
251
+ keyword.arg: keyword.value
252
+ for keyword in value.keywords
253
+ if keyword.arg in {"responses", "default_response_class"}
254
+ }
246
255
  if self._constructor_is_hidden(value):
247
256
  self.hidden_receivers.add(key)
248
257
  changed = True
@@ -346,6 +355,7 @@ class FastapiIndex:
346
355
  path = path_node.value if isinstance(path_node, ast.Constant) and isinstance(path_node.value, str) else None
347
356
  route_methods = self._route_methods(decorator) if method == "api_route" else (method,)
348
357
  origin = self.receiver_origins[receiver_key]
358
+ defaults = self.receiver_defaults.get(origin, {})
349
359
  routes.extend(
350
360
  Route(
351
361
  decorator=decorator,
@@ -354,6 +364,8 @@ class FastapiIndex:
354
364
  path=path,
355
365
  receiver_kind=self.receiver_kinds[receiver_key],
356
366
  inherited_hidden=receiver_key in self.hidden_receivers,
367
+ inherited_responses=defaults.get("responses"),
368
+ inherited_response_class=defaults.get("default_response_class"),
357
369
  )
358
370
  for route_method in route_methods
359
371
  )
@@ -49,6 +49,12 @@ class ImportIndex:
49
49
  resolved = self._resolve(node)
50
50
  return resolved is not None and resolved.module in sources and resolved.symbol == symbol
51
51
 
52
+ def resolved_symbol(self, node: ast.expr, *, sources: frozenset[str]) -> str | None:
53
+ resolved = self._resolve(node)
54
+ if resolved is None or resolved.module not in sources:
55
+ return None
56
+ return resolved.symbol
57
+
52
58
  def builtin_is_unshadowed(self, name: str) -> bool:
53
59
  return name not in self.shadowed_names and name not in self.bindings
54
60
 
@@ -198,7 +198,7 @@ def _docstring_groups(tree: ast.Module) -> list[ProseGroup]:
198
198
  ):
199
199
  continue
200
200
  if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) and (
201
- decorator_markers(node) & PROMPT_DECORATOR_MARKERS
201
+ _decorator_terminal_names(node) & PROMPT_DECORATOR_MARKERS
202
202
  ):
203
203
  continue
204
204
  if isinstance(node, ast.ClassDef) and _is_schema_class(node):
@@ -244,6 +244,17 @@ def _is_schema_class(node: ast.ClassDef) -> bool:
244
244
  return bool(bases & _SCHEMA_BASES or decorator_markers(node) & _SCHEMA_DECORATORS)
245
245
 
246
246
 
247
+ def _decorator_terminal_names(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> frozenset[str]:
248
+ names: set[str] = set()
249
+ for decorator in node.decorator_list:
250
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
251
+ if isinstance(target, ast.Name):
252
+ names.add(target.id.casefold())
253
+ elif isinstance(target, ast.Attribute):
254
+ names.add(target.attr.casefold())
255
+ return frozenset(names)
256
+
257
+
247
258
  def _fully_typed(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
248
259
  args = node.args
249
260
  parameters = (*args.posonlyargs, *args.args, *args.kwonlyargs)
@@ -270,21 +281,23 @@ def _typed_restatement_lines(
270
281
  docstring_line: int,
271
282
  ) -> tuple[int, ...]:
272
283
  annotations = {
273
- arg.arg: _normalise_type(ast.unparse(arg.annotation))
284
+ arg.arg: _annotation_type(arg.annotation)
274
285
  for arg in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs)
275
286
  if arg.annotation is not None
276
287
  }
277
288
  if node.args.vararg is not None and node.args.vararg.annotation is not None:
278
- annotations[f"*{node.args.vararg.arg}"] = _normalise_type(ast.unparse(node.args.vararg.annotation))
289
+ annotations[f"*{node.args.vararg.arg}"] = _annotation_type(node.args.vararg.annotation)
279
290
  if node.args.kwarg is not None and node.args.kwarg.annotation is not None:
280
- annotations[f"**{node.args.kwarg.arg}"] = _normalise_type(ast.unparse(node.args.kwarg.annotation))
281
- return_type = _normalise_type(ast.unparse(node.returns)) if node.returns is not None else ""
282
- body_line_counts = _section_body_line_counts(doc)
291
+ annotations[f"**{node.args.kwarg.arg}"] = _annotation_type(node.args.kwarg.annotation)
292
+ return_type = _annotation_type(node.returns) if node.returns is not None else None
293
+ yield_type = _yielded_type(node.returns) if node.returns is not None else None
294
+ visible_doc = _without_literal_examples(doc)
295
+ body_line_counts = _section_body_line_counts(visible_doc)
283
296
 
284
297
  active: str | None = None
285
298
  active_header: int | None = None
286
299
  result: list[int] = []
287
- for index, raw in enumerate(doc.splitlines()):
300
+ for index, raw in enumerate(visible_doc.splitlines()):
288
301
  if heading := _SECTION_HEADING_RE.match(raw):
289
302
  active = heading.group(1)
290
303
  active_header = index
@@ -297,20 +310,63 @@ def _typed_restatement_lines(
297
310
  continue
298
311
  if active in {"Args", "Arguments", "Parameters", "Params", "Keyword Args", "Keyword Arguments"}:
299
312
  match = _ARG_TYPE_ENTRY_RE.match(raw) or _NUMPY_ARG_TYPE_ENTRY_RE.match(raw)
300
- if match is not None and _normalise_type(match.group(2)) == annotations.get(match.group(1)):
313
+ if match is not None and _documented_type(match.group(2)) == annotations.get(match.group(1)):
301
314
  result.append(docstring_line + index)
302
315
  elif match := _RETURN_TYPE_ENTRY_RE.match(raw):
303
- if _normalise_type(match.group(1)) == return_type:
316
+ expected = yield_type if active in {"Yields", "Yield"} else return_type
317
+ if expected is not None and _documented_type(match.group(1)) == expected:
304
318
  result.append(docstring_line + index)
305
319
  elif (
306
320
  active_header is not None
307
321
  and body_line_counts.get(active_header) == 1
308
- and _normalise_type(raw.strip()) == return_type
322
+ and (expected := yield_type if active in {"Yields", "Yield"} else return_type) is not None
323
+ and _documented_type(raw.strip()) == expected
309
324
  ):
310
325
  result.append(docstring_line + index)
311
326
  return tuple(result)
312
327
 
313
328
 
329
+ def _without_literal_examples(doc: str) -> str:
330
+ visible: list[str] = []
331
+ fence: str | None = None
332
+ literal_indent: int | None = None
333
+ awaiting_literal_block = False
334
+ for raw in doc.splitlines():
335
+ stripped = raw.lstrip()
336
+ indent = len(raw) - len(stripped)
337
+ fence_marker = stripped[:3] if stripped.startswith(("```", "~~~")) else None
338
+ if fence is not None:
339
+ visible.append("")
340
+ if fence_marker == fence:
341
+ fence = None
342
+ continue
343
+ if fence_marker is not None:
344
+ fence = fence_marker
345
+ visible.append("")
346
+ continue
347
+ if stripped.startswith((">>>", "...")):
348
+ visible.append("")
349
+ continue
350
+ if literal_indent is not None:
351
+ if not stripped or indent > literal_indent:
352
+ visible.append("")
353
+ continue
354
+ literal_indent = None
355
+ if awaiting_literal_block:
356
+ if not stripped:
357
+ visible.append(raw)
358
+ continue
359
+ if indent > 0:
360
+ literal_indent = indent - 1
361
+ visible.append("")
362
+ awaiting_literal_block = False
363
+ continue
364
+ awaiting_literal_block = False
365
+ visible.append(raw)
366
+ awaiting_literal_block = stripped.endswith("::")
367
+ return "\n".join(visible)
368
+
369
+
314
370
  def _section_body_line_counts(doc: str) -> dict[int, int]:
315
371
  counts: dict[int, int] = {}
316
372
  active_header: int | None = None
@@ -325,5 +381,59 @@ def _section_body_line_counts(doc: str) -> dict[int, int]:
325
381
  return counts
326
382
 
327
383
 
384
+ def _annotation_type(node: ast.expr) -> str:
385
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
386
+ parsed = _parse_type_expression(node.value)
387
+ return _normalise_type(ast.unparse(parsed)) if parsed is not None else node.value
388
+ return _normalise_type(ast.unparse(node))
389
+
390
+
391
+ def _documented_type(value: str) -> str | None:
392
+ parsed = _parse_type_expression(value)
393
+ return None if parsed is None else _normalise_type(ast.unparse(parsed))
394
+
395
+
396
+ def _parse_type_expression(value: str) -> ast.expr | None:
397
+ try:
398
+ parsed = ast.parse(value.strip(), mode="eval").body
399
+ except SyntaxError:
400
+ return None
401
+ unsafe = (
402
+ ast.Await,
403
+ ast.BoolOp,
404
+ ast.Call,
405
+ ast.Compare,
406
+ ast.DictComp,
407
+ ast.GeneratorExp,
408
+ ast.IfExp,
409
+ ast.Lambda,
410
+ ast.ListComp,
411
+ ast.NamedExpr,
412
+ ast.SetComp,
413
+ ast.Yield,
414
+ ast.YieldFrom,
415
+ )
416
+ if any(isinstance(part, unsafe) for part in ast.walk(parsed)):
417
+ return None
418
+ if any(isinstance(part, ast.BinOp) and not isinstance(part.op, ast.BitOr) for part in ast.walk(parsed)):
419
+ return None
420
+ return parsed
421
+
422
+
423
+ def _yielded_type(node: ast.expr) -> str | None:
424
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
425
+ parsed = _parse_type_expression(node.value)
426
+ if parsed is None:
427
+ return None
428
+ node = parsed
429
+ if not isinstance(node, ast.Subscript):
430
+ return None
431
+ name = node.value.attr if isinstance(node.value, ast.Attribute) else node.value.id if isinstance(node.value, ast.Name) else ""
432
+ if name not in {"AsyncGenerator", "AsyncIterable", "AsyncIterator", "Generator", "Iterable", "Iterator"}:
433
+ return None
434
+ members = node.slice.elts if isinstance(node.slice, ast.Tuple) else (node.slice,)
435
+ return _annotation_type(members[0]) if members else None
436
+
437
+
328
438
  def _normalise_type(value: str) -> str:
329
- return re.sub(r"\s+", "", value).removeprefix("typing.").removesuffix(",optional").casefold()
439
+ return re.sub(r"\s+", "", value).removeprefix("typing.").casefold()
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  from types import MappingProxyType
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from sarj_python_lint.rules.complex_postgres_query_requires_architecture_review import (
7
+ ComplexPostgresQueryRequiresArchitectureReview,
8
+ )
6
9
  from sarj_python_lint.rules.defect_xfail_requires_explicit_strict import DefectXfailRequiresExplicitStrict
7
10
  from sarj_python_lint.rules.docstring_args_restate_signature import (
8
11
  DocstringArgsRestateSignature,
@@ -45,14 +48,8 @@ from sarj_python_lint.rules.no_generic_single_export_module import NoGenericSing
45
48
  from sarj_python_lint.rules.no_hidden_constructor_fallback import (
46
49
  NoHiddenConstructorFallback,
47
50
  )
48
- from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
49
- from sarj_python_lint.rules.no_long_comment import NoLongComment
50
51
  from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
51
52
  from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
52
- from sarj_python_lint.rules.no_optional_tenant_predicate import (
53
- NoOptionalTenantPredicate,
54
- )
55
- from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
56
53
  from sarj_python_lint.rules.no_random_uuid_in_sql import NoRandomUuidInSql
57
54
  from sarj_python_lint.rules.no_raw_connection_in_tests import NoRawConnectionInTests
58
55
  from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
@@ -61,8 +58,6 @@ from sarj_python_lint.rules.no_repeated_test_body import NoRepeatedTestBody
61
58
  from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
62
59
  from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
63
60
  from sarj_python_lint.rules.no_select_star import NoSelectStar
64
- from sarj_python_lint.rules.no_sentinel_return_on_except import NoSentinelReturnOnExcept
65
- from sarj_python_lint.rules.no_stdlib_logging import NoStdlibLogging
66
61
  from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
67
62
  from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
68
63
  from sarj_python_lint.rules.no_typed_doc_sections import NoTypedDocSections
@@ -73,6 +68,9 @@ from sarj_python_lint.rules.no_unnecessary_docstring import NoUnnecessaryDocstri
73
68
  from sarj_python_lint.rules.no_vague_suppression_description import (
74
69
  NoVagueSuppressionDescription,
75
70
  )
71
+ from sarj_python_lint.rules.no_whole_request_response_payload_in_log import (
72
+ NoWholeRequestResponsePayloadInLog,
73
+ )
76
74
  from sarj_python_lint.rules.opaque_parametrize_case_needs_id import OpaqueParametrizeCaseNeedsId
77
75
  from sarj_python_lint.rules.over_mocked_test import OverMockedTest
78
76
  from sarj_python_lint.rules.phase_label_comment import TestPhaseLabelComment
@@ -165,6 +163,7 @@ if TYPE_CHECKING:
165
163
 
166
164
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
167
165
  {
166
+ ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
168
167
  ExcessiveCommentary.id: ExcessiveCommentary,
169
168
  PreferWalrusAwaitedNoneGuard.id: PreferWalrusAwaitedNoneGuard,
170
169
  TimestampOrderRequiresTiebreaker.id: TimestampOrderRequiresTiebreaker,
@@ -172,14 +171,12 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
172
171
  PreferClassRow.id: PreferClassRow,
173
172
  PreferCollectionComprehension.id: PreferCollectionComprehension,
174
173
  PreferStrEnum.id: PreferStrEnum,
175
- NoIsinstanceUnionChain.id: NoIsinstanceUnionChain,
176
174
  NoOffsetPagination.id: NoOffsetPagination,
177
175
  PreferNamedtupleOverTupleReturn.id: PreferNamedtupleOverTupleReturn,
178
176
  NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
179
177
  PydanticAtBoundaries.id: PydanticAtBoundaries,
180
178
  FastapiExplicitOpenapiContract.id: FastapiExplicitOpenapiContract,
181
179
  FakesInSharedLocation.id: FakesInSharedLocation,
182
- NoSentinelReturnOnExcept.id: NoSentinelReturnOnExcept,
183
180
  PreferConstantTimeSecretCompare.id: PreferConstantTimeSecretCompare,
184
181
  NoSecretInLog.id: NoSecretInLog,
185
182
  PreferTimedeltaForDurations.id: PreferTimedeltaForDurations,
@@ -189,7 +186,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
189
186
  StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
190
187
  SourceCoupledTest.id: SourceCoupledTest,
191
188
  IacSourceCoupledTest.id: IacSourceCoupledTest,
192
- NoQueryWithManyJoins.id: NoQueryWithManyJoins,
193
189
  NoRawConnectionInTests.id: NoRawConnectionInTests,
194
190
  NoAnalyticalAggregationInPostgresStore.id: NoAnalyticalAggregationInPostgresStore,
195
191
  NoSelectStar.id: NoSelectStar,
@@ -212,12 +208,10 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
212
208
  NoRestatedComment.id: NoRestatedComment,
213
209
  RedundantDocstring.id: RedundantDocstring,
214
210
  TrailingValueNarration.id: TrailingValueNarration,
215
- NoStdlibLogging.id: NoStdlibLogging,
216
211
  NoRandomUuidInSql.id: NoRandomUuidInSql,
217
212
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
218
213
  NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
219
214
  NoFastapiOnEvent.id: NoFastapiOnEvent,
220
- NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
221
215
  NoTautologicalExpect.id: NoTautologicalExpect,
222
216
  PreferLibraryFake.id: PreferLibraryFake,
223
217
  OverMockedTest.id: OverMockedTest,
@@ -245,7 +239,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
245
239
  DocstringReturnsRestateSignature.id: DocstringReturnsRestateSignature,
246
240
  RestatedTestDocstring.id: RestatedTestDocstring,
247
241
  TestPhaseLabelComment.id: TestPhaseLabelComment,
248
- NoLongComment.id: NoLongComment,
249
242
  NoNestedPydanticFieldValidator.id: NoNestedPydanticFieldValidator,
250
243
  NoRedundantLiteralDescription.id: NoRedundantLiteralDescription,
251
244
  NoTypedDocSections.id: NoTypedDocSections,
@@ -263,6 +256,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
263
256
  RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
264
257
  RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
265
258
  NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
259
+ NoWholeRequestResponsePayloadInLog.id: NoWholeRequestResponsePayloadInLog,
266
260
  TypedErrorReasons.id: TypedErrorReasons,
267
261
  }
268
262
  )
@@ -16,12 +16,12 @@ def is_store_module(path: Path) -> bool:
16
16
  return path.name == "store.py" or path.name.endswith("_store.py") or "stores" in path.parts
17
17
 
18
18
 
19
- def sql_string_value(node: ast.expr) -> str | None:
19
+ def sql_string_value(node: ast.expr, *, interpolation_placeholder: str = " ") -> str | None:
20
20
  if isinstance(node, ast.Constant) and isinstance(node.value, str):
21
21
  return node.value
22
22
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
23
- left = sql_string_value(node.left)
24
- right = sql_string_value(node.right)
23
+ left = sql_string_value(node.left, interpolation_placeholder=interpolation_placeholder)
24
+ right = sql_string_value(node.right, interpolation_placeholder=interpolation_placeholder)
25
25
  if left is not None and right is not None:
26
26
  return left + right
27
27
  if isinstance(node, ast.JoinedStr):
@@ -32,7 +32,7 @@ def sql_string_value(node: ast.expr) -> str | None:
32
32
  elif isinstance(value, ast.FormattedValue):
33
33
  # Keep static SQL tokens on either side adjacent while ensuring
34
34
  # an interpolation can never manufacture SELECT/FROM/* itself.
35
- pieces.append(" ")
35
+ pieces.append(interpolation_placeholder)
36
36
  else: # pragma: no cover - the parser emits only these two forms
37
37
  return None
38
38
  return "".join(pieces)
@@ -67,6 +67,11 @@ def strip_sql_noise(text: str, *, mask_dollar_quotes: bool = True) -> str:
67
67
  out[i] = " "
68
68
  i += 1
69
69
  continue
70
+ if ch == "#" and (i == 0 or text[i - 1].isspace()):
71
+ while i < n and text[i] != "\n":
72
+ out[i] = " "
73
+ i += 1
74
+ continue
70
75
  if ch == "/" and i + 1 < n and text[i + 1] == "*":
71
76
  out[i] = out[i + 1] = " "
72
77
  i += 2