sarj-python-lint 1.57.0__tar.gz → 1.58.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 (225) hide show
  1. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resource_provenance.py +1 -1
  4. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +30 -0
  5. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +152 -45
  6. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/.gitignore +0 -0
  7. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/LICENSE +0 -0
  8. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/README.md +0 -0
  9. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_source.py +0 -0
  18. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/benchmark.py +0 -0
  20. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  21. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/json_boundary.py +0 -0
  22. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/py.typed +0 -0
  23. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/ratchet.py +0 -0
  24. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  30. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  31. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  32. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  33. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  34. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  35. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  36. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  37. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  38. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  39. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  40. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  41. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  42. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  43. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  44. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  46. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  47. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  48. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  49. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  50. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  51. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  52. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  53. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  54. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  55. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  56. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  57. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  58. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  59. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  60. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  61. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  62. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  63. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  64. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  65. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  66. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  67. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  68. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  69. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  70. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  71. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  72. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  73. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  74. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  75. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  76. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  77. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  78. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  79. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  80. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  81. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  82. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  83. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  84. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  85. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  86. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  87. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  88. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  89. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  90. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  91. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  92. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  93. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  94. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  95. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  96. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  97. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  98. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  99. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  100. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  101. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  102. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  103. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  104. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  105. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  106. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  107. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  108. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  109. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  110. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  111. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  112. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  113. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  114. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  115. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  116. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  117. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  118. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  119. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  120. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  121. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  122. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  123. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  124. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  125. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  126. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  127. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  128. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  129. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  130. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  131. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  132. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  133. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  134. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  135. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  136. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  137. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  138. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  139. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  140. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  141. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  142. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  143. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  144. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  145. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  146. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  147. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  148. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  149. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  150. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  151. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  152. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  153. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  154. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  155. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  156. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  157. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  158. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  159. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  160. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  161. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  162. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  163. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  164. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  165. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  166. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  167. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  168. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  169. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  170. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  171. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  172. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  173. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  174. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  175. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  176. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  177. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  178. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  179. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  180. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  181. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  182. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  183. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  184. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  185. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  186. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  187. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  188. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  189. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  190. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  191. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  192. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  193. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  194. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  195. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  196. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  197. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  198. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  199. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  200. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  201. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  202. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  203. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  204. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  205. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  206. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  207. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  208. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  209. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  210. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  211. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  212. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  213. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  214. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  215. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  216. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  217. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  218. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  219. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  220. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  221. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  222. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  223. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  224. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  225. {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.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.58.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.58.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",
@@ -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
@@ -84,7 +84,12 @@ _OPERATOR_WRITES: Final[dict[str, tuple[frozenset[_Kind], int]]] = {
84
84
  "operator.ixor": (frozenset({"set"}), 2),
85
85
  "operator.ior": (frozenset({"dict", "set"}), 2),
86
86
  }
87
- _IDENTITY_ARGUMENTS: Final[dict[str, int]] = {"typing.cast": 1, "typing.assert_type": 0}
87
+ _IDENTITY_ARGUMENTS: Final[dict[str, int]] = {
88
+ "typing.cast": 1,
89
+ "typing.assert_type": 0,
90
+ "typing_extensions.cast": 1,
91
+ "typing_extensions.assert_type": 0,
92
+ }
88
93
  _IDENTITY_CALL_ARITY = 2
89
94
  _LINEAR = (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Delete, ast.Expr, ast.Pass)
90
95
  _DEFERRED_OR_REBINDING = (
@@ -113,32 +118,43 @@ class _Evaluation:
113
118
  origins: dict[str, _Borrowed]
114
119
 
115
120
 
121
+ @dataclass(frozen=True, slots=True)
122
+ class _TerminalPaths:
123
+ terminal: bool
124
+ returns_value: bool
125
+
126
+
127
+ type _AssignedValue = _Borrowed | tuple[_AssignedValue, ...] | None
128
+
129
+
116
130
  @final
117
131
  class NoMutatingContainerTransform(Rule):
118
132
  id = "no-mutating-container-transform"
119
133
  code = "SARJ520"
120
134
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
121
- default_level=Severity.ERROR,
135
+ default_level=Severity.WARNING,
122
136
  summary="Return container replacements without mutating borrowed builtin inputs.",
123
137
  rationale="A container-valued transformation can change caller aliases before returning its result. Copying after the write leaves the original input changed.",
124
138
  remediation="Copy each changed container before writing, return a fresh replacement, or expose a separate command returning None. Keep documented in-place contracts explicit; do not discard the operation or add a dummy assignment.",
125
139
  category=RuleCategory.CORRECTNESS,
126
140
  autofix=AutofixPolicy.NONE,
127
141
  limitations=(
128
- "Checks undecorated functions and methods with a direct builtin list/dict/set/tuple/frozenset return annotation, typed primitive mutable builtin container inputs, and assignments, returns, raises and structured if branches ending in a value return. Returned container kinds and element types do not change borrowed input ownership.",
142
+ "Checks undecorated functions and methods with a direct builtin list/dict/set/tuple/frozenset return annotation, typed primitive mutable builtin container inputs, and assignments, returns, raises and structured if branches. Terminal paths must return a value or raise, with at least one value return. Returned container kinds and element types do not change borrowed input ownership.",
129
143
  "Direct unshadowed builtin staticmethod/classmethod and canonical typing.final markers preserve the method body in plain class namespaces. Custom, patched, inherited or metaclass-prepared namespaces and multiple binding markers remain excluded.",
144
+ "Wildcard providers, type-parameter names that shadow builtin annotations, and method annotations in inherited, decorated or metaclass-prepared class namespaces do not prove builtin primitive ownership.",
130
145
  "Tracks direct aliases and conditional-expression aliases with one proven borrowed owner, item writes/deletions, known builtin mutators and supported in-place operators. Rebinding to a fresh local builder ends the borrowed root's provenance.",
131
146
  "Canonical source-resolved operator item writes/deletions and supported in-place functions are checked with positional arity and builtin kind contracts. Patched, escaped or ambiguous operator providers and unknown functional helpers are excluded.",
132
147
  "Direct unshadowed builtin list/dict/set mutation descriptors and explicit mutating special methods use the same borrowed-target contract. Unknown classes, read methods and fresh local targets remain excluded.",
133
- "Source-proven typing.cast/assert_type calls with two positional arguments preserve the borrowed argument's identity through aliases. They do not allocate a replacement; copying before the helper still ends borrowed provenance.",
148
+ "Source-proven typing and typing_extensions cast/assert_type calls preserve borrowed identity. Two positional arguments are supported; cast also accepts its native typ/val keyword forms. Positional-only assert_type, duplicate/unknown arguments and opaque expansions are not guessed. Copying before the helper still ends borrowed provenance.",
134
149
  "Branches retain only origins shared by every continuing path; uncertain joins lose provenance. Proven literal truth selects reachable statement/expression branches and respects boolean short-circuiting; return/raise paths stop flow. Eager comprehensions isolate target bindings; their outermost iterable uses the enclosing scope.",
135
150
  "Boolean value selections preserve aliases only when every reachable result has one proven borrowed owner. Unknown conditions that can return unrelated values lose provenance.",
136
151
  "Comprehension operands follow generator/filter order. Empty literal iterables and literal-false filters prevent later operands and bodies from being visited; effects in earlier operands remain checked.",
137
152
  "Native collection literals supply truth facts for empty literals, explicit members and directly empty expansions. Contents are still evaluated; unknown spreads and application truth methods remain unresolved.",
138
153
  "Literal primitive equality/order and singleton identity comparisons supply branch facts. Comparison chains stop before later operands after a proven false link; unknown application comparisons are never evaluated.",
139
- "Synchronous generator expressions passed as the sole argument of a direct unshadowed native list/tuple/set/frozenset constructor are eagerly consumed and share comprehension flow. Unknown, shadowed, patched and asynchronous consumers are excluded.",
154
+ "Synchronous generator expressions passed as the sole argument of a direct unshadowed native list/tuple/set/frozenset constructor are eagerly consumed and share comprehension flow. Other generator expressions evaluate only their outer iterable at creation; their bodies, filters and later iterables remain deferred, including asynchronous generators.",
140
155
  "Local annotated assignments visit their value and evaluated target operands, not deferred annotation expressions; an annotation without a value does not perform an item write.",
141
- "Multiple assignment/deletion targets, unpacking assignments, more than 32 nested branches, loops, try/context-manager orchestration, closures, other generators, assignment expressions, opaque type aliases, unknown receivers and dynamic namespaces are excluded.",
156
+ "Chained assignment and deletion targets execute in order. Exact tuple/list literal destructuring captures right-side origins before binding targets in order; opaque, starred and mismatched destructuring remain excluded.",
157
+ "More than 32 nested branches, loops, try/context-manager orchestration, closures, assignment expressions, opaque type aliases, unknown receivers and dynamic namespaces are excluded.",
142
158
  "Model-valued inputs and framework validators retain their existing rule owners. Mutation of nested descendants, receiver state, I/O and scalar command/result APIs are outside this rule's supported contract.",
143
159
  "Generated and vendored sources are excluded; maintained tests are checked. No automatic copying, deletion, or command rewrite is offered.",
144
160
  ),
@@ -187,7 +203,7 @@ class NoMutatingContainerTransform(Rule):
187
203
  col=mutation.node.col_offset + 1,
188
204
  code=self.code,
189
205
  message=f"This container-valued transformation mutates borrowed {mutation.owner.kind} input `{mutation.owner.parameter}`; copy before writing or use a command returning None.",
190
- severity=Severity.ERROR,
206
+ severity=Severity.WARNING,
191
207
  )
192
208
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)
193
209
  for mutation in analysis.mutations(function)
