sarj-python-lint 1.14.4__tar.gz → 1.15.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 (173) hide show
  1. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_analysis_session.py +2 -0
  4. sarj_python_lint-1.15.0/src/sarj_python_lint/rules/_local_source.py +123 -0
  5. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  6. sarj_python_lint-1.15.0/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +384 -0
  7. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/.gitignore +0 -0
  8. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/LICENSE +0 -0
  9. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/README.md +0 -0
  10. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  29. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  30. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  34. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  35. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  36. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  37. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  38. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  39. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  40. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  41. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  42. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  43. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  44. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  45. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  46. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  47. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  48. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  49. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  50. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  51. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  52. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  53. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  54. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  55. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  56. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  57. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  58. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  59. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  60. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  61. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  62. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  63. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  64. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  65. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  66. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  67. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  68. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  69. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  70. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  71. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  72. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  73. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  74. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  75. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  76. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  77. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  78. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  79. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  80. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  81. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  82. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  83. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  84. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  85. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  86. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  87. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  88. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  89. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  90. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  91. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  92. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  93. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  94. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  95. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  96. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  97. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  98. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  99. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  100. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  101. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  102. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  103. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  104. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  105. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  106. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  107. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  108. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  109. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  110. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  111. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  112. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  113. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  114. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  115. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  116. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  117. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  118. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  119. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  120. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  121. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  122. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  123. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  124. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  125. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  126. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  127. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  128. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  129. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  130. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  131. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  132. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  133. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  134. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  135. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  136. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  137. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  138. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  139. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  140. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  141. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  142. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  143. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  144. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  145. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  146. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  147. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  148. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  149. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  150. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  151. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  152. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  153. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  154. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  155. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  156. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  157. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  158. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  159. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  160. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  161. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  162. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  163. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  164. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  165. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  166. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  167. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  168. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  169. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  170. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  171. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  172. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  173. {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.14.4
3
+ Version: 1.15.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.14.4"
3
+ version = "1.15.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" }]
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, final
4
4
 
5
5
  from sarj_python_lint._python_target import PythonTargetFacts
6
6
  from sarj_python_lint.rules._first_party import FirstPartyFacts
7
+ from sarj_python_lint.rules._local_source import LocalSourceFacts
7
8
  from sarj_python_lint.rules._nominal_project import NominalProjectFacts
8
9
 
9
10
 
@@ -16,5 +17,6 @@ class AnalysisSession:
16
17
  def __init__(self) -> None:
17
18
  self.project: ProjectIndexSet | None = None
18
19
  self.first_party = FirstPartyFacts()
20
+ self.local_source = LocalSourceFacts()
19
21
  self.python_target = PythonTargetFacts()
