sarj-python-lint 1.52.0__tar.gz → 1.54.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.52.0 → sarj_python_lint-1.54.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__main__.py +5 -0
  4. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +168 -44
  5. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  6. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +2 -2
  7. sarj_python_lint-1.54.0/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +467 -0
  8. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/.gitignore +0 -0
  9. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/LICENSE +0 -0
  10. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/README.md +0 -0
  11. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__init__.py +0 -0
  12. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_source.py +0 -0
  19. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  22. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/json_boundary.py +0 -0
  23. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  28. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  30. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  31. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  33. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  34. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  35. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  36. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  37. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  38. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  39. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  40. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  41. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  42. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  43. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  44. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  46. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  47. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  48. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  49. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  50. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  51. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  52. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  54. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  55. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  56. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  59. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  60. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  61. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  62. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  63. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  64. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  65. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  66. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  67. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  68. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  69. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  70. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  71. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  72. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  73. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  74. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  75. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  76. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  77. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  78. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  79. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  80. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  81. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  82. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  83. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  84. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  85. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  86. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  87. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  88. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  89. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  90. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  91. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  92. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  93. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  94. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  95. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  96. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  97. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  98. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  99. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  100. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  101. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  102. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  103. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  104. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  105. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  106. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  107. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  108. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  109. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  110. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  111. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  112. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  113. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  114. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  115. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  116. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  117. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  118. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  119. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  120. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  121. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  122. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  123. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  124. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  125. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  126. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  127. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  128. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  129. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  130. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  131. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  132. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  133. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  134. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  135. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  136. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  137. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  138. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  139. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  140. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  141. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  142. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  143. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  144. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  145. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  146. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  147. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  148. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  149. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  150. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  151. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  152. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  153. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  154. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  155. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  156. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  157. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  158. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  159. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  160. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  161. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  162. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  163. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  164. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  165. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  166. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  167. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  168. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  169. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  170. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  171. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  172. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  173. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  174. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  175. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  176. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  177. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  178. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  179. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  180. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  181. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  182. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  183. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  184. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  185. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  186. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  187. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  188. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  189. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  190. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  191. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  192. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  193. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  194. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  195. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  196. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  197. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  198. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  199. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  200. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  201. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  202. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  203. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  204. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  205. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  206. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  207. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  208. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  209. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  210. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  211. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  212. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  213. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  214. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  215. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  216. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  217. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  218. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  219. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  220. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  221. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  222. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  223. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  224. {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.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.52.0
3
+ Version: 1.54.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.52.0"
3
+ version = "1.54.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" }]
@@ -171,10 +171,15 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
171
171
  "SARJ094": frozenset({"SARJ008", "SARJ447"}),
172
172
  "SARJ095": frozenset({"SARJ468"}),
173
173
  "SARJ099": frozenset({"SARJ420"}),
174
+ # A proven external or nested request boundary already requires schema
175
+ # validation at this field read. Retain standalone shape advice when
176
+ # that more specific rule is unselected or locally suppressed.
177
+ "SARJ411": frozenset({"SARJ521"}),
174
178
  # Any-valued mappings are the stronger correctness failure. SARJ008
175
179
  # still owns every fixed dictionary return when selected alone, while
176
180
  # an all-rules run emits one actionable diagnostic for this overlap.
177
181
  "SARJ447": frozenset({"SARJ008"}),
182
+ "SARJ450": frozenset({"SARJ521"}),
178
183
  "SARJ457": frozenset({"SARJ045"}),
179
184
  "SARJ465": frozenset({"SARJ071"}),
180
185
  "SARJ491": frozenset({"SARJ400"}),
@@ -17,8 +17,13 @@ if TYPE_CHECKING:
17
17
  _DATACLASS_SOURCES = frozenset({"dataclasses"})
18
18
  _MAX_EXPANSION = 128
19
19
  _MIN_VARIANTS = 2
20
+ _DISCRIMINATOR_SOURCES = frozenset({"pydantic", "pydantic.types"})
21
+ _FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields"})
22
+ _NON_LITERAL_BUILTINS = frozenset(
23
+ {"str", "bytes", "int", "bool", "float", "complex", "object", "list", "dict", "tuple", "set", "frozenset", "type"}
24
+ )
20
25
 
21
- type _Tag = str | bytes | int | None
26
+ type _Tag = tuple[str, str | bytes | int | None]
22
27
 
23
28
 
24
29
  @dataclass(frozen=True, slots=True)
@@ -107,7 +112,7 @@ class _DataUnions:
107
112
  return findings
108
113
 
109
114
  def _untagged_union(self, value: ast.expr) -> bool:
