sarj-python-lint 1.28.1__tar.gz → 1.28.2__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 (192) hide show
  1. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_resource_provenance.py +10 -0
  4. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +23 -3
  5. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +8 -6
  6. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/unused_mock_setup.py +159 -78
  7. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/.gitignore +0 -0
  8. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/LICENSE +0 -0
  9. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/README.md +0 -0
  10. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_source.py +0 -0
  19. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  22. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/json_boundary.py +0 -0
  23. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  28. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  30. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  31. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  33. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  34. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  35. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  36. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  37. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  38. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  39. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  40. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  41. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  42. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  43. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  44. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  45. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  46. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  47. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  48. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  49. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  50. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  51. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  52. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  53. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  54. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  55. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  56. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  57. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  58. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  59. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  60. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  61. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  62. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  63. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  64. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  65. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  66. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  67. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  68. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  69. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  70. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  71. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  72. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  73. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  74. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  75. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  76. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  77. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  78. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  79. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  80. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  81. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  82. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  83. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  84. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  85. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  86. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  87. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  88. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  89. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  90. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  91. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  92. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  93. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  94. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  95. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  96. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  97. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  98. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  99. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  100. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  101. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  102. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  103. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  104. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  105. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  106. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  107. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  108. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  109. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  110. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  111. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  112. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  113. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  114. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  115. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  116. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  117. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  118. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  119. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  120. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  121. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  122. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  123. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  124. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  125. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  126. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  127. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  128. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  129. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  130. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  131. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  132. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  133. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  134. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  135. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  136. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  137. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  138. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  139. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  140. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  141. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  142. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  143. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  144. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  145. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  146. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  147. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  148. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  149. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  150. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  151. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  152. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  153. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  154. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  155. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  156. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  157. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  158. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  159. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  160. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  161. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  162. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  163. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  164. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  165. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  166. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  167. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  168. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  169. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  170. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  171. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  172. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  173. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  174. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  175. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  176. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  177. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  178. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  179. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  180. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  181. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  182. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  183. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  184. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  185. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  186. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  187. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  188. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  189. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  190. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  191. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  192. {sarj_python_lint-1.28.1 → sarj_python_lint-1.28.2}/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.28.1
3
+ Version: 1.28.2
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.28.1"
3
+ version = "1.28.2"
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" }]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from collections import defaultdict
5
+ from functools import cached_property
5
6
  from typing import TYPE_CHECKING, final
6
7
 
7
8
 
@@ -44,6 +45,15 @@ class ResourceProvenance:
44
45
  if root is not None:
45
46
  self.mutated.add(root)
46
47
 
