sarj-python-lint 1.75.0__tar.gz → 1.76.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.76.0}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/__main__.py +11 -6
  4. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/benchmark.py +13 -3
  5. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  6. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +130 -36
  7. sarj_python_lint-1.76.0/src/sarj_python_lint/rules/prefer_grouped_settings.py +137 -0
  8. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +61 -6
  9. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +213 -13
  10. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/.gitignore +0 -0
  11. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/LICENSE +0 -0
  12. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/README.md +0 -0
  13. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/__init__.py +0 -0
  14. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  15. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix.py +0 -0
  16. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  17. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  18. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  19. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  20. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  21. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  22. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  23. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  24. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  25. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  26. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  27. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  28. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  29. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  30. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_file_context.py +0 -0
  31. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_filesystem.py +0 -0
  32. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_fixes.py +0 -0
  33. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_parallel.py +0 -0
  34. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_python_target.py +0 -0
  35. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  36. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_secret_names.py +0 -0
  37. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_source.py +0 -0
  38. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_version.py +0 -0
  39. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/autofix.py +0 -0
  40. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/cast_access.py +0 -0
  41. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  42. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/json_boundary.py +0 -0
  43. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/py.typed +0 -0
  44. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/ratchet.py +0 -0
  45. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rule_base.py +0 -0
  46. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  47. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  48. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  49. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  50. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  51. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  52. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  53. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  54. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  55. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  56. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  57. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  58. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  59. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  60. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  61. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  62. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  63. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  64. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  65. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  66. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  67. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  68. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  69. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  70. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  71. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  72. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  73. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  74. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  75. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  76. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  77. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  78. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  79. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  80. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  81. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  82. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  83. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  84. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  85. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  86. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  87. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  88. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  89. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  90. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  91. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  92. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  93. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  94. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  95. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  96. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  97. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  98. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  99. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  100. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  101. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  102. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  103. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  104. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  105. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  106. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  107. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  108. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  109. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  110. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  113. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  114. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  116. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  117. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  118. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  121. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  122. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  125. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  126. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  127. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  128. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  130. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  134. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  135. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.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.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  143. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  144. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  146. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  147. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  148. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  150. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  152. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  153. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  155. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  157. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  158. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  159. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  165. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  166. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  168. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  169. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  170. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  171. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  172. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  178. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  179. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  180. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  181. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  184. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  185. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  187. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  188. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  192. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  193. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  195. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  196. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  197. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  199. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  200. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  201. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  202. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  204. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  205. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  206. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  207. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  208. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  209. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  213. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  214. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  215. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  216. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  217. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  218. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  219. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  220. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  221. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  222. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  223. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  224. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  225. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  226. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  227. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  228. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  229. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  231. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  232. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  233. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  234. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  237. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  238. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  239. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  240. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  242. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  243. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  244. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  246. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  247. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  248. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  251. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  252. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  253. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  258. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  259. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  260. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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.76.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  264. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  265. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  266. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  267. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  268. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  269. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  270. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  271. {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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.76.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)}"
@@ -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,
@@ -15,6 +15,7 @@ from sarj_python_lint.rule_base import (
15
15
  RuleDocumentation,
16
16
  RuleExample,
17
17
  Severity,
18
+ is_suppressed,
18
19
  )
19
20
  from sarj_python_lint.rules._ast_index import walk, walk as walk_ast
20
21
  from sarj_python_lint.rules._logging import LOG_METHODS, is_logger_expr
@@ -32,8 +33,22 @@ if TYPE_CHECKING:
32
33
  _SQL_RE = re.compile(
33
34
  r"^\s*(?:(?i:(?:select\b.+\bfrom|insert\s+into|update\s+\S+\s+set|delete\s+from|merge\s+into)\b|"
34
35
  r"with\s+\w+\s+as\s*\(|(?:create|alter|drop)\s+(?:table|index|view|schema|type|database)\b|"
35
- r"explain\s+(?:select|insert|update|delete|with)\b)|(?:FROM|JOIN|ORDER\s+BY|GROUP\s+BY|VALUES)\b)",
36
+ r"explain\s+(?:select|insert|update|delete|with)\b)|(?:SELECT|FROM|JOIN|ORDER\s+BY|GROUP\s+BY|VALUES)\b|CAST\s*\()",
36
37
  )
