sarj-python-lint 1.75.0__tar.gz → 1.77.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/__main__.py +11 -6
  4. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/benchmark.py +13 -3
  5. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_fastapi.py +21 -17
  6. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  7. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +53 -4
  8. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +86 -0
  9. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +130 -36
  10. sarj_python_lint-1.77.0/src/sarj_python_lint/rules/prefer_grouped_settings.py +137 -0
  11. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +61 -6
  12. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +213 -13
  13. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/.gitignore +0 -0
  14. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/LICENSE +0 -0
  15. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/README.md +0 -0
  16. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/__init__.py +0 -0
  17. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  18. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix.py +0 -0
  19. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  20. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  21. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  22. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  23. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  24. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  25. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  26. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  27. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  28. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  29. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  30. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  31. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  32. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  33. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_file_context.py +0 -0
  34. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_filesystem.py +0 -0
  35. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_fixes.py +0 -0
  36. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_parallel.py +0 -0
  37. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_python_target.py +0 -0
  38. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  39. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_secret_names.py +0 -0
  40. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_source.py +0 -0
  41. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_version.py +0 -0
  42. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/autofix.py +0 -0
  43. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/cast_access.py +0 -0
  44. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  45. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/json_boundary.py +0 -0
  46. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/py.typed +0 -0
  47. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/ratchet.py +0 -0
  48. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rule_base.py +0 -0
  49. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  50. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  51. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  52. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  53. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  54. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  55. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  56. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  57. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  58. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  59. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  60. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  61. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  62. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  63. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  64. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  65. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  66. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  67. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  68. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  69. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  70. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  71. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  72. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  73. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  74. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  75. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  76. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  77. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  78. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  79. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  80. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  81. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  82. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  83. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  84. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  85. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  86. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  87. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  88. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  89. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  90. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  91. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  92. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  93. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  94. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  95. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  96. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  97. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  98. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  99. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  100. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  101. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  102. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  103. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  104. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  105. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  106. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  107. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  108. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  109. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  110. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  111. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  112. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  113. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  114. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  115. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  116. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  117. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  118. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  119. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  120. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  121. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  122. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  123. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  124. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  125. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  126. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  127. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  128. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  129. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  130. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  131. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  132. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  133. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  134. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  135. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  136. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  137. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  138. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  139. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  140. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  141. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  142. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  143. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  144. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  145. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  146. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  147. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  148. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  149. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  150. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  151. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  152. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  153. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  154. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  155. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  156. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  157. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  158. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  159. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  160. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  161. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  162. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  163. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  164. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  165. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  166. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  167. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  168. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  169. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  170. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  171. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  172. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  173. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  174. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  175. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  176. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  177. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  178. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  179. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  180. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  181. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  182. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  183. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  184. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  185. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  186. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  187. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  188. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  189. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  190. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  191. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  192. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  193. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  194. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  195. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  196. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  197. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  198. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  199. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  200. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  201. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  202. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  203. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  204. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  205. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  206. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  207. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  208. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  209. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  210. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  211. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  212. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  213. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  214. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  215. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  216. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  217. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  218. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  219. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  220. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  221. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  222. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  223. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  224. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  225. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  226. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  227. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  228. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  229. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  230. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  231. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  232. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  233. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  234. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  235. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  236. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  237. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  238. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  239. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  240. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  241. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  242. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  243. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  244. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  245. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  246. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  247. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  248. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  249. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  250. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  251. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  252. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  253. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  254. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  255. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  256. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  257. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  258. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  259. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  260. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  261. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  262. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  263. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  264. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  265. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  266. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  267. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  268. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  269. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  270. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  271. {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/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.75.0
3
+ Version: 1.77.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.17.2
22
- Requires-Dist: sarj-rule-contracts==0.4.0
22
+ Requires-Dist: sarj-rule-contracts==0.4.1
23
23
  Requires-Dist: sqlglot==30.24.0
24
24
  Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.75.0"
3
+ version = "1.77.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.4.0", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
18
+ dependencies = ["sarj-rule-contracts==0.4.1", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -9,8 +9,9 @@ import json
9
9
  from pathlib import Path
10
10
  import sys
11
11
  from types import MappingProxyType
12
- from typing import Annotated, NamedTuple
12
+ from typing import TYPE_CHECKING, Annotated, NamedTuple
13
13
 
14
+ from sarj_rule_contracts import RuleId
14
15
  import typer
15
16
 
16
17
  from sarj_python_lint import __version__
@@ -26,6 +27,10 @@ from sarj_python_lint.rules._project_index import ProjectIndexSet
26
27
  from sarj_python_lint.suppression_policy import PROTECTED_SUPPRESSION_CODES
27
28
 
28
29
 
30
+ if TYPE_CHECKING:
31
+ from collections.abc import Sequence
32
+
33
+
29
34
  SKIP_DIR_NAMES = frozenset(
30
35
  {
31
36
  "node_modules",
@@ -86,18 +91,18 @@ def _python_files(p: Path) -> list[Path]:
86
91
  return sorted(out)
87
92
 
88
93
 
89
- def _canonical_rule_ids(rule_ids: list[str]) -> list[str]:
94
+ def _canonical_rule_ids(rule_names: Sequence[str]) -> list[RuleId]:
90
95
  aliases = {
91
96
  alias: rule_id
92
97
  for rule_id, cls in REGISTRY.items()
93
98
  if (documentation := cls.documentation) is not None
94
99
  for alias in documentation.aliases
95
100
  }
96
- return list(dict.fromkeys(rid if rid in REGISTRY else aliases.get(rid, rid) for rid in rule_ids))
101
+ return list(dict.fromkeys(RuleId(name if name in REGISTRY else aliases.get(name, name)) for name in rule_names))
97
102
 
98
103
 
99
- def _check(rule_ids: list[str], paths: list[Path], *, jobs: int = 1) -> list[Diagnostic]:
100
- rule_ids = _canonical_rule_ids(rule_ids)
104
+ def _check(rule_names: Sequence[str], paths: list[Path], *, jobs: int = 1) -> list[Diagnostic]:
105
+ rule_ids = _canonical_rule_ids(rule_names)
101
106
  unknown = [rid for rid in rule_ids if rid not in REGISTRY]
102
107
  if unknown:
103
108
  sys.stderr.write(f"unknown rule(s): {', '.join(unknown)}\n")
@@ -145,7 +150,7 @@ def check_source(
145
150
 
146
151
 
147
152
  def analyze(
148
- rule_ids: list[str],
153
+ rule_ids: Sequence[str], # sarj-noqa: SARJ093 -- Public parsing boundary accepts raw names.
149
154
  paths: list[Path],
150
155
  *,
151
156
  baseline: Path | None = None,
@@ -8,14 +8,19 @@ from pathlib import Path
8
8
  from statistics import median
9
9
  import sys
10
10
  from time import perf_counter
11
- from typing import Annotated
11
+ from typing import TYPE_CHECKING, Annotated
12
12
 
13
+ from sarj_rule_contracts import RuleId
13
14
  import typer
14
15
 
15
16
  from sarj_python_lint.__main__ import analyze, expand_paths
16
17
  from sarj_python_lint.rules import REGISTRY
17
18
 
18
19
 
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Sequence
22
+
23
+
19
24
  @dataclass(frozen=True, slots=True)
20
25
  class BenchmarkSample:
21
26
  seconds: float
@@ -43,11 +48,16 @@ def peak_memory_bytes() -> int | None:
43
48
  return peak if sys.platform == "darwin" else peak * 1024
44
49
 
45
50
 
46
- def measure(paths: list[Path], rule_ids: list[str] | None = None, *, repeats: int = 5) -> BenchmarkReport:
51
+ def measure(
52
+ paths: list[Path],
53
+ rule_ids: Sequence[str] | None = None, # sarj-noqa: SARJ093 -- Public input accepts raw rule names.
54
+ *,
55
+ repeats: int = 5,
56
+ ) -> BenchmarkReport:
47
57
  if repeats < 1:
48
58
  msg = "repeats must be positive"
49
59
  raise ValueError(msg)
50
- selected = sorted(REGISTRY) if rule_ids is None else sorted(rule_ids)
60
+ selected = [RuleId(name) for name in sorted(REGISTRY if rule_ids is None else rule_ids)]
51
61
  unknown = sorted(set(selected) - REGISTRY.keys())
52
62
  if unknown:
53
63
  msg = f"unknown rule(s): {', '.join(unknown)}"
@@ -32,6 +32,7 @@ RESPONSE_TYPES = frozenset(
32
32
  }
33
33
  )
34
34
  _DEPENDENCY_MARKERS = frozenset({"Depends", "Security"})
35
+ _CONSTRUCTOR_SOURCES = frozenset({"fastapi", "fastapi.applications", "fastapi.routing"})
35
36
  _MAX_IMPORTED_MODULE_BYTES = 500_000
36
37
  _MAX_IMPORT_HOPS = 8
37
38
  _MAX_PATH_ANCESTORS = 32
@@ -134,6 +135,7 @@ class FastapiIndex:
134
135
  self._node_classes: dict[int, int | None] = {}
135
136
  self._route_scopes: dict[int, int] = {}
136
137
  self._scope_parents: dict[int, int | None] = {0: None}
138
+ self._class_scopes: set[int] = set()
137
139
  self._index_scopes(tree)
138
140
  self._read_imports(tree)
139
141
  self._read_aliases(tree)
@@ -145,6 +147,7 @@ class FastapiIndex:
145
147
  self._node_scopes[id(node)] = scope
146
148
  if isinstance(node, ast.ClassDef):
147
149
  class_owner = node.lineno
150
+ self._class_scopes.add(node.lineno)
148
151
  self._scope_parents[node.lineno] = scope
149
152
  scope = node.lineno
150
153
  self._node_classes[id(node)] = class_owner
@@ -272,7 +275,7 @@ class FastapiIndex:
272
275
 
273
276
  def _resolve_alias(self, key: _ReceiverKey, value: ast.expr) -> bool:
274
277
  source = self._receiver_key(key.scope, _binding_name(value))
275
- constructor_kind = self._constructor_kind(value)
278
+ constructor_kind = self.constructor_kind(value)
276
279
  if constructor_kind is not None:
277
280
  self._register_constructor(key, value, constructor_kind)
278
281
  return True
@@ -337,25 +340,26 @@ class FastapiIndex:
337
340
  return isinstance(value, ast.Constant) and value.value is False
338
341
 
339
342
  def _is_constructor_call(self, node: ast.expr) -> bool:
340
- return self._constructor_kind(node) is not None
343
+ return self.constructor_kind(node) is not None
341
344
 
342
- def _constructor_kind(self, node: ast.expr) -> Literal["FastAPI", "APIRouter"] | None:
345
+ def constructor_kind(self, node: ast.expr) -> Literal["FastAPI", "APIRouter"] | None:
343
346
  if not isinstance(node, ast.Call):
344
347
  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"
348
+ root = self._root_name(node.func) if isinstance(node.func, ast.Attribute) else flat_name(node.func)
349
+ local_scope = self._node_scopes.get(id(node), 0)
350
+ scope: int | None = local_scope
351
+ while scope is not None and scope != 0:
352
+ # Method bodies do not close over their class namespace.
353
+ if (scope == local_scope or scope not in self._class_scopes) and _ReceiverKey(
354
+ scope, root
355
+ ) in self.bound_names:
356
+ return None
357
+ scope = self._scope_parents[scope]
358
+ canonical = self.imports.resolved_symbol(node.func, sources=_CONSTRUCTOR_SOURCES)
359
+ if canonical == "FastAPI":
360
+ return "FastAPI"
361
+ if canonical == "APIRouter":
362
+ return "APIRouter"
359
363
  return None
360
364
 
361
365
  def routes(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[Route, ...]:
@@ -240,6 +240,7 @@ from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
240
240
  from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
241
241
  from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
242
242
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
243
+ from sarj_python_lint.rules.prefer_grouped_settings import PreferGroupedSettings
243
244
  from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
244
245
  from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
245
246
  from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
@@ -255,6 +256,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
255
256
 
256
257
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
257
258
  {
259
+ PreferGroupedSettings.id: PreferGroupedSettings,
258
260
  NoRedundantLocalAlias.id: NoRedundantLocalAlias,
259
261
  PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
260
262
  DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
@@ -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,50 @@ 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
+ if (
931
+ route.is_hidden
932
+ or route.path is None
933
+ or route.method == "*"
934
+ or "openapi_extra" in route.keywords
935
+ or _conditional_registration(route.decorator, parents)
936
+ ):
937
+ continue
938
+ operation_id = route.keywords.get("operation_id")
939
+ if (
940
+ not isinstance(operation_id, ast.Constant)
941
+ or not isinstance(operation_id.value, str)
942
+ or not operation_id.value
943
+ ):
944
+ continue
945
+ key = scope, route.receiver, operation_id.value
946
+ earlier = seen.get(key)
947
+ if earlier is None:
948
+ seen[key] = route
949
+ elif (earlier.path, earlier.method) != (route.path, route.method) and id(route.decorator) not in reported:
950
+ findings.append(
951
+ _Finding(
952
+ operation_id,
953
+ f"[operation-id] duplicate operation_id `{operation_id.value}`; use a unique ID for each operation.",
954
+ )
955
+ )
956
+ reported.add(id(route.decorator))
957
+ return findings
958
+
959
+
960
+ def _conditional_registration(decorator: ast.Call, parents: Mapping[ast.AST, ast.AST]) -> bool:
961
+ owner = parents.get(decorator)
962
+ while owner is not None:
963
+ if isinstance(owner, (ast.If, ast.Match, ast.Try, ast.TryStar, ast.For, ast.AsyncFor, ast.While)):
964
+ return True
965
+ owner = parents.get(owner)
966
+ return False
967
+
968
+
883
969
  def _path_pattern(path: str) -> re.Pattern[str] | None:
884
970
  parts: list[str] = []
885
971
  position = 0