sarj-python-lint 1.50.0__tar.gz → 1.52.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 (223) hide show
  1. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_annotation_semantics.py +2 -1
  4. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_resource_provenance.py +2 -1
  5. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +2 -2
  6. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +290 -33
  7. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/.gitignore +0 -0
  8. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/LICENSE +0 -0
  9. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/README.md +0 -0
  10. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_source.py +0 -0
  19. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  22. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/json_boundary.py +0 -0
  23. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  30. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  31. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  32. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  33. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  34. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  35. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  36. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  37. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  38. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  39. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  40. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  41. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  42. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  43. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  44. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  46. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  47. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  48. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  49. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  50. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  51. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  52. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  54. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  55. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  56. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  59. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  60. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  61. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  62. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  63. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  64. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  65. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  66. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  67. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  68. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  69. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  70. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  71. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  72. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  73. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  74. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  75. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  76. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  77. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  78. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  79. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  80. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  81. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  82. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  83. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  84. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  85. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  86. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  87. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  88. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  89. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  90. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  91. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  92. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  93. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  94. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  95. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  96. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  97. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  98. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  99. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  100. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  101. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  102. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  103. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  104. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  105. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  106. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  107. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  108. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  109. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  110. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  111. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  112. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  113. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  114. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  115. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  116. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  117. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  118. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  119. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  120. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  121. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  122. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  123. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  124. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  125. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  126. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  127. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  128. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  129. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  130. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  131. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  132. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  133. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  134. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  135. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  136. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  137. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  138. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  139. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  140. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  141. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  142. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  143. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  144. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  145. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  146. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  147. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  148. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  149. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  150. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  151. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  152. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  153. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  154. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  155. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  156. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  157. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  158. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  159. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  160. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  161. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  162. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  163. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  164. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  165. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  166. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  167. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  168. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  169. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  170. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  171. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  172. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  173. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  174. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  175. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  176. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  177. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  178. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  179. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  180. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  181. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  182. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  183. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  184. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  185. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  186. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  187. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  188. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  189. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  190. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  191. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  192. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  193. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  194. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  195. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  196. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  197. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  198. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  199. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  200. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  201. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  202. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  203. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  204. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  205. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  206. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  207. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  208. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  209. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  210. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  211. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  212. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  213. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  214. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  215. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  216. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  217. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  218. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  219. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  220. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  221. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  222. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  223. {sarj_python_lint-1.50.0 → sarj_python_lint-1.52.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.50.0
3
+ Version: 1.52.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.50.0"
3
+ version = "1.52.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" }]
@@ -47,7 +47,8 @@ class AnnotationSemantics:
47
47
  aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
48
48
  return cls(imports, MappingProxyType(aliases))
49
49
 
50
- def parse(self, annotation: ast.expr | None) -> ast.expr | None:
50
+ @staticmethod
51
+ def parse(annotation: ast.expr | None) -> ast.expr | None:
51
52
  if not isinstance(annotation, ast.Constant) or not isinstance(annotation.value, str):
52
53
  return annotation
53
54
  try:
