sarj-python-lint 1.45.4__tar.gz → 1.46.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 (216) hide show
  1. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/pyproject.toml +1 -1
  3. sarj_python_lint-1.46.0/src/sarj_python_lint/rules/_native_namespace_capability.py +116 -0
  4. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +4 -2
  5. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  6. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +2 -107
  7. sarj_python_lint-1.46.0/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +325 -0
  8. sarj_python_lint-1.46.0/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +599 -0
  9. sarj_python_lint-1.45.4/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -347
  10. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/.gitignore +0 -0
  11. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/LICENSE +0 -0
  12. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/README.md +0 -0
  13. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/__init__.py +0 -0
  14. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/__main__.py +0 -0
  15. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  16. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_file_context.py +0 -0
  17. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_filesystem.py +0 -0
  18. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_python_target.py +0 -0
  19. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  20. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_secret_names.py +0 -0
  21. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_source.py +0 -0
  22. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_version.py +0 -0
  23. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/benchmark.py +0 -0
  24. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  25. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/json_boundary.py +0 -0
  26. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/py.typed +0 -0
  27. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/ratchet.py +0 -0
  28. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rule_base.py +0 -0
  29. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  30. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  31. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  32. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  34. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  35. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  36. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  37. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  38. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  39. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  40. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  41. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  42. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  43. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  44. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  45. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  46. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  47. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  48. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  49. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  50. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  51. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  52. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  53. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  54. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  55. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  56. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  59. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  60. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  61. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  62. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  63. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  64. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  65. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  66. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  67. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  68. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  69. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  70. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  71. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  72. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  73. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  74. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  75. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  76. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  77. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  78. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  79. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  80. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  81. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  82. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  83. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  84. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  85. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  86. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  87. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  88. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  89. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  90. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  91. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  92. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  93. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  94. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  95. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  96. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  97. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  98. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  99. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  100. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  101. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  102. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  103. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  104. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  105. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  106. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  107. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  108. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  109. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  110. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  111. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  112. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  113. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  114. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  115. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  116. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  117. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  118. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  119. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  120. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  121. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  122. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  123. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  124. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  125. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  126. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  127. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  128. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  129. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  130. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  131. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  132. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  133. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  134. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  135. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  136. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  137. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  138. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  139. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  140. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  141. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  142. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  143. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  144. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  145. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  146. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  147. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  148. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  149. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  150. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  151. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  152. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  153. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  154. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  155. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  156. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  157. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  158. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  159. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  160. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  161. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  162. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  163. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  164. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  165. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  166. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  167. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  168. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  169. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  170. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  171. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  172. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  173. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  174. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  175. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  176. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  177. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  178. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  179. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  180. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  181. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  182. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  183. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  184. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  185. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  186. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  187. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  188. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  189. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  190. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  191. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  192. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  193. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  194. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  195. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  196. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  197. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  198. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  199. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  200. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  201. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  202. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  203. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  204. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  205. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  206. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  207. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  208. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  209. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  210. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  211. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  212. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  213. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  214. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  215. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  216. {sarj_python_lint-1.45.4 → sarj_python_lint-1.46.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.45.4
3
+ Version: 1.46.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.45.4"
3
+ version = "1.46.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" }]
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from sarj_python_lint._file_context import PythonFileContext
9
+
10
+
11
+ _NAMESPACE_RETURNING_FUNCTIONS = frozenset({"__lazy_import__"})
12
+ _BUILTIN_NAMESPACE_FUNCTIONS = frozenset(
13
+ {
14
+ "__build_class__",
15
+ "__import__",
16
+ "__lazy_import__",
17
+ "abs",
18
+ "aiter",
19
+ "all",
20
+ "anext",
21
+ "any",
22
+ "ascii",
23
+ "bin",
24
+ "breakpoint",
25
+ "callable",
26
+ "chr",
27
+ "compile",
28
+ "delattr",
29
+ "dir",
30
+ "divmod",
31
+ "eval",
32
+ "exec",
33
+ "format",
34
+ "getattr",
35
+ "globals",
36
+ "hasattr",
37
+ "hash",
38
+ "hex",
39
+ "id",
40
+ "input",
41
+ "isinstance",
42
+ "issubclass",
43
+ "iter",
44
+ "len",
45
+ "locals",
46
+ "max",
47
+ "min",
48
+ "next",
49
+ "oct",
50
+ "ord",
51
+ "pow",
52
+ "print",
53
+ "repr",
54
+ "round",
55
+ "setattr",
56
+ "sorted",
57
+ "sum",
58
+ "vars",
59
+ }
60
+ )
61
+
62
+
63
+ def native_namespace_capability_escapes(context: PythonFileContext) -> bool:
64
+ function_names = _possible_native_function_names(context)
65
+ namespace_function_names = _possible_native_function_names(context, symbols=_NAMESPACE_RETURNING_FUNCTIONS)
66
+ module_names = frozenset(
67
+ alias.asname or alias.name
68
+ for node in context.nodes(ast.Import)
69
+ for alias in node.names
70
+ if alias.name == "builtins"
71
+ )
72
+ for node in context.nodes(ast.Name, ast.Attribute):
73
+ if isinstance(node, ast.Attribute) and (
74
+ not isinstance(node.value, ast.Name) or node.attr not in _BUILTIN_NAMESPACE_FUNCTIONS
75
+ ):
76
+ continue
77
+ if not isinstance(node.ctx, ast.Load) or not _possible_native_function(node, function_names, module_names):
78
+ continue
79
+ parent = context.parents.get(node)
80
+ if (
81
+ not isinstance(parent, ast.Call)
82
+ or parent.func is not node
83
+ or _namespace_returning_function(node, namespace_function_names)
84
+ ):
85
+ return True
86
+ return False
87
+
88
+
89
+ def _possible_native_function_names(
90
+ context: PythonFileContext, *, symbols: frozenset[str] = _BUILTIN_NAMESPACE_FUNCTIONS
91
+ ) -> frozenset[str]:
92
+ return symbols | frozenset(
93
+ alias.asname or alias.name
94
+ for node in context.nodes(ast.ImportFrom)
95
+ if node.level == 0 and node.module == "builtins"
96
+ for alias in node.names
97
+ if alias.name in symbols
98
+ )
99
+
100
+
101
+ def _namespace_returning_function(node: ast.Name | ast.Attribute, function_names: frozenset[str]) -> bool:
102
+ if isinstance(node, ast.Name):
103
+ return node.id in function_names
104
+ return node.attr in _NAMESPACE_RETURNING_FUNCTIONS
105
+
106
+
107
+ def _possible_native_function(
108
+ node: ast.Name | ast.Attribute,
109
+ function_names: frozenset[str],
110
+ module_names: frozenset[str],
111
+ ) -> bool:
112
+ if isinstance(node, ast.Name):
113
+ return node.id in function_names
114
+ return (
115
+ isinstance(node.value, ast.Name) and node.value.id in module_names and node.attr in _BUILTIN_NAMESPACE_FUNCTIONS
116
+ )
@@ -12,6 +12,7 @@ from packaging.version import InvalidVersion, Version
12
12
  from sarj_python_lint.json_boundary import is_object_mapping
