sarj-python-lint 1.53.0__tar.gz → 1.55.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.53.0 → sarj_python_lint-1.55.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/__main__.py +5 -0
  4. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +220 -26
  5. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  6. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +1 -1
  7. sarj_python_lint-1.55.0/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +467 -0
  8. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/.gitignore +0 -0
  9. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/LICENSE +0 -0
  10. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/README.md +0 -0
  11. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/__init__.py +0 -0
  12. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_source.py +0 -0
  19. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  22. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/json_boundary.py +0 -0
  23. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  28. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  30. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  31. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  33. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  34. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  35. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  36. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  37. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  38. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  39. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  40. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  41. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  42. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  43. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  44. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  46. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  47. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  48. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  49. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  50. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  51. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  52. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  54. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  55. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  56. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  59. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  60. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  61. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  62. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  63. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  64. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  65. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  66. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  67. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  68. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  69. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  70. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  71. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  72. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  73. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  74. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  75. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  76. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  77. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  78. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  79. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  80. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  81. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  82. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  83. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  84. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  85. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  86. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  87. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  88. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  89. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  90. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  91. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  92. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  93. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  94. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  95. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  96. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  97. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  98. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  99. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  100. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  101. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  102. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  103. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  104. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  105. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  106. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  107. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  108. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  109. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  110. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  111. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  112. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  113. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  114. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  115. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  116. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  117. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  118. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  119. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  120. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  121. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  122. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  123. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  124. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  125. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  126. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  127. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  128. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  129. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  130. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  131. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  132. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  133. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  134. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  135. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  136. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  137. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  138. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  139. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  140. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  141. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  142. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  143. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  144. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  145. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  146. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  147. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  148. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  149. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  150. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  151. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  152. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  153. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  154. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  155. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  156. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  157. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  158. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  159. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  160. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  161. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  162. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  163. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  164. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  165. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  166. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  167. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  168. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  169. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  170. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  171. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  172. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  173. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  174. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  175. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  176. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  177. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  178. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  179. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  180. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  181. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  182. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  183. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  184. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  185. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  186. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  187. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  188. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  189. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  190. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  191. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  192. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  193. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  194. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  195. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  196. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  197. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  198. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  199. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  200. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  201. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  202. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  203. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  204. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  205. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  206. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  207. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  208. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  209. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  210. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  211. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  212. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  213. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  214. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  215. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  216. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  217. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  218. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  219. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  220. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  221. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  222. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  223. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  224. {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.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.53.0
3
+ Version: 1.55.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.53.0"
3
+ version = "1.55.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"}),
@@ -80,13 +80,29 @@ class _DataUnions:
80
80
  if isinstance(node, ast.ClassDef)
81
81
  and len(bindings.get(node.name, ())) == 1
82
82
  and node.name not in self.provenance.mutated
83
+ and node.name not in self.provenance.external_bindings
83
84
  }
84
85
  self.aliases: dict[str, ast.expr] = {}
85
86
  self.declarations: dict[str, ast.stmt] = {}
87
+ self.values: dict[str, ast.expr] = {}
88
+ self.metadata_aliases = _metadata_alias_graph(tree, bindings)
89
+ self.metadata_mutation_cache: dict[str, bool | None] = {}
90
+ self.metadata_writes = _metadata_attribute_writes(context, tree, self.provenance)
86
91
  for node in tree.body:
87
- value = _alias_value(node, context)
92
+ binding = _value_binding(node)
93
+ if binding is not None:
94
+ bound_name, bound_value = binding
95
+ if len(bindings.get(bound_name, ())) == 1:
96
+ self.values[bound_name] = bound_value
97
+ value = _alias_value(node, context, self.provenance)
88
98
  name = _alias_name(node)
89
- if value is not None and name is not None and len(bindings.get(name, ())) == 1:
99
+ if (
100
+ value is not None
101
+ and name is not None
102
+ and len(bindings.get(name, ())) == 1
103
+ and name not in self.provenance.external_bindings
104
+ and name not in self.provenance.mutated
105
+ ):
90
106
  self.aliases[name] = value
91
107
  self.declarations[name] = node
92
108
  self.records: dict[str, _Record | None] = {}
@@ -161,25 +177,119 @@ class _DataUnions:
161
177
  )
162
178
 
163
179
  def _discriminator_metadata(self, node: ast.expr) -> bool:
164
- if not isinstance(node, ast.Call) or not _untouched(node.func, self.provenance):
180
+ seen: set[str] = set()
181
+ for _ in range(_MAX_EXPANSION):
182
+ if isinstance(node, ast.Name) and node.id in self.values:
183
+ if node.id in seen:
184
+ return True
185
+ seen.add(node.id)
186
+ node = self.values[node.id]
187
+ continue
188
+ if not isinstance(node, ast.Call):
189
+ return self._metadata_aliases_discriminate(seen, callable_metadata=False)
190
+ step = self._metadata_step(node)
191
+ if isinstance(step, bool):
192
+ return self._metadata_aliases_discriminate(seen, callable_metadata=step)
193
+ node = step
194
+ return True
195
+
196
+ def _metadata_step(self, node: ast.Call) -> ast.expr | bool:
197
+ if not _untouched(node.func, self.provenance):
165
198
  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
199
+ discriminator = next((keyword.value for keyword in node.keywords if keyword.arg == "discriminator"), None)
200
+ if self._metadata_constructor(node.func, _DISCRIMINATOR_SOURCES, "Discriminator"):
201
+ argument = node.args[0] if node.args else discriminator
202
+ return argument is not None and not self._string_discriminator(argument)
203
+ if self._metadata_constructor(node.func, _FIELD_SOURCES, "Field") and discriminator is not None:
204
+ return discriminator
205
+ return False
206
+
207
+ def _metadata_aliases_discriminate(self, names: set[str], *, callable_metadata: bool) -> bool:
208
+ mutations = [self._metadata_mutated(name) for name in names]
209
+ return any(mutation is None for mutation in mutations) or (callable_metadata and not any(mutations))
210
+
211
+ def _metadata_mutated(self, name: str) -> bool | None:
212
+ if name in self.metadata_mutation_cache:
213
+ return self.metadata_mutation_cache[name]
214
+ pending = [name]
215
+ seen: set[str] = set()
216
+ mutated = False
217
+ for _ in range(_MAX_EXPANSION):
218
+ if not pending:
219
+ self.metadata_mutation_cache.update(dict.fromkeys(seen, mutated))
220
+ return mutated
221
+ current = pending.pop()
222
+ if current in seen:
223
+ continue
224
+ seen.add(current)
225
+ if current in self.provenance.external_bindings or self._metadata_escaped(current):
226
+ return None
227
+ if current in self.provenance.mutated:
228
+ if not self._metadata_string_writes(current):
229
+ return None
230
+ mutated = True
231
+ pending.extend(self.metadata_aliases.get(current, set()).difference(seen))
232
+ return None
233
+
234
+ def _metadata_string_writes(self, name: str) -> bool:
235
+ writes = self.metadata_writes.get(name, ())
236
+ return (
237
+ bool(writes)
238
+ and len(writes) <= _MAX_EXPANSION
239
+ and all(attribute == "discriminator" and self._string_discriminator(value) for attribute, value in writes)
240
+ )
241
+
242
+ def _metadata_escaped(self, name: str) -> bool:
243
+ reads = self.provenance.reads.get(name, ())
244
+ if len(reads) > _MAX_EXPANSION:
245
+ return True
246
+ for read in reads:
247
+ node: ast.AST | None = read
248
+ for _ in range(_MAX_EXPANSION):
249
+ node = self.context.parents.get(node)
250
+ if isinstance(node, ast.Call):
251
+ return True
252
+ if node is None or isinstance(node, ast.stmt):
253
+ break
254
+ else:
255
+ return True
256
+ return False
257
+
258
+ def _metadata_constructor(self, node: ast.expr, sources: frozenset[str], symbol: str) -> bool:
259
+ imports = self.context.module_imports
260
+ return imports.resolves(node, sources=sources, symbol=symbol) or (
261
+ isinstance(node, ast.Attribute)
262
+ and node.attr == symbol
263
+ and imports.resolved_qualified_name(node.value) in sources
177
264
  )
178
265
 
266
+ def _string_discriminator(self, node: ast.expr) -> bool:
267
+ pending = [_Expansion(node)]
268
+ for _ in range(_MAX_EXPANSION):
269
+ if not pending:
270
+ return True
271
+ item = pending.pop()
272
+ node = item.node
273
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
274
+ continue
275
+ if (
276
+ isinstance(node, ast.Name)
277
+ and node.id in self.values
278
+ and node.id not in item.aliases
279
+ and node.id not in self.provenance.mutated
280
+ and node.id not in self.provenance.external_bindings
281
+ ):
282
+ pending.append(_Expansion(self.values[node.id], item.aliases | {node.id}))
283
+ elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
284
+ pending.extend(_Expansion(child, item.aliases) for child in (node.left, node.right))
285
+ else:
286
+ return False
287
+ return False
288
+
179
289
  def _typing_symbol(self, node: ast.expr) -> str | None:
180
290
  if not _untouched(node, self.provenance):
181
291
  return None
182
- return self.context.imports.resolved_symbol(node, sources=TYPING_SOURCES)
292
+ return self.context.module_imports.resolved_symbol(node, sources=TYPING_SOURCES)
183
293
 
184
294
  def _variants(self, expression: ast.expr) -> set[str] | None:
185
295
  leaves = self._leaves(expression)
@@ -296,13 +406,17 @@ class _DataUnions:
296
406
  return _Domain(frozenset({_tag(None)}))
297
407
  if isinstance(node, ast.Name | ast.Attribute) and (
298
408
  (isinstance(node, ast.Name) and node.id in self.classes)
299
- or self.context.imports.resolved_qualified_name(node) is not None
409
+ or (
410
+ _untouched(node, self.provenance)
411
+ and self.context.module_imports.resolved_qualified_name(node) is not None
412
+ )
300
413
  ):
301
414
  return _Domain(unresolved=True)
302
415
  if (
303
416
  isinstance(node, ast.Name)
304
417
  and node.id in _NON_LITERAL_BUILTINS
305
- and self.context.imports.builtin_is_unshadowed(node.id)
418
+ and self.context.module_imports.builtin_is_unshadowed(node.id)
419
+ and _untouched(node, self.provenance)
306
420
  ):
307
421
  return _Domain()
308
422
  if not isinstance(node, ast.Subscript):
@@ -313,7 +427,8 @@ class _DataUnions:
313
427
  if symbol is not None or (
314
428
  isinstance(node.value, ast.Name)
315
429
  and node.value.id in {"list", "dict", "tuple", "set", "frozenset", "type"}
316
- and self.context.imports.builtin_is_unshadowed(node.value.id)
430
+ and self.context.module_imports.builtin_is_unshadowed(node.value.id)
431
+ and _untouched(node.value, self.provenance)
317
432
  ):
318
433
  return _Domain()
319
434
  return _Domain(opaque=True)
@@ -381,9 +496,37 @@ def _record(node: ast.ClassDef, context: PythonFileContext, provenance: Resource
381
496
  continue
382
497
  else:
383
498
  return None
499
+ field_names = frozenset(fields)
500
+ if any(_class_annotation_shadowed(field.annotation, field_names, context) for field in fields.values()):
501
+ return None
384
502
  return _Record(fields, required)
385
503
 
386
504
 
505
+ def _class_annotation_shadowed(annotation: ast.expr, names: frozenset[str], context: PythonFileContext) -> bool:
506
+ pending = [(annotation, True)]
507
+ for _ in range(_MAX_EXPANSION):
508
+ if not pending:
509
+ return False
510
+ node, forward = pending.pop()
511
+ parsed = _string_expression(node) if forward else node
512
+ if parsed is None or (isinstance(parsed, ast.Name) and parsed.id in names):
513
+ return True
514
+ pending.extend(_class_annotation_children(parsed, forward=forward, context=context))
515
+ return True
516
+
517
+
518
+ def _class_annotation_children(
519
+ node: ast.expr, *, forward: bool, context: PythonFileContext
520
+ ) -> list[tuple[ast.expr, bool]]:
521
+ if not isinstance(node, ast.Subscript):
522
+ return [(child, forward) for child in ast.iter_child_nodes(node) if isinstance(child, ast.expr)]
523
+ members = _subscript_members(node)
524
+ symbol = context.module_imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
525
+ if symbol == "Annotated" and members:
526
+ return [(node.value, False), (members[0], True)]
527
+ return [(node.value, False), *((member, symbol != "Literal") for member in members)]
528
+
529
+
387
530
  def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool | None:
388
531
  if node.type_params:
389
532
  return None
@@ -394,16 +537,16 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
394
537
  for decorator in node.decorator_list
395
538
  if not (
396
539
  _untouched(decorator, provenance)
397
- and context.imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
540
+ and context.module_imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
398
541
  )
399
542
  ]
400
543
  if decorators:
401
544
  return _direct_dataclass(node, context, provenance, decorators)
402
545
  if len(node.bases) != 1:
403
546
  return None
404
- if context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
547
+ if context.module_imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
405
548
  return True if not node.keywords else None
406
- if not context.imports.resolves(node.bases[0], sources=TYPING_SOURCES, symbol="TypedDict"):
549
+ if not context.module_imports.resolves(node.bases[0], sources=TYPING_SOURCES, symbol="TypedDict"):
407
550
  return None
408
551
  required = True
