sarj-python-lint 1.66.3__tar.gz → 1.68.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 (249) hide show
  1. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/pyproject.toml +1 -1
  3. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/_declared_contracts.py +243 -0
  4. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_registry.py +14 -0
  5. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_resource_provenance.py +4 -0
  6. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_composition.py +8 -2
  7. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +174 -0
  8. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/excessive_commentary.py +36 -14
  9. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +83 -0
  10. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +7 -3
  11. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_precise_object_contract.py +220 -0
  12. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +192 -0
  13. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +154 -0
  14. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_typed_settings_field.py +199 -0
  15. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +18 -2
  16. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +190 -0
  17. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +7 -7
  18. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/.gitignore +0 -0
  19. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/LICENSE +0 -0
  20. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/README.md +0 -0
  21. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/__init__.py +0 -0
  22. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/__main__.py +0 -0
  23. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  24. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_file_context.py +0 -0
  25. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_filesystem.py +0 -0
  26. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_python_target.py +0 -0
  27. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  28. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_secret_names.py +0 -0
  29. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_source.py +0 -0
  30. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_version.py +0 -0
  31. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/benchmark.py +0 -0
  32. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/cast_access.py +0 -0
  33. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  34. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/json_boundary.py +0 -0
  35. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/py.typed +0 -0
  36. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/ratchet.py +0 -0
  37. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rule_base.py +0 -0
  38. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  39. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  40. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  41. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  42. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  43. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  44. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  45. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  46. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  47. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  48. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  49. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  50. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  51. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  52. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  53. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  54. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  55. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  56. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  57. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  58. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  59. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  60. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  61. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  62. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  63. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  64. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  65. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  66. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  67. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  68. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  69. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  70. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  71. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  72. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  73. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  74. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  75. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  76. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  77. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  78. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  79. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  80. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  81. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  82. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  83. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  84. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  85. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  86. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  87. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  88. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  89. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  90. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  91. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  92. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  93. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  94. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  95. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  96. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  97. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  98. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  99. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  100. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  101. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  102. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  103. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  104. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  105. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  106. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  107. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  108. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  109. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  110. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  111. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  112. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  113. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  114. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  115. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  116. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  117. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  118. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  119. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  120. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  121. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  122. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  123. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  124. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  125. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  126. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  127. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  128. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  129. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  130. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  131. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  132. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  133. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  134. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  135. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  136. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  137. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  138. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  139. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  140. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  141. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  142. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  143. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  144. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  145. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  146. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  147. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  148. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  149. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  150. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  151. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  152. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  153. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  154. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  155. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  156. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  157. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  158. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  159. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  160. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  161. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  162. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  163. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  164. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  165. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  166. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  167. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  168. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  169. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  170. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  171. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  172. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  173. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  174. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  175. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  176. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  177. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  178. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  179. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  180. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  181. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  182. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  183. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  184. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  185. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  186. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  187. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  188. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  189. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  190. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  191. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  192. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  193. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  194. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  195. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  196. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  197. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  198. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  199. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  200. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  201. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  202. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  203. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  204. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  205. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  206. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  207. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  208. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  209. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  210. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  211. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  212. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  213. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  214. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  215. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  216. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  217. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  218. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  219. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  220. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  221. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  222. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  223. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  224. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  225. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  226. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  227. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  228. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  229. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  230. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  231. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  232. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  233. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  234. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  235. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  236. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  237. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  238. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  239. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  240. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  241. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  242. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  243. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  244. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  245. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  246. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  247. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  248. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  249. {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.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.66.3
3
+ Version: 1.68.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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.66.3"
3
+ version = "1.68.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" }]
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+ from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+ from sarj_python_lint._file_context import PythonFileContext
16
+
17
+
18
+ MODEL_BASES = frozenset({"pydantic.BaseModel", "pydantic.main.BaseModel"})
19
+ SETTINGS_BASES = frozenset({"pydantic_settings.BaseSettings", "pydantic_settings.main.BaseSettings"})
20
+ _MAX_DEPTH = 8
21
+ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class DeclaredClass:
26
+ unit: SourceUnit
27
+ declaration: ast.ClassDef
28
+
29
+
30
+ def _class_metadata_annotation(annotation: ast.expr, semantics: AnnotationSemantics) -> bool:
31
+ pending = [annotation]
32
+ seen: set[str] = set()
33
+ while pending:
34
+ node = semantics.parse(pending.pop())
35
+ if node is None:
36
+ return True
37
+ for member in ast.walk(node):
38
+ if isinstance(member, ast.Subscript) and semantics.imports.resolved_symbol(
39
+ member.value, sources=_TYPING_SOURCES
40
+ ) in {"ClassVar", "Final"}:
41
+ return True
42
+ if isinstance(member, ast.Name) and member.id in semantics.aliases and member.id not in seen:
43
+ seen.add(member.id)
44
+ pending.append(semantics.aliases[member.id])
45
+ return False
46
+
47
+
48
+ class DeclaredContracts:
49
+ def __init__(self, context: PythonFileContext) -> None:
50
+ self.context = context
51
+ self._field_cache: dict[ast.ClassDef, dict[str, ast.AnnAssign]] = {}
52
+ self._module_annotations: dict[Path, AnnotationSemantics] = {}
53
+ self.project = context.session.project or ProjectIndexSet.single(context.path, context.source)
54
+ self.unit = self.project.unit(context.path)
55
+ if self.unit is not None and self.unit.tree is not context.tree:
56
+ self.unit = SourceUnit(self.unit.path, self.unit.module, self.unit.source, context.tree, self.unit.imports)
57
+ self.project = self.project.with_unit(self.unit)
58
+ if self.unit is not None and self.unit.module is None:
59
+ self.unit = SourceUnit(
60
+ context.path,
61
+ "__snippet__",
62
+ context.source,
63
+ context.tree,
64
+ {
65
+ name: SymbolRef(target.module, target.symbol or "")
66
+ for name, target in context.module_imports.bindings.items()
67
+ },
68
+ )
69
+ self.project = self.project.with_unit(self.unit)
70
+
71
+ def class_type(
72
+ self, expression: ast.expr, unit: SourceUnit | None = None, *, at: ast.AST | None = None
73
+ ) -> DeclaredClass | None:
74
+ owner = unit or self.unit
75
+ if owner is None or not self._lookup_allowed(expression, owner, at):
76
+ return None
77
+ summary = self.project.class_for(owner, expression)
78
+ source = self.project.source_unit(summary.symbol.module) if summary is not None else None
79
+ if source is None or source.tree is None or summary is None:
80
+ return None
81
+ declaration = self._stable_declaration(source, summary.symbol.name)
82
+ return DeclaredClass(source, declaration) if declaration is not None else None
83
+
84
+ def _lookup_allowed(self, expression: ast.expr, owner: SourceUnit, at: ast.AST | None) -> bool:
85
+ if owner is not self.unit or (at or expression) not in self.context.parents:
86
+ return True
87
+ root = expression
88
+ while isinstance(root, ast.Attribute):
89
+ root = root.value
90
+ if not isinstance(root, ast.Name):
91
+ return True
92
+ binding = self.context.static_resource_provenance.binding(root.id, at or expression)
93
+ return (
94
+ isinstance(binding, ast.alias | ast.ClassDef)
95
+ and root.id not in self.context.static_resource_provenance.mutated
96
+ )
97
+
98
+ def _stable_declaration(self, source: SourceUnit, name: str) -> ast.ClassDef | None:
99
+ if source.tree is None:
100
+ return None
101
+ declarations = [node for node in source.tree.body if isinstance(node, ast.ClassDef) and node.name == name]
102
+ if len(declarations) != 1 or declarations[0].keywords:
103
+ return None
104
+ if not self._identity_decorators(source, declarations[0]):
105
+ return None
106
+ if any(
107
+ isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Store | ast.Del)
108
+ for statement in source.tree.body
109
+ if not isinstance(statement, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef)
110
+ for node in ast.walk(statement)
111
+ ):
112
+ return None
113
+ return declarations[0]
114
+
115
+ def _identity_decorators(self, source: SourceUnit, declaration: ast.ClassDef) -> bool:
116
+ if not declaration.decorator_list:
117
+ return True
118
+ if source.tree is None:
119
+ return False
120
+ imports = (
121
+ self.context.module_imports
122
+ if source.path == self.context.path
123
+ else ImportIndex.from_tree(source.tree, module_scope_only=True)
124
+ )
125
+ return all(
126
+ imports.resolves(decorator, sources=_TYPING_SOURCES, symbol="final")
127
+ for decorator in declaration.decorator_list
128
+ )
129
+
130
+ def inherits(self, model: DeclaredClass, bases: frozenset[str]) -> bool:
131
+ return self.project.class_inherits_from(model.unit, model.declaration.name, bases)
132
+
133
+ def model_fields(self, model: DeclaredClass) -> dict[str, ast.AnnAssign]:
134
+ if model.unit.tree is None:
135
+ return {}
136
+ cached = self._field_cache.get(model.declaration)
137
+ if cached is not None:
138
+ return cached
139
+ semantics = self._module_semantics(model.unit)
140
+ fields = {
141
+ node.target.id: node
142
+ for node in model.declaration.body
143
+ if isinstance(node, ast.AnnAssign)
144
+ and isinstance(node.target, ast.Name)
145
+ and not node.target.id.startswith("_")
146
+ and node.target.id != "model_config"
147
+ and not _class_metadata_annotation(node.annotation, semantics)
148
+ }
149
+ self._field_cache[model.declaration] = fields
150
+ return fields
151
+
152
+ def _module_semantics(self, unit: SourceUnit) -> AnnotationSemantics:
153
+ if unit.tree is None:
154
+ return self.context.annotation_semantics
155
+ semantics = self._module_annotations.get(unit.path)
156
+ if semantics is None:
157
+ imports = (
158
+ self.context.module_imports
159
+ if unit.path == self.context.path
160
+ else ImportIndex.from_tree(unit.tree, module_scope_only=True)
161
+ )
162
+ semantics = AnnotationSemantics.from_imports(unit.tree, imports)
163
+ self._module_annotations[unit.path] = semantics
164
+ return semantics
165
+
166
+ def annotation(self, expression: ast.expr, unit: SourceUnit | None = None) -> DeclaredClass | None:
167
+ semantics = self.context.annotation_semantics
168
+ if unit is not None and unit is not self.unit and unit.tree is not None:
169
+ semantics = self._module_semantics(unit)
170
+ candidates = [
171
+ model
172
+ for member in semantics.transparent_members(expression)
173
+ if (model := self.class_type(member, unit, at=expression)) is not None
174
+ ]
175
+ return candidates[0] if len(candidates) == 1 else None
176
+
177
+ def value(self, expression: ast.expr, *, depth: int = 0) -> DeclaredClass | None:
178
+ if depth > _MAX_DEPTH:
179
+ return None
180
+ match expression:
181
+ case ast.Await():
182
+ return self.value(expression.value, depth=depth + 1)
183
+ case ast.Name():
184
+ return self._local_value(expression, depth)
185
+ case ast.Call():
186
+ return self.class_type(expression.func) or self._call_result(expression, depth)
187
+ case _:
188
+ return None
189
+
190
+ def _local_value(self, expression: ast.Name, depth: int) -> DeclaredClass | None:
191
+ binding = self.context.static_resource_provenance.binding(expression.id, expression)
192
+ if isinstance(binding, ast.arg) and binding.annotation is not None:
193
+ return self.annotation(binding.annotation)
194
+ parent = self.context.parents.get(binding) if binding is not None else None
195
+ if isinstance(parent, ast.AnnAssign):
196
+ return self.annotation(parent.annotation)
197
+ if isinstance(binding, ast.Name):
198
+ value = self.context.static_resource_provenance.assignment_value(binding, expression)
199
+ return self.value(value, depth=depth + 1) if value is not None else None
200
+ return None
201
+
202
+ def _call_result(self, expression: ast.Call, depth: int) -> DeclaredClass | None:
203
+ match expression.func:
204
+ case ast.Name():
205
+ binding = self.context.static_resource_provenance.binding(expression.func.id, expression)
206
+ if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef) and binding.returns is not None:
207
+ return self.annotation(binding.returns)
208
+ return None
209
+ case ast.Attribute():
210
+ return self._method_result(expression.func, depth)
211
+ case _:
212
+ return None
213
+
214
+ def _method_result(self, target: ast.Attribute, depth: int) -> DeclaredClass | None:
215
+ model = self.class_type(target.value) or self.value(target.value, depth=depth + 1)
216
+ if model is None:
217
+ return None
218
+ if target.attr in {"model_validate", "model_validate_json", "model_copy"} and self.inherits(
219
+ model, MODEL_BASES | SETTINGS_BASES
220
+ ):
221
+ return model
222
+ methods = [
223
+ node
224
+ for node in model.declaration.body
225
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == target.attr
226
+ ]
227
+ if len(methods) == 1 and methods[0].returns is not None and not methods[0].decorator_list:
228
+ return self.annotation(methods[0].returns, model.unit)
229
+ return None
230
+
231
+
232
+ def ordinary_pytest_test(function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
233
+ if not function.name.startswith("test_"):
234
+ return False
235
+ owner = context.parents.get(function)
236
+ if isinstance(owner, ast.ClassDef) and (owner.bases or owner.decorator_list or owner.keywords):
237
+ return False
238
+ for decorator in function.decorator_list:
239
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
240
+ qualified = context.imports.resolved_qualified_name(target)
241
+ if qualified is None or not qualified.startswith("pytest.mark."):
242
+ return False
243
+ return True
@@ -219,6 +219,7 @@ if TYPE_CHECKING:
219
219
  from sarj_python_lint.rules.delete_route_returns_no_content import DeleteRouteReturnsNoContent
220
220
  from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
221
221
  from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
222
+ from sarj_python_lint.rules.duplicate_pytest_fixture_params import DuplicatePytestFixtureParams
222
223
  from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
223
224
  from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
224
225
  from sarj_python_lint.rules.no_create_verb_in_post_path import NoCreateVerbInPostPath
@@ -236,8 +237,14 @@ from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlash
236
237
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
237
238
  from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
238
239
  from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
240
+ from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
239
241
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
242
+ from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
240
243
  from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
244
+ from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
245
+ from sarj_python_lint.rules.prefer_pytest_dependency_fixtures import PreferPytestDependencyFixtures
246
+ from sarj_python_lint.rules.prefer_typed_settings_field import PreferTypedSettingsField
247
+ from sarj_python_lint.rules.pytest_fixture_returns_closed_resource import PytestFixtureReturnsClosedResource
241
248
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
242
249
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
243
250
  from sarj_python_lint.rules.require_exp_on_jwt_decode import RequireExpOnJwtDecode
@@ -247,6 +254,13 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
247
254
 
248
255
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
249
256
  {
257
+ PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
258
+ DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
259
+ PreferTypedSettingsField.id: PreferTypedSettingsField,
260
+ PreferDirectImportModule.id: PreferDirectImportModule,
261
+ PreferPreciseObjectContract.id: PreferPreciseObjectContract,
262
+ PreferPydanticModelAssertion.id: PreferPydanticModelAssertion,
263
+ PreferPytestDependencyFixtures.id: PreferPytestDependencyFixtures,
250
264
  PreferBulkUpsert.id: PreferBulkUpsert,
251
265
  PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
252
266
  StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
@@ -31,6 +31,10 @@ _SCOPES = (
31
31
  )
32
32
  _RESOURCE_OWNERS = MappingProxyType(
33
33
  {
34
+ "builtins": frozenset({"open"}),
35
+ "io": frozenset({"open", "StringIO", "BytesIO"}),
36
+ "tempfile": frozenset({"TemporaryFile", "NamedTemporaryFile"}),
37
+ "httpx": frozenset({"Client", "AsyncClient"}),
34
38
  "asyncio": frozenset({"sleep", "CancelledError", "exceptions.CancelledError"}),
35
39
  "contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
36
40
  "subprocess": frozenset({"Popen", "TimeoutExpired"}),
@@ -65,7 +65,13 @@ def _class_owner(
65
65
  return unit, owner
66
66
 
67
67
 
68
- def constructor(context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet) -> Constructor | None:
68
+ def constructor(
69
+ context: PythonFileContext,
70
+ expression: ast.expr,
71
+ project: ProjectIndexSet,
72
+ *,
73
+ minimum_collaborators: int = _MIN_COLLABORATORS,
74
+ ) -> Constructor | None:
69
75
  resolved = _class_owner(context, expression, project)
70
76
  if resolved is None:
71
77
  return None
@@ -87,7 +93,7 @@ def constructor(context: PythonFileContext, expression: ast.expr, project: Proje
87
93
  and _is_port(project, unit, parameter.annotation)
88
94
  and stored.get(parameter.arg) in invoked
89
95
  )
90
- if len(collaborators) < _MIN_COLLABORATORS:
96
+ if len(collaborators) < minimum_collaborators:
91
97
  return None
92
98
  return Constructor(
93
99
  SymbolRef(unit.module, owner.name),
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._ast_index import walk as walk_ast
20
+ from sarj_python_lint.rules._paths import is_test_path
21
+ from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import fixture_declarations
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from sarj_python_lint._file_context import PythonFileContext
26
+ from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import FixtureDeclaration
27
+
28
+
29
+ _BAD = 'import pytest\n\n@pytest.fixture(params=("empty", "empty"))\ndef payload(request):\n return request.param\n'
30
+
31
+
32
+ @final
33
+ class DuplicatePytestFixtureParams(Rule):
34
+ id: str = "duplicate-pytest-fixture-params"
35
+ code: str = "SARJ524"
36
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
37
+ default_level=Severity.WARNING,
38
+ summary="Review repeated immutable values in a pytest fixture parameter list.",
39
+ rationale=(
40
+ "Each fixture parameter reruns its dependent tests. Repeating the same immutable literal can add "
41
+ "execution time without exercising a distinct input; Ruff PT014 covers test parametrization, not fixture params."
42
+ ),
43
+ remediation=(
44
+ "Review the dependent test contracts, then remove an accidental duplicate. Preserve intentional repetition "
45
+ "with explicit case IDs or an exact SARJ524 suppression explaining the repeated-run contract."
46
+ ),
47
+ category=RuleCategory.TESTING,
48
+ autofix=AutofixPolicy.NONE,
49
+ limitations=(
50
+ "Only directly imported pytest/pytest-asyncio fixtures in test paths with one decorator and literal list/tuple params are analyzed.",
51
+ "Compares immutable scalar/tuple literals with type and floating-point sign preserved; mutable values, calls, aliases and pytest.param metadata are not compared.",
52
+ "Explicit IDs, decorator unpacking and fixtures using request beyond direct request.param loads are excluded.",
53
+ "Dependent fixtures, hooks and tests may observe case indices, generated IDs or repeated execution; findings require contract review and are never auto-deleted.",
54
+ ),
55
+ examples=(
56
+ RuleExample(
57
+ example_id="repeated-fixture-input",
58
+ title="The same input schedules the dependent tests twice",
59
+ outcome=ExampleOutcome.MATCH,
60
+ files=(ExampleFile.python("tests/conftest.py", _BAD),),
61
+ focus_path=PurePosixPath("tests/conftest.py"),
62
+ expected_count=1,
63
+ public=True,
64
+ ),
65
+ RuleExample(
66
+ example_id="distinct-fixture-inputs",
67
+ title="Each fixture parameter represents a distinct input",
68
+ outcome=ExampleOutcome.NO_MATCH,
69
+ files=(ExampleFile.python("tests/conftest.py", _BAD.replace('"empty", "empty"', '"empty", "filled"')),),
70
+ focus_path=PurePosixPath("tests/conftest.py"),
71
+ expected_count=0,
72
+ public=True,
73
+ ),
74
+ ),
75
+ )
76
+ description: str = documentation.summary
77
+
78
+ @override
79
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
80
+ if (
81
+ not is_test_path(context.path)
82
+ or context.generated
83
+ or "params" not in context.symbol_source
84
+ or context.tree is None
85
+ ):
86
+ return []
87
+ findings: list[Diagnostic] = []
88
+ for fixture in fixture_declarations(context.tree):
89
+ params = _parameter_values(fixture, context)
90
+ if params is None:
91
+ continue
92
+ first_values: dict[str, ast.expr] = {}
93
+ for value in params.elts:
94
+ key = _literal_key(value)
95
+ if key is None:
96
+ continue
97
+ first = first_values.setdefault(key, value)
98
+ if first is value or is_suppressed(context.source_lines, value.lineno, self.code):
99
+ continue
100
+ findings.append(
101
+ Diagnostic(
102
+ path=context.path,
103
+ line=value.lineno,
104
+ col=value.col_offset + 1,
105
+ code=self.code,
106
+ severity=Severity.WARNING,
107
+ message=(
108
+ f"fixture `{fixture.name}` repeats the immutable parameter from line {first.lineno}; "
109
+ "review dependent contracts before removing it, or give intentional repetition explicit IDs"
110
+ ),
111
+ )
112
+ )
113
+ return findings
114
+
115
+
116
+ def _parameter_values(fixture: FixtureDeclaration, context: PythonFileContext) -> ast.List | ast.Tuple | None:
117
+ decorator = fixture.decorator
118
+ if (
119
+ len(fixture.node.decorator_list) != 1
120
+ or not isinstance(decorator, ast.Call)
121
+ or decorator.args
122
+ or any(keyword.arg is None for keyword in decorator.keywords)
123
+ or _observes_request_identity(fixture.node, context)
124
+ ):
125
+ return None
126
+ keywords = {keyword.arg: keyword.value for keyword in decorator.keywords}
127
+ if len(keywords) != len(decorator.keywords):
128
+ return None
129
+ ids = keywords.get("ids")
130
+ if ids is not None and not (isinstance(ids, ast.Constant) and ids.value is None):
131
+ return None
132
+ params = keywords.get("params")
133
+ return params if isinstance(params, ast.List | ast.Tuple) else None
134
+
135
+
136
+ def _literal_key(value: ast.expr) -> str | None:
137
+ if isinstance(value, ast.Constant) and isinstance(value.value, str | bytes | bool | int | float | type(None)):
138
+ return _scalar_key(value=value.value)
139
+ if (
140
+ isinstance(value, ast.UnaryOp)
141
+ and isinstance(value.op, ast.UAdd | ast.USub)
142
+ and isinstance(value.operand, ast.Constant)
143
+ and isinstance(value.operand.value, int | float)
144
+ and not isinstance(value.operand.value, bool)
145
+ ):
146
+ number = value.operand.value
147
+ number = -number if isinstance(value.op, ast.USub) else +number
148
+ return _scalar_key(value=number)
149
+ if isinstance(value, ast.Tuple):
150
+ keys = tuple(_literal_key(item) for item in value.elts)
151
+ if all(key is not None for key in keys):
152
+ return "tuple:" + "".join(f"{len(key)}:{key}" for key in keys if key is not None)
153
+ return None
154
+
155
+
156
+ def _scalar_key(*, value: str | bytes | float | None) -> str:
157
+ if isinstance(value, int) and not isinstance(value, bool):
158
+ return f"int:{value:x}"
159
+ return f"{type(value).__name__}:{value!r}"
160
+
161
+
162
+ def _observes_request_identity(node: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
163
+ for child in walk_ast(node):
164
+ if isinstance(child, ast.Name) and child.id == "request":
165
+ parent = context.parents.get(child)
166
+ if not (
167
+ isinstance(child.ctx, ast.Load)
168
+ and isinstance(parent, ast.Attribute)
169
+ and parent.value is child
170
+ and parent.attr == "param"
171
+ and isinstance(parent.ctx, ast.Load)
172
+ ):
173
+ return True
174
+ return False
@@ -14,8 +14,10 @@ from sarj_python_lint.rule_base import (
14
14
  RuleCategory,
15
15
  RuleDocumentation,
16
16
  RuleExample,
17
+ Severity,
17
18
  )
18
19
  from sarj_python_lint.rules._ast_index import walk as walk_ast
20
+ from sarj_python_lint.rules._comments import has_external_reference
19
21
  from sarj_python_lint.rules._prose_budget import (
20
22
  groups,
21
23
  has_technical_anchor,
@@ -27,8 +29,10 @@ if TYPE_CHECKING:
27
29
  from sarj_python_lint.rules._ast_index import NodeIndex
28
30
 
29
31
 
30
- _MIN_LINES = 4
31
- _MIN_WORDS = 28
32
+ _MIN_LINES = 3
33
+ _MIN_WORDS = 20
34
+ _ERROR_MIN_LINES = 4
35
+ _ERROR_MIN_WORDS = 28
32
36
  _RATIONALE_RE = re.compile(
33
37
  r"\b(?:because|otherwise|therefore|must|never|cannot|can't|required?|invariant|"
34
38
  r"compatibility|security|race|atomic|deadlock|rollback|lock|data loss)\b",
@@ -45,21 +49,24 @@ class ExcessiveCommentary(Rule):
45
49
  id = "excessive-commentary"
46
50
  code = "SARJ434"
47
51
  documentation = RuleDocumentation(
48
- summary="Long standalone implementation commentary — make the code self-documenting and retain only durable constraints.",
52
+ summary="Review long implementation comments; express behavior through code and keep constraints concise.",
49
53
  rationale=(
50
54
  "A paragraph that narrates nearby implementation behavior competes with the code and can drift independently "
51
55
  "from it."
52
56
  ),
53
57
  remediation=(
54
- "Delete the narration and clarify names, types, or structure. Keep concise comments that record a durable "
55
- "constraint or externally owned contract."
58
+ "Make behavior clear through names, types, validators and cohesive functions. Shorten remaining explanation "
59
+ "to a concise constraint or protocol reference. Keep necessary detail with an explained exact-code "
60
+ "exception; never delete a required constraint or create abstraction solely to satisfy this warning."
56
61
  ),
57
62
  category=RuleCategory.MAINTAINABILITY,
58
63
  autofix=AutofixPolicy.NONE,
59
64
  limitations=(
60
- "Only contiguous standalone line-comment blocks with at least four non-empty lines and 28 words are inspected.",
61
- "The paragraph must immediately precede an empty local collection that is then populated by at least two consecutive accumulator calls.",
62
- "Generated files, directives, licenses, structured lists, docstrings, inline comments, rationale markers, and concrete technical anchors are excluded.",
65
+ "Contiguous standalone implementation comment blocks with at least three non-empty lines and 20 words are inspected.",
66
+ "The paragraph must immediately precede executable implementation code; narrative comments in tests and module setup are included.",
67
+ "The previously shipped accumulator pattern remains an error; expanded implementation commentary is a warning requiring review, with no automatic deletion.",
68
+ "Rationale, technical vocabulary and external citations do not exempt a long paragraph from warning-level review. Their existing protection against accumulator errors remains intact.",
69
+ "Generated files, directives, licenses, structured lists, docstrings, inline comments and short constraints are excluded.",
63
70
  ),
64
71
  examples=(
65
72
  RuleExample(
@@ -111,24 +118,31 @@ class ExcessiveCommentary(Rule):
111
118
  tree = context.tree
112
119
  if tree is None:
113
120
  return []
121
+ implementation_lines = {
122
+ node.lineno
123
+ for node in context.nodes(ast.stmt)
124
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Import, ast.ImportFrom))
125
+ }
114
126
  accumulator_lines = _narrated_accumulator_lines(tree, node_index=context.node_index)
115
127
  findings: list[Diagnostic] = []
116
128
  for group in groups(path, source):
117
129
  lines = tuple(stripped for line in group.text.splitlines() if (stripped := line.strip()))
118
- if group.kind != "comment" or len(lines) < _MIN_LINES or len(group.text.split()) < _MIN_WORDS:
130
+ words = len(group.text.split())
131
+ if group.kind != "comment" or len(lines) < _MIN_LINES or words < _MIN_WORDS:
119
132
  continue
120
133
  comment_end_line = group.line + len(group.text.splitlines()) - 1
121
- if comment_end_line + 1 not in accumulator_lines:
134
+ if comment_end_line + 1 not in implementation_lines:
122
135
  continue
123
- if any(
136
+ if _BULLET_RE.search(group.text):
137
+ continue
138
+ review_only = any(
124
139
  (
125
- _BULLET_RE.search(group.text),
126
140
  has_technical_anchor(group.text),
141
+ has_external_reference(group.text),
127
142
  _RATIONALE_RE.search(group.text),
128
143
  _RATIONALE_SO_RE.search(group.text),
129
144
  )
130
- ):
131
- continue
145
+ )
132
146
  findings.append(
133
147
  Diagnostic(
134
148
  path,
@@ -136,6 +150,14 @@ class ExcessiveCommentary(Rule):
136
150
  group.col,
137
151
  self.code,
138
152
  self.description,
153
+ severity=(
154
+ Severity.ERROR
155
+ if not review_only
156
+ and comment_end_line + 1 in accumulator_lines
157
+ and len(lines) >= _ERROR_MIN_LINES
158
+ and words >= _ERROR_MIN_WORDS
159
+ else Severity.WARNING
160
+ ),
139
161
  column_encoding=group.column_encoding,
140
162
  )
141
163
  )