@@ -207,7 +223,7 @@ class _ContainerTransforms:
207
223
  return {}
208
224
  calls: dict[ast.Call, str] = {}
209
225
  for call in self.context.nodes(ast.Call):
210
- if call.keywords or any(isinstance(argument, ast.Starred) for argument in call.args):
226
+ if any(isinstance(argument, ast.Starred) for argument in call.args):
211
227
  continue
212
228
  qualified = self.provenance.runtime_import_name(call.func)
213
229
  if qualified is not None:
@@ -222,7 +238,7 @@ class _ContainerTransforms:
222
238
  if contract is None:
223
239
  continue
224
240
  kinds, arity = contract
225
- if len(call.args) == arity:
241
+ if len(call.args) == arity and not call.keywords:
226
242
  calls[call] = kinds
227
243
  for call in self.context.nodes(ast.Call):
228
244
  if not call.args or isinstance(call.args[0], ast.Starred):
@@ -240,11 +256,32 @@ class _ContainerTransforms:
240
256
  calls: dict[ast.Call, ast.expr] = {}
241
257
  for call, qualified in self.native_calls.items():
242
258
  argument = _IDENTITY_ARGUMENTS.get(qualified)
243
- if argument is not None and len(call.args) == _IDENTITY_CALL_ARITY:
259
+ if argument is None:
260
+ continue
261
+ if len(call.args) == _IDENTITY_CALL_ARITY and not call.keywords:
244
262
  calls[call] = call.args[argument]
