sarj-python-lint 1.6.1__tar.gz → 1.7.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 (159) hide show
  1. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_registry.py +4 -0
  4. sarj_python_lint-1.7.0/src/sarj_python_lint/rules/_resource_provenance.py +184 -0
  5. sarj_python_lint-1.7.0/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +101 -0
  6. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +22 -5
  7. sarj_python_lint-1.7.0/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +143 -0
  8. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/.gitignore +0 -0
  9. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/LICENSE +0 -0
  10. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/README.md +0 -0
  11. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/__init__.py +0 -0
  12. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/__main__.py +0 -0
  13. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  14. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/json_boundary.py +0 -0
  22. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/py.typed +0 -0
  23. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/ratchet.py +0 -0
  24. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  31. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  32. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  33. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  34. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  35. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  36. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  38. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  39. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  40. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  41. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  42. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  43. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  44. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  45. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  46. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  47. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  48. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  49. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  50. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  51. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  52. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  53. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  54. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  55. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  56. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  57. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  58. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  59. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  60. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  61. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  62. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  63. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  64. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  65. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  66. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  67. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  68. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  69. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  70. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  71. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  72. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  73. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  74. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  75. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  76. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  77. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  78. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  79. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  80. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  81. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  82. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  83. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  84. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  85. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  86. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  87. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  88. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  89. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  90. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  91. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  92. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  93. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  94. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  95. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  96. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  97. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  98. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  99. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  100. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  101. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  102. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  103. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  104. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  105. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  106. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  107. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  108. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  109. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  110. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  111. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  112. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  113. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  114. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  115. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  116. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  117. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  118. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  119. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  120. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  121. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  122. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  123. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  124. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  125. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  126. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  127. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  128. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  129. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  130. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  131. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  132. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  133. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  134. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  135. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  136. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  137. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  138. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  139. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  140. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  141. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  142. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  143. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  144. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  145. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  146. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  147. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  148. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  149. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  150. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  151. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  152. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  153. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  154. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  155. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  156. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  157. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  158. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  159. {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.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.6.1
3
+ Version: 1.7.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.6.1"
3
+ version = "1.7.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" }]
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from types import MappingProxyType
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from sarj_python_lint.rules.async_cleanup_registered_synchronously import AsyncCleanupRegisteredSynchronously
6
7
  from sarj_python_lint.rules.async_mock_call_without_await_assertion import AsyncMockCallWithoutAwaitAssertion