@@ -34,8 +34,9 @@ _RESOURCE_OWNERS = MappingProxyType(
34
34
  "contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
35
35
  "subprocess": frozenset({"Popen", "TimeoutExpired"}),
36
36
  "re": frozenset({"compile", "Match", "Pattern"}),
37
+ "operator": frozenset({"setitem", "delitem", "iadd", "iconcat", "imul", "iand", "isub", "ixor", "ior"}),
37
38
  "unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
38
- "typing": frozenset({"final", "Literal", "Annotated"}),
39
+ "typing": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
39
40
  "typing_extensions": frozenset({"final", "Literal", "Annotated"}),
40
41
  "pydantic": frozenset(
41
42
  {
@@ -140,7 +140,7 @@ class NoDiscardedValidationResult(Rule):
140
140
  id = "no-discarded-validation-result"
141
141
  code = "SARJ519"
142
142
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
143
- default_level=Severity.WARNING,
143
+ default_level=Severity.ERROR,
144
144
  summary="Consume a value-producing validator's result instead of relying on its exceptions.",
145
145
  rationale="Discarding a conversion's returned value makes validation an implicit exception side effect. The caller can continue with unvalidated or unnormalized inputs even after constructing a checked replacement.",
146
146
  remediation="Use the checked result in the following operation, or expose a separate validation command returning None. Preserve exception behavior; do not delete the call or add a dummy assignment.",
@@ -212,7 +212,7 @@ class NoDiscardedValidationResult(Rule):
212
212
  col=statement.col_offset + 1,
213
213
  code=self.code,
214
214
  message="This value-producing validator's result is discarded; consume the checked value or use a validation command returning None.",
215
- severity=Severity.WARNING,
215
+ severity=Severity.ERROR,
216
216
  )
217
217
  for statement, call in calls
218
218
  if isinstance(call, ast.Call)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from dataclasses import dataclass
5
+ from functools import cached_property
5
6
  from pathlib import PurePosixPath
6
7
  from typing import TYPE_CHECKING, ClassVar, Final, Literal, final, override
7
8
 
@@ -17,20 +18,42 @@ from sarj_python_lint.rule_base import (
17
18
  Severity,
18
19
  is_suppressed,
19
20
  )
21
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
20
22
  from sarj_python_lint.rules._resource_provenance import ResourceProvenance
21
23
 
22
24
 
23
25
  if TYPE_CHECKING:
26
+ from collections.abc import Mapping
27
+
24
28
  from sarj_python_lint._file_context import PythonFileContext
25
29
 
26
30
 
27
31
  type _Kind = Literal["list", "dict", "set"]
28
32
  type _Function = ast.FunctionDef | ast.AsyncFunctionDef
33
+ type _Comprehension = ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp
29
34
  _MAX_BRANCH_DEPTH = 32
30
35
  _PRIMITIVES = frozenset({"str", "int", "float", "bool", "bytes", "complex"})
31
36
  _MUTATORS: Final[dict[_Kind, frozenset[str]]] = {
32
- "list": frozenset({"append", "extend", "insert", "pop", "remove", "clear", "sort", "reverse"}),
33
- "dict": frozenset({"clear", "pop", "popitem", "setdefault", "update"}),
37
+ "list": frozenset(
38
+ {
39
+ "append",
40
+ "extend",
41
+ "insert",
42
+ "pop",
43
+ "remove",
44
+ "clear",
45
+ "sort",
46
+ "reverse",
47
+ "__setitem__",
48
+ "__delitem__",
49
+ "__iadd__",
50
+ "__imul__",
51
+ "__init__",
52
+ }
53
+ ),
54
+ "dict": frozenset(
55
+ {"clear", "pop", "popitem", "setdefault", "update", "__setitem__", "__delitem__", "__ior__", "__init__"}
56
+ ),
34
57
  "set": frozenset(
35
58
  {
36
59
  "add",
@@ -42,13 +65,30 @@ _MUTATORS: Final[dict[_Kind, frozenset[str]]] = {
42
65
  "difference_update",
43
66
  "intersection_update",
44
67
  "symmetric_difference_update",
68
+ "__iand__",
69
+ "__ior__",
70
+ "__isub__",
71
+ "__ixor__",
72
+ "__init__",
45
73
  }
46
74
  ),
47
75
  }
76
+ _OPERATOR_WRITES: Final[dict[str, tuple[frozenset[_Kind], int]]] = {
77
+ "operator.setitem": (frozenset({"list", "dict"}), 3),
78
+ "operator.delitem": (frozenset({"list", "dict"}), 2),
79
+ "operator.iadd": (frozenset({"list"}), 2),
80
+ "operator.iconcat": (frozenset({"list"}), 2),
81
+ "operator.imul": (frozenset({"list"}), 2),
82
+ "operator.iand": (frozenset({"set"}), 2),
83
+ "operator.isub": (frozenset({"set"}), 2),
84
+ "operator.ixor": (frozenset({"set"}), 2),
85
+ "operator.ior": (frozenset({"dict", "set"}), 2),
86
+ }
87
+ _IDENTITY_ARGUMENTS: Final[dict[str, int]] = {"typing.cast": 1, "typing.assert_type": 0}
88
+ _IDENTITY_CALL_ARITY = 2
48
89
  _LINEAR = (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Delete, ast.Expr, ast.Pass)
49
90
  _DEFERRED_OR_REBINDING = (
50
91
  ast.Lambda,
51
- ast.GeneratorExp,
52
92
  ast.NamedExpr,
53
93
  ast.Yield,
54
94
  ast.YieldFrom,
@@ -78,19 +118,27 @@ class NoMutatingContainerTransform(Rule):
78
118
  id = "no-mutating-container-transform"
79
119
  code = "SARJ520"
80
120
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
81
- default_level=Severity.WARNING,
121
+ default_level=Severity.ERROR,
82
122
  summary="Return container replacements without mutating borrowed builtin inputs.",
83
123
  rationale="A container-valued transformation can change caller aliases before returning its result. Copying after the write leaves the original input changed.",
84
124
  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.",
85
125
  category=RuleCategory.CORRECTNESS,
86
126
  autofix=AutofixPolicy.NONE,
87
127
  limitations=(
88
- "Checks undecorated functions and methods with a direct builtin list/dict/set return annotation, typed primitive builtin container inputs, and assignments, returns, raises and structured if branches ending in a value return. Returned element types do not change borrowed input ownership.",
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.",
129
+ "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.",
89
130
  "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
+ "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
+ "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.",
90
134
  "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
+ "Boolean value selections preserve aliases only when every reachable result has one proven borrowed owner. Unknown conditions that can return unrelated values lose provenance.",
91
136
  "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
+ "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
+ "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.",
92
140
  "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.",
93
- "Multiple assignment/deletion targets, unpacking assignments, more than 32 nested branches, loops, try/context-manager orchestration, closures, generators, assignment expressions, opaque type aliases, unknown receivers and dynamic namespaces are excluded.",
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.",
94
142
  "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.",
95
143
  "Generated and vendored sources are excluded; maintained tests are checked. No automatic copying, deletion, or command rewrite is offered.",
96
144
  ),
@@ -139,7 +187,7 @@ class NoMutatingContainerTransform(Rule):
139
187
  col=mutation.node.col_offset + 1,
140
188
  code=self.code,
141
189
  message=f"This container-valued transformation mutates borrowed {mutation.owner.kind} input `{mutation.owner.parameter}`; copy before writing or use a command returning None.",
142
- severity=Severity.WARNING,
190
+ severity=Severity.ERROR,
143
191
  )
144
192
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)
145
193
  for mutation in analysis.mutations(function)
@@ -153,12 +201,51 @@ class _ContainerTransforms:
153
201
  self.context = context
154
202
  self.provenance = ResourceProvenance(context, require_static_namespace=True)
155
203
 
204
+ @cached_property
205
+ def native_calls(self) -> dict[ast.Call, str]:
206
+ if self.provenance.mock_namespace_patching:
207
+ return {}
208
+ calls: dict[ast.Call, str] = {}
209
+ for call in self.context.nodes(ast.Call):
210
+ if call.keywords or any(isinstance(argument, ast.Starred) for argument in call.args):
211
+ continue
212
+ qualified = self.provenance.runtime_import_name(call.func)
213
+ if qualified is not None:
214
+ calls[call] = qualified
215
+ return calls
216
+
217
+ @cached_property
218
+ def argument_writes(self) -> dict[ast.Call, frozenset[_Kind]]:
219
+ calls: dict[ast.Call, frozenset[_Kind]] = {}
220
+ for call, qualified in self.native_calls.items():
221
+ contract = _OPERATOR_WRITES.get(qualified)
222
+ if contract is None:
223
+ continue
224
+ kinds, arity = contract
225
+ if len(call.args) == arity:
226
+ calls[call] = kinds
227
+ for call in self.context.nodes(ast.Call):
228
+ if not call.args or isinstance(call.args[0], ast.Starred):
229
+ continue
230
+ if not isinstance(call.func, ast.Attribute) or not isinstance(call.func.value, ast.Name):
231
+ continue
232
+ name = call.func.value.id
233
+ for kind, methods in _MUTATORS.items():
234
+ if name == kind and call.func.attr in methods and self.context.imports.builtin_is_unshadowed(name):
235
+ calls[call] = frozenset({kind})
236
+ return calls
237
+
238
+ @cached_property
239
+ def identity_calls(self) -> dict[ast.Call, ast.expr]:
240
+ calls: dict[ast.Call, ast.expr] = {}
241
+ for call, qualified in self.native_calls.items():
242
+ argument = _IDENTITY_ARGUMENTS.get(qualified)
243
+ if argument is not None and len(call.args) == _IDENTITY_CALL_ARITY:
244
+ calls[call] = call.args[argument]
245
+ return calls
246
+
156
247
  def kind(self, annotation: ast.expr | None, *, primitive_elements: bool) -> _Kind | None:
157
- if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
158
- try:
159
- annotation = ast.parse(annotation.value, mode="eval").body
160
- except SyntaxError:
161
- return None
248
+ annotation = AnnotationSemantics.parse(annotation)
162
249
  if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.value, ast.Name):
163
250
  return None
164
251
  match annotation.value.id:
@@ -180,10 +267,64 @@ class _ContainerTransforms:
180
267
  return None
181
268
  return name
182
269
 
270
+ def returns_container(self, annotation: ast.expr | None) -> bool:
271
+ annotation = AnnotationSemantics.parse(annotation)
272
+ if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.value, ast.Name):
273
+ return False
274
+ name = annotation.value.id
275
+ 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):
278
+ return False
279
+ elements = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
280
+ return name == "tuple" or len(elements) == 1
281
+
282
+ def deferred(self, node: ast.AST) -> bool:
283
+ if not isinstance(node, ast.GeneratorExp):
284
+ return isinstance(node, _DEFERRED_OR_REBINDING)
285
+ parent = self.context.parents.get(node)
286
+ return not (
287
+ isinstance(parent, ast.Call)
288
+ and parent.args == [node]
289
+ and not parent.keywords
290
+ and isinstance(parent.func, ast.Name)
291
+ and parent.func.id in {"list", "tuple", "set", "frozenset"}
292
+ and self.context.imports.builtin_is_unshadowed(parent.func.id)
293
+ and not self.provenance.mock_namespace_patching
294
+ and not any(generator.is_async for generator in node.generators)
295
+ )
296
+
297
+ def decorators_preserve_body(self, function: _Function) -> bool:
298
+ if not function.decorator_list:
299
+ return True
300
+ if not self._plain_class_namespace(function) or self.provenance.mock_namespace_patching:
301
+ return False
302
+ binders = 0
303
+ for decorator in function.decorator_list:
304
+ if self._native_method_binder(decorator):
305
+ binders += 1
306
+ elif self.provenance.runtime_import_name(decorator) != "typing.final":
307
+ return False
308
+ return binders <= 1
309
+
310
+ def _plain_class_namespace(self, function: _Function) -> bool:
311
+ owner = self.provenance.scope(function)
312
+ return isinstance(owner, ast.ClassDef) and not (
313
+ owner.bases or owner.keywords or owner.decorator_list or owner.type_params
314
+ )
315
+
316
+ def _native_method_binder(self, decorator: ast.expr) -> bool:
317
+ return (
318
+ isinstance(decorator, ast.Name)
319
+ and decorator.id in {"staticmethod", "classmethod"}
320
+ and self.context.imports.builtin_is_unshadowed(decorator.id)
321
+ and self.provenance.runtime_binding(decorator.id, decorator) is None
322
+ )
323
+
183
324
  def mutations(self, function: _Function) -> list[_Mutation]:
184
325
  if (
185
- function.decorator_list
186
- or self.kind(function.returns, primitive_elements=False) is None
326
+ not self.decorators_preserve_body(function)
327
+ or not self.returns_container(function.returns)
187
328
  or self.provenance.dynamic_namespace
188
329
  or not self.provenance.builtin_namespace_untouched
189
330
  ):
@@ -193,7 +334,7 @@ class _ContainerTransforms:
193
334
  if isinstance(function.body[-1].value, ast.Constant) and function.body[-1].value.value is None:
194
335
  return []
195
336
  if not _supported_body(function.body) or any(
196
- isinstance(node, _DEFERRED_OR_REBINDING) for statement in function.body for node in ast.walk(statement)
337
+ self.deferred(node) for statement in function.body for node in ast.walk(statement)
197
338
  ):
198
339
  return []
199
340
  parameters = (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
@@ -204,7 +345,7 @@ class _ContainerTransforms:
204
345
  }
205
346
  if not origins:
206
347
  return []
207
- flow = _ContainerFlow(origins)
348
+ flow = _ContainerFlow(origins, self.argument_writes, self.identity_calls)
208
349
  for statement in function.body:
209
350
  flow.statement(statement)
210
351
  return flow.events
@@ -212,8 +353,15 @@ class _ContainerTransforms:
212
353
 
