sarj-python-lint 1.67.0__tar.gz → 1.69.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 (250) hide show
  1. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/__main__.py +19 -5
  4. sarj_python_lint-1.69.0/src/sarj_python_lint/_parallel.py +102 -0
  5. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/_declared_contracts.py +243 -0
  6. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_local_source.py +13 -3
  7. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_project_index.py +38 -0
  8. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_registry.py +10 -0
  9. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_composition.py +8 -2
  10. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/excessive_commentary.py +36 -14
  11. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +83 -0
  12. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +7 -3
  13. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_precise_object_contract.py +220 -0
  14. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +192 -0
  15. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +154 -0
  16. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_typed_settings_field.py +199 -0
  17. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +7 -7
  18. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/.gitignore +0 -0
  19. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/LICENSE +0 -0
  20. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/README.md +0 -0
  21. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/__init__.py +0 -0
  22. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  23. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_file_context.py +0 -0
  24. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_filesystem.py +0 -0
  25. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_python_target.py +0 -0
  26. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  27. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_secret_names.py +0 -0
  28. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_source.py +0 -0
  29. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_version.py +0 -0
  30. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/benchmark.py +0 -0
  31. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/cast_access.py +0 -0
  32. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  33. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/json_boundary.py +0 -0
  34. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/py.typed +0 -0
  35. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/ratchet.py +0 -0
  36. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rule_base.py +0 -0
  37. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  38. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  39. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  40. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  41. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  42. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  43. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  44. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  45. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  46. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  47. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  48. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  49. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  50. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  51. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  52. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  53. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  54. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  55. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  56. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  57. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  58. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  59. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  60. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  61. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  62. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  63. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  64. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  65. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  66. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  67. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  68. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  69. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  70. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  71. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  72. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  73. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  74. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  75. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  76. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  77. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  78. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  79. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  80. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  81. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  82. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  83. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  84. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  85. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  86. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  87. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  88. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  89. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  90. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  91. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  92. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  93. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  94. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  95. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  96. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  97. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  98. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  99. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  100. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  101. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  102. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  103. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  104. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  105. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  106. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  107. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  108. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  109. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  110. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  111. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  112. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  113. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  114. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  115. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  116. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  117. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  118. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  119. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  120. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  121. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  122. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  123. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  124. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  125. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  126. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  127. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  128. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  129. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  130. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  131. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  132. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  133. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  134. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  135. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  136. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  137. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  138. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  139. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  140. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  141. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  142. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  143. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  144. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  145. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  146. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  147. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  148. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  149. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  150. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  151. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  152. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  153. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  154. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  155. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  156. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  157. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  158. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  159. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  160. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  161. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  162. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  163. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  164. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  165. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  166. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  167. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  168. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  169. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  170. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  171. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  172. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  173. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  174. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  175. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  176. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  177. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  178. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  179. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  180. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  181. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  182. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  183. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  184. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  185. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  186. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  187. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  188. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  189. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  190. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  191. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  192. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  193. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  194. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  195. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  196. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  197. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  198. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  199. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  200. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  201. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  202. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  203. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  204. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  205. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  206. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  207. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  208. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  209. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  210. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  211. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  212. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  213. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  214. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  215. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  216. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  217. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  218. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  219. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  220. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  221. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  222. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  223. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  224. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  225. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  226. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  227. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  228. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  229. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  230. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  231. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  232. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  233. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  234. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  235. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  236. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  237. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  238. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  239. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  240. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  241. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  242. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  243. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  244. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  245. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  246. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  247. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  248. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  249. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  250. {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.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.69.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.69.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" }]
@@ -96,7 +96,7 @@ def _canonical_rule_ids(rule_ids: list[str]) -> list[str]:
96
96
  return list(dict.fromkeys(rid if rid in REGISTRY else aliases.get(rid, rid) for rid in rule_ids))
97
97
 
98
98
 
99
- def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
99
+ def _check(rule_ids: list[str], paths: list[Path], *, jobs: int = 1) -> list[Diagnostic]:
100
100
  rule_ids = _canonical_rule_ids(rule_ids)
101
101
  unknown = [rid for rid in rule_ids if rid not in REGISTRY]
102
102
  if unknown:
@@ -116,6 +116,12 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
116
116
  session.project = indexes
117
117
  for rule in project_rules:
118
118
  rule.prepare(indexes)
119
+ if jobs > 1 and len(loaded) > 1:
120
+ from sarj_python_lint._parallel import check_parallel # ruff: ignore[import-outside-top-level] -- serial API users do not need multiprocessing.
121
+
122
+ parallel = check_parallel([type(rule) for rule in rules], loaded, session.project, jobs=jobs)
123
+ if parallel is not None:
124
+ return parallel
119
125
  diags: list[Diagnostic] = []
120
126
  for p, source in loaded.items():
121
127
  diags.extend(check_source(rules, p, source, session=session))
@@ -144,8 +150,12 @@ def analyze(
144
150
  *,
145
151
  baseline: Path | None = None,
146
152
  root: Path | None = None,
153
+ jobs: int = 1,
147
154
  ) -> list[Diagnostic]:
148
- diagnostics = _check(rule_ids, paths)
155
+ if jobs < 1:
156
+ msg = "jobs must be at least 1"
157
+ raise ValueError(msg)
158
+ diagnostics = _check(rule_ids, paths, jobs=jobs)
149
159
  return diagnostics if baseline is None else _apply_baseline(diagnostics, _read_baseline(baseline), root=root)
150
160
 
151
161
 
@@ -481,9 +491,11 @@ def _apply_baseline(
481
491
  return out
482
492
 
483
493
 
484
- def _run_check(rule: list[str], files: list[Path], baseline: Path | None, update_baseline: Path | None) -> int:
494
+ def _run_check(
495
+ rule: list[str], files: list[Path], baseline: Path | None, update_baseline: Path | None, *, jobs: int = 1
496
+ ) -> int:
485
497
  try:
486
- diags = analyze(rule, files)
498
+ diags = analyze(rule, files, jobs=jobs)
487
499
  except (OSError, ValueError, SyntaxError) as exc:
488
500
  sys.stderr.write(f"error: {exc}\n")
489
501
  return 2
@@ -554,14 +566,16 @@ def _check_command(
554
566
  context: typer.Context,
555
567
  files: Annotated[list[Path], typer.Argument()],
556
568
  rule: Annotated[list[str], typer.Option("--rule", help="Rule ID (repeat for multiple).")],
569
+ *,
557
570
  baseline: Annotated[
558
571
  Path | None, typer.Option(help="Per-file shrink-only baseline JSON: {path: {CODE: count}}.")
559
572
  ] = None,
560
573
  update_baseline: Annotated[
561
574
  Path | None, typer.Option(help="Write current per-file diagnostic counts to JSON and exit 0.")
562
575
  ] = None,
576
+ jobs: Annotated[int, typer.Option(min=1, help="Analysis worker budget (up to 2); default is serial.")] = 1,
563
577
  ) -> None:
564
- context.ensure_object(_Result).code = _run_check(rule, files, baseline, update_baseline)
578
+ context.ensure_object(_Result).code = _run_check(rule, files, baseline, update_baseline, jobs=jobs)
565
579
 
566
580
 
567
581
  def main(argv: list[str] | None = None) -> int:
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import ProcessPoolExecutor
4
+ from dataclasses import dataclass
5
+ import multiprocessing
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from sarj_python_lint._analysis_session import AnalysisSession
10
+ from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule
11
+ from sarj_python_lint.rules._local_source import LOCAL_SOURCE_CACHE_LIMIT
12
+ from sarj_python_lint.rules._paths import clear_path_caches
13
+ from sarj_python_lint.rules._project_index import ProjectIndexSet
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from sarj_python_lint.rules._project_index import ProjectSourceSnapshot
18
+
19
+
20
+ type _ResolutionKey = tuple[Path, str | None, int]
21
+
22
+
23
+ @dataclass
24
+ class _WorkerState:
25
+ rules: list[Rule]
26
+ session: AnalysisSession
27
+ resolutions: frozenset[_ResolutionKey] = frozenset()
28
+ modules: frozenset[Path] = frozenset()
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class _FileResult:
33
+ diagnostics: list[Diagnostic]
34
+ resolutions: frozenset[_ResolutionKey]
35
+ modules: frozenset[Path]
36
+
37
+
38
+ _state: _WorkerState | None = None
39
+
40
+
41
+ def _initialize(rule_types: list[type[Rule]], snapshot: ProjectSourceSnapshot | None) -> None:
42
+ global _state # ruff: ignore[global-statement] -- each spawned process owns one initialized session.
43
+ rules = [rule_type() for rule_type in rule_types]
44
+ session = AnalysisSession()
45
+ for rule in rules:
46
+ rule.prepare_session(session)
47
+ clear_path_caches()
48
+ project = ProjectIndexSet.from_source_snapshot(snapshot) if snapshot is not None else None
49
+ session.project = project
50
+ for rule in rules:
51
+ if isinstance(rule, ProjectRule) and project is not None:
52
+ rule.prepare(project)
53
+ _state = _WorkerState(rules, session)
54
+
55
+
56
+ def _check_file(item: tuple[Path, str]) -> _FileResult:
57
+ from sarj_python_lint.__main__ import check_source # ruff: ignore[import-outside-top-level] -- keep spawn targets in an importable module.
58
+
59
+ if _state is None:
60
+ msg = "Python analysis worker was not initialized"
61
+ raise RuntimeError(msg)
62
+ path, source = item
63
+ diagnostics = check_source(_state.rules, path, source, session=_state.session)
64
+ keys = _state.session.local_source.cache_keys()
65
+ result = _FileResult(diagnostics, keys.resolutions - _state.resolutions, keys.modules - _state.modules)
66
+ _state.resolutions, _state.modules = keys.resolutions, keys.modules
67
+ return result
68
+
69
+
70
+ def check_parallel(
71
+ rule_types: list[type[Rule]],
72
+ loaded: dict[Path, str],
73
+ project: ProjectIndexSet | None,
74
+ *,
75
+ jobs: int,
76
+ ) -> list[Diagnostic] | None:
77
+ # The untouched parent rules/session remain the serial fallback. Workers
78
+ # receive the same complete project snapshot and selected source strings.
79
+ workers = min(jobs, 2, len(loaded))
80
+ pool = ProcessPoolExecutor(
81
+ max_workers=workers,
82
+ mp_context=multiprocessing.get_context("spawn"),
83
+ initializer=_initialize,
84
+ initargs=(rule_types, project.source_snapshot() if project is not None else None),
85
+ )
86
+ diagnostics: list[Diagnostic] = []
87
+ resolutions: set[_ResolutionKey] = set()
88
+ modules: set[Path] = set()
89
+ try:
90
+ for result in pool.map(_check_file, loaded.items(), chunksize=1, buffersize=workers):
91
+ resolutions.update(result.resolutions)
92
+ modules.update(result.modules)
93
+ # A worker cannot hit either order-dependent limit when the whole
94
+ # union stays strictly below it. At the boundary, discard all output.
95
+ if len(resolutions) >= LOCAL_SOURCE_CACHE_LIMIT or len(modules) >= LOCAL_SOURCE_CACHE_LIMIT:
96
+ return None
97
+ diagnostics.extend(result.diagnostics)
98
+ return diagnostics
99
+ finally:
100
+ # Bound queued work and reap before fallback/error return. Running file
101
+ # checks finish normally; group cancellation belongs to the supervisor.
102
+ pool.shutdown(wait=True, cancel_futures=True)
@@ -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
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
 
16
16
  _MAX_ANCESTORS = 24
17
17
  _MAX_MODULE_BYTES = 256_000
18
- _MAX_MODULES = 512
18
+ LOCAL_SOURCE_CACHE_LIMIT = 512
19
19
 
20
20
 
21
21
  @dataclass(frozen=True, slots=True)
@@ -26,6 +26,12 @@ class LocalModule:
26
26
  runtime_imports: ImportIndex
27
27
 
28
28
 
29
+ @dataclass(frozen=True, slots=True)
30
+ class LocalSourceKeys:
31
+ resolutions: frozenset[tuple[Path, str | None, int]]
32
+ modules: frozenset[Path]
33
+
34
+
29
35
  @final
30
36
  class LocalSourceFacts:
31
37
  def __init__(self) -> None:
@@ -40,7 +46,7 @@ class LocalSourceFacts:
40
46
  key = (current, module, level)
41
47
  if key in self._resolutions:
42
48
  return self._resolutions[key]
43
- if len(self._resolutions) >= _MAX_MODULES:
49
+ if len(self._resolutions) >= LOCAL_SOURCE_CACHE_LIMIT:
44
50
  return None
45
51
  result = _resolve_module(current, module, level, facts=facts)
46
52
  self._resolutions[key] = result
@@ -49,12 +55,16 @@ class LocalSourceFacts:
49
55
  def read_module(self, path: Path) -> LocalModule | None:
50
56
  if path in self._modules:
51
57
  return self._modules[path]
52
- if len(self._modules) >= _MAX_MODULES:
58
+ if len(self._modules) >= LOCAL_SOURCE_CACHE_LIMIT:
53
59
  return None
54
60
  module = _read_module(path)
55
61
  self._modules[path] = module
56
62
  return module
57
63
 
64
+ def cache_keys(self) -> LocalSourceKeys:
65
+ # Failed lookups consume the same semantic budgets as successful ones.
66
+ return LocalSourceKeys(frozenset(self._resolutions), frozenset(self._modules))
67
+
58
68
 
59
69
  def _resolve_module(current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
60
70
  if ".." in current.parts or level > _MAX_ANCESTORS:
@@ -154,6 +154,20 @@ class LoadedSource:
154
154
  source: str
155
155
 
156
156
 
157
+ @dataclass(frozen=True, slots=True)
158
+ class _IndexedSource:
159
+ key: Path
160
+ path: Path
161
+ module: str | None
162
+ source: str
163
+
164
+
165
+ @dataclass(frozen=True, slots=True)
166
+ class ProjectSourceSnapshot:
167
+ roots: tuple[Path, ...]
168
+ units: tuple[_IndexedSource, ...]
169
+
170
+
157
171
  @dataclass(frozen=True, slots=True)
158
172
  class _ClassValueUse:
159
173
  reference: ast.Name | ast.Attribute
@@ -196,6 +210,30 @@ class ProjectIndexSet:
196
210
  self._classes = MappingProxyType(classes)
197
211
  self._nominals = MappingProxyType({key: frozenset(value) for key, value in nominals.items()})
198
212
 
213
+ def source_snapshot(self) -> ProjectSourceSnapshot:
214
+ # Rebuild memoized facts without rescanning roots or changing the
215
+ # module identities/order of the complete prepared source graph.
216
+ return ProjectSourceSnapshot(
217
+ self._roots,
218
+ tuple(_IndexedSource(key, unit.path, unit.module, unit.source) for key, unit in self._units.items()),
219
+ )
220
+
221
+ @classmethod
222
+ def from_source_snapshot(cls, snapshot: ProjectSourceSnapshot) -> Self:
223
+ units: dict[Path, SourceUnit] = {}
224
+ for source in snapshot.units:
225
+ tree: ast.Module | None = None
226
+ with suppress(SyntaxError):
227
+ tree = ast.parse(source.source, filename=str(source.path))
228
+ units[source.key] = SourceUnit(
229
+ path=source.path,
230
+ module=source.module,
231
+ source=source.source,
232
+ tree=tree,
233
+ imports=MappingProxyType(_imports(source.module, tree, is_package=source.path.name == "__init__.py")),
234
+ )
235
+ return cls(units, snapshot.roots)
236
+
199
237
  @classmethod
200
238
  def build(
201
239
  cls,
@@ -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),