sarj-python-lint 1.58.0__tar.gz → 1.60.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_store_writes.py +140 -23
  4. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +1 -1
  5. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +26 -2
  6. sarj_python_lint-1.60.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +475 -0
  7. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +81 -6
  8. sarj_python_lint-1.58.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -240
  9. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/.gitignore +0 -0
  10. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/LICENSE +0 -0
  11. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/README.md +0 -0
  12. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  15. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_file_context.py +0 -0
  16. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_filesystem.py +0 -0
  17. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_python_target.py +0 -0
  18. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  19. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_secret_names.py +0 -0
  20. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_source.py +0 -0
  21. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_version.py +0 -0
  22. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/benchmark.py +0 -0
  23. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  24. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/json_boundary.py +0 -0
  25. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/py.typed +0 -0
  26. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/ratchet.py +0 -0
  27. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rule_base.py +0 -0
  28. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  29. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  30. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  31. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  32. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  33. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  34. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  35. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  36. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  37. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  38. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  39. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  40. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  41. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  42. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  43. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  44. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  45. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  46. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  47. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  48. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  49. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  50. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  51. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  52. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  53. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  54. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  55. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  56. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  57. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  58. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  59. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  60. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  61. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  62. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  63. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  64. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  65. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  66. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  67. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  68. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  69. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  70. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  71. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  72. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  73. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  74. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  75. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  76. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  77. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  78. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  79. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  80. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  81. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  82. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  83. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  84. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  85. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  86. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  87. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  88. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  89. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  90. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  91. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  92. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  93. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  94. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  95. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  96. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  97. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  98. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  99. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  100. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  101. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  102. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  103. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  104. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  105. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  106. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  107. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  108. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  109. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  110. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  111. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  112. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  113. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  114. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  115. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  116. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  117. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  118. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  119. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  120. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  121. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  122. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  123. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  124. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  125. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  126. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  127. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  128. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  129. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  130. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  131. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  132. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  133. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  134. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  135. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  136. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  137. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  138. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  139. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  140. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  141. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  142. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  143. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  144. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  145. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  146. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  147. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  148. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  149. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  150. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  151. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  152. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  153. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  154. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  155. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  156. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  157. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  158. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  159. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  160. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  161. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  162. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  163. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  164. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  165. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  166. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  167. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  168. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  169. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  170. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  171. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  172. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  173. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  174. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  175. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  176. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  177. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  178. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  179. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  180. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  181. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  182. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  183. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  184. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  185. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  186. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  187. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  188. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  189. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  190. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  191. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  192. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  193. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  194. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  195. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  196. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  197. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  198. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  199. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  200. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  201. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  202. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  203. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  204. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  205. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  206. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  207. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  208. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  209. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  210. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  211. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  212. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  213. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  214. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  215. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  216. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  217. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  218. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  219. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  220. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  221. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  222. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  223. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  224. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  225. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  226. {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.58.0
3
+ Version: 1.60.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.58.0"
3
+ version = "1.60.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -8,7 +8,7 @@ import sqlglot
8
8
  from sqlglot import exp
9
9
  from sqlglot.errors import SqlglotError
10
10
 
11
- from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
11
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
12
12
  from sarj_python_lint.rules._ast_index import walk
13
13
  from sarj_python_lint.rules._paths import is_generated
14
14
  from sarj_python_lint.rules._project_index import ProjectIndexSet
@@ -55,10 +55,20 @@ def methods(owner: ast.ClassDef) -> dict[str, Function]:
55
55
  if node.name in result:
56
56
  ambiguous.add(node.name)
57
57
  result[node.name] = node
58
+ else:
59
+ ambiguous.update(scope_bound_names([node]))
60
+ ambiguous.update(
61
+ node.attr
62
+ for method in result.values()
63
+ for node in own_nodes(method)
64
+ if isinstance(node, ast.Attribute) and _writes_self_attribute(node, node.attr)
65
+ )
58
66
  return {name: method for name, method in result.items() if name not in ambiguous}
59
67
 
60
68
 
61
69
  def upsert_pair(owner: ast.ClassDef, imports: ImportIndex) -> tuple[Function, Function] | None:
70
+ if owner.keywords:
71
+ return None
62
72
  declared = methods(owner)
63
73
  single, bulk = declared.get("upsert"), declared.get("upsert_many")
64
74
  if (
@@ -101,6 +111,7 @@ def collection_item(annotation: ast.expr | None, imports: ImportIndex) -> ast.ex
101
111
 
102
112
 
103
113
  def same_type(left: ast.expr | None, right: ast.expr | None) -> bool:
114
+ left, right = AnnotationSemantics.parse(left), AnnotationSemantics.parse(right)
104
115
  return (
105
116
  left is not None
106
117
  and right is not None
@@ -129,6 +140,10 @@ def discarded_call(loop: ast.For) -> tuple[ast.Call, bool] | None:
129
140
  and not any(isinstance(item, ast.Starred) for item in loop.iter.elts)
130
141
  ):
131
142
  return None
143
+ if isinstance(loop.iter, ast.Dict) and len(loop.iter.keys) <= 1 and None not in loop.iter.keys:
144
+ return None
145
+ if isinstance(loop.iter, ast.Constant) and isinstance(loop.iter.value, str | bytes) and len(loop.iter.value) <= 1:
146
+ return None
132
147
  value = loop.body[0].value
133
148
  awaited = isinstance(value, ast.Await)
134
149
  value = value.value if awaited else value
@@ -136,20 +151,53 @@ def discarded_call(loop: ast.For) -> tuple[ast.Call, bool] | None:
136
151
 
137
152
 
138
153
  def depends_on_target(expression: ast.expr, target: ast.expr) -> bool:
139
- names = {node.id for node in walk(target) if isinstance(node, ast.Name)}
140
- return any(isinstance(node, ast.Name) and node.id in names for node in own_nodes(expression))
154
+ names = _target_names(target)
155
+ return any(name in names for name in _free_names(expression))
156
+
157
+
158
+ def _target_names(target: ast.expr) -> frozenset[str]:
159
+ return frozenset(node.id for node in walk(target) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store))
160
+
161
+
162
+ def _free_names(expression: ast.expr) -> Iterator[str]:
163
+ pending: list[tuple[ast.AST, frozenset[str]]] = [(expression, frozenset())]
164
+ while pending:
165
+ node, shadowed = pending.pop()
166
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id not in shadowed:
167
+ yield node.id
168
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
169
+ continue
170
+ if isinstance(node, ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp):
171
+ pending.extend(_comprehension_scopes(node, shadowed))
172
+ continue
173
+ pending.extend((child, shadowed) for child in ast.iter_child_nodes(node))
174
+
175
+
176
+ def _comprehension_scopes(
177
+ node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp, shadowed: frozenset[str]
178
+ ) -> Iterator[tuple[ast.AST, frozenset[str]]]:
179
+ for generator in node.generators:
180
+ yield generator.iter, shadowed
181
+ shadowed |= _target_names(generator.target)
182
+ yield from ((condition, shadowed) for condition in generator.ifs)
183
+ values = (node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,)
184
+ yield from ((value, shadowed) for value in values)
141
185
 
142
186
 
143
187
  class StoreOwners:
144
188
  def __init__(self, context: PythonFileContext) -> None:
145
189
  self.context = context
146
- self.provenance = ResourceProvenance(context)
190
+ self.provenance = ResourceProvenance(context, require_static_namespace=True)
147
191
  self.project = context.session.project
148
192
 
149
193
  def receiver(self, expression: ast.expr, at: ast.AST) -> tuple[ast.ClassDef, ImportIndex] | None:
150
194
  if isinstance(expression, ast.Name):
151
195
  binding = self.provenance.binding(expression.id, at)
152
- if isinstance(binding, ast.arg) and binding.annotation is not None:
196
+ if (
197
+ isinstance(binding, ast.arg)
198
+ and binding.annotation is not None
199
+ and not self._receiver_method_replaced(expression, binding)
200
+ ):
153
201
  return self.annotation(binding.annotation)
154
202
  return None
155
203
  if (
@@ -158,39 +206,57 @@ class StoreOwners:
158
206
  or expression.value.id != "self"
159
207
  ):
160
208
  return None
161
- if not isinstance(self.provenance.binding("self", at), ast.arg):
209
+ binding = self.provenance.binding("self", at)
210
+ if not isinstance(binding, ast.arg):
211
+ return None
212
+ arguments = self.context.parents.get(binding)
213
+ method = self.context.parents.get(arguments) if arguments is not None else None
214
+ if (
215
+ not isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef)
216
+ or not isinstance(self.context.parents.get(method), ast.ClassDef)
217
+ or not (positional := [*method.args.posonlyargs, *method.args.args])
218
+ or positional[0] is not binding
219
+ or any(
220
+ self.context.module_imports.resolved_qualified_name(decorator) not in _NEUTRAL_DECORATORS
221
+ for decorator in method.decorator_list
222
+ )
223
+ ):
162
224
  return None
