sarj-python-lint 1.43.0__tar.gz → 1.43.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_resource_provenance.py +10 -6
  4. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +100 -24
  5. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +157 -23
  6. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/.gitignore +0 -0
  7. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/LICENSE +0 -0
  8. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/README.md +0 -0
  9. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_source.py +0 -0
  18. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/benchmark.py +0 -0
  20. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  21. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/json_boundary.py +0 -0
  22. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/py.typed +0 -0
  23. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/ratchet.py +0 -0
  24. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  30. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  31. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  32. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  33. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  34. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  35. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  36. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  37. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  38. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  39. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  40. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  41. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  42. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  43. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  44. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  45. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  46. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  47. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  48. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  49. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  50. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  51. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  52. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  53. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  54. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  55. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  56. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  57. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  58. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  59. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  60. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  61. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  62. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  63. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  64. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  65. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  66. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  67. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  68. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  69. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  70. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  71. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  72. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  73. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  74. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  75. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  76. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  77. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  78. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  79. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  80. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  81. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  82. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  83. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  84. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  85. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  86. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  87. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  88. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  89. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  90. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  91. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  92. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  93. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  94. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  95. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  96. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  97. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  98. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  99. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  100. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  101. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  102. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  103. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  104. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  105. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  106. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  107. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  108. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  109. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  110. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  111. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  112. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  113. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  114. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  115. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  116. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  117. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  118. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  119. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  120. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  121. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  122. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  123. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  124. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  125. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  126. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  127. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  128. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  129. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  130. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  131. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  132. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  133. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  134. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  135. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  136. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  137. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  138. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  139. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  140. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  141. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  142. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  143. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  144. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  145. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  146. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  147. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  148. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  149. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  150. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  151. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  152. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  153. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  154. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  155. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  156. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  157. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  158. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  159. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  160. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  161. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  162. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  163. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  164. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  165. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  166. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  167. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  168. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  169. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  170. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  171. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  172. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  173. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  174. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  175. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  176. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  177. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  178. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  179. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  180. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  181. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  182. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  183. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  184. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  185. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  186. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  187. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  188. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  189. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  190. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  191. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  192. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  193. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  194. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  195. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  196. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  197. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  198. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  199. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  200. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  201. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  202. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  203. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  204. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  205. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  206. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  207. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  208. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  209. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  210. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  211. {sarj_python_lint-1.43.0 → sarj_python_lint-1.43.1}/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.43.0
3
+ Version: 1.43.1
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.43.0"
3
+ version = "1.43.1"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -56,6 +56,15 @@ _RESOURCE_OWNERS = MappingProxyType(
56
56
  )
57
57
 
58
58
 
