sarj-python-lint 1.55.0__tar.gz → 1.57.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 (225) hide show
  1. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +221 -7
  4. sarj_python_lint-1.57.0/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +260 -0
  5. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +1 -1
  6. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +40 -118
  7. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/.gitignore +0 -0
  8. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/LICENSE +0 -0
  9. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/README.md +0 -0
  10. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_source.py +0 -0
  19. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  22. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/json_boundary.py +0 -0
  23. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  28. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  30. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  31. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  33. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  34. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  35. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  36. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  37. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  38. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  39. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  40. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  41. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  42. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  43. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  44. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  46. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  47. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  48. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  49. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  50. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  51. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  52. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  54. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  55. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  56. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  57. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  58. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  59. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  60. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  61. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  62. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  63. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  64. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  65. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  66. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  67. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  68. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  69. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  70. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  71. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  72. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  73. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  74. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  75. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  76. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  77. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  78. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  79. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  80. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  81. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  82. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  83. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  84. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  85. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  86. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  87. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  88. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  89. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  90. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  91. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  92. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  93. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  94. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  95. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  96. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  97. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  98. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  99. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  100. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  101. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  102. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  103. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  104. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  105. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  106. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  107. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  108. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  109. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  110. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  111. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  112. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  113. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  114. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  115. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  116. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  117. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  118. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  119. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  120. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  121. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  122. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  123. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  124. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  125. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  126. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  127. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  128. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  129. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  130. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  131. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  132. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  133. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  134. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  135. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  136. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  137. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  138. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  139. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  140. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  141. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  142. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  143. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  144. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  145. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  146. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  147. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  148. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  149. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  150. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  151. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  152. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  153. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  154. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  155. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  156. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  157. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  158. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  159. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  160. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  161. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  162. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  163. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  164. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  165. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  166. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  167. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  168. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  169. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  170. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  171. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  172. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  173. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  174. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  175. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  176. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  177. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  178. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  179. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  180. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  181. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  182. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  183. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  184. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  185. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  186. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  187. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  188. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  189. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  190. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  191. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  192. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  193. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  194. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  195. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  196. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  197. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  198. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  199. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  200. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  201. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  202. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  203. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  204. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  205. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  206. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  207. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  208. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  209. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  210. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  211. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  212. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  213. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  214. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  215. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  216. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  217. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  218. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  219. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  220. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  221. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  222. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  223. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  224. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  225. {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.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.55.0
3
+ Version: 1.57.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.55.0"
3
+ version = "1.57.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -73,6 +73,7 @@ class _DataUnions:
73
73
  def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
74
74
  self.context = context
75
75
  self.provenance = ResourceProvenance(context, require_static_namespace=True)
76
+ self.provenance.mutated = _scoped_module_mutations(self.provenance, tree)
76
77
  bindings = self.provenance.bindings.get(tree, {})
77
78
  self.classes = {
78
79
  node.name: node
@@ -177,13 +178,13 @@ class _DataUnions:
177
178
  )
178
179
 
179
180
  def _discriminator_metadata(self, node: ast.expr) -> bool:
180
- seen: set[str] = set()
181
+ seen: frozenset[str] = frozenset()
181
182
  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]
183
+ reference = self._metadata_reference(node, seen)
184
+ if isinstance(reference, bool):
185
+ return reference
186
+ if reference is not None:
187
+ node, seen = reference.node, reference.aliases
187
188
  continue
188
189
  if not isinstance(node, ast.Call):
189
190
  return self._metadata_aliases_discriminate(seen, callable_metadata=False)
@@ -193,6 +194,81 @@ class _DataUnions:
193
194
  node = step
194
195
  return True
195
196
 