263
+ elif qualified in {"typing.cast", "typing_extensions.cast"}:
264
+ value = _cast_value_argument(call)
265
+ if value is not None:
266
+ calls[call] = value
245
267
  return calls
246
268
 
247
- def kind(self, annotation: ast.expr | None, *, primitive_elements: bool) -> _Kind | None:
269
+ def annotation_builtin(self, name: str, at: _Function) -> bool:
270
+ if not self.context.imports.builtin_is_unshadowed(name):
271
+ return False
272
+ current: ast.AST | None = at
273
+ while current is not None:
274
+ if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) and any(
275
+ isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name
276
+ for parameter in current.type_params
277
+ ):
278
+ return False
279
+ if isinstance(current, ast.ClassDef) and (current.bases or current.keywords or current.decorator_list):
280
+ return False
281
+ current = self.context.parents.get(current)
282
+ return True
283
+
284
+ def kind(self, annotation: ast.expr | None, at: _Function, *, primitive_elements: bool) -> _Kind | None:
248
285
  annotation = AnnotationSemantics.parse(annotation)
249
286
  if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.value, ast.Name):
250
287
  return None
@@ -253,37 +290,36 @@ class _ContainerTransforms:
253
290
  pass
254
291
  case _:
255
292
  return None
256
- if not self.context.imports.builtin_is_unshadowed(name):
293
+ if not self.annotation_builtin(name, at):
257
294
  return None