59
+ def builtin_namespace_untouched(context: PythonFileContext) -> bool:
60
+ return not any(
61
+ (isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
62
+ or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
63
+ or (isinstance(node, ast.Name) and node.id == "__builtins__")
64
+ for node in context.nodes(ast.Import, ast.ImportFrom, ast.Name)
65
+ )
66
+
67
+
59
68
  @final
60
69
  class ResourceProvenance:
61
70
  def __init__(self, context: PythonFileContext, *, require_static_namespace: bool = False) -> None:
@@ -86,12 +95,7 @@ class ResourceProvenance:
86
95
 
87
96
  @cached_property
88
97
  def builtin_namespace_untouched(self) -> bool:
89
- return not any(
90
- (isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
91
- or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
92
- or (isinstance(node, ast.Name) and node.id == "__builtins__")
93
- for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
94
- )
98
+ return builtin_namespace_untouched(self.context)
95
99
 
96
100
  def scope(self, node: ast.AST) -> ast.AST | None:
97
101
  parent = self.context.parents.get(node)
@@ -21,7 +21,7 @@ from sarj_python_lint.rule_base import (
21
21
  from sarj_python_lint.rules._ast_index import walk as walk_ast
22
22
  from sarj_python_lint.rules._imports import ImportIndex
23
23
  from sarj_python_lint.rules._pytest import collected_tests
24
- from sarj_python_lint.rules._resource_provenance import ResourceProvenance
24
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance, builtin_namespace_untouched
25
25
 
26
26
 
27
27
  if TYPE_CHECKING:
@@ -97,13 +97,44 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
97
97
  "Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
98
98
  "Visible provider replacement, escape or unresolved lexical ownership retains WARNING; only the closed canonical AsyncMock provider proof blocks.",
99
99
  "Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
100
- "Positive call evidence includes direct assertion methods and literal integer or boolean call-state checks; unresolved call expectations are excluded.",
101
- "Await evidence excludes provably vacuous checks, including zero or nonnegative counts and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
100
+ "Positive call evidence includes direct assertion methods and literal numeric or boolean call-state checks; unresolved call expectations are excluded.",
101
+ "Await evidence excludes provably vacuous checks, including non-None count/list state, zero or nonnegative numeric bounds, and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
102
+ "Length comparisons over await_args_list use count semantics only for an unshadowed builtin len without visible builtin namespace access; custom length functions remain unresolved.",
102
103
  "Conjunctions need one accepted await branch; disjunctions need accepted evidence for the same mock in every branch. Argument equality and assertion strength are not compared.",
103
104
  "Compound negation and custom expression semantics remain unresolved; no variable values or fixture graphs are inferred.",
104
105
  "Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
105
106
  ),
106
107
  examples=(
108
+ RuleExample(
109
+ example_id="vacuous-await-length",
110
+ scenario="await-length-evidence",
111
+ title="An empty await history still has nonnegative length",
112
+ outcome=ExampleOutcome.MATCH,
113
+ files=(
114
+ ExampleFile.python(
115
+ "tests/test_delivery.py",
116
+ "from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n assert len(send.await_args_list) >= 0\n",
117
+ ),
118
+ ),
119
+ focus_path=PurePosixPath("tests/test_delivery.py"),
120
+ expected_count=1,
121
+ public=True,
122
+ ),
123
+ RuleExample(
124
+ example_id="positive-await-length",
125
+ scenario="await-length-evidence",
126
+ title="A positive await-history length distinguishes a dropped coroutine",
127
+ outcome=ExampleOutcome.NO_MATCH,
128
+ files=(
129
+ ExampleFile.python(
130
+ "tests/test_delivery.py",
131
+ "from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n assert len(send.await_args_list) == 1\n",
132
+ ),
133
+ ),
134
+ focus_path=PurePosixPath("tests/test_delivery.py"),
135
+ expected_count=0,
136
+ public=True,
137
+ ),
107
138
  RuleExample(
108
139
  example_id="spaced-positive-called-state",
109
140
  scenario="called-attribute-spacing",
@@ -212,6 +243,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
212
243
  if tree is None:
213
244
  return []
214
245
  imports = context.module_imports
246
+ builtin_len = builtin_namespace_untouched(context)
215
247
  writes = {
216
248
  _reference(node)
217
249
  for node in context.nodes(ast.AST)
@@ -221,14 +253,20 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
221
253
  finding
222
254
  for test in collected_tests(tree.body)
223
255
  if not _is_pytest_fixture(test, imports)
224
- for finding in _test_diagnostics(test, context, imports, writes, self.code)
256
+ for finding in _test_diagnostics(test, context, imports, writes, self.code, builtin_len=builtin_len)
225
257
  ]
226
258
 
227
259
 
228
260
  def _test_diagnostics(
229
- test: _TestFunction, context: PythonFileContext, imports: ImportIndex, writes: set[_Reference], code: str
261
+ test: _TestFunction,
262
+ context: PythonFileContext,
263
+ imports: ImportIndex,
264
+ writes: set[_Reference],
265
+ code: str,
266
+ *,
267
+ builtin_len: bool,
230
268
  ) -> list[Diagnostic]:
231
- calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes))
269
+ calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes, builtin_len=builtin_len))
232
270
  if not calls:
233
271
  return []
234
272
  severity = Severity.ERROR if _mock_provider_proven(test, context) else Severity.WARNING
@@ -281,11 +319,11 @@ def _test_imports(module: ImportIndex, test: _TestFunction) -> ImportIndex:
281
319
 
282
320
 