197
+ def _metadata_reference(self, node: ast.expr, seen: frozenset[str]) -> _Expansion | bool | None:
198
+ if isinstance(node, ast.Name) and node.id in self.values:
199
+ return True if node.id in seen else _Expansion(self.values[node.id], seen | {node.id})
200
+ projected = self._metadata_projection(node)
201
+ if projected is None or isinstance(projected, bool):
202
+ return projected
203
+ if not projected.aliases.isdisjoint(seen) or any(
204
+ self._metadata_projection_escaped(name) for name in projected.aliases
205
+ ):
206
+ return True
207
+ return _Expansion(projected.node, seen | projected.aliases)
208
+
209
+ def _metadata_projection(self, node: ast.expr) -> _Expansion | bool | None:
210
+ if isinstance(node, ast.Subscript):
211
+ return self._metadata_tuple_item(node)
212
+ if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
213
+ return None
214
+ bindings = self.provenance.bindings.get(self.context.tree, {}).get(node.value.id, ())
215
+ if len(bindings) != 1 or not isinstance(bindings[0], ast.ClassDef):
216
+ return None
217
+ value = _namespace_metadata_value(bindings[0], node.attr)
218
+ return _Expansion(value, frozenset({node.value.id})) if value is not None else None
219
+
220
+ def _metadata_tuple_item(self, node: ast.Subscript) -> _Expansion | bool | None:
221
+ value = node.value
222
+ names: frozenset[str] = frozenset()
223
+ for _ in range(_MAX_EXPANSION):
224
+ if not isinstance(value, ast.Name) or value.id not in self.values:
225
+ break
226
+ if value.id in names:
227
+ return True
228
+ names |= {value.id}
229
+ value = self.values[value.id]
230
+ else:
231
+ return True
232
+ if not isinstance(value, ast.Tuple):
233
+ return None
234
+ if len(value.elts) > _MAX_EXPANSION:
235
+ return True
236
+ if any(isinstance(item, ast.Starred) for item in value.elts):
237
+ return None
238
+ index = _metadata_tuple_index(node.slice)
239
+ if index is None:
240
+ return True
241
+ if not -len(value.elts) <= index < len(value.elts):
242
+ return None
243
+ return _Expansion(value.elts[index], names)
244
+
245
+ def _metadata_projection_escaped(self, name: str) -> bool:
246
+ bindings = self.provenance.bindings.get(self.context.tree, {}).get(name, ())
247
+ reads = self.provenance.reads.get(name, ())
248
+ if len(bindings) != 1 or len(reads) > _MAX_EXPANSION:
249
+ return True
250
+ for read in reads:
251
+ if self.provenance.binding(name, read) is not bindings[0]:
252
+ continue
253
+ projection = self.context.parents.get(read)
254
+ if not isinstance(projection, ast.Attribute | ast.Subscript) or projection.value is not read:
255
+ return True
256
+ if not self._metadata_annotation_member(projection):
257
+ return True
258
+ return False
259
+
260
+ def _metadata_annotation_member(self, node: ast.expr) -> bool:
261
+ parent = self.context.parents.get(node)
262
+ if not isinstance(parent, ast.Tuple) or len(parent.elts) > _MAX_EXPANSION:
263
+ return False
264
+ owner = self.context.parents.get(parent)
265
+ return (
266
+ isinstance(owner, ast.Subscript)
267
+ and owner.slice is parent
268
+ and self._typing_symbol(owner.value) == "Annotated"
269
+ and node in parent.elts[1:]
270
+ )
271
+
196
272
  def _metadata_step(self, node: ast.Call) -> ast.expr | bool:
197
273
  if not _untouched(node.func, self.provenance):
198
274
  return False
@@ -204,7 +280,7 @@ class _DataUnions:
204
280
  return discriminator
205
281
  return False
206
282
 
207
- def _metadata_aliases_discriminate(self, names: set[str], *, callable_metadata: bool) -> bool:
283
+ def _metadata_aliases_discriminate(self, names: frozenset[str], *, callable_metadata: bool) -> bool:
208
284
  mutations = [self._metadata_mutated(name) for name in names]
209
285
  return any(mutation is None for mutation in mutations) or (callable_metadata and not any(mutations))