213
354
  @final
214
355
  class _ContainerFlow:
215
- def __init__(self, origins: dict[str, _Borrowed]) -> None:
356
+ def __init__(
357
+ self,
358
+ origins: dict[str, _Borrowed],
359
+ operator_calls: Mapping[ast.Call, frozenset[_Kind]],
360
+ identity_calls: Mapping[ast.Call, ast.expr],
361
+ ) -> None:
216
362
  self.origins = origins
363
+ self.operator_calls = operator_calls
364
+ self.identity_calls = identity_calls
217
365
  self.events: list[_Mutation] = []
218
366
  self.active = True
219
367
 
@@ -232,7 +380,7 @@ class _ContainerFlow:
232
380
  pending = [_Evaluation(expression, self.origins)]
233
381
  while pending:
234
382
  current = pending.pop()
235
- event = _mutation(current.node, current.origins)
383
+ event = _mutation(current.node, current.origins, self.operator_calls)
236
384
  if event is not None:
237
385
  self.events.append(event)
238
386
  pending.extend(_evaluated_children(current))
@@ -260,7 +408,7 @@ class _ContainerFlow:
260
408
  }
261
409
 
262
410
  def _fork(self, statements: list[ast.stmt]) -> _ContainerFlow:
263
- branch = _ContainerFlow(self.origins.copy())
411
+ branch = _ContainerFlow(self.origins.copy(), self.operator_calls, self.identity_calls)
264
412
  for statement in statements:
265
413
  branch.statement(statement)
266
414
  return branch
@@ -268,11 +416,11 @@ class _ContainerFlow:
268
416
  def _bindings(self, statement: ast.stmt) -> None:
269
417
  match statement:
270
418
  case ast.Assign(targets=targets, value=value):
271
- origin = _borrowed_origin(value, self.origins)
419
+ origin = _borrowed_origin(value, self.origins, self.identity_calls)
272
420
  for target in targets:
273
421
  self._assign(target, origin)
274
422
  case ast.AnnAssign(target=target, value=value) if value is not None:
275
- self._assign(target, _borrowed_origin(value, self.origins))
423
+ self._assign(target, _borrowed_origin(value, self.origins, self.identity_calls))
276
424
  case ast.AugAssign(target=ast.Name(id=name), op=operation):
277
425
  owner = self.origins.get(name)
278
426
  if owner is not None and _in_place(owner.kind, operation):
@@ -304,8 +452,14 @@ class _ContainerFlow:
304
452
  pass
305
453
 
306
454
 
307
- def _mutation(node: ast.AST, origins: dict[str, _Borrowed]) -> _Mutation | None:
455
+ def _mutation(
456
+ node: ast.AST, origins: dict[str, _Borrowed], operator_calls: Mapping[ast.Call, frozenset[_Kind]]
457
+ ) -> _Mutation | None:
308
458
  match node:
459
+ case ast.Call(args=[ast.Name(id=name), *_]) if node in operator_calls:
460
+ owner = origins.get(name)
461
+ if owner is not None and owner.kind in operator_calls[node]:
462
+ return _Mutation(node, owner)
309
463
  case ast.Call(func=ast.Attribute(value=ast.Name(id=name), attr=method)):
310
464
  owner = origins.get(name)
311
465
  if owner is not None and method in _MUTATORS[owner.kind]:
@@ -328,9 +482,11 @@ def _evaluated_children(current: _Evaluation) -> list[_Evaluation]:
328
482
  return [_Evaluation(target, origins), _Evaluation(value, origins)]
329
483
  case ast.BoolOp():
330
484
  return _boolean_operands(node, origins)
485
+ case ast.Compare():
486
+ return _comparison_operands(node, origins)
331
487
  case ast.IfExp():
332
488
  return _conditional_operands(node, origins)
333
- case ast.ListComp() | ast.SetComp() | ast.DictComp():
489
+ case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
334
490
  return _comprehension_operands(node, origins)
335
491
  case _:
336
492
  return [_Evaluation(child, origins) for child in ast.iter_child_nodes(node)]
@@ -357,9 +513,18 @@ def _conditional_operands(node: ast.IfExp, origins: dict[str, _Borrowed]) -> lis
357
513
  return [_Evaluation(node.test, origins), *(_Evaluation(child, origins) for child in alternatives)]
358
514
 
359
515
 
360
- def _comprehension_operands(
361
- node: ast.ListComp | ast.SetComp | ast.DictComp, origins: dict[str, _Borrowed]
362
- ) -> list[_Evaluation]:
516
+ def _comparison_operands(node: ast.Compare, origins: dict[str, _Borrowed]) -> list[_Evaluation]:
517
+ left = node.left
518
+ operands = [_Evaluation(left, origins)]
519
+ for operation, right in zip(node.ops, node.comparators, strict=True):
520
+ operands.append(_Evaluation(right, origins))
521
+ if _literal_comparison(left, operation, right) is False:
522
+ break
523
+ left = right
524
+ return operands
525
+
526
+
527
+ def _comprehension_operands(node: _Comprehension, origins: dict[str, _Borrowed]) -> list[_Evaluation]:
363
528
  scoped = _comprehension_origins(node, origins)
364
529
  operands = [_Evaluation(node.generators[0].iter, origins)]
365
530
  for index, generator in enumerate(node.generators):
