sarj-python-lint 1.47.0__tar.gz → 1.49.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 (222) hide show
  1. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_registry.py +8 -0
  4. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_resource_provenance.py +126 -0
  5. sarj_python_lint-1.49.0/src/sarj_python_lint/rules/_runtime_bindings.py +113 -0
  6. sarj_python_lint-1.49.0/src/sarj_python_lint/rules/_store_writes.py +319 -0
  7. sarj_python_lint-1.49.0/src/sarj_python_lint/rules/no_discarded_validation_result.py +453 -0
  8. sarj_python_lint-1.49.0/src/sarj_python_lint/rules/prefer_bulk_upsert.py +139 -0
  9. sarj_python_lint-1.49.0/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +139 -0
  10. sarj_python_lint-1.49.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +240 -0
  11. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +13 -2
  12. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/.gitignore +0 -0
  13. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/LICENSE +0 -0
  14. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/README.md +0 -0
  15. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/__init__.py +0 -0
  16. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/__main__.py +0 -0
  17. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  18. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_file_context.py +0 -0
  19. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_filesystem.py +0 -0
  20. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_python_target.py +0 -0
  21. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  22. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_secret_names.py +0 -0
  23. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_source.py +0 -0
  24. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_version.py +0 -0
  25. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/benchmark.py +0 -0
  26. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  27. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/json_boundary.py +0 -0
  28. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/py.typed +0 -0
  29. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/ratchet.py +0 -0
  30. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rule_base.py +0 -0
  31. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  32. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  33. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  34. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  35. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  36. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  37. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  38. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  39. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  40. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  41. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  42. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  43. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  44. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  45. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  46. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  47. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  48. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  49. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  50. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  51. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  52. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  53. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  54. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  55. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  56. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  57. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  58. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  59. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  60. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  61. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  62. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  63. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  64. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  65. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  66. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  67. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  68. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  69. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  70. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  71. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  72. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  73. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  74. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  75. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  76. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  77. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  78. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  79. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  80. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  81. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  82. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  83. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  84. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  85. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  86. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  87. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  88. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  89. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  90. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  91. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  92. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  93. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  94. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  95. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  96. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  97. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  98. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  99. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  100. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  101. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  102. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  103. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  104. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  105. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  106. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  107. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  108. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  109. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  110. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  111. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  112. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  113. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  114. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  115. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  116. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  117. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  118. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  119. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  120. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  121. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  122. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  123. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  124. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  125. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  126. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  127. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  128. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  129. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  130. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  131. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  132. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  133. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  134. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  135. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  136. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  137. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  138. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  139. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  140. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  141. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  142. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  143. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  144. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  145. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  146. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  147. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  148. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  149. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  150. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  151. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  152. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  153. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  154. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  155. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  156. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  157. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  158. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  159. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  160. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  161. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  162. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  163. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  164. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  165. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  166. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  167. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  168. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  169. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  170. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  171. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  172. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  173. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  174. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  175. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  176. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  177. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  178. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  179. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  180. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  181. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  182. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  183. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  184. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  185. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  186. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  187. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  188. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  189. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  190. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  191. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  192. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  193. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  194. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  195. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  196. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  197. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  198. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  199. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  200. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  201. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  202. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  203. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  204. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  205. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  206. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  207. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  208. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  209. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  210. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  211. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  212. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  213. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  214. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  215. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  216. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  217. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  218. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  219. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  220. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  221. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  222. {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.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.47.0
3
+ Version: 1.49.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.47.0"
3
+ version = "1.49.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" }]
@@ -43,6 +43,7 @@ from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
43
43
  )
44
44
  from sarj_python_lint.rules.no_database_programmability import NoDatabaseProgrammability
45
45
  from sarj_python_lint.rules.no_delete_statement import NoDeleteStatement
46
+ from sarj_python_lint.rules.no_discarded_validation_result import NoDiscardedValidationResult
46
47
  from sarj_python_lint.rules.no_dunder_all import NoDunderAll
47
48
  from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
48
49
  from sarj_python_lint.rules.no_excessive_cognitive_complexity import NoExcessiveCognitiveComplexity
@@ -221,14 +222,20 @@ from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvali
221
222
  from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
222
223
  from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