163
225
  annotation = self._injected_annotation(expression.attr, at)
164
226
  return self.annotation(annotation) if annotation is not None else None
165
227
 
228
+ def _receiver_method_replaced(self, receiver: ast.Name, binding: ast.arg) -> bool:
229
+ for node in self.context.nodes(ast.Attribute, ast.Call):
230
+ target = _replaced_receiver(node, self.context.imports)
231
+ if (
232
+ isinstance(target, ast.Name)
233
+ and target.id == receiver.id
234
+ and self.provenance.binding(receiver.id, target) is binding
235
+ ):
236
+ return True
237
+ return False
238
+
166
239
  def _injected_annotation(self, attribute: str, at: ast.AST) -> ast.expr | None:
167
240
  parent = self.context.parents.get(at)
168
241
  while parent is not None and not isinstance(parent, ast.ClassDef):
169
242
  parent = self.context.parents.get(parent)
170
243
  if parent is None:
171
244
  return None
172
- constructor = methods(parent).get("__init__")
245
+ constructor = _injection_constructor(parent, attribute)
173
246
  if constructor is None:
174
247
  return None
248
+ declared_methods = [node for node in parent.body if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)]
175
249
  assignments = [
176
- node
177
- for method in methods(parent).values()
178
- for node in own_nodes(method)
179
- if _writes_self_attribute(node, attribute)
250
+ node for method in declared_methods for node in own_nodes(method) if _writes_self_attribute(node, attribute)
180
251
  ]
