sarj-python-lint 1.56.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.56.0 → sarj_python_lint-1.57.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/pyproject.toml +1 -1
  3. sarj_python_lint-1.57.0/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +260 -0
  4. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +40 -118
  5. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/.gitignore +0 -0
  6. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/LICENSE +0 -0
  7. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/README.md +0 -0
  8. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_source.py +0 -0
  17. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  20. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  29. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  31. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  32. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  33. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  34. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  35. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  36. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  37. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  38. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  39. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  40. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  41. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  42. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  43. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  44. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  45. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  46. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  47. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  48. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  49. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  50. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  51. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  52. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  53. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  54. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  55. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  56. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  59. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  60. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  61. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  62. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  63. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  64. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  65. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  66. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  67. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  68. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  69. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  70. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  71. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  72. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  73. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  74. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  75. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  76. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  77. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  78. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  79. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  80. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  81. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  82. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  83. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  84. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  85. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  86. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  87. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  88. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  89. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  90. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  91. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  92. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  93. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  94. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  95. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  96. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  97. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  98. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  99. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  100. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  101. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  102. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  103. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  104. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  105. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  106. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  107. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  108. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  109. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  110. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  111. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  112. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  113. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  114. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  115. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  116. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  117. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  118. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  119. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  120. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  121. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  122. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  123. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  124. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  125. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  126. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  127. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  128. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  129. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  130. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  131. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  132. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  133. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  134. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  135. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  136. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  137. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  138. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  139. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  140. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  141. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  142. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  143. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  144. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  145. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  146. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  147. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  148. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  149. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  150. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  151. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  152. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  153. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  154. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  155. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  156. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  157. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  158. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  159. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  160. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  161. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  162. {sarj_python_lint-1.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  164. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  165. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  166. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  167. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  168. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  169. {sarj_python_lint-1.56.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.56.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.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  173. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  174. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  175. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  176. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  177. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  178. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  179. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  180. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  181. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  182. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  183. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  184. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  185. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  186. {sarj_python_lint-1.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  188. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  189. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  190. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  191. {sarj_python_lint-1.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  193. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  194. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  195. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  196. {sarj_python_lint-1.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  198. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  199. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  200. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  201. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  202. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  203. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  204. {sarj_python_lint-1.56.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.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  207. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  208. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  209. {sarj_python_lint-1.56.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.56.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.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  213. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  214. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  215. {sarj_python_lint-1.56.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.56.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.56.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.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  219. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  220. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  221. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  222. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  223. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  224. {sarj_python_lint-1.56.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  225. {sarj_python_lint-1.56.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.56.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.56.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" }]
@@ -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
+ )
@@ -18,6 +18,7 @@ from sarj_python_lint.rule_base import (
18
18
  )
19
19
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
20
20
  from sarj_python_lint.rules._imports import ImportIndex
21
+ from sarj_python_lint.rules._mapping_shape_callbacks import RawMappingCallbacks
21
22
  from sarj_python_lint.rules._paths import is_test_path
22
23
  from sarj_python_lint.rules._resource_provenance import ResourceProvenance
23
24
 
@@ -39,23 +40,6 @@ _MAPPINGS = frozenset(
39
40
  }
40
41
  )
41
42
  _WRAPPERS = frozenset({"Annotated", "Final", "ClassVar", "Required", "NotRequired", "ReadOnly", "Optional"})
42
- _VALIDATORS = frozenset(
43
- {
44
- "pydantic.model_validator",
45
- "pydantic.field_validator",
46
- "pydantic.functional_validators.model_validator",
47
- "pydantic.functional_validators.field_validator",
48
- }
49
- )
50
- _DISCRIMINATORS = frozenset({"pydantic.Discriminator", "pydantic.types.Discriminator"})
51
- _RAW_METADATA = frozenset(
52
- {
53
- "pydantic.BeforeValidator",
54
- "pydantic.WrapValidator",
55
- "pydantic.functional_validators.BeforeValidator",
56
- "pydantic.functional_validators.WrapValidator",
57
- }
58
- )
59
43
  _MAX_DEPTH = 32
60
44
  _MAPPING_ARITY = 2
61
45
  _BAD = (
@@ -102,8 +86,8 @@ class RequirePydanticForMappingShape(Rule):
102
86
  limitations=(
103
87
  "Checks literal-string get/subscript reads on parameters or local names annotated as standard string mappings with object, Any or pydantic.JsonValue values. Stable module type aliases, quoted annotations, wrappers and local value aliases are recognized with bounded resolution.",
104
88
  "A field must feed builtin isinstance/type checks, literal comparisons, truth tests or a constrained match pattern. Ordinary reads/forwarding, dynamic keys, homogeneous typed maps, imported application aliases, attributes and ambiguous/rebound assignments are excluded.",
105
- "Import-resolved Pydantic before/wrap decorator or metadata validators and explicitly registered Discriminator callbacks own raw input normalization and are excluded. Stable callback function aliases and simple local callable instances/bound __call__ methods are recognized. Prefer declarative fields before introducing such callbacks.",
106
- "Callable classes must be unique undecorated local declarations with no bases, class options, custom constructor or descriptor hooks; only an undecorated __call__ method and inert class data are admitted. Imported, inherited, decorated, rebound and directly mutated callback classes/instances abstain.",
89
+ "Import-resolved Pydantic before/wrap decorators with literal or stable local mode aliases, metadata validators and registered Discriminator callbacks own raw normalization. Stable function aliases, functools.partial binding only keyword-only configuration and simple bound/static/class methods are recognized. Prefer declarative fields before introducing callbacks.",
90
+ "Callback classes must be stable direct local declarations with no bases, class options, custom constructor or descriptor hooks. Import-proven typing.final and empty __slots__ preserve ownership. Other decorators, imported/inherited classes and directly mutated callback owners abstain. Nonescaping direct local helpers inherit raw ownership; escaped helpers remain checked. Generic type parameters shadow matching builtin/import annotation names.",
107
91
  "Parsing helpers in authored tests are checked, but assert-contained test oracles are excluded. Generated/vendor sources and dynamic namespaces are excluded. This is a warning with no automatic rewrite because coercion, missing-field handling and error policy can differ.",
108
92
  ),
109
93
  examples=(
@@ -168,58 +152,20 @@ class _Facts:
168
152
  node: ImportIndex.from_tree(ast.Module(body=[node], type_ignores=[]))
169
153
  for node in context.nodes(ast.Import, ast.ImportFrom)
170
154
  }
171
- self.callbacks = {
172
- binding
173
- for call in context.nodes(ast.Call)
174
- if (qualified := self.qualified(call.func, call)) in _DISCRIMINATORS | _RAW_METADATA
175
- if (argument := _callback_argument(call, qualified)) is not None
176
- if (binding := self.callback(argument)) is not None
177
- }
155
+ self.callbacks = RawMappingCallbacks(context, self.provenance, self.qualified, self.builtin, self.shadowed)
178
156
 
179
- def callback(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> ast.AST | None:
180
- if len(seen) >= _MAX_DEPTH:
181
- return None
182
- if isinstance(node, ast.Call):
183
- return self.callable_method(node, seen=seen)
184
- if isinstance(node, ast.Attribute) and node.attr == "__call__":
185
- return self.callable_method(node.value, seen=seen)
186
- if not isinstance(node, ast.Name) or node.id in self.provenance.mutated | self.provenance.external_bindings:
187
- return None
188
- binding = self.provenance.binding(node.id, node)
189
- if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef):
190
- return binding if not binding.decorator_list else None
191
- if not isinstance(binding, ast.Name) or binding in seen:
192
- return None
193
- value = self.provenance.assignment_value(binding, node)
194
- return self.callback(value, seen=seen | {binding}) if value is not None else None
195
-
196
- def callable_method(self, node: ast.expr, *, seen: frozenset[ast.AST]) -> ast.FunctionDef | None:
197
- if len(seen) >= _MAX_DEPTH:
198
- return None
199
- if isinstance(node, ast.Name):
200
- if node.id in self.provenance.mutated | self.provenance.external_bindings:
201
- return None
202
- binding = self.provenance.binding(node.id, node)
203
- if not isinstance(binding, ast.Name) or binding in seen:
204
- return None
205
- value = self.provenance.assignment_value(binding, node)
206
- return self.callable_method(value, seen=seen | {binding}) if value is not None else None
207
- if not isinstance(node, ast.Call) or node.args or node.keywords or not isinstance(node.func, ast.Name):
208
- return None
209
- if node.func.id in self.provenance.mutated | self.provenance.external_bindings:
210
- return None
211
- binding = self.provenance.binding(node.func.id, node)
212
- if not isinstance(binding, ast.ClassDef) or not isinstance(
213
- self.context.parents.get(binding), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef
214
- ):
215
- return None
216
- return _plain_callable_method(binding)
157
+ def shadowed(self, name: str, at: ast.AST) -> bool:
158
+ return _type_parameter_shadow(name, at, self.context)
217
159
 
218
160
  def qualified(self, node: ast.expr, at: ast.AST) -> str | None:
219
161
  root = node
220
162
  while isinstance(root, ast.Attribute):
221
163
  root = root.value
222
- if not isinstance(root, ast.Name) or root.id in self.provenance.mutated | self.provenance.external_bindings:
164
+ if (
165
+ not isinstance(root, ast.Name)
166
+ or root.id in self.provenance.mutated | self.provenance.external_bindings
167
+ or _type_parameter_shadow(root.id, at, self.context)
168
+ ):
223
169
  return None
224
170
  binding = self.provenance.binding(root.id, at)
225
171
  parent = self.context.parents.get(binding) if binding is not None else None
@@ -231,6 +177,7 @@ class _Facts:
231
177
  self.provenance.builtin_namespace_untouched
232
178
  and isinstance(node, ast.Name)
233
179
  and node.id == name
180
+ and not _type_parameter_shadow(name, at, self.context)
234
181
  and self.provenance.binding(name, at) is None
235
182
  )
236
183
 
@@ -240,6 +187,8 @@ class _Facts:
240
187
  parsed = self.semantics.parse(node)
241
188
  if parsed is None:
242
189
  return False
190
+ if isinstance(parsed, ast.Name) and _type_parameter_shadow(parsed.id, at, self.context):
191
+ return False
243
192
  if isinstance(parsed, ast.Name) and parsed.id in self.semantics.aliases:
244
193
  return self.alias_annotation(parsed, at, mapping=mapping, depth=depth)
245
194
  if isinstance(parsed, ast.BinOp) and isinstance(parsed.op, ast.BitOr):
@@ -320,27 +269,34 @@ class _Facts:
320
269
  return None
321
270
 
322
271
  def raw_boundary(self, node: ast.AST) -> bool:
323
- current: ast.AST | None = node
324
- while current is not None:
325
- if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef) and self.raw_validator(current):
326
- return True
327
- current = self.context.parents.get(current)
328
- return False
272
+ return self.callbacks.owns(node)
329
273
 
330
- def raw_validator(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
331
- if function in self.callbacks:
274
+
275
+ def _type_parameter_shadow(name: str, at: ast.AST, context: PythonFileContext) -> bool:
276
+ outside: set[ast.AST] = set()
277
+ current = at
278
+ while (parent := context.parents.get(current)) is not None:
279
+ if (
280
+ isinstance(parent, ast.arguments)
281
+ and (current in parent.defaults or current in parent.kw_defaults)
282
+ and (owner := context.parents.get(parent)) is not None
283
+ ):
284
+ outside.add(owner)
285
+ if (
286
+ isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias)
287
+ and parent not in outside
288
+ and not (
289
+ isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
290
+ and current in parent.decorator_list
291
+ )
292
+ and any(
293
+ isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name
294
+ for parameter in parent.type_params
295
+ )
296
+ ):
332
297
  return True
333
- for decorator in function.decorator_list:
334
- if not isinstance(decorator, ast.Call) or self.qualified(decorator.func, decorator) not in _VALIDATORS:
335
- continue
336
- if any(
337
- keyword.arg == "mode"
338
- and isinstance(keyword.value, ast.Constant)
339
- and keyword.value.value in {"before", "wrap"}
340
- for keyword in decorator.keywords
341
- ):
342
- return True
343
- return False
298
+ current = parent
299
+ return False
344
300
 
345
301
 
346
302
  def _string(node: ast.expr) -> bool:
@@ -368,40 +324,6 @@ def _get_receiver(node: ast.expr) -> ast.expr | None:
368
324
  return node.func.value
369
325
 
370
326
 
371
- def _callback_argument(call: ast.Call, qualified: str | None) -> ast.expr | None:
372
- if call.args:
373
- return call.args[0]
374
- parameter = "discriminator" if qualified in _DISCRIMINATORS else "func"
375
- return next((keyword.value for keyword in call.keywords if keyword.arg == parameter), None)
376
-
377
-
378
- def _plain_callable_method(node: ast.ClassDef) -> ast.FunctionDef | None:
379
- if node.bases or node.keywords or node.decorator_list or node.type_params:
380
- return None
381
- method: ast.FunctionDef | None = None
382
- for statement in node.body:
383
- match statement:
384
- case ast.FunctionDef(name="__call__", decorator_list=[]) if method is None:
385
- method = statement
386
- case ast.Assign(value=ast.Constant(), targets=targets) if all(
387
- isinstance(target, ast.Name) and _inert_callable_attribute(target.id) for target in targets
388
- ):
389
- continue
390
- case ast.AnnAssign(target=ast.Name(id=name), value=None | ast.Constant()) if _inert_callable_attribute(
391
- name
392
- ):
393
- continue
394
- case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
395
- continue
396
- case _:
397
- return None
398
- return method
399
-
400
-
401
- def _inert_callable_attribute(name: str) -> bool:
402
- return name == "__name__" or not (name.startswith("__") and name.endswith("__"))
403
-
404
-
405
327
  def _probe_sources(node: ast.AST, facts: _Facts) -> Iterator[ast.expr]:
406
328
  match node:
407
329
  case ast.Call(func=func, args=[value, _], keywords=[]) if facts.builtin(func, "isinstance", node):