20
22
  self.nominal_project = NominalProjectFacts()
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, final
7
+
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from sarj_python_lint.rules._first_party import FirstPartyFacts
13
+
14
+
15
+ _MAX_ANCESTORS = 24
16
+ _MAX_MODULE_BYTES = 256_000
17
+ _MAX_MODULES = 512
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class LocalModule:
22
+ path: Path
23
+ source: str
24
+ tree: ast.Module
25
+ runtime_imports: ImportIndex
26
+
27
+
28
+ @final
29
+ class LocalSourceFacts:
30
+ def __init__(self) -> None:
31
+ self._resolutions: dict[tuple[Path, str | None, int], Path | None] = {}
32
+ self._modules: dict[Path, LocalModule | None] = {}
33
+
34
+ def resolve_module(self, current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
35
+ try:
36
+ current = current.absolute()
37
+ except OSError:
38
+ return None
39
+ key = (current, module, level)
40
+ if key in self._resolutions:
41
+ return self._resolutions[key]
42
+ if len(self._resolutions) >= _MAX_MODULES:
43
+ return None
44
+ result = _resolve_module(current, module, level, facts=facts)
45
+ self._resolutions[key] = result
46
+ return result
47
+
48
+ def read_module(self, path: Path) -> LocalModule | None:
49
+ if path in self._modules:
50
+ return self._modules[path]
51
+ if len(self._modules) >= _MAX_MODULES:
52
+ return None
53
+ module = _read_module(path)
54
+ self._modules[path] = module
55
+ return module
56
+
57
+
58
+ def _resolve_module(current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
59
+ if ".." in current.parts or level > _MAX_ANCESTORS:
60
+ return None
61
+ root = facts.project_root(current)
62
+ if root is None or not _safe_path(current, root):
63
+ return None
64
+ parts = module.split(".") if module else []
65
+ if any(not part.isidentifier() for part in parts):
66
+ return None
67
+ candidates: set[Path] = set()
68
+ for base in _import_bases(current, root, level):
69
+ target = base.joinpath(*parts)
70
+ for candidate in (target.with_suffix(".py"), target / "__init__.py"):
71
+ if _safe_path(candidate, root) and candidate.is_file():
72
+ candidates.add(candidate)
73
+ return next(iter(candidates)) if len(candidates) == 1 else None
74
+
75
+
76
+ def _import_bases(current: Path, root: Path, level: int) -> list[Path]:
77
+ if level:
78
+ base = current.parent
79
+ for _ in range(level - 1):
80
+ if base == root:
81
+ return []
82
+ base = base.parent
83
+ return [base]
84
+ bases: list[Path] = []
85
+ for ancestor in list(current.parents)[:_MAX_ANCESTORS]:
86
+ if not ancestor.is_relative_to(root):
87
+ break
88
+ bases.append(ancestor)
89
+ if (ancestor / "pyproject.toml").is_file():
90
+ bases.append(ancestor / "src")
91
+ if ancestor == root:
92
+ break
93
+ return bases
94
+
95
+
96
+ def _safe_path(path: Path, root: Path) -> bool:
97
+ if not path.is_relative_to(root):
98
+ return False
99
+ try:
100
+ for entry in (path, *list(path.parents)[:_MAX_ANCESTORS]):
101
+ if entry == root:
102
+ return True
103
+ if entry.is_symlink() or (entry != path and (entry / ".git").exists()):
104
+ return False
105
+ except OSError:
106
+ return False
107
+ return False
108
+
109
+
110
+ def _read_module(path: Path) -> LocalModule | None:
111
+ try:
112
+ with path.open("rb") as stream:
113
+ data = stream.read(_MAX_MODULE_BYTES + 1)
114
+ if len(data) > _MAX_MODULE_BYTES:
115
+ return None
116
+ source = data.decode("utf-8")
117
+ tree = ast.parse(source, filename=str(path))
118
+ except OSError, UnicodeError, SyntaxError:
119
+ return None
120
+ runtime_imports = ast.Module(
121
+ body=[node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))], type_ignores=[]
122
+ )
123
+ return LocalModule(path, source, tree, ImportIndex.from_tree(runtime_imports))
@@ -133,6 +133,7 @@ from sarj_python_lint.rules.prefer_non_nullable_collection import (
133
133
  )
134
134
  from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
135
135
  from sarj_python_lint.rules.prefer_pydantic_json_value import PreferPydanticJsonValue
136
+ from sarj_python_lint.rules.prefer_pytest_fixture_injection import PreferPytestFixtureInjection
136
137
  from sarj_python_lint.rules.prefer_regex_fullmatch import PreferRegexFullmatch