283
321
  def _uncovered_calls(
284
- imports: ImportIndex, test: _TestFunction, writes: set[_Reference]
322
+ imports: ImportIndex, test: _TestFunction, writes: set[_Reference], *, builtin_len: bool
285
323
  ) -> Iterator[ast.Call | ast.Attribute]:
286
324
  nodes = tuple(walk_ast(test))
287
325
  bindings = _mock_bindings(imports, test.body, writes)
288
- covered = _await_oracles(test.body)
326
+ covered = _await_oracles(test.body, builtin_len=builtin_len and imports.builtin_is_unshadowed("len"))
289
327
  reported: set[_Reference] = set()
290
328
  for statement in test.body:
291
329
  for reference, oracle in _call_oracles(statement):
@@ -375,7 +413,7 @@ def _is_async_reference(reference: _Reference, constructor: ast.Call) -> bool:
375
413
  )
376
414
 
377
415
 
378
- def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
416
+ def _await_oracles(statements: list[ast.stmt], *, builtin_len: bool) -> set[_Reference]:
379
417
  covered: set[_Reference] = set()
380
418
  for statement in statements:
381
419
  match statement:
@@ -385,7 +423,7 @@ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
385
423
  if method != "assert_has_awaits" or _calls_evidence(call) is not _Evidence.INSUFFICIENT:
386
424
  covered.add(_reference(receiver))
387
425
  case ast.Assert(test=condition):
388
- covered.update(_asserted_states(condition, _AWAIT_STATE))
426
+ covered.update(_asserted_states(condition, _AWAIT_STATE, builtin_len=builtin_len))
389
427
  case _:
390
428
  pass
391
429
  return covered
@@ -409,17 +447,21 @@ def _sequence_evidence(node: ast.expr) -> _Evidence:
409
447
  return _Evidence.UNRESOLVED
410
448
 
411
449
 
412
- def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
450
+ def _asserted_states(condition: ast.expr, states: frozenset[str], *, builtin_len: bool = False) -> set[_Reference]:
413
451
  match condition:
414
452
  case ast.UnaryOp(op=ast.Not(), operand=ast.UnaryOp(op=ast.Not(), operand=operand)):
415
- return _asserted_states(operand, states)
453
+ return _asserted_states(operand, states, builtin_len=builtin_len)
416
454
  case ast.UnaryOp(op=ast.Not(), operand=ast.Compare(left=left, ops=[operator], comparators=[right])):
417
455
  inverse = _negated_operator(operator)
418
- return _comparison_states(left, inverse, right, states) if inverse is not None else set()
456
+ return (
457
+ _comparison_states(left, inverse, right, states, builtin_len=builtin_len)
458
+ if inverse is not None
459
+ else set()
460
+ )
419
461
  case ast.UnaryOp(op=ast.Not(), operand=ast.Attribute(attr=attribute)) if attribute in states:
420
462
  return set()
421
463
  case ast.BoolOp(op=operator, values=values):
422
- branches = [_asserted_states(value, states) for value in values]
464
+ branches = [_asserted_states(value, states, builtin_len=builtin_len) for value in values]
423
465
  return (
424
466
  branches[0].union(*branches[1:])
425
467
  if isinstance(operator, ast.And)
@@ -430,7 +472,7 @@ def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Refere
430
472
  case ast.Compare(left=left, ops=operators, comparators=rights):
431
473
  result: set[_Reference] = set()
432
474
  for first, operator, second in zip((left, *rights[:-1]), operators, rights, strict=True):
433
- result.update(_comparison_states(first, operator, second, states))
475
+ result.update(_comparison_states(first, operator, second, states, builtin_len=builtin_len))
434
476
  return result
435
477
  case _:
436
478
  # Preserve acceptance of unresolved await-state assertions, such as
@@ -453,18 +495,37 @@ def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
453
495
  return factory() if factory is not None else None
454
496
 
455
497
 
456
- def _comparison_states(left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str]) -> set[_Reference]:
498
+ def _comparison_states(
499
+ left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str], *, builtin_len: bool = False
500
+ ) -> set[_Reference]:
457
501
  result: set[_Reference] = set()