7
8
  from sarj_python_lint.rules.complex_postgres_query_requires_architecture_review import (
8
9
  ComplexPostgresQueryRequiresArchitectureReview,
@@ -171,6 +172,7 @@ from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDele
171
172
  from sarj_python_lint.rules.store_insert_requires_on_conflict import (
172
173
  StoreInsertRequiresOnConflict,
173
174
  )
175
+ from sarj_python_lint.rules.subprocess_kill_without_reap import SubprocessKillWithoutReap
174
176
  from sarj_python_lint.rules.timestamp_order_requires_tiebreaker import TimestampOrderRequiresTiebreaker
175
177
  from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
176
178
  from sarj_python_lint.rules.typed_error_reasons import TypedErrorReasons
@@ -186,6 +188,8 @@ if TYPE_CHECKING:
186
188
 
187
189
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
188
190
  {
191
+ AsyncCleanupRegisteredSynchronously.id: AsyncCleanupRegisteredSynchronously,
192
+ SubprocessKillWithoutReap.id: SubprocessKillWithoutReap,
189
193
  NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
190
194
  RequireExplicitPsycopgTransaction.id: RequireExplicitPsycopgTransaction,
191
195
  AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import defaultdict
5
+ from typing import TYPE_CHECKING, final
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from sarj_python_lint._file_context import PythonFileContext
10
+
11
+
12
+ _SCOPES = (
13
+ ast.Module,
14
+ ast.FunctionDef,
15
+ ast.AsyncFunctionDef,
16
+ ast.ClassDef,
17
+ ast.Lambda,
18
+ ast.ListComp,
19
+ ast.SetComp,
20
+ ast.DictComp,
21
+ ast.GeneratorExp,
22
+ )
23
+
24
+
25
+ @final
26
+ class ResourceProvenance:
27
+ def __init__(self, context: PythonFileContext) -> None:
28
+ self.context = context
29
+ self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
30
+ self.reads: dict[str, list[ast.Name]] = defaultdict(list)
31
+ self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
32
+ self.wildcard = False
33
+ self.mutated: set[str] = set()
34
+ for node in context.nodes(ast.AST):
35
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
36
+ self.reads[node.id].append(node)
37
+
38
+ for name in _bound_names(node):
39
+ self.bindings[self.scope(node)][name].append(node)
40
+ self.wildcard |= name == "*"
41
+ if isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store | ast.Del):
42
+ root = _root_name(node)
43
+ if root is not None:
44
+ self.mutated.add(root)
45
+
46
+ def scope(self, node: ast.AST) -> ast.AST | None:
47
+ parent = self.context.parents.get(node)
48
+ while parent is not None and not isinstance(parent, _SCOPES):
49
+ parent = self.context.parents.get(parent)
50
+ return parent
51
+
52
+ def binding(self, name: str, at: ast.AST) -> ast.AST | None:
53
+ scope = self.scope(at)
54
+ while scope is not None:
55
+ nodes = self.bindings.get(scope, {}).get(name)
56
+ if nodes:
57
+ return nodes[0] if len(nodes) == 1 else None
58
+ scope = self.scope(scope)
59
+ return None
60
+
61
+ def imported(self, node: ast.expr, qualified: str) -> bool:
62
+ root = _root_name(node)
63
+ if self.wildcard or root is None or root in self.mutated:
64
+ return False
65
+ binding = self.binding(root, node)
66
+ for read in self.reads.get(root, ()):
67
+ parent = self.context.parents.get(read)
68
+ if isinstance(parent, ast.Attribute | ast.ExceptHandler):
69
+ continue
70
+ if not (isinstance(parent, ast.Call) and parent.func is read):
71
+ return False
72
+ return (
73
+ isinstance(binding, ast.alias)
74
+ and isinstance(self.scope(binding), ast.Module)
75
+ and self.context.imports.resolved_qualified_name(node) == qualified
76
+ )
77
+
78
+ def constructor(self, name: ast.Name, at: ast.AST) -> ast.Call | None:
79
+ binding = self.binding(name.id, at)
80
+ if not isinstance(binding, ast.Name):
81
+ return None
82
+ statement = self.context.parents.get(binding)
83
+ if isinstance(statement, ast.withitem):
84
+ owner = self.context.parents.get(statement)
85
+ if (
86
+ binding is statement.optional_vars
87
+ and self.contains(owner, at)
88
+ and isinstance(statement.context_expr, ast.Call)
89
+ ):
90
+ return statement.context_expr
91
+ return None
92
+ value = self.assignment_value(binding, at)
93
+ return value if isinstance(value, ast.Call) else None
94
+
95
+ def assignment_value(self, binding: ast.Name, at: ast.AST) -> ast.expr | None:
96
+ statement = self.context.parents.get(binding)
97
+ if not isinstance(statement, ast.Assign | ast.AnnAssign):
98
+ return None
99
+ if isinstance(statement, ast.Assign) and len(statement.targets) != 1:
100
+ return None
101
+ if not isinstance(self.context.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
102
+ return None
103
+ return statement.value if statement.lineno < getattr(at, "lineno", 0) else None
104
+
105
+ def async_function(self, value: ast.expr, at: ast.AST) -> ast.AsyncFunctionDef | None:
106
+ seen: set[str] = set()
107
+ while isinstance(value, ast.Name) and value.id not in seen:
108
+ seen.add(value.id)
109
+ if value.id in self.mutated:
110
+ return None
111
+ binding = self.binding(value.id, at)
112
+ if isinstance(binding, ast.AsyncFunctionDef):
113
+ owner = self.context.parents.get(binding)
114
+ if not isinstance(owner, ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
115
+ return None
116
+ if binding.decorator_list or any(
117
+ isinstance(node, ast.Yield | ast.YieldFrom) for node in ast.walk(binding)
118
+ ):
119
+ return None
120
+ return binding
121
+ if not isinstance(binding, ast.Name):
122
+ return None
123
+ resolved = self.assignment_value(binding, at)
124
+ if resolved is None:
125
+ return None
126
+ value, at = resolved, binding
127
+ return None
128
+
129
+ def unescaped(self, name: ast.Name, at: ast.AST, *, allow_context: bool = False) -> bool:
130
+ binding = self.binding(name.id, at)
131
+ if binding is None or name.id in self.mutated:
132
+ return False
133
+ key = (binding, allow_context)
134
+ if key not in self.escape_cache:
135
+ self.escape_cache[key] = all(
136
+ self._method_or_context_read(read, allow_context=allow_context)
137
+ for read in self.reads.get(name.id, ())
138
+ if self.binding(read.id, read) is binding
139
+ )
140
+ return self.escape_cache[key]
141
+
142
+ def _method_or_context_read(self, read: ast.Name, *, allow_context: bool) -> bool:
143
+ parent = self.context.parents.get(read)
144
+ if allow_context and isinstance(parent, ast.withitem) and parent.context_expr is read:
145
+ return True
146
+ if not isinstance(parent, ast.Attribute) or parent.value is not read:
147
+ return False
148
+ call = self.context.parents.get(parent)
149
+ return isinstance(call, ast.Call) and call.func is parent
150
+
151
+ def contains(self, owner: ast.AST | None, node: ast.AST) -> bool:
152
+ while node in self.context.parents:
153
+ node = self.context.parents[node]
154
+ if node is owner:
155
+ return True
156
+ return False
157
+
158
+
159
+ def _root_name(node: ast.expr) -> str | None:
160
+ while isinstance(node, ast.Attribute):
161
+ node = node.value
162
+ return node.id if isinstance(node, ast.Name) else None
163
+
164
+
165
+ def _bound_names(node: ast.AST) -> tuple[str, ...]:
166
+ match node:
167
+ case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
168
+ return (name,)
169
+ case ast.alias():
170
+ return (node.asname or node.name.partition(".")[0],)
171
+ case (
172
+ ast.FunctionDef(name=name)
173
+ | ast.AsyncFunctionDef(name=name)
174
+ | ast.ClassDef(name=name)
175
+ | ast.ExceptHandler(name=str() as name)
176
+ | ast.MatchAs(name=str() as name)
177
+ | ast.MatchStar(name=str() as name)
178
+ | ast.MatchMapping(rest=str() as name)
179
+ ):
180
+ return (name,)
181
+ case ast.Global(names=names) | ast.Nonlocal(names=names):
182
+ return tuple(names)
183
+ case _:
184
+ return ()
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from sarj_python_lint._file_context import PythonFileContext
24
+
25
+
26
+ _BAD = "from contextlib import AsyncExitStack\nasync def close():\n await release()\nasync def run():\n async with AsyncExitStack() as stack:\n stack.callback(close)\n"
27
+
28
+
29
+ class AsyncCleanupRegisteredSynchronously(Rule):
30
+ id: str = "async-cleanup-registered-synchronously"
31
+ code: str = "SARJ463"
32
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
33
+ default_level=Severity.WARNING,
34
+ summary="Do not register a proven async cleanup function with ExitStack.callback.",
35
+ rationale="The synchronous callback API discards the returned coroutine without awaiting it, so the cleanup body never runs.",
36
+ remediation="Use AsyncExitStack and register the coroutine function with push_async_callback.",
37
+ category=RuleCategory.CORRECTNESS,
38
+ autofix=AutofixPolicy.NONE,
39
+ limitations=(
40
+ "Requires a proven contextlib ExitStack or AsyncExitStack constructor and an undecorated, visible async function, optionally through simple function aliases.",
41
+ "Unknown methods/callables, decorated functions, async generators, conditional bindings, transferred stacks, wildcard imports, and generated files are excluded.",
42
+ "Resource aliases and interprocedural mutation are not inferred. No autofix is offered because executing previously dropped cleanup can raise new exceptions.",
43
+ ),
44
+ examples=(
45
+ RuleExample(
46
+ example_id="dropped-cleanup",
47
+ title="Synchronous callbacks do not await cleanup",
48
+ outcome=ExampleOutcome.MATCH,
49
+ files=(ExampleFile.python("app/resources.py", _BAD),),
50
+ focus_path=PurePosixPath("app/resources.py"),
51
+ expected_count=1,
52
+ public=True,
53
+ ),
54
+ RuleExample(
55
+ example_id="awaited-cleanup",
56
+ title="Register cleanup with the async API",
57
+ outcome=ExampleOutcome.NO_MATCH,
58
+ files=(
59
+ ExampleFile.python("app/resources.py", _BAD.replace("stack.callback", "stack.push_async_callback")),
60
+ ),
61
+ focus_path=PurePosixPath("app/resources.py"),
62
+ expected_count=0,
63
+ public=True,
64
+ ),
65
+ ),
66
+ )
67
+ description: str = documentation.summary
68
+
69
+ @override
70
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
71
+ if "callback" not in context.source or context.generated or context.tree is None:
72
+ return []
73
+ provenance = ResourceProvenance(context)
74
+ findings: list[Diagnostic] = []
75
+ for call in context.nodes(ast.Call):
76
+ if not _wrong_callback(call, provenance) or is_suppressed(context.source_lines, call.lineno, self.code):
77
+ continue
78
+ findings.append(
79
+ Diagnostic(
80
+ path=context.path,
81
+ line=call.lineno,
82
+ col=call.col_offset + 1,
83
+ code=self.code,
84
+ severity=Severity.WARNING,
85
+ message="synchronous ExitStack.callback does not await this async cleanup function; use AsyncExitStack.push_async_callback",
86
+ )
87
+ )
88
+ return findings
89
+
90
+
91
+ def _wrong_callback(call: ast.Call, provenance: ResourceProvenance) -> bool:
92
+ if not isinstance(call.func, ast.Attribute) or call.func.attr != "callback" or not call.args:
93
+ return False
94
+ receiver = call.func.value
95
+ if not isinstance(receiver, ast.Name) or not provenance.unescaped(receiver, call, allow_context=True):
96
+ return False
97
+ constructor = provenance.constructor(receiver, call)
98
+ if constructor is None or constructor.args or constructor.keywords:
99
+ return False
100
+ known = any(provenance.imported(constructor.func, f"contextlib.{name}") for name in ("ExitStack", "AsyncExitStack"))
101
+ return known and provenance.async_function(call.args[0], call) is not None
@@ -36,13 +36,13 @@ class PreferRegexFullmatch(Rule):
36
36
  code: str = "SARJ460"
37
37
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
38
  default_level=Severity.WARNING,
39
- summary="An anchored regex match rejects invalid values without requiring a full-string match.",
39
+ summary="An anchored regex validation predicate does not require a full-string match.",
40
40
  rationale="The regex `$` anchor can match before a final newline, so an anchored match can accept a value that was not fully validated.",
41
41
  remediation="Use `re.fullmatch` or the compiled pattern's `fullmatch` method for whole-value validation; review whether a terminal newline is intentionally allowed.",
42
42
  category=RuleCategory.CORRECTNESS,
43
43
  autofix=AutofixPolicy.NONE,
44
44
  limitations=(
45
- "Only direct negative match or `is None` conditions whose sole rejection statement raises are checked; extraction, search, compound conditions, and return-based validators are excluded.",
45
+ "Checks direct negative match/is-None rejection guards and returns of bool(match), not match, or match is/is-not None. Match-object returns, extraction, search, and compound conditions are excluded.",
46
46
  "Patterns must be valid string or bytes literals bounded by `^` or `\\A` and an unescaped outer `$`; top-level alternation, inline extension groups other than noncapturing groups, and flags other than literal zero are excluded.",
47
47
  "Only imported standard-library re functions and unique, unconditional module or same-function compile bindings are resolved. Rebinding, import shadowing, escaped regex objects or modules, explicit match positions, tests, and generated files are excluded.",
48
48
  "Binding checks are deliberately file-wide, so unrelated same-named locals can suppress findings. Dynamic monkeypatching and interprocedural changes are not inferred.",
@@ -85,8 +85,8 @@ class PreferRegexFullmatch(Rule):
85
85
  )
86
86
  compiled = _compiled_patterns(context, bindings, mutated)
87
87
  findings: list[Diagnostic] = []
88
- for guard in context.nodes(ast.If):
89
- call = _rejection_call(guard)
88
+ for guard in context.nodes(ast.If, ast.Return):
89
+ call = _validation_call(guard, context, bindings)
90
90
  if call is None or is_suppressed(context.source_lines, call.lineno, self.code):
91
91
  continue
92
92
  pattern = _match_pattern(context, call, bindings, mutated, compiled)
@@ -98,13 +98,30 @@ class PreferRegexFullmatch(Rule):
98
98
  line=call.lineno,
99
99
  col=call.col_offset + 1,
100
100
  code=self.code,
101
- message="anchored match is used to reject invalid values, but `$` can accept a final newline; use fullmatch for whole-value validation and review newline handling.",
101
+ message="anchored match is used as a validation predicate, but `$` can accept a final newline; use fullmatch for whole-value validation and review newline handling.",
102
102
  severity=Severity.WARNING,
103
103
  )
104
104
  )