110
- if not _union_expression(value, self.context):
115
+ if not _union_expression(value, self.context) or self._explicit_discriminator(value):
111
116
  return False
112
117
  variants = self._variants(value)
113
118
  if variants is None or len(variants) < _MIN_VARIANTS:
@@ -120,6 +125,57 @@ class _DataUnions:
120
125
  records.append(record)
121
126
  return not self._has_discriminator(records, _Budget())
122
127
 
128
+ def _explicit_discriminator(self, node: ast.expr) -> bool:
129
+ pending = [_Expansion(node)]
130
+ covered = False
131
+ for _ in range(_MAX_EXPANSION):
132
+ if not pending:
133
+ return covered
134
+ item = pending.pop()
135
+ parsed = _string_expression(item.node)
136
+ if parsed is None:
137
+ return True
138
+ node = parsed
139
+ if isinstance(node, ast.Name) and node.id in self.aliases:
140
+ if node.id in item.aliases:
141
+ return True
142
+ pending.append(_Expansion(self.aliases[node.id], item.aliases | {node.id}))
143
+ continue
144
+ if isinstance(node, ast.Constant) and node.value is None:
145
+ continue
146
+ if self._discriminated_annotation(node):
147
+ covered = True
148
+ continue
149
+ children = self._union_members(node)
150
+ if children is None:
151
+ return False
152
+ pending.extend(_Expansion(child, item.aliases) for child in children)
153
+ return True
154
+
155
+ def _discriminated_annotation(self, node: ast.expr) -> bool:
156
+ return (
157
+ isinstance(node, ast.Subscript)
158
+ and self._typing_symbol(node.value) == "Annotated"
159
+ and isinstance(node.slice, ast.Tuple)
160
+ and any(self._discriminator_metadata(item) for item in node.slice.elts[1:])
161
+ )
162
+
163
+ def _discriminator_metadata(self, node: ast.expr) -> bool:
164
+ if not isinstance(node, ast.Call) or not _untouched(node.func, self.provenance):
165
+ 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
177
+ )
178
+
123
179
  def _typing_symbol(self, node: ast.expr) -> str | None:
124
180
  if not _untouched(node, self.provenance):
125
181
  return None
@@ -184,7 +240,7 @@ class _DataUnions:
184
240
  return True
185
241
  budget.remaining -= 1
186
242
  common = set(records[0].fields).intersection(*(record.fields for record in records[1:]))
187
- common.difference_update({name for name in common if name.startswith("__")})
243
+ common.difference_update({name for name in common if name.startswith("__") and not name.endswith("__")})
188
244
  uncertain = False
189
245
  for name in sorted(common):
190
246
  domains = [self._field_domain(record.fields[name], required=record.required) for record in records]
@@ -196,27 +252,29 @@ class _DataUnions:
196
252
 
197
253
  def _field_domain(self, field: ast.AnnAssign, *, required: bool) -> _Domain:
198
254
  node = field.annotation
255
+ aliases: set[str] = set()
199
256
  for _ in range(_MAX_EXPANSION):
200
257
  parsed = _string_expression(node)
201
258
  if parsed is None:
202
259
  return _Domain(opaque=True)
203
260
  node = parsed
261
+ if isinstance(node, ast.Name) and node.id in self.aliases:
262
+ if node.id in aliases:
263
+ return _Domain(opaque=True)
264
+ aliases.add(node.id)
265
+ node = self.aliases[node.id]
266
+ continue
204
267
  if not isinstance(node, ast.Subscript):
205
268
  break
206
269
  symbol = self._typing_symbol(node.value)
207
- if symbol in {"Required", "NotRequired"}:
208
- required = symbol == "Required"
209
- node = node.slice
210
- elif symbol == "ClassVar":
211
- if field.value is None:
212
- return _Domain()
213
- node = node.slice
214
- elif symbol == "ReadOnly":
215
- node = node.slice
216
- elif symbol == "Annotated" and isinstance(node.slice, ast.Tuple) and node.slice.elts:
217
- node = node.slice.elts[0]
218
- else:
270
+ if symbol == "ClassVar" and field.value is None:
271
+ return _Domain()
272
+ wrapped = _wrapped_annotation(node, symbol, required=required)
273
+ if wrapped is None:
219
274
  break
275
+ node, required = wrapped
276
+ else:
277
+ return _Domain(opaque=True)
220
278
  return self._literal_domain(node) if required else _Domain()
221
279
 
222
280
  def _literal_domain(self, expression: ast.expr) -> _Domain:
@@ -226,24 +284,32 @@ class _DataUnions:
226
284
  domains = [self._leaf_domain(node) for node in leaves]
