sarj-python-lint 1.45.0__tar.gz → 1.45.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/__main__.py +18 -4
  4. sarj_python_lint-1.45.2/src/sarj_python_lint/json_boundary.py +29 -0
  5. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/ratchet.py +1 -1
  6. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_tautological_expect.py +52 -17
  7. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +58 -54
  8. sarj_python_lint-1.45.0/src/sarj_python_lint/json_boundary.py +0 -10
  9. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/.gitignore +0 -0
  10. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/LICENSE +0 -0
  11. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/README.md +0 -0
  12. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  14. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_source.py +0 -0
  20. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  23. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  30. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  31. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  32. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  33. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  34. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  35. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  36. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  37. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  38. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  39. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  40. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  41. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  42. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  43. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  44. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  45. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  46. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  47. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  48. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  49. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  50. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  51. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  52. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  53. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  54. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  55. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  56. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  57. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  58. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  59. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  60. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  61. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  62. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  63. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  64. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  65. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  66. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  67. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  68. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  69. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  70. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  71. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  72. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  73. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  74. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  75. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  76. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  77. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  78. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  79. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  80. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  81. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  82. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  83. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  84. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  85. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  86. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  87. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  88. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  89. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  90. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  91. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  92. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  93. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  94. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  95. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  96. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  97. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  98. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  99. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  100. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  101. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  102. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  103. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  104. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  105. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  106. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  107. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  108. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  109. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  110. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  111. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  112. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  113. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  114. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  115. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  116. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  117. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  118. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  119. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  120. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  121. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  122. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  123. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  124. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  125. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  126. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  127. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  128. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  129. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  130. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  131. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  132. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  133. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  134. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  135. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  136. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  137. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  138. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  139. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  140. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  141. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  142. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  143. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  144. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  145. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  146. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  147. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  148. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  149. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  150. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  151. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  152. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  153. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  154. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  155. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  156. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  157. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  158. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  159. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  160. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  161. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  162. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  163. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  164. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  165. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  166. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  167. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  168. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  169. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  170. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  171. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  172. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  173. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  174. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  175. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  176. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  177. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  178. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  179. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  180. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  181. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  182. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  183. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  184. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  185. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  186. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  187. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  188. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  189. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  190. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  191. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  192. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  193. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  194. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  195. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  196. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  197. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  198. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  199. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  200. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  201. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  202. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  203. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  204. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  205. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  206. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  207. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  208. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  209. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  210. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  211. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  212. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  213. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  214. {sarj_python_lint-1.45.0 → sarj_python_lint-1.45.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.45.0
3
+ Version: 1.45.2
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.45.0"
3
+ version = "1.45.2"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -430,12 +430,26 @@ def _baseline_path(path: Path, *, root: Path | None = None) -> str:
430
430
  def _read_baseline(path: Path) -> dict[str, dict[str, int]]:
431
431
  raw = parse_json(path.read_text(encoding="utf-8"))
432
432
  if not is_object_mapping(raw):
433
- return {}
433
+ msg = "baseline must be an object of file paths and diagnostic counts"
434
+ raise ValueError(msg)
434
435
  counts: dict[str, dict[str, int]] = {}
435
436
  for file_key, per_code in raw.items():
436
- if not isinstance(file_key, str) or not is_object_mapping(per_code):
437
- continue
438
- counts[file_key] = {code: n for code, n in per_code.items() if isinstance(code, str) and isinstance(n, int)}
437
+ if not isinstance(file_key, str) or not file_key or not is_object_mapping(per_code):
438
+ msg = "baseline entries must map non-empty file paths to diagnostic counts"
439
+ raise ValueError(msg)
440
+ validated: dict[str, int] = {}
441
+ for code, count in per_code.items():
442
+ if (
443
+ not isinstance(code, str)
444
+ or not code
445
+ or not isinstance(count, int)
446
+ or isinstance(count, bool)
447
+ or count < 0
448
+ ):
449
+ msg = "baseline counts must map non-empty diagnostic codes to non-negative integers"
450
+ raise ValueError(msg)
451
+ validated[code] = count
452
+ counts[file_key] = validated
439
453
  return counts
440
454
 
441
455
 
@@ -0,0 +1,29 @@
1
+ import json
2
+ from typing import TypeIs
3
+
4
+
5
+ def parse_json(text: str) -> object:
6
+ try:
7
+ return json.loads(text, object_pairs_hook=_unique_pairs, parse_constant=_invalid_constant) # pyright: ignore[reportAny] -- untyped stdlib parser boundary.
8
+ except RecursionError as error:
9
+ message = "JSON nesting exceeds the supported depth"
10
+ raise ValueError(message) from error
11
+
12
+
13
+ def _unique_pairs(pairs: list[tuple[str, object]]) -> dict[str, object]:
14
+ result: dict[str, object] = {}
15
+ for key, value in pairs:
16
+ if key in result:
17
+ message = f"duplicate JSON key: {key}"
18
+ raise ValueError(message)
19
+ result[key] = value
20
+ return result
21
+
22
+
23
+ def _invalid_constant(value: str) -> None:
24
+ message = f"non-finite JSON constant: {value}"
25
+ raise ValueError(message)
26
+
27
+
28
+ def is_object_mapping(value: object) -> TypeIs[dict[object, object]]:
29
+ return isinstance(value, dict)
@@ -221,7 +221,7 @@ def seed(measurement: Measurement, baseline: Baseline) -> Baseline:
221
221
  def load_baseline(path: Path) -> Baseline:
222
222
  raw = parse_json(path.read_text(encoding="utf-8"))
223
223
  schema = _get(raw, "schema_version")
224
- if schema is not None and schema != BASELINE_SCHEMA_VERSION:
224
+ if schema is not None and (type(schema) is not int or schema != BASELINE_SCHEMA_VERSION):
225
225
  msg = f"unsupported suppression baseline schema_version: {schema!r}"
226
226
  raise ValueError(msg)
227
227
  files = _get(raw, "files")
@@ -56,6 +56,7 @@ class NoTautologicalExpect(Rule):
56
56
  "Detection covers truthy scalar constants, definitely non-empty list/set/dict displays, and directly asserted generator expressions or lambdas.",
57
57
  "Generator consumption, lambda invocation, comprehensions, and runtime iterator/callable values are not inferred; empty universal-quantification contracts remain valid.",
58
58
  "Test-local immutable scalar bindings and their aliases are tracked only through straight-line assignments; calls, control flow, mutable values, and global/nonlocal writes invalidate tracking.",
59
+ "Pure Boolean conditions, compatible numeric/string/bytes ordering and chained comparisons over known scalar setup are evaluated; proven-true pure assertions retain bindings because their messages do not execute.",
59
60
  "Ruff owns asserted strings and tuples, literal comparisons and identity, and unittest-style assertion methods.",
60
61
  "Generated files, always-failing assertions, deliberate match-arm success markers, and runtime-value comparisons are excluded.",
61
62
  ),