137
138
  from sarj_python_lint.rules.prefer_self_documenting_constant import (
138
139
  PreferSelfDocumentingConstant,
@@ -278,6 +279,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
278
279
  NoRepeatedTestBody.id: NoRepeatedTestBody,
279
280
  UnusedMockSetup.id: UnusedMockSetup,
280
281
  UnusedTestFactoryOption.id: UnusedTestFactoryOption,
282
+ PreferPytestFixtureInjection.id: PreferPytestFixtureInjection,
281
283
  PreferFstringOverConcat.id: PreferFstringOverConcat,
282
284
  PreferOrPattern.id: PreferOrPattern,
283
285
  PreferPydanticJsonValue.id: PreferPydanticJsonValue,
@@ -0,0 +1,384 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import Counter
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, ClassVar, NamedTuple, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ )
19
+ from sarj_python_lint.rules._paths import is_generated, is_test_support_path
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Iterator
24
+
25
+ from sarj_python_lint._file_context import PythonFileContext
26
+ from sarj_python_lint.rules._local_source import LocalModule
27
+
28
+
29
+ type Function = ast.FunctionDef | ast.AsyncFunctionDef
30
+ _POOL_CONSTRUCTORS = frozenset({"psycopg_pool.ConnectionPool", "psycopg_pool.AsyncConnectionPool"})
31
+ _SETUP_AND_CONSUMER = 2
32
+ _SUPPORT_SOURCE = (
33
+ "from psycopg_pool import ConnectionPool\n\n"
34
+ "def pool() -> ConnectionPool[object]:\n"
35
+ " return ConnectionPool[object](open=False)\n"
36
+ )
37
+
38
+
39
+ class _HelperImport(NamedTuple):
40
+ local: str
41
+ symbol: str
42
+ module: str
43
+ level: int
44
+
45
+
46
+ class PreferPytestFixtureInjection(Rule):
47
+ id: str = "prefer-pytest-fixture-injection"
48
+ code: str = "SARJ476"
49
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
50
+ default_level=Severity.WARNING,
51
+ summary="Request shared database-pool setup through a pytest fixture.",
52
+ rationale=(
53
+ "Calling a shared conftest pool helper during initial test setup bypasses an explicit pytest "
54
+ "fixture dependency for that setup."
55
+ ),
56
+ remediation=(
57
+ "Request one function-scoped fixture visible to the test, preserving initial pool state, creation "
58
+ "timing, and cleanup ownership. Use yield teardown when the fixture owns cleanup; keep factories "
59
+ "for variants or fresh instances."
60
+ ),
61
+ category=RuleCategory.TESTING,
62
+ autofix=AutofixPolicy.NONE,
63
+ limitations=(
64
+ "Only directly collected module-level test functions in conventional pytest module names are checked.",
65
+ "Only unshadowed runtime imports from a uniquely resolved local conftest module are followed.",
66
+ "Helpers must be undecorated synchronous functions with no formal inputs and one direct return of an imported psycopg_pool pool constructor.",
67
+ "Setup must be the first executable statement and immediately feed a plain call, or appear inline as a direct argument of that first call.",
68
+ "Class tests, decorated helpers, unknown test decorators, dynamic imports, re-exports, and ambiguous or out-of-bound source paths are excluded.",
69
+ ),
70
+ examples=(
71
+ RuleExample(
72
+ example_id="manual-shared-generic-pool",
73
+ title="A test calls shared generic pool setup",
74
+ outcome=ExampleOutcome.MATCH,
75
+ files=(
76
+ ExampleFile.python("tests/conftest.py", _SUPPORT_SOURCE),
77
+ ExampleFile.python(
78
+ "tests/test_app.py",
79
+ "from tests.conftest import pool\n\n"
80
+ "def test_app():\n resource = pool()\n app = Application(resource)\n assert app\n",
81
+ ),
82
+ ),
83
+ focus_path=PurePosixPath("tests/test_app.py"),
84
+ expected_count=1,
85
+ public=True,
86
+ ),
87
+ RuleExample(
88
+ example_id="injected-generic-pool",
89
+ title="A test requests its shared resource",
90
+ outcome=ExampleOutcome.NO_MATCH,
91
+ files=(
92
+ ExampleFile.python(
93
+ "tests/conftest.py",
94
+ "import pytest\nfrom psycopg_pool import ConnectionPool\n\n"
95
+ "@pytest.fixture\ndef pool() -> ConnectionPool[object]:\n"
96
+ " return ConnectionPool[object](open=False)\n",
97
+ ),
98
+ ExampleFile.python(
99
+ "tests/test_app.py", "def test_app(pool):\n app = Application(pool)\n assert app\n"
100
+ ),
101
+ ),
102
+ focus_path=PurePosixPath("tests/test_app.py"),
103
+ expected_count=0,
104
+ public=True,
105
+ ),
106
+ ),
107
+ )
108
+ description: str = documentation.summary
109
+
110
+ @override
111
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
112
+ if (
113
+ context.path.suffix != ".py"
114
+ or not (context.path.name.startswith("test_") or context.path.name.endswith("_test.py"))
115
+ or is_test_support_path(context.path)
116
+ or context.generated
117
+ or context.tree is None
118
+ ):
119
+ return []
120
+ tree = context.tree
121
+ module_bindings = _scope_bindings(tree)
122
+ if "__test__" in module_bindings or any(
123
+ isinstance(node, ast.Attribute) and node.attr == "__test__" and isinstance(node.ctx, (ast.Store, ast.Del))
124
+ for node in ast.walk(tree)
125
+ ):
126
+ return []
127
+ helpers = _resource_helpers(context)
128
+ diagnostics: list[Diagnostic] = []
129
+ for statement in tree.body:
130
+ if (
131
+ not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
132
+ or module_bindings[statement.name] != 1
133
+ or _module_reference(tree, statement.name)
134
+ ):
135
+ continue
136
+ call = _setup_call(context, statement, helpers)
137
+ if call is not None:
138
+ diagnostics.append(
139
+ Diagnostic(
140
+ path=context.path,
141
+ line=call.lineno,
142
+ col=call.col_offset + 1,
143
+ code=self.code,
144
+ message=(
145
+ "test calls a shared conftest pool helper during initial setup; request one visible "
146
+ "function-scoped fixture by parameter, preserving initial pool state, creation timing, "
147
+ "and cleanup ownership; keep factories for variants or fresh instances."
148
+ ),
149
+ severity=Severity.WARNING,
150
+ )
151
+ )
152
+ return diagnostics
153
+
154
+
155
+ def _resource_helpers(context: PythonFileContext) -> frozenset[str]:
156
+ tree = context.tree
157
+ if tree is None:
158
+ return frozenset()
159
+ bindings = _scope_bindings(tree)
160
+ helpers: set[str] = set()
161
+ for reference in _helper_imports(tree):
162
+ if bindings[reference.local] != 1 or _root_mutated(tree, reference.local):
163
+ continue
164
+ target = context.session.local_source.resolve_module(
165
+ context.path, reference.module, reference.level, facts=context.session.first_party
166
+ )
167
+ module = (
168
+ context.session.local_source.read_module(target)
169
+ if target is not None and target.name == "conftest.py"
170
+ else None
171
+ )
172
+ if module is not None and _is_resource_helper(module, reference.symbol):
173
+ helpers.add(reference.local)
174
+ return frozenset(helpers)
175
+
176
+
177
+ def _helper_imports(tree: ast.Module) -> Iterator[_HelperImport]:
178
+ if any(isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in tree.body):
179
+ return
180
+ for statement in tree.body:
181
+ if not isinstance(statement, ast.ImportFrom) or not statement.module:
182
+ continue
183
+ if statement.module.rsplit(".", 1)[-1] != "conftest":
184
+ continue
185
+ for alias in statement.names:
186
+ yield _HelperImport(alias.asname or alias.name, alias.name, statement.module, statement.level)
187
+
188
+
189
+ def _is_resource_helper(module: LocalModule, symbol: str) -> bool:
190
+ if (
191
+ is_generated(module.path, module.source)
192
+ or _scope_bindings(module.tree)[symbol] != 1
193
+ or _has_wildcard(module.tree)
194
+ or _root_mutated(module.tree, symbol)
195
+ ):
196
+ return False
197
+ helper = next(
198
+ (node for node in module.tree.body if isinstance(node, ast.FunctionDef) and node.name == symbol), None
199
+ )
200
+ if helper is None or helper.decorator_list:
201
+ return False
202
+ arguments = helper.args
203
+ if arguments.posonlyargs or arguments.args or arguments.kwonlyargs or arguments.vararg or arguments.kwarg:
204
+ return False
205
+ body = _executable_body(helper)
206
+ if len(body) != 1 or not isinstance(body[0], ast.Return) or not isinstance(body[0].value, ast.Call):
207
+ return False
208
+ constructor = body[0].value.func
209
+ if isinstance(constructor, ast.Subscript):
210
+ constructor = constructor.value
211
+ root = _root_name(constructor)
212
+ return (
213
+ root is not None
214
+ and _scope_bindings(module.tree)[root] == 1
215
+ and not _root_mutated(module.tree, root)
216
+ and module.runtime_imports.resolved_qualified_name(constructor) in _POOL_CONSTRUCTORS
217
+ )
218
+
219
+
220
+ def _setup_call(context: PythonFileContext, function: Function, helpers: frozenset[str]) -> ast.Call | None:
221
+ if not function.name.startswith("test_") or any(
222
+ not _pytest_mark(context, decorator) for decorator in function.decorator_list
223
+ ):
224
+ return None
225
+ bindings = _scope_bindings(function)
226
+ available = helpers - bindings.keys()
227
+ # Looking through nested scopes here conservatively excludes captures and callable escapes.
228
+ uses = [node for node in ast.walk(function) if isinstance(node, ast.Name) and node.id in available]
229
+ if len(uses) != 1:
230
+ return None
231
+ body = _executable_body(function)
232
+ if not body:
233
+ return None
234
+ first = body[0]
235
+ setup = _plain_call(first)
236
+ if setup is None:
237
+ return None
238
+ if _helper_call(setup, available):
239
+ return _assigned_dependency_call(context, body, bindings, setup)
240
+ if not _unshadowed_consumer(context, setup, bindings):
241
+ return None
242
+ inline = [arg for arg in _arguments(setup) if isinstance(arg, ast.Call) and _helper_call(arg, available)]
243
+ return inline[0] if len(inline) == 1 else None
244
+
245
+
246
+ def _assigned_dependency_call(
247
+ context: PythonFileContext, body: list[ast.stmt], bindings: Counter[str], setup: ast.Call
248
+ ) -> ast.Call | None:
249
+ resource = _assignment_name(body[0])
250
+ if resource is None or bindings[resource] != 1 or len(body) < _SETUP_AND_CONSUMER:
251
+ return None
252
+ consumer = _plain_call(body[1])
253
+ if consumer is None or not _unshadowed_consumer(context, consumer, bindings):
254
+ return None
255
+ for argument in _arguments(consumer):
256
+ if isinstance(argument, ast.Name) and argument.id == resource:
257
+ return setup
258
+ return None
259
+
260
+
261
+ def _pytest_mark(context: PythonFileContext, decorator: ast.expr) -> bool:
262
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
263
+ qualified = context.module_imports.resolved_qualified_name(target)
264
+ return qualified is not None and qualified.startswith("pytest.mark.")
265
+
266
+
267
+ def _unshadowed_consumer(context: PythonFileContext, call: ast.Call, bindings: Counter[str]) -> bool:
268
+ root = _root_name(call.func)
269
+ tree = context.tree
270
+ return (
271
+ root is not None
272
+ and root not in bindings
273
+ and tree is not None
274
+ and _scope_bindings(tree)[root] <= 1
275
+ and not _root_mutated(tree, root)
276
+ )
277
+
278
+
279
+ def _module_reference(tree: ast.Module, name: str) -> bool:
280
+ return any(
281
+ isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load)
282
+ for statement in tree.body
283
+ if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
284
+ for node in ast.walk(statement)
285
+ )
286
+
287
+
288
+ def _helper_call(call: ast.Call, helpers: frozenset[str] | set[str]) -> bool:
289
+ return isinstance(call.func, ast.Name) and call.func.id in helpers and not call.args and not call.keywords
290
+
291
+
292
+ def _arguments(call: ast.Call) -> Iterator[ast.expr]:
293
+ yield from call.args
294
+ yield from (keyword.value for keyword in call.keywords if keyword.arg is not None)
295
+
296
+
297
+ def _plain_call(statement: ast.stmt) -> ast.Call | None:
298
+ if (
299
+ isinstance(statement, (ast.Expr, ast.Assign, ast.AnnAssign))
300
+ and isinstance(statement.value, ast.Call)
301
+ and isinstance(statement.value.func, (ast.Name, ast.Attribute))
302
+ ):
303
+ return statement.value
304
+ return None
305
+
306
+
307
+ def _assignment_name(statement: ast.stmt) -> str | None:
308
+ if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name):
309
+ return statement.targets[0].id
310
+ if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name):
311
+ return statement.target.id
312
+ return None
313
+
314
+
315
+ def _root_name(expression: ast.expr) -> str | None:
316
+ while isinstance(expression, ast.Attribute):
317
+ expression = expression.value
318
+ return expression.id if isinstance(expression, ast.Name) else None
319
+
320
+
321
+ def _executable_body(function: Function) -> list[ast.stmt]:
322
+ body = function.body
323
+ if (
324
+ body
325
+ and isinstance(body[0], ast.Expr)
326
+ and isinstance(body[0].value, ast.Constant)
327
+ and isinstance(body[0].value.value, str)
328
+ ):
329
+ return body[1:]
330
+ return body
331
+
332
+
333
+ def _scope_bindings(scope: ast.Module | Function) -> Counter[str]:
334
+ names: Counter[str] = Counter()
335
+ if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
336
+ arguments = scope.args
337
+ names.update(arg.arg for arg in (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs))
338
+ names.update(arg.arg for arg in (arguments.vararg, arguments.kwarg) if arg is not None)
339
+ for statement in scope.body:
340
+ _collect_bindings(statement, names)
341
+ return names
342
+
343
+
344
+ def _collect_bindings(node: ast.AST, names: Counter[str]) -> None:
345
+ match node:
346
+ case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.ClassDef(name=name):
347
+ names[name] += 1
348
+ return
349
+ case ast.Lambda():
350
+ return
351
+ case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
352
+ names.update(alias.asname or alias.name.partition(".")[0] for alias in aliases)
353
+ case (
354
+ ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
355
+ | ast.ExceptHandler(name=str(name))
356
+ | ast.MatchAs(name=str(name))
357
+ | ast.MatchStar(name=str(name))
358
+ | ast.MatchMapping(rest=str(name))
359
+ ):
360
+ names[name] += 1
361
+ case ast.comprehension():
362
+ _collect_bindings(node.iter, names)
363
+ for condition in node.ifs:
364
+ _collect_bindings(condition, names)
365
+ return
366
+ case _:
367
+ pass
368
+ for child in ast.iter_child_nodes(node):
369
+ _collect_bindings(child, names)
370
+
371
+
372
+ def _has_wildcard(tree: ast.Module) -> bool:
373
+ return any(
374
+ isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in ast.walk(tree)
375
+ )
376
+
377
+
378
+ def _root_mutated(tree: ast.Module, root: str) -> bool:
379
+ return any(
380
+ isinstance(node, (ast.Attribute, ast.Subscript))
381
+ and isinstance(node.ctx, (ast.Store, ast.Del))
382
+ and _root_name(node.value) == root
383
+ for node in ast.walk(tree)
384
+ )