13
13
  from sarj_python_lint.rules._first_party import distribution_root
14
14
  from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
15
+ from sarj_python_lint.rules._native_namespace_capability import native_namespace_capability_escapes
15
16
  from sarj_python_lint.rules._resource_provenance import ResourceProvenance
16
17
 
17
18
 
@@ -35,7 +36,8 @@ def direct_models(context: PythonFileContext, *, methods: bool = False) -> tuple
35
36
  if tree is None or context.generated or context.has_wildcard_import:
36
37
  return ()
37
38
  if (
38
- _mutated_imports(context)
39
+ native_namespace_capability_escapes(context)
40
+ or _mutated_imports(context)
39
41
  or _ambiguous_imports(context)
40
42
  or _escaped_imports(context)
41
43
  or dynamic_namespace_access(context.nodes(ast.AST), context.imports, context.parents)
@@ -83,7 +85,7 @@ def inert_annotation(node: ast.expr, context: PythonFileContext) -> bool:
83
85
  if isinstance(node, ast.Constant) and isinstance(node.value, str):
84
86
  try:
85
87
  node = ast.parse(node.value, mode="eval").body
86
- except SyntaxError:
88
+ except SyntaxError, UnicodeError:
87
89
  return False
88
90
  return _inert_expr(node, context)
89
91
 
@@ -217,6 +217,7 @@ from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoCo
217
217
  from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
218
218
  from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
219
219
  from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
220
+ from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
220
221
  from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
221
222
  from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
222
223
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
@@ -231,6 +232,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
231
232
  NoExplicitTestOracle.id: NoExplicitTestOracle,
232
233
  PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
233
234
  NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
235
+ NoInvalidPydanticValidatorReturn.id: NoInvalidPydanticValidatorReturn,
234
236
  NoPartialTypeIs.id: NoPartialTypeIs,
235
237
  NoContradictoryTypeGuard.id: NoContradictoryTypeGuard,
236
238
  NoShallowRecordTypeGuard.id: NoShallowRecordTypeGuard,
@@ -5,6 +5,7 @@ from collections import Counter
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from sarj_python_lint.rules._imports import TYPING_SOURCES
8
+ from sarj_python_lint.rules._native_namespace_capability import native_namespace_capability_escapes
8
9
 
9
10
 
10
11
  if TYPE_CHECKING:
@@ -15,56 +16,6 @@ if TYPE_CHECKING:
15
16
  _NAMESPACE_IMPORT_CAPABILITIES = frozenset(
16
17
  {"__dict__", "__builtins__", "__getattribute__", "__setattr__", "__delattr__"}
17
18
  )
18
- _NAMESPACE_RETURNING_FUNCTIONS = frozenset({"__lazy_import__"})
19
- _BUILTIN_NAMESPACE_FUNCTIONS = frozenset(
20
- {
21
- "__build_class__",
22
- "__import__",
23
- "__lazy_import__",
24
- "abs",
25
- "aiter",
26
- "all",
27
- "anext",
28
- "any",
29
- "ascii",
30
- "bin",
31
- "breakpoint",
32
- "callable",
33
- "chr",
34
- "compile",
35
- "delattr",
36
- "dir",
37
- "divmod",
38
- "eval",
39
- "exec",
40
- "format",
41
- "getattr",
42
- "globals",
43
- "hasattr",
44
- "hash",
45
- "hex",
46
- "id",
47
- "input",
48
- "isinstance",
49
- "issubclass",
50
- "iter",
51
- "len",
52
- "locals",
53
- "max",
54
- "min",
55
- "next",
56
- "oct",
57
- "ord",
58
- "pow",
59
- "print",
60
- "repr",
61
- "round",
62
- "setattr",
63
- "sorted",
64
- "sum",
65
- "vars",
66
- }
67
- )
68
19
 
69
20
 
70
21
  def builtin_guard_symbol(node: ast.expr | None, imports: ImportIndex, builtin_symbols: frozenset[str]) -> str | None:
@@ -150,7 +101,7 @@ def _namespace_escapes(context: PythonFileContext, namespace_names: frozenset[st
150
101
  for node in context.nodes(ast.ImportFrom)
151
102
  ):
152
103
  return True
153
- if _native_function_escapes(context):
104
+ if native_namespace_capability_escapes(context):
154
105
  return True
155
106
  for name in context.nodes(ast.Name):
156
107
  if name.id not in namespace_names:
@@ -161,62 +112,6 @@ def _namespace_escapes(context: PythonFileContext, namespace_names: frozenset[st
161
112
  return False
162
113
 
163
114
 
164
- def _native_function_escapes(context: PythonFileContext) -> bool:
165
- function_names = _possible_native_function_names(context)
166
- namespace_function_names = _possible_native_function_names(context, symbols=_NAMESPACE_RETURNING_FUNCTIONS)
167
- module_names = frozenset(
168
- alias.asname or alias.name
169
- for node in context.nodes(ast.Import)
170
- for alias in node.names
171
- if alias.name == "builtins"
172
- )
173
- for node in context.nodes(ast.Name, ast.Attribute):
174
- if isinstance(node, ast.Attribute) and (
175
- not isinstance(node.value, ast.Name) or node.attr not in _BUILTIN_NAMESPACE_FUNCTIONS
176
- ):
177
- continue
178
- if not isinstance(node.ctx, ast.Load) or not _possible_native_function(node, function_names, module_names):
179
- continue
180
- parent = context.parents.get(node)
181
- if (
182
- not isinstance(parent, ast.Call)
183
- or parent.func is not node
184
- or _namespace_returning_function(node, namespace_function_names)
185
- ):
186
- return True
187
- return False
188
-
189
-
190
- def _possible_native_function_names(
191
- context: PythonFileContext, *, symbols: frozenset[str] = _BUILTIN_NAMESPACE_FUNCTIONS
192
- ) -> frozenset[str]:
193
- return symbols | frozenset(
194
- alias.asname or alias.name
195
- for node in context.nodes(ast.ImportFrom)
196
- if node.level == 0 and node.module == "builtins"
197
- for alias in node.names
198
- if alias.name in symbols
199
- )
200
-
201
-
202
- def _namespace_returning_function(node: ast.Name | ast.Attribute, function_names: frozenset[str]) -> bool:
203
- if isinstance(node, ast.Name):
204
- return node.id in function_names
205
- return node.attr in _NAMESPACE_RETURNING_FUNCTIONS
206
-
207
-
208
- def _possible_native_function(
209
- node: ast.Name | ast.Attribute,
210
- function_names: frozenset[str],
211
- module_names: frozenset[str],
212
- ) -> bool:
213
- if isinstance(node, ast.Name):
214
- return node.id in function_names
215
- return (
216
- isinstance(node.value, ast.Name) and node.value.id in module_names and node.attr in _BUILTIN_NAMESPACE_FUNCTIONS
217
- )
218
-
219
-
220
115
  def _ambiguous_node(
221
116
  node: ast.AST, context: PythonFileContext, mutation_targets: set[str], sensitive_names: frozenset[str]
222
117
  ) -> bool:
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ is_suppressed,
19
+ )
20
+ from sarj_python_lint.rules._pydantic_field_contract import (
21
+ DECORATOR_SOURCES,
22
+ TYPING_SOURCES,
23
+ direct_models,
24
+ unique_members,
25
+ )
26
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from sarj_python_lint._file_context import PythonFileContext
31
+
32
+
33
+ type _Primitive = str | bytes | int | float | complex | bool | None
34
+ _SCALARS = frozenset({"str", "bytes", "int", "float", "complex", "bool"})
35
+ _MODEL_CALLS = frozenset({"model_validate", "model_validate_json", "model_validate_strings", "model_construct"})
36
+ _MAX_ANNOTATION_NODES = 256
37
+ _MAX_ANNOTATION_DEPTH = 32
38
+ _VALIDATOR_ARITY = 2
39
+ _DECORATOR_STACK_SIZE = 2
40
+ _BAD = "from pydantic import BaseModel, field_validator\nclass Payload(BaseModel):\n text: str\n @field_validator('text', mode='after')\n @classmethod\n def replace(cls, value: str) -> int:\n return 3\n"
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class _Returned:
45
+ node: ast.expr
46
+ value: _Primitive
47
+
48
+
49
+ @final
50
+ class NoInvalidPydanticValidatorReturn(Rule):
51
+ id = "no-invalid-pydantic-validator-return"
52
+ code = "SARJ515"
53
+ documentation = RuleDocumentation(
54
+ default_level=Severity.WARNING,
55
+ summary="A terminal field validator must not return a literal contradicting its stored field type.",
56
+ rationale="Pydantic trusts the result of after and plain field validators without validating it against the field again. A correctly annotated validator can therefore store an integer in a string field while the model still promises a string to callers.",
57
+ remediation="Return a value satisfying every targeted field's stored contract. Split validators for different field domains, or perform input conversion in a before validator so the normal field validation follows it. Preserve intentional validation chains and wire serializers.",
58
+ category=RuleCategory.CORRECTNESS,
59
+ autofix=AutofixPolicy.NONE,
60
+ limitations=(
61
+ "Checks stable direct module-level BaseModel classes containing one synchronous field-validator method, plain annotated fields, and optional literal defaults. Inheritance, configuration, custom methods, field metadata, class decorators and escaped or mutated model classes are excluded.",
62
+ "The validator must have the canonical field_validator/classmethod stack, explicit literal field targets, mode after or plain (including omitted default after), two required positional parameters, and a sole primitive literal return, optionally after a docstring. Before, wrap, serializer, unknown and composed validator contracts abstain.",
63
+ "Only contradictions with builtin scalar types, None, canonical Optional/Union and closed string Literal domains are proved. Numeric assignability preserves bool as int and int as float/complex. Opaque aliases, numeric Literal domains, containers and annotation graphs exceeding fixed budgets abstain.",
64
+ "Every targeted field must exist; unknown-field diagnostics belong to their existing owner. Multiple validators are excluded because a later validator can repair an intermediate value. One warning identifies all contradictory fields for the returned literal.",
65
+ "Generated and vendor files are excluded; authored tests remain checked. No autofix changes validation phase, chooses domain values or narrows a public contract.",
66
+ ),
67
+ examples=(
68
+ RuleExample(
69
+ example_id="invalid-after-output",
70
+ title="An after validator stores an incompatible primitive unchanged",
71
+ outcome=ExampleOutcome.MATCH,
72
+ files=(ExampleFile.python("app/models.py", _BAD),),
73
+ focus_path=PurePosixPath("app/models.py"),
74
+ expected_count=1,
75
+ public=True,
76
+ ),
77
+ RuleExample(
78
+ example_id="valid-after-output",
79
+ title="The validator output satisfies the stored field contract",
80
+ outcome=ExampleOutcome.NO_MATCH,
81
+ files=(
82
+ ExampleFile.python(
83
+ "app/models.py", _BAD.replace("-> int", "-> str").replace("return 3", "return 'ready'")
84
+ ),
85
+ ),
86
+ focus_path=PurePosixPath("app/models.py"),
87
+ expected_count=0,
88
+ public=True,
89
+ ),
90
+ RuleExample(
91
+ example_id="plain-output-is-not-revalidated",
92
+ scenario="validation-phase",
93
+ title="A plain validator also bypasses subsequent field validation",
94
+ outcome=ExampleOutcome.MATCH,
95
+ files=(ExampleFile.python("app/models.py", _BAD.replace("mode='after'", "mode='plain'")),),
96
+ focus_path=PurePosixPath("app/models.py"),
97
+ expected_count=1,
98
+ public=True,
99
+ ),
100
+ RuleExample(
101
+ example_id="before-output-is-revalidated",
102
+ scenario="validation-phase",
103
+ title="Input conversion is followed by ordinary field validation",
104
+ outcome=ExampleOutcome.NO_MATCH,
105
+ files=(
106
+ ExampleFile.python(
107
+ "app/models.py",
108
+ _BAD.replace("text: str", "text: int")
109
+ .replace("mode='after'", "mode='before'")
110
+ .replace("-> int", "-> str")
111
+ .replace("return 3", "return '3'"),
112
+ ),
113
+ ),
114
+ focus_path=PurePosixPath("app/models.py"),
115
+ expected_count=0,
116
+ public=True,
117
+ ),
118
+ ),
119
+ )
120
+ description = documentation.summary
121
+
122
+ @override
123
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
124
+ if context.tree is None or context.generated or "field_validator" not in context.symbol_source:
125
+ return []
126
+ annotations = [node.annotation for node in context.nodes(ast.AnnAssign)]
127
+ if any(sum(1 for _ in ast.walk(annotation)) > _MAX_ANNOTATION_NODES for annotation in annotations):
128
+ return []
129
+ provenance = ResourceProvenance(context, require_static_namespace=True)
130
+ findings: list[Diagnostic] = []
131
+ for model in direct_models(context, methods=True):
132
+ proof = _model_return(model, context, provenance)
133
+ if proof is None:
134
+ continue
135
+ returned, fields = proof
136
+ if not is_suppressed(context.source_lines, returned.node.lineno, self.code):
137
+ findings.append(
138
+ Diagnostic(
139
+ context.path,
140
+ returned.node.lineno,
141
+ returned.node.col_offset + 1,
142
+ self.code,
143
+ f"This terminal validator returns a literal outside the stored contract of {', '.join(sorted(fields))}. Return valid field values or validate converted input before storing it.",
144
+ severity=Severity.WARNING,
145
+ )
146
+ )
147
+ return findings
148
+
149
+
150
+ def _model_return(
151
+ model: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance
152
+ ) -> tuple[_Returned, frozenset[str]] | None:
153
+ if not _stable_model(model, context, provenance):
154
+ return None
155
+ methods = [member for member in model.body if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef))]
156
+ if len(methods) != 1 or not isinstance(methods[0], ast.FunctionDef):
157
+ return None
158
+ fields = _plain_fields(model)
159
+ if fields is None:
160
+ return None
161
+ validator = methods[0]
162
+ targets = _targets(validator, context)
163
+ returned = _returned_literal(validator)
164
+ if targets is None or returned is None or not targets <= fields.keys():
165
+ return None
166
+ rejected = frozenset(name for name in targets if _rejects(fields[name], returned.value, context, depth=0))
167
+ return (returned, rejected) if rejected else None
168
+
169
+
170
+ def _plain_fields(model: ast.ClassDef) -> dict[str, ast.expr] | None:
171
+ fields: dict[str, ast.expr] = {}
172
+ unique = unique_members(model)
173
+ if unique & (_SCALARS | {"Literal", "Optional", "Union", "classmethod", "field_validator"}):
174
+ return None
175
+ for member in model.body:
176
+ if isinstance(member, ast.AnnAssign) and isinstance(member.target, ast.Name):
177
+ if member.target.id not in unique or member.target.id.startswith("_"):
178
+ return None
179
+ if member.value is not None and not isinstance(member.value, ast.Constant):
180
+ return None
181
+ fields[member.target.id] = member.annotation
182
+ elif not isinstance(member, (ast.FunctionDef, ast.Pass)) and not (
183
+ isinstance(member, ast.Expr)
184
+ and isinstance(member.value, ast.Constant)
185
+ and isinstance(member.value.value, str)
186
+ ):
187
+ return None
188
+ return fields
189
+
190
+
191
+ def _stable_model(model: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
192
+ if model.name in provenance.mutated or model.name in provenance.external_bindings:
193
+ return False
194
+ return all(
195
+ _stable_model_read(read, context)
196
+ for read in provenance.reads.get(model.name, ())
197
+ if provenance.binding(read.id, read) is model
198
+ )
199
+
200
+
201
+ def _stable_model_read(read: ast.Name, context: PythonFileContext) -> bool:
202
+ parent = context.parents.get(read)
203
+ if isinstance(parent, ast.Call) and parent.func is read:
204
+ return True
205
+ if isinstance(parent, (ast.arg, ast.AnnAssign)) and parent.annotation is read:
206
+ return True
207
+ if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) and parent.returns is read:
208
+ return True
209
+ if isinstance(parent, ast.Attribute) and parent.value is read and parent.attr in _MODEL_CALLS:
210
+ call = context.parents.get(parent)
211
+ return isinstance(call, ast.Call) and call.func is parent
212
+ return False
213
+
214
+
215
+ def _targets(validator: ast.FunctionDef, context: PythonFileContext) -> frozenset[str] | None:
216
+ arguments = validator.args
217
+ if validator.type_params or len(validator.decorator_list) != _DECORATOR_STACK_SIZE:
218
+ return None
219
+ if (
220
+ len((*arguments.posonlyargs, *arguments.args)) != _VALIDATOR_ARITY
221
+ or arguments.vararg
222
+ or arguments.kwarg
223
+ or arguments.kwonlyargs
224
+ or arguments.defaults
225
+ ):
226
+ return None
227
+ outer, inner = validator.decorator_list
228
+ if not (
229
+ isinstance(inner, ast.Name)
230
+ and inner.id == "classmethod"
231
+ and context.imports.builtin_is_unshadowed("classmethod")
232
+ and isinstance(outer, ast.Call)
233
+ and context.imports.resolves(outer.func, sources=DECORATOR_SOURCES, symbol="field_validator")
234
+ ):
235
+ return None
236
+ if len(outer.keywords) > 1 or any(
237
+ keyword.arg != "mode"
238
+ or not isinstance(keyword.value, ast.Constant)
239
+ or keyword.value.value not in {"after", "plain"}
240
+ for keyword in outer.keywords
241
+ ):
242
+ return None
243
+ targets: set[str] = set()
244
+ for argument in outer.args:
245
+ if not isinstance(argument, ast.Constant) or not isinstance(argument.value, str) or argument.value == "*":
246
+ return None
247
+ targets.add(argument.value)
248
+ return frozenset(targets) if targets else None
249
+
250
+
251
+ def _returned_literal(validator: ast.FunctionDef) -> _Returned | None:
252
+ body = validator.body
253
+ if (
254
+ body
255
+ and isinstance(body[0], ast.Expr)
256
+ and isinstance(body[0].value, ast.Constant)
257
+ and isinstance(body[0].value.value, str)
258
+ ):
259
+ body = body[1:]
260
+ if len(body) != 1 or not isinstance(body[0], ast.Return):
261
+ return None
262
+ value = body[0].value
263
+ if isinstance(value, ast.Constant) and (
264
+ value.value is None or isinstance(value.value, (str, bytes, int, float, complex, bool))
265
+ ):
266
+ return _Returned(value, value.value)
267
+ if (
268
+ isinstance(value, ast.UnaryOp)
269
+ and isinstance(value.op, (ast.USub, ast.UAdd))
270
+ and isinstance(value.operand, ast.Constant)
271
+ and isinstance(value.operand.value, (int, float, complex))
272
+ ):
273
+ number = value.operand.value
274
+ return _Returned(value, -number if isinstance(value.op, ast.USub) else +number)
275
+ return None
276
+
277
+
278
+ def _rejects(annotation: ast.expr, value: _Primitive, context: PythonFileContext, *, depth: int) -> bool:
279
+ if depth > _MAX_ANNOTATION_DEPTH:
280
+ return False
281
+ if isinstance(annotation, ast.Constant):
282
+ return annotation.value is None and value is not None
283
+ if isinstance(annotation, ast.Name) and context.imports.builtin_is_unshadowed(annotation.id):
284
+ return _scalar_rejects(annotation.id, value)
285
+ if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
286
+ return _rejects(annotation.left, value, context, depth=depth + 1) and _rejects(
287
+ annotation.right, value, context, depth=depth + 1
288
+ )
289
+ if not isinstance(annotation, ast.Subscript):
290
+ return False
291
+ return _subscript_rejects(annotation, value, context, depth=depth)
292
+
293
+
294
+ def _scalar_rejects(name: str, value: _Primitive) -> bool:
295
+ match name:
296
+ case "str":
297
+ return not isinstance(value, str)
298
+ case "bytes":
299
+ return not isinstance(value, bytes)
300
+ case "bool":
301
+ return not isinstance(value, bool)
302
+ case "int":
303
+ return not isinstance(value, int)
304
+ case "float":
305
+ return not isinstance(value, (int, float))
306
+ case "complex":
307
+ return not isinstance(value, (int, float, complex))
308
+ case _:
309
+ return False
310
+
311
+
312
+ def _subscript_rejects(annotation: ast.Subscript, value: _Primitive, context: PythonFileContext, *, depth: int) -> bool:
313
+ members = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
314
+ wrapper = context.imports.resolved_symbol(annotation.value, sources=TYPING_SOURCES)
315
+ if wrapper == "Optional" and len(members) == 1:
316
+ return value is not None and _rejects(members[0], value, context, depth=depth + 1)
317
+ if wrapper == "Union" and members:
318
+ return all(_rejects(member, value, context, depth=depth + 1) for member in members)
319
+ if (
320
+ wrapper == "Literal"
321
+ and members
322
+ and all(isinstance(member, ast.Constant) and isinstance(member.value, str) for member in members)
323
+ ):
324
+ return not any(isinstance(member, ast.Constant) and member.value == value for member in members)
325
+ return False