38
+ _SOURCE_FRAGMENT_RE = re.compile(
39
+ r"(?:^|\n)[ \t]*(?:(?:async[ \t]+)?def[ \t]+\w+|class[ \t]+\w+[ \t]*[(:]|"
40
+ r"from[ \t]+[\w.]+[ \t]+import\b|import[ \t]+[\w.]+(?:[ \t]+as[ \t]+\w+)?[ \t]*(?:\n|$)|"
41
+ r"(?:return|yield|raise|assert)[ \t]+\w+[ \t]*\(|"
42
+ r"\#[^\r\n]*(?:\r\n?|\n)|"
43
+ r"(?:\#[ \t]*@generated|--[ \t]*(?:@generated|generated))\b)"
44
+ )
45
+ _CONFIG_FRAGMENT_RE = re.compile(
46
+ r"(?m)^[ \t]*(?:[^\W\d][\w.-]*[ \t]*=|"
47
+ r"\[{1,2}[A-Za-z_][\w.\"'-]*\]{1,2}[ \t]*(?:\n|$)|"
48
+ r"[A-Za-z_][\w.-]*:[ \t]*[\[{])"
49
+ )
50
+ _MAPPING_FIELD_RE = re.compile(r"(?m)^[ \t]*[A-Za-z_][\w.-]*:[ \t]*(?:\S|$)")
51
+ _MIN_CONFIG_FIELDS = 2
37
52
 
38
53
  # Calls returning a lazy translation proxy or an escape-aware SafeString.
