sarj-python-lint 1.76.0__tar.gz → 1.77.2__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 (271) hide show
  1. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_fixes.py +3 -1
  4. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_fastapi.py +156 -60
  5. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +53 -4
  6. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +87 -0
  7. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/.gitignore +0 -0
  8. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/LICENSE +0 -0
  9. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/README.md +0 -0
  10. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix.py +0 -0
  14. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  15. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  16. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_comments.py +0 -0
  17. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  18. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  19. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_docs.py +0 -0
  20. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_enums.py +0 -0
  21. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  22. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_inert.py +0 -0
  23. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  24. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  25. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_security.py +0 -0
  26. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  27. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_tests.py +0 -0
  28. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_file_context.py +0 -0
  29. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_filesystem.py +0 -0
  30. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_parallel.py +0 -0
  31. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_python_target.py +0 -0
  32. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  33. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_secret_names.py +0 -0
  34. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_source.py +0 -0
  35. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_version.py +0 -0
  36. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/autofix.py +0 -0
  37. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/benchmark.py +0 -0
  38. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/cast_access.py +0 -0
  39. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  40. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/json_boundary.py +0 -0
  41. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/py.typed +0 -0
  42. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/ratchet.py +0 -0
  43. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rule_base.py +0 -0
  44. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  45. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  46. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  47. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  48. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  49. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  50. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  51. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  52. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  53. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  54. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  55. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  56. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  57. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  58. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  59. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  60. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  61. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  62. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  63. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  64. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  65. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  66. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  67. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  68. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  69. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  70. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  71. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  72. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  73. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  74. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  75. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  76. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  77. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  78. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  79. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  80. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  81. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  82. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  83. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  84. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  85. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  86. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  87. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  88. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  89. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  90. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  91. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  92. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  93. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  94. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  95. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  96. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  97. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  98. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  99. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  100. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  101. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  102. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  103. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  104. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  105. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  106. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  107. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  108. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  109. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  110. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  111. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  112. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  113. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  114. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  115. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  116. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  117. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  118. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  119. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  120. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  121. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  122. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  123. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  124. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  125. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  126. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  127. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  128. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  129. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  130. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  131. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  132. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  133. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  134. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  135. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  136. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  137. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  138. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  139. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  140. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  141. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  142. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  143. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  144. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  145. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  146. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  147. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  148. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  149. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  150. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  151. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  152. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  153. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  154. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  155. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  156. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  157. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  158. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  159. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  160. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  161. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  162. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  163. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  164. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  165. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  166. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  167. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  168. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  169. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  170. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  171. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  172. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  173. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  174. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  175. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  176. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  177. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  178. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  179. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  180. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  181. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  182. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  183. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  184. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  185. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  186. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  187. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  188. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  189. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  190. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  191. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  192. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  193. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  194. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  195. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  196. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  197. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  198. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  199. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  200. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  201. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  202. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  203. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  204. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  205. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  206. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  207. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  208. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  209. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  210. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  211. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  212. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  213. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  214. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  215. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  216. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  217. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  218. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  219. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  220. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  221. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  222. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  223. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  224. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  225. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  226. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  227. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  228. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  229. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  230. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  231. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  232. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  233. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  234. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  235. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  236. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  237. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  238. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  239. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  240. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  241. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  242. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  243. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  244. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  245. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  246. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  247. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  248. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  249. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  250. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  251. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  252. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  253. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  254. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  255. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  256. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  257. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  258. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  259. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  260. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  261. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  262. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  263. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  264. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  265. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  266. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  267. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  268. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  269. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  270. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  271. {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.76.0
3
+ Version: 1.77.2
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.76.0"
3
+ version = "1.77.2"
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" }]
@@ -54,12 +54,14 @@ def apply_fixes(source: str, fixes: Sequence[Fix]) -> str:
54
54
  edits.sort()
55
55
  result: list[bytes] = []
56
56
  previous = 0
57
+ previous_start: int | None = None
57
58
  for start, end, replacement in edits:
58
- if start < previous:
59
+ if start < previous or start == previous_start:
59
60
  msg = "fix groups overlap"
60
61
  raise ValueError(msg)
61
62
  result.extend((raw[previous:start], replacement))
62
63
  previous = end
64
+ previous_start = start
63
65
  result.append(raw[previous:])
64
66
  fixed = b"".join(result).decode("utf-8")
65
67
  tree = ast.parse(fixed)
@@ -11,6 +11,7 @@ from sarj_python_lint.rules._imports import ImportIndex
11
11
 
12
12
 
13
13
  if TYPE_CHECKING:
14
+ from collections.abc import Sequence
14
15
  from pathlib import Path
15
16
 
16
17
 
@@ -32,6 +33,7 @@ RESPONSE_TYPES = frozenset(
32
33
  }
33
34
  )
