sarj-python-lint 1.68.0__tar.gz → 1.69.1__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 (251) hide show
  1. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/__main__.py +19 -5
  4. sarj_python_lint-1.69.1/src/sarj_python_lint/_parallel.py +102 -0
  5. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_local_source.py +13 -3
  6. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_project_index.py +38 -0
  7. sarj_python_lint-1.69.1/src/sarj_python_lint/rules/prefer_direct_import_module.py +113 -0
  8. sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -83
  9. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/.gitignore +0 -0
  10. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/LICENSE +0 -0
  11. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/README.md +0 -0
  12. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  14. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_source.py +0 -0
  20. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/cast_access.py +0 -0
  23. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  24. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/json_boundary.py +0 -0
  25. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/py.typed +0 -0
  26. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/ratchet.py +0 -0
  27. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rule_base.py +0 -0
  28. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  29. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  30. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  31. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  32. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  34. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  35. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  36. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  37. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  38. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  39. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  40. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  41. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  42. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  43. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  44. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  45. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  46. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  47. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  48. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  49. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  50. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  51. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  52. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  53. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  54. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  55. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  56. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  57. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  58. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  59. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  60. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  61. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  62. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  63. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  64. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  65. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  66. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  67. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  68. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  69. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  70. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  71. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  72. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  73. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  74. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  75. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  76. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  77. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  78. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  79. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  80. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  81. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  82. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  83. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  84. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  85. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  86. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  87. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  88. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  89. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  90. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  91. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  92. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  93. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  94. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  95. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  96. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  97. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  98. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  99. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  100. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  101. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  102. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  103. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  104. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  105. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  106. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  107. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  108. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  109. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  110. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  111. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  112. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  113. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  114. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  115. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  116. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  117. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  118. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  119. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  120. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  121. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  122. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  123. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  124. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  125. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  126. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  127. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  128. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  129. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  130. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  131. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  132. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  133. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  134. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  135. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  136. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  137. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  138. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  139. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  140. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  141. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  142. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  143. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  144. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  145. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  146. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  147. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  148. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  149. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  150. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  151. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  152. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  153. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  154. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  155. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  156. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  157. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  158. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  159. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  160. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  161. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  162. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  163. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  164. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  165. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  166. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  167. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  168. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  169. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  170. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  171. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  172. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  173. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  174. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  175. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  176. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  177. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  178. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  179. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  180. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  181. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  182. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  183. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  184. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  185. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  186. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  187. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  188. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  189. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  190. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  191. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  192. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  193. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  194. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  195. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  196. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  197. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  198. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  199. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  200. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  201. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  202. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  203. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  204. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  205. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  206. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  207. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  208. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  209. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  210. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  211. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  212. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  213. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  214. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  215. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  216. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  217. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  218. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  219. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  220. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  221. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  222. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  223. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  224. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  225. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  226. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  227. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  228. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  229. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  230. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  231. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  232. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  233. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  234. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  235. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  236. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  237. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  238. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  239. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  240. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  241. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  242. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  243. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  244. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  245. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  246. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  247. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  248. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  249. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  250. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  251. {sarj_python_lint-1.68.0 → sarj_python_lint-1.69.1}/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.68.0
3
+ Version: 1.69.1
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.68.0"
3
+ version = "1.69.1"
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)
@@ -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,
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from keyword import iskeyword
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, final, override
7
+ from unicodedata import is_normalized
8
+
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ Rule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
19
+ )
20
+ from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+
26
+
27
+ @final
28
+ class PreferDirectImportModule(Rule):
29
+ id = "prefer-direct-import-module"
30
+ code = "SARJ529"
31
+ documentation = RuleDocumentation(
32
+ default_level=Severity.WARNING,
33
+ summary="Prefer static imports for eager module loads with a literal module name.",
34
+ rationale="Static imports expose known dependencies to readers and tooling without dynamic loader indirection.",
35
+ remediation="Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Keep dynamic loading when module selection or import timing requires it.",
36
+ category=RuleCategory.STYLE,
37
+ autofix=AutofixPolicy.NONE,
38
+ limitations=(
39
+ "Checks direct module-level assignments and expression statements calling the proven stdlib import_module function, through qualified or directly imported names, with one literal absolute module name.",
40
+ "Function/class bodies, conditional or guarded imports, variable module names, relative imports, callback references, shadowed or mutated providers and generated code are excluded. Static import syntax must be valid for every module-name segment.",
41
+ "This is a warning without automatic rewriting: moving an import can change execution order, optional-dependency behavior or cycle handling. The rule does not prefer one spelling of genuinely dynamic import_module calls.",
42
+ ),
43
+ examples=(
44
+ RuleExample(
45
+ example_id="literal-eager-loader",
46
+ title="A known eager dependency is loaded dynamically",
47
+ outcome=ExampleOutcome.MATCH,
48
+ files=(
49
+ ExampleFile.python(
50
+ "app.py", "import importlib\nplugin = importlib.import_module('plugins.billing')\n"
51
+ ),
52
+ ),
53
+ focus_path=PurePosixPath("app.py"),
54
+ expected_count=1,
55
+ public=True,
56
+ ),
57
+ RuleExample(
58
+ example_id="static-module-import",
59
+ title="A static import preserves the module binding",
60
+ outcome=ExampleOutcome.NO_MATCH,
61
+ files=(ExampleFile.python("app.py", "import plugins.billing as plugin\n"),),
62
+ focus_path=PurePosixPath("app.py"),
63
+ expected_count=0,
64
+ public=True,
65
+ ),
66
+ ),
67
+ )
68
+ description = documentation.summary
69
+
70
+ @override
71
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
72
+ if context.generated or context.tree is None or "import_module" not in context.symbol_source:
73
+ return []
74
+ loader_references = {
75
+ reference
76
+ for call in context.nodes(ast.Call)
77
+ if context.imports.resolved_qualified_name(call.func) == "importlib.import_module"
78
+ for reference in ast.walk(call.func)
79
+ }
80
+ if dynamic_namespace_access(
81
+ (node for node in context.nodes(ast.AST) if node not in loader_references),
82
+ context.imports,
83
+ context.parents,
84
+ ):
85
+ return []
86
+ return [
87
+ Diagnostic(
88
+ context.path,
89
+ node.lineno,
90
+ node.col_offset + 1,
91
+ self.code,
92
+ f"Use a module-level static import for the known module '{module}'; preserve the binding with 'import {module} as plugin' when needed.",
93
+ severity=Severity.WARNING,
94
+ )
95
+ for statement in context.tree.body
96
+ if isinstance(statement, ast.Assign | ast.AnnAssign | ast.Expr)
97
+ and isinstance(node := statement.value, ast.Call)
98
+ and (module := _literal_module(node)) is not None
99
+ and context.resource_provenance.runtime_import_name(node.func) == "importlib.import_module"
100
+ ]
101
+
102
+
103
+ def _literal_module(call: ast.Call) -> str | None:
104
+ match call:
105
+ case ast.Call(args=[ast.Constant(value=str() as module)], keywords=[]):
106
+ pass
107
+ case ast.Call(args=[], keywords=[ast.keyword(arg="name", value=ast.Constant(value=str() as module))]):
108
+ pass
109
+ case _:
110
+ return None
111
+ if not is_normalized("NFKC", module):
112
+ return None
113
+ return module if all(part.isidentifier() and not iskeyword(part) for part in module.split(".")) else None
@@ -1,83 +0,0 @@
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
- ]