@@ -379,9 +544,7 @@ def _comprehension_operands(
379
544
  return operands
380
545
 
381
546
 
382
- def _comprehension_origins(
383
- node: ast.ListComp | ast.SetComp | ast.DictComp, origins: dict[str, _Borrowed]
384
- ) -> dict[str, _Borrowed]:
547
+ def _comprehension_origins(node: _Comprehension, origins: dict[str, _Borrowed]) -> dict[str, _Borrowed]:
385
548
  targets = {
386
549
  child.id
387
550
  for generator in node.generators
@@ -405,6 +568,10 @@ def _literal_truth(expression: ast.expr) -> bool | None:
405
568
  match expression:
406
569
  case ast.Constant(value=value):
407
570
  return bool(value)
571
+ case ast.List() | ast.Tuple() | ast.Set() | ast.Dict():
572
+ return _literal_collection_truth(expression)
573
+ case ast.Compare():
574
+ return _comparison_truth(expression)
408
575
  case ast.UnaryOp(op=ast.Not(), operand=operand):
409
576
  truth = _literal_truth(operand)
410
577
  return None if truth is None else not truth
@@ -426,21 +593,111 @@ def _literal_truth(expression: ast.expr) -> bool | None:
426
593
  return None
427
594
 
428
595
 
429
- def _borrowed_origin(value: ast.expr, origins: dict[str, _Borrowed]) -> _Borrowed | None:
596
+ def _comparison_truth(expression: ast.Compare) -> bool | None:
597
+ left = expression.left
598
+ for operation, right in zip(expression.ops, expression.comparators, strict=True):
599
+ truth = _literal_comparison(left, operation, right)
600
+ if truth is not True:
601
+ return truth
602
+ left = right
603
+ return True
604
+
605
+
606
+ def _literal_collection_truth(expression: ast.List | ast.Tuple | ast.Set | ast.Dict) -> bool | None:
607
+ if isinstance(expression, ast.Dict):
608
+ explicit_member = any(key is not None for key in expression.keys)
609
+ empty = all(_empty_literal_iterable(value) for value in expression.values)
610
+ else:
611
+ explicit_member = any(not isinstance(element, ast.Starred) for element in expression.elts)
612
+ empty = all(
613
+ isinstance(element, ast.Starred) and _empty_literal_iterable(element.value) for element in expression.elts
614
+ )
615
+ if explicit_member:
616
+ return True
617
+ return False if empty else None
618
+
619
+
620
+ def _literal_comparison(left: ast.expr, operation: ast.cmpop, right: ast.expr) -> bool | None:
621
+ if not isinstance(left, ast.Constant) or not isinstance(right, ast.Constant):
622
+ return None
623
+ first, second = left.value, right.value
624
+ match operation:
625
+ case ast.Eq():
626
+ return first == second
627
+ case ast.NotEq():
628
+ return first != second
629
+ case ast.Is() | ast.IsNot():
630
+ return _singleton_identity(first, second, negated=isinstance(operation, ast.IsNot))
631
+ case _:
632
+ pass
633
+ match first, second:
634
+ case (int() | float() as a, int() | float() as b):
635
+ return _scalar_order(a, operation, b)
636
+ case (str() as a, str() as b):
637
+ return _scalar_order(a, operation, b)
638
+ case (bytes() as a, bytes() as b):
639
+ return _scalar_order(a, operation, b)
640
+ case _:
641
+ return None
642
+
643
+
644
+ def _scalar_order[T: (int, float, str, bytes)](left: T, operation: ast.cmpop, right: T) -> bool | None:
645
+ match operation:
646
+ case ast.Lt():
647
+ return left < right
648
+ case ast.LtE():
649
+ return left <= right
650
+ case ast.Gt():
651
+ return left > right
652
+ case ast.GtE():
653
+ return left >= right
654
+ case _:
655
+ return None
656
+
657
+
658
+ def _singleton_identity(left: object, right: object, *, negated: bool) -> bool | None:
659
+ if not ((left is None or isinstance(left, bool)) and (right is None or isinstance(right, bool))):
660
+ return None
661
+ same = left is right
662
+ return not same if negated else same
663
+
664
+
665
+ def _borrowed_origin(
666
+ value: ast.expr, origins: dict[str, _Borrowed], identity_calls: Mapping[ast.Call, ast.expr]
667
+ ) -> _Borrowed | None:
668
+ while isinstance(value, ast.Call) and value in identity_calls:
669
+ value = identity_calls[value]
430
670
  match value:
431
671
  case ast.Name(id=name):
432
672
  return origins.get(name)
433
673
  case ast.IfExp(test=test, body=body, orelse=otherwise):
434
674
  truth = _literal_truth(test)
435
675
  if truth is not None:
436
- return _borrowed_origin(body if truth else otherwise, origins)
437
- first = _borrowed_origin(body, origins)
438
- second = _borrowed_origin(otherwise, origins)
676
+ return _borrowed_origin(body if truth else otherwise, origins, identity_calls)
677
+ first = _borrowed_origin(body, origins, identity_calls)
678
+ second = _borrowed_origin(otherwise, origins, identity_calls)
439
679
  return first if first is not None and first == second else None
680
+ case ast.BoolOp():
681
+ return _boolean_origin(value, origins, identity_calls)
440
682
  case _:
441
683
  return None
442
684
 
443
685
 
686
+ def _boolean_origin(
687
+ value: ast.BoolOp, origins: dict[str, _Borrowed], identity_calls: Mapping[ast.Call, ast.expr]
688
+ ) -> _Borrowed | None:
689
+ candidates: list[_Borrowed | None] = []
690
+ for index, operand in enumerate(value.values):
691
+ truth = _literal_truth(operand)
692
+ continues = isinstance(value.op, ast.And) if truth else isinstance(value.op, ast.Or)
693
+ if truth is None or not continues or index == len(value.values) - 1:
694
+ candidates.append(_borrowed_origin(operand, origins, identity_calls))
695
+ if truth is not None and not continues:
696
+ break
697
+ first, *rest = candidates
698
+ return first if first is not None and all(candidate == first for candidate in rest) else None
699
+
700
+
444
701
  def _in_place(kind: _Kind, operation: ast.operator) -> bool:
445
702
  return (
446
703
  (kind == "list" and isinstance(operation, ast.Add | ast.Mult))