34
35
  _DEPENDENCY_MARKERS = frozenset({"Depends", "Security"})
36
+ _CONSTRUCTOR_SOURCES = frozenset({"fastapi", "fastapi.applications", "fastapi.routing"})
35
37
  _MAX_IMPORTED_MODULE_BYTES = 500_000
36
38
  _MAX_IMPORT_HOPS = 8
37
39
  _MAX_PATH_ANCESTORS = 32
@@ -80,6 +82,12 @@ class _ReceiverKey(NamedTuple):
80
82
  name: str
81
83
 
82
84
 
85
+ class _ScopeFrame(NamedTuple):
86
+ node: ast.AST
87
+ scope: int
88
+ class_owner: int | None
89
+
90
+
83
91
  class _DecoratorBinding(NamedTuple):
84
92
  receiver: str
85
93
  method: str
@@ -108,6 +116,24 @@ def _binding_name(node: ast.expr) -> str:
108
116
  return ""
109
117
 
110
118
 
119
+ def _binding_targets(node: ast.AST) -> Sequence[ast.expr]:
120
+ match node:
121
+ case ast.Assign(targets=targets) | ast.Delete(targets=targets):
122
+ return targets
123
+ case (
124
+ ast.AnnAssign(target=target)
125
+ | ast.AugAssign(target=target)
126
+ | ast.For(target=target)
127
+ | ast.AsyncFor(target=target)
128
+ | ast.NamedExpr(target=target)
129
+ ):
130
+ return (target,)
131
+ case ast.With(items=items) | ast.AsyncWith(items=items):
132
+ return tuple(item.optional_vars for item in items if item.optional_vars is not None)
133
+ case _:
134
+ return ()
135
+
136
+
111
137
  class FastapiIndex:
112
138
  def __init__(self, tree: ast.Module, *, path: Path | None = None) -> None:
113
139
  self.tree: ast.Module = tree
@@ -134,25 +160,82 @@ class FastapiIndex:
134
160
  self._node_classes: dict[int, int | None] = {}
135
161
  self._route_scopes: dict[int, int] = {}
136
162
  self._scope_parents: dict[int, int | None] = {0: None}
163
+ self._class_scopes: set[int] = set()
164
+ self._comprehension_scopes: set[int] = set()
137
165
  self._index_scopes(tree)
138
166
  self._read_imports(tree)
139
167
  self._read_aliases(tree)
140
168
 
141
169
  def _index_scopes(self, tree: ast.Module) -> None:
142
- pending: list[tuple[ast.AST, int, int | None]] = [(tree, 0, None)]
170
+ pending = [_ScopeFrame(tree, 0, None)]
143
171
  while pending:
144
172
  node, scope, class_owner = pending.pop()
145
173
  self._node_scopes[id(node)] = scope
146
- if isinstance(node, ast.ClassDef):
147
- class_owner = node.lineno
148
- self._scope_parents[node.lineno] = scope
149
- scope = node.lineno
150
174
  self._node_classes[id(node)] = class_owner