458
502
  for node, comparison, expected in ((left, operator, right), (right, _reversed_operator(operator), left)):
459
- if isinstance(node, ast.Attribute) and node.attr in states:
460
- evidence = _state_evidence(node.attr, comparison, expected)
503
+ state = _state_operand(node, states, builtin_len=builtin_len)
504
+ if state is not None:
505
+ evidence = _state_evidence(
506
+ "await_count" if isinstance(node, ast.Call) else state.attr, comparison, expected
507
+ )
461
508
  if evidence is _Evidence.POSITIVE or (states == _AWAIT_STATE and evidence is _Evidence.UNRESOLVED):
462
- result.add(_reference(node.value))
509
+ result.add(_reference(state.value))
463
510
  else:
464
511
  result.update(_unresolved_await_states(node, states))
465
512
  return result
466
513
 
467
514
 
515
+ def _state_operand(node: ast.expr, states: frozenset[str], *, builtin_len: bool) -> ast.Attribute | None:
516
+ if isinstance(node, ast.Attribute) and node.attr in states:
517
+ return node
518
+ if builtin_len and states == _AWAIT_STATE:
519
+ match node:
520
+ case ast.Call(
521
+ func=ast.Name(id="len"), args=[ast.Attribute(attr="await_args_list") as argument], keywords=[]
522
+ ):
523
+ return argument
524
+ case _:
525
+ pass
526
+ return None
527
+
528
+
468
529
  def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
469
530
  match operator:
470
531
  case ast.Lt():
@@ -489,12 +550,14 @@ def _unresolved_await_states(node: ast.expr, states: frozenset[str]) -> set[_Ref
489
550
 
490
551
  def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) -> _Evidence:
491
552
  positive: bool
492
- if (
493
- attribute in {"call_count", "await_count"}
553
+ if attribute in {"call_count", "await_count"} and (value := _literal_number(expected)) is not None:
554
+ positive = _proves_positive_count(operator, value)
555
+ elif (
556
+ attribute in {"call_count", "await_count", "await_args_list"}
494
557
  and isinstance(expected, ast.Constant)
495
- and isinstance(expected.value, int)
558
+ and expected.value is None
496
559
  ):
497
- positive = _proves_positive_count(operator, expected.value)
560
+ return _Evidence.INSUFFICIENT
498
561
  elif attribute == "called" and isinstance(expected, ast.Constant) and isinstance(expected.value, bool):
499
562
  positive = (isinstance(operator, (ast.Eq, ast.Is)) and expected.value) or (
500
563
  isinstance(operator, (ast.NotEq, ast.IsNot)) and not expected.value
@@ -513,7 +576,20 @@ def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) ->
513
576
  return _Evidence.POSITIVE if positive else _Evidence.INSUFFICIENT
514
577
 
515
578
 
516
- def _proves_positive_count(operator: ast.cmpop, value: int) -> bool:
579
+ def _literal_number(node: ast.expr) -> int | float | None:
580
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
581
+ return node.value
582
+ if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant):
583
+ value = node.operand.value
584
+ if isinstance(value, (int, float)):
585
+ if isinstance(node.op, ast.USub):
586
+ return -value
587
+ if isinstance(node.op, ast.UAdd):
588
+ return +value
589
+ return None
590
+
591
+
592
+ def _proves_positive_count(operator: ast.cmpop, value: float) -> bool:
517
593
  match operator:
518
594
  case ast.Eq() | ast.GtE():
519
595
  return value > 0
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from collections import Counter
4
5
  from pathlib import PurePosixPath
5
6
  from typing import TYPE_CHECKING, NamedTuple, final, override
6
7
 
@@ -21,7 +22,7 @@ from sarj_python_lint.rules._paths import is_test_path
21
22
 
22
23
 
23
24
  if TYPE_CHECKING:
24
- from collections.abc import Mapping
25
+ from collections.abc import Container, Mapping
25
26
  from pathlib import Path
26
27
 
27
28
  from sarj_python_lint._file_context import PythonFileContext
@@ -60,6 +61,11 @@ class _CollectedTest(NamedTuple):
60
61
  class_bindings: set[str]
