sarj-python-lint 1.56.0__tar.gz → 1.58.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.58.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/pyproject.toml +1 -1
  3. sarj_python_lint-1.58.0/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +260 -0
  4. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resource_provenance.py +1 -1
  5. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +30 -0
  6. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +152 -45
  7. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +40 -118
  8. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/.gitignore +0 -0
  9. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/LICENSE +0 -0
  10. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/README.md +0 -0
  11. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__init__.py +0 -0
  12. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__main__.py +0 -0
  13. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  14. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_source.py +0 -0
  20. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  23. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/json_boundary.py +0 -0
  24. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/py.typed +0 -0
  25. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/ratchet.py +0 -0
  26. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rule_base.py +0 -0
  27. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  29. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  30. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  31. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  32. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  33. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  34. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  35. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  36. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  37. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  38. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  39. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  40. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  41. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  42. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  43. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  44. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  45. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  46. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  47. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  48. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  49. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  50. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  51. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  52. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  53. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  54. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  55. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  56. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  57. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  58. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  59. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  60. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  61. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  62. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  63. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  64. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  65. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  66. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  67. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  68. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  69. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  70. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  71. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  72. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  73. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  74. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  75. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  76. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  77. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  78. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  79. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  80. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  81. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  82. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  83. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  84. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  85. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  86. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  87. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  88. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  89. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  90. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  91. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  92. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  93. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  94. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  95. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  96. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  97. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  98. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  99. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  100. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  101. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  102. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  103. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  104. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  105. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  106. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  107. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  108. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  109. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  110. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  111. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  112. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  113. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  114. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  115. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  116. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  117. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  118. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  120. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  121. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  122. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.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.58.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.58.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.58.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  128. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  130. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  131. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  132. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  133. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  134. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.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.58.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.58.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  139. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  140. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  141. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  142. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  145. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  146. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  148. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  149. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  152. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  153. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  155. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  156. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  157. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  159. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  160. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  161. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  162. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  164. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  165. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  166. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  167. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  168. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  169. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.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.58.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  173. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  174. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  175. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  176. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  177. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  178. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  179. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  180. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  181. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  182. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  183. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  184. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  185. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  186. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  188. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  189. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  190. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  191. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  193. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  194. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  195. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  196. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  198. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  199. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  200. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  201. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  202. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  203. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  204. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  207. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  208. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  209. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.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.58.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  213. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  214. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  215. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.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.58.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  219. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  220. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  221. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  222. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  223. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  224. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  225. {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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.58.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
+ )
@@ -37,7 +37,7 @@ _RESOURCE_OWNERS = MappingProxyType(
37
37
  "operator": frozenset({"setitem", "delitem", "iadd", "iconcat", "imul", "iand", "isub", "ixor", "ior"}),
38
38
  "unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
39
39
  "typing": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
40
- "typing_extensions": frozenset({"final", "Literal", "Annotated"}),
40
+ "typing_extensions": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
41
41
  "pydantic": frozenset(
42
42
  {
43
43
  "BaseModel",
@@ -155,6 +155,7 @@ class NoDiscardedValidationResult(Rule):
155
155
  "Stable local callable aliases with prior direct assignments are followed through bounded chains. Conditional assignments and unresolved callable escapes are excluded.",
156
156
  "Direct execution in class bodies is excluded because sequential lookup and metaclass-prepared namespaces can supply different callees. Method bodies remain checked.",
157
157
  "Functions with explicit mutation, standalone command calls, generators, nested definitions, or context-manager/try/loop orchestration are excluded. This is not a general purity proof.",
158
+ "Calls inside returned values must resolve to supported builtin or source-resolved nominal value construction. Opaque command functions, receiver calls, and direct returned attribute/subscript reads are excluded; a conversion around an unknown command does not prove validation.",
158
159
  "Import-resolved pytest.raises and source-proven unittest exception assertion blocks observe the exception contract and are excluded. Generated and vendored sources are excluded; maintained tests are checked.",
159
160
  "Top-level return contracts follow stable lexical aliases, first-party type imports, unions, and Annotated type content through a bounded source-only analysis. Nullable collection elements and annotation metadata do not make the returned collection nullable.",
160
161
  "Nullable, object, Any, Never, NoReturn, generic type-parameter, structural protocol, and unresolved return contracts are excluded. No automatic assignment, deletion, or validation rewrite is offered.",
@@ -246,12 +247,26 @@ class _ValidationResults:
246
247
  and _value_validator(function)
247
248
  and provenance is not None
248
249
  and self._value_annotation(provenance, function.returns, function, _MAX_ANNOTATION_DEPTH, frozenset())
250
+ and self._returned_constructions(provenance, function)
249
251
  )
250
252
  return self.contracts[function] and not any(
251
253
  _namespace_uncertain(provenance, owner.path) or _namespace_uncertain(provenance, provenance.context.path)
252
254
  for provenance in resolved.namespaces
253
255
  )
254
256
 
257
+ def _returned_constructions(self, provenance: ResourceProvenance, function: _Function) -> bool:
258
+ for statement in ast.walk(function):
259
+ if not isinstance(statement, ast.Return) or statement.value is None:
260
+ continue
261
+ if _opaque_return_selection(statement.value):
262
+ return False
263
+ for node in ast.walk(statement.value):
264
+ if isinstance(node, ast.Call) and not self._value_annotation(
265
+ provenance, node.func, node, _MAX_ANNOTATION_DEPTH, frozenset()
266
+ ):
267
+ return False
268
+ return True
269
+
255
270
  def _resolve(self, expression: ast.expr) -> _ResolvedValidation | None:
256
271
  if isinstance(self.provenance.scope(expression), ast.ClassDef):
257
272
  return None
@@ -712,6 +727,21 @@ def _expression_effect(value: ast.expr, *, allow_calls: bool = False) -> bool:
712
727
  return any(isinstance(node, effects) for node in ast.walk(value))
713
728
 
714
729
 
730
+ def _opaque_return_selection(value: ast.expr) -> bool:
731
+ pending = [value]
732
+ while pending:
733
+ match pending.pop():
734
+ case ast.Attribute() | ast.Subscript():
735
+ return True
736
+ case ast.IfExp(body=body, orelse=otherwise):
737
+ pending.extend((body, otherwise))
738
+ case ast.BoolOp(values=values):
739
+ pending.extend(values)
740
+ case _:
741
+ pass
742
+ return False
743
+
744
+
715
745
  def _returned_value(value: ast.expr | None) -> bool:
716
746
  return (
717
747
  value is not None