223
224
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
225
+ from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
224
226
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
227
+ from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
225
228
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
226
229
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
230
+ from sarj_python_lint.rules.store_field_update_reuses_upsert import StoreFieldUpdateReusesUpsert
227
231
  from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
228
232
 
229
233
 
230
234
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
231
235
  {
236
+ PreferBulkUpsert.id: PreferBulkUpsert,
237
+ PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
238
+ StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
232
239
  NoExplicitTestOracle.id: NoExplicitTestOracle,
233
240
  PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
234
241
  NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
@@ -243,6 +250,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
243
250
  NoFixedSleepInLoop.id: NoFixedSleepInLoop,
244
251
  NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
245
252
  NoUnusedCopyResult.id: NoUnusedCopyResult,
253
+ NoDiscardedValidationResult.id: NoDiscardedValidationResult,
246
254
  NoBeforeValidatorInputMutation.id: NoBeforeValidatorInputMutation,
247
255
  NoInputModelMutation.id: NoInputModelMutation,
248
256
  NoDatabaseProgrammability.id: NoDatabaseProgrammability,
@@ -8,9 +8,12 @@ from typing import TYPE_CHECKING, final
8
8
 
9
9
  from sarj_python_lint.rules._ast_index import has_own_yield
10
10
  from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
11
+ from sarj_python_lint.rules._runtime_bindings import RuntimeBindings
11
12
 
12
13
 
13
14
  if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
14
17
  from sarj_python_lint._file_context import PythonFileContext
15
18
 
16
19
 
@@ -76,6 +79,7 @@ class ResourceProvenance:
76
79
  self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
77
80
  self.async_contract_cache: dict[ast.AsyncFunctionDef, bool] = {}
78
81
  self.import_contract_cache: dict[str, bool] = {}
82
+ self.runtime_import_cache: dict[ast.alias, bool] = {}
79
83
  self.wildcard = False
80
84
  self.mutated: set[str] = set()
81
85
  self.external_bindings: set[str] = set()
@@ -133,6 +137,113 @@ class ResourceProvenance:
133
137
  scope = self.scope(scope)
134
138
  return None
135
139
 
140
+ @cached_property
141
+ def runtime_bindings(self) -> RuntimeBindings:
142
+ return RuntimeBindings(self.context, self.bindings)
143
+
144
+ def runtime_binding(self, name: str, at: ast.AST) -> ast.AST | None:
145
+ nodes = self.runtime_bindings.lookup(self.scope(at), name)
146
+ if not nodes:
147
+ return None
148
+ return nodes[0] if len(nodes) == 1 or self._same_import_bindings(list(nodes)) else None
149
+
150
+ def runtime_import_name(self, node: ast.expr) -> str | None:
151
+ root = _root_name(node)
152
+ if self.wildcard or self.dynamic_namespace or root is None or root in self.mutated:
153
+ return None
154
+ binding = self.runtime_binding(root, node)
155
+ if not isinstance(binding, ast.alias):
156
+ return None
157
+ qualified = _runtime_import_name(node, binding, self.context.parents.get(binding))
158
+ if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
159
+ return None
160
+ if binding not in self.runtime_import_cache:
161
+ self.runtime_import_cache[binding] = all(
162
+ self.runtime_binding(root, read) is not binding or self._safe_import_use(read)
163
+ for read in self.reads.get(root, ())
164
+ )
165
+ return qualified if self.runtime_import_cache[binding] else None
166
+
167
+ @cached_property
168
+ def mock_namespace_patching(self) -> bool:
169
+ return any(
170
+ self.runtime_import_name(node.func)
171
+ in {
172
+ "unittest.mock.patch",
173
+ "unittest.mock.patch.object",
174
+ "unittest.mock.patch.multiple",
175
+ "unittest.mock.patch.dict",
176
+ }
177
+ for node in self.context.nodes(ast.Call)
178
+ )
179
+
180
+ @cached_property
181
+ def imported_namespace_hazards(self) -> frozenset[Path]:
182
+ hazards: set[Path] = set()
183
+ for node in self.context.nodes(ast.Name, ast.Attribute):
184
+ match node:
185
+ case ast.Attribute(value=value, ctx=ast.Store() | ast.Del()):
186
+ hazards.update(self._namespace_paths(value, include_root=True))
187
+ case ast.Name(ctx=ast.Load()) | ast.Attribute(ctx=ast.Load()) if not self._safe_import_use(node):
188
+ hazards.update(self._namespace_paths(node, include_root=False))
189
+ case _:
190
+ pass
191
+ return frozenset(hazards)
192
+
193
+ @cached_property
194
+ def _namespace_aliases(self) -> dict[str, tuple[ast.alias, ...]]:
195
+ aliases: dict[str, list[ast.alias]] = defaultdict(list)
196
+ for binding in self.context.nodes(ast.alias):
197
+ parent = self.context.parents.get(binding)
198
+ name = binding.asname or (
199
+ binding.name.partition(".")[0] if isinstance(parent, ast.Import) else binding.name
200
+ )
201
+ aliases[name].append(binding)
202
+ return {name: tuple(bindings) for name, bindings in aliases.items()}
203
+
204
+ def _namespace_paths(self, expression: ast.expr, *, include_root: bool) -> set[Path]:
205
+ attributes: list[str] = []
206
+ root = expression
207
+ while isinstance(root, ast.Attribute):
208
+ attributes.append(root.attr)
209
+ root = root.value
210
+ if not isinstance(root, ast.Name):
211
+ return set()
212
+ binding = self.runtime_binding(root.id, expression)
213
+ aliases: tuple[ast.alias, ...]
214
+ match binding:
215
+ case ast.alias():
216
+ aliases = (binding,)
217
+ case None:
218
+ aliases = self._namespace_aliases.get(root.id, ())
219
+ case _:
220
+ return set()
221
+ paths: set[Path] = set()
222
+ for alias in aliases:
223
+ path = self._namespace_module_path(alias, tuple(reversed(attributes)))
224
+ if path is not None:
225
+ paths.add(path)
226
+ if include_root and attributes:
227
+ base = self._namespace_module_path(alias, ())
228
+ if base is not None:
229
+ paths.add(base)
230
+ return paths
231
+
232
+ def _namespace_module_path(self, binding: ast.alias, attributes: tuple[str, ...]) -> Path | None:
233
+ parent = self.context.parents.get(binding)
234
+ match parent:
235
+ case ast.Import():
236
+ prefix = binding.name if binding.asname else binding.name.partition(".")[0]
237
+ level = 0
238
+ case ast.ImportFrom(module=module, level=level):
239
+ prefix = ".".join(part for part in (module, binding.name) if part)
240
+ case _:
241
+ return None
242
+ module = ".".join((prefix, *attributes))
243
+ return self.context.session.local_source.resolve_module(
244
+ self.context.path, module, level, facts=self.context.session.first_party
245
+ )
246
+
136
247
  def _same_import_bindings(self, nodes: list[ast.AST]) -> bool:
137
248
  targets: set[tuple[str, str | None, int, str]] = set()
138
249
  for node in nodes:
@@ -417,6 +528,21 @@ def _root_name(node: ast.expr) -> str | None:
417
528
  return node.id if isinstance(node, ast.Name) else None
418
529
 
419
530
 
531
+ def _runtime_import_name(expression: ast.expr, binding: ast.alias, parent: ast.AST | None) -> str | None:
532
+ attributes: list[str] = []
533
+ while isinstance(expression, ast.Attribute):
534
+ attributes.append(expression.attr)
535
+ expression = expression.value
536
+ match parent:
537
+ case ast.Import():
538
+ prefix = binding.name if binding.asname else binding.name.partition(".")[0]
539
+ case ast.ImportFrom(module=str(module), level=0):
540
+ prefix = f"{module}.{binding.name}"
541
+ case _:
542
+ return None
543
+ return ".".join((prefix, *reversed(attributes)))
544
+
545
+
420
546
  def _bound_names(node: ast.AST) -> tuple[str, ...]:
421
547
  match node:
422
548
  case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import defaultdict
5
+ import symtable
6
+ from typing import TYPE_CHECKING, final
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Mapping, Sequence
11
+
12
+ from sarj_python_lint._file_context import PythonFileContext
13
+
14
+
15
+ @final
16
+ class RuntimeBindings:
17
+ def __init__(
18
+ self, context: PythonFileContext, bindings: Mapping[ast.AST | None, Mapping[str, Sequence[ast.AST]]]
19
+ ) -> None:
20
+ self.bindings = bindings
21
+ self.tables: dict[int, symtable.SymbolTable] = {}
22
+ self.parents: dict[int, int | None] = {}
23
+ self.by_scope: dict[ast.AST, int] = {}
24
+ self.scopes: dict[int, ast.AST] = {}
25
+ self.mutable: set[tuple[int, str]] = set()
26
+ self.module: int | None = None
27
+ tree = context.tree
28
+ if tree is None:
29
+ return
30
+ try:
31
+ root = symtable.symtable(context.source, str(context.path), "exec")
32
+ except SyntaxError, UnicodeError, RecursionError:
33
+ return
34
+ self.module = root.get_id()
35
+ self._collect(root)
36
+ self._map_scopes(context, tree, root)
37
+ self._index_writes()
38
+
39
+ def lookup(self, scope: ast.AST | None, name: str) -> Sequence[ast.AST] | None:
40
+ if scope is None or (table_id := self.by_scope.get(scope)) is None:
41
+ return None
42
+ symbol = _symbol(self.tables[table_id], name)
43
+ if symbol is None or (owner := self._owner(table_id, symbol)) is None or (owner, name) in self.mutable:
44
+ return None
45
+ owner_scope = self.scopes.get(owner)
46
+ return self.bindings.get(owner_scope, {}).get(name) if owner_scope is not None else None
47
+
48
+ def _collect(self, root: symtable.SymbolTable) -> None:
49
+ pending: list[tuple[symtable.SymbolTable, int | None]] = [(root, None)]
50
+ while pending:
51
+ table, parent = pending.pop()
52
+ identity = table.get_id()
53
+ self.tables[identity] = table
54
+ self.parents[identity] = parent
55
+ pending.extend((child, identity) for child in table.get_children())
56
+
57
+ def _map_scopes(self, context: PythonFileContext, tree: ast.Module, root: symtable.SymbolTable) -> None:
58
+ candidates: dict[tuple[symtable.SymbolTableType, str, int], list[int]] = defaultdict(list)
59
+ for identity, table in self.tables.items():
60
+ candidates[table.get_type(), table.get_name(), table.get_lineno()].append(identity)
61
+ self.by_scope[tree] = root.get_id()
62
+ self.scopes[root.get_id()] = tree
63
+ for node in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef):
64
+ kind = (
65
+ symtable.SymbolTableType.CLASS if isinstance(node, ast.ClassDef) else symtable.SymbolTableType.FUNCTION
66
+ )
67
+ matches = candidates.get((kind, node.name, node.lineno), ())
68
+ if len(matches) == 1:
69
+ [identity] = matches
70
+ self.by_scope[node] = identity
71
+ self.scopes[identity] = node
72
+
73
+ def _index_writes(self) -> None:
74
+ for identity, table in self.tables.items():
75
+ if identity == self.module:
76
+ continue
77
+ for symbol in table.get_symbols():
78
+ if not (symbol.is_global() or symbol.is_nonlocal()):
79
+ continue
80
+ if not (symbol.is_assigned() or symbol.is_imported() or symbol.is_namespace()):
81
+ continue
82
+ owner = self._owner(identity, symbol)
83
+ if owner is not None:
84
+ self.mutable.add((owner, symbol.get_name()))
85
+
86
+ def _owner(self, identity: int, symbol: symtable.Symbol) -> int | None:
87
+ if symbol.is_global():
88
+ return self.module
89
+ if symbol.is_local():
90
+ return identity
91
+ if not symbol.is_free():
92
+ return None
93
+ parent = self.parents.get(identity)
94
+ while parent is not None:
95
+ table = self.tables[parent]
96
+ if table.get_type() is symtable.SymbolTableType.MODULE:
97
+ break
98
+ if table.get_type() is symtable.SymbolTableType.CLASS and symbol.get_name() == "__class__":
99
+ # The compiler binds this cell to the lexically defined class.
100
+ return None
101
+ if table.get_type() is not symtable.SymbolTableType.CLASS:
102
+ ancestor = _symbol(table, symbol.get_name())
103
+ if ancestor is not None and ancestor.is_local():
104
+ return parent
105
+ parent = self.parents.get(parent)
106
+ return None
107
+
108
+
109
+ def _symbol(table: symtable.SymbolTable, name: str) -> symtable.Symbol | None:
110
+ try:
111
+ return table.lookup(name)
112
+ except KeyError:
113
+ return None
@@ -0,0 +1,319 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from types import MappingProxyType
5
+ from typing import TYPE_CHECKING
6
+
7
+ import sqlglot
8
+ from sqlglot import exp
9
+ from sqlglot.errors import SqlglotError
10
+
11
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
12
+ from sarj_python_lint.rules._ast_index import walk
13
+ from sarj_python_lint.rules._paths import is_generated
14
+ from sarj_python_lint.rules._project_index import ProjectIndexSet
15
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Iterator
20
+
21
+ from sarj_python_lint._file_context import PythonFileContext
22
+ from sarj_python_lint.rules._imports import ImportIndex
23
+
24
+
25
+ type Function = ast.FunctionDef | ast.AsyncFunctionDef
26
+ _COLLECTIONS = frozenset(
27
+ {
28
+ "collections.abc.Collection",
29
+ "collections.abc.Iterable",
30
+ "collections.abc.Sequence",
31
+ "typing.Collection",
32
+ "typing.Iterable",
33
+ "typing.Sequence",
34
+ "typing.List",
35
+ }
36
+ )
37
+ _NEUTRAL_DECORATORS = frozenset({"abc.abstractmethod", "typing.override", "typing_extensions.override"})
38
+
39
+
40
+ def method_parameters(method: Function) -> list[ast.arg] | None:
41
+ args = method.args
42
+ positional = [*args.posonlyargs, *args.args]
43
+ if not positional or positional[0].arg != "self":
44
+ return None
45
+ if args.vararg or args.kwarg or args.defaults or any(args.kw_defaults):
46
+ return None
47
+ return [*positional[1:], *args.kwonlyargs]
48
+
49
+
50
+ def methods(owner: ast.ClassDef) -> dict[str, Function]:
51
+ result: dict[str, Function] = {}
52
+ ambiguous: set[str] = set()
53
+ for node in owner.body:
54
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
55
+ if node.name in result:
56
+ ambiguous.add(node.name)
57
+ result[node.name] = node
58
+ return {name: method for name, method in result.items() if name not in ambiguous}
59
+
60
+
61
+ def upsert_pair(owner: ast.ClassDef, imports: ImportIndex) -> tuple[Function, Function] | None:
62
+ declared = methods(owner)
63
+ single, bulk = declared.get("upsert"), declared.get("upsert_many")
64
+ if (
65
+ single is None
66
+ or bulk is None
67
+ or isinstance(single, ast.AsyncFunctionDef) != isinstance(bulk, ast.AsyncFunctionDef)
68
+ ):
69
+ return None
70
+ if any(
71
+ imports.resolved_qualified_name(decorator) not in _NEUTRAL_DECORATORS
72
+ for method in (single, bulk)
73
+ for decorator in method.decorator_list
74
+ ):
75
+ return None
76
+ one, many = method_parameters(single), method_parameters(bulk)
77
+ if one is None or many is None or len(one) != 1 or len(many) != 1:
78
+ return None
79
+ item = collection_item(many[0].annotation, imports)
80
+ result = collection_item(bulk.returns, imports)
81
+ if (
82
+ item is None
83
+ or result is None
84
+ or not same_type(one[0].annotation, item)
85
+ or not same_type(single.returns, result)
86
+ ):
87
+ return None
88
+ return single, bulk
89
+
90
+
91
+ def collection_item(annotation: ast.expr | None, imports: ImportIndex) -> ast.expr | None:
92
+ annotation = AnnotationSemantics(imports, MappingProxyType({})).parse(annotation)
93
+ if not isinstance(annotation, ast.Subscript):
94
+ return None
95
+ container = annotation.value
96
+ if isinstance(container, ast.Name) and container.id == "list" and imports.builtin_is_unshadowed("list"):
97
+ return annotation.slice
98
+ if imports.resolved_qualified_name(container) in _COLLECTIONS:
99
+ return annotation.slice
100
+ return None
101
+
102
+
103
+ def same_type(left: ast.expr | None, right: ast.expr | None) -> bool:
104
+ return (
105
+ left is not None
106
+ and right is not None
107
+ and ast.dump(left, include_attributes=False) == ast.dump(right, include_attributes=False)
108
+ )
109
+
110
+
111
+ def own_nodes(node: ast.AST) -> Iterator[ast.AST]:
112
+ pending = [node]
113
+ while pending:
114
+ current = pending.pop()
115
+ yield current
116
+ pending.extend(
117
+ child
118
+ for child in reversed(list(ast.iter_child_nodes(current)))
119
+ if not isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda)
120
+ )
121
+
122
+
123
+ def discarded_call(loop: ast.For) -> tuple[ast.Call, bool] | None:
124
+ if loop.orelse or len(loop.body) != 1 or not isinstance(loop.body[0], ast.Expr):
125
+ return None
126
+ if (
127
+ isinstance(loop.iter, ast.List | ast.Tuple | ast.Set)
128
+ and len(loop.iter.elts) <= 1
129
+ and not any(isinstance(item, ast.Starred) for item in loop.iter.elts)
130
+ ):
131
+ return None
132
+ value = loop.body[0].value
133
+ awaited = isinstance(value, ast.Await)
134
+ value = value.value if awaited else value
135
+ return (value, awaited) if isinstance(value, ast.Call) else None
136
+
137
+
138
+ 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))
141
+
142
+
143
+ class StoreOwners:
144
+ def __init__(self, context: PythonFileContext) -> None:
145
+ self.context = context
146
+ self.provenance = ResourceProvenance(context)
147
+ self.project = context.session.project
148
+
149
+ def receiver(self, expression: ast.expr, at: ast.AST) -> tuple[ast.ClassDef, ImportIndex] | None:
150
+ if isinstance(expression, ast.Name):
151
+ binding = self.provenance.binding(expression.id, at)
152
+ if isinstance(binding, ast.arg) and binding.annotation is not None:
153
+ return self.annotation(binding.annotation)
154
+ return None
155
+ if (
156
+ not isinstance(expression, ast.Attribute)
157
+ or not isinstance(expression.value, ast.Name)
158
+ or expression.value.id != "self"
159
+ ):
160
+ return None
161
+ if not isinstance(self.provenance.binding("self", at), ast.arg):
162
+ return None
163
+ annotation = self._injected_annotation(expression.attr, at)
164
+ return self.annotation(annotation) if annotation is not None else None
165
+
166
+ def _injected_annotation(self, attribute: str, at: ast.AST) -> ast.expr | None:
167
+ parent = self.context.parents.get(at)
168
+ while parent is not None and not isinstance(parent, ast.ClassDef):
169
+ parent = self.context.parents.get(parent)
170
+ if parent is None:
171
+ return None
172
+ constructor = methods(parent).get("__init__")
173
+ if constructor is None:
174
+ return None
175
+ assignments = [
176
+ node
177
+ for method in methods(parent).values()
178
+ for node in own_nodes(method)
179
+ if _writes_self_attribute(node, attribute)
180
+ ]
181
+ if len(assignments) != 1:
182
+ 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
192
+
193
+ def annotation(self, expression: ast.expr | None) -> tuple[ast.ClassDef, ImportIndex] | None:
194
+ original = expression
195
+ expression = AnnotationSemantics(self.context.module_imports, MappingProxyType({})).parse(expression)
196
+ if expression is None or self.context.tree is None:
197
+ return None
198
+ root = expression.value if isinstance(expression, ast.Attribute) else expression
199
+ if (
200
+ not isinstance(root, ast.Name)
201
+ or original is None
202
+ or not isinstance(self.provenance.binding(root.id, original), ast.ClassDef | ast.alias)
203
+ ):
204
+ return None
205
+ if root.id in self.provenance.mutated:
206
+ return None
207
+ if isinstance(expression, ast.Name):
208
+ declaration = _declared_class(self.context.tree, expression.id)
209
+ if declaration is not None:
210
+ return declaration, self.context.module_imports
211
+ return self._imported_annotation(expression)
212
+
213
+ def _imported_annotation(self, expression: ast.expr) -> tuple[ast.ClassDef, ImportIndex] | None:
214
+ if self.project is None or self.context.tree is None:
215
+ return None
216
+ unit = self.project.unit_or_source(self.context.path, self.context.source, self.context.tree)
217
+ symbol = ProjectIndexSet.resolve(unit, expression) if unit is not None else None
218
+ owner = self.project.source_unit(symbol.module) if symbol is not None else None
219
+ if symbol is None or owner is None or owner.tree is None or is_generated(owner.path, owner.source):
220
+ return None
221
+ declaration = _declared_class(owner.tree, symbol.name)
222
+ if declaration is None:
223
+ return None
224
+ 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
+
226
+ 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:
229
+ return None
230
+ return declaration, definition.module_imports
231
+
232
+
233
+ def _writes_self_attribute(node: ast.AST, attribute: str) -> bool:
234
+ return (
235
+ isinstance(node, ast.Attribute)
236
+ and node.attr == attribute
237
+ and isinstance(node.value, ast.Name)
238
+ and node.value.id == "self"
239
+ and isinstance(node.ctx, ast.Store | ast.Del)
240
+ )
241
+
242
+
243
+ def _injected_parameter(constructor: Function, value: ast.Name, provenance: ResourceProvenance) -> ast.expr | None:
244
+ parameter = next(
245
+ (
246
+ arg
247
+ for arg in (*constructor.args.posonlyargs, *constructor.args.args, *constructor.args.kwonlyargs)
248
+ if arg.arg == value.id
249
+ ),
250
+ None,
251
+ )
252
+ if parameter is None or provenance.binding(parameter.arg, value) is not parameter:
253
+ return None
254
+ return parameter.annotation
255
+
256
+
257
+ def _declared_class(tree: ast.Module, name: str) -> ast.ClassDef | None:
258
+ candidates = [node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == name]
259
+ return candidates[0] if len(candidates) == 1 and not candidates[0].decorator_list else None
260
+
261
+
262
+ def sql_query(node: ast.expr, imports: ImportIndex) -> exp.Expr | None:
263
+ if (
264
+ isinstance(node, ast.Call)
265
+ and imports.resolved_qualified_name(node.func) == "psycopg.sql.SQL"
266
+ and len(node.args) == 1
267
+ and not node.keywords
268
+ ):
269
+ node = node.args[0]
270
+ if not isinstance(node, ast.Constant) or not isinstance(node.value, str):
271
+ return None
272
+ try:
273
+ statements = sqlglot.parse(node.value, read="postgres")
274
+ except SqlglotError:
275
+ return None
276
+ return statements[0] if len(statements) == 1 else None
277
+
278
+
279
+ def executed_queries(method: Function, imports: ImportIndex) -> list[exp.Expr]:
280
+ queries: list[exp.Expr] = []
281
+ calls = [
282
+ node
283
+ for statement in method.body
284
+ if not isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
285
+ for node in own_nodes(statement)
286
+ if isinstance(node, ast.Call)
287
+ and isinstance(node.func, ast.Attribute)
288
+ and node.func.attr in {"execute", "executemany"}
289
+ ]
290
+ for call in calls:
291
+ if not call.args or (query := sql_query(call.args[0], imports)) is None:
292
+ return []
293
+ queries.append(query)
294
+ return queries
295
+
296
+
297
+ def sql_arg(node: exp.Expr, key: str) -> exp.Expr | None:
298
+ return next((child for child in node.iter_expressions() if child.arg_key == key), None)
299
+
300
+
301
+ def sql_args(node: exp.Expr, key: str) -> tuple[exp.Expr, ...]:
302
+ return tuple(child for child in node.iter_expressions() if child.arg_key == key)
303
+
304
+
305
+ def sql_identifier(node: exp.Expr | None) -> str | None:
306
+ if isinstance(node, exp.Column):
307
+ node = sql_arg(node, "this")
308
+ if not isinstance(node, exp.Identifier):
309
+ return None
310
+ return node.name if node.quoted else node.name.lower()
311
+
312
+
313
+ def sql_table(node: exp.Expr | None) -> tuple[str | None, ...] | None:
314
+ if not isinstance(node, exp.Table) or sql_arg(node, "alias") is not None:
315
+ return None
316
+ return tuple(
317
+ sql_identifier(value) if (value := sql_arg(node, key)) is not None else None
318
+ for key in ("catalog", "db", "this")
319
+ )