210
286
 
@@ -243,7 +319,12 @@ class _DataUnions:
243
319
  reads = self.provenance.reads.get(name, ())
244
320
  if len(reads) > _MAX_EXPANSION:
245
321
  return True
322
+ bindings = self.provenance.bindings.get(self.context.tree, {}).get(name, ())
323
+ if len(bindings) != 1:
324
+ return True
246
325
  for read in reads:
326
+ if self.provenance.binding(name, read) is not bindings[0]:
327
+ continue
247
328
  node: ast.AST | None = read
248
329
  for _ in range(_MAX_EXPANSION):
249
330
  node = self.context.parents.get(node)
@@ -361,6 +442,8 @@ class _DataUnions:
361
442
  return uncertain
362
443
 
363
444
  def _field_domain(self, field: ast.AnnAssign, *, required: bool) -> _Domain:
445
+ if _direct_initvar_field(field, self.context, self.provenance):
446
+ return _Domain()
364
447
  node = field.annotation
365
448
  aliases: set[str] = set()
366
449
  for _ in range(_MAX_EXPANSION):
@@ -478,6 +561,26 @@ def _wrapped_annotation(node: ast.Subscript, symbol: str | None, *, required: bo
478
561
  return None
479
562
 
480
563
 
564
+ def _direct_initvar_field(field: ast.AnnAssign, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
565
+ if field.value is not None or not isinstance(field.annotation, ast.Subscript):
566
+ return False
567
+ target = field.annotation.value
568
+ if not _untouched(target, provenance) or not context.module_imports.resolves(
569
+ target, sources=_DATACLASS_SOURCES, symbol="InitVar"
570
+ ):
571
+ return False
572
+ owner = context.parents.get(field)
573
+ if not isinstance(owner, ast.ClassDef):
574
+ return False
575
+ for decorator in owner.decorator_list:
576
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
577
+ if _untouched(target, provenance) and context.module_imports.resolves(
578
+ target, sources=_DATACLASS_SOURCES, symbol="dataclass"
579
+ ):
580
+ return True
581
+ return False
582
+
583
+
481
584
  def _record(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> _Record | None:
482
585
  required = _record_required(node, context, provenance)
483
586
  if required is None:
@@ -622,6 +725,37 @@ def _alias_name(node: ast.stmt) -> str | None:
622
725
  return None
623
726
 
624
727
 
728
+ def _namespace_metadata_value(node: ast.ClassDef, attribute: str) -> ast.expr | None:
729
+ if node.bases or node.keywords or node.decorator_list or node.type_params or len(node.body) > _MAX_EXPANSION:
730
+ return None
731
+ values: list[tuple[str, ast.expr]] = []
732
+ for member in node.body:
733
+ binding = _value_binding(member)
734
+ if binding is not None and not binding[0].startswith("__"):
735
+ values.append(binding)
736
+ elif isinstance(member, ast.Pass | ast.Expr) and (
737
+ isinstance(member, ast.Pass)
738
+ or (isinstance(member.value, ast.Constant) and isinstance(member.value.value, str))
739
+ ):
740
+ continue
741
+ else:
742
+ return None
743
+ if len(values) != 1 or values[0][0] != attribute:
744
+ return None
745
+ return values[0][1]
746
+
747
+
748
+ def _metadata_tuple_index(node: ast.expr) -> int | None:
749
+ if isinstance(node, ast.Constant) and type(node.value) is int:
750
+ return node.value
751
+ if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant) and type(node.operand.value) is int:
752
+ if isinstance(node.op, ast.USub):
753
+ return -node.operand.value
754
+ if isinstance(node.op, ast.UAdd):
755
+ return node.operand.value
756
+ return None
757
+
758
+
625
759
  def _metadata_alias_graph(tree: ast.Module, bindings: dict[str, list[ast.AST]]) -> dict[str, set[str]]:
626
760
  graph: dict[str, set[str]] = {}
627
761
  for node in tree.body:
@@ -691,6 +825,86 @@ def _string_expression(node: ast.expr) -> ast.expr | None:
691
825
  return None
692
826
 
693
827
 
828
+ def _scoped_module_mutations(provenance: ResourceProvenance, tree: ast.Module) -> set[str]:
829
+ original = provenance.mutated
830
+ if not original:
831
+ return original
832
+ bindings = provenance.bindings.get(tree, {})
833
+ owners = {name: bindings[name][0] for name in original if len(bindings.get(name, ())) == 1}
834
+ writes = _module_attribute_mutations(provenance, owners)
835
+ if writes is None:
836
+ return original
837
+ return {
838
+ name
839
+ for name in original
840
+ if name not in owners or name in writes or _module_binding_escaped(name, owners[name], provenance)
841
+ }
842
+
843
+
844
+ def _module_attribute_mutations(provenance: ResourceProvenance, owners: dict[str, ast.AST]) -> set[str] | None:
845
+ changed: set[str] = set()
846
+ count = 0
847
+ for target in provenance.context.nodes(ast.Attribute):
848
+ if not isinstance(target.ctx, ast.Store | ast.Del):
849
+ continue
850
+ count += 1
851
+ if count > _MAX_EXPANSION:
852
+ return None
853
+ root: ast.expr = target
854
+ while isinstance(root, ast.Attribute):
855
+ root = root.value
856
+ if not isinstance(root, ast.Name) or root.id not in owners:
857
+ continue
858
+ binding = provenance.binding(root.id, target)
859
+ if binding is None or binding is owners[root.id]:
860
+ changed.add(root.id)
861
+ return changed
862
+
863
+
864
+ def _module_binding_escaped(name: str, owner: ast.AST, provenance: ResourceProvenance) -> bool:
865
+ reads = provenance.reads.get(name, ())
866
+ if len(reads) > _MAX_EXPANSION:
867
+ return True
868
+ return any(
869
+ provenance.binding(name, read) is owner and _module_read_escapes(read, provenance.context) for read in reads
870
+ )
871
+
872
+
873
+ def _module_read_escapes(read: ast.Name, context: PythonFileContext) -> bool:
874
+ current: ast.AST = read
875
+ for _ in range(_MAX_EXPANSION):
876
+ parent = context.parents.get(current)
877
+ if parent is None:
878
+ return False
879
+ step = _module_escape_step(parent, current, context)
880
+ if step is not None:
881
+ return step
882
+ current = parent
883
+ return True
884
+
885
+
886
+ def _module_escape_step(parent: ast.AST, current: ast.AST, context: PythonFileContext) -> bool | None:
887
+ match parent:
888
+ case ast.Call():
889
+ return parent.func is not current
890
+ case ast.Return() | ast.Yield() | ast.YieldFrom() | ast.NamedExpr():
891
+ return True
892
+ case ast.arguments():
893
+ return True if current in parent.defaults or current in parent.kw_defaults else None
894
+ case ast.Assign() | ast.AnnAssign() | ast.AugAssign():
895
+ value = parent.value
896
+ return value is not None and value is current and not _union_expression(value, context)
897
+ case ast.Subscript() if parent.value is not current:
898
+ symbol = context.module_imports.resolved_symbol(parent.value, sources=TYPING_SOURCES)
899
+ if symbol is None:
900
+ return True
901
+ case ast.withitem():
902
+ return parent.context_expr is current
903
+ case _:
904
+ pass
905
+ return False if isinstance(parent, ast.stmt) else None
906
+
907
+
694
908
  def _untouched(node: ast.expr, provenance: ResourceProvenance) -> bool:
695
909
  while isinstance(node, ast.Attribute):
696
910
  node = node.value
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING, final
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from sarj_python_lint._file_context import PythonFileContext
11
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
12
+
13
+
14
+ type _Function = ast.FunctionDef | ast.AsyncFunctionDef
15
+ _VALIDATORS = frozenset(
16
+ {
17
+ "pydantic.model_validator",
18
+ "pydantic.field_validator",
19
+ "pydantic.functional_validators.model_validator",
20
+ "pydantic.functional_validators.field_validator",
21
+ }
22
+ )
23
+ _DISCRIMINATORS = frozenset({"pydantic.Discriminator", "pydantic.types.Discriminator"})
24
+ _RAW_METADATA = frozenset(
25
+ {
26
+ "pydantic.BeforeValidator",
27
+ "pydantic.WrapValidator",
28
+ "pydantic.functional_validators.BeforeValidator",
29
+ "pydantic.functional_validators.WrapValidator",
30
+ }
31
+ )
32
+ _MAX_DEPTH = 32
33
+
34
+
35
+ @final
36
+ class RawMappingCallbacks:
37
+ def __init__(
38
+ self,
39
+ context: PythonFileContext,
40
+ provenance: ResourceProvenance,
41
+ qualified: Callable[[ast.expr, ast.AST], str | None],
42
+ builtin: Callable[[ast.expr, str, ast.AST], bool],
43
+ shadowed: Callable[[str, ast.AST], bool],
44
+ ) -> None:
45
+ self.context = context
46
+ self.provenance = provenance
47
+ self.qualified = qualified
48
+ self.builtin = builtin
49
+ self.shadowed = shadowed
50
+ self.ownership: dict[_Function, bool] = {}
51
+ self.callbacks = {
52
+ binding
53
+ for call in context.nodes(ast.Call)
54
+ if (origin := qualified(call.func, call)) in _DISCRIMINATORS | _RAW_METADATA
55
+ if (argument := _callback_argument(call, origin)) is not None
56
+ if (binding := self.callback(argument)) is not None
57
+ }
58
+
59
+ def callback(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> _Function | None:
60
+ if len(seen) >= _MAX_DEPTH:
61
+ return None
62
+ if isinstance(node, ast.Call):
63
+ return (
64
+ self._partial(node, seen)
65
+ if self.qualified(node.func, node) == "functools.partial"
66
+ else self._method(node, "__call__", seen)
67
+ )
68
+ if isinstance(node, ast.Attribute):
69
+ return self._method(node.value, node.attr, seen)
70
+ if not isinstance(node, ast.Name) or not self._stable(node.id) or self.shadowed(node.id, node):
71
+ return None
72
+ binding = self.provenance.binding(node.id, node)
73
+ if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef):
74
+ return binding if not binding.decorator_list else None
75
+ if not isinstance(binding, ast.Name) or binding in seen:
76
+ return None
77
+ value = self.provenance.assignment_value(binding, node)
78
+ return self.callback(value, seen=seen | {binding}) if value is not None else None
79
+
80
+ def _partial(self, node: ast.Call, seen: frozenset[ast.AST]) -> _Function | None:
81
+ if len(node.args) != 1 or isinstance(node.args[0], ast.Starred):
82
+ return None
83
+ callback = self.callback(node.args[0], seen=seen | {node})
84
+ if callback is None or not (callback.args.posonlyargs or callback.args.args):
85
+ return None
86
+ configuration = {argument.arg for argument in callback.args.kwonlyargs}
87
+ return callback if all(keyword.arg in configuration for keyword in node.keywords) else None
88
+
89
+ def _stable(self, name: str) -> bool:
90
+ return name not in self.provenance.mutated | self.provenance.external_bindings
91
+
92
+ def _method(self, node: ast.expr, name: str, seen: frozenset[ast.AST]) -> ast.FunctionDef | None:
93
+ owner = self._owner(node, seen)
94
+ if owner is None:
95
+ return None
96
+ declaration, on_class = owner
97
+ if declaration.bases or declaration.keywords or declaration.type_params:
98
+ return None
99
+ if (
100
+ not isinstance(self.context.parents.get(declaration), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef)
101
+ or not self._stable(declaration.name)
102
+ or any(
103
+ self.qualified(decorator, decorator) not in {"typing.final", "typing_extensions.final"}
104
+ for decorator in declaration.decorator_list
105
+ )
106
+ ):
107
+ return None
108
+ method = _plain_method(declaration, name)
109
+ if method is None:
110
+ return None
111
+ if not method.decorator_list:
112
+ return method if not on_class else None
113
+ if len(method.decorator_list) != 1:
114
+ return None
115
+ decorator = method.decorator_list[0]
116
+ return (
117
+ method
118
+ if any(self.builtin(decorator, kind, decorator) for kind in ("staticmethod", "classmethod"))
119
+ else None
120
+ )
121
+
122
+ def _owner(self, node: ast.expr, seen: frozenset[ast.AST]) -> tuple[ast.ClassDef, bool] | None:
123
+ if len(seen) >= _MAX_DEPTH:
124
+ return None
125
+ if isinstance(node, ast.Call):
126
+ if node.args or node.keywords or not isinstance(node.func, ast.Name):
127
+ return None
128
+ owner = self._owner(node.func, seen | {node})
129
+ return (owner[0], False) if owner is not None and owner[1] else None
130
+ if not isinstance(node, ast.Name) or not self._stable(node.id) or self.shadowed(node.id, node):
131
+ return None
132
+ binding = self.provenance.binding(node.id, node)
133
+ if isinstance(binding, ast.ClassDef):
134
+ return binding, True
135
+ if not isinstance(binding, ast.Name) or binding in seen:
136
+ return None
137
+ value = self.provenance.assignment_value(binding, node)
138
+ return self._owner(value, seen | {binding}) if value is not None else None
139
+
140
+ def owns(self, node: ast.AST) -> bool:
141
+ function = self._function(node)
142
+ return function is not None and self._owned(function, frozenset())
143
+
144
+ def _function(self, node: ast.AST) -> _Function | None:
145
+ current = self.context.parents.get(node)
146
+ while current is not None:
147
+ if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef):
148
+ return current
149
+ if isinstance(current, ast.ClassDef | ast.Lambda):
150
+ return None
151
+ current = self.context.parents.get(current)
152
+ return None
153
+
154
+ def _owned(self, function: _Function, seen: frozenset[ast.AST]) -> bool:
155
+ if function in self.ownership:
156
+ return self.ownership[function]
157
+ if function in seen or len(seen) >= _MAX_DEPTH:
158
+ return False
159
+ result = self._raw_validator(function)
160
+ if not result:
161
+ owner = self._function(function)
162
+ result = owner is not None and self._owned(owner, seen | {function}) and self._local_helper(function, owner)
163
+ self.ownership[function] = result
164
+ return result
165
+
166
+ def _local_helper(self, function: _Function, owner: _Function) -> bool:
167
+ if (
168
+ function.decorator_list
169
+ or self.context.parents.get(function) is not owner
170
+ or not self._stable(function.name)
171
+ ):
172
+ return False
173
+ reads = [
174
+ read
175
+ for read in self.provenance.reads.get(function.name, ())
176
+ if self.provenance.binding(function.name, read) is function
177
+ ]
178
+ return bool(reads) and all(
179
+ isinstance(parent := self.context.parents.get(read), ast.Call)
180
+ and parent.func is read
181
+ and self._function(read) in {owner, function}
182
+ for read in reads
183
+ )
184
+
185
+ def _raw_validator(self, function: _Function) -> bool:
186
+ if function in self.callbacks:
187
+ return True
188
+ return any(
189
+ isinstance(decorator, ast.Call)
190
+ and self.qualified(decorator.func, decorator) in _VALIDATORS
191
+ and any(
192
+ keyword.arg == "mode" and self._mode(keyword.value) in {"before", "wrap"}
193
+ for keyword in decorator.keywords
194
+ )
195
+ for decorator in function.decorator_list
196
+ )
197
+
198
+ def _mode(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> str | None:
199
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
200
+ return node.value
201
+ if (
202
+ len(seen) >= _MAX_DEPTH
203
+ or not isinstance(node, ast.Name)
204
+ or not self._stable(node.id)
205
+ or self.shadowed(node.id, node)
206
+ ):
207
+ return None
208
+ binding = self.provenance.binding(node.id, node)
209
+ if not isinstance(binding, ast.Name) or binding in seen:
210
+ return None
211
+ statement = self.context.parents.get(binding)
212
+ if not isinstance(statement, ast.Assign | ast.AnnAssign):
213
+ return None
214
+ if (
215
+ (isinstance(statement, ast.Assign) and len(statement.targets) != 1)
216
+ or statement.value is None
217
+ or statement.lineno >= node.lineno
218
+ or not isinstance(
219
+ self.context.parents.get(statement), ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
220
+ )
221
+ ):
222
+ return None
223
+ return self._mode(statement.value, seen=seen | {binding})
224
+
225
+
226
+ def _callback_argument(call: ast.Call, qualified: str | None) -> ast.expr | None:
227
+ if call.args:
228
+ return call.args[0]
229
+ parameter = "discriminator" if qualified in _DISCRIMINATORS else "func"
230
+ return next((keyword.value for keyword in call.keywords if keyword.arg == parameter), None)
231
+
232
+
233
+ def _plain_method(node: ast.ClassDef, name: str) -> ast.FunctionDef | None:
234
+ method: ast.FunctionDef | None = None
235
+ for statement in node.body:
236
+ match statement:
237
+ case ast.FunctionDef(name=declared) if declared == name and method is None:
238
+ method = statement
239
+ case ast.Assign(value=value, targets=targets) if all(
240
+ isinstance(target, ast.Name) and target.id != name and _inert_attribute(target.id, value)
241
+ for target in targets
242
+ ):
243
+ continue
244
+ case ast.AnnAssign(target=ast.Name(id=attribute), value=value) if attribute != name and _inert_attribute(
245
+ attribute, value
246
+ ):
247
+ continue
248
+ case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
249
+ continue
250
+ case _:
251
+ return None
252
+ return method
253
+
254
+
255
+ def _inert_attribute(name: str, value: ast.expr | None) -> bool:
256
+ if name == "__slots__":
257
+ return isinstance(value, ast.Tuple) and not value.elts
258
+ return (name == "__name__" or not (name.startswith("__") and name.endswith("__"))) and (
259
+ value is None or isinstance(value, ast.Constant)
260
+ )
@@ -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. 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.",
110
+ "String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Direct import-proven InitVar annotations without defaults are constructor inputs rather than instance tags; quoted, wrapped, locally aliased or defaulted InitVar forms remain uncertain. Import-proven typing final decorators preserve record recognition. Explicit Pydantic callable Discriminators, bounded stable reusable metadata, single-value plain local class namespaces, literal immutable tuple projections and imported pydantic.types constructors are respected; source-proven string field names, including local bindings and concatenation, still need Literal fields. Source-proven string discriminator writes through stable aliases invalidate callable coverage; opaque mutation, helper escapes and excessive alias graphs abstain. Module bindings are independent of unrelated local parameters and their attribute writes; class annotation shadows and global rebinding remain excluded. Other runtime configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
111
111
  "For nullable-field dependencies, only stable direct module-level import-proven Pydantic BaseModel subclasses with pure direct after model validators are inspected.",
112
112
  "Bounded paths may contain finite tag comparisons or literal-container membership, None identity tests, Boolean combinations, nested guards and returns of the same instance. Every rejection region must prove one tag-dependent nullable-field requirement; joint missing-field or independent-tag-axis conditions abstain.",
113
113
  "Tags must use imported Literal domains, optionally composed through direct unions, with at least two distinct homogeneous string, integer or Boolean values, optionally including an explicit None state. Mixed primitive families, bool/int equality collisions, open or computed tags abstain. Payloads must explicitly include None in a union, imported Union or Optional.",