227
285
  if any(domain.opaque for domain in domains):
228
286
  return _Domain(opaque=True)
287
+ values = frozenset(value for domain in domains for value in domain.values)
229
288
  if any(domain.unresolved for domain in domains):
230
- return _Domain(unresolved=True)
289
+ return _Domain(values, unresolved=True)
231
290
  if any(not domain.values for domain in domains):
232
291
  return _Domain()
233
- return _Domain(frozenset(value for domain in domains for value in domain.values))
292
+ return _Domain(values)
234
293
 
235
294
  def _leaf_domain(self, node: ast.expr) -> _Domain:
236
295
  if isinstance(node, ast.Constant) and node.value is None:
237
- return _Domain(frozenset({None}))
238
- if isinstance(node, ast.Name) and (
239
- node.id in self.classes or self.context.imports.resolved_qualified_name(node) is not None
296
+ return _Domain(frozenset({_tag(None)}))
297
+ if isinstance(node, ast.Name | ast.Attribute) and (
298
+ (isinstance(node, ast.Name) and node.id in self.classes)
299
+ or self.context.imports.resolved_qualified_name(node) is not None
240
300
  ):
241
301
  return _Domain(unresolved=True)
242
- if not isinstance(node, ast.Subscript):
302
+ if (
303
+ isinstance(node, ast.Name)
304
+ and node.id in _NON_LITERAL_BUILTINS
305
+ and self.context.imports.builtin_is_unshadowed(node.id)
306
+ ):
243
307
  return _Domain()
308
+ if not isinstance(node, ast.Subscript):
309
+ return _Domain(opaque=True)
244
310
  symbol = self._typing_symbol(node.value)
245
311
  if symbol == "Literal":
246
- return _literal_values(node)
312
+ return self._literal_values(node)
247
313
  if symbol is not None or (
248
314
  isinstance(node.value, ast.Name)
249
315
  and node.value.id in {"list", "dict", "tuple", "set", "frozenset", "type"}
@@ -252,6 +318,50 @@ class _DataUnions:
252
318
  return _Domain()
253
319
  return _Domain(opaque=True)
254
320
 
321
+ def _literal_values(self, node: ast.Subscript) -> _Domain:
322
+ values: set[_Tag] = set()
323
+ pending = [_Expansion(member) for member in _subscript_members(node)]
324
+ for _ in range(_MAX_EXPANSION):
325
+ if not pending:
326
+ return _Domain(frozenset(values))
327
+ piece = self._literal_piece(pending.pop())
328
+ if isinstance(piece, list):
329
+ pending.extend(piece)
330
+ continue
331
+ if piece.opaque:
332
+ return piece
333
+ values.update(piece.values)
334
+ return _Domain(opaque=True)
335
+
336
+ def _literal_piece(self, item: _Expansion) -> _Domain | list[_Expansion]:
337
+ member = item.node
338
+ if isinstance(member, ast.Name) and member.id in self.aliases:
339
+ if member.id in item.aliases:
340
+ return _Domain(opaque=True)
341
+ parsed = _string_expression(self.aliases[member.id])
342
+ if parsed is None:
343
+ return _Domain(opaque=True)
344
+ return [_Expansion(parsed, item.aliases | {member.id})]
345
+ if isinstance(member, ast.Subscript):
346
+ children = (
347
+ _subscript_members(member)
348
+ if self._typing_symbol(member.value) == "Literal"
349
+ else self._union_members(member)
350
+ )
351
+ if children is not None:
352
+ return [_Expansion(child, item.aliases) for child in children]
353
+ return _literal_value(member)
354
+
355
+
356
+ def _wrapped_annotation(node: ast.Subscript, symbol: str | None, *, required: bool) -> tuple[ast.expr, bool] | None:
357
+ if symbol in {"Required", "NotRequired"}:
358
+ return node.slice, symbol == "Required"
359
+ if symbol in {"ClassVar", "ReadOnly", "Final"}:
360
+ return node.slice, required
361
+ if symbol == "Annotated" and isinstance(node.slice, ast.Tuple) and node.slice.elts:
362
+ return node.slice.elts[0], required
363
+ return None
364
+
255
365
 
256
366
  def _record(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> _Record | None:
257
367
  required = _record_required(node, context, provenance)
@@ -279,8 +389,16 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
279
389
  return None
280
390
  if any(not _untouched(base, provenance) for base in node.bases):
281
391
  return None
282
- if node.decorator_list:
283
- return _direct_dataclass(node, context, provenance)
392
+ decorators = [
393
+ decorator
394
+ for decorator in node.decorator_list
395
+ if not (
396
+ _untouched(decorator, provenance)
397
+ and context.imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
398
+ )
399
+ ]
400
+ if decorators:
401
+ return _direct_dataclass(node, context, provenance, decorators)
284
402
  if len(node.bases) != 1:
285
403
  return None
286
404
  if context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
@@ -297,10 +415,12 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
297
415
  return required
298
416
 
299
417
 
300
- def _direct_dataclass(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool | None:
301
- if node.bases or node.keywords or len(node.decorator_list) != 1:
418
+ def _direct_dataclass(
419
+ node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance, decorators: list[ast.expr]
420
+ ) -> bool | None:
421
+ if node.bases or node.keywords or len(decorators) != 1:
302
422
  return None
303
- decorator = node.decorator_list[0]
423
+ decorator = decorators[0]
304
424
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
305
425
  if not _untouched(target, provenance) or not context.imports.resolves(
306
426
  target, sources=_DATACLASS_SOURCES, symbol="dataclass"
@@ -309,26 +429,30 @@ def _direct_dataclass(node: ast.ClassDef, context: PythonFileContext, provenance
309
429
  return True
310
430
 
311
431
 
312
- def _literal_values(node: ast.Subscript) -> _Domain:
313
- values: set[_Tag] = set()
314
- members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
315
- for member in members:
316
- match member:
317
- case ast.Constant(value=(str() | bytes() | int()) as value):
318
- values.add(value)
319
- case ast.Constant(value=None):
320
- values.add(None)
321
- case ast.UnaryOp(op=op, operand=ast.Constant(value=int() as number)) if (
322
- isinstance(op, ast.USub | ast.UAdd) and type(number) is int
323
- ):
324
- values.add(-number if isinstance(op, ast.USub) else number)
325
- case _:
326
- return _Domain(opaque=True)
327
- return _Domain(frozenset(values))
432
+ def _literal_value(node: ast.expr) -> _Domain:
433
+ match node:
434
+ case ast.Constant(value=(str() | bytes() | int()) as value):
435
+ return _Domain(frozenset({_tag(value)}))
436
+ case ast.Constant(value=None):
437
+ return _Domain(frozenset({_tag(None)}))
438
+ case ast.UnaryOp(op=op, operand=ast.Constant(value=int() as number)) if (
439
+ isinstance(op, ast.USub | ast.UAdd) and type(number) is int
440
+ ):
441
+ return _Domain(frozenset({_tag(-number if isinstance(op, ast.USub) else number)}))
442
+ case _:
443
+ return _Domain(opaque=True)
444
+
445
+
446
+ def _subscript_members(node: ast.Subscript) -> tuple[ast.expr, ...]:
447
+ return tuple(node.slice.elts) if isinstance(node.slice, ast.Tuple) else (node.slice,)
448
+
449
+
450
+ def _tag(value: str | bytes | int | None) -> _Tag:
451
+ return type(value).__name__, value
328
452
 
329
453
 
330
454
  def _tag_groups(records: list[_Record], domains: list[_Domain]) -> list[list[_Record]]:
331
- if any(not domain.values or domain.opaque for domain in domains):
455
+ if any(not domain.values or domain.opaque or domain.unresolved for domain in domains):
332
456
  return []
333
457
  groups: dict[_Tag, list[_Record]] = {}
334
458
  for record, domain in zip(records, domains, strict=True):
@@ -189,6 +189,7 @@ from sarj_python_lint.rules.require_public_dependency_contract import RequirePub
189
189
  from sarj_python_lint.rules.require_pydantic_for_external_json import (
190
190
  RequirePydanticForExternalJson,
191
191
  )
192
+ from sarj_python_lint.rules.require_pydantic_for_mapping_shape import RequirePydanticForMappingShape
192
193
  from sarj_python_lint.rules.require_pydantic_for_structured_payload import RequirePydanticForStructuredPayload
193
194
  from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
194
195
  RequirePydanticOrdinalLowerBound,
@@ -305,6 +306,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
305
306
  PreferMatchAssertNever.id: PreferMatchAssertNever,
306
307
  RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
307
308
  RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
309
+ RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
308
310
  PreferModuleLevelConstant.id: PreferModuleLevelConstant,
309
311
  PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
310
312
  PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
@@ -106,8 +106,8 @@ class PreferDiscriminatedModelUnion(Rule):
106
106
  autofix=AutofixPolicy.NONE,
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
- "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.",
110
- "String, bytes, integer, Boolean and None tag values are supported. Boolean/integer equality collisions overlap. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. TypedDict tags must be required; dataclass ClassVar tags must have a value. Runtime discriminator configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
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.",
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.",