39
54
  _LAZY_CALLS = frozenset(
@@ -118,11 +133,16 @@ class _RegexBindings(NamedTuple):
118
133
  scoped_names: frozenset[str]
119
134
 
120
135
 
136
+ class _InterpolationParts(NamedTuple):
137
+ literals: list[str]
138
+ dynamic: list[ast.expr]
139
+
140
+
121
141
  class PreferFstringOverConcat(Rule):
122
142
  id: str = "prefer-fstring-over-concat"
123
143
  code: str = "SARJ068"
124
144
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
125
- default_level=Severity.WARNING,
145
+ default_level=Severity.ERROR,
126
146
  summary="Prefer f-strings for short human-readable interpolation when they make the result clearer.",
127
147
  rationale="For prose-like strings, f-strings keep interpolated values and surrounding text visible together.",
128
148
  remediation=(
@@ -136,12 +156,13 @@ class PreferFstringOverConcat(Rule):
136
156
  limitations=(
137
157
  "Safe fixes require every runtime operand to be an explicit unshadowed builtin str, repr or ascii conversion of a proven exact native primitive. Bare strings, subclasses, custom operators and formatting contracts remain review-only.",
138
158
  "Runtime operands must have concrete string evidence.",
159
+ "Unique literal-string bindings and aliases supply surrounding text, including module prefixes. Rebinding, global/nonlocal writers, conditional assignments, imported values, and unresolved scopes are not inferred.",
139
160
  "String evidence alone does not prove custom concatenation and formatting equivalence; only exact-native-primitive builtin-conversion subsets are fixed.",
140
161
  (
141
- "Logging, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long "
162
+ "Logging, source and configuration fragments, generated headers, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long "
142
163
  "literals, and punctuation-oriented fragment composition are excluded."
143
164
  ),
144
- "The warning is stylistic and intentionally leaves URL, path, protocol, and declarative fragments alone.",
165
+ "The policy is stylistic and intentionally leaves URL, path, protocol and declarative fragments alone. Stable aliases of join-built strings retain the fragment-composition exclusion; unresolved or rebound values are not inferred.",
145
166
  ),
146
167
  examples=(
147
168
  RuleExample(
@@ -170,6 +191,36 @@ class PreferFstringOverConcat(Rule):
170
191
  expected_count=0,
171
192
  public=True,
172
193
  ),
194
+ RuleExample(
195
+ example_id="named-literal-prefix",
196
+ scenario="literal-prefix",
197
+ title="A literal prefix still participates in interpolation",
198
+ outcome=ExampleOutcome.MATCH,
199
+ files=(
200
+ ExampleFile.python(
201
+ "src/render.py",
202
+ 'PREFIX = "Question: "\ndef render(value: str) -> str:\n return PREFIX + value\n',
203
+ ),
204
+ ),
205
+ focus_path=PurePosixPath("src/render.py"),
206
+ expected_count=1,
207
+ public=True,
208
+ ),
209
+ RuleExample(
210
+ example_id="formatted-literal-prefix",
211
+ scenario="literal-prefix",
212
+ title="The prefix and value share one f-string",
213
+ outcome=ExampleOutcome.NO_MATCH,
214
+ files=(
215
+ ExampleFile.python(
216
+ "src/render.py",
217
+ 'PREFIX = "Question: "\ndef render(value: str) -> str:\n return f"{PREFIX}{value}"\n',
218
+ ),
219
+ ),
220
+ focus_path=PurePosixPath("src/render.py"),
221
+ expected_count=0,
222
+ public=True,
223
+ ),
173
224
  RuleExample(
174
225
  example_id="safe-autofix-subset",
175
226
  title="A proven subset has a convergent automatic fix",
@@ -218,12 +269,14 @@ class PreferFstringOverConcat(Rule):
218
269
  for node in adds:
219
270
  if id(node) in inner or id(node) in excluded:
220
271
  continue
272
+ if is_suppressed(context.source_lines, node.lineno, self.code):
273
+ continue
221
274
  if not _in_callable_scope(node, parents):
222
275
  continue
223
276
  if module_evidence is None:
224
277
  shadowed = _shadowed_producers(tree.body, None)
225
278
  module_evidence = _StringEvidence(frozenset(), shadowed, _imported_modules(tree.body, None) - shadowed)
226
- message = _verdict(node, _string_evidence(node, parents, module_evidence, scope_shadowing))
279
+ message = _verdict(node, _string_evidence(node, parents, module_evidence, scope_shadowing), context)
227
280
  if message is None:
228
281
  continue
229
282
  diags.append(
@@ -232,7 +285,7 @@ class PreferFstringOverConcat(Rule):
232
285
  line=node.lineno,
233
286
  col=node.col_offset + 1,
234
287
  code=self.code,
235
- severity=Severity.WARNING,
288
+ severity=Severity.ERROR,
236
289
  message=message,
237
290
  )
238
291
  )
@@ -353,20 +406,13 @@ def _in_callable_scope(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> boo
353
406
  return False
354
407
 
355
408
 
356
- def _verdict(node: ast.BinOp, evidence: _StringEvidence) -> str | None:
357
- literals: list[str] = []
358
- dynamic: list[ast.expr] = []
409
+ def _verdict(node: ast.BinOp, evidence: _StringEvidence, context: PythonFileContext) -> str | None:
359
410
  operands = _flatten(node)
360
- for operand in operands:
361
- if isinstance(operand, ast.Constant):
362
- if not isinstance(operand.value, str):
363
- # A bytes/int/float literal is positive evidence this `+` is not
364
- # string concatenation at all.
365
- return None
366
- literals.append(operand.value)
367
- else:
368
- dynamic.append(operand)
369
- if _unsupported_interpolation_operands(literals, dynamic, evidence):
411
+ parts = _interpolation_parts(operands, evidence, context)
412
+ if parts is None:
413
+ return None
414
+ literals, dynamic = parts
415
+ if _unsupported_interpolation_operands(literals, dynamic, evidence) or _has_bound_join(dynamic, context):
370
416
  return None
371
417
  if len(operands) == _TERMINATOR_OPERANDS and all(not text.strip() for text in literals):
372
418
  return None
@@ -389,12 +435,70 @@ def _verdict(node: ast.BinOp, evidence: _StringEvidence) -> str | None:
389
435
  return message
390
436
 
391
437
 
438
+ def _interpolation_parts(
439
+ operands: list[ast.expr], evidence: _StringEvidence, context: PythonFileContext
440
+ ) -> _InterpolationParts | None:
441
+ literals: list[str] = []
442
+ dynamic: list[ast.expr] = []
443
+ inline_literal = any(isinstance(operand, ast.Constant) and isinstance(operand.value, str) for operand in operands)
444
+ for operand in operands:
445
+ literal = (
446
+ None
447
+ if inline_literal and not isinstance(operand, ast.Constant) and _is_string_expr(operand, evidence)
448
+ else _literal_string(operand, context)
449
+ )
450
+ if literal is not None:
451
+ literals.append(literal)
452
+ elif isinstance(operand, ast.Constant):
453
+ # A bytes/int/float literal is positive evidence this `+` is not
454
+ # string concatenation at all.
455
+ return None
456
+ else:
457
+ dynamic.append(operand)
458
+ return _InterpolationParts(literals, dynamic)
459
+
460
+
461
+ def _literal_string(expression: ast.expr, context: PythonFileContext) -> str | None:
462
+ value = _bound_value(expression, context)
463
+ return value.value if isinstance(value, ast.Constant) and isinstance(value.value, str) else None
464
+
465
+
466
+ def _bound_value(expression: ast.expr, context: PythonFileContext) -> ast.expr | None:
467
+ seen: set[ast.AST] = set()
468
+ while isinstance(expression, ast.Name):
469
+ provenance = context.static_resource_provenance
470
+ if provenance.dynamic_namespace:
471
+ return None
472
+ binding = provenance.runtime_binding(expression.id, expression)
473
+ if not isinstance(binding, ast.Name) or binding in seen:
474
+ return None
475
+ seen.add(binding)
476
+ value = provenance.assignment_value(binding, expression)
477
+ if value is None:
478
+ return None
479
+ expression = value
480
+ return expression
481
+
482
+
483
+ def _has_bound_join(dynamic: list[ast.expr], context: PythonFileContext) -> bool:
484
+ if not any(_is_join_call(call) for call in context.nodes(ast.Call)):
485
+ return False
486
+ return any(_is_join_call(_bound_value(expression, context)) for expression in dynamic)
487
+
488
+
392
489
  def _is_structural_fragment(literals: list[str]) -> bool:
490
+ if any(
491
+ _SOURCE_FRAGMENT_RE.search(text)
492
+ or _CONFIG_FRAGMENT_RE.search(text)
493
+ or len(_MAPPING_FIELD_RE.findall(text)) >= _MIN_CONFIG_FIELDS
494
+ for text in literals
495
+ ):
496
+ return True
393
497
  substantive = "".join(literals)
394
498
  return bool(substantive) and (
395
499
  "\\" in substantive
396
500
  or any(marker in substantive for marker in ("(?:", "(?=", "(?!", ".*", ".+", "^", "$"))
397
- or not any(character.isalpha() or character.isspace() for character in substantive)
501
+ or not any(character.isalpha() for character in substantive)
398
502
  )
399
503
 
400
504
 
@@ -463,7 +567,7 @@ def _unsupported_interpolation_operands(
463
567
  return bool(any(isinstance(expr, ast.IfExp | ast.BoolOp) for expr in dynamic))
464
568
 
465
569
 
466
- def _is_join_call(expr: ast.expr) -> bool:
570
+ def _is_join_call(expr: ast.expr | None) -> bool:
467
571
  return isinstance(expr, ast.Call) and isinstance(expr.func, ast.Attribute) and expr.func.attr == "join"
468
572
 
469
573
 
@@ -573,9 +677,11 @@ def _string_evidence(
573
677
 
574
678
 
575
679
  def _annotation_is_string(annotation: ast.expr, shadowed: frozenset[str]) -> bool:
576
- if isinstance(annotation, ast.Name):
577
- return annotation.id == "str" and "str" not in shadowed
578
- return False
680
+ match annotation:
681
+ case ast.Name(id="str") | ast.Constant(value="str"):
682
+ return "str" not in shadowed
683
+ case _:
684
+ return False
579
685
 
580
686
 
581
687
  def _is_string_expr(expr: ast.expr, evidence: _StringEvidence) -> bool:
@@ -681,18 +787,6 @@ def _collect_string_parameters(
681
787
  for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs):
682
788
  if arg.annotation is not None and _annotation_is_string(arg.annotation, shadowed):
683
789
  known.add(arg.arg)
684
- if (
685
- args.vararg is not None
686
- and args.vararg.annotation is not None
687
- and _annotation_is_string(args.vararg.annotation, shadowed)
688
- ):
689
- known.add(args.vararg.arg)
690
- if (
691
- args.kwarg is not None
692
- and args.kwarg.annotation is not None
693
- and _annotation_is_string(args.kwarg.annotation, shadowed)
694
- ):
695
- known.add(args.kwarg.arg)
696
790
 
697
791
 
698
792
  def _update_string_bindings(
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import defaultdict
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ ProjectRule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ is_suppressed,
19
+ )
20
+ from sarj_python_lint.rules._declared_contracts import SETTINGS_BASES, DeclaredContracts
21
+ from sarj_python_lint.rules._paths import is_test_path
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from sarj_python_lint._file_context import PythonFileContext
26
+
27
+
28
+ _MIN_GROUP_FIELDS = 2
29
+ _MODIFIER_PREFIXES = frozenset({"is", "has", "min", "max", "num", "total", "default", "enable", "disable"})
30
+
31
+
32
+ @final
33
+ class PreferGroupedSettings(ProjectRule):
34
+ id = "prefer-grouped-settings"
35
+ code = "SARJ532"
36
+ documentation = RuleDocumentation(
37
+ default_level=Severity.WARNING,
38
+ summary="Group settings fields that share a Python namespace prefix into a nested settings object.",
39
+ rationale=(
40
+ "Repeated namespace prefixes flatten one component's configuration across the parent settings API. "
41
+ "A focused settings object gives that component an explicit boundary."
42
+ ),
43
+ remediation=(
44
+ "Move the related fields into a focused nested settings object, normally constructed with "
45
+ "Field(default_factory=...). Preserve existing environment aliases, prefixes, validation, defaults "
46
+ "and source precedence, and verify real environment loading before changing the settings API."
47
+ ),
48
+ category=RuleCategory.ARCHITECTURE,
49
+ autofix=AutofixPolicy.NONE,
50
+ limitations=(
51
+ "Only source-proven Pydantic BaseSettings classes and directly owned public fields are checked. Two distinct fields must share the first complete snake-case namespace token; character prefixes, environment aliases, inherited fields and unrelated data classes do not establish a group.",
52
+ "Boolean and quantitative modifiers such as is, has, min, max, num, total and default do not establish component namespaces. A prefix already present as a field is left for explicit API review.",
53
+ "ClassVar, Final, private attributes, model_config, computed fields, tests, generated code, unknown decorators and unresolved bases are excluded. Each namespace produces one warning, anchored at its first field; an exact class or first-field suppression is honored.",
54
+ "There is no automatic rewrite: nesting may change environment-variable decoding, aliases, source precedence and consumer access paths.",
55
+ ),
56
+ examples=(
57
+ RuleExample(
58
+ example_id="flat-component-settings",
59
+ title="Related settings expose a repeated component namespace",
60
+ outcome=ExampleOutcome.MATCH,
61
+ files=(
62
+ ExampleFile.python(
63
+ "app/settings.py",
64
+ "from pydantic_settings import BaseSettings\n"
65
+ "class Settings(BaseSettings):\n"
66
+ " embedder_url: str\n"
67
+ " embedder_token: str\n",
68
+ ),
69
+ ),
70
+ focus_path=PurePosixPath("app/settings.py"),
71
+ expected_count=1,
72
+ public=True,
73
+ ),
74
+ RuleExample(
75
+ example_id="nested-component-settings",
76
+ title="The component owns its settings and environment prefix",
77
+ outcome=ExampleOutcome.NO_MATCH,
78
+ files=(
79
+ ExampleFile.python(
80
+ "app/settings.py",
81
+ "from pydantic import Field\n"
82
+ "from pydantic_settings import BaseSettings, SettingsConfigDict\n"
83
+ "class EmbedderSettings(BaseSettings):\n"
84
+ " model_config = SettingsConfigDict(env_prefix='EMBEDDER_')\n"
85
+ " url: str\n"
86
+ " token: str\n"
87
+ "class Settings(BaseSettings):\n"
88
+ " embedder: EmbedderSettings = Field(default_factory=EmbedderSettings)\n",
89
+ ),
90
+ ),
91
+ focus_path=PurePosixPath("app/settings.py"),
92
+ expected_count=0,
93
+ public=True,
94
+ ),
95
+ ),
96
+ )
97
+ description = documentation.summary
98
+
99
+ @override
100
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
101
+ if context.tree is None or context.generated or is_test_path(context.path):
102
+ return []
103
+ contracts = DeclaredContracts(context)
104
+ findings: list[Diagnostic] = []
105
+ for declaration in context.tree.body:
106
+ if not isinstance(declaration, ast.ClassDef):
107
+ continue
108
+ model = contracts.class_type(ast.Name(id=declaration.name))
109
+ if model is None or not contracts.inherits(model, SETTINGS_BASES):
110
+ continue
111
+ if is_suppressed(context.source_lines, declaration.lineno, self.code):
112
+ continue
113
+ for prefix, members in _settings_groups(contracts.model_fields(model)).items():
114
+ first = next(iter(members.values()))
115
+ if is_suppressed(context.source_lines, first.lineno, self.code):
116
+ continue
117
+ names = ", ".join(f"`{name}`" for name in members)
118
+ findings.append(
119
+ Diagnostic(
120
+ context.path,
121
+ first.lineno,
122
+ first.col_offset + 1,
123
+ self.code,
124
+ f"Settings fields {names} share the `{prefix}_` namespace; group them under a focused `{prefix}` settings object while preserving environment loading.",
125
+ Severity.WARNING,
126
+ )
127
+ )
128
+ return findings
129
+
130
+
131
+ def _settings_groups(fields: dict[str, ast.AnnAssign]) -> dict[str, dict[str, ast.AnnAssign]]:
132
+ groups: dict[str, dict[str, ast.AnnAssign]] = defaultdict(dict)
133
+ for name, field in fields.items():
134
+ prefix, separator, tail = name.partition("_")
135
+ if separator and tail and prefix not in _MODIFIER_PREFIXES and prefix not in fields:
136
+ groups[prefix][name] = field
137
+ return {prefix: members for prefix, members in groups.items() if len(members) >= _MIN_GROUP_FIELDS}