@@ -196,12 +197,13 @@ def _function_bound_tautologies(function: ast.FunctionDef | ast.AsyncFunctionDef
196
197
  for statement in function.body:
197
198
  if _record_scalar_assignment(statement, bindings):
198
199
  continue
199
- if isinstance(statement, ast.Assert):
200
+ if isinstance(statement, ast.Assert) and bindings and _scalar_condition(statement.test, bindings) is True:
200
201
  references_setup = any(
201
202
  isinstance(node, ast.Name) and node.id in bindings for node in ast.walk(statement.test)
202
203
  )
203
- if references_setup and _scalar_condition(statement.test, bindings) is True:
204
+ if references_setup:
204
205
  found.append(_Tautology(statement, "the condition follows only from unchanged scalar setup"))
206
+ continue
205
207
  bindings.clear()
206
208
  return found
207
209
 
@@ -239,26 +241,59 @@ def _scalar_value(node: ast.expr, bindings: dict[str, object]) -> object:
239
241
 
240
242
 
241
243
  def _scalar_condition(node: ast.expr, bindings: dict[str, object]) -> bool | None:
242
- if isinstance(node, ast.Compare) and len(node.ops) == 1:
243
- left = _scalar_value(node.left, bindings)
244
- right = _scalar_value(node.comparators[0], bindings)
245
- if left is _UNKNOWN or right is _UNKNOWN:
244
+ if isinstance(node, ast.BoolOp):
245
+ values = [_scalar_condition(value, bindings) for value in node.values]
246
+ if None in values:
246
247
  return None
247
- match node.ops[0]:
248
- case ast.Eq():
249
- return left == right
250
- case ast.NotEq():
251
- return left != right
252
- case ast.Is() if left is None or isinstance(left, bool):
253
- return left is right
254
- case ast.IsNot() if left is None or isinstance(left, bool):
255
- return left is not right
256
- case _:
257
- return None
248
+ return all(values) if isinstance(node.op, ast.And) else any(values)
249
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
250
+ value = _scalar_condition(node.operand, bindings)
251
+ return None if value is None else not value
252
+ if isinstance(node, ast.Compare):
253
+ values = [_scalar_value(value, bindings) for value in (node.left, *node.comparators)]
254
+ results = list(map(_scalar_relation, values[:-1], values[1:], node.ops, strict=True))
255
+ return None if None in results else all(results)
258
256
  value = _scalar_value(node, bindings)
259
257
  return None if value is _UNKNOWN else bool(value)
260
258
 
261
259
 
260
+ def _scalar_relation(left: object, right: object, op: ast.cmpop) -> bool | None:
261
+ if left is _UNKNOWN or right is _UNKNOWN:
262
+ return None
263
+ match op:
264
+ case ast.Eq():
265
+ return left == right
266
+ case ast.NotEq():
267
+ return left != right
268
+ case ast.Is() if left is None or right is None or isinstance(left, bool) or isinstance(right, bool):
269
+ return left is right
270
+ case ast.IsNot() if left is None or right is None or isinstance(left, bool) or isinstance(right, bool):
271
+ return left is not right
272
+ case _:
273
+ pass
274
+ if isinstance(left, (int, float)) and isinstance(right, (int, float)):
275
+ return _ordered_relation(left, right, op)
276
+ if isinstance(left, str) and isinstance(right, str):
277
+ return _ordered_relation(left, right, op)
278
+ if isinstance(left, bytes) and isinstance(right, bytes):
279
+ return _ordered_relation(left, right, op)
280
+ return None
281
+
282
+
283
+ def _ordered_relation[T: (int | float, str, bytes)](left: T, right: T, op: ast.cmpop) -> bool | None:
284
+ match op:
285
+ case ast.Lt():
286
+ return left < right
287
+ case ast.LtE():
288
+ return left <= right
289
+ case ast.Gt():
290
+ return left > right
291
+ case ast.GtE():
292
+ return left >= right
293
+ case _:
294
+ return None
295
+
296
+
262
297
  def _exempt_nodes(tree: ast.Module, *, node_index: NodeIndex | None = None) -> set[ast.AST]:
263
298
  exempt: set[ast.AST] = set()
264
299
  for node in walk_ast(tree, index=node_index):
@@ -2,8 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from collections import Counter
5
+ from functools import cached_property
5
6
  from pathlib import PurePosixPath
6
- from typing import TYPE_CHECKING, ClassVar, NamedTuple, override
7
+ from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
7
8
 
8
9
  from sarj_python_lint.rule_base import (
9
10
  AutofixPolicy,
@@ -43,6 +44,34 @@ class _HelperImport(NamedTuple):
43
44
  level: int
44
45
 
45
46
 
47
+ @final
48
+ class _ModuleFacts:
49
+ def __init__(self, tree: ast.Module) -> None:
50
+ self.tree = tree
51
+
52
+ @cached_property
53
+ def bindings(self) -> Counter[str]:
54
+ return _scope_bindings(self.tree)
55
+
56
+ @cached_property
57
+ def writes(self) -> tuple[ast.Attribute | ast.Subscript, ...]:
58
+ return tuple(
59
+ node
60
+ for node in ast.walk(self.tree)
61
+ if isinstance(node, (ast.Attribute, ast.Subscript)) and isinstance(node.ctx, (ast.Store, ast.Del))
62
+ )
63
+
64
+ @cached_property
65
+ def mutated_roots(self) -> frozenset[str]:
66
+ return frozenset(root for node in self.writes if (root := _root_name(node.value)) is not None)
67
+
68
+ @cached_property
69
+ def references(self) -> frozenset[str]:
70
+ return frozenset(
71
+ node.id for node in ast.walk(self.tree) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
72
+ )
73
+
74
+
46
75
  class PreferPytestFixtureInjection(Rule):
47
76
  id: str = "prefer-pytest-fixture-injection"
48
77
  code: str = "SARJ476"
@@ -70,6 +99,7 @@ class PreferPytestFixtureInjection(Rule):
70
99
  "Async pools require an explicit literal open=False, no unpacked arguments, and no calls inside constructor arguments; opened or dynamically configured async pools need lifecycle-specific fixture design.",
71
100
  "Setup must be the first executable statement and immediately feed a plain call, or appear inline as a direct argument of that first call.",
72
101
  "Class tests, decorated helpers, unknown test decorators, dynamic imports, re-exports, and ambiguous or out-of-bound source paths are excluded.",
102
+ "Tests with statically visible callable references in any scope are excluded; scope-ambiguous references are handled conservatively.",
73
103
  "Command-line conftest discovery overrides and external pytest plugin registrations are not inferred.",
74
104
  ),
75
105
  examples=(
@@ -167,24 +197,23 @@ class PreferPytestFixtureInjection(Rule):
167
197
  ):
168
198
  return []
169
199
  tree = context.tree
170
- helpers = _resource_helpers(context)
200
+ facts = _ModuleFacts(tree)
201
+ helpers = _resource_helpers(context, facts)
171
202
  if not helpers:
172
203
  return []
173
- module_bindings = _scope_bindings(tree)
174
- if "__test__" in module_bindings or any(
175
- isinstance(node, ast.Attribute) and node.attr == "__test__" and isinstance(node.ctx, (ast.Store, ast.Del))
176
- for node in ast.walk(tree)
204
+ if "__test__" in facts.bindings or any(
205
+ isinstance(node, ast.Attribute) and node.attr == "__test__" for node in facts.writes
177
206
  ):
178
207
  return []
179
208
  diagnostics: list[Diagnostic] = []
180
209
  for statement in tree.body:
181
210
  if (
182
211
  not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
183
- or module_bindings[statement.name] != 1
184
- or _module_reference(tree, statement.name)
212
+ or facts.bindings[statement.name] != 1
213
+ or statement.name in facts.references
185
214
  ):
186
215
  continue
187
- call = _setup_call(context, statement, helpers)
216
+ call = _setup_call(context, statement, helpers, facts)
188
217
  if call is not None:
189
218
  diagnostics.append(
190
219
  Diagnostic(
@@ -205,17 +234,16 @@ class PreferPytestFixtureInjection(Rule):
205
234
  return diagnostics
206
235
 
207
236
 
208
- def _resource_helpers(context: PythonFileContext) -> frozenset[str]:
237
+ def _resource_helpers(context: PythonFileContext, facts: _ModuleFacts) -> frozenset[str]:
209
238
  tree = context.tree
210
239
  if tree is None:
211
240
  return frozenset()
212
241
  references = tuple(_helper_imports(tree))
213
242
  if not references:
214
243
  return frozenset()
215
- bindings = _scope_bindings(tree)
216
244
  helpers: set[str] = set()
217
245
  for reference in references:
218
- if bindings[reference.local] != 1 or _root_mutated(tree, reference.local):
246
+ if facts.bindings[reference.local] != 1 or reference.local in facts.mutated_roots:
219
247
  continue
220
248
  target = context.session.local_source.resolve_module(
221
249
  context.path, reference.module, reference.level, facts=context.session.first_party
@@ -249,11 +277,12 @@ def _helper_imports(tree: ast.Module) -> Iterator[_HelperImport]:
249
277
 
250
278
 
251
279
  def _is_resource_helper(module: LocalModule, symbol: str) -> bool:
280
+ facts = _ModuleFacts(module.tree)
252
281
  if (
253
282
  is_generated(module.path, module.source)
254
- or _scope_bindings(module.tree)[symbol] != 1
283
+ or facts.bindings[symbol] != 1
255
284
  or _has_wildcard(module.tree)
256
- or _root_mutated(module.tree, symbol)
285
+ or symbol in facts.mutated_roots
257
286
  ):
258
287
  return False
259
288
  helper = next(
@@ -275,8 +304,8 @@ def _is_resource_helper(module: LocalModule, symbol: str) -> bool:
275
304
  qualified = module.runtime_imports.resolved_qualified_name(constructor)
276
305
  return (
277
306
  root is not None
278
- and _scope_bindings(module.tree)[root] == 1
279
- and not _root_mutated(module.tree, root)
307
+ and facts.bindings[root] == 1
308
+ and root not in facts.mutated_roots
280
309
  and qualified in _POOL_CONSTRUCTORS
281
310
  and (qualified != "psycopg_pool.AsyncConnectionPool" or _unopened_async_allocation(allocation))
282
311
  )
@@ -294,9 +323,11 @@ def _unopened_async_allocation(allocation: ast.Call) -> bool:
294
323
  )
295
324
 
296
325
 
297
- def _setup_call(context: PythonFileContext, function: Function, helpers: frozenset[str]) -> ast.Call | None:
326
+ def _setup_call(
327
+ context: PythonFileContext, function: Function, helpers: frozenset[str], facts: _ModuleFacts
328
+ ) -> ast.Call | None:
298
329
  if not function.name.startswith("test_") or any(
299
- not _pytest_mark(context, decorator) for decorator in function.decorator_list
330
+ not _pytest_mark(context, decorator, facts) for decorator in function.decorator_list
300
331
  ):
301
332
  return None
302
333
  bindings = _scope_bindings(function)
@@ -313,21 +344,21 @@ def _setup_call(context: PythonFileContext, function: Function, helpers: frozens
313
344
  if setup is None:
314
345
  return None
315
346
  if _helper_call(setup, available):
316
- return _assigned_dependency_call(context, body, bindings, setup)
317
- if not _unshadowed_consumer(context, setup, bindings):
347
+ return _assigned_dependency_call(body, bindings, setup, facts)
348
+ if not _unshadowed_consumer(setup, bindings, facts):
318
349
  return None
319
350
  inline = [arg for arg in _arguments(setup) if isinstance(arg, ast.Call) and _helper_call(arg, available)]
320
351
  return inline[0] if len(inline) == 1 else None
321
352
 
322
353
 
323
354
  def _assigned_dependency_call(
324
- context: PythonFileContext, body: list[ast.stmt], bindings: Counter[str], setup: ast.Call
355
+ body: list[ast.stmt], bindings: Counter[str], setup: ast.Call, facts: _ModuleFacts
325
356
  ) -> ast.Call | None:
326
357
  resource = _assignment_name(body[0])
327
358
  if resource is None or bindings[resource] != 1 or len(body) < _SETUP_AND_CONSUMER:
328
359
  return None
329
360
  consumer = _plain_call(body[1])
330
- if consumer is None or not _unshadowed_consumer(context, consumer, bindings):
361
+ if consumer is None or not _unshadowed_consumer(consumer, bindings, facts):
331
362
  return None
332
363
  for argument in _arguments(consumer):
333
364
  if isinstance(argument, ast.Name) and argument.id == resource:
@@ -335,7 +366,7 @@ def _assigned_dependency_call(
335
366
  return None
336
367
 
337
368
 
338
- def _pytest_mark(context: PythonFileContext, decorator: ast.expr) -> bool:
369
+ def _pytest_mark(context: PythonFileContext, decorator: ast.expr, facts: _ModuleFacts) -> bool:
339
370
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
340
371
  qualified = context.module_imports.resolved_qualified_name(target)
341
372
  tree = context.tree
@@ -345,12 +376,10 @@ def _pytest_mark(context: PythonFileContext, decorator: ast.expr) -> bool:
345
376
  or not qualified.startswith("pytest.mark.")
346
377
  or tree is None
347
378
  or root is None
348
- or _scope_bindings(tree)[root] != 1
379
+ or facts.bindings[root] != 1
349
380
  ):
350
381
  return False
351
- for node in ast.walk(tree):
352
- if not isinstance(node, (ast.Attribute, ast.Subscript)) or not isinstance(node.ctx, (ast.Store, ast.Del)):
353
- continue
382
+ for node in facts.writes:
354
383
  if isinstance(node, ast.Subscript) and _root_name(node.value) == root:
355
384
  return False
356
385
  changed = context.module_imports.resolved_qualified_name(node)
@@ -359,25 +388,9 @@ def _pytest_mark(context: PythonFileContext, decorator: ast.expr) -> bool:
359
388
  return True
360
389
 
361
390
 
362
- def _unshadowed_consumer(context: PythonFileContext, call: ast.Call, bindings: Counter[str]) -> bool:
391
+ def _unshadowed_consumer(call: ast.Call, bindings: Counter[str], facts: _ModuleFacts) -> bool:
363
392
  root = _root_name(call.func)
364
- tree = context.tree
365
- return (
366
- root is not None
367
- and root not in bindings
368
- and tree is not None
369
- and _scope_bindings(tree)[root] <= 1
370
- and not _root_mutated(tree, root)
371
- )
372
-
373
-
374
- def _module_reference(tree: ast.Module, name: str) -> bool:
375
- return any(
376
- isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load)
377
- for statement in tree.body
378
- if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
379
- for node in ast.walk(statement)
380
- )
393
+ return root is not None and root not in bindings and facts.bindings[root] <= 1 and root not in facts.mutated_roots
381
394
 
382
395
 
383
396
  def _helper_call(call: ast.Call, helpers: frozenset[str] | set[str]) -> bool:
@@ -509,12 +522,3 @@ def _has_wildcard(tree: ast.Module) -> bool:
509
522
  return any(
510
523
  isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in ast.walk(tree)
511
524
  )
512
-
513
-
514
- def _root_mutated(tree: ast.Module, root: str) -> bool:
515
- return any(
516
- isinstance(node, (ast.Attribute, ast.Subscript))
517
- and isinstance(node.ctx, (ast.Store, ast.Del))
518
- and _root_name(node.value) == root
519
- for node in ast.walk(tree)
520
- )
@@ -1,10 +0,0 @@
1
- import json
2
- from typing import TypeIs
3
-
4
-
5
- def parse_json(text: str) -> object:
6
- return json.loads(text) # pyright: ignore[reportAny] -- untyped stdlib parser boundary.
7
-
8
-
9
- def is_object_mapping(value: object) -> TypeIs[dict[object, object]]:
10
- return isinstance(value, dict)