409
552
  for keyword in node.keywords:
@@ -422,7 +565,7 @@ def _direct_dataclass(
422
565
  return None
423
566
  decorator = decorators[0]
424
567
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
425
- if not _untouched(target, provenance) or not context.imports.resolves(
568
+ if not _untouched(target, provenance) or not context.module_imports.resolves(
426
569
  target, sources=_DATACLASS_SOURCES, symbol="dataclass"
427
570
  ):
428
571
  return None
@@ -479,12 +622,60 @@ def _alias_name(node: ast.stmt) -> str | None:
479
622
  return None
480
623
 
481
624
 
482
- def _alias_value(node: ast.stmt, context: PythonFileContext) -> ast.expr | None:
625
+ def _metadata_alias_graph(tree: ast.Module, bindings: dict[str, list[ast.AST]]) -> dict[str, set[str]]:
626
+ graph: dict[str, set[str]] = {}
627
+ for node in tree.body:
628
+ binding = _value_binding(node)
629
+ if binding is None:
630
+ continue
631
+ name, value = binding
632
+ if len(bindings.get(name, ())) != 1 or not isinstance(value, ast.Name):
633
+ continue
634
+ graph.setdefault(name, set()).add(value.id)
635
+ graph.setdefault(value.id, set()).add(name)
636
+ return graph
637
+
638
+
639
+ def _metadata_attribute_writes(
640
+ context: PythonFileContext, tree: ast.Module, provenance: ResourceProvenance
641
+ ) -> dict[str, list[tuple[str, ast.expr]]]:
642
+ writes: dict[str, list[tuple[str, ast.expr]]] = {}
643
+ for target in context.nodes(ast.Attribute):
644
+ if not isinstance(target.ctx, ast.Store) or not isinstance(target.value, ast.Name):
645
+ continue
646
+ binding = provenance.binding(target.value.id, target)
647
+ if binding is None or provenance.scope(binding) is not tree or provenance.scope(target) is not tree:
648
+ continue
649
+ parent = context.parents.get(target)
650
+ if isinstance(parent, ast.Assign | ast.AnnAssign) and parent.value is not None:
651
+ writes.setdefault(target.value.id, []).append((target.attr, parent.value))
652
+ return writes
653
+
654
+
655
+ def _value_binding(node: ast.stmt) -> tuple[str, ast.expr] | None:
656
+ match node:
657
+ case (
658
+ ast.Assign(targets=[ast.Name(id=name)], value=value)
659
+ | ast.AnnAssign(target=ast.Name(id=name), value=ast.expr() as value)
660
+ ):
661
+ return name, value
662
+ case _:
663
+ return None
664
+
665
+
666
+ def _alias_value(
667
+ node: ast.stmt, context: PythonFileContext, provenance: ResourceProvenance | None = None
668
+ ) -> ast.expr | None:
483
669
  match node:
484
670
  case ast.TypeAlias(type_params=[]):
485
671
  return node.value
486
672
  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
673
+ return (
674
+ value
675
+ if context.module_imports.resolves(annotation, sources=TYPING_SOURCES, symbol="TypeAlias")
676
+ and (provenance is None or _untouched(annotation, provenance))
677
+ else None
678
+ )
488
679
  case ast.Assign(targets=[ast.Name()], value=value):
489
680
  return None if isinstance(value, ast.Constant) and isinstance(value.value, str) else value
490
681
  case _:
@@ -503,7 +694,10 @@ def _string_expression(node: ast.expr) -> ast.expr | None:
503
694
  def _untouched(node: ast.expr, provenance: ResourceProvenance) -> bool:
504
695
  while isinstance(node, ast.Attribute):
505
696
  node = node.value
506
- return isinstance(node, ast.Name) and node.id not in provenance.mutated
697
+ if not isinstance(node, ast.Name) or node.id in provenance.mutated or node.id in provenance.external_bindings:
698
+ return False
699
+ binding = provenance.binding(node.id, node)
700
+ return binding is None or isinstance(binding, ast.alias)
507
701
 
508
702
 
509
703
  def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
@@ -516,7 +710,7 @@ def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
516
710
  return True
517
711
  if not isinstance(node, ast.Subscript):
518
712
  return False
519
- symbol = context.imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
713
+ symbol = context.module_imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
520
714
  if symbol == "Union":
521
715
  return True
522
716
  if symbol == "Optional":
@@ -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,
@@ -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. Import-proven typing final decorators preserve record recognition. Explicit Pydantic callable Discriminators, bounded stable reusable metadata 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 imports are independent of unrelated local parameters; 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.",