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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_resource_provenance.py +10 -6
  4. sarj_python_lint-1.44.0/src/sarj_python_lint/rules/_type_guard_symbols.py +263 -0
  5. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +100 -24
  6. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +64 -13
  7. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_partial_type_is.py +152 -20
  8. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +18 -4
  9. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +157 -23
  10. sarj_python_lint-1.43.0/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -112
  11. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/.gitignore +0 -0
  12. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/LICENSE +0 -0
  13. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/README.md +0 -0
  14. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/__main__.py +0 -0
  16. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  17. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_file_context.py +0 -0
  18. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_filesystem.py +0 -0
  19. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_python_target.py +0 -0
  20. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  21. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_secret_names.py +0 -0
  22. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_source.py +0 -0
  23. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/_version.py +0 -0
  24. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/benchmark.py +0 -0
  25. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  26. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/json_boundary.py +0 -0
  27. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/py.typed +0 -0
  28. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/ratchet.py +0 -0
  29. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rule_base.py +0 -0
  30. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  31. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  32. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  33. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  34. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  35. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  36. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  37. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  38. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  39. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  40. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  41. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  42. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  43. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  44. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  45. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  46. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  47. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  48. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  49. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  50. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  51. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  52. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  53. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  54. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  55. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  56. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  57. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  58. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  59. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  60. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  61. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  62. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  63. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  64. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  65. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  66. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  67. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  68. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  69. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  70. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  71. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  72. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  73. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  74. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  75. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  76. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  77. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  78. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  79. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  80. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  81. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  82. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  83. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  84. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  85. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  86. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  87. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  88. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  89. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  90. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  91. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  92. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  93. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  94. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  95. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  96. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  97. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  98. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  99. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  100. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  101. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  102. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  103. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  104. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  105. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  106. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  107. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  108. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  109. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  110. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  111. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  112. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  113. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  114. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  115. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  116. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  117. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  118. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  119. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  120. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  121. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  122. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  123. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  124. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  125. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  126. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  127. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  128. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  129. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  130. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  131. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  132. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  133. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  134. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  135. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  136. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  137. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  138. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  139. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  140. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  141. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  142. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  143. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  144. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  145. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  146. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  147. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  148. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  149. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  150. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  151. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  152. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  153. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  154. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  155. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  156. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  157. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  158. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  159. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  160. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  161. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  162. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  163. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  164. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  165. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  166. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  167. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  168. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  169. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  170. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  171. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  172. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  173. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  174. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  175. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  176. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  177. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  178. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  179. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  180. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  181. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  182. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  183. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  184. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  185. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  186. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  187. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  188. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  189. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  190. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  191. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  192. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  193. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  194. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  195. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  196. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  197. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  198. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  199. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  200. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  201. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  202. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  203. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  204. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  205. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  206. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  207. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  208. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  209. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  210. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  211. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  212. {sarj_python_lint-1.43.0 → sarj_python_lint-1.44.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.43.0
3
+ Version: 1.44.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.43.0"
3
+ version = "1.44.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -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)
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import Counter
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sarj_python_lint.rules._imports import TYPING_SOURCES
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from sarj_python_lint._file_context import PythonFileContext
12
+ from sarj_python_lint.rules._imports import ImportIndex
13
+
14
+
15
+ _NAMESPACE_IMPORT_CAPABILITIES = frozenset(
16
+ {"__dict__", "__builtins__", "__getattribute__", "__setattr__", "__delattr__"}
17
+ )
18
+ _NAMESPACE_RETURNING_FUNCTIONS = frozenset({"__lazy_import__"})
19
+ _BUILTIN_NAMESPACE_FUNCTIONS = frozenset(
20
+ {
21
+ "__build_class__",
22
+ "__import__",
23
+ "__lazy_import__",
24
+ "abs",
25
+ "aiter",
26
+ "all",
27
+ "anext",
28
+ "any",
29
+ "ascii",
30
+ "bin",
31
+ "breakpoint",
32
+ "callable",
33
+ "chr",
34
+ "compile",
35
+ "delattr",
36
+ "dir",
37
+ "divmod",
38
+ "eval",
39
+ "exec",
40
+ "format",
41
+ "getattr",
42
+ "globals",
43
+ "hasattr",
44
+ "hash",
45
+ "hex",
46
+ "id",
47
+ "input",
48
+ "isinstance",
49
+ "issubclass",
50
+ "iter",
51
+ "len",
52
+ "locals",
53
+ "max",
54
+ "min",
55
+ "next",
56
+ "oct",
57
+ "ord",
58
+ "pow",
59
+ "print",
60
+ "repr",
61
+ "round",
62
+ "setattr",
63
+ "sorted",
64
+ "sum",
65
+ "vars",
66
+ }
67
+ )
68
+
69
+
70
+ def builtin_guard_symbol(node: ast.expr | None, imports: ImportIndex, builtin_symbols: frozenset[str]) -> str | None:
71
+ if node is None:
72
+ return None
73
+ symbol = imports.resolved_symbol(node, sources=frozenset({"builtins"}))
74
+ if symbol in builtin_symbols:
75
+ return symbol
76
+ if isinstance(node, ast.Name) and node.id in builtin_symbols and imports.builtin_is_unshadowed(node.id):
77
+ return node.id
78
+ return None
79
+
80
+
81
+ def unique_guard_function_names(context: PythonFileContext) -> frozenset[str]:
82
+ if context.tree is None:
83
+ return frozenset()
84
+ counts: Counter[str] = Counter()
85
+ pending: list[ast.AST] = list(context.tree.body)
86
+ while pending:
87
+ node = pending.pop()
88
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
89
+ counts[node.name] += 1
90
+ elif not isinstance(node, (ast.ClassDef, ast.Lambda)):
91
+ pending.extend(ast.iter_child_nodes(node))
92
+ return frozenset(name for name, count in counts.items() if count == 1)
93
+
94
+
95
+ def guard_symbols_ambiguous(
96
+ context: PythonFileContext,
97
+ builtin_symbols: frozenset[str],
98
+ *,
99
+ typing_symbols: frozenset[str] = frozenset({"TypeGuard", "TypeIs"}),
100
+ ) -> bool:
101
+ namespace_names = _namespace_import_names(context)
102
+ if _namespace_escapes(context, namespace_names):
103
+ return True
104
+ mutation_targets = (
105
+ {f"builtins.{symbol}" for symbol in builtin_symbols}
106
+ | {f"{module}.{symbol}" for module in TYPING_SOURCES for symbol in typing_symbols}
107
+ | {f"{module}.__class__" for module in TYPING_SOURCES | {"builtins"}}
108
+ )
109
+ sensitive_names = (
110
+ builtin_symbols
111
+ | namespace_names
112
+ | {
113
+ name
114
+ for name, target in context.module_imports.bindings.items()
115
+ if target.module in TYPING_SOURCES | {"builtins"}
116
+ and (target.symbol is None or target.symbol in builtin_symbols | typing_symbols)
117
+ }
118
+ )
119
+ return any(
120
+ _ambiguous_node(node, context, mutation_targets, sensitive_names)
121
+ for node in context.nodes(
122
+ ast.Import,
123
+ ast.ImportFrom,
124
+ ast.Attribute,
125
+ ast.ExceptHandler,
126
+ ast.MatchAs,
127
+ ast.MatchStar,
128
+ ast.MatchMapping,
129
+ ast.NamedExpr,
130
+ )
131
+ )
132
+
133
+
134
+ def _namespace_import_names(context: PythonFileContext) -> frozenset[str]:
135
+ return frozenset(
136
+ alias.asname or alias.name
137
+ for node in context.nodes(ast.Import)
138
+ for alias in node.names
139
+ if alias.name in TYPING_SOURCES | {"builtins"}
140
+ )
141
+
142
+
143
+ def _namespace_escapes(context: PythonFileContext, namespace_names: frozenset[str]) -> bool:
144
+ if context.dynamic_namespace or any(node.id == "__builtins__" for node in context.nodes(ast.Name)):
145
+ return True
146
+ if any(
147
+ node.level == 0
148
+ and node.module in TYPING_SOURCES | {"builtins"}
149
+ and any(alias.name in _NAMESPACE_IMPORT_CAPABILITIES for alias in node.names)
150
+ for node in context.nodes(ast.ImportFrom)
151
+ ):
152
+ return True
153
+ if _native_function_escapes(context):
154
+ return True
155
+ for name in context.nodes(ast.Name):
156
+ if name.id not in namespace_names:
157
+ continue
158
+ parent = context.parents.get(name)
159
+ if not isinstance(parent, ast.Attribute) or parent.value is not name or parent.attr == "__dict__":
160
+ return True
161
+ return False
162
+
163
+
164
+ def _native_function_escapes(context: PythonFileContext) -> bool:
165
+ function_names = _possible_native_function_names(context)
166
+ namespace_function_names = _possible_native_function_names(context, symbols=_NAMESPACE_RETURNING_FUNCTIONS)
167
+ module_names = frozenset(
168
+ alias.asname or alias.name
169
+ for node in context.nodes(ast.Import)
170
+ for alias in node.names
171
+ if alias.name == "builtins"
172
+ )
173
+ for node in context.nodes(ast.Name, ast.Attribute):
174
+ if isinstance(node, ast.Attribute) and (
175
+ not isinstance(node.value, ast.Name) or node.attr not in _BUILTIN_NAMESPACE_FUNCTIONS
176
+ ):
177
+ continue
178
+ if not isinstance(node.ctx, ast.Load) or not _possible_native_function(node, function_names, module_names):
179
+ continue
180
+ parent = context.parents.get(node)
181
+ if (
182
+ not isinstance(parent, ast.Call)
183
+ or parent.func is not node
184
+ or _namespace_returning_function(node, namespace_function_names)
185
+ ):
186
+ return True
187
+ return False
188
+
189
+
190
+ def _possible_native_function_names(
191
+ context: PythonFileContext, *, symbols: frozenset[str] = _BUILTIN_NAMESPACE_FUNCTIONS
192
+ ) -> frozenset[str]:
193
+ return symbols | frozenset(
194
+ alias.asname or alias.name
195
+ for node in context.nodes(ast.ImportFrom)
196
+ if node.level == 0 and node.module == "builtins"
197
+ for alias in node.names
198
+ if alias.name in symbols
199
+ )
200
+
201
+
202
+ def _namespace_returning_function(node: ast.Name | ast.Attribute, function_names: frozenset[str]) -> bool:
203
+ if isinstance(node, ast.Name):
204
+ return node.id in function_names
205
+ return node.attr in _NAMESPACE_RETURNING_FUNCTIONS
206
+
207
+
208
+ def _possible_native_function(
209
+ node: ast.Name | ast.Attribute,
210
+ function_names: frozenset[str],
211
+ module_names: frozenset[str],
212
+ ) -> bool:
213
+ if isinstance(node, ast.Name):
214
+ return node.id in function_names
215
+ return (
216
+ isinstance(node.value, ast.Name) and node.value.id in module_names and node.attr in _BUILTIN_NAMESPACE_FUNCTIONS
217
+ )
218
+
219
+
220
+ def _ambiguous_node(
221
+ node: ast.AST, context: PythonFileContext, mutation_targets: set[str], sensitive_names: frozenset[str]
222
+ ) -> bool:
223
+ match node:
224
+ case ast.Import() | ast.ImportFrom():
225
+ return _ambiguous_import(node, context.module_imports, sensitive_names)
226
+ case ast.Attribute(ctx=(ast.Store() | ast.Del())):
227
+ return context.module_imports.resolved_qualified_name(node) in mutation_targets
228
+ case (
229
+ ast.ExceptHandler(name=name)
230
+ | ast.MatchAs(name=name)
231
+ | ast.MatchStar(name=name)
232
+ | ast.MatchMapping(rest=name)
233
+ | ast.NamedExpr(target=ast.Name(id=name))
234
+ ):
235
+ return name in sensitive_names
236
+ case _:
237
+ return False
238
+
239
+
240
+ def _ambiguous_import(node: ast.Import | ast.ImportFrom, imports: ImportIndex, sensitive_names: frozenset[str]) -> bool:
241
+ if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
242
+ return True
243
+ for alias in node.names:
244
+ name = alias.asname or alias.name.partition(".")[0]
245
+ if name not in sensitive_names:
246
+ continue
247
+ if not _preserves_canonical_import(node, alias, imports):
248
+ return True
249
+ return False
250
+
251
+
252
+ def _preserves_canonical_import(node: ast.Import | ast.ImportFrom, alias: ast.alias, imports: ImportIndex) -> bool:
253
+ if isinstance(node, ast.ImportFrom):
254
+ if node.level:
255
+ return False
256
+ module, symbol = node.module, alias.name
257
+ else:
258
+ module, symbol = alias.name, None
259
+ name = alias.asname or alias.name.partition(".")[0]
260
+ target = imports.bindings.get(name)
261
+ if target is None:
262
+ return module == "builtins" and symbol == name
263
+ return target.module == module and target.symbol == symbol
@@ -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