sarj-python-lint 1.42.2__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.42.2 → sarj_python_lint-1.43.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_registry.py +6 -0
  4. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_resource_provenance.py +10 -6
  5. sarj_python_lint-1.43.1/src/sarj_python_lint/rules/_type_guard_symbols.py +112 -0
  6. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +100 -24
  7. sarj_python_lint-1.43.1/src/sarj_python_lint/rules/no_contradictory_type_guard.py +198 -0
  8. sarj_python_lint-1.43.1/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +501 -0
  9. sarj_python_lint-1.43.1/src/sarj_python_lint/rules/no_partial_type_is.py +270 -0
  10. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +4 -78
  11. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +16 -4
  12. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +157 -23
  13. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/.gitignore +0 -0
  14. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/LICENSE +0 -0
  15. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/README.md +0 -0
  16. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/__init__.py +0 -0
  17. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/__main__.py +0 -0
  18. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  19. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_file_context.py +0 -0
  20. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_filesystem.py +0 -0
  21. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_python_target.py +0 -0
  22. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  23. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_secret_names.py +0 -0
  24. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_source.py +0 -0
  25. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/_version.py +0 -0
  26. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/benchmark.py +0 -0
  27. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  28. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/json_boundary.py +0 -0
  29. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/py.typed +0 -0
  30. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/ratchet.py +0 -0
  31. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rule_base.py +0 -0
  32. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  33. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  34. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  35. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  36. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  37. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  38. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  39. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  40. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  41. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  42. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  43. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  44. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  45. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  46. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  47. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  48. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  49. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  50. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  51. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  52. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  53. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  54. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  55. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  56. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  57. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  58. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  59. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  60. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  61. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  62. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  63. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  64. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  65. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  66. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  67. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  68. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  69. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  70. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  71. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  72. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  73. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  74. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  75. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  76. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  77. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  78. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  79. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  80. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  81. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  82. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  83. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  84. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  85. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  86. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  87. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  88. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  89. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  90. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  91. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  92. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  93. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  94. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  95. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  96. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  97. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  98. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  99. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  100. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  101. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  102. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  103. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  104. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  105. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  106. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  107. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  108. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  109. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  110. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  111. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  112. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  113. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  114. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  115. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  116. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  117. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  118. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  119. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  120. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  121. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  122. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  123. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  124. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  125. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  126. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  127. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  128. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  129. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  130. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  131. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  132. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  133. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  134. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  135. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  136. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  137. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  138. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  139. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  140. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  141. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  142. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  143. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  144. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  145. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  146. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  147. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  148. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  149. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  150. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  151. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  152. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  153. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  154. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  155. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  156. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  157. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  158. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  159. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  160. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  161. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  162. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  163. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  164. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  165. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  166. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  167. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  168. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  169. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  170. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  171. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  172. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  173. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  174. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  175. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  176. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  177. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  178. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  179. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  180. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  181. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  182. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  183. {sarj_python_lint-1.42.2 → 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.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  185. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  186. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  187. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  188. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  189. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  190. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  191. {sarj_python_lint-1.42.2 → 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.42.2 → 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.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  194. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  195. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  196. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  197. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  198. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  199. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  200. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  201. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  202. {sarj_python_lint-1.42.2 → 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.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  204. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  205. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  206. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  207. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  208. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  209. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  210. {sarj_python_lint-1.42.2 → sarj_python_lint-1.43.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  211. {sarj_python_lint-1.42.2 → 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.42.2
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.42.2"
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" }]
@@ -214,7 +214,10 @@ if TYPE_CHECKING:
214
214
  from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
215
215
  from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
216
216
  from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
217
+ from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
217
218
  from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
219
+ from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
220
+ from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
218
221
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
219
222
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
220
223
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
@@ -226,6 +229,9 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
226
229
  {
227
230
  NoExplicitTestOracle.id: NoExplicitTestOracle,
228
231
  PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
232
+ NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
233
+ NoPartialTypeIs.id: NoPartialTypeIs,
234
+ NoContradictoryTypeGuard.id: NoContradictoryTypeGuard,
229
235
  NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
230
236
  NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
231
237
  NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
@@ -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,112 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sarj_python_lint.rules._imports import TYPING_SOURCES
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from sarj_python_lint._file_context import PythonFileContext
11
+ from sarj_python_lint.rules._imports import ImportIndex
12
+
13
+
14
+ def builtin_guard_symbol(node: ast.expr | None, imports: ImportIndex, builtin_symbols: frozenset[str]) -> str | None:
15
+ if node is None:
16
+ return None
17
+ symbol = imports.resolved_symbol(node, sources=frozenset({"builtins"}))
18
+ if symbol in builtin_symbols:
19
+ return symbol
20
+ if isinstance(node, ast.Name) and node.id in builtin_symbols and imports.builtin_is_unshadowed(node.id):
21
+ return node.id
22
+ return None
23
+
24
+
25
+ def guard_symbols_ambiguous(context: PythonFileContext, builtin_symbols: frozenset[str]) -> bool:
26
+ if _namespace_escapes(context):
27
+ return True
28
+ mutation_targets = {f"builtins.{symbol}" for symbol in builtin_symbols} | {
29
+ f"{module}.{symbol}" for module in TYPING_SOURCES for symbol in ("TypeGuard", "TypeIs")
30
+ }
31
+ sensitive_names = builtin_symbols | {
32
+ name
33
+ for name, target in context.module_imports.bindings.items()
34
+ if target.module in TYPING_SOURCES | {"builtins"}
35
+ and (target.symbol is None or target.symbol in builtin_symbols | {"TypeGuard", "TypeIs"})
36
+ }
37
+ return any(
38
+ _ambiguous_node(node, context, mutation_targets, sensitive_names)
39
+ for node in context.nodes(
40
+ ast.Import,
41
+ ast.ImportFrom,
42
+ ast.Attribute,
43
+ ast.ExceptHandler,
44
+ ast.MatchAs,
45
+ ast.MatchStar,
46
+ ast.MatchMapping,
47
+ ast.NamedExpr,
48
+ )
49
+ )
50
+
51
+
52
+ def _namespace_escapes(context: PythonFileContext) -> bool:
53
+ if context.dynamic_namespace or any(node.id == "__builtins__" for node in context.nodes(ast.Name)):
54
+ return True
55
+ namespace_names = {
56
+ name
57
+ for name, target in context.module_imports.bindings.items()
58
+ if target.symbol is None and target.module in TYPING_SOURCES | {"builtins"}
59
+ }
60
+ for name in context.nodes(ast.Name):
61
+ if name.id not in namespace_names:
62
+ continue
63
+ parent = context.parents.get(name)
64
+ if not isinstance(parent, ast.Attribute) or parent.value is not name or parent.attr == "__dict__":
65
+ return True
66
+ return False
67
+
68
+
69
+ def _ambiguous_node(
70
+ node: ast.AST, context: PythonFileContext, mutation_targets: set[str], sensitive_names: frozenset[str]
71
+ ) -> bool:
72
+ match node:
73
+ case ast.Import() | ast.ImportFrom():
74
+ return _ambiguous_import(node, context.module_imports, sensitive_names)
75
+ case ast.Attribute(ctx=(ast.Store() | ast.Del())):
76
+ return context.module_imports.resolved_qualified_name(node) in mutation_targets
77
+ case (
78
+ ast.ExceptHandler(name=name)
79
+ | ast.MatchAs(name=name)
80
+ | ast.MatchStar(name=name)
81
+ | ast.MatchMapping(rest=name)
82
+ | ast.NamedExpr(target=ast.Name(id=name))
83
+ ):
84
+ return name in sensitive_names
85
+ case _:
86
+ return False
87
+
88
+
89
+ def _ambiguous_import(node: ast.Import | ast.ImportFrom, imports: ImportIndex, sensitive_names: frozenset[str]) -> bool:
90
+ if isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
91
+ return True
92
+ for alias in node.names:
93
+ name = alias.asname or alias.name.partition(".")[0]
94
+ if name not in sensitive_names:
95
+ continue
96
+ if not _preserves_canonical_import(node, alias, imports):
97
+ return True
98
+ return False
99
+
100
+
101
+ def _preserves_canonical_import(node: ast.Import | ast.ImportFrom, alias: ast.alias, imports: ImportIndex) -> bool:
102
+ if isinstance(node, ast.ImportFrom):
103
+ if node.level:
104
+ return False
105
+ module, symbol = node.module, alias.name
106
+ else:
107
+ module, symbol = alias.name, None
108
+ name = alias.asname or alias.name.partition(".")[0]
109
+ target = imports.bindings.get(name)
110
+ if target is None:
111
+ return module == "builtins" and symbol == name
112
+ 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
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, Final, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._imports import TYPING_SOURCES
20
+ from sarj_python_lint.rules._type_guard_symbols import builtin_guard_symbol, guard_symbols_ambiguous
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+ from sarj_python_lint.rules._imports import ImportIndex
26
+
27
+
28
+ _SCALARS = frozenset({"int", "bool", "str", "bytes"})
29
+ _BUILTINS = _SCALARS | {"object", "isinstance"}
30
+ _WITNESSES: Final = {
31
+ "int": frozenset({"int", "bool"}),
32
+ "bool": frozenset({"bool"}),
33
+ "str": frozenset({"str"}),
34
+ "bytes": frozenset({"bytes"}),
35
+ }
36
+ _ALL_WITNESSES = frozenset({"int", "bool", "str", "bytes"})
37
+ _MAX_INPUT_NODES = 4096
38
+ _BAD = (
39
+ "from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n return isinstance(value, int)\n"
40
+ )
41
+ _GOOD = _BAD.replace("isinstance(value, int)", "isinstance(value, str)")
42
+
43
+
44
+ @final
45
+ class NoContradictoryTypeGuard(Rule):
46
+ id = "no-contradictory-type-guard"
47
+ code = "SARJ513"
48
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
49
+ default_level=Severity.WARNING,
50
+ summary="Primitive type guards must agree with the isinstance check they return.",
51
+ rationale=(
52
+ "A guard annotation does not validate its implementation. Returning an incompatible isinstance "
53
+ "check makes callers trust the wrong primitive; TypeIs also incorrectly excludes rejected target "
54
+ "values in the false branch."
55
+ ),
56
+ remediation=(
57
+ "Align the declared target and runtime test. For an intentional partial predicate, TypeGuard may "
58
+ "narrow only the true branch; TypeIs must recognize every target value admitted by its input. "
59
+ "Review callers before changing the narrowing contract."
60
+ ),
61
+ category=RuleCategory.CORRECTNESS,
62
+ autofix=AutofixPolicy.NONE,
63
+ limitations=(
64
+ "Checks undecorated, non-generic module functions with one parameter and a single returned direct builtin isinstance call, optionally after a docstring.",
65
+ "Guard symbols must resolve to typing or typing_extensions TypeGuard or TypeIs. Targets and checked classes are builtin int, bool, str or bytes; bool is treated as an int subtype.",
66
+ "Inputs must be object or fully classified plain unions of those primitive types and None. Impossible isinstance tests and compiler-invalid TypeIs target/input relationships are excluded.",
67
+ "Input unions are traversed iteratively with a bounded node budget; overly complex annotations are excluded.",
68
+ "Literal and refined targets, aliases, custom classes, float numeric promotion, tuple isinstance tests, helper predicates, nested functions and complex bodies are not inferred.",
69
+ "TypeGuard may intentionally recognize a subset of its target, so TypeGuard[int] with an isinstance bool test remains legal. Visible namespace mutation and ambiguous or shadowed symbols abstain conservatively.",
70
+ "Generated files are excluded; maintained tests remain checked. No autofix guesses a domain or changes false-branch behavior.",
71
+ ),
72
+ examples=(
73
+ RuleExample(
74
+ example_id="wrong-primitive-promise",
75
+ title="An integer test does not prove a string",
76
+ outcome=ExampleOutcome.MATCH,
77
+ files=(ExampleFile.python("app/guards.py", _BAD),),
78
+ focus_path=PurePosixPath("app/guards.py"),
79
+ expected_count=1,
80
+ public=True,
81
+ ),
82
+ RuleExample(
83
+ example_id="consistent-primitive-promise",
84
+ title="Recognize the primitive promised to callers",
85
+ outcome=ExampleOutcome.NO_MATCH,
86
+ files=(ExampleFile.python("app/guards.py", _GOOD),),
87
+ focus_path=PurePosixPath("app/guards.py"),
88
+ expected_count=0,
89
+ public=True,
90
+ ),
91
+ ),
92
+ )
93
+ description = documentation.summary
94
+
95
+ @override
96
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
97
+ if context.generated or not any(symbol in context.symbol_source for symbol in ("TypeGuard", "TypeIs")):
98
+ return []
99
+ if context.tree is None or guard_symbols_ambiguous(context, _BUILTINS):
100
+ return []
101
+ findings: list[Diagnostic] = []
102
+ for function in context.tree.body:
103
+ if not isinstance(function, ast.FunctionDef):
104
+ continue
105
+ statement = _contradictory_return(function, context)
106
+ if statement is None or is_suppressed(context.source_lines, statement.lineno, self.code):
107
+ continue
108
+ findings.append(
109
+ Diagnostic(
110
+ context.path,
111
+ statement.lineno,
112
+ statement.col_offset + 1,
113
+ self.code,
114
+ "This guard's isinstance test contradicts its primitive narrowing promise; align the target "
115
+ "and test, preserving TypeIs false-branch behavior and intentional TypeGuard partial predicates.",
116
+ severity=Severity.WARNING,
117
+ )
118
+ )
119
+ return findings
120
+
121
+
122
+ def _contradictory_return(function: ast.FunctionDef, context: PythonFileContext) -> ast.Return | None:
123
+ parameter = _sole_parameter(function)
124
+ annotation = function.returns
125
+ if parameter is None or not isinstance(annotation, ast.Subscript):
126
+ return None
127
+ guard = context.module_imports.resolved_symbol(annotation.value, sources=TYPING_SOURCES)
128
+ if guard not in {"TypeGuard", "TypeIs"}:
129
+ return None
130
+ target_kind = builtin_guard_symbol(annotation.slice, context.module_imports, _BUILTINS)
131
+ target = _WITNESSES.get(target_kind) if target_kind is not None else None
132
+ inputs = _input_witnesses(parameter.annotation, context.module_imports)
133
+ if target is None or inputs is None or (guard == "TypeIs" and not target <= inputs):
134
+ return None
135
+ statement = _single_return(function)
136
+ checked_kind = _checked_kind(statement.value, parameter.arg, context) if statement is not None else None
137
+ checked = _WITNESSES.get(checked_kind) if checked_kind is not None else None
138
+ if checked is None or not inputs & checked:
139
+ return None
140
+ accepts_wrong = bool((inputs & checked) - target)
141
+ rejects_target = guard == "TypeIs" and bool((inputs & target) - checked)
142
+ return statement if accepts_wrong or rejects_target else None
143
+
144
+
145
+ def _sole_parameter(function: ast.FunctionDef) -> ast.arg | None:
146
+ args = function.args
147
+ positional = [*args.posonlyargs, *args.args]
148
+ if function.decorator_list or function.type_params or len(positional) != 1:
149
+ return None
150
+ if args.kwonlyargs or args.vararg is not None or args.kwarg is not None:
151
+ return None
152
+ return positional[0]
153
+
154
+
155
+ def _single_return(function: ast.FunctionDef) -> ast.Return | None:
156
+ body = function.body
157
+ if (
158
+ body
159
+ and isinstance(body[0], ast.Expr)
160
+ and isinstance(body[0].value, ast.Constant)
161
+ and isinstance(body[0].value.value, str)
162
+ ):
163
+ body = body[1:]
164
+ return body[0] if len(body) == 1 and isinstance(body[0], ast.Return) else None
165
+
166
+
167
+ def _input_witnesses(annotation: ast.expr | None, imports: ImportIndex) -> frozenset[str] | None:
168
+ pending = [annotation]
169
+ visited = 0
170
+ witnesses: set[str] = set()
171
+ while pending:
172
+ member = pending.pop()
173
+ visited += 1
174
+ if visited > _MAX_INPUT_NODES:
175
+ return None
176
+ if isinstance(member, ast.Constant) and member.value is None:
177
+ continue
178
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
179
+ pending.extend((member.left, member.right))
180
+ continue
181
+ kind = builtin_guard_symbol(member, imports, _BUILTINS)
182
+ if kind == "object":
183
+ witnesses.update(_ALL_WITNESSES)
184
+ elif kind in _WITNESSES:
185
+ witnesses.update(_WITNESSES[kind])
186
+ else:
187
+ return None
188
+ return frozenset(witnesses)
189
+
190
+
191
+ def _checked_kind(node: ast.expr | None, parameter: str, context: PythonFileContext) -> str | None:
192
+ match node:
193
+ case ast.Call(func=func, args=[ast.Name(id=name), target], keywords=[]) if name == parameter:
194
+ if builtin_guard_symbol(func, context.imports, _BUILTINS) == "isinstance":
195
+ return builtin_guard_symbol(target, context.imports, _BUILTINS)
196
+ case _:
197
+ pass
198
+ return None