sarj-python-lint 0.80.4__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.4 → sarj_python_lint-0.80.6}/PKG-INFO +2 -1
  2. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/pyproject.toml +2 -2
  3. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/__main__.py +3 -5
  4. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_fastapi.py +12 -0
  5. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_imports.py +6 -0
  6. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_prose_budget.py +121 -11
  7. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_registry.py +12 -14
  8. {sarj_python_lint-0.80.4 → 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.4 → 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.6/src/sarj_python_lint/rules/excessive_commentary.py +218 -0
  14. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +64 -13
  15. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +99 -21
  16. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +14 -6
  17. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +219 -14
  18. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/mock_without_spec.py +255 -35
  19. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +103 -42
  20. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +187 -0
  21. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +33 -21
  22. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +270 -0
  23. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_offset_pagination.py +167 -0
  24. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +124 -49
  25. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +83 -41
  26. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +41 -50
  27. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_restated_comment.py +56 -18
  28. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_secret_in_log.py +183 -0
  29. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_select_star.py +258 -0
  30. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +206 -193
  31. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_tautological_expect.py +218 -0
  32. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_typed_doc_sections.py +134 -0
  33. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_unique_violation_message_match.py +399 -0
  34. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +202 -0
  35. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +181 -54
  36. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +273 -0
  37. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/source_coupled_test.py +27 -11
  38. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +103 -22
  39. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +68 -15
  40. sarj_python_lint-0.80.6/src/sarj_python_lint/rules/typed_error_reasons.py +228 -0
  41. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -140
  42. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -201
  43. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -212
  44. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -299
  45. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_long_comment.py +0 -114
  46. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -171
  47. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_offset_pagination.py +0 -118
  48. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -202
  49. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -155
  50. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_secret_in_log.py +0 -171
  51. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_select_star.py +0 -138
  52. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -580
  53. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -167
  54. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_tautological_expect.py +0 -279
  55. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -86
  56. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -170
  57. sarj_python_lint-0.80.4/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -206
  58. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/.gitignore +0 -0
  59. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/LICENSE +0 -0
  60. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/README.md +0 -0
  61. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/__init__.py +0 -0
  62. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_analysis_session.py +0 -0
  63. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_filesystem.py +0 -0
  64. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  65. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_secret_names.py +0 -0
  66. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/_version.py +0 -0
  67. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/py.typed +0 -0
  68. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/ratchet.py +0 -0
  69. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rule_base.py +0 -0
  70. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/__init__.py +0 -0
  71. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  72. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_comments.py +0 -0
  73. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  74. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
  75. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_logging.py +0 -0
  76. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_paths.py +0 -0
  77. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
  78. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
  79. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  80. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  81. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  82. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  83. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  84. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  85. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  86. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  87. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  88. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  89. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  90. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  91. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  92. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  93. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  94. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  95. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  96. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  97. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  98. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  99. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  100. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  101. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  102. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  103. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  104. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  105. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  106. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  107. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  108. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  109. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  110. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
  111. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  112. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  113. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  114. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  115. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  116. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  117. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  118. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  119. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  120. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  121. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  122. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  123. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  124. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  125. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  126. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  127. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  128. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  129. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  130. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  131. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  132. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  133. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  134. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  135. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  136. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  137. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  138. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
  139. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  140. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  141. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  142. {sarj_python_lint-0.80.4 → sarj_python_lint-0.80.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  143. {sarj_python_lint-0.80.4 → 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.4
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.4"
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,
@@ -10,6 +13,7 @@ from sarj_python_lint.rules.docstring_args_restate_signature import (
10
13
  from sarj_python_lint.rules.docstring_returns_restate_signature import (
11
14
  DocstringReturnsRestateSignature,
12
15
  )
16
+ from sarj_python_lint.rules.excessive_commentary import ExcessiveCommentary
13
17
  from sarj_python_lint.rules.fakes_in_shared_location import FakesInSharedLocation
14
18
  from sarj_python_lint.rules.fastapi_explicit_openapi_contract import FastapiExplicitOpenapiContract
15
19
  from sarj_python_lint.rules.iac_source_coupled_test import IacSourceCoupledTest
@@ -44,14 +48,8 @@ from sarj_python_lint.rules.no_generic_single_export_module import NoGenericSing
44
48
  from sarj_python_lint.rules.no_hidden_constructor_fallback import (
45
49
  NoHiddenConstructorFallback,
46
50
  )
47
- from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
48
- from sarj_python_lint.rules.no_long_comment import NoLongComment
49
51
  from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
50
52
  from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
51
- from sarj_python_lint.rules.no_optional_tenant_predicate import (
52
- NoOptionalTenantPredicate,
53
- )
54
- from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
55
53
  from sarj_python_lint.rules.no_random_uuid_in_sql import NoRandomUuidInSql
56
54
  from sarj_python_lint.rules.no_raw_connection_in_tests import NoRawConnectionInTests
57
55
  from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
@@ -60,8 +58,6 @@ from sarj_python_lint.rules.no_repeated_test_body import NoRepeatedTestBody
60
58
  from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
61
59
  from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
62
60
  from sarj_python_lint.rules.no_select_star import NoSelectStar
63
- from sarj_python_lint.rules.no_sentinel_return_on_except import NoSentinelReturnOnExcept
64
- from sarj_python_lint.rules.no_stdlib_logging import NoStdlibLogging
65
61
  from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
66
62
  from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
67
63
  from sarj_python_lint.rules.no_typed_doc_sections import NoTypedDocSections
@@ -72,6 +68,9 @@ from sarj_python_lint.rules.no_unnecessary_docstring import NoUnnecessaryDocstri
72
68
  from sarj_python_lint.rules.no_vague_suppression_description import (
73
69
  NoVagueSuppressionDescription,
74
70
  )
71
+ from sarj_python_lint.rules.no_whole_request_response_payload_in_log import (
72
+ NoWholeRequestResponsePayloadInLog,
73
+ )
75
74
  from sarj_python_lint.rules.opaque_parametrize_case_needs_id import OpaqueParametrizeCaseNeedsId
76
75
  from sarj_python_lint.rules.over_mocked_test import OverMockedTest
77
76
  from sarj_python_lint.rules.phase_label_comment import TestPhaseLabelComment
@@ -152,6 +151,7 @@ from sarj_python_lint.rules.store_insert_requires_on_conflict import (
152
151
  from sarj_python_lint.rules.timestamp_order_requires_tiebreaker import TimestampOrderRequiresTiebreaker
153
152
  from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
154
153
  from sarj_python_lint.rules.trivially_true_assertion import TriviallyTrueAssertion
154
+ from sarj_python_lint.rules.typed_error_reasons import TypedErrorReasons
155
155
  from sarj_python_lint.rules.uncontrolled_randomness_in_test import UncontrolledRandomnessInTest
156
156
  from sarj_python_lint.rules.unused_mock_setup import UnusedMockSetup
157
157
 
@@ -163,20 +163,20 @@ if TYPE_CHECKING:
163
163
 
164
164
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
165
165
  {
166
+ ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
167
+ ExcessiveCommentary.id: ExcessiveCommentary,
166
168
  PreferWalrusAwaitedNoneGuard.id: PreferWalrusAwaitedNoneGuard,
167
169
  TimestampOrderRequiresTiebreaker.id: TimestampOrderRequiresTiebreaker,
168
170
  NoStringConcatInLoop.id: NoStringConcatInLoop,
169
171
  PreferClassRow.id: PreferClassRow,
170
172
  PreferCollectionComprehension.id: PreferCollectionComprehension,
171
173
  PreferStrEnum.id: PreferStrEnum,
172
- NoIsinstanceUnionChain.id: NoIsinstanceUnionChain,
173
174
  NoOffsetPagination.id: NoOffsetPagination,
174
175
  PreferNamedtupleOverTupleReturn.id: PreferNamedtupleOverTupleReturn,
175
176
  NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
176
177
  PydanticAtBoundaries.id: PydanticAtBoundaries,
177
178
  FastapiExplicitOpenapiContract.id: FastapiExplicitOpenapiContract,
178
179
  FakesInSharedLocation.id: FakesInSharedLocation,
179
- NoSentinelReturnOnExcept.id: NoSentinelReturnOnExcept,
180
180
  PreferConstantTimeSecretCompare.id: PreferConstantTimeSecretCompare,
181
181
  NoSecretInLog.id: NoSecretInLog,
182
182
  PreferTimedeltaForDurations.id: PreferTimedeltaForDurations,
@@ -186,7 +186,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
186
186
  StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
187
187
  SourceCoupledTest.id: SourceCoupledTest,
188
188
  IacSourceCoupledTest.id: IacSourceCoupledTest,
189
- NoQueryWithManyJoins.id: NoQueryWithManyJoins,
190
189
  NoRawConnectionInTests.id: NoRawConnectionInTests,
191
190
  NoAnalyticalAggregationInPostgresStore.id: NoAnalyticalAggregationInPostgresStore,
192
191
  NoSelectStar.id: NoSelectStar,
@@ -209,12 +208,10 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
209
208
  NoRestatedComment.id: NoRestatedComment,
210
209
  RedundantDocstring.id: RedundantDocstring,
211
210
  TrailingValueNarration.id: TrailingValueNarration,
212
- NoStdlibLogging.id: NoStdlibLogging,
213
211
  NoRandomUuidInSql.id: NoRandomUuidInSql,
214
212
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
215
213
  NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
216
214
  NoFastapiOnEvent.id: NoFastapiOnEvent,
217
- NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
218
215
  NoTautologicalExpect.id: NoTautologicalExpect,
219
216
  PreferLibraryFake.id: PreferLibraryFake,
220
217
  OverMockedTest.id: OverMockedTest,
@@ -242,7 +239,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
242
239
  DocstringReturnsRestateSignature.id: DocstringReturnsRestateSignature,
243
240
  RestatedTestDocstring.id: RestatedTestDocstring,
244
241
  TestPhaseLabelComment.id: TestPhaseLabelComment,
245
- NoLongComment.id: NoLongComment,
246
242
  NoNestedPydanticFieldValidator.id: NoNestedPydanticFieldValidator,
247
243
  NoRedundantLiteralDescription.id: NoRedundantLiteralDescription,
248
244
  NoTypedDocSections.id: NoTypedDocSections,
@@ -260,6 +256,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
260
256
  RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
261
257
  RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
262
258
  NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
259
+ NoWholeRequestResponsePayloadInLog.id: NoWholeRequestResponsePayloadInLog,
260
+ TypedErrorReasons.id: TypedErrorReasons,
263
261
  }
264
262
  )
265
263
 
@@ -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