sarj-python-lint 1.54.0__tar.gz → 1.56.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 (224) hide show
  1. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +434 -26
  4. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +1 -1
  5. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/.gitignore +0 -0
  6. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/LICENSE +0 -0
  7. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/README.md +0 -0
  8. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_source.py +0 -0
  17. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  20. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  29. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  31. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  32. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  33. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  34. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  35. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  36. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  37. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  38. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  39. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  40. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  41. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  42. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  43. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  44. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  45. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  46. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  47. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  48. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  49. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  50. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  51. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  52. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  53. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  54. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  55. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  56. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  57. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  58. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  59. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  60. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  61. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  62. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  63. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  64. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  65. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  66. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  67. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  68. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  69. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  70. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  71. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  72. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  73. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  74. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  75. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  76. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  77. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  78. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  79. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  80. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  81. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  82. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  83. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  84. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  85. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  86. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  87. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  88. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  89. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  90. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  91. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  92. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  93. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  94. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  95. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  96. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  97. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  98. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  99. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  100. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  101. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  102. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  103. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  104. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  105. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  106. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  107. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  108. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  109. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  110. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  111. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  112. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  113. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  114. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  115. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  116. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  117. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  118. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  119. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  120. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  121. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  122. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  123. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  124. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  125. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  126. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  127. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  128. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  129. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  130. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  131. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  132. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  133. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  134. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  135. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  136. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  137. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  138. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  139. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  140. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  141. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  142. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  143. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  144. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  145. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  146. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  147. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  148. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  149. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  150. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  151. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  152. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  153. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  154. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  155. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  156. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  157. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  158. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  159. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  160. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  161. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  162. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  163. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  164. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  165. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  166. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  167. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  168. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  169. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  170. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  171. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  172. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  173. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  174. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  175. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  176. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  177. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  178. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  179. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  180. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  181. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  182. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  183. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  184. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  185. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  186. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  187. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  188. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  189. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  190. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  191. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  192. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  193. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  194. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  195. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  196. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  197. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  198. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  199. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  200. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  201. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  202. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  203. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  204. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  205. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  206. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  207. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  208. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  209. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  210. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  211. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  212. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  213. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  214. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  215. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  216. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  217. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  218. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  219. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  220. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  221. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  222. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  223. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  224. {sarj_python_lint-1.54.0 → sarj_python_lint-1.56.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.54.0
3
+ Version: 1.56.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.54.0"
3
+ version = "1.56.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" }]
@@ -73,6 +73,7 @@ class _DataUnions:
73
73
  def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
74
74
  self.context = context
75
75
  self.provenance = ResourceProvenance(context, require_static_namespace=True)
76
+ self.provenance.mutated = _scoped_module_mutations(self.provenance, tree)
76
77
  bindings = self.provenance.bindings.get(tree, {})
77
78
  self.classes = {
78
79
  node.name: node
@@ -80,13 +81,29 @@ class _DataUnions:
80
81
  if isinstance(node, ast.ClassDef)
81
82
  and len(bindings.get(node.name, ())) == 1
82
83
  and node.name not in self.provenance.mutated
84
+ and node.name not in self.provenance.external_bindings
83
85
  }
84
86
  self.aliases: dict[str, ast.expr] = {}
85
87
  self.declarations: dict[str, ast.stmt] = {}
88
+ self.values: dict[str, ast.expr] = {}
89
+ self.metadata_aliases = _metadata_alias_graph(tree, bindings)
90
+ self.metadata_mutation_cache: dict[str, bool | None] = {}
91
+ self.metadata_writes = _metadata_attribute_writes(context, tree, self.provenance)
86
92
  for node in tree.body:
87
- value = _alias_value(node, context)
93
+ binding = _value_binding(node)
94
+ if binding is not None:
95
+ bound_name, bound_value = binding
96
+ if len(bindings.get(bound_name, ())) == 1:
97
+ self.values[bound_name] = bound_value
98
+ value = _alias_value(node, context, self.provenance)
88
99
  name = _alias_name(node)
