sarj-python-lint 1.53.0__tar.gz → 1.54.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__main__.py +5 -0
  4. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  5. sarj_python_lint-1.54.0/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +467 -0
  6. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/.gitignore +0 -0
  7. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/LICENSE +0 -0
  8. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/README.md +0 -0
  9. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_source.py +0 -0
  17. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  20. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  29. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  31. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  32. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  33. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  34. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  35. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  36. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  37. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  38. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  39. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  40. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  41. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  42. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  43. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  44. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  45. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  46. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  47. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  48. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  49. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  50. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  51. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  52. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  53. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  54. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  55. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  56. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  57. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  58. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  59. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  60. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  61. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  62. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  63. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  64. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  65. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  66. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  67. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  68. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  69. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  70. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  71. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  72. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  73. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  74. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  75. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  76. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  77. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  78. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  79. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  80. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  81. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  82. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  83. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  84. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  85. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  86. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  87. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  88. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  89. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  90. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  91. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  92. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  93. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  94. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  95. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  96. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  97. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  98. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  99. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  100. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  101. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  102. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  103. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  104. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  105. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  106. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  107. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  108. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  109. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  110. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  111. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  112. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  113. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  114. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  115. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  116. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  117. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  118. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  119. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  120. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  121. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  122. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  123. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  124. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  125. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  126. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  127. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  128. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  129. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  130. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  131. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  132. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  133. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  134. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  135. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  136. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  137. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  138. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  139. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  140. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  141. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  142. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  143. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  144. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  145. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  146. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  147. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  148. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  149. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  150. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  151. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  152. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  153. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  154. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  155. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  156. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  157. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  158. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  159. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  160. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  161. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  162. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  163. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  164. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  165. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  166. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  167. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  168. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  169. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  170. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  171. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  172. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  173. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  174. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  175. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  176. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  177. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  178. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  179. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  180. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  181. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  182. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  183. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  184. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  185. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  186. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  187. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  188. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  189. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  190. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  191. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  192. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  193. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  194. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  195. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  196. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  197. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  198. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  199. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  200. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  201. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  202. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  203. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  204. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  205. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  206. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  207. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  208. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  209. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  210. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  211. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  212. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  213. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  214. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  215. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  216. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  217. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  218. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  219. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  220. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  221. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  222. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  223. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  224. {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.53.0
3
+ Version: 1.54.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.53.0"
3
+ version = "1.54.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -171,10 +171,15 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
171
171
  "SARJ094": frozenset({"SARJ008", "SARJ447"}),
172
172
  "SARJ095": frozenset({"SARJ468"}),
173
173
  "SARJ099": frozenset({"SARJ420"}),
174
+ # A proven external or nested request boundary already requires schema
175
+ # validation at this field read. Retain standalone shape advice when
176
+ # that more specific rule is unselected or locally suppressed.
177
+ "SARJ411": frozenset({"SARJ521"}),
174
178
  # Any-valued mappings are the stronger correctness failure. SARJ008
175
179
  # still owns every fixed dictionary return when selected alone, while
176
180
  # an all-rules run emits one actionable diagnostic for this overlap.
177
181
  "SARJ447": frozenset({"SARJ008"}),
182
+ "SARJ450": frozenset({"SARJ521"}),
178
183
  "SARJ457": frozenset({"SARJ045"}),
179
184
  "SARJ465": frozenset({"SARJ071"}),
180
185
  "SARJ491": frozenset({"SARJ400"}),
@@ -189,6 +189,7 @@ from sarj_python_lint.rules.require_public_dependency_contract import RequirePub
189
189
  from sarj_python_lint.rules.require_pydantic_for_external_json import (
190
190
  RequirePydanticForExternalJson,
191
191
  )
192
+ from sarj_python_lint.rules.require_pydantic_for_mapping_shape import RequirePydanticForMappingShape
192
193
  from sarj_python_lint.rules.require_pydantic_for_structured_payload import RequirePydanticForStructuredPayload
193
194
  from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
194
195
  RequirePydanticOrdinalLowerBound,
@@ -305,6 +306,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
305
306
  PreferMatchAssertNever.id: PreferMatchAssertNever,
306
307
  RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
307
308
  RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
309
+ RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
308
310
  PreferModuleLevelConstant.id: PreferModuleLevelConstant,
309
311
  PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
310
312
  PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
@@ -0,0 +1,467 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
20
+ from sarj_python_lint.rules._imports import ImportIndex
21
+ from sarj_python_lint.rules._paths import is_test_path
22
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Iterator
27
+
28
+ from sarj_python_lint._file_context import PythonFileContext
29
+
30
+
31
+ _TYPING = frozenset({"typing", "typing_extensions"})
32
+ _MAPPINGS = frozenset(
33
+ {
34
+ "typing.Dict",
35
+ "typing.Mapping",
36
+ "typing.MutableMapping",
37
+ "collections.abc.Mapping",
38
+ "collections.abc.MutableMapping",
39
+ }
40
+ )
41
+ _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
+ _MAX_DEPTH = 32
60
+ _MAPPING_ARITY = 2
61
+ _BAD = (
62
+ "def verified(raw: dict[str, object]) -> bool:\n"
63
+ " match raw.get('factor_age'):\n"
64
+ " case [int(), int(age)] if not isinstance(age, bool):\n"
65
+ " return age >= 0\n"
66
+ " case _:\n"
67
+ " return False\n"
68
+ )
69
+ _GOOD = (
70
+ "from typing import Annotated\n"
71
+ "from pydantic import BaseModel, Field, StrictInt, ValidationError\n"
72
+ "class FactorClaim(BaseModel):\n"
73
+ " factor_age: tuple[StrictInt, Annotated[StrictInt, Field(ge=0)]]\n"
74
+ "def verified(raw: object) -> bool:\n"
75
+ " try:\n"
76
+ " FactorClaim.model_validate(raw)\n"
77
+ " except ValidationError:\n"
78
+ " return False\n"
79
+ " return True\n"
80
+ )
81
+
82
+
83
+ @final
84
+ class RequirePydanticForMappingShape(Rule):
85
+ id = "require-pydantic-for-mapping-shape"
86
+ code = "SARJ521"
87
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
88
+ default_level=Severity.WARNING,
89
+ summary="Declare opaque mapping field shapes in Pydantic instead of probing them by hand.",
90
+ rationale=(
91
+ "Fixed-key reads followed by runtime type, shape, or discriminator checks spread a record contract "
92
+ "across control flow. An object/Any/JsonValue mapping does not declare those field contracts."
93
+ ),
94
+ remediation=(
95
+ "Parse the raw object with a named Pydantic BaseModel or RootModel before using typed attributes. "
96
+ "Prefer strict fields, Field bounds, aliases/AliasPath and discriminated unions to handwritten probes. "
97
+ "For an already trusted internal record, declare a precise named record such as TypedDict instead of "
98
+ "parsing it again. Preserve rejection, defaults and fail-closed behavior explicitly; bool is an int subclass."
99
+ ),
100
+ category=RuleCategory.MAINTAINABILITY,
101
+ autofix=AutofixPolicy.NONE,
102
+ limitations=(
103
+ "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
+ "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.",
107
+ "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
+ ),
109
+ examples=(
110
+ RuleExample(
111
+ example_id="manual-mapping-shape",
112
+ title="A match statement parses an opaque field by hand",
113
+ outcome=ExampleOutcome.MATCH,
114
+ files=(ExampleFile.python("app/claims.py", _BAD),),
115
+ focus_path=PurePosixPath("app/claims.py"),
116
+ expected_count=1,
117
+ public=True,
118
+ ),
119
+ RuleExample(
120
+ example_id="declared-mapping-shape",
121
+ title="Strict fields and bounds declare the accepted shape",
122
+ outcome=ExampleOutcome.NO_MATCH,
123
+ files=(ExampleFile.python("app/claims.py", _GOOD),),
124
+ focus_path=PurePosixPath("app/claims.py"),
125
+ expected_count=0,
126
+ public=True,
127
+ ),
128
+ ),
129
+ )
130
+ description = documentation.summary
131
+
132
+ @override
133
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
134
+ if (
135
+ context.tree is None
136
+ or context.generated
137
+ or not {"vendor", "vendored", "third_party"}.isdisjoint(context.path.parts)
138
+ ):
139
+ return []
140
+ facts = _Facts(context, context.tree)
141
+ if facts.provenance.wildcard or facts.provenance.dynamic_namespace:
142
+ return []
143
+ sources: set[ast.expr] = set()
144
+ for node in context.nodes(ast.Call, ast.Compare, ast.Match, ast.If, ast.While, ast.IfExp, ast.Assert):
145
+ if facts.raw_boundary(node) or _test_assertion(node, context):
146
+ continue
147
+ sources.update(_probe_sources(node, facts))
148
+ return [
149
+ Diagnostic(
150
+ path=context.path,
151
+ line=node.lineno,
152
+ col=node.col_offset + 1,
153
+ code=self.code,
154
+ severity=Severity.WARNING,
155
+ message="Declare this mapping field's type, shape and discriminator in a named Pydantic model; use a precise named record for already trusted internals instead of probing an opaque get/subscript result.",
156
+ )
157
+ for node in sorted(sources, key=lambda source: (source.lineno, source.col_offset))
158
+ if not is_suppressed(context.source_lines, node.lineno, self.code)
159
+ ]
160
+
161
+
162
+ class _Facts:
163
+ def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
164
+ self.context = context
165
+ self.provenance = ResourceProvenance(context, require_static_namespace=True)
166
+ self.semantics = AnnotationSemantics.from_tree(tree)
167
+ self.imports = {
168
+ node: ImportIndex.from_tree(ast.Module(body=[node], type_ignores=[]))
169
+ for node in context.nodes(ast.Import, ast.ImportFrom)
170
+ }
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
+ }
178
+
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)
217
+
218
+ def qualified(self, node: ast.expr, at: ast.AST) -> str | None:
219
+ root = node
220
+ while isinstance(root, ast.Attribute):
221
+ root = root.value
222
+ if not isinstance(root, ast.Name) or root.id in self.provenance.mutated | self.provenance.external_bindings:
223
+ return None
224
+ binding = self.provenance.binding(root.id, at)
225
+ parent = self.context.parents.get(binding) if binding is not None else None
226
+ index = self.imports.get(parent) if isinstance(parent, ast.Import | ast.ImportFrom) else None
227
+ return index.resolved_qualified_name(node) if index is not None else None
228
+
229
+ def builtin(self, node: ast.expr, name: str, at: ast.AST) -> bool:
230
+ return (
231
+ self.provenance.builtin_namespace_untouched
232
+ and isinstance(node, ast.Name)
233
+ and node.id == name
234
+ and self.provenance.binding(name, at) is None
235
+ )
236
+
237
+ def annotation(self, node: ast.expr, at: ast.AST, *, mapping: bool, depth: int = 0) -> bool:
238
+ if depth >= _MAX_DEPTH:
239
+ return False
240
+ parsed = self.semantics.parse(node)
241
+ if parsed is None:
242
+ return False
243
+ if isinstance(parsed, ast.Name) and parsed.id in self.semantics.aliases:
244
+ return self.alias_annotation(parsed, at, mapping=mapping, depth=depth)
245
+ if isinstance(parsed, ast.BinOp) and isinstance(parsed.op, ast.BitOr):
246
+ return any(
247
+ self.annotation(member, at, mapping=mapping, depth=depth + 1) for member in (parsed.left, parsed.right)
248
+ )
249
+ qualified = self.qualified(parsed, at)
250
+ if not mapping and (
251
+ self.builtin(parsed, "object", at)
252
+ or qualified in {"typing.Any", "typing_extensions.Any", "pydantic.JsonValue"}
253
+ ):
254
+ return True
255
+ if not isinstance(parsed, ast.Subscript):
256
+ return False
257
+ return self.subscription_annotation(parsed, at, mapping=mapping, depth=depth)
258
+
259
+ def alias_annotation(self, node: ast.Name, at: ast.AST, *, mapping: bool, depth: int) -> bool:
260
+ binding = self.provenance.binding(node.id, at)
261
+ if not isinstance(binding, ast.Name) or not isinstance(
262
+ self.context.parents.get(binding), ast.TypeAlias | ast.AnnAssign
263
+ ):
264
+ return False
265
+ value = self.semantics.aliases[node.id]
266
+ return self.annotation(value, value, mapping=mapping, depth=depth + 1)
267
+
268
+ def subscription_annotation(self, node: ast.Subscript, at: ast.AST, *, mapping: bool, depth: int) -> bool:
269
+ qualified = self.qualified(node.value, at)
270
+ members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
271
+ wrapper = qualified.rpartition(".")[-1] if qualified is not None else None
272
+ if qualified is not None and qualified.partition(".")[0] in _TYPING:
273
+ if wrapper in _WRAPPERS and members:
274
+ return self.annotation(members[0], at, mapping=mapping, depth=depth + 1)
275
+ if wrapper == "Union":
276
+ return any(self.annotation(member, at, mapping=mapping, depth=depth + 1) for member in members)
277
+ if not mapping or len(members) != _MAPPING_ARITY:
278
+ return False
279
+ key, value = members
280
+ return (
281
+ (self.builtin(node.value, "dict", at) or qualified in _MAPPINGS)
282
+ and self.builtin(key, "str", at)
283
+ and self.annotation(value, at, mapping=False, depth=depth + 1)
284
+ )
285
+
286
+ def mapping(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> bool:
287
+ if not isinstance(node, ast.Name) or len(seen) >= _MAX_DEPTH or node.id in self.provenance.external_bindings:
288
+ return False
289
+ binding = self.provenance.binding(node.id, node)
290
+ if binding is None or binding in seen:
291
+ return False
292
+ if isinstance(binding, ast.arg):
293
+ return binding.annotation is not None and self.annotation(binding.annotation, binding, mapping=True)
294
+ parent = self.context.parents.get(binding)
295
+ if isinstance(parent, ast.AnnAssign) and parent.annotation is not None:
296
+ return self.annotation(parent.annotation, binding, mapping=True)
297
+ value = self.provenance.assignment_value(binding, node) if isinstance(binding, ast.Name) else None
298
+ return value is not None and self.mapping(value, seen=seen | {binding})
299
+
300
+ def field(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> ast.expr | None:
301
+ if len(seen) >= _MAX_DEPTH:
302
+ return None
303
+ if isinstance(node, ast.NamedExpr):
304
+ return self.field(node.value, seen=seen)
305
+ if isinstance(node, ast.Name):
306
+ binding = self.provenance.binding(node.id, node)
307
+ if not isinstance(binding, ast.Name) or binding in seen or node.id in self.provenance.external_bindings:
308
+ return None
309
+ value = self.provenance.assignment_value(binding, node)
310
+ return self.field(value, seen=seen | {binding}) if value is not None else None
311
+ if (
312
+ isinstance(node, ast.Subscript)
313
+ and isinstance(node.ctx, ast.Load)
314
+ and _string(node.slice)
315
+ and self.mapping(node.value)
316
+ ):
317
+ return node
318
+ if (receiver := _get_receiver(node)) is not None and self.mapping(receiver):
319
+ return node
320
+ return None
321
+
322
+ 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
329
+
330
+ def raw_validator(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
331
+ if function in self.callbacks:
332
+ 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
344
+
345
+
346
+ def _string(node: ast.expr) -> bool:
347
+ return isinstance(node, ast.Constant) and isinstance(node.value, str)
348
+
349
+
350
+ def _test_assertion(node: ast.AST, context: PythonFileContext) -> bool:
351
+ if not is_test_path(context.path):
352
+ return False
353
+ current: ast.AST | None = node
354
+ while current is not None:
355
+ if isinstance(current, ast.Assert):
356
+ return True
357
+ if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
358
+ return False
359
+ current = context.parents.get(current)
360
+ return False
361
+
362
+
363
+ def _get_receiver(node: ast.expr) -> ast.expr | None:
364
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute) or node.func.attr != "get":
365
+ return None
366
+ if len(node.args) not in {1, 2} or node.keywords or not _string(node.args[0]):
367
+ return None
368
+ return node.func.value
369
+
370
+
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
+ def _probe_sources(node: ast.AST, facts: _Facts) -> Iterator[ast.expr]:
406
+ match node:
407
+ case ast.Call(func=func, args=[value, _], keywords=[]) if facts.builtin(func, "isinstance", node):
408
+ if (field := facts.field(value)) is not None:
409
+ yield field
410
+ case ast.Compare():
411
+ yield from _comparison_fields(node, facts)
412
+ case ast.Match(subject=subject, cases=cases):
413
+ for case in cases:
414
+ yield from _match_fields(subject, case.pattern, facts)
415
+ case ast.If(test=test) | ast.While(test=test) | ast.IfExp(test=test) | ast.Assert(test=test):
416
+ yield from _truth_fields(test, facts)
417
+ case _:
418
+ return
419
+
420
+
421
+ def _comparison_fields(node: ast.Compare, facts: _Facts) -> Iterator[ast.expr]:
422
+ operands = [node.left, *node.comparators]
423
+ for value in operands:
424
+ if (
425
+ any(_literal(other) for other in operands if other is not value)
426
+ and (field := facts.field(value)) is not None
427
+ ):
428
+ yield field
429
+ if (field := _type_field(value, facts)) is not None:
430
+ yield field
431
+
432
+
433
+ def _literal(node: ast.expr) -> bool:
434
+ return isinstance(node, ast.Constant) or (
435
+ isinstance(node, ast.Set | ast.List | ast.Tuple) and all(isinstance(item, ast.Constant) for item in node.elts)
436
+ )
437
+
438
+
439
+ def _type_field(node: ast.expr, facts: _Facts) -> ast.expr | None:
440
+ if not isinstance(node, ast.Call) or len(node.args) != 1 or node.keywords:
441
+ return None
442
+ return facts.field(node.args[0]) if facts.builtin(node.func, "type", node) else None
443
+
444
+
445
+ def _truth_fields(node: ast.expr, facts: _Facts) -> Iterator[ast.expr]:
446
+ if (field := facts.field(node)) is not None:
447
+ yield field
448
+ elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
449
+ yield from _truth_fields(node.operand, facts)
450
+ elif isinstance(node, ast.BoolOp):
451
+ for value in node.values:
452
+ yield from _truth_fields(value, facts)
453
+
454
+
455
+ def _match_fields(subject: ast.expr, pattern: ast.pattern, facts: _Facts) -> Iterator[ast.expr]:
456
+ if isinstance(pattern, ast.MatchAs):
457
+ if pattern.pattern is not None:
458
+ yield from _match_fields(subject, pattern.pattern, facts)
459
+ elif isinstance(pattern, ast.MatchOr):
460
+ for member in pattern.patterns:
461
+ yield from _match_fields(subject, member, facts)
462
+ elif isinstance(subject, ast.Tuple | ast.List) and isinstance(pattern, ast.MatchSequence):
463
+ if len(subject.elts) == len(pattern.patterns):
464
+ for value, member in zip(subject.elts, pattern.patterns, strict=True):
465
+ yield from _match_fields(value, member, facts)
466
+ elif not isinstance(pattern, ast.MatchStar) and (field := facts.field(subject)) is not None:
467
+ yield field