sarj-python-lint 1.41.2__tar.gz → 1.42.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 (207) hide show
  1. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-1.42.0/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +340 -0
  5. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/.gitignore +0 -0
  6. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/LICENSE +0 -0
  7. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/README.md +0 -0
  8. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_source.py +0 -0
  17. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  20. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  29. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  31. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  32. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  33. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  34. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  35. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  36. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  37. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  38. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  39. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  40. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  41. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  42. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  43. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  44. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  45. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  46. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  47. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  48. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  49. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  50. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  51. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  52. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  53. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  54. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  55. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  56. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  57. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  58. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  59. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  60. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  61. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  62. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  63. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  64. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  65. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  66. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  67. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  68. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  69. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  70. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  71. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  72. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  73. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  74. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  75. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  76. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  77. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  78. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  79. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  80. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  81. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  82. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  83. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  84. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  85. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  86. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  87. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  88. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  89. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  90. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  91. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  92. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  93. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  94. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  95. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  96. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  97. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  98. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  99. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  100. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  101. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  102. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  103. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  104. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  105. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  106. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  107. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  108. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  109. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  110. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  111. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  112. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  113. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  114. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  115. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  116. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  117. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  118. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  119. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  120. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  121. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  122. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  123. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  124. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  125. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  126. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  127. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  128. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  129. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  130. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  131. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  132. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  133. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  134. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  135. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  136. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  137. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  138. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  139. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  140. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  141. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  142. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  143. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  144. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  145. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  146. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  147. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  148. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  149. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  150. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  151. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  152. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  153. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  154. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  155. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  156. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  157. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  158. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  159. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  160. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  161. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  162. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  163. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  164. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  165. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  166. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  167. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  168. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  169. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  170. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  171. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  172. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  173. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  174. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  175. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  176. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  177. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  178. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  179. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  180. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  181. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  182. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  183. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  184. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  185. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  186. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  187. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  188. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  189. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  190. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  191. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  192. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  193. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  194. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  195. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  196. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  197. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  198. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  199. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  200. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  201. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  202. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  203. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  204. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  205. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  206. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  207. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.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.41.2
3
+ Version: 1.42.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.41.2"
3
+ version = "1.42.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" }]
@@ -216,6 +216,7 @@ from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedP
216
216
  from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
217
217
  from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
218
218
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
219
+ from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
219
220
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
220
221
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
221
222
  from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