258
295
  elements = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
259
296
  if len(elements) != (2 if name == "dict" else 1):
260
297
  return None
261
298
  if primitive_elements and not all(
262
- isinstance(element, ast.Name)
263
- and element.id in _PRIMITIVES
264
- and self.context.imports.builtin_is_unshadowed(element.id)
299
+ isinstance(element, ast.Name) and element.id in _PRIMITIVES and self.annotation_builtin(element.id, at)
265
300
  for element in elements
266
301
  ):
267
302
  return None
268
303
  return name
269
304
 
270
- def returns_container(self, annotation: ast.expr | None) -> bool:
305
+ def returns_container(self, annotation: ast.expr | None, at: _Function) -> bool:
271
306
  annotation = AnnotationSemantics.parse(annotation)
272
307
  if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.value, ast.Name):
273
308
  return False
274
309
  name = annotation.value.id
275
310
  if name not in {"tuple", "frozenset"}:
276
- return self.kind(annotation, primitive_elements=False) is not None
277
- if not self.context.imports.builtin_is_unshadowed(name):
311
+ return self.kind(annotation, at, primitive_elements=False) is not None
312
+ if not self.annotation_builtin(name, at):
278
313
  return False
279
314
  elements = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
280
315
  return name == "tuple" or len(elements) == 1
281
316
 
282
317
  def deferred(self, node: ast.AST) -> bool:
283
- if not isinstance(node, ast.GeneratorExp):
284
- return isinstance(node, _DEFERRED_OR_REBINDING)
318
+ return isinstance(node, _DEFERRED_OR_REBINDING)
319
+
320
+ def eager_generator(self, node: ast.GeneratorExp) -> bool:
285
321
  parent = self.context.parents.get(node)