89
- if value is not None and name is not None and len(bindings.get(name, ())) == 1:
100
+ if (
101
+ value is not None
102
+ and name is not None
103
+ and len(bindings.get(name, ())) == 1
104
+ and name not in self.provenance.external_bindings
105
+ and name not in self.provenance.mutated
106
+ ):
90
107
  self.aliases[name] = value
91
108
  self.declarations[name] = node
92
109
  self.records: dict[str, _Record | None] = {}
@@ -161,25 +178,199 @@ class _DataUnions:
161
178
  )
162
179
 
163
180
  def _discriminator_metadata(self, node: ast.expr) -> bool:
164
- if not isinstance(node, ast.Call) or not _untouched(node.func, self.provenance):
181
+ seen: frozenset[str] = frozenset()
182
+ for _ in range(_MAX_EXPANSION):
183
+ reference = self._metadata_reference(node, seen)
184
+ if isinstance(reference, bool):
185
+ return reference
186
+ if reference is not None:
187
+ node, seen = reference.node, reference.aliases
188
+ continue
189
+ if not isinstance(node, ast.Call):
190
+ return self._metadata_aliases_discriminate(seen, callable_metadata=False)
191
+ step = self._metadata_step(node)
192
+ if isinstance(step, bool):
193
+ return self._metadata_aliases_discriminate(seen, callable_metadata=step)
194
+ node = step
195
+ return True
196
+
197
+ def _metadata_reference(self, node: ast.expr, seen: frozenset[str]) -> _Expansion | bool | None:
198
+ if isinstance(node, ast.Name) and node.id in self.values:
199
+ return True if node.id in seen else _Expansion(self.values[node.id], seen | {node.id})
200
+ projected = self._metadata_projection(node)
201
+ if projected is None or isinstance(projected, bool):
202
+ return projected
203
+ if not projected.aliases.isdisjoint(seen) or any(
204
+ self._metadata_projection_escaped(name) for name in projected.aliases
205
+ ):
206
+ return True
207
+ return _Expansion(projected.node, seen | projected.aliases)
208
+
209
+ def _metadata_projection(self, node: ast.expr) -> _Expansion | bool | None:
210
+ if isinstance(node, ast.Subscript):
211
+ return self._metadata_tuple_item(node)
212
+ if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
213
+ return None
214
+ bindings = self.provenance.bindings.get(self.context.tree, {}).get(node.value.id, ())
215
+ if len(bindings) != 1 or not isinstance(bindings[0], ast.ClassDef):
216
+ return None
217
+ value = _namespace_metadata_value(bindings[0], node.attr)
218
+ return _Expansion(value, frozenset({node.value.id})) if value is not None else None
219
+
220
+ def _metadata_tuple_item(self, node: ast.Subscript) -> _Expansion | bool | None:
221
+ value = node.value
222
+ names: frozenset[str] = frozenset()
223
+ for _ in range(_MAX_EXPANSION):
224
+ if not isinstance(value, ast.Name) or value.id not in self.values:
225
+ break
226
+ if value.id in names:
227
+ return True
228
+ names |= {value.id}
229
+ value = self.values[value.id]
230
+ else:
231
+ return True
232
+ if not isinstance(value, ast.Tuple):
233
+ return None
234
+ if len(value.elts) > _MAX_EXPANSION:
235
+ return True
236
+ if any(isinstance(item, ast.Starred) for item in value.elts):
237
+ return None
238
+ index = _metadata_tuple_index(node.slice)
239
+ if index is None:
240
+ return True
241
+ if not -len(value.elts) <= index < len(value.elts):
242
+ return None
243
+ return _Expansion(value.elts[index], names)
244
+
245
+ def _metadata_projection_escaped(self, name: str) -> bool:
246
+ bindings = self.provenance.bindings.get(self.context.tree, {}).get(name, ())
247
+ reads = self.provenance.reads.get(name, ())
248
+ if len(bindings) != 1 or len(reads) > _MAX_EXPANSION:
249
+ return True
250
+ for read in reads:
251
+ if self.provenance.binding(name, read) is not bindings[0]:
252
+ continue
253
+ projection = self.context.parents.get(read)
254
+ if not isinstance(projection, ast.Attribute | ast.Subscript) or projection.value is not read:
255
+ return True
256
+ if not self._metadata_annotation_member(projection):
257
+ return True
258
+ return False
259
+
260
+ def _metadata_annotation_member(self, node: ast.expr) -> bool:
261
+ parent = self.context.parents.get(node)
262
+ if not isinstance(parent, ast.Tuple) or len(parent.elts) > _MAX_EXPANSION:
165
263
  return False