48
+ @cached_property
49
+ def builtin_namespace_untouched(self) -> bool:
50
+ return not any(
51
+ (isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
52
+ or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
53
+ or (isinstance(node, ast.Name) and node.id == "__builtins__")
54
+ for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
55
+ )
56
+
47
57
  def scope(self, node: ast.AST) -> ast.AST | None:
48
58
  parent = self.context.parents.get(node)
49
59
  while parent is not None and not isinstance(parent, _SCOPES):
@@ -59,8 +59,8 @@ class DuplicateAdjacentExceptionHandler(ProjectRule):
59
59
  autofix=AutofixPolicy.NONE,
60
60
  limitations=(
61
61
  "Only ordinary try statements with adjacent handlers, identical exception bindings, executable ASTs and comments are compared; exception groups and bare re-raise/pass handlers are excluded.",
62
- "Handler types must be simple names or tuples of unshadowed builtin or statically resolved first-party Exception subclasses, with unconditional stable declarations/imports.",
63
- "Dynamic namespace access, unknown external exception classes, generated/vendor sources, different comments, and intervening handlers are excluded. No autofix preserves intentional translation seams.",
62
+ "Handler types must be simple names or tuples of unshadowed builtin or statically resolved first-party Exception subclasses, with stable declarations/imports initialized before the enclosing module statement. Declarations after a callable's definition are unresolved and excluded.",
63
+ "Dynamic or explicit builtin namespace access, unknown external exception classes, generated/vendor sources, different comments, and intervening handlers are excluded. Unseen external builtin mutations are not resolved. No autofix preserves intentional translation seams.",
64
64
  ),
65
65
  examples=tuple(
66
66
  RuleExample(
@@ -88,6 +88,8 @@ class DuplicateAdjacentExceptionHandler(ProjectRule):
88
88
  project = context.session.project or ProjectIndexSet.single(context.path, context.source)
89
89
  unit = project.unit_or_source(context.path, context.source, tree)
90
90
  provenance = ResourceProvenance(context)
91
+ if not provenance.builtin_namespace_untouched:
92
+ return []
91
93
  comments = scan_comments(context.source)
92
94
  findings: list[Diagnostic] = []
93
95
  for statement in context.nodes(ast.Try):
@@ -176,7 +178,11 @@ def _first_party_exception(
176
178
  from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer until the rule registry finishes loading.
177
179
 
178
180
  binding = provenance.binding(expression.id, expression)
179
- if not isinstance(binding, ast.ClassDef | ast.alias) or unit is None:
181
+ if (
182
+ not isinstance(binding, ast.ClassDef | ast.alias)
183
+ or unit is None
184
+ or not _initialized_before(binding, expression, context)
185
+ ):
180
186
  return False
181
187
  if isinstance(binding, ast.alias):
182
188
  statement = context.parents.get(binding)
@@ -200,6 +206,7 @@ def _first_party_exception(
200
206
  )
201
207
  return (
202
208
  declaration is not None
209
+ and definition_provenance.builtin_namespace_untouched
203
210
  and stable_class(definition_context, declaration, definition_provenance)
204
211
  and len(declaration.bases) == 1
205
212
  and _safe_types(
@@ -211,3 +218,16 @@ def _first_party_exception(
211
218
  seen=seen | {summary.symbol},
212
219
  )
213
220
  )
221
+
222
+
223
+ def _initialized_before(binding: ast.ClassDef | ast.alias, read: ast.Name, context: PythonFileContext) -> bool:
224
+ declaration = context.parents.get(binding) if isinstance(binding, ast.alias) else binding
225
+ boundary: ast.AST = read
226
+ while (parent := context.parents.get(boundary)) is not None and parent is not context.tree:
227
+ boundary = parent
228
+ return (
229
+ isinstance(declaration, ast.stmt)
230
+ and context.parents.get(declaration) is context.tree
231
+ and isinstance(boundary, ast.stmt)
232
+ and (declaration.end_lineno or declaration.lineno) < boundary.lineno
233
+ )
@@ -57,10 +57,10 @@ class OpaqueParametrizeCaseNeedsId(Rule):
57
57
  code: str = "SARJ042"
58
58
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
59
59
  default_level=Severity.WARNING,
60
- summary="Opaque static pytest parameter cases rely on argument-name-and-index fallback IDs.",
61
- rationale="Fallback IDs such as `payload0` are hard to diagnose and silently change when the table is reordered.",
60
+ summary="Opaque static pytest parameter cases lack table-local semantic IDs.",
61
+ rationale="Without a semantic naming owner, fallback IDs such as `payload0` are hard to diagnose and silently change when the table is reordered.",
62
62
  remediation=(
63
- "Give each opaque row a semantic `pytest.param(..., id=...)`, or use a semantic callable `ids=` when one "
63
+ "Check the owning pytest_make_parametrize_id hook before adding IDs; reuse an existing semantic naming owner. Otherwise give each opaque row a semantic `pytest.param(..., id=...)`, or use a semantic callable `ids=` when one "
64
64
  "mapping applies to the entire table."
65
65
  ),
66
66
  category=RuleCategory.TESTING,
@@ -129,10 +129,12 @@ class OpaqueParametrizeCaseNeedsId(Rule):
129
129
 
130
130
 
131
131
  def _message(count: int) -> str:
132
- subject = "case relies" if count == 1 else "cases rely"
132
+ subject = "case lacks" if count == 1 else "cases lack"
133
133
  return (
134
- f"{count} {subject} on pytest's argument-name-and-index fallback (for example, `payload0`), which renumbers "
135
- 'when rows are reordered. Add semantic `pytest.param(..., id="...")` values or a semantic callable `ids=`.'
134
+ f"{count} {subject} a table-local semantic ID. Check the owning `pytest_make_parametrize_id` hook before "
135
+ 'adding IDs; reuse an existing semantic naming owner, or add semantic `pytest.param(..., id="...")` values '
136
+ "or a semantic callable `ids=`. Without a semantic ID, pytest may use an argument-name-and-index fallback "
137
+ "(for example, `payload0`), which renumbers when rows are reordered."
136
138
  )
137
139
 
138
140
 
@@ -1,8 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ import builtins
4
5
  from pathlib import PurePosixPath
5
- from typing import TYPE_CHECKING, ClassVar, NamedTuple, override
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ ClassVar,
9
+ NamedTuple,
10
+ cast, # ruff: ignore[banned-api] -- vars exposes Any; the object view permits native exception narrowing.
11
+ override,
12
+ )
6
13
 
7
14
  from sarj_python_lint.rule_base import (
8
15
  AutofixPolicy,
@@ -16,18 +23,23 @@ from sarj_python_lint.rule_base import (
16
23
  Severity,
17
24
  )
18
25
  from sarj_python_lint.rules._ast_index import children, walk
26
+ from sarj_python_lint.rules._closed_helpers import dynamic_namespace
19
27
  from sarj_python_lint.rules._paths import is_test_path
28
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
20
29
 
21
30
 
22
31
  if TYPE_CHECKING:
23
- from collections.abc import Iterator
32
+ from collections.abc import Iterator, Mapping
24
33
 
25
34
  from sarj_python_lint._file_context import PythonFileContext
26
- from sarj_python_lint.rules._imports import ImportIndex
27
35
 
28
36
 
29
37
  # The two attributes that *configure* what a mock does when called.
30
38
  _CONFIG_ATTRS = frozenset({"return_value", "side_effect"})
39
+ _BUILTIN_NAMESPACE = cast("Mapping[str, object]", vars(builtins))
40
+ _EXCEPTION_TYPES = frozenset(
41
+ name for name, value in _BUILTIN_NAMESPACE.items() if isinstance(value, type) and issubclass(value, BaseException)
42
+ )
31
43
 
32
44
  # Callables whose constructor keywords configure the returned double. Import
33
45
  # resolution is required for bare/module-qualified spellings; the pytest-mock
@@ -45,6 +57,7 @@ _MOCK_CONSTRUCTORS = frozenset(
45
57
  _MOCKER_CONFIGURATORS = _MOCK_CONSTRUCTORS | {"patch"}
46
58
  _UNITTEST_MOCK = frozenset({"unittest.mock"})
47
59
  _MOCKER = "mocker"
60
+ _PATCH_OBJECT_POSITIONAL_ARGUMENTS = 2
48
61
 
49
62
  # Assertions that the mock was never reached.
50
63
  _NOT_CALLED_ASSERTIONS = frozenset({"assert_not_called", "assert_not_awaited"})
@@ -89,14 +102,14 @@ class UnusedMockSetup(Rule):
89
102
  "terminal exact-path assertion proves the configured mock was never called."
90
103
  ),
91
104
  remediation=(
92
- "Delete the earlier overwritten configuration, or place the intended action before reconfiguration; "
105
+ "Delete the earlier inert configuration, retaining the mock constructor and binding when removing a keyword, or place the intended action before reconfiguration; "
93
106
  "delete behavior configuration contradicted by a terminal never-called assertion."
94
107
  ),
95
108
  category=RuleCategory.TESTING,
96
109
  autofix=AutofixPolicy.NONE,
97
110
  limitations=(
98
- "Only non-generated test paths and mock values proven from unittest.mock or an unshadowed pytest-mock fixture are analyzed.",
99
- "Only same-block overwrites and exact-path terminal never-called assertions are checked; uncertain evaluation, control flow, reads, and escapes are excluded.",
111
+ "Only non-generated test paths and stable local names unconditionally initialized from unittest.mock or an unshadowed pytest-mock fixture before configuration are analyzed. Dotted stores, auto-children, configured return-value chains, replacement or unpacked options, dynamic namespace access, and direct constructor-root or builtin namespace mutation are excluded. Unseen external mutations are not resolved.",
112
+ "Only same-block overwrites and exact-path terminal never-called assertions are checked; uncertain evaluation, native setter/iteration effects, custom hash/equality, control flow, reads, and terminal receiver escapes are excluded. Configuration values require literal/native primitive provenance.",
100
113
  ),
101
114
  examples=(
102
115
  RuleExample(
@@ -138,14 +151,16 @@ class UnusedMockSetup(Rule):
138
151
  if not is_test_path(path) or context.generated:
139
152
  return []
140
153
  tree = context.tree
141
- if tree is None:
154
+ if tree is None or dynamic_namespace(context):
142
155
  return []
143
156
 
144
- imports = context.imports
157
+ provenance = ResourceProvenance(context)
158
+ if not provenance.builtin_namespace_untouched:
159
+ return []
145
160
  seen: set[tuple[int, int]] = set()
146
161
  diags: list[Diagnostic] = []
147
162
  for fn in context.nodes(*_FUNC_NODES):
148
- for finding in _dead_setups(fn, imports):
163
+ for finding in _dead_setups(fn, provenance):
149
164
  position = (finding.node.lineno, finding.node.col_offset + 1)
150
165
  if position in seen:
151
166
  continue
@@ -164,20 +179,22 @@ class UnusedMockSetup(Rule):
164
179
  return diags
165
180
 
166
181
 
167
- def _dead_setups(fn: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex) -> Iterator[_Finding]:
168
- origins = _mock_origins(fn, imports)
182
+ def _dead_setups(fn: ast.FunctionDef | ast.AsyncFunctionDef, provenance: ResourceProvenance) -> Iterator[_Finding]:
183
+ origins = _mock_origins(fn, provenance)
169
184
  if not origins:
170
185
  return
171
- yield from _overwritten_before_use(fn, imports, origins)
172
- yield from _asserted_never_called(fn, origins)
186
+ yield from _overwritten_before_use(fn, origins, provenance)
187
+ yield from _asserted_never_called(fn, origins, provenance)
173
188
 
174
189
 
175
190
  def _overwritten_before_use(
176
- fn: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex, origins: frozenset[str]
191
+ fn: ast.FunctionDef | ast.AsyncFunctionDef,
192
+ origins: frozenset[str],
193
+ provenance: ResourceProvenance,
177
194
  ) -> Iterator[_Finding]:
178
195
  has_mocker_fixture = _has_unshadowed_mocker_fixture(fn)
179
196
  for block in _blocks(fn):
180
- yield from _overwritten_block_setups(block, imports, origins, has_mocker_fixture=has_mocker_fixture)
197
+ yield from _overwritten_block_setups(block, origins, provenance, has_mocker_fixture=has_mocker_fixture)
181
198
 
182
199
 
183
200
  def _has_unshadowed_mocker_fixture(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
@@ -202,39 +219,37 @@ def _owned_nodes(root: ast.AST) -> Iterator[ast.AST]:
202
219
  stack.extend(reversed(list(ast.iter_child_nodes(node))))
203
220
 
204
221
 
205
- def _mock_origins(fn: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex) -> frozenset[str]:
222
+ def _mock_origins(fn: ast.FunctionDef | ast.AsyncFunctionDef, provenance: ResourceProvenance) -> frozenset[str]:
206
223
  has_mocker_fixture = _has_unshadowed_mocker_fixture(fn)
207
- binding_counts: dict[str, int] = {}
208
- for argument in (*fn.args.posonlyargs, *fn.args.args, *fn.args.kwonlyargs):
209
- binding_counts[argument.arg] = binding_counts.get(argument.arg, 0) + 1
210
- for node in _owned_nodes(fn):
211
- if isinstance(node, (ast.Name, ast.Attribute)) and isinstance(node.ctx, (ast.Store, ast.Del)):
212
- path = _dotted(node)
213
- if path is not None:
214
- binding_counts[path] = binding_counts.get(path, 0) + 1
215
224
  origins: set[str] = set()
216
- for stmt in _scope_statements(fn):
225
+ for stmt in fn.body:
217
226
  match stmt:
218
- case ast.Assign(targets=[target], value=ast.Call() as call):
227
+ case ast.Assign(targets=[ast.Name() as target], value=ast.Call() as call):
219
228
  pass
220
- case ast.AnnAssign(target=target, value=ast.Call() as call):
229
+ case ast.AnnAssign(target=ast.Name() as target, value=ast.Call() as call):
221
230
  pass
222
231
  case _:
223
232
  continue
224
- path = _dotted(target)
225
- if (
226
- path is not None
227
- and binding_counts.get(path) == 1
228
- and _is_mock_configurator(call.func, imports, has_mocker_fixture=has_mocker_fixture)
233
+ if provenance.binding(target.id, call) is target and _is_mock_configurator(
234
+ call, provenance, has_mocker_fixture=has_mocker_fixture
229
235
  ):
230
- origins.add(path)
236
+ origins.add(target.id)
231
237
  return frozenset(origins)
232
238
 
233
239
 
234
- def _is_mock_configurator(func: ast.expr, imports: ImportIndex, *, has_mocker_fixture: bool) -> bool:
240
+ def _is_mock_configurator(call: ast.Call, provenance: ResourceProvenance, *, has_mocker_fixture: bool) -> bool:
241
+ func = call.func
242
+ path = _dotted(func)
243
+ if (
244
+ path is None
245
+ or path.partition(".")[0] in provenance.mutated
246
+ or any(isinstance(argument, ast.Starred) for argument in call.args)
247
+ or any(keyword.arg in {None, "new", "new_callable", "_mock_delegate"} for keyword in call.keywords)
248
+ ):
249
+ return False
235
250
  if has_mocker_fixture and isinstance(func, ast.Attribute):
236
251
  if isinstance(func.value, ast.Name) and func.value.id == _MOCKER:
237
- return func.attr in _MOCKER_CONFIGURATORS
252
+ return func.attr in _MOCKER_CONFIGURATORS and (func.attr != "patch" or len(call.args) <= 1)
238
253
  if (
239
254
  func.attr == "object"
240
255
  and isinstance(func.value, ast.Attribute)
@@ -242,11 +257,16 @@ def _is_mock_configurator(func: ast.expr, imports: ImportIndex, *, has_mocker_fi
242
257
  and isinstance(func.value.value, ast.Name)
243
258
  and func.value.value.id == _MOCKER
244
259
  ):
245
- return True
246
- return any(imports.resolves(func, sources=_UNITTEST_MOCK, symbol=symbol) for symbol in _MOCK_CONSTRUCTORS)
260
+ return len(call.args) <= _PATCH_OBJECT_POSITIONAL_ARGUMENTS
261
+ return any(
262
+ provenance.context.imports.resolves(func, sources=_UNITTEST_MOCK, symbol=symbol)
263
+ for symbol in _MOCK_CONSTRUCTORS
264
+ )
247
265
 
248
266
 
249
- def _asserted_never_called(fn: ast.FunctionDef | ast.AsyncFunctionDef, origins: frozenset[str]) -> Iterator[_Finding]:
267
+ def _asserted_never_called(
268
+ fn: ast.FunctionDef | ast.AsyncFunctionDef, origins: frozenset[str], provenance: ResourceProvenance
269
+ ) -> Iterator[_Finding]:
250
270
  if not fn.name.startswith("test_") or _is_fixture(fn):
251
271
  return
252
272
  introspected = _introspected_paths(fn)
@@ -256,8 +276,14 @@ def _asserted_never_called(fn: ast.FunctionDef | ast.AsyncFunctionDef, origins:
256
276
  for block in _blocks(fn):
257
277
  not_called = _not_called_assertions(block, origins)
258
278
  for stmt in block:
259
- target = _config_target(stmt, origins)
260
- if target is None or id(stmt) in loop_statements or target in loaded_configs:
279
+ target = _config_target(stmt, origins, provenance)
280
+ if (
281
+ target is None
282
+ or id(stmt) in loop_statements
283
+ or target in loaded_configs
284
+ or not _inert_configuration(stmt, target, provenance)
285
+ or _receiver_escapes(target.rsplit(".", 1)[0], stmt, provenance)
286
+ ):
261
287
  continue
262
288
  configured = target.rsplit(".", 1)[0]
263
289
  if any(_touches(configured, other) for other in introspected):
@@ -265,6 +291,22 @@ def _asserted_never_called(fn: ast.FunctionDef | ast.AsyncFunctionDef, origins:
265
291
  yield from _unused_asserted_setup(stmt, target, configured, not_called, last_effect)
266
292
 
267
293
 
294
+ def _receiver_escapes(name: str, at: ast.stmt, provenance: ResourceProvenance) -> bool:
295
+ binding = provenance.binding(name, at)
296
+ for read in provenance.reads.get(name, ()):
297
+ if provenance.binding(name, read) is not binding:
298
+ continue
299
+ parent = provenance.context.parents.get(read)
300
+ if not isinstance(parent, ast.Attribute) or parent.value is not read:
301
+ return True
302
+ if isinstance(parent.ctx, ast.Store) and parent.attr in _CONFIG_ATTRS:
303
+ continue
304
+ call = provenance.context.parents.get(parent)
305
+ if not (isinstance(call, ast.Call) and call.func is parent and parent.attr in _NOT_CALLED_ASSERTIONS):
306
+ return True
307
+ return False
308
+
309
+
268
310
  def _is_fixture(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
269
311
  return any(
270
312
  (isinstance(decorator, ast.Name) and decorator.id == "fixture")
@@ -327,22 +369,19 @@ def _child_blocks(node: ast.AST) -> list[list[ast.stmt]]:
327
369
  return [block for block in blocks if block]
328
370
 
329
371
 
330
- def _scope_statements(fn: ast.FunctionDef | ast.AsyncFunctionDef) -> Iterator[ast.stmt]:
331
- for block in _blocks(fn):
332
- yield from block
333
-
334
-
335
- def _config_target(stmt: ast.stmt, origins: frozenset[str]) -> str | None:
372
+ def _config_target(stmt: ast.stmt, origins: frozenset[str], provenance: ResourceProvenance) -> str | None:
336
373
  if not isinstance(stmt, ast.Assign) or len(stmt.targets) != 1:
337
374
  return None
338
375
  target = stmt.targets[0]
339
- if not isinstance(target, ast.Attribute) or target.attr not in _CONFIG_ATTRS:
340
- return None
341
- path = _dotted(target)
342
- if path is None:
376
+ if (
377
+ not isinstance(target, ast.Attribute)
378
+ or target.attr not in _CONFIG_ATTRS
379
+ or not isinstance(target.value, ast.Name)
380
+ or target.value.id not in origins
381
+ or provenance.constructor(target.value, stmt) is None
382
+ ):
343
383
  return None
344
- configured = path.rsplit(".", 1)[0]
345
- return path if any(_is_prefix(origin, configured) for origin in origins) else None
384
+ return f"{target.value.id}.{target.attr}"
346
385
 
347
386
 
348
387
  def _dotted(node: ast.expr) -> str | None:
@@ -357,20 +396,45 @@ def _dotted(node: ast.expr) -> str | None:
357
396
  return ".".join(reversed(parts))
358
397
 
359
398
 
360
- def _is_safe_value(node: ast.expr) -> bool:
399
+ def _is_safe_value(node: ast.expr, provenance: ResourceProvenance, *, hash_key: bool = False) -> bool:
361
400
  match node:
362
- case ast.Constant() | ast.Name():
401
+ case ast.Constant():
363
402
  return True
364
- case ast.List() | ast.Tuple() | ast.Set():
365
- return all(not isinstance(element, ast.Starred) and _is_safe_value(element) for element in node.elts)
403
+ case ast.Name():
404
+ binding = provenance.binding(node.id, node)
405
+ if binding is None:
406
+ return node.id in _EXCEPTION_TYPES and provenance.context.imports.builtin_is_unshadowed(node.id)
407
+ if not isinstance(binding, ast.Name) or provenance.scope(binding) is not provenance.scope(node):
408
+ return False
409
+ # Stable, already executed scalar assignments prove both initialization and native hashing.
410
+ return isinstance(provenance.assignment_value(binding, node), ast.Constant)
411
+ case ast.List() | ast.Tuple():
412
+ return (not hash_key or isinstance(node, ast.Tuple)) and all(
413
+ _is_safe_value(element, provenance, hash_key=hash_key) for element in node.elts
414
+ )
415
+ case ast.Set():
416
+ return not hash_key and all(_is_safe_value(element, provenance, hash_key=True) for element in node.elts)
366
417
  case ast.Dict():
367
- return all(key is not None and _is_safe_value(key) for key in node.keys) and all(
368
- _is_safe_value(value) for value in node.values
418
+ return (
419
+ not hash_key
420
+ and all(key is not None and _is_safe_value(key, provenance, hash_key=True) for key in node.keys)
421
+ and all(_is_safe_value(value, provenance) for value in node.values)
369
422
  )
370
423
  case _:
371
424
  return False
372
425
 
373
426
 
427
+ def _inert_configuration(stmt: ast.stmt, target: str, provenance: ResourceProvenance) -> bool:
428
+ if not isinstance(stmt, ast.Assign | ast.AnnAssign):
429
+ return False
430
+ value = stmt.value
431
+ assigned = stmt.targets[0] if isinstance(stmt, ast.Assign) else stmt.target
432
+ if isinstance(assigned, ast.Name) and isinstance(value, ast.Call):
433
+ attribute = target.rsplit(".", 1)[1]
434
+ value = next((keyword.value for keyword in value.keywords if keyword.arg == attribute), None)
435
+ return value is not None and _is_safe_value(value, provenance)
436
+
437
+
374
438
  def _not_called_assertions(statements: list[ast.stmt], origins: frozenset[str]) -> dict[str, int]:
375
439
  found: dict[str, int] = {}
376
440
  for stmt in statements:
@@ -437,15 +501,24 @@ def _touches(path: str, other: str) -> bool:
437
501
 
438
502
 
439
503
  def _overwritten_block_setups(
440
- block: list[ast.stmt], imports: ImportIndex, origins: frozenset[str], *, has_mocker_fixture: bool
504
+ block: list[ast.stmt],
505
+ origins: frozenset[str],
506
+ provenance: ResourceProvenance,
507
+ *,
508
+ has_mocker_fixture: bool,
441
509
  ) -> Iterator[_Finding]:
442
510
  pending: dict[str, ast.stmt] = {}
443
511
  for stmt in block:
444
- target = _config_target(stmt, origins)
512
+ target = _config_target(stmt, origins, provenance)
445
513
  if target is None:
446
- pending = _pending_after_statement(stmt, imports, origins, pending, has_mocker_fixture=has_mocker_fixture)
514
+ pending = _pending_after_statement(
515
+ stmt, origins, pending, provenance, has_mocker_fixture=has_mocker_fixture
516
+ )
447
517
  continue
448
- if _statement_observes_pending(stmt, pending):
518
+ if not _inert_configuration(stmt, target, provenance):
519
+ pending.clear()
520
+ continue
521
+ if _statement_observes_pending(stmt, pending, provenance):
449
522
  pending.clear()
450
523
  previous = pending.get(target)
451
524
  if previous is not None:
@@ -453,14 +526,18 @@ def _overwritten_block_setups(
453
526
  previous,
454
527
  (
455
528
  f"`{target}` is set here and overwritten on line {stmt.lineno} with nothing in "
456
- "between that could call the mock, so this value is never used. Delete the dead "
457
- "setup, or move the code under test between the two configurations"
529
+ "between that could call the mock, so this value is never used. "
530
+ + (
531
+ "Remove the overwritten constructor keyword, retaining the mock constructor and binding"
532
+ if isinstance(previous, ast.Assign | ast.AnnAssign) and isinstance(previous.value, ast.Call)
533
+ else "Delete the dead setup, or move the code under test between the two configurations"
534
+ )
458
535
  ),
459
536
  )
460
537
  pending[target] = stmt
461
538
 
462
539
 
463
- def _statement_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt]) -> bool:
540
+ def _statement_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt], provenance: ResourceProvenance) -> bool:
464
541
  if not pending:
465
542
  return False
466
543
  value = stmt.value if isinstance(stmt, (ast.Assign, ast.AnnAssign)) else None
@@ -472,7 +549,7 @@ def _statement_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt]) ->
472
549
  path = _dotted(node)
473
550
  if path is not None and any(_touches(path, target.rsplit(".", 1)[0]) for target in pending):
474
551
  return True
475
- return not _is_safe_value(value)
552
+ return not _is_safe_value(value, provenance)
476
553
 
477
554
 
478
555
  def _unused_asserted_setup(
@@ -484,7 +561,7 @@ def _unused_asserted_setup(
484
561
  stmt,
485
562
  (
486
563
  f"`{target}` is configured here, but the test ends by proving `{asserted}` was never "
487
- "called, so its behavior cannot affect the test. Delete the contradictory configuration"
564
+ "called, so its behavior cannot affect the test. Delete this inert configuration; retain the mock constructor and binding"
488
565
  ),
489
566
  )
490
567
  break
@@ -492,32 +569,34 @@ def _unused_asserted_setup(
492
569
 
493
570
  def _pending_after_statement(
494
571
  stmt: ast.stmt,
495
- imports: ImportIndex,
496
572
  origins: frozenset[str],
497
573
  pending: dict[str, ast.stmt],
574
+ provenance: ResourceProvenance,
498
575
  *,
499
576
  has_mocker_fixture: bool,
500
577
  ) -> dict[str, ast.stmt]:
501
- initial = _initial_mock_configs(stmt, imports, has_mocker_fixture=has_mocker_fixture)
578
+ initial = _initial_mock_configs(stmt, provenance, has_mocker_fixture=has_mocker_fixture)
502
579
  if initial and initial[0].partition(".")[0] in origins:
503
- if _constructor_observes_pending(stmt, pending):
580
+ if _constructor_observes_pending(stmt, pending, provenance):
504
581
  pending.clear()
505
582
  configured_name = initial[0].partition(".")[0]
506
583
  pending = {target: setup for target, setup in pending.items() if not target.startswith(f"{configured_name}.")}
507
- pending.update((configured, stmt) for configured in initial)
584
+ pending.update(
585
+ (configured, stmt) for configured in initial if _inert_configuration(stmt, configured, provenance)
586
+ )
508
587
  return pending
509
- if not _is_inert(stmt):
588
+ if not _is_inert(stmt, provenance):
510
589
  pending.clear()
511
590
  return pending
512
591
 
513
592
 
514
- def _constructor_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt]) -> bool:
593
+ def _constructor_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt], provenance: ResourceProvenance) -> bool:
515
594
  value = stmt.value if isinstance(stmt, (ast.Assign, ast.AnnAssign)) else None
516
595
  if not isinstance(value, ast.Call):
517
596
  return True
518
597
  expressions = (*value.args, *(keyword.value for keyword in value.keywords))
519
598
  return any(
520
- not _is_safe_value(expression)
599
+ not _is_safe_value(expression, provenance)
521
600
  or any(
522
601
  isinstance(node, (ast.Name, ast.Attribute))
523
602
  and isinstance(node.ctx, ast.Load)
@@ -529,7 +608,7 @@ def _constructor_observes_pending(stmt: ast.stmt, pending: dict[str, ast.stmt])
529
608
  )
530
609
 
531
610
 
532
- def _is_inert(stmt: ast.stmt) -> bool:
611
+ def _is_inert(stmt: ast.stmt, provenance: ResourceProvenance) -> bool:
533
612
  if isinstance(stmt, (ast.Pass, ast.Import, ast.ImportFrom)):
534
613
  return True
535
614
  if isinstance(stmt, ast.Expr):
@@ -539,10 +618,12 @@ def _is_inert(stmt: ast.stmt) -> bool:
539
618
  if not all(isinstance(target, ast.Name) for target in stmt.targets):
540
619
  # `obj.attr = 1` can hit a property setter, `d[k] = 1` a `__setitem__`.
541
620
  return False
542
- return _is_safe_value(stmt.value)
621
+ return _is_safe_value(stmt.value, provenance)
543
622
 
544
623
 
545
- def _initial_mock_configs(stmt: ast.stmt, imports: ImportIndex, *, has_mocker_fixture: bool) -> tuple[str, ...]:
624
+ def _initial_mock_configs(
625
+ stmt: ast.stmt, provenance: ResourceProvenance, *, has_mocker_fixture: bool
626
+ ) -> tuple[str, ...]:
546
627
  match stmt:
547
628
  case ast.Assign(targets=[ast.Name(id=name)], value=ast.Call() as call):
548
629
  pass
@@ -550,6 +631,6 @@ def _initial_mock_configs(stmt: ast.stmt, imports: ImportIndex, *, has_mocker_fi
550
631
  pass
551
632
  case _:
552
633
  return ()
553
- if not _is_mock_configurator(call.func, imports, has_mocker_fixture=has_mocker_fixture):
634
+ if not _is_mock_configurator(call, provenance, has_mocker_fixture=has_mocker_fixture):
554
635
  return ()
555
636
  return tuple(f"{name}.{keyword.arg}" for keyword in call.keywords if keyword.arg in _CONFIG_ATTRS)