286
- return not (
322
+ return (
287
323
  isinstance(parent, ast.Call)
288
324
  and parent.args == [node]
289
325
  and not parent.keywords
@@ -324,28 +360,31 @@ class _ContainerTransforms:
324
360
  def mutations(self, function: _Function) -> list[_Mutation]:
325
361
  if (
326
362
  not self.decorators_preserve_body(function)
327
- or not self.returns_container(function.returns)
363
+ or not self.returns_container(function.returns, function)
328
364
  or self.provenance.dynamic_namespace
365
+ or self.provenance.wildcard
329
366
  or not self.provenance.builtin_namespace_untouched
330
367
  ):
331
368
  return []
332
- if not function.body or not isinstance(function.body[-1], ast.Return) or function.body[-1].value is None:
333
- return []
334
- if isinstance(function.body[-1].value, ast.Constant) and function.body[-1].value.value is None:
335
- return []
336
369
  if not _supported_body(function.body) or any(
337
370
  self.deferred(node) for statement in function.body for node in ast.walk(statement)
338
371
  ):
339
372
  return []
373
+ ending = _terminal_paths(function.body)
374
+ if not ending.terminal or not ending.returns_value:
375
+ return []
340
376
  parameters = (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
341
377
  origins = {
342
378
  argument.arg: _Borrowed(argument.arg, kind)
343
379
  for argument in parameters
344
- if (kind := self.kind(argument.annotation, primitive_elements=True)) is not None
380
+ if (kind := self.kind(argument.annotation, function, primitive_elements=True)) is not None
345
381
  }
346
382
  if not origins:
347
383
  return []
348
- flow = _ContainerFlow(origins, self.argument_writes, self.identity_calls)
384
+ eager_generators = frozenset(
385
+ node for node in ast.walk(function) if isinstance(node, ast.GeneratorExp) and self.eager_generator(node)
386
+ )
387
+ flow = _ContainerFlow(origins, self.argument_writes, self.identity_calls, eager_generators)
349
388
  for statement in function.body:
350
389
  flow.statement(statement)
351
390
  return flow.events
@@ -358,19 +397,34 @@ class _ContainerFlow:
358
397
  origins: dict[str, _Borrowed],
359
398
  operator_calls: Mapping[ast.Call, frozenset[_Kind]],
360
399
  identity_calls: Mapping[ast.Call, ast.expr],
400
+ eager_generators: frozenset[ast.GeneratorExp],
361
401
  ) -> None:
362
402
  self.origins = origins
363
403
  self.operator_calls = operator_calls
364
404
  self.identity_calls = identity_calls
405
+ self.eager_generators = eager_generators
365
406
  self.events: list[_Mutation] = []
366
407
  self.active = True
367
408
 
368
409
  def statement(self, statement: ast.stmt) -> None:
369
410
  if not self.active:
370
411
  return
371
- if isinstance(statement, ast.If):
372
- self._conditional(statement)
373
- return
412
+ match statement:
413
+ case ast.If():
414
+ self._conditional(statement)
415
+ return
416
+ case ast.Assign():
417
+ self._evaluate(statement.value)
418
+ value = self._assigned_value(statement.value)
419
+ for target in statement.targets:
420
+ self._assignment(target, value)
421
+ return
422
+ case ast.Delete():
423
+ for target in statement.targets:
424
+ self._assignment(target, None)
425
+ return
426
+ case _:
427
+ pass
374
428
  self._evaluate(statement)
375
429
  self._bindings(statement)
376
430
  if isinstance(statement, ast.Return | ast.Raise):
@@ -383,7 +437,7 @@ class _ContainerFlow:
383
437
  event = _mutation(current.node, current.origins, self.operator_calls)
384
438
  if event is not None:
385
439
  self.events.append(event)
386
- pending.extend(_evaluated_children(current))
440
+ pending.extend(_evaluated_children(current, self.eager_generators))
387
441
 
388
442
  def _conditional(self, statement: ast.If) -> None:
389
443
  self._evaluate(statement.test)
@@ -408,17 +462,13 @@ class _ContainerFlow:
408
462
  }
409
463
 
410
464
  def _fork(self, statements: list[ast.stmt]) -> _ContainerFlow:
411
- branch = _ContainerFlow(self.origins.copy(), self.operator_calls, self.identity_calls)
465
+ branch = _ContainerFlow(self.origins.copy(), self.operator_calls, self.identity_calls, self.eager_generators)
412
466
  for statement in statements:
413
467
  branch.statement(statement)
414
468
  return branch
415
469
 
416
470
  def _bindings(self, statement: ast.stmt) -> None:
417
471
  match statement:
418
- case ast.Assign(targets=targets, value=value):
419
- origin = _borrowed_origin(value, self.origins, self.identity_calls)
420
- for target in targets:
421
- self._assign(target, origin)
422
472
  case ast.AnnAssign(target=target, value=value) if value is not None:
423
473
  self._assign(target, _borrowed_origin(value, self.origins, self.identity_calls))
424
474
  case ast.AugAssign(target=ast.Name(id=name), op=operation):
@@ -427,12 +477,25 @@ class _ContainerFlow:
427
477
  self.events.append(_Mutation(statement, owner))
428
478
  else:
429
479
  self.origins.pop(name, None)
430
- case ast.Delete(targets=targets):
431
- for target in targets:
432
- self._assign(target, None)
433
480
  case _:
434
481
  pass
435
482
 
483
+ def _assigned_value(self, value: ast.expr) -> _AssignedValue:
484
+ if isinstance(value, ast.Tuple | ast.List):
485
+ return tuple(self._assigned_value(element) for element in value.elts)
486
+ return _borrowed_origin(value, self.origins, self.identity_calls)
487
+
488
+ def _assignment(self, target: ast.expr, value: _AssignedValue) -> None:
489
+ if isinstance(target, ast.Tuple | ast.List):
490
+ values = (
491
+ value if isinstance(value, tuple) and len(value) == len(target.elts) else (None,) * len(target.elts)
492
+ )
493
+ for element, assigned in zip(target.elts, values, strict=True):
494
+ self._assignment(element, assigned)
495
+ return
496
+ self._evaluate(target)
497
+ self._assign(target, value if isinstance(value, _Borrowed) else None)
498
+
436
499
  def _assign(self, target: ast.expr, owner: _Borrowed | None) -> None:
437
500
  match target:
438
501
  case ast.Name(id=name):
@@ -473,7 +536,7 @@ def _mutation(
473
536
  return None
474
537
 
475
538
 
476
- def _evaluated_children(current: _Evaluation) -> list[_Evaluation]:
539
+ def _evaluated_children(current: _Evaluation, eager_generators: frozenset[ast.GeneratorExp]) -> list[_Evaluation]:
477
540
  node, origins = current.node, current.origins
478
541
  match node:
479
542
  case ast.AnnAssign(target=target, value=value):
@@ -486,6 +549,8 @@ def _evaluated_children(current: _Evaluation) -> list[_Evaluation]:
486
549
  return _comparison_operands(node, origins)
487
550
  case ast.IfExp():
488
551
  return _conditional_operands(node, origins)
552
+ case ast.GeneratorExp() if node not in eager_generators:
553
+ return [_Evaluation(node.generators[0].iter, origins)]
489
554
  case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
490
555
  return _comprehension_operands(node, origins)
491
556
  case _:
@@ -721,9 +786,51 @@ def _supported_body(statements: list[ast.stmt]) -> bool:
721
786
 
722
787
  def _compound_targets(statement: ast.stmt) -> bool:
723
788
  match statement:
724
- case ast.Assign(targets=targets):
725
- return len(targets) != 1 or isinstance(targets[0], ast.Tuple | ast.List)
726
- case ast.Delete(targets=targets):
727
- return len(targets) != 1
789
+ case ast.Assign(targets=targets, value=value):
790
+ return any(not _supported_assignment(target, value) for target in targets)
728
791
  case _:
729
792
  return False
793
+
794
+
795
+ def _supported_assignment(target: ast.expr, value: ast.expr) -> bool:
796
+ if not isinstance(target, ast.Tuple | ast.List):
797
+ return not isinstance(target, ast.Starred)
798
+ if not isinstance(value, ast.Tuple | ast.List) or len(target.elts) != len(value.elts):
799
+ return False
800
+ return all(
801
+ not isinstance(element, ast.Starred)
802
+ and not isinstance(assigned, ast.Starred)
803
+ and _supported_assignment(element, assigned)
804
+ for element, assigned in zip(target.elts, value.elts, strict=True)
805
+ )
806
+
807
+
808
+ def _terminal_paths(statements: list[ast.stmt]) -> _TerminalPaths:
809
+ if not statements:
810
+ return _TerminalPaths(terminal=False, returns_value=False)
811
+ match statements[-1]:
812
+ case ast.Return(value=value):
813
+ returns_value = value is not None and not (isinstance(value, ast.Constant) and value.value is None)
814
+ return _TerminalPaths(terminal=returns_value, returns_value=returns_value)
815
+ case ast.Raise():
816
+ return _TerminalPaths(terminal=True, returns_value=False)
817
+ case ast.If(body=body, orelse=otherwise):
818
+ first = _terminal_paths(body)
819
+ second = _terminal_paths(otherwise)
820
+ return _TerminalPaths(
821
+ terminal=first.terminal and second.terminal,
822
+ returns_value=first.returns_value or second.returns_value,
823
+ )
824
+ case _:
825
+ return _TerminalPaths(terminal=False, returns_value=False)
826
+
827
+
828
+ def _cast_value_argument(call: ast.Call) -> ast.expr | None:
829
+ if len(call.args) > _IDENTITY_CALL_ARITY:
830
+ return None
831
+ values: dict[str, ast.expr] = dict(zip(("typ", "val"), call.args, strict=False))
832
+ for keyword in call.keywords:
833
+ if keyword.arg not in {"typ", "val"} or keyword.arg in values:
834
+ return None
835
+ values[keyword.arg] = keyword.value
836
+ return values.get("val") if values.keys() == {"typ", "val"} else None