sarj-python-lint 1.20.1__tar.gz → 1.20.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 (179) hide show
  1. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_resource_provenance.py +8 -2
  4. sarj_python_lint-1.20.2/src/sarj_python_lint/rules/prefer_native_string_check.py +486 -0
  5. sarj_python_lint-1.20.1/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -169
  6. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/.gitignore +0 -0
  7. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/LICENSE +0 -0
  8. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/README.md +0 -0
  9. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  29. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  30. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  34. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  35. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  36. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  38. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  39. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  40. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  41. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  42. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  43. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  44. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  45. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  46. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  47. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  48. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  49. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  50. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  51. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  52. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  53. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  54. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  55. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  56. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  57. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  58. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  59. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  60. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  61. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  62. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  63. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  64. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  65. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  66. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  67. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  68. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  69. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  70. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  71. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  72. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  73. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  74. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  75. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  76. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  77. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  78. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  79. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  80. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  81. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  82. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  83. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  84. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  85. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  86. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  87. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  88. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  89. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  90. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  91. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  92. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  93. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  94. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  95. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  96. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  97. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  98. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  99. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  100. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  101. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  102. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  103. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  104. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  105. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  106. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  107. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  108. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  109. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  110. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  111. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  112. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  113. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  114. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  115. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  116. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  117. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  118. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  119. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  120. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  121. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  122. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  123. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  124. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  125. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  126. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  127. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  128. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  129. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  130. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  131. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  132. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  133. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  134. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  135. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  136. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  137. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  138. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  139. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  140. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  141. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  142. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  143. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  144. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  145. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  146. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  147. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  148. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  149. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  150. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  151. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  152. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  153. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  154. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  155. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  156. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  157. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  158. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  159. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  160. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  161. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  162. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  163. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  164. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  165. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  166. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  167. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  168. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  169. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  170. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  171. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  172. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  173. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  174. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  175. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  176. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  177. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  178. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  179. {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.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.20.1
3
+ Version: 1.20.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.20.1"
3
+ version = "1.20.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" }]
@@ -28,6 +28,7 @@ class ResourceProvenance:
28
28
  self.context = context
29
29
  self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
30
30
  self.reads: dict[str, list[ast.Name]] = defaultdict(list)
31
+ self._reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
31
32
  self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
32
33
  self.wildcard = False
33
34
  self.mutated: set[str] = set()
@@ -132,10 +133,15 @@ class ResourceProvenance:
132
133
  return False
133
134
  key = (binding, allow_context)
134
135
  if key not in self.escape_cache:
136
+ if name.id not in self._reads_by_binding:
137
+ grouped: dict[ast.AST, list[ast.Name]] = defaultdict(list)
138
+ for read in self.reads.get(name.id, ()):
139
+ if (read_binding := self.binding(read.id, read)) is not None:
140
+ grouped[read_binding].append(read)
141
+ self._reads_by_binding[name.id] = grouped
135
142
  self.escape_cache[key] = all(
136
143
  self._method_or_context_read(read, allow_context=allow_context)
137
- for read in self.reads.get(name.id, ())
138
- if self.binding(read.id, read) is binding
144
+ for read in self._reads_by_binding[name.id].get(binding, ())
139
145
  )
140
146
  return self.escape_cache[key]
141
147
 