181
- if len(assignments) != 1:
252
+ if len(assignments) != 1 or _injected_receiver_replaced(declared_methods, attribute, self.context.imports):
182
253
  return None
183
- for statement in constructor.body:
184
- if (
185
- not isinstance(statement, ast.Assign)
186
- or not isinstance(statement.value, ast.Name)
187
- or assignments[0] not in statement.targets
188
- ):
189
- continue
190
- return _injected_parameter(constructor, statement.value, self.provenance)
191
- return None
254
+ value = _constructor_injected_value(constructor, assignments[0])
255
+ return _injected_parameter(constructor, value, self.provenance) if value is not None else None
192
256
 
193
257
  def annotation(self, expression: ast.expr | None) -> tuple[ast.ClassDef, ImportIndex] | None:
258
+ if self.provenance.dynamic_namespace or self.provenance.wildcard:
259
+ return None
194
260
  original = expression
195
261
  expression = AnnotationSemantics(self.context.module_imports, MappingProxyType({})).parse(expression)
196
262
  if expression is None or self.context.tree is None:
@@ -224,8 +290,13 @@ class StoreOwners:
224
290
  from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer to avoid the rule registry's context import cycle.
225
291
 
226
292
  definition = PythonFileContext(owner.path, owner.source, self.context.session)