166
- if self.context.module_imports.resolves(node.func, sources=_DISCRIMINATOR_SOURCES, symbol="Discriminator"):
167
- discriminator = (
168
- node.args[0]
169
- if node.args
170
- else next((keyword.value for keyword in node.keywords if keyword.arg == "discriminator"), None)
171
- )
172
- return discriminator is not None and not (
173
- isinstance(discriminator, ast.Constant) and isinstance(discriminator.value, str)
174
- )
175
- return self.context.module_imports.resolves(node.func, sources=_FIELD_SOURCES, symbol="Field") and any(
176
- keyword.arg == "discriminator" and self._discriminator_metadata(keyword.value) for keyword in node.keywords
264
+ owner = self.context.parents.get(parent)
265
+ return (
266
+ isinstance(owner, ast.Subscript)
267
+ and owner.slice is parent
268
+ and self._typing_symbol(owner.value) == "Annotated"
269
+ and node in parent.elts[1:]
270
+ )
271
+
272
+ def _metadata_step(self, node: ast.Call) -> ast.expr | bool:
273
+ if not _untouched(node.func, self.provenance):
274
+ return False
275
+ discriminator = next((keyword.value for keyword in node.keywords if keyword.arg == "discriminator"), None)
276
+ if self._metadata_constructor(node.func, _DISCRIMINATOR_SOURCES, "Discriminator"):
277
+ argument = node.args[0] if node.args else discriminator
278
+ return argument is not None and not self._string_discriminator(argument)
279
+ if self._metadata_constructor(node.func, _FIELD_SOURCES, "Field") and discriminator is not None:
280
+ return discriminator
281
+ return False
282
+
283
+ def _metadata_aliases_discriminate(self, names: frozenset[str], *, callable_metadata: bool) -> bool:
284
+ mutations = [self._metadata_mutated(name) for name in names]
285
+ return any(mutation is None for mutation in mutations) or (callable_metadata and not any(mutations))
286
+
287
+ def _metadata_mutated(self, name: str) -> bool | None:
288
+ if name in self.metadata_mutation_cache:
289
+ return self.metadata_mutation_cache[name]
290
+ pending = [name]
291
+ seen: set[str] = set()
292
+ mutated = False
293
+ for _ in range(_MAX_EXPANSION):
294
+ if not pending:
295
+ self.metadata_mutation_cache.update(dict.fromkeys(seen, mutated))
296
+ return mutated
297
+ current = pending.pop()
298
+ if current in seen:
299
+ continue
300
+ seen.add(current)
301
+ if current in self.provenance.external_bindings or self._metadata_escaped(current):
302
+ return None
303
+ if current in self.provenance.mutated:
304
+ if not self._metadata_string_writes(current):
305
+ return None
306
+ mutated = True
307
+ pending.extend(self.metadata_aliases.get(current, set()).difference(seen))
308
+ return None
309
+
310
+ def _metadata_string_writes(self, name: str) -> bool:
311
+ writes = self.metadata_writes.get(name, ())
312
+ return (
313
+ bool(writes)
314
+ and len(writes) <= _MAX_EXPANSION
315
+ and all(attribute == "discriminator" and self._string_discriminator(value) for attribute, value in writes)
177
316
  )
178
317
 
318
+ def _metadata_escaped(self, name: str) -> bool:
319
+ reads = self.provenance.reads.get(name, ())
320
+ if len(reads) > _MAX_EXPANSION:
321
+ return True
322
+ bindings = self.provenance.bindings.get(self.context.tree, {}).get(name, ())
323
+ if len(bindings) != 1:
324
+ return True
325
+ for read in reads:
326
+ if self.provenance.binding(name, read) is not bindings[0]:
327
+ continue
328
+ node: ast.AST | None = read
329
+ for _ in range(_MAX_EXPANSION):
330
+ node = self.context.parents.get(node)
331
+ if isinstance(node, ast.Call):
332
+ return True
333
+ if node is None or isinstance(node, ast.stmt):
334
+ break
335
+ else:
336
+ return True
337
+ return False
338
+
339
+ def _metadata_constructor(self, node: ast.expr, sources: frozenset[str], symbol: str) -> bool:
340
+ imports = self.context.module_imports
341
+ return imports.resolves(node, sources=sources, symbol=symbol) or (
342
+ isinstance(node, ast.Attribute)
343
+ and node.attr == symbol
344
+ and imports.resolved_qualified_name(node.value) in sources
345
+ )
346
+
347
+ def _string_discriminator(self, node: ast.expr) -> bool:
348
+ pending = [_Expansion(node)]
349
+ for _ in range(_MAX_EXPANSION):
350
+ if not pending:
351
+ return True
352
+ item = pending.pop()
353
+ node = item.node
354
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
355
+ continue
356
+ if (
357
+ isinstance(node, ast.Name)
358
+ and node.id in self.values
359
+ and node.id not in item.aliases
360
+ and node.id not in self.provenance.mutated
361
+ and node.id not in self.provenance.external_bindings
362
+ ):
363
+ pending.append(_Expansion(self.values[node.id], item.aliases | {node.id}))
364
+ elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
365
+ pending.extend(_Expansion(child, item.aliases) for child in (node.left, node.right))
366
+ else:
367
+ return False
368
+ return False
369
+
179
370
  def _typing_symbol(self, node: ast.expr) -> str | None:
180
371
  if not _untouched(node, self.provenance):
181
372
  return None
182
- return self.context.imports.resolved_symbol(node, sources=TYPING_SOURCES)
373
+ return self.context.module_imports.resolved_symbol(node, sources=TYPING_SOURCES)
183
374
 
184
375
  def _variants(self, expression: ast.expr) -> set[str] | None:
185
376
  leaves = self._leaves(expression)
@@ -251,6 +442,8 @@ class _DataUnions:
251
442
  return uncertain
252
443
 
253
444
  def _field_domain(self, field: ast.AnnAssign, *, required: bool) -> _Domain:
445
+ if _direct_initvar_field(field, self.context, self.provenance):
446
+ return _Domain()
254
447
  node = field.annotation
255
448
  aliases: set[str] = set()
256
449
  for _ in range(_MAX_EXPANSION):
@@ -296,13 +489,17 @@ class _DataUnions:
296
489
  return _Domain(frozenset({_tag(None)}))
297
490
  if isinstance(node, ast.Name | ast.Attribute) and (
298
491
  (isinstance(node, ast.Name) and node.id in self.classes)
299
- or self.context.imports.resolved_qualified_name(node) is not None
492
+ or (
493
+ _untouched(node, self.provenance)
494
+ and self.context.module_imports.resolved_qualified_name(node) is not None
495
+ )
300
496
  ):
301
497
  return _Domain(unresolved=True)
302
498
  if (
303
499
  isinstance(node, ast.Name)
304
500
  and node.id in _NON_LITERAL_BUILTINS
305
- and self.context.imports.builtin_is_unshadowed(node.id)
501
+ and self.context.module_imports.builtin_is_unshadowed(node.id)
502
+ and _untouched(node, self.provenance)
306
503
  ):
307
504
  return _Domain()
308
505
  if not isinstance(node, ast.Subscript):
@@ -313,7 +510,8 @@ class _DataUnions:
313
510
  if symbol is not None or (
314
511
  isinstance(node.value, ast.Name)
315
512
  and node.value.id in {"list", "dict", "tuple", "set", "frozenset", "type"}
316
- and self.context.imports.builtin_is_unshadowed(node.value.id)
513
+ and self.context.module_imports.builtin_is_unshadowed(node.value.id)
514
+ and _untouched(node.value, self.provenance)
317
515
  ):
318
516
  return _Domain()
319
517
  return _Domain(opaque=True)
@@ -363,6 +561,26 @@ def _wrapped_annotation(node: ast.Subscript, symbol: str | None, *, required: bo
363
561
  return None
364
562
 
365
563
 
564
+ def _direct_initvar_field(field: ast.AnnAssign, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
565
+ if field.value is not None or not isinstance(field.annotation, ast.Subscript):
566
+ return False
567
+ target = field.annotation.value
568
+ if not _untouched(target, provenance) or not context.module_imports.resolves(
569
+ target, sources=_DATACLASS_SOURCES, symbol="InitVar"
570
+ ):
571
+ return False
572
+ owner = context.parents.get(field)
573
+ if not isinstance(owner, ast.ClassDef):
574
+ return False
575
+ for decorator in owner.decorator_list:
576
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
577
+ if _untouched(target, provenance) and context.module_imports.resolves(
578
+ target, sources=_DATACLASS_SOURCES, symbol="dataclass"
579
+ ):
580
+ return True
581
+ return False
582
+
583
+
366
584
  def _record(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> _Record | None:
367
585
  required = _record_required(node, context, provenance)
368
586
  if required is None:
@@ -381,9 +599,37 @@ def _record(node: ast.ClassDef, context: PythonFileContext, provenance: Resource
381
599
  continue
382
600
  else:
383
601
  return None
602
+ field_names = frozenset(fields)
603
+ if any(_class_annotation_shadowed(field.annotation, field_names, context) for field in fields.values()):
604
+ return None
384
605
  return _Record(fields, required)
385
606
 
386
607
 
608
+ def _class_annotation_shadowed(annotation: ast.expr, names: frozenset[str], context: PythonFileContext) -> bool:
609
+ pending = [(annotation, True)]
610
+ for _ in range(_MAX_EXPANSION):
611
+ if not pending:
612
+ return False
613
+ node, forward = pending.pop()
614
+ parsed = _string_expression(node) if forward else node
615
+ if parsed is None or (isinstance(parsed, ast.Name) and parsed.id in names):
616
+ return True
617
+ pending.extend(_class_annotation_children(parsed, forward=forward, context=context))
618
+ return True
619
+
620
+
621
+ def _class_annotation_children(
622
+ node: ast.expr, *, forward: bool, context: PythonFileContext
623
+ ) -> list[tuple[ast.expr, bool]]:
624
+ if not isinstance(node, ast.Subscript):
625
+ return [(child, forward) for child in ast.iter_child_nodes(node) if isinstance(child, ast.expr)]
626
+ members = _subscript_members(node)
627
+ symbol = context.module_imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
628
+ if symbol == "Annotated" and members:
629
+ return [(node.value, False), (members[0], True)]
630
+ return [(node.value, False), *((member, symbol != "Literal") for member in members)]
631
+
632
+
387
633
  def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool | None:
388
634
  if node.type_params:
389
635
  return None
@@ -394,16 +640,16 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
394
640
  for decorator in node.decorator_list
395
641
  if not (
396
642
  _untouched(decorator, provenance)
397
- and context.imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
643
+ and context.module_imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
398
644
  )
399
645
  ]
400
646
  if decorators:
401
647
  return _direct_dataclass(node, context, provenance, decorators)
402
648
  if len(node.bases) != 1:
403
649
  return None
404
- if context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
650
+ if context.module_imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
405
651
  return True if not node.keywords else None
406
- if not context.imports.resolves(node.bases[0], sources=TYPING_SOURCES, symbol="TypedDict"):
652
+ if not context.module_imports.resolves(node.bases[0], sources=TYPING_SOURCES, symbol="TypedDict"):
407
653
  return None
408
654
  required = True
409
655
  for keyword in node.keywords:
@@ -422,7 +668,7 @@ def _direct_dataclass(
422
668
  return None
423
669
  decorator = decorators[0]
424
670
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
425
- if not _untouched(target, provenance) or not context.imports.resolves(
671
+ if not _untouched(target, provenance) or not context.module_imports.resolves(
426
672
  target, sources=_DATACLASS_SOURCES, symbol="dataclass"
427
673
  ):
428
674
  return None
@@ -479,12 +725,91 @@ def _alias_name(node: ast.stmt) -> str | None:
479
725
  return None
480
726
 
481
727
 
482
- def _alias_value(node: ast.stmt, context: PythonFileContext) -> ast.expr | None:
728
+ def _namespace_metadata_value(node: ast.ClassDef, attribute: str) -> ast.expr | None:
729
+ if node.bases or node.keywords or node.decorator_list or node.type_params or len(node.body) > _MAX_EXPANSION:
730
+ return None
731
+ values: list[tuple[str, ast.expr]] = []
732
+ for member in node.body:
733
+ binding = _value_binding(member)
734
+ if binding is not None and not binding[0].startswith("__"):
735
+ values.append(binding)
736
+ elif isinstance(member, ast.Pass | ast.Expr) and (
737
+ isinstance(member, ast.Pass)
738
+ or (isinstance(member.value, ast.Constant) and isinstance(member.value.value, str))
739
+ ):
740
+ continue
741
+ else:
742
+ return None
743
+ if len(values) != 1 or values[0][0] != attribute:
744
+ return None
745
+ return values[0][1]
746
+
747
+
748
+ def _metadata_tuple_index(node: ast.expr) -> int | None:
749
+ if isinstance(node, ast.Constant) and type(node.value) is int:
750
+ return node.value
751
+ if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant) and type(node.operand.value) is int:
752
+ if isinstance(node.op, ast.USub):
753
+ return -node.operand.value
754
+ if isinstance(node.op, ast.UAdd):
755
+ return node.operand.value
756
+ return None
757
+
758
+
759
+ def _metadata_alias_graph(tree: ast.Module, bindings: dict[str, list[ast.AST]]) -> dict[str, set[str]]:
760
+ graph: dict[str, set[str]] = {}
761
+ for node in tree.body:
762
+ binding = _value_binding(node)
763
+ if binding is None:
764
+ continue
765
+ name, value = binding
766
+ if len(bindings.get(name, ())) != 1 or not isinstance(value, ast.Name):
767
+ continue
768
+ graph.setdefault(name, set()).add(value.id)
769
+ graph.setdefault(value.id, set()).add(name)
770
+ return graph
771
+
772
+
773
+ def _metadata_attribute_writes(
774
+ context: PythonFileContext, tree: ast.Module, provenance: ResourceProvenance
775
+ ) -> dict[str, list[tuple[str, ast.expr]]]:
776
+ writes: dict[str, list[tuple[str, ast.expr]]] = {}
777
+ for target in context.nodes(ast.Attribute):
778
+ if not isinstance(target.ctx, ast.Store) or not isinstance(target.value, ast.Name):
779
+ continue
780
+ binding = provenance.binding(target.value.id, target)
781
+ if binding is None or provenance.scope(binding) is not tree or provenance.scope(target) is not tree:
782
+ continue
783
+ parent = context.parents.get(target)
784
+ if isinstance(parent, ast.Assign | ast.AnnAssign) and parent.value is not None:
785
+ writes.setdefault(target.value.id, []).append((target.attr, parent.value))
786
+ return writes
787
+
788
+
789
+ def _value_binding(node: ast.stmt) -> tuple[str, ast.expr] | None:
790
+ match node:
791
+ case (
792
+ ast.Assign(targets=[ast.Name(id=name)], value=value)
793
+ | ast.AnnAssign(target=ast.Name(id=name), value=ast.expr() as value)
794
+ ):
795
+ return name, value
796
+ case _:
797
+ return None
798
+
799
+
800
+ def _alias_value(
801
+ node: ast.stmt, context: PythonFileContext, provenance: ResourceProvenance | None = None
802
+ ) -> ast.expr | None:
483
803
  match node:
484
804
  case ast.TypeAlias(type_params=[]):
485
805
  return node.value
486
806
  case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
487
- return value if context.imports.resolves(annotation, sources=TYPING_SOURCES, symbol="TypeAlias") else None
807
+ return (
808
+ value
809
+ if context.module_imports.resolves(annotation, sources=TYPING_SOURCES, symbol="TypeAlias")
810
+ and (provenance is None or _untouched(annotation, provenance))
811
+ else None
812
+ )
488
813
  case ast.Assign(targets=[ast.Name()], value=value):
489
814
  return None if isinstance(value, ast.Constant) and isinstance(value.value, str) else value
490
815
  case _:
@@ -500,10 +825,93 @@ def _string_expression(node: ast.expr) -> ast.expr | None:
500
825
  return None
501
826
 
502
827
 
828
+ def _scoped_module_mutations(provenance: ResourceProvenance, tree: ast.Module) -> set[str]:
829
+ original = provenance.mutated
830
+ if not original:
831
+ return original
832
+ bindings = provenance.bindings.get(tree, {})
833
+ owners = {name: bindings[name][0] for name in original if len(bindings.get(name, ())) == 1}
834
+ writes = _module_attribute_mutations(provenance, owners)
835
+ if writes is None:
836
+ return original
837
+ return {
838
+ name
839
+ for name in original
840
+ if name not in owners or name in writes or _module_binding_escaped(name, owners[name], provenance)
841
+ }
842
+
843
+
844
+ def _module_attribute_mutations(provenance: ResourceProvenance, owners: dict[str, ast.AST]) -> set[str] | None:
845
+ changed: set[str] = set()
846
+ count = 0
847
+ for target in provenance.context.nodes(ast.Attribute):
848
+ if not isinstance(target.ctx, ast.Store | ast.Del):
849
+ continue
850
+ count += 1
851
+ if count > _MAX_EXPANSION:
852
+ return None
853
+ root: ast.expr = target
854
+ while isinstance(root, ast.Attribute):
855
+ root = root.value
856
+ if not isinstance(root, ast.Name) or root.id not in owners:
857
+ continue
858
+ binding = provenance.binding(root.id, target)
859
+ if binding is None or binding is owners[root.id]:
860
+ changed.add(root.id)
861
+ return changed
862
+
863
+
864
+ def _module_binding_escaped(name: str, owner: ast.AST, provenance: ResourceProvenance) -> bool:
865
+ reads = provenance.reads.get(name, ())
866
+ if len(reads) > _MAX_EXPANSION:
867
+ return True
868
+ return any(
869
+ provenance.binding(name, read) is owner and _module_read_escapes(read, provenance.context) for read in reads
870
+ )
871
+
872
+
873
+ def _module_read_escapes(read: ast.Name, context: PythonFileContext) -> bool:
874
+ current: ast.AST = read
875
+ for _ in range(_MAX_EXPANSION):
876
+ parent = context.parents.get(current)
877
+ if parent is None:
878
+ return False
879
+ step = _module_escape_step(parent, current, context)
880
+ if step is not None:
881
+ return step
882
+ current = parent
883
+ return True
884
+
885
+
886
+ def _module_escape_step(parent: ast.AST, current: ast.AST, context: PythonFileContext) -> bool | None:
887
+ match parent:
888
+ case ast.Call():
889
+ return parent.func is not current
890
+ case ast.Return() | ast.Yield() | ast.YieldFrom() | ast.NamedExpr():
891
+ return True
892
+ case ast.arguments():
893
+ return True if current in parent.defaults or current in parent.kw_defaults else None
894
+ case ast.Assign() | ast.AnnAssign() | ast.AugAssign():
895
+ value = parent.value
896
+ return value is not None and value is current and not _union_expression(value, context)
897
+ case ast.Subscript() if parent.value is not current:
898
+ symbol = context.module_imports.resolved_symbol(parent.value, sources=TYPING_SOURCES)
899
+ if symbol is None:
900
+ return True
901
+ case ast.withitem():
902
+ return parent.context_expr is current
903
+ case _:
904
+ pass
905
+ return False if isinstance(parent, ast.stmt) else None
906
+
907
+
503
908
  def _untouched(node: ast.expr, provenance: ResourceProvenance) -> bool:
504
909
  while isinstance(node, ast.Attribute):
505
910
  node = node.value
506
- return isinstance(node, ast.Name) and node.id not in provenance.mutated
911
+ if not isinstance(node, ast.Name) or node.id in provenance.mutated or node.id in provenance.external_bindings:
912
+ return False
913
+ binding = provenance.binding(node.id, node)
914
+ return binding is None or isinstance(binding, ast.alias)
507
915
 
508
916
 
509
917
  def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
@@ -516,7 +924,7 @@ def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
516
924
  return True
517
925
  if not isinstance(node, ast.Subscript):
518
926
  return False
519
- symbol = context.imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
927
+ symbol = context.module_imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
520
928
  if symbol == "Union":
521
929
  return True
522
930
  if symbol == "Optional":
@@ -107,7 +107,7 @@ class PreferDiscriminatedModelUnion(Rule):
107
107
  limitations=(
108
108
  "Declared module-level unions of at least two source-visible direct dataclasses, Pydantic BaseModels or TypedDicts require a shared disjoint Literal field. PEP 604, typing Union, Optional, Annotated, forward references and bounded local aliases are supported; a None branch is allowed.",
109
109
  "The explicit-tag policy is a modeling preference: nominal variants already support isinstance narrowing. Scalar/container unions, inline annotations, generic or inherited variants, behavior classes, unresolved variants and dynamic or ambiguous namespaces are excluded. Imported tag aliases are not resolved; an explicit literal field paired with an unresolved imported or local class domain abstains, as do opaque explicit Literal domains. Qualified imported domains and unsupported computed aliases remain uncertain rather than proving a missing tag.",
110
- "String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Import-proven typing final decorators preserve record recognition. An explicit Pydantic callable Discriminator is respected; string field-name discriminators still need Literal fields. Other runtime configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
110
+ "String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Direct import-proven InitVar annotations without defaults are constructor inputs rather than instance tags; quoted, wrapped, locally aliased or defaulted InitVar forms remain uncertain. Import-proven typing final decorators preserve record recognition. Explicit Pydantic callable Discriminators, bounded stable reusable metadata, single-value plain local class namespaces, literal immutable tuple projections and imported pydantic.types constructors are respected; source-proven string field names, including local bindings and concatenation, still need Literal fields. Source-proven string discriminator writes through stable aliases invalidate callable coverage; opaque mutation, helper escapes and excessive alias graphs abstain. Module bindings are independent of unrelated local parameters and their attribute writes; class annotation shadows and global rebinding remain excluded. Other runtime configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
111
111
  "For nullable-field dependencies, only stable direct module-level import-proven Pydantic BaseModel subclasses with pure direct after model validators are inspected.",
112
112
  "Bounded paths may contain finite tag comparisons or literal-container membership, None identity tests, Boolean combinations, nested guards and returns of the same instance. Every rejection region must prove one tag-dependent nullable-field requirement; joint missing-field or independent-tag-axis conditions abstain.",
113
113
  "Tags must use imported Literal domains, optionally composed through direct unions, with at least two distinct homogeneous string, integer or Boolean values, optionally including an explicit None state. Mixed primitive families, bool/int equality collisions, open or computed tags abstain. Payloads must explicitly include None in a union, imported Union or Optional.",