@@ -0,0 +1,486 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, ClassVar, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ is_suppressed,
19
+ )
20
+ from sarj_python_lint.rules._imports import ImportIndex
21
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from sarj_python_lint._file_context import PythonFileContext
26
+
27
+
28
+ _PYDANTIC = frozenset({"pydantic", "pydantic.type_adapter"})
29
+ _BUILTINS = frozenset({"builtins"})
30
+ _PYTEST = frozenset({"pytest"})
31
+ _VALIDATION_ERROR = frozenset({"pydantic", "pydantic_core"})
32
+ _MODULES = _PYDANTIC | _BUILTINS | _PYTEST | _VALIDATION_ERROR
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class _ImportReference:
37
+ index: ImportIndex
38
+ root_key: str
39
+
40
+
41
+ @final
42
+ class PreferNativeStringCheck(Rule):
43
+ id = "prefer-native-string-check"
44
+ code = "SARJ480"
45
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
46
+ default_level=Severity.WARNING,
47
+ summary="Use native string assertions instead of unconstrained Pydantic coercion.",
48
+ rationale="An unconstrained TypeAdapter(str).validate_python(...) obscures a simple string check and can decode bytes instead of rejecting them.",
49
+ remediation="Use isinstance(value, str) and the appropriate failure path; in tests, assert the type. Keep structured validation at the model or JSON boundary. Preserve intentional coercion or ValidationError contracts explicitly.",
50
+ category=RuleCategory.MAINTAINABILITY,
51
+ autofix=AutofixPolicy.NONE,
52
+ limitations=(
53
+ "Only plain built-in str adapters used by validate_python inside an assertion without validation options are reported. Configured adapters, constrained types, unions, models, containers, other scalar types and JSON/serialization operations are excluded.",
54
+ "Scoped import aliases and unescaped unconditional module or local adapter bindings are recognized, including independent functions reusing a name. Conditional, reassigned, indirect or class/comprehension/parameter/global/nonlocal cached bindings are excluded, as are shadowed symbols, wildcard imports and imports with visible mutation or escape.",
55
+ "Only assertion tests are checked: messages and lambda or generator expressions are excluded. Proven pytest.raises(ValidationError) contexts are preserved; other intentional byte-decoding or exception contracts need a local reasoned suppression. No automatic replacement can preserve those contracts.",
56
+ ),
57
+ examples=(
58
+ RuleExample(
59
+ example_id="adapter-for-string-check",
60
+ title="A string assertion unnecessarily constructs a schema validator",
61
+ outcome=ExampleOutcome.MATCH,
62
+ files=(
63
+ ExampleFile.python(
64
+ "tests/test_receipt.py",
65
+ 'from pydantic import TypeAdapter\ndef test_receipt():\n assert TypeAdapter(str).validate_python(receipt["id"]) == "abc"\n',
66
+ ),
67
+ ),
68
+ focus_path=PurePosixPath("tests/test_receipt.py"),
69
+ expected_count=1,
70
+ public=True,
71
+ ),
72
+ RuleExample(
73
+ example_id="native-string-assertion",
74
+ title="A native assertion rejects unexpected types and narrows the value",
75
+ outcome=ExampleOutcome.NO_MATCH,
76
+ files=(
77
+ ExampleFile.python(
78
+ "tests/test_receipt.py",
79
+ 'def test_receipt():\n value = receipt["id"]\n assert isinstance(value, str)\n assert value == "abc"\n',
80
+ ),
81
+ ),
82
+ focus_path=PurePosixPath("tests/test_receipt.py"),
83
+ expected_count=0,
84
+ public=True,
85
+ ),
86
+ ),
87
+ )
88
+ description = documentation.summary
89
+
90
+ @override
91
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
92
+ if (
93
+ context.generated
94
+ or not {"vendor", "vendored", "third_party"}.isdisjoint(context.path.parts)
95
+ or "validate_python" not in context.source
96
+ or context.tree is None
97
+ ):
98
+ return []
99
+ provenance = ResourceProvenance(context)
100
+ imports = {
101
+ statement: ImportIndex.from_tree(ast.Module(body=[statement], type_ignores=[]))
102
+ for statement in (*context.nodes(ast.Import), *context.nodes(ast.ImportFrom))
103
+ }
104
+ unsafe_imports = _unsafe_imports(context, provenance, imports)
105
+ uncertain_names = _uncertain_cached_names(context, provenance)
106
+ mutated_bindings = _mutated_cached_bindings(context, provenance)
107
+ findings: list[Diagnostic] = []
108
+ for call in context.nodes(ast.Call):
109
+ receiver = _validation_receiver(call)
110
+ if receiver is None:
111
+ continue
112
+ if not _inside_assertion(call, context) or _expects_validation_error(
113
+ call, context, provenance, imports, unsafe_imports
114
+ ):
115
+ continue
116
+ if isinstance(receiver, ast.Name):
117
+ receiver = _cached_constructor(receiver, call, provenance, uncertain_names, mutated_bindings)
118
+ if receiver is None or not _plain_string_adapter(receiver, context, provenance, imports, unsafe_imports):
119
+ continue
120
+ if is_suppressed(context.source_lines, call.lineno, self.code):
121
+ continue
122
+ findings.append(
123
+ Diagnostic(
124
+ path=context.path,
125
+ line=call.lineno,
126
+ col=call.col_offset + 1,
127
+ code=self.code,
128
+ severity=Severity.WARNING,
129
+ message="plain str adapter in an assertion; use isinstance(value, str) with an explicit failure path, or preserve the intentional Pydantic contract explicitly",
130
+ )
131
+ )
132
+ return sorted(findings, key=lambda finding: (finding.line, finding.col))
133
+
134
+
135
+ def _cached_constructor(
136
+ receiver: ast.Name,
137
+ at: ast.Call,
138
+ provenance: ResourceProvenance,
139
+ uncertain_names: set[str],
140
+ mutated_bindings: set[ast.AST | None],
141
+ ) -> ast.Call | None:
142
+ if (
143
+ receiver.id in uncertain_names
144
+ or provenance.binding(receiver.id, at) in mutated_bindings
145
+ or not provenance.unescaped(receiver, at)
146
+ ):
147
+ return None
148
+ return provenance.constructor(receiver, at)
149
+
150
+
151
+ def _validation_receiver(call: ast.Call) -> ast.expr | None:
152
+ if (
153
+ isinstance(call.func, ast.Attribute)
154
+ and call.func.attr == "validate_python"
155
+ and len(call.args) == 1
156
+ and not call.keywords
157
+ and not isinstance(call.args[0], ast.Starred)
158
+ ):
159
+ return call.func.value
160
+ return None
161
+
162
+
163
+ def _uncertain_cached_names(context: PythonFileContext, provenance: ResourceProvenance) -> set[str]:
164
+ uncertain_names = _nonlocal_names(context)
165
+ uncertain_names.update(
166
+ name
167
+ for scope, bindings in provenance.bindings.items()
168
+ if isinstance(scope, ast.ClassDef | ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp)
169
+ for name in bindings
170
+ )
171
+ uncertain_names.update(argument.arg for argument in context.nodes(ast.arg))
172
+ return uncertain_names
173
+
174
+
175
+ def _mutated_cached_bindings(context: PythonFileContext, provenance: ResourceProvenance) -> set[ast.AST | None]:
176
+ return {
177
+ provenance.binding(call.func.value.id, call)
178
+ for call in context.nodes(ast.Call)
179
+ if isinstance(call.func, ast.Attribute)
180
+ and isinstance(call.func.value, ast.Name)
181
+ and call.func.attr in {"__init__", "__setattr__", "__delattr__"}
182
+ }
183
+
184
+
185
+ def _plain_string_adapter(
186
+ node: ast.expr,
187
+ context: PythonFileContext,
188
+ provenance: ResourceProvenance,
189
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
190
+ unsafe_imports: set[str],
191
+ ) -> bool:
192
+ if not isinstance(node, ast.Call):
193
+ return False
194
+ function = node.func.value if isinstance(node.func, ast.Subscript) else node.func
195
+ if not _resolves(function, context, provenance, imports, unsafe_imports, sources=_PYDANTIC, symbol="TypeAdapter"):
196
+ return False
197
+ value = _single_value(node, "type")
198
+ if value is None:
199
+ return False
200
+ return (
201
+ isinstance(value, ast.Name)
202
+ and value.id == "str"
203
+ and context.imports.builtin_is_unshadowed("str")
204
+ and _lexical_binding("str", value, provenance) is None
205
+ and "builtins" not in unsafe_imports
206
+ and "builtins.str" not in unsafe_imports
207
+ ) or _resolves(value, context, provenance, imports, unsafe_imports, sources=_BUILTINS, symbol="str")
208
+
209
+
210
+ def _single_value(call: ast.Call, keyword: str) -> ast.expr | None:
211
+ if len(call.args) == 1 and not call.keywords and not isinstance(call.args[0], ast.Starred):
212
+ return call.args[0]
213
+ if not call.args and len(call.keywords) == 1 and call.keywords[0].arg == keyword:
214
+ return call.keywords[0].value
215
+ return None
216
+
217
+
218
+ def _resolves(
219
+ node: ast.expr,
220
+ context: PythonFileContext,
221
+ provenance: ResourceProvenance,
222
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
223
+ unsafe_imports: set[str],
224
+ *,
225
+ sources: frozenset[str],
226
+ symbol: str,
227
+ ) -> bool:
228
+ reference = _import_reference(node, context, provenance, imports)
229
+ if reference is None or provenance.wildcard:
230
+ return False
231
+ index = reference.index
232
+ root_key = reference.root_key
233
+ qualified = index.resolved_qualified_name(node)
234
+ return (
235
+ qualified is not None
236
+ and root_key not in unsafe_imports
237
+ and (root_key not in _MODULES or qualified.rsplit(".", 1)[0] not in unsafe_imports)
238
+ and _import_key(*qualified.rsplit(".", 1)) not in unsafe_imports
239
+ and index.resolves(node, sources=sources, symbol=symbol)
240
+ )
241
+
242
+
243
+ def _lexical_binding(name: str, at: ast.AST, provenance: ResourceProvenance) -> ast.AST | None:
244
+ initial = scope = provenance.scope(at)
245
+ while scope is not None:
246
+ if (scope is initial or not isinstance(scope, ast.ClassDef)) and (
247
+ nodes := provenance.bindings.get(scope, {}).get(name)
248
+ ):
249
+ return nodes[0] if len(nodes) == 1 else None
250
+ scope = provenance.scope(scope)
251
+ return None
252
+
253
+
254
+ def _import_key(module: str, symbol: str | None) -> str:
255
+ if symbol == "TypeAdapter" and module in _PYDANTIC:
256
+ return f"pydantic.{symbol}"
257
+ if symbol == "ValidationError" and module in _VALIDATION_ERROR:
258
+ return f"pydantic_core.{symbol}"
259
+ return f"{module}.{symbol}" if symbol is not None else module
260
+
261
+
262
+ def _import_reference(
263
+ node: ast.expr,
264
+ context: PythonFileContext,
265
+ provenance: ResourceProvenance,
266
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
267
+ ) -> _ImportReference | None:
268
+ root = node
269
+ while isinstance(root, ast.Attribute):
270
+ root = root.value
271
+ if not isinstance(root, ast.Name):
272
+ return None
273
+ binding = _lexical_binding(root.id, node, provenance)
274
+ owner = context.parents.get(binding) if isinstance(binding, ast.alias) else None
275
+ index = imports.get(owner) if isinstance(owner, ast.Import | ast.ImportFrom) else None
276
+ target = index.bindings.get(root.id) if index is not None else None
277
+ if index is None or target is None:
278
+ return None
279
+ return _ImportReference(index, _import_key(target.module, target.symbol))
280
+
281
+
282
+ def _reference_key(
283
+ node: ast.expr,
284
+ context: PythonFileContext,
285
+ provenance: ResourceProvenance,
286
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
287
+ ) -> str | None:
288
+ reference = _import_reference(node, context, provenance, imports)
289
+ if reference is None:
290
+ return None
291
+ index = reference.index
292
+ root_key = reference.root_key
293
+ if qualified := index.resolved_qualified_name(node):
294
+ return qualified if qualified in _MODULES else _import_key(*qualified.rsplit(".", 1))
295
+ return root_key
296
+
297
+
298
+ def _unsafe_imports(
299
+ context: PythonFileContext,
300
+ provenance: ResourceProvenance,
301
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
302
+ ) -> set[str]:
303
+ unsafe = _mutated_imports(context, provenance, imports)
304
+ for read in (*context.nodes(ast.Name), *context.nodes(ast.Attribute)):
305
+ if not isinstance(read.ctx, ast.Load):
306
+ continue
307
+ key = _reference_key(read, context, provenance, imports)
308
+ if key is None or not _import_escapes(read, key, context, provenance, imports):
309
+ continue
310
+ unsafe.add(key)
311
+ if key in _PYDANTIC:
312
+ unsafe.add("pydantic.TypeAdapter")
313
+ if key in _VALIDATION_ERROR:
314
+ unsafe.add("pydantic_core.ValidationError")
315
+ if key == "pytest":
316
+ unsafe.add("pytest.raises")
317
+ return unsafe
318
+
319
+
320
+ def _mutated_imports(
321
+ context: PythonFileContext,
322
+ provenance: ResourceProvenance,
323
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
324
+ ) -> set[str]:
325
+ unsafe = _rebound_imports(context, imports)
326
+ for attribute in context.nodes(ast.Attribute):
327
+ if (isinstance(attribute.ctx, ast.Store | ast.Del) or attribute.attr == "__dict__") and (
328
+ key := _reference_key(attribute.value, context, provenance, imports)
329
+ ) is not None:
330
+ _mark_unsafe_import(key, attribute, context, imports, unsafe)
331
+ for call in context.nodes(ast.Call):
332
+ if (
333
+ isinstance(call.func, ast.Attribute)
334
+ and call.func.attr in {"__setattr__", "__delattr__"}
335
+ and (key := _reference_key(call.func.value, context, provenance, imports)) is not None
336
+ ):
337
+ _mark_unsafe_import(key, call, context, imports, unsafe)
338
+ if (
339
+ call.args
340
+ and _builtin_mutation(call, context, provenance, imports)
341
+ and (key := _reference_key(call.args[0], context, provenance, imports)) is not None
342
+ ):
343
+ _mark_unsafe_import(key, call, context, imports, unsafe)
344
+ return unsafe
345
+
346
+
347
+ def _rebound_imports(
348
+ context: PythonFileContext,
349
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
350
+ ) -> set[str]:
351
+ uncertain_names = _nonlocal_names(context)
352
+ return {
353
+ _import_key(target.module, target.symbol)
354
+ for index in imports.values()
355
+ for name, target in index.bindings.items()
356
+ if name in uncertain_names
357
+ }
358
+
359
+
360
+ def _nonlocal_names(context: PythonFileContext) -> set[str]:
361
+ return {
362
+ name for statement in (*context.nodes(ast.Global), *context.nodes(ast.Nonlocal)) for name in statement.names
363
+ }
364
+
365
+
366
+ def _import_escapes(
367
+ read: ast.expr,
368
+ key: str,
369
+ context: PythonFileContext,
370
+ provenance: ResourceProvenance,
371
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
372
+ ) -> bool:
373
+ parent = context.parents.get(read)
374
+ if key == "pydantic.TypeAdapter":
375
+ return not (
376
+ isinstance(parent, ast.Attribute)
377
+ or (isinstance(parent, ast.Subscript) and parent.value is read)
378
+ or (isinstance(parent, ast.Call) and parent.func is read)
379
+ or (isinstance(parent, ast.AnnAssign) and parent.annotation is read)
380
+ )
381
+ if key not in _MODULES or isinstance(parent, ast.Attribute | ast.ExceptHandler):
382
+ return False
383
+ return not (
384
+ isinstance(parent, ast.Call)
385
+ and (parent.func is read or _builtin_mutation(parent, context, provenance, imports))
386
+ )
387
+
388
+
389
+ def _builtin_mutation(
390
+ call: ast.Call,
391
+ context: PythonFileContext,
392
+ provenance: ResourceProvenance,
393
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
394
+ ) -> bool:
395
+ return (
396
+ isinstance(call.func, ast.Name)
397
+ and call.func.id in {"setattr", "delattr"}
398
+ and context.imports.builtin_is_unshadowed(call.func.id)
399
+ and _lexical_binding(call.func.id, call.func, provenance) is None
400
+ ) or _reference_key(call.func, context, provenance, imports) in {
401
+ "builtins.setattr",
402
+ "builtins.delattr",
403
+ }
404
+
405
+
406
+ def _mark_unsafe_import(
407
+ key: str,
408
+ at: ast.AST,
409
+ context: PythonFileContext,
410
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
411
+ unsafe: set[str],
412
+ ) -> None:
413
+ unsafe.add(key)
414
+ if key not in _MODULES:
415
+ return
416
+ for statement, index in imports.items():
417
+ if isinstance(context.parents.get(statement), ast.Module) and (statement.lineno, statement.col_offset) < (
418
+ getattr(at, "lineno", 0),
419
+ getattr(at, "col_offset", 0),
420
+ ):
421
+ continue
422
+ unsafe.update(
423
+ _import_key(target.module, target.symbol)
424
+ for target in index.bindings.values()
425
+ if target.symbol is not None and target.module == key
426
+ )
427
+
428
+
429
+ def _inside_assertion(call: ast.Call, context: PythonFileContext) -> bool:
430
+ child: ast.AST = call
431
+ parent = context.parents.get(call)
432
+ while parent is not None:
433
+ if isinstance(parent, ast.Assert):
434
+ return child is parent.test
435
+ if isinstance(parent, (ast.Lambda, ast.GeneratorExp, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
436
+ return False
437
+ child = parent
438
+ parent = context.parents.get(parent)
439
+ return False
440
+
441
+
442
+ def _expects_validation_error(
443
+ call: ast.Call,
444
+ context: PythonFileContext,
445
+ provenance: ResourceProvenance,
446
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
447
+ unsafe_imports: set[str],
448
+ ) -> bool:
449
+ parent = context.parents.get(call)
450
+ while parent is not None and not isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef):
451
+ if isinstance(parent, ast.With | ast.AsyncWith) and any(
452
+ _raises_validation_error(item.context_expr, context, provenance, imports, unsafe_imports)
453
+ for item in parent.items
454
+ ):
455
+ return True
456
+ parent = context.parents.get(parent)
457
+ return False
458
+
459
+
460
+ def _raises_validation_error(
461
+ value: ast.expr,
462
+ context: PythonFileContext,
463
+ provenance: ResourceProvenance,
464
+ imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
465
+ unsafe_imports: set[str],
466
+ ) -> bool:
467
+ if (
468
+ not isinstance(value, ast.Call)
469
+ or len(value.args) > 1
470
+ or not _resolves(value.func, context, provenance, imports, unsafe_imports, sources=_PYTEST, symbol="raises")
471
+ ):
472
+ return False
473
+ expected = (
474
+ value.args[0]
475
+ if value.args
476
+ else next((keyword.value for keyword in value.keywords if keyword.arg == "expected_exception"), None)
477
+ )
478
+ if expected is None:
479
+ return False
480
+ types = expected.elts if isinstance(expected, ast.Tuple) else (expected,)
481
+ return any(
482
+ _resolves(
483
+ item, context, provenance, imports, unsafe_imports, sources=_VALIDATION_ERROR, symbol="ValidationError"
484
+ )
485
+ for item in types
486
+ )