151
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
152
- self._route_scopes[id(node)] = scope
153
- self._scope_parents[node.lineno] = scope
154
- scope = node.lineno
155
- pending.extend((child, scope, class_owner) for child in reversed(tuple(ast.iter_child_nodes(node))))
175
+ match node:
176
+ case ast.ClassDef() | ast.FunctionDef() | ast.AsyncFunctionDef():
177
+ self._index_definition(node, scope, class_owner, pending)
178
+ case ast.Lambda():
179
+ self._index_lambda(node, scope, class_owner, pending)
180
+ case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
181
+ self._index_comprehension(node, scope, class_owner, pending)
182
+ case _:
183
+ pending.extend(
184
+ _ScopeFrame(child, scope, class_owner) for child in reversed(tuple(ast.iter_child_nodes(node)))
185
+ )
186
+
187
+ def _index_definition(
188
+ self,
189
+ node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
190
+ scope: int,
191
+ class_owner: int | None,
192
+ pending: list[_ScopeFrame],
193
+ ) -> None:
194
+ body = set(node.body)
195
+ owner = class_owner
196
+ self._scope_parents[node.lineno] = scope
197
+ if isinstance(node, ast.ClassDef):
198
+ owner = node.lineno
199
+ self._class_scopes.add(node.lineno)
200
+ self._node_classes[id(node)] = owner
201
+ else:
202
+ self._route_scopes[id(node)] = scope
203
+ pending.extend(
204
+ _ScopeFrame(child, node.lineno, owner) if child in body else _ScopeFrame(child, scope, class_owner)
205
+ for child in reversed(tuple(ast.iter_child_nodes(node)))
206
+ )
207
+
208
+ def _index_lambda(self, node: ast.Lambda, scope: int, class_owner: int | None, pending: list[_ScopeFrame]) -> None:
209
+ implicit_scope = -id(node)
210
+ self._scope_parents[implicit_scope] = scope
211
+ self._bind_arguments(node.args, implicit_scope)
212
+ pending.extend(
213
+ (_ScopeFrame(node.args, scope, class_owner), _ScopeFrame(node.body, implicit_scope, class_owner))
214
+ )
215
+
216
+ def _index_comprehension(
217
+ self,
218
+ node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp,
219
+ outer_scope: int,
220
+ class_owner: int | None,
221
+ pending: list[_ScopeFrame],
222
+ ) -> None:
223
+ scope = -id(node)
224
+ self._scope_parents[scope] = outer_scope
225
+ self._comprehension_scopes.add(scope)
226
+ for index, generator in enumerate(node.generators):
227
+ self._node_scopes[id(generator)] = scope
228
+ self._node_classes[id(generator)] = class_owner
229
+ self.bound_names.update(_ReceiverKey(scope, name) for name in self._target_names(generator.target))
230
+ pending.extend(
231
+ _ScopeFrame(child, outer_scope if index == 0 and child is generator.iter else scope, class_owner)
232
+ for child in ast.iter_child_nodes(generator)
233
+ )
234
+ pending.extend(
235
+ _ScopeFrame(child, scope, class_owner)
236
+ for child in ast.iter_child_nodes(node)
237
+ if not isinstance(child, ast.comprehension)
238
+ )
156
239
 
157
240
  def _read_imports(self, tree: ast.Module) -> None:
158
241
  for node in walk_ast(tree):
@@ -219,7 +302,17 @@ class FastapiIndex:
219
302
  changed = True
220
303
 
221
304
  def _read_bound_names(self, node: ast.AST) -> None:
222
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
305
+ if targets := _binding_targets(node):
306
+ self.bound_names.update(
307
+ _ReceiverKey(self._binding_scope(node), name)
308
+ for target in targets
309
+ for name in self._target_names(target)
310
+ )
311
+ elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name is not None:
312
+ self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
313
+ elif isinstance(node, ast.MatchMapping) and node.rest is not None:
314
+ self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.rest))
315
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
223
316
  self._bind_function_names(node)
224
317
  elif isinstance(node, ast.ClassDef):
225
318
  self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
@@ -228,34 +321,30 @@ class FastapiIndex:
228
321
  _ReceiverKey(self._node_scopes[id(node)], alias.asname or alias.name.split(".")[0])
229
322
  for alias in node.names
230
323
  )
231
- elif isinstance(node, (ast.For, ast.AsyncFor)):
232
- self.bound_names.update(
233
- _ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(node.target)
234
- )
235
- elif isinstance(node, ast.With):
236
- self._bind_with_targets(node)
237
324
  elif isinstance(node, ast.ExceptHandler) and node.name is not None:
238
325
  self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
