sarj-python-lint 1.67.0__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.67.0 → sarj_python_lint-1.68.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.67.0 → 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.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_registry.py +10 -0
  5. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_composition.py +8 -2
  6. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/excessive_commentary.py +36 -14
  7. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +83 -0
  8. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +7 -3
  9. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_precise_object_contract.py +220 -0
  10. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +192 -0
  11. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +154 -0
  12. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_typed_settings_field.py +199 -0
  13. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +7 -7
  14. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/.gitignore +0 -0
  15. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/LICENSE +0 -0
  16. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/README.md +0 -0
  17. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/__init__.py +0 -0
  18. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/__main__.py +0 -0
  19. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  20. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_file_context.py +0 -0
  21. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_filesystem.py +0 -0
  22. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_python_target.py +0 -0
  23. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  24. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_secret_names.py +0 -0
  25. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_source.py +0 -0
  26. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_version.py +0 -0
  27. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/benchmark.py +0 -0
  28. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/cast_access.py +0 -0
  29. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  30. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/json_boundary.py +0 -0
  31. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/py.typed +0 -0
  32. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/ratchet.py +0 -0
  33. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rule_base.py +0 -0
  34. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  35. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  36. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  37. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  38. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  39. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  40. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  41. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  42. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  43. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  44. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  45. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  46. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  47. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  48. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  49. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  50. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  51. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  52. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  53. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  54. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  55. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  56. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  57. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  58. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  59. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  60. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  61. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  62. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  63. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  64. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  65. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  66. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  67. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  68. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  69. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  70. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  71. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  72. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  73. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  74. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  75. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  76. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  77. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  78. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  79. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  80. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  81. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  82. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  83. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  84. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  85. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  86. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  87. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  88. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  89. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  90. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  91. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  92. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  93. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  94. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  95. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  96. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  97. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  98. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  99. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  100. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  101. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  102. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  103. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  104. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  105. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  106. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  107. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  108. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  109. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  110. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  111. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  112. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  113. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  114. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  115. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  116. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  117. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  118. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  119. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  120. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  121. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  122. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  123. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  124. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  125. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  126. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  127. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  128. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  129. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  130. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  131. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  132. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  133. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  134. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  135. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  136. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  137. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  138. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  139. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  140. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  141. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  142. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  143. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  144. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  145. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  146. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  147. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  148. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  149. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  150. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  151. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  152. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  153. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  154. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  155. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  156. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  157. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  158. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  159. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  160. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  161. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  162. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  163. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  164. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  165. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  166. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  167. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  168. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  169. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  170. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  171. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  172. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  173. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  174. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  175. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  176. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  177. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  178. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  179. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  180. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  181. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  182. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  183. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  184. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  185. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  186. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  187. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  188. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  189. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  190. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  191. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  192. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  193. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  194. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  195. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  196. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  197. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  198. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  199. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  200. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  201. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  202. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  203. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  204. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  205. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  206. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  207. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  208. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  209. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  210. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  211. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  212. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  213. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  214. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  215. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  216. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  217. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  218. {sarj_python_lint-1.67.0 → 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.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  220. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  221. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  222. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  223. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  224. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  225. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  226. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  227. {sarj_python_lint-1.67.0 → 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.67.0 → 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.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  230. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  231. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  232. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  233. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  234. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  235. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  236. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  237. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  238. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  239. {sarj_python_lint-1.67.0 → 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.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  241. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  242. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  243. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  244. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  245. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  246. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  247. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  248. {sarj_python_lint-1.67.0 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  249. {sarj_python_lint-1.67.0 → 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.67.0
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.67.0"
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
@@ -237,8 +237,13 @@ from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlash
237
237
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
238
238
  from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
239
239
  from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
240
+ from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
240
241
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
242
+ from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
241
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
242
247
  from sarj_python_lint.rules.pytest_fixture_returns_closed_resource import PytestFixtureReturnsClosedResource
243
248
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
244
249
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
@@ -251,6 +256,11 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
251
256
  {
252
257
  PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
253
258
  DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
259
+ PreferTypedSettingsField.id: PreferTypedSettingsField,
260
+ PreferDirectImportModule.id: PreferDirectImportModule,
261
+ PreferPreciseObjectContract.id: PreferPreciseObjectContract,
262
+ PreferPydanticModelAssertion.id: PreferPydanticModelAssertion,
263
+ PreferPytestDependencyFixtures.id: PreferPytestDependencyFixtures,
254
264
  PreferBulkUpsert.id: PreferBulkUpsert,
255
265
  PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
256
266
  StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
@@ -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),
@@ -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
  )
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, 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
+ )
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from sarj_python_lint._file_context import PythonFileContext
22
+
23
+
24
+ @final
25
+ class PreferDirectImportModule(Rule):
26
+ id = "prefer-direct-import-module"
27
+ code = "SARJ529"
28
+ documentation = RuleDocumentation(
29
+ default_level=Severity.WARNING,
30
+ summary="Import import_module directly instead of accessing importlib.import_module.",
31
+ rationale="A focused import makes the loader dependency explicit and removes repeated module qualification.",
32
+ remediation="Use 'from importlib import import_module' and annotate importer callables with 'Callable[[str], ModuleType]'. Keep intentional module patching behind an explained local exception.",
33
+ category=RuleCategory.STYLE,
34
+ autofix=AutofixPolicy.NONE,
35
+ limitations=(
36
+ "Only provenance-resolved importlib.import_module attribute reads are checked. Other importlib APIs, direct imports, shadowed or mutated namespaces and generated code are preserved. No automatic rewrite because changing the lookup can change monkeypatch behavior.",
37
+ ),
38
+ examples=(
39
+ RuleExample(
40
+ example_id="qualified-loader",
41
+ title="The module loader is qualified at each use",
42
+ outcome=ExampleOutcome.MATCH,
43
+ files=(ExampleFile.python("app.py", "import importlib\nloader = importlib.import_module\n"),),
44
+ focus_path=PurePosixPath("app.py"),
45
+ expected_count=1,
46
+ public=True,
47
+ ),
48
+ RuleExample(
49
+ example_id="focused-loader",
50
+ title="Import only the loader",
51
+ outcome=ExampleOutcome.NO_MATCH,
52
+ files=(ExampleFile.python("app.py", "from importlib import import_module\nloader = import_module\n"),),
53
+ focus_path=PurePosixPath("app.py"),
54
+ expected_count=0,
55
+ public=True,
56
+ ),
57
+ ),
58
+ )
59
+ description = documentation.summary
60
+
61
+ @override
62
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
63
+ if context.generated or context.tree is None or "import_module" not in context.symbol_source:
64
+ return []
65
+ provenance = context.static_resource_provenance
66
+ return [
67
+ Diagnostic(
68
+ context.path,
69
+ node.lineno,
70
+ node.col_offset + 1,
71
+ self.code,
72
+ "Use 'from importlib import import_module' and call 'import_module(...)' directly.",
73
+ severity=Severity.WARNING,
74
+ )
75
+ for node in context.nodes(ast.Attribute)
76
+ if node.attr == "import_module"
77
+ and isinstance(node.ctx, ast.Load)
78
+ and context.imports.resolved_qualified_name(node) == "importlib.import_module"
79
+ and isinstance(node.value, ast.Name)
80
+ and isinstance(provenance.binding(node.value.id, node), ast.alias)
81
+ and node.value.id not in provenance.mutated
82
+ and not provenance.wildcard
83
+ ]
@@ -54,7 +54,8 @@ class PreferNamedtupleOverTupleReturn(Rule):
54
54
  aliases=("prefer-namedtuple-over-tuple-return",),
55
55
  limitations=(
56
56
  "Generated files and documentation examples are excluded.",
57
- "Explicit provenance-resolved builtin or typing fixed tuple return annotations with at least two slots are reported; variadic tuples remain valid collections.",
57
+ "Provenance-resolved builtin or typing fixed tuple return annotations with at least two slots are reported, including stable local aliases, quoted annotations, Annotated wrappers and optional unions in test helpers and fixtures; variadic tuples remain valid collections.",
58
+ "Existing direct tuple records remain errors; newly resolved local aliases and optional-union records start as warnings for adoption review.",
58
59
  "Classes with explicit bases, including object, are excluded without requiring an override decorator. Local base declarations do not prove runtime member ownership remains closed; class construction, native hooks and escaped handles can change inheritance. Independent module functions and plain classes remain eligible.",
59
60
  ),
60
61
  examples=(
@@ -109,7 +110,10 @@ class PreferNamedtupleOverTupleReturn(Rule):
109
110
  for node in context.nodes(ast.AST):
110
111
  if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) or node.returns is None:
111
112
  continue
112
- if not _is_public_record_tuple(node.returns, imports):
113
+ if not any(
114
+ _is_public_record_tuple(member, imports)
115
+ for member in context.annotation_semantics.transparent_members(node.returns)
116
+ ):
113
117
  continue
114
118
  if inherited_contract(context.parents.get(node)):
115
119
  continue
@@ -120,7 +124,7 @@ class PreferNamedtupleOverTupleReturn(Rule):
120
124
  col=node.col_offset + 1,
121
125
  code=self.code,
122
126
  message=_MSG,
123
- severity=Severity.ERROR,
127
+ severity=Severity.ERROR if _is_public_record_tuple(node.returns, imports) else Severity.WARNING,
124
128
  )
125
129
  )
126
130
  return diagnostics