@@ -224,6 +225,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
224
225
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
225
226
  {
226
227
  NoExplicitTestOracle.id: NoExplicitTestOracle,
228
+ PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
227
229
  NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
228
230
  NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
229
231
  NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
@@ -0,0 +1,340 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ is_suppressed,
19
+ )
20
+ from sarj_python_lint.rules._paths import is_test_path
21
+ from sarj_python_lint.rules._pydantic_field_contract import (
22
+ DECORATOR_SOURCES,
23
+ TYPING_SOURCES,
24
+ direct_models,
25
+ unique_members,
26
+ )
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from sarj_python_lint._file_context import PythonFileContext
31
+
32
+
33
+ _PAIR_ARITY = 2
34
+ _BUILTIN_SOURCES = frozenset({"builtins"})
35
+ _INHABITED_SCALARS = frozenset({"str", "int", "float", "bool", "bytes", "complex"})
36
+ _EMPTY_CONTAINERS = frozenset({"list", "dict", "set", "frozenset", "tuple"})
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class _Requirement:
41
+ guard: ast.If
42
+ tag: str
43
+ value: str
44
+ payload: str
45
+
46
+
47
+ @final
48
+ class PreferDiscriminatedModelUnion(Rule):
49
+ id = "prefer-discriminated-model-union"
50
+ code = "SARJ510"
51
+ documentation = RuleDocumentation(
52
+ default_level=Severity.WARNING,
53
+ summary="Represent literal-tag-dependent required fields with discriminated model variants.",
54
+ rationale=(
55
+ "A nullable field remains statically nullable even when an after validator requires it for a literal "
56
+ "tag. A union of models with distinct Literal tags carries that dependency in the type and supports "
57
+ "safe narrowing without repeating runtime guards."
58
+ ),
59
+ remediation=(
60
+ "Consider a discriminated union of model variants. Make the payload non-nullable only in variants "
61
+ "proven to reject None, and preserve every other accepted field combination, defaults, aliases and wire shape. "
62
+ "Independent optional fields can remain optional within each variant."
63
+ ),
64
+ category=RuleCategory.ARCHITECTURE,
65
+ autofix=AutofixPolicy.NONE,
66
+ limitations=(
67
+ "Only direct module-level import-proven Pydantic BaseModel subclasses with one direct after model validator are inspected.",
68
+ "The validator must contain only direct guards and a final return of its instance; each guard must reject a two-part conjunction of literal-tag equality and nullable-field identity with None, using a literal exception message or a local assignment of that message.",
69
+ "Tags must use an imported Literal of at least two distinct strings; payloads must explicitly include None in a union or imported Optional.",
70
+ "Every non-null payload member must have a source-proven inhabited built-in scalar or container type; opaque aliases, custom types and None-only or bottom-type unions are excluded.",
71
+ "A field required by every admitted tag value is a common field constraint rather than evidence for a discriminated union and is excluded.",
72
+ "Other validators, inherited models, computed tags, helper validation, model mutation, ambiguous namespaces, tests and generated files are excluded.",
73
+ "This warning requests domain modeling review; it does not require absent payloads in other variants or change external compatibility automatically.",
74
+ ),
75
+ examples=(
76
+ RuleExample(
77
+ example_id="literal-tag-dependent-payload",
78
+ title="An after validator encodes a field dependency that its type cannot express",
79
+ outcome=ExampleOutcome.MATCH,
80
+ files=(
81
+ ExampleFile.python(
82
+ "app/models.py",
83
+ "from typing import Literal, Self\nfrom pydantic import BaseModel, model_validator\n"
84
+ "class Delivery(BaseModel):\n kind: Literal['email', 'pickup']\n address: str | None = None\n"
85
+ " @model_validator(mode='after')\n def validate_address(self) -> Self:\n"
86
+ " if self.kind == 'email' and self.address is None:\n"
87
+ " raise ValueError('Email requires an address')\n return self\n",
88
+ ),
89
+ ),
90
+ focus_path=PurePosixPath("app/models.py"),
91
+ expected_count=1,
92
+ public=True,
93
+ ),
94
+ RuleExample(
95
+ example_id="variants-preserve-other-accepted-states",
96
+ title="Only the email variant requires an address; pickup preserves its optional address",
97
+ outcome=ExampleOutcome.NO_MATCH,
98
+ files=(
99
+ ExampleFile.python(
100
+ "app/models.py",
101
+ "from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\n"
102
+ "class EmailDelivery(BaseModel):\n kind: Literal['email']\n address: str\n"
103
+ "class PickupDelivery(BaseModel):\n kind: Literal['pickup']\n address: str | None = None\n"
104
+ "type Delivery = Annotated[EmailDelivery | PickupDelivery, Field(discriminator='kind')]\n",
105
+ ),
106
+ ),
107
+ focus_path=PurePosixPath("app/models.py"),
108
+ expected_count=0,
109
+ public=True,
110
+ ),
111
+ ),
112
+ )
113
+ description = documentation.summary
114
+
115
+ @override
116
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
117
+ if (
118
+ is_test_path(context.path)
119
+ or "after" not in context.source
120
+ or "None" not in context.symbol_source
121
+ or "model_validator" not in context.symbol_source
122
+ ):
123
+ return []
124
+ diagnostics: list[Diagnostic] = []
125
+ for model in direct_models(context, methods=True):
126
+ requirements = _model_requirements(model, context)
127
+ if not requirements:
128
+ continue
129
+ first = requirements[0]
130
+ if is_suppressed(context.source_lines, first.guard.lineno, self.code):
131
+ continue
132
+ diagnostics.append(
133
+ Diagnostic(
134
+ context.path,
135
+ first.guard.lineno,
136
+ first.guard.col_offset + 1,
137
+ self.code,
138
+ f"`{model.name}` requires nullable `{first.payload}` when `{first.tag}` is {first.value!r}. "
139
+ "Consider a discriminated model union with that payload non-nullable in the matching variant; "
140
+ "preserve every other accepted combination and the external contract.",
141
+ severity=Severity.WARNING,
142
+ )
143
+ )
144
+ return diagnostics
145
+
146
+
147
+ def _model_requirements(model: ast.ClassDef, context: PythonFileContext) -> list[_Requirement]:
148
+ methods = [member for member in model.body if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef))]
149
+ if len(methods) != 1 or not isinstance(methods[0], ast.FunctionDef):
150
+ return []
151
+ validator = methods[0]
152
+ instance = _validator_instance(validator, context)
153
+ if instance is None:
154
+ return []
155
+ unique = unique_members(model)
156
+ fields = {
157
+ member.target.id: member.annotation
158
+ for member in model.body
159
+ if isinstance(member, ast.AnnAssign)
160
+ and isinstance(member.target, ast.Name)
161
+ and member.target.id in unique
162
+ and not member.target.id.startswith("_")
163
+ }
164
+ body = validator.body
165
+ if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant):
166
+ body = body[1:]
167
+ if not body or not isinstance(body[-1], ast.Return) or not _is_instance(body[-1].value, instance):
168
+ return []
169
+ requirements: list[_Requirement] = []
170
+ for statement in body[:-1]:
171
+ requirement = _guard_requirement(statement, fields, instance, context)
172
+ if requirement is None:
173
+ return []
174
+ requirements.append(requirement)
175
+ return _variant_specific_requirements(requirements, fields, context)
176
+
177
+
178
+ def _variant_specific_requirements(
179
+ requirements: list[_Requirement], fields: dict[str, ast.expr], context: PythonFileContext
180
+ ) -> list[_Requirement]:
181
+ required_tags: dict[tuple[str, str], set[str]] = {}
182
+ for requirement in requirements:
183
+ required_tags.setdefault((requirement.tag, requirement.payload), set()).add(requirement.value)
184
+ return [
185
+ requirement
186
+ for requirement in requirements
187
+ if required_tags[requirement.tag, requirement.payload] < _literal_strings(fields[requirement.tag], context)
188
+ ]
189
+
190
+
191
+ def _validator_instance(validator: ast.FunctionDef, context: PythonFileContext) -> str | None:
192
+ arguments = validator.args
193
+ positional = [*arguments.posonlyargs, *arguments.args]
194
+ if len(positional) != 1 or len(validator.decorator_list) != 1:
195
+ return None
196
+ if arguments.vararg is not None or arguments.kwarg is not None or arguments.kwonlyargs or arguments.defaults:
197
+ return None
198
+ decorator = validator.decorator_list[0]
199
+ match decorator:
200
+ case ast.Call(args=[], keywords=[ast.keyword(arg="mode", value=ast.Constant(value="after"))]):
201
+ if context.imports.resolves(decorator.func, sources=DECORATOR_SOURCES, symbol="model_validator"):
202
+ return positional[0].arg
203
+ case _:
204
+ pass
205
+ return None
206
+
207
+
208
+ def _guard_requirement(
209
+ statement: ast.stmt, fields: dict[str, ast.expr], instance: str, context: PythonFileContext
210
+ ) -> _Requirement | None:
211
+ if not isinstance(statement, ast.If) or statement.orelse or not _rejection_body(statement.body, context):
212
+ return None
213
+ if (
214
+ not isinstance(statement.test, ast.BoolOp)
215
+ or not isinstance(statement.test.op, ast.And)
216
+ or len(statement.test.values) != _PAIR_ARITY
217
+ ):
218
+ return None
219
+ for tag_test, payload_test in (statement.test.values, list(reversed(statement.test.values))):
220
+ tagged = _tag_test(tag_test, instance)
221
+ payload = _none_test(payload_test, instance)
222
+ if tagged is None or payload is None or payload not in fields or tagged[0] not in fields:
223
+ continue
224
+ tag, value = tagged
225
+ if tag != payload and value in _literal_strings(fields[tag], context) and _nullable(fields[payload], context):
226
+ return _Requirement(statement, tag, value, payload)
227
+ return None
228
+
229
+
230
+ def _tag_test(node: ast.expr, instance: str) -> tuple[str, str] | None:
231
+ if not isinstance(node, ast.Compare) or len(node.ops) != 1 or not isinstance(node.ops[0], ast.Eq):
232
+ return None
233
+ for attribute, literal in ((node.left, node.comparators[0]), (node.comparators[0], node.left)):
234
+ field = _instance_field(attribute, instance)
235
+ if field is not None and isinstance(literal, ast.Constant) and isinstance(literal.value, str):
236
+ return field, literal.value
237
+ return None
238
+
239
+
240
+ def _none_test(node: ast.expr, instance: str) -> str | None:
241
+ if not isinstance(node, ast.Compare) or len(node.ops) != 1 or not isinstance(node.ops[0], ast.Is):
242
+ return None
243
+ for attribute, literal in ((node.left, node.comparators[0]), (node.comparators[0], node.left)):
244
+ if isinstance(literal, ast.Constant) and literal.value is None:
245
+ return _instance_field(attribute, instance)
246
+ return None
247
+
248
+
249
+ def _instance_field(node: ast.expr, instance: str) -> str | None:
250
+ return node.attr if isinstance(node, ast.Attribute) and _is_instance(node.value, instance) else None
251
+
252
+
253
+ def _is_instance(node: ast.expr | None, instance: str) -> bool:
254
+ return isinstance(node, ast.Name) and node.id == instance
255
+
256
+
257
+ def _literal_strings(annotation: ast.expr, context: PythonFileContext) -> frozenset[str]:
258
+ if not isinstance(annotation, ast.Subscript) or not context.imports.resolves(
259
+ annotation.value, sources=TYPING_SOURCES, symbol="Literal"
260
+ ):
261
+ return frozenset()
262
+ values = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
263
+ strings = frozenset(
264
+ value.value for value in values if isinstance(value, ast.Constant) and isinstance(value.value, str)
265
+ )
266
+ return strings if len(strings) > 1 and len(strings) == len(values) else frozenset()
267
+
268
+
269
+ def _nullable(annotation: ast.expr, context: PythonFileContext) -> bool:
270
+ members = _union_members(annotation, context)
271
+ non_null = [member for member in members if not _is_none(member)]
272
+ return (
273
+ len(non_null) < len(members)
274
+ and bool(non_null)
275
+ and all(_non_none_witness(member, context) for member in non_null)
276
+ )
277
+
278
+
279
+ def _union_members(annotation: ast.expr, context: PythonFileContext) -> list[ast.expr]:
280
+ if isinstance(annotation, ast.Subscript) and context.imports.resolves(
281
+ annotation.value, sources=TYPING_SOURCES, symbol="Optional"
282
+ ):
283
+ if isinstance(annotation.slice, ast.Tuple):
284
+ return [annotation]
285
+ return [ast.Constant(value=None), *_union_members(annotation.slice, context)]
286
+ if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
287
+ return [*_union_members(annotation.left, context), *_union_members(annotation.right, context)]
288
+ return [annotation]
289
+
290
+
291
+ def _non_none_witness(annotation: ast.expr, context: PythonFileContext) -> bool:
292
+ if isinstance(annotation, ast.Subscript):
293
+ builtin = _builtin_type(annotation.value, context)
294
+ if builtin not in _EMPTY_CONTAINERS:
295
+ return False
296
+ if builtin != "tuple":
297
+ return True
298
+ arguments = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
299
+ return not arguments or (
300
+ len(arguments) == _PAIR_ARITY
301
+ and isinstance(arguments[-1], ast.Constant)
302
+ and arguments[-1].value is Ellipsis
303
+ )
304
+ return _builtin_type(annotation, context) in _INHABITED_SCALARS | _EMPTY_CONTAINERS
305
+
306
+
307
+ def _builtin_type(annotation: ast.expr, context: PythonFileContext) -> str | None:
308
+ if isinstance(annotation, ast.Name) and context.imports.builtin_is_unshadowed(annotation.id):
309
+ return annotation.id
310
+ return context.imports.resolved_symbol(annotation, sources=_BUILTIN_SOURCES)
311
+
312
+
313
+ def _is_none(node: ast.expr) -> bool:
314
+ return isinstance(node, ast.Constant) and node.value is None
315
+
316
+
317
+ def _rejection_body(body: list[ast.stmt], context: PythonFileContext) -> bool:
318
+ if len(body) == 1:
319
+ return _rejection(body[0], context)
320
+ match body:
321
+ case [ast.Assign(targets=[ast.Name(id=message)], value=ast.Constant(value=str())), rejection]:
322
+ return _rejection(rejection, context, message=message)
323
+ case _:
324
+ return False
325
+
326
+
327
+ def _rejection(statement: ast.stmt, context: PythonFileContext, *, message: str | None = None) -> bool:
328
+ return (
329
+ isinstance(statement, ast.Raise)
330
+ and statement.cause is None
331
+ and isinstance(statement.exc, ast.Call)
332
+ and isinstance(statement.exc.func, ast.Name)
333
+ and statement.exc.func.id in {"ValueError", "AssertionError"}
334
+ and context.imports.builtin_is_unshadowed(statement.exc.func.id)
335
+ and not statement.exc.keywords
336
+ and all(
337
+ isinstance(argument, ast.Constant) or (message is not None and _is_instance(argument, message))
338
+ for argument in statement.exc.args
339
+ )
340
+ )