239
- elif isinstance(node, ast.NamedExpr):
240
- self.bound_names.update(
241
- _ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(node.target)
242
- )
326
+
327
+ def _binding_scope(self, node: ast.AST) -> int:
328
+ scope = self._node_scopes[id(node)]
329
+ if isinstance(node, ast.NamedExpr):
330
+ while scope in self._comprehension_scopes:
331
+ parent = self._scope_parents[scope]
332
+ if parent is None:
333
+ break
334
+ scope = parent
335
+ return scope
243
336
 
244
337
  def _bind_function_names(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
245
338
  self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
246
- arguments = [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]
247
- if node.args.vararg is not None:
248
- arguments.append(node.args.vararg)
249
- if node.args.kwarg is not None:
250
- arguments.append(node.args.kwarg)
251
- self.bound_names.update(_ReceiverKey(node.lineno, argument.arg) for argument in arguments)
252
-
253
- def _bind_with_targets(self, node: ast.With) -> None:
254
- for item in node.items:
255
- if item.optional_vars is not None:
256
- self.bound_names.update(
257
- _ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(item.optional_vars)
258
- )
339
+ self._bind_arguments(node.args, node.lineno)
340
+
341
+ def _bind_arguments(self, arguments: ast.arguments, scope: int) -> None:
342
+ names = [*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs]
343
+ if arguments.vararg is not None:
344
+ names.append(arguments.vararg)
345
+ if arguments.kwarg is not None:
346
+ names.append(arguments.kwarg)
347
+ self.bound_names.update(_ReceiverKey(scope, argument.arg) for argument in names)
259
348
 
260
349
  def _alias_assignment(self, node: ast.AST) -> tuple[int, str, ast.expr] | None:
261
350
  if isinstance(node, ast.Assign) and len(node.targets) == 1:
@@ -272,7 +361,7 @@ class FastapiIndex:
272
361
 
273
362
  def _resolve_alias(self, key: _ReceiverKey, value: ast.expr) -> bool:
274
363
  source = self._receiver_key(key.scope, _binding_name(value))
275
- constructor_kind = self._constructor_kind(value)
364
+ constructor_kind = self.constructor_kind(value)
276
365
  if constructor_kind is not None:
277
366
  self._register_constructor(key, value, constructor_kind)
278
367
  return True
@@ -306,11 +395,15 @@ class FastapiIndex:
306
395
  self.hidden_receivers.add(key)
307
396
 
308
397
  def _target_names(self, node: ast.expr) -> tuple[str, ...]:
309
- if isinstance(node, ast.Name):
310
- return (node.id,)
311
- if isinstance(node, (ast.Tuple, ast.List)):
312
- return tuple(name for item in node.elts for name in self._target_names(item))
313
- return ()
398
+ match node:
399
+ case ast.Name(id=name):
400
+ return (name,)
401
+ case ast.Starred(value=value):
402
+ return self._target_names(value)
403
+ case ast.Tuple(elts=items) | ast.List(elts=items):
404
+ return tuple(name for item in items for name in self._target_names(item))
405
+ case _:
406
+ return ()
314
407
 
315
408
  def _assignment_scope(self, node: ast.Assign | ast.AnnAssign, name: str) -> int:
316
409
  if name.startswith("self."):
@@ -337,25 +430,26 @@ class FastapiIndex:
337
430
  return isinstance(value, ast.Constant) and value.value is False
338
431
 
339
432
  def _is_constructor_call(self, node: ast.expr) -> bool:
340
- return self._constructor_kind(node) is not None
433
+ return self.constructor_kind(node) is not None
341
434
 
342
- def _constructor_kind(self, node: ast.expr) -> Literal["FastAPI", "APIRouter"] | None:
435
+ def constructor_kind(self, node: ast.expr) -> Literal["FastAPI", "APIRouter"] | None:
343
436
  if not isinstance(node, ast.Call):
344
437
  return None
345
- if isinstance(node.func, ast.Name):
346
- canonical = self.symbols.get(node.func.id)
347
- if canonical == "FastAPI":
348
- return "FastAPI"
349
- if canonical == "APIRouter":
350
- return "APIRouter"
351
- return None
352
- if (
353
- isinstance(node.func, ast.Attribute)
354
- and isinstance(node.func.value, ast.Name)
355
- and node.func.value.id in self.modules
356
- and node.func.attr in {"FastAPI", "APIRouter"}
357
- ):
358
- return "FastAPI" if node.func.attr == "FastAPI" else "APIRouter"
438
+ root = self._root_name(node.func) if isinstance(node.func, ast.Attribute) else flat_name(node.func)
439
+ local_scope = self._node_scopes.get(id(node), 0)
440
+ scope: int | None = local_scope
441
+ while scope is not None and scope != 0:
442
+ # Method bodies do not close over their class namespace.
443
+ if (scope == local_scope or scope not in self._class_scopes) and _ReceiverKey(
444
+ scope, root
445
+ ) in self.bound_names:
446
+ return None
447
+ scope = self._scope_parents[scope]
448
+ canonical = self.imports.resolved_symbol(node.func, sources=_CONSTRUCTOR_SOURCES)
449
+ if canonical == "FastAPI":
450
+ return "FastAPI"
451
+ if canonical == "APIRouter":
452
+ return "APIRouter"
359
453
  return None
360
454
 
361
455
  def routes(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[Route, ...]:
@@ -522,11 +616,13 @@ class FastapiIndex:
522
616
  if isinstance(node, ast.Attribute):
523
617
  root = self._root_name(node)
524
618
  node_scope = self._node_scopes.get(id(node), 0)
525
- # An annotation is evaluated outside the function whose signature owns
526
- # it, so begin with that function's parent scope.
527
- scope: int | None = self._scope_parents.get(node_scope)
619
+ # The index records where the annotation is evaluated. A method body
620
+ # does not close over an intervening class namespace.
621
+ scope: int | None = node_scope
528
622
  while scope is not None and scope != 0:
529
- if _ReceiverKey(scope, root) in self.bound_names:
623
+ if (scope == node_scope or scope not in self._class_scopes) and _ReceiverKey(
624
+ scope, root
625
+ ) in self.bound_names:
530
626
  return True
531
627
  scope = self._scope_parents.get(scope)
532
628
  return False
@@ -42,19 +42,21 @@ class FastapiClassRouterContract(Rule):
42
42
  id = "fastapi-class-router-contract"
43
43
  code = "SARJ451"
44
44
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
45
- summary="FastAPI routers use injected `*Router.build()` owners and explicit named object response models.",
45
+ summary="FastAPI HTTP routes use injected `*Router.build()` owners and explicit named object response models.",
46
46
  rationale=(
47
47
  "Class-owned construction gives dependencies one reviewable composition boundary, while explicit object models "
48
48
  "keep success envelopes stable and generated clients precise."
49
49
  ),
50
50
  remediation=(
51
51
  "Inject dependencies through `*Router.__init__`, create and return a local `APIRouter` from `build()`, and declare "
52
- "`response_model=NamedResponse` with a matching named return annotation."
52
+ "`response_model=NamedResponse` with a matching named return annotation. Compose the application with "
53
+ "`FastAPI.include_router(...)` instead of declaring HTTP handlers directly on it."
53
54
  ),
54
55
  category=RuleCategory.ARCHITECTURE,
55
56
  autofix=AutofixPolicy.NONE,
56
57
  limitations=(
57
- "Tests, generated code, hidden routes, 204/304 responses, and explicit stream/file/HTML/text/redirect responses are excluded.",
58
+ "Tests, generated code, and hidden routes are excluded. FastAPI application construction and include_router composition are allowed.",
59
+ "Response-model checks exclude 204/304 responses and explicit stream/file/HTML/text/redirect responses.",
58
60
  ),
59
61
  examples=(
60
62
  RuleExample(
@@ -66,6 +68,38 @@ class FastapiClassRouterContract(Rule):
66
68
  expected_count=1,
67
69
  public=True,
68
70
  ),
71
+ RuleExample(
72
+ example_id="direct-app-route",
73
+ title="An application declares an HTTP handler directly",
74
+ outcome=ExampleOutcome.MATCH,
75
+ files=(
76
+ ExampleFile.python(
77
+ "app/routes.py",
78
+ "from fastapi import FastAPI\napp = FastAPI()\n"
79
+ "@app.get('/items', response_model=ItemsResponse)\n"
80
+ "def items() -> ItemsResponse:\n return ItemsResponse(items=[])\n",
81
+ ),
82
+ ),
83
+ focus_path=PurePosixPath("app/routes.py"),
84
+ expected_count=1,
85
+ public=True,
86
+ scenario="application-composition",
87
+ ),
88
+ RuleExample(
89
+ example_id="app-router-composition",
90
+ title="An application includes a class-owned router",
91
+ outcome=ExampleOutcome.NO_MATCH,
92
+ files=(
93
+ ExampleFile.python(
94
+ "app/routes.py",
95
+ "from fastapi import FastAPI\napp = FastAPI()\napp.include_router(ItemRouter(service).build())\n",
96
+ ),
97
+ ),
98
+ focus_path=PurePosixPath("app/routes.py"),
99
+ expected_count=0,
100
+ public=True,
101
+ scenario="application-composition",
102
+ ),
69
103
  RuleExample(
70
104
  example_id="class-router",
71
105
  title="A class builds and returns its router",
@@ -99,6 +133,7 @@ class FastapiClassRouterContract(Rule):
99
133
  node for node in context.nodes(ast.AST) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
100
134
  ):
101
135
  routes = tuple(route for route in index.routes(function) if not route.is_hidden)
136
+ findings.extend(_direct_app_route_findings(path, routes))
102
137
  for route in routes:
103
138
  problem = _response_problem(function, route, index)
104
139
  if problem is not None:
@@ -106,6 +141,20 @@ class FastapiClassRouterContract(Rule):
106
141
  return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.message))
107
142
 
108
143
 
144
+ def _direct_app_route_findings(path: Path, routes: tuple[Route, ...]) -> list[Diagnostic]:
145
+ decorators = {route.decorator for route in routes if route.receiver_kind == "FastAPI"}
146
+ return [
147
+ Diagnostic(
148
+ path,
149
+ decorator.lineno,
150
+ decorator.col_offset + 1,
151
+ FastapiClassRouterContract.code,
152
+ "declare HTTP routes on a local APIRouter returned by `*Router.build()`, then compose with include_router",
153
+ )
154
+ for decorator in decorators
155
+ ]
156
+
157
+
109
158
  def _router_construction_findings(
110
159
  path: Path,
111
160
  tree: ast.Module,
@@ -116,7 +165,7 @@ def _router_construction_findings(
116
165
  ) -> list[Diagnostic]:
117
166
  findings: list[Diagnostic] = []
118
167
  for call in (node for node in walk_ast(tree, index=node_index) if isinstance(node, ast.Call)):
119
- if index.canonical(call.func) != "APIRouter":
168
+ if index.constructor_kind(call) != "APIRouter":
120
169
  continue
121
170
  function = _ancestor(call, parents, (ast.FunctionDef, ast.AsyncFunctionDef))
122
171
  cls = _ancestor(call, parents, ast.ClassDef)
@@ -30,6 +30,8 @@ from sarj_python_lint.rules._paths import is_test_path
30
30
 
31
31
 
32
32
  if TYPE_CHECKING:
33
+ from collections.abc import Mapping
34
+
33
35
  from sarj_python_lint._file_context import PythonFileContext
34
36
 
35
37
 
@@ -87,6 +89,7 @@ class FastapiExplicitOpenapiContract(Rule):
87
89
  limitations=(
88
90
  "Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
89
91
  "Dynamic response mappings are accepted when their contents cannot be resolved statically.",
92
+ "Operation ID conflicts require unconditional registrations with literal IDs and paths on the same local router; conditional registrations, dynamic IDs, OpenAPI overrides, and cross-router composition require generated-schema validation.",
90
93
  "Explicit status and parameter-location findings are organizational policy even when FastAPI inference would generate valid OpenAPI.",
91
94
  (
92
95
  "Fixed-record return inference is local and conservative: dynamic keys, opaque escapes, and ambiguous "
@@ -193,6 +196,44 @@ class FastapiExplicitOpenapiContract(Rule):
193
196
  scenario="unannotated-record",
194
197
  public=True,
195
198
  ),
199
+ RuleExample(
200
+ example_id="duplicate-operation-id",
201
+ title="Two operations reuse one client method identifier",
202
+ outcome=ExampleOutcome.MATCH,
203
+ files=(
204
+ ExampleFile.python(
205
+ "api.py",
206
+ "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
207
+ "@router.get('/items', status_code=200, operation_id='items')\n"
208
+ "async def read_items() -> ItemsResponse: ...\n\n"
209
+ "@router.post('/items', status_code=201, operation_id='items')\n"
210
+ "async def create_item() -> ItemResponse: ...\n",
211
+ ),
212
+ ),
213
+ focus_path=PurePosixPath("api.py"),
214
+ expected_count=1,
215
+ scenario="operation-id",
216
+ public=True,
217
+ ),
218
+ RuleExample(
219
+ example_id="unique-operation-ids",
220
+ title="Each operation has a distinct client method identifier",
221
+ outcome=ExampleOutcome.NO_MATCH,
222
+ files=(
223
+ ExampleFile.python(
224
+ "api.py",
225
+ "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
226
+ "@router.get('/items', status_code=200, operation_id='read_items')\n"
227
+ "async def read_items() -> ItemsResponse: ...\n\n"
228
+ "@router.post('/items', status_code=201, operation_id='create_item')\n"
229
+ "async def create_item() -> ItemResponse: ...\n",
230
+ ),
231
+ ),
232
+ focus_path=PurePosixPath("api.py"),
233
+ expected_count=0,
234
+ scenario="operation-id",
235
+ public=True,
236
+ ),
196
237
  ),
197
238
  )
198
239
  description: str = documentation.summary
@@ -228,6 +269,7 @@ class FastapiExplicitOpenapiContract(Rule):
228
269
  findings.extend(_check_projection(route))
229
270
  findings.extend(_check_response_statuses(route, index))
230
271
  findings.extend(_check_route_conflicts(declared))
272
+ findings.extend(_check_operation_ids(declared, context.parents))
231
273
  return [
232
274
  Diagnostic(
233
275
  path=path,
@@ -880,6 +922,51 @@ def _check_route_conflicts(routes: list[tuple[int, Route]]) -> list[_Finding]:
880
922
  return findings
881
923
 
882
924
 
925
+ def _check_operation_ids(routes: list[tuple[int, Route]], parents: Mapping[ast.AST, ast.AST]) -> list[_Finding]:
926
+ findings: list[_Finding] = []
927
+ seen: dict[tuple[int, str, str], Route] = {}
928
+ reported: set[int] = set()
929
+ for scope, route in sorted(routes, key=lambda item: item[1].decorator.lineno):
930
+ schema_override = route.has_unpack or "openapi_extra" in route.keywords
931
+ if (
932
+ route.is_hidden
933
+ or route.path is None
934
+ or route.method == "*"
935
+ or schema_override
936
+ or _conditional_registration(route.decorator, parents)
937
+ ):
938
+ continue
939
+ operation_id = route.keywords.get("operation_id")
940
+ if (
941
+ not isinstance(operation_id, ast.Constant)
942
+ or not isinstance(operation_id.value, str)
943
+ or not operation_id.value
944
+ ):
945
+ continue
946
+ key = scope, route.receiver, operation_id.value
947
+ earlier = seen.get(key)
948
+ if earlier is None:
949
+ seen[key] = route
950
+ elif (earlier.path, earlier.method) != (route.path, route.method) and id(route.decorator) not in reported:
951
+ findings.append(
952
+ _Finding(
953
+ operation_id,
954
+ f"[operation-id] duplicate operation_id `{operation_id.value}`; use a unique ID for each operation.",
955
+ )
956
+ )
957
+ reported.add(id(route.decorator))
958
+ return findings
959
+
960
+
961
+ def _conditional_registration(decorator: ast.Call, parents: Mapping[ast.AST, ast.AST]) -> bool:
962
+ owner = parents.get(decorator)
963
+ while owner is not None:
964
+ if isinstance(owner, (ast.If, ast.Match, ast.Try, ast.TryStar, ast.For, ast.AsyncFor, ast.While)):
965
+ return True
966
+ owner = parents.get(owner)
967
+ return False
968
+
969
+
883
970
  def _path_pattern(path: str) -> re.Pattern[str] | None:
884
971
  parts: list[str] = []
885
972
  position = 0