227
- provenance = ResourceProvenance(definition)
228
- if provenance.binding(symbol.name, declaration) is not declaration or symbol.name in provenance.mutated:
293
+ provenance = ResourceProvenance(definition, require_static_namespace=True)
294
+ if (
295
+ provenance.dynamic_namespace
296
+ or provenance.wildcard
297
+ or provenance.binding(symbol.name, declaration) is not declaration
298
+ or symbol.name in provenance.mutated
299
+ ):
229
300
  return None
230
301
  return declaration, definition.module_imports
231
302
 
@@ -240,6 +311,52 @@ def _writes_self_attribute(node: ast.AST, attribute: str) -> bool:
240
311
  )
241
312
 
242
313
 
314
+ def _injection_constructor(owner: ast.ClassDef, attribute: str) -> Function | None:
315
+ runtime_statements = [
316
+ statement for statement in owner.body if not (isinstance(statement, ast.AnnAssign) and statement.value is None)
317
+ ]
318
+ return None if attribute in scope_bound_names(runtime_statements) else methods(owner).get("__init__")
319
+
320
+
321
+ def _injected_receiver_replaced(declared: list[Function], attribute: str, imports: ImportIndex) -> bool:
322
+ return any(
323
+ isinstance(target := _replaced_receiver(node, imports), ast.Attribute)
324
+ and target.attr == attribute
325
+ and isinstance(target.value, ast.Name)
326
+ and target.value.id == "self"
327
+ for method in declared
328
+ for node in own_nodes(method)
329
+ )
330
+
331
+
332
+ def _constructor_injected_value(constructor: Function, assignment: ast.AST) -> ast.Name | None:
333
+ for statement in constructor.body:
334
+ match statement:
335
+ case ast.Assign(value=ast.Name() as value, targets=targets) if assignment in targets:
336
+ return value
337
+ case ast.AnnAssign(value=ast.Name() as value, target=target) if target is assignment:
338
+ return value
339
+ case _:
340
+ pass
341
+ return None
342
+
343
+
344
+ def _replaced_receiver(node: ast.AST, imports: ImportIndex) -> ast.expr | None:
345
+ members = {"upsert", "upsert_many", "__class__"}
346
+ if isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store | ast.Del) and node.attr in members:
347
+ return node.value
348
+ if not isinstance(node, ast.Call) or node.keywords:
349
+ return None
350
+ name = imports.resolved_qualified_name(node.func)
351
+ if isinstance(node.func, ast.Name) and imports.builtin_is_unshadowed(node.func.id):
352
+ name = "builtins." + node.func.id
353
+ required = {"builtins.setattr": 3, "builtins.delattr": 2}.get(name or "")
354
+ if required is None or len(node.args) != required:
355
+ return None
356
+ member = node.args[1]
357
+ return node.args[0] if isinstance(member, ast.Constant) and member.value in members else None
358
+
359
+
243
360
  def _injected_parameter(constructor: Function, value: ast.Name, provenance: ResourceProvenance) -> ast.expr | None:
244
361
  parameter = next(
245
362
  (
@@ -45,7 +45,7 @@ class PreferBulkUpsert(ProjectRule):
45
45
  autofix=AutofixPolicy.NONE,
46
46
  limitations=(
47
47
  "Only a synchronous for loop containing one discarded upsert call is considered. Its receiver must resolve to a typed parameter or a constructor-injected attribute with source-declared compatible upsert/upsert_many methods.",
48
- "Signatures must have one required command argument and matching scalar/collection input and output types. Annotated first-party imports use the existing project index; unresolved, inherited, overloaded, decorated, rebound and dynamic contracts are excluded.",
48
+ "Signatures must have one required command argument and matching scalar/collection input and output types, including equivalent quoted annotations. Annotated first-party imports use the existing project index; unresolved, inherited, overloaded, decorated, rebound, reassigned and dynamic contracts are excluded, as are explicit metaclasses and class keywords.",
49
49
  "Result-dependent loops, comprehensions, async streams, loop else clauses, extra statements and per-row exception or transaction blocks are excluded. A batch can change duplicate-key and failure semantics, so this remains an advisory without an autofix.",
50
50
  "Maintained tests are checked; generated/vendor sources are excluded. A bulk method is not invented when none is declared.",
51
51
  ),
@@ -47,6 +47,7 @@ class PreferPsycopgExecutemany(Rule):
47
47
  "RETURNING, queries, dynamic SQL, extra execute options, async streams, loop else clauses, additional statements and per-row exception or transaction contexts are excluded. Connection.execute is excluded because connections do not expose the same executemany API.",
48
48
  "No automatic batching or performance claim is made for a particular workload. Advisory severity preserves deliberately sequential database effects; exact suppressions document those contracts.",
49
49
  "Generated/vendor sources are excluded; maintained tests are checked. Stable aliases and cursors acquired through context managers are intentionally outside the initial proof boundary.",
50
+ "Cursor state reads, iteration and opaque cursor escapes are excluded: executemany accumulates rowcount and can change observable execution interleaving.",
50
51
  ),
51
52
  examples=(
52
53
  RuleExample(
@@ -80,7 +81,7 @@ class PreferPsycopgExecutemany(Rule):
80
81
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
81
82
  if context.generated or "execute" not in context.symbol_source or context.tree is None:
82
83
  return []
83
- provenance = ResourceProvenance(context)
84
+ provenance = ResourceProvenance(context, require_static_namespace=True)
84
85
  findings: list[Diagnostic] = []
85
86
  for loop in context.nodes(ast.For):
86
87
  candidate = discarded_call(loop)
@@ -100,6 +101,8 @@ class PreferPsycopgExecutemany(Rule):
100
101
  qualified == "psycopg.AsyncCursor"
101
102
  ):
102
103
  continue
104
+ if not _cursor_uses_are_batch_safe(call.func.value, call, context, provenance):
105
+ continue
103
106
  query = sql_query(call.args[0], context.imports)
104
107
  if (
105
108
  not isinstance(query, exp.Insert | exp.Update | exp.Delete)
@@ -124,11 +127,32 @@ class PreferPsycopgExecutemany(Rule):
124
127
  return findings
125
128
 
126
129
 
130
+ def _cursor_uses_are_batch_safe(
131
+ receiver: ast.Name, call: ast.Call, context: PythonFileContext, provenance: ResourceProvenance
132
+ ) -> bool:
133
+ binding = provenance.binding(receiver.id, call)
134
+ for read in provenance.reads.get(receiver.id, ()):
135
+ if provenance.binding(receiver.id, read) is not binding:
136
+ continue
137
+ attribute = context.parents.get(read)
138
+ if not isinstance(attribute, ast.Attribute):
139
+ return False
140
+ invocation = context.parents.get(attribute)
141
+ if (
142
+ attribute.value is not read
143
+ or attribute.attr not in {"execute", "executemany", "close"}
144
+ or not isinstance(invocation, ast.Call)
145
+ or invocation.func is not attribute
146
+ ):
147
+ return False
148
+ return True
149
+
150
+
127
151
  def _cursor_contract(
128
152
  receiver: ast.Name, call: ast.Call, context: PythonFileContext, provenance: ResourceProvenance
129
153
  ) -> str | None:
130
154
  binding = provenance.binding(receiver.id, call)
131
- if not isinstance(binding, ast.arg) or context.tree is None or provenance.dynamic_namespace:
155
+ if not isinstance(binding, ast.arg) or context.tree is None or provenance.dynamic_namespace or provenance.wildcard:
132
156
  return None
133
157
  annotation = AnnotationSemantics.from_tree(context.tree).parse(binding.annotation)
134
158
  if isinstance(annotation, ast.Subscript):