sarj-python-lint 1.57.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.57.0 → sarj_python_lint-1.60.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resource_provenance.py +1 -1
  4. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_store_writes.py +140 -23
  5. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +30 -0
  6. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +152 -45
  7. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +1 -1
  8. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +26 -2
  9. sarj_python_lint-1.60.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +475 -0
  10. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +81 -6
  11. sarj_python_lint-1.57.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -240
  12. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/.gitignore +0 -0
  13. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/LICENSE +0 -0
  14. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/README.md +0 -0
  15. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__init__.py +0 -0
  16. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__main__.py +0 -0
  17. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  18. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_file_context.py +0 -0
  19. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_filesystem.py +0 -0
  20. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_python_target.py +0 -0
  21. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  22. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_secret_names.py +0 -0
  23. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_source.py +0 -0
  24. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_version.py +0 -0
  25. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/benchmark.py +0 -0
  26. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  27. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/json_boundary.py +0 -0
  28. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/py.typed +0 -0
  29. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/ratchet.py +0 -0
  30. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rule_base.py +0 -0
  31. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  32. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  33. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  34. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  35. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  36. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  37. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  38. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  39. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  40. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  41. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  42. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  43. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  44. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  45. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  46. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  47. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  48. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  49. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  50. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  51. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  52. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  53. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  54. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  55. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  56. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  57. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  58. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  59. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  60. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  61. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  62. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  63. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  64. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  65. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  66. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  67. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  68. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  69. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  70. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  71. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  72. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  73. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  74. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  75. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  76. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  77. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  78. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  79. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  80. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  81. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  82. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  83. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  84. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  85. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  86. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  87. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  88. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  89. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  90. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  91. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  92. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  93. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  94. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  95. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  96. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  97. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  98. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  99. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  100. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  101. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  102. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  103. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  104. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  105. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  106. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  107. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  108. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  109. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  110. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  111. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  112. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  113. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  114. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  115. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  116. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  117. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  118. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  119. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  120. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  121. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  122. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  124. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  125. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  126. {sarj_python_lint-1.57.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.57.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.57.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.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  132. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  134. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  135. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  136. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  137. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  138. {sarj_python_lint-1.57.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.57.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.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  143. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  144. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  145. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  146. {sarj_python_lint-1.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  149. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  150. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  152. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  153. {sarj_python_lint-1.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  156. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  157. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  159. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  160. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  162. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  163. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  164. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  165. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  167. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  168. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  169. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  170. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  171. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  172. {sarj_python_lint-1.57.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.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  176. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  177. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  178. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  179. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  180. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  181. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  182. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  183. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  184. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  185. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  186. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  187. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  188. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  190. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  191. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  192. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  193. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  195. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  196. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  197. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  198. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  200. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  201. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  202. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  203. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  204. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  205. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  206. {sarj_python_lint-1.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  209. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  210. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  211. {sarj_python_lint-1.57.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.57.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.57.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.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  216. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  217. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  218. {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  220. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  221. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  222. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  223. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  224. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  225. {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  226. {sarj_python_lint-1.57.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.57.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.57.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" }]
@@ -37,7 +37,7 @@ _RESOURCE_OWNERS = MappingProxyType(
37
37
  "operator": frozenset({"setitem", "delitem", "iadd", "iconcat", "imul", "iand", "isub", "ixor", "ior"}),
38
38
  "unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
39
39
  "typing": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
40
- "typing_extensions": frozenset({"final", "Literal", "Annotated"}),
40
+ "typing_extensions": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
41
41
  "pydantic": frozenset(
42
42
  {
43
43
  "BaseModel",
@@ -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
  (
@@ -155,6 +155,7 @@ class NoDiscardedValidationResult(Rule):
155
155
  "Stable local callable aliases with prior direct assignments are followed through bounded chains. Conditional assignments and unresolved callable escapes are excluded.",
156
156
  "Direct execution in class bodies is excluded because sequential lookup and metaclass-prepared namespaces can supply different callees. Method bodies remain checked.",
157
157
  "Functions with explicit mutation, standalone command calls, generators, nested definitions, or context-manager/try/loop orchestration are excluded. This is not a general purity proof.",
158
+ "Calls inside returned values must resolve to supported builtin or source-resolved nominal value construction. Opaque command functions, receiver calls, and direct returned attribute/subscript reads are excluded; a conversion around an unknown command does not prove validation.",
158
159
  "Import-resolved pytest.raises and source-proven unittest exception assertion blocks observe the exception contract and are excluded. Generated and vendored sources are excluded; maintained tests are checked.",
159
160
  "Top-level return contracts follow stable lexical aliases, first-party type imports, unions, and Annotated type content through a bounded source-only analysis. Nullable collection elements and annotation metadata do not make the returned collection nullable.",
160
161
  "Nullable, object, Any, Never, NoReturn, generic type-parameter, structural protocol, and unresolved return contracts are excluded. No automatic assignment, deletion, or validation rewrite is offered.",
@@ -246,12 +247,26 @@ class _ValidationResults:
246
247
  and _value_validator(function)
247
248
  and provenance is not None
248
249
  and self._value_annotation(provenance, function.returns, function, _MAX_ANNOTATION_DEPTH, frozenset())
250
+ and self._returned_constructions(provenance, function)
249
251
  )
250
252
  return self.contracts[function] and not any(
251
253
  _namespace_uncertain(provenance, owner.path) or _namespace_uncertain(provenance, provenance.context.path)
252
254
  for provenance in resolved.namespaces
253
255
  )
254
256
 
257
+ def _returned_constructions(self, provenance: ResourceProvenance, function: _Function) -> bool:
258
+ for statement in ast.walk(function):
259
+ if not isinstance(statement, ast.Return) or statement.value is None:
260
+ continue
261
+ if _opaque_return_selection(statement.value):
262
+ return False
263
+ for node in ast.walk(statement.value):
264
+ if isinstance(node, ast.Call) and not self._value_annotation(
265
+ provenance, node.func, node, _MAX_ANNOTATION_DEPTH, frozenset()
266
+ ):
267
+ return False
268
+ return True
269
+
255
270
  def _resolve(self, expression: ast.expr) -> _ResolvedValidation | None:
256
271
  if isinstance(self.provenance.scope(expression), ast.ClassDef):
257
272
  return None
@@ -712,6 +727,21 @@ def _expression_effect(value: ast.expr, *, allow_calls: bool = False) -> bool:
712
727
  return any(isinstance(node, effects) for node in ast.walk(value))
713
728
 
714
729
 
730
+ def _opaque_return_selection(value: ast.expr) -> bool:
731
+ pending = [value]
732
+ while pending:
733
+ match pending.pop():
734
+ case ast.Attribute() | ast.Subscript():
735
+ return True
736
+ case ast.IfExp(body=body, orelse=otherwise):
737
+ pending.extend((body, otherwise))
738
+ case ast.BoolOp(values=values):
739
+ pending.extend(values)
740
+ case _:
741
+ pass
742
+ return False
743
+
744
+
715
745
  def _returned_value(value: ast.expr | None) -> bool:
716
746
  return (
717
747
  value is not None