105
105
  return sorted(findings, key=lambda item: (item.line, item.col))
106
106
 
107
107
 
108
+ def _validation_call(node: ast.If | ast.Return, context: PythonFileContext, bindings: Counter[str]) -> ast.Call | None:
109
+ if isinstance(node, ast.If):
110
+ return _rejection_call(node)
111
+ match node.value:
112
+ case (
113
+ ast.UnaryOp(op=ast.Not(), operand=ast.Call() as call)
114
+ | ast.Compare(left=ast.Call() as call, ops=[ast.Is() | ast.IsNot()], comparators=[ast.Constant(value=None)])
115
+ ):
116
+ return call
117
+ case ast.Call(func=ast.Name(id="bool"), args=[ast.Call() as call], keywords=[]):
118
+ if context.imports.builtin_is_unshadowed("bool") and bindings["bool"] == 0:
119
+ return call
120
+ case _:
121
+ pass
122
+ return None
123
+
124
+
108
125
  def _rejection_call(guard: ast.If) -> ast.Call | None:
109
126
  if len(guard.body) != 1 or not isinstance(guard.body[0], ast.Raise):
110
127
  return None
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from sarj_python_lint._file_context import PythonFileContext
24
+
25
+
26
+ _BAD = "import subprocess\ndef run(args):\n process = subprocess.Popen(args)\n try:\n process.communicate(timeout=1)\n except subprocess.TimeoutExpired:\n process.kill()\n"
27
+ _REAP_METHODS = frozenset({"wait", "communicate", "poll"})
28
+
29
+
30
+ class SubprocessKillWithoutReap(Rule):
31
+ id: str = "subprocess-kill-without-reap"
32
+ code: str = "SARJ464"
33
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
34
+ default_level=Severity.WARNING,
35
+ summary="Reap a locally owned subprocess after killing it on timeout.",
36
+ rationale="Sending a kill signal does not wait for process termination or collect the exit status; deterministic cleanup still requires waiting or communication.",
37
+ remediation="After kill(), call communicate() when owning captured pipes, or wait() when output is handled separately; an enclosing Popen context manager can also own cleanup.",
38
+ category=RuleCategory.CORRECTNESS,
39
+ autofix=AutofixPolicy.NONE,
40
+ limitations=(
41
+ "Requires a unique, dominating local subprocess.Popen assignment and a resolved TimeoutExpired handler ending with a direct kill() after that process's wait or communicate.",
42
+ "Borrowed processes, custom factories, context-managed ownership, aliases, escaped handles or cleanup callbacks, and any later wait/communicate/poll are excluded conservatively.",
43
+ "Only direct timeout handlers are checked. Cross-function lifecycle, fields, dynamic mutation, arbitrary exit paths, and generated files are not analyzed. Choosing wait versus communicate requires reviewing output ownership.",
44
+ ),
45
+ examples=(
46
+ RuleExample(
47
+ example_id="unreaped-child",
48
+ title="Killing does not complete cleanup",
49
+ outcome=ExampleOutcome.MATCH,
50
+ files=(ExampleFile.python("app/processes.py", _BAD),),
51
+ focus_path=PurePosixPath("app/processes.py"),
52
+ expected_count=1,
53
+ public=True,
54
+ ),
55
+ RuleExample(
56
+ example_id="reaped-child",
57
+ title="Drain and wait after killing",
58
+ outcome=ExampleOutcome.NO_MATCH,
59
+ files=(ExampleFile.python("app/processes.py", _BAD + " process.communicate()\n"),),
60
+ focus_path=PurePosixPath("app/processes.py"),
61
+ expected_count=0,
62
+ public=True,
63
+ ),
64
+ ),
65
+ )
66
+ description: str = documentation.summary
67
+
68
+ @override
69
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
70
+ if "kill" not in context.source or context.generated or context.tree is None:
71
+ return []
72
+ provenance = ResourceProvenance(context)
73
+ findings: list[Diagnostic] = []
74
+ for handler in context.nodes(ast.ExceptHandler):
75
+ call = _unreaped_kill(context, provenance, handler)
76
+ if call is None or is_suppressed(context.source_lines, call.lineno, self.code):
77
+ continue
78
+ findings.append(
79
+ Diagnostic(
80
+ path=context.path,
81
+ line=call.lineno,
82
+ col=call.col_offset + 1,
83
+ code=self.code,
84
+ severity=Severity.WARNING,
85
+ message="this Popen timeout handler kills the child without a subsequent reap; call communicate() for owned pipes or wait() when output is handled separately",
86
+ )
87
+ )
88
+ return findings
89
+
90
+
91
+ def _unreaped_kill(
92
+ context: PythonFileContext, provenance: ResourceProvenance, handler: ast.ExceptHandler
93
+ ) -> ast.Call | None:
94
+ if handler.type is None or not provenance.imported(handler.type, "subprocess.TimeoutExpired"):
95
+ return None
96
+ last = handler.body[-1]
97
+ if not isinstance(last, ast.Expr) or not isinstance(last.value, ast.Call):
98
+ return None
99
+ call = last.value
100
+ receiver = _method_receiver(call, frozenset({"kill"}))
101
+ if receiver is None or call.args or call.keywords or not provenance.unescaped(receiver, call):
102
+ return None
103
+ binding = provenance.binding(receiver.id, call)
104
+ if not isinstance(binding, ast.Name) or provenance.scope(binding) is not provenance.scope(call):
105
+ return None
106
+ if not isinstance(context.parents.get(binding), ast.Assign | ast.AnnAssign):
107
+ return None
108
+ if not isinstance(provenance.scope(call), ast.FunctionDef | ast.AsyncFunctionDef):
109
+ return None
110
+ constructor = provenance.constructor(receiver, call)
111
+ if constructor is None or not provenance.imported(constructor.func, "subprocess.Popen"):
112
+ return None
113
+ owner = context.parents.get(handler)
114
+ if not isinstance(owner, ast.Try) or not _waits_for_process(owner.body, receiver.id):
115
+ return None
116
+ for other in context.nodes(ast.Call):
117
+ other_receiver = _method_receiver(other, _REAP_METHODS)
118
+ if (
119
+ other_receiver is not None
120
+ and other.lineno > call.lineno
121
+ and provenance.binding(other_receiver.id, other) is binding
122
+ ):
123
+ return None
124
+ return call
125
+
126
+
127
+ def _waits_for_process(body: list[ast.stmt], name: str) -> bool:
128
+ for statement in body:
129
+ value = statement.value if isinstance(statement, ast.Expr | ast.Assign | ast.AnnAssign) else None
130
+ if (
131
+ isinstance(value, ast.Call)
132
+ and (receiver := _method_receiver(value, frozenset({"wait", "communicate"}))) is not None
133
+ and receiver.id == name
134
+ ):
135
+ return True
136
+ return False
137
+
138
+
139
+ def _method_receiver(call: ast.Call, methods: frozenset[str]) -> ast.Name | None:
140
+ function = call.func
141
+ if isinstance(function, ast.Attribute) and function.attr in methods and isinstance(function.value, ast.Name):
142
+ return function.value
143
+ return None