61
62
 
62
63
 
64
+ class _ImportedCollection(NamedTuple):
65
+ module: str
66
+ symbol: str
67
+
68
+
63
69
  def _scope_binding_counts(statements: list[ast.stmt]) -> dict[str, int]:
64
70
  counts: dict[str, int] = {}
65
71
  stack: list[ast.AST] = [*reversed(statements)]
@@ -115,8 +121,8 @@ def _imported_bindings(
115
121
  binding_counts: dict[str, int],
116
122
  path: Path,
117
123
  facts: FirstPartyFacts,
118
- ) -> dict[str, str]:
119
- bindings: dict[str, str] = {}
124
+ ) -> dict[str, _ImportedCollection]:
125
+ bindings: dict[str, _ImportedCollection] = {}
120
126
  for node in tree.body:
121
127
  if (
122
128
  isinstance(node, ast.ImportFrom)
@@ -125,7 +131,7 @@ def _imported_bindings(
125
131
  and _is_first_party_production_module(node.module, path, facts)
126
132
  ):
127
133
  bindings.update(
128
- (local, node.module)
134
+ (local, _ImportedCollection(node.module, alias.name))
129
135
  for alias in node.names
130
136
  if alias.name != "*"
131
137
  and binding_counts.get(local := alias.asname or alias.name, 0) == 1
@@ -170,7 +176,7 @@ def _pytest_bindings(tree: ast.Module, binding_counts: dict[str, int]) -> _Pytes
170
176
 
171
177
  def _direct_imported_collection(
172
178
  node: ast.expr,
173
- imported: Mapping[str, str],
179
+ imported: Mapping[str, _ImportedCollection],
174
180
  blocked: set[str],
175
181
  builtin_wrappers: frozenset[str],
176
182
  ) -> ast.Name | None:
@@ -199,7 +205,7 @@ def _is_registry(name: str) -> bool:
199
205
 
200
206
  def _is_imported_collection_wrapper(
201
207
  node: ast.Call,
202
- imported: Mapping[str, str],
208
+ imported: Mapping[str, _ImportedCollection],
203
209
  builtin_wrappers: frozenset[str],
204
210
  ) -> bool:
205
211
  return (
@@ -254,30 +260,134 @@ def _collected_tests(tree: ast.Module) -> list[_CollectedTest]:
254
260
 
255
261
  def _independently_asserted_collections(
256
262
  tree: ast.Module,
257
- imported: Mapping[str, str],
263
+ imported: Mapping[str, _ImportedCollection],
258
264
  tests: list[_CollectedTest],
259
265
  ) -> set[str]:
260
- expected = _independent_expected_bindings(tree, _scope_binding_counts(tree.body))
266
+ expected = _independent_expected_bindings(tree.body, _scope_binding_counts(tree.body))
267
+ asserted = _asserted_collections(tree.body, imported, expected, blocked=set(), proven_reads={})
268
+ for test in tests:
269
+ if imported.keys() <= asserted:
270
+ break
271
+ function = test.function
272
+ if not any(
273
+ isinstance(statement, ast.Assert)
274
+ and isinstance(statement.test, ast.Compare)
275
+ and any(
276
+ isinstance(operand, ast.Name) and operand.id in imported
277
+ for operand in (statement.test.left, *statement.test.comparators)
278
+ )
279
+ for statement in function.body
280
+ ):
281
+ continue
282
+ bindings = _scope_binding_counts(function.body)
283
+ parameters = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
284
+ if function.args.vararg is not None:
285
+ parameters.append(function.args.vararg)
286
+ if function.args.kwarg is not None:
287
+ parameters.append(function.args.kwarg)
288
+ for parameter in parameters:
289
+ bindings[parameter.arg] = bindings.get(parameter.arg, 0) + 1
290
+ local_expected = _local_expected_bindings(function, bindings)
291
+ proven_reads = _captured_unchanged_defaults(test, imported, bindings)
292
+ proven_reads.update(_direct_production_reimports(function, imported, bindings))
293
+ captured_expected = _captured_unchanged_defaults(test, expected, bindings)
294
+ asserted.update(
295
+ _asserted_collections(
296
+ function.body,
297
+ imported,
298
+ (expected - bindings.keys()) | local_expected | captured_expected.keys(),
299
+ blocked=set(bindings),
300
+ proven_reads=proven_reads,
301
+ )
302
+ )
303
+ return asserted
304
+
305
+
306
+ def _captured_unchanged_defaults(
307
+ test: _CollectedTest, names: Container[str], bindings: dict[str, int]
308
+ ) -> dict[str, tuple[int, int]]:
309
+ function = test.function
310
+ defaults = list(zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True))
311
+ if function.args.defaults:
312
+ positional = [*function.args.posonlyargs, *function.args.args]
313
+ defaults.extend(zip(positional[-len(function.args.defaults) :], function.args.defaults, strict=True))
314
+ return {
315
+ argument.arg: (function.lineno, function.col_offset)
316
+ for argument, default in defaults
317
+ if isinstance(default, ast.Name)
318
+ and default.id == argument.arg
319
+ and argument.arg in names
320
+ and argument.arg not in test.class_bindings
321
+ and bindings.get(argument.arg) == 1
322
+ }
323
+
324
+
325
+ def _direct_production_reimports(
326
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
327
+ imported: Mapping[str, _ImportedCollection],
328
+ bindings: dict[str, int],
329
+ ) -> dict[str, tuple[int, int]]:
330
+ proven: dict[str, tuple[int, int]] = {}
331
+ for statement in function.body:
332
+ if not isinstance(statement, ast.ImportFrom) or statement.level != 0 or statement.module is None:
333
+ continue
334
+ local_counts = Counter(alias.asname or alias.name for alias in statement.names)
335
+ for alias in statement.names:
336
+ local = alias.asname or alias.name
337
+ if (
338
+ local_counts[local] == 1
339
+ and bindings.get(local) == 1
340
+ and imported.get(local) == _ImportedCollection(statement.module, alias.name)
341
+ ):
342
+ proven[local] = (statement.lineno, statement.col_offset)
343
+ return proven
344
+
345
+
346
+ def _local_expected_bindings(function: ast.FunctionDef | ast.AsyncFunctionDef, bindings: dict[str, int]) -> set[str]:
347
+ expected = _independent_expected_bindings(function.body, bindings)
348
+ reads = Counter(
349
+ node.id for node in ast.walk(function) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
350
+ )
351
+ expected.update(
352
+ target.id
353
+ for statement in function.body
354
+ if isinstance(statement, (ast.Assign, ast.AnnAssign)) and _is_literal_table(statement.value)
355
+ for target in _assignment_targets(statement)
356
+ if isinstance(target, ast.Name) and bindings.get(target.id) == 1 and reads[target.id] == 1
357
+ )
358
+ return expected
359
+
360
+
361
+ def _asserted_collections(
362
+ statements: list[ast.stmt],
363
+ imported: Mapping[str, _ImportedCollection],
364
+ expected: set[str],
365
+ *,
366
+ blocked: set[str],
367
+ proven_reads: Mapping[str, tuple[int, int]],
368
+ ) -> set[str]:
261
369
  asserted: set[str] = set()
262
- assertions = _direct_assertions(tree, tests)
263
- for node in assertions:
264
- if not isinstance(node.test, ast.Compare):
370
+ for node in statements:
371
+ if not isinstance(node, ast.Assert) or not isinstance(node.test, ast.Compare):
265
372
  continue
266
373
  compare = node.test
267
374
  if len(compare.ops) != 1 or not isinstance(compare.ops[0], ast.Eq) or len(compare.comparators) != 1:
268
375
  continue
376
+ visible = (imported.keys() - blocked) | {
377
+ name for name, position in proven_reads.items() if position < (node.lineno, node.col_offset)
378
+ }
269
379
  left, right = compare.left, compare.comparators[0]
270
- if isinstance(left, ast.Name) and left.id in imported and _is_independent_expected(right, expected):
380
+ if isinstance(left, ast.Name) and left.id in visible and _is_independent_expected(right, expected):
271
381
  asserted.add(left.id)
272
- if isinstance(right, ast.Name) and right.id in imported and _is_independent_expected(left, expected):
382
+ if isinstance(right, ast.Name) and right.id in visible and _is_independent_expected(left, expected):
273
383
  asserted.add(right.id)
274
384
  return asserted
275
385
 
276
386
 
277
- def _independent_expected_bindings(tree: ast.Module, binding_counts: dict[str, int]) -> set[str]:
387
+ def _independent_expected_bindings(statements: list[ast.stmt], binding_counts: dict[str, int]) -> set[str]:
278
388
  return {
279
389
  target.id
280
- for statement in tree.body
390
+ for statement in statements
281
391
  if isinstance(statement, (ast.Assign, ast.AnnAssign))
282
392
  for target in _assignment_targets(statement)
283
393
  if isinstance(target, ast.Name)
@@ -323,8 +433,40 @@ class ProductionDerivedTestCases(Rule):
323
433
  "Only proven first-party, non-test imports with eligible, supported, allowed, or required in their names are checked in collected pytest parametrization.",
324
434
  "Direct collections, simple unshadowed builtin wrappers, set expressions, and positional or keyword argvalues are recognized.",
325
435
  "A direct module-level or collected-test equality against a distinct literal expected table proves an independent oracle; helper-indirected relationships remain unreported.",
436
+ "Collected-test oracles must read the imported production binding without parameter/local shadowing. Expected tuple bindings may be module-level or local and must be assigned once; fresh local literal lists/sets are accepted only when their sole read is the assertion.",
437
+ "Unchanged same-name default captures of production imports or immutable expected tuples, and single direct local reimports of the exact absolute module and symbol, retain provenance. Class-scope default shadowing, later writes, and imports after the assertion do not prove a production read.",
326
438
  ),
327
439
  examples=(
440
+ RuleExample(
441
+ example_id="shadowed-membership-oracle",
442
+ scenario="oracle-lexical-bindings",
443
+ title="A fixture equality does not observe the imported production collection",
444
+ outcome=ExampleOutcome.MATCH,
445
+ files=(
446
+ ExampleFile.python(
447
+ "app/tests/test_models.py",
448
+ "import pytest\nfrom app.models import ELIGIBLE_MODELS\n\ndef test_membership(ELIGIBLE_MODELS):\n assert ELIGIBLE_MODELS == ('a', 'b')\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n assert build(model)\n",
449
+ ),
450
+ ),
451
+ focus_path=PurePosixPath("app/tests/test_models.py"),
452
+ expected_count=1,
453
+ public=True,
454
+ ),
455
+ RuleExample(
456
+ example_id="local-independent-membership-oracle",
457
+ scenario="oracle-lexical-bindings",
458
+ title="A fresh local expected table observes the actual production collection",
459
+ outcome=ExampleOutcome.NO_MATCH,
460
+ files=(
461
+ ExampleFile.python(
462
+ "app/tests/test_models.py",
463
+ "import pytest\nfrom app.models import ELIGIBLE_MODELS\n\ndef test_membership():\n expected = ['a', 'b']\n assert ELIGIBLE_MODELS == expected\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n assert build(model)\n",
464
+ ),
465
+ ),
466
+ focus_path=PurePosixPath("app/tests/test_models.py"),
467
+ expected_count=0,
468
+ public=True,
469
+ ),
328
470
  RuleExample(
329
471
  example_id="independent-model-cases",
330
472
  title="Drive behavior from an independent expected table",
@@ -424,11 +566,3 @@ def _collect_pytest_modules(node: ast.Import, binding_counts: dict[str, int], mo
424
566
  for alias in node.names
425
567
  if alias.name == "pytest" and binding_counts.get(local := alias.asname or alias.name, 0) == 1
426
568
  )
427
-
428
-
429
- def _direct_assertions(tree: ast.Module, tests: list[_CollectedTest]) -> list[ast.Assert]:
430
- assertions = [node for node in tree.body if isinstance(node, ast.Assert)]
431
- assertions.extend(
432
- statement for test in tests for statement in test.function.body if isinstance(statement, ast.Assert)
433
- )
434
- return assertions