sarj-python-lint 1.7.0__tar.gz → 1.7.1__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 (159) hide show
  1. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +353 -63
  4. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/.gitignore +0 -0
  5. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/LICENSE +0 -0
  6. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/README.md +0 -0
  7. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/__init__.py +0 -0
  8. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/__main__.py +0 -0
  9. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  10. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_file_context.py +0 -0
  11. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_filesystem.py +0 -0
  12. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_python_target.py +0 -0
  13. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  14. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_secret_names.py +0 -0
  15. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_version.py +0 -0
  16. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/benchmark.py +0 -0
  17. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/json_boundary.py +0 -0
  18. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/py.typed +0 -0
  19. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/ratchet.py +0 -0
  20. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rule_base.py +0 -0
  21. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  22. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  23. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  24. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  25. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  26. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  27. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  28. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  29. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  30. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  31. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  32. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  33. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  34. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  35. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  36. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  37. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  38. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  39. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  40. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  41. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  42. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  43. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  44. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  45. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  46. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  47. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  48. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  49. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  50. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  51. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  52. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  53. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  54. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  55. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  56. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  57. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  58. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  59. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  60. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  61. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  62. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  63. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  64. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  65. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  66. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  67. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  68. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  69. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  70. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  71. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  72. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  73. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  74. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  75. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  76. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  77. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  78. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  79. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  80. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  81. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  82. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  83. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  84. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  85. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  86. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  87. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  88. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  89. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  90. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  91. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  92. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  93. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  94. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  95. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  96. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  97. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  98. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  99. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  100. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  101. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  102. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  103. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  104. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  105. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  106. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  107. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  108. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  109. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  110. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  111. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  112. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  113. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  114. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  115. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  116. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  117. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  118. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  119. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  120. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  121. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  122. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  123. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  124. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  125. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  126. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  127. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  128. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  129. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  130. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  131. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  132. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  133. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  134. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  135. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  136. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  137. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  138. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  139. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  140. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  141. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  142. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  143. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  144. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  145. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  146. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  147. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  148. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  149. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  150. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  151. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  152. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  153. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  154. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  155. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  156. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  157. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  158. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  159. {sarj_python_lint-1.7.0 → sarj_python_lint-1.7.1}/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.7.0
3
+ Version: 1.7.1
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.7.0"
3
+ version = "1.7.1"
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" }]
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from collections import Counter
5
- from dataclasses import dataclass, replace
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field, replace
6
7
  from pathlib import PurePosixPath
7
8
  from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
8
9
 
@@ -71,6 +72,9 @@ class _WrapperSummary:
71
72
  class _WrapperFactorySummary:
72
73
  wrapper: _WrapperSummary
73
74
  accessor_origins: dict[str, frozenset[ast.Call]]
75
+ accessor_parameters: dict[str, frozenset[str]]
76
+ signature: ast.arguments
77
+ narrowing_helpers: frozenset[str]
74
78
 
75
79
 
76
80
  @dataclass(frozen=True, slots=True)
@@ -130,7 +134,7 @@ class RequirePydanticForExternalJson(Rule):
130
134
  limitations=(
131
135
  "The rule follows import-proven JSON decoders, requests/httpx/aiohttp responses and clients, environment or subprocess results, and simple module-local helpers through single-assignment names.",
132
136
  "Simple same-module wrappers preserve constructor arguments through direct stored-field accessor methods, including local identity-or-empty-dict narrowing helpers; inheritance, decorators, mutation, nested captures, unknown member escapes, and unknown helpers are excluded.",
133
- "No-argument synchronous module factories with an optional docstring and a single direct, fully bound wrapper-constructor return preserve accessor provenance; annotations alone, parameterized/async/method factories, returned aliases, and factory chains are not inferred.",
137
+ "Synchronous module factories preserve direct wrapper returns, single-assignment local aliases, and required positional or keyword parameters substituted from proven call-site inputs. Defaults, variadics, branches, unknown effects or transforms, async/method/decorated factories, captures, and factory chains are excluded; annotations alone do not establish factory provenance.",
134
138
  "It diagnoses literal-key subscription and get calls; iteration and other dynamic JSON use remain out of scope.",
135
139
  "Unannotated parameters and unknown expressions are not assumed to be external; interprocedural and framework-specific boundaries can remain unreported.",
136
140
  "Names bound by loops, comprehensions, context managers, exception handlers, pattern matching, imports, or destructuring are excluded when their provenance is ambiguous; unrelated binders do not suppress the rest of a function.",
@@ -151,6 +155,36 @@ class RequirePydanticForExternalJson(Rule):
151
155
  expected_count=1,
152
156
  public=True,
153
157
  ),
158
+ RuleExample(
159
+ example_id="factory-forwarded-external-json",
160
+ scenario="factory-forwarding",
161
+ title="A local factory forwards an unvalidated response",
162
+ outcome=ExampleOutcome.MATCH,
163
+ files=(
164
+ ExampleFile.python(
165
+ "protocol.py",
166
+ "import httpx\n\nclass Envelope:\n def __init__(self, body):\n self.body = body\n def payload(self):\n return self.body\n\ndef wrap(body):\n result = Envelope(body)\n return result\n\ndef fetch():\n raw = httpx.get('https://api.example/report').json()\n return wrap(raw).payload()['version']\n",
167
+ ),
168
+ ),
169
+ focus_path=PurePosixPath("protocol.py"),
170
+ expected_count=1,
171
+ public=True,
172
+ ),
173
+ RuleExample(
174
+ example_id="factory-local-record",
175
+ scenario="factory-forwarding",
176
+ title="A factory receives a locally constructed record",
177
+ outcome=ExampleOutcome.NO_MATCH,
178
+ files=(
179
+ ExampleFile.python(
180
+ "protocol.py",
181
+ "import httpx\n\nclass Envelope:\n def __init__(self, body):\n self.body = body\n def payload(self):\n return self.body\n\ndef wrap(body):\n return Envelope(body)\n\ndef fetch():\n remote = httpx.get('https://api.example/report').json()\n return wrap({'version': 1}).payload()['version']\n",
182
+ ),
183
+ ),
184
+ focus_path=PurePosixPath("protocol.py"),
185
+ expected_count=0,
186
+ public=True,
187
+ ),
154
188
  RuleExample(
155
189
  example_id="pydantic-external-json-validation",
156
190
  title="External JSON validated by a boundary model",
@@ -550,6 +584,7 @@ def _function_findings(
550
584
  bindings = _unique_bindings(scope, excluded_names=_ambiguous_binding_names(scope))
551
585
  local_bound_names = _function_local_bound_names(function, scope)
552
586
  response_names = _http_response_names(scope, imports, summaries, function, shadowed_names=local_bound_names)
587
+ validated_names = _validated_names(function, imports, summaries.jsonschema_validator_names)
553
588
  resolver = _OriginResolver(
554
589
  imports,
555
590
  summaries,
@@ -559,8 +594,8 @@ def _function_findings(
559
594
  bindings,
560
595
  local_bound_names,
561
596
  scope,
597
+ validated_names,
562
598
  )
563
- validated_names = _validated_names(function, imports, summaries.jsonschema_validator_names)
564
599
  findings: list[_ExternalRecordFinding] = []
565
600
  for node in scope:
566
601
  access = _record_access(node)
@@ -770,6 +805,7 @@ class _OriginResolver:
770
805
  bindings: dict[str, ast.expr]
771
806
  local_bound_names: frozenset[str]
772
807
  scope: tuple[ast.AST, ...]
808
+ validated_names: _ValidationHistory = field(default_factory=dict)
773
809
 
774
810
  def origins(self, expression: ast.expr, resolving: frozenset[str] = frozenset()) -> frozenset[ast.Call]:
775
811
  expression = _unwrap_await(expression)
@@ -812,30 +848,94 @@ class _OriginResolver:
812
848
  found_origins.update(self.origins(keyword.value, resolving))
813
849
  return frozenset(found_origins)
814
850
 
815
- def factory_argument_origins(self, value: ast.expr, narrowing_helpers: frozenset[str]) -> frozenset[ast.Call]:
851
+ def factory_call_is_known(self, call: ast.Call) -> bool:
852
+ if self._is_validation_call(call):
853
+ return True
854
+ if not self._is_source(call):
855
+ return False
856
+ if (
857
+ isinstance(call.func, ast.Attribute)
858
+ and isinstance(call.func.value, ast.Name)
859
+ and call.func.attr == "json"
860
+ and call.func.value.id in self.response_names
861
+ ):
862
+ return True
863
+ receiver = call.func
864
+ while isinstance(receiver, ast.Attribute):
865
+ receiver = receiver.value
866
+ return not isinstance(receiver, ast.Name) or receiver.id not in self.local_bound_names
867
+
868
+ def factory_argument_origins(
869
+ self,
870
+ value: ast.expr,
871
+ narrowing_helpers: frozenset[str],
872
+ *,
873
+ use: ast.expr | None = None,
874
+ resolving: frozenset[str] = frozenset(),
875
+ ) -> frozenset[ast.Call]:
876
+ if isinstance(value, ast.Name):
877
+ if use is not None and _was_validated_before(value, use, self.validated_names):
878
+ return frozenset()
879
+ binding = self.bindings.get(value.id)
880
+ if binding is None or value.id in resolving or _node_position(binding) >= _node_position(value):
881
+ return frozenset()
882
+ return self.factory_argument_origins(binding, narrowing_helpers, use=use, resolving=resolving | {value.id})
816
883
  if not isinstance(value, ast.Call) or self._is_validation_call(value):
817
884
  return frozenset()
818
885
  if self._is_source(value):
819
886
  return frozenset({value})
820
- if (
821
- isinstance(value.func, ast.Name)
822
- and value.func.id in narrowing_helpers
823
- and len(value.args) == 1
824
- and not value.keywords
825
- ):
826
- return self.factory_argument_origins(value.args[0], narrowing_helpers)
887
+ if _is_narrowing_call(value, narrowing_helpers, self.local_bound_names):
888
+ return self.factory_argument_origins(value.args[0], narrowing_helpers, use=use, resolving=resolving)
827
889
  return frozenset()
828
890
 
829
891
  def _factory_accessor_origins(self, access: ast.Attribute) -> frozenset[ast.Call] | None:
830
892
  receiver = self._resolved_binding(access.value) if isinstance(access.value, ast.Name) else access.value
831
893
  if not isinstance(receiver, ast.Call) or not isinstance(receiver.func, ast.Name):
832
894
  return None
833
- if receiver.args or receiver.keywords or receiver.func.id in self.local_bound_names:
895
+ if receiver.func.id in self.local_bound_names:
834
896
  return None
835
897
  factory = self.summaries.wrapper_factories.get(receiver.func.id)
836
898
  if factory is None or not self._wrapper_is_stable(receiver, factory.wrapper):
837
899
  return None
838
- return factory.accessor_origins.get(access.attr, frozenset())
900
+ arguments = _factory_call_arguments(receiver, factory.signature)
901
+ if arguments is None:
902
+ return frozenset()
903
+ origins = set(factory.accessor_origins.get(access.attr, ()))
904
+ for parameter in factory.accessor_parameters.get(access.attr, ()):
905
+ value = arguments[parameter]
906
+ if self._factory_argument_is_stable(value, receiver, factory.narrowing_helpers):
907
+ origins.update(self.factory_argument_origins(value, factory.narrowing_helpers, use=access))
908
+ return frozenset(origins)
909
+
910
+ def _factory_argument_is_stable(
911
+ self, value: ast.expr, factory_call: ast.Call, narrowing_helpers: frozenset[str]
912
+ ) -> bool:
913
+ if not isinstance(value, ast.Name):
914
+ if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id in narrowing_helpers:
915
+ return len(value.args) == 1 and self._factory_argument_is_stable(
916
+ value.args[0], factory_call, narrowing_helpers
917
+ )
918
+ return True
919
+ original = self._resolved_binding(value)
920
+
921
+ def refers_to_payload(node: ast.AST) -> bool:
922
+ return isinstance(node, ast.Name) and self._resolved_binding(node) is original
923
+
924
+ def contains_payload(node: ast.AST) -> bool:
925
+ if node is factory_call:
926
+ return False
927
+ return refers_to_payload(node) or any(contains_payload(child) for child in ast.iter_child_nodes(node))
928
+
929
+ for node in self.scope:
930
+ if _reference_escapes(node, contains_payload):
931
+ return False
932
+ if not isinstance(node, ast.Call) or node is factory_call or self._is_validation_call(node):
933
+ continue
934
+ if _is_narrowing_call(node, narrowing_helpers, self.local_bound_names):
935
+ continue
936
+ if contains_payload(node):
937
+ return False
938
+ return True
839
939
 
840
940
  def _wrapper_value(self, access: ast.Attribute) -> ast.expr | None:
841
941
  receiver = self._resolved_binding(access.value) if isinstance(access.value, ast.Name) else access.value
@@ -858,24 +958,15 @@ class _OriginResolver:
858
958
  return next((keyword.value for keyword in receiver.keywords if keyword.arg == parameter), None)
859
959
 
860
960
  def _wrapper_is_stable(self, constructor: ast.Call, summary: _WrapperSummary) -> bool:
861
- def refers_to_wrapper(value: ast.expr) -> bool:
862
- return isinstance(value, ast.Name) and self._resolved_binding(value) is constructor
863
-
961
+ references = _WrapperReferences(constructor, summary, self._resolved_binding)
864
962
  for node in self.scope:
865
- if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) and any(
866
- isinstance(child, ast.Name) and refers_to_wrapper(child) for child in ast.walk(node)
867
- ):
963
+ if _reference_escapes(node, references.contains):
868
964
  return False
869
- if (
870
- isinstance(node, ast.Attribute)
871
- and refers_to_wrapper(node.value)
872
- and (isinstance(node.ctx, ast.Store | ast.Del) or node.attr not in summary.accessors | summary.fields)
873
- ):
965
+ if isinstance(node, ast.Attribute) and references.unknown_member(node):
874
966
  return False
875
- if not isinstance(node, ast.Call):
876
- continue
877
- if any(refers_to_wrapper(argument) for argument in node.args) or any(
878
- refers_to_wrapper(keyword.value) for keyword in node.keywords
967
+ if isinstance(node, ast.Call) and any(
968
+ references.contains(argument)
969
+ for argument in (*node.args, *(keyword.value for keyword in node.keywords))
879
970
  ):
880
971
  return False
881
972
  return True
@@ -975,8 +1066,57 @@ class _OriginResolver:
975
1066
  return False
976
1067
 
977
1068
 
1069
+ @dataclass(frozen=True, slots=True)
1070
+ class _WrapperReferences:
1071
+ constructor: ast.Call
1072
+ summary: _WrapperSummary
1073
+ resolve: Callable[[ast.Name], ast.expr]
1074
+
1075
+ def wrapper(self, value: ast.AST) -> bool:
1076
+ return value is self.constructor or (isinstance(value, ast.Name) and self.resolve(value) is self.constructor)
1077
+
1078
+ def payload(self, value: ast.AST) -> bool:
1079
+ if isinstance(value, ast.Name):
1080
+ value = self.resolve(value)
1081
+ if isinstance(value, ast.Call) and not value.args and not value.keywords:
1082
+ return (
1083
+ isinstance(value.func, ast.Attribute)
1084
+ and value.func.attr in self.summary.accessors
1085
+ and self.wrapper(value.func.value)
1086
+ )
1087
+ return isinstance(value, ast.Attribute) and value.attr in self.summary.fields and self.wrapper(value.value)
1088
+
1089
+ def contains(self, node: ast.AST) -> bool:
1090
+ return any(self.wrapper(child) or self.payload(child) for child in ast.walk(node))
1091
+
1092
+ def unknown_member(self, node: ast.Attribute) -> bool:
1093
+ if self.wrapper(node.value):
1094
+ return node.attr not in self.summary.accessors | self.summary.fields
1095
+ return self.payload(node.value) and node.attr not in _RECORD_METHODS
1096
+
1097
+
1098
+ def _reference_escapes(node: ast.AST, contains_reference: Callable[[ast.AST], bool]) -> bool:
1099
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
1100
+ return contains_reference(node)
1101
+ if isinstance(node, ast.List | ast.Tuple | ast.Set | ast.Dict):
1102
+ return contains_reference(node)
1103
+ if isinstance(node, ast.Attribute | ast.Subscript) and isinstance(node.ctx, ast.Store | ast.Del):
1104
+ return contains_reference(node.value)
1105
+ return False
1106
+
1107
+
1108
+ def _is_narrowing_call(call: ast.Call, helpers: frozenset[str], local_names: frozenset[str]) -> bool:
1109
+ return (
1110
+ isinstance(call.func, ast.Name)
1111
+ and call.func.id in helpers
1112
+ and call.func.id not in local_names
1113
+ and len(call.args) == 1
1114
+ and not call.keywords
1115
+ )
1116
+
1117
+
978
1118
  def _record_access(node: ast.AST) -> _RecordAccess | None:
979
- if isinstance(node, ast.Subscript) and _literal_string(node.slice) is not None:
1119
+ if isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load) and _literal_string(node.slice) is not None:
980
1120
  return _RecordAccess(node, node.value)
981
1121
  if isinstance(node, ast.Call) and (receiver := _fixed_record_call_receiver(node)) is not None:
982
1122
  return _RecordAccess(node, receiver)
@@ -1734,56 +1874,206 @@ def _wrapper_descriptor(statement: ast.stmt) -> bool:
1734
1874
  def _wrapper_factory_summaries(
1735
1875
  tree: ast.Module, imports: ImportIndex, summaries: _ModuleSummaries
1736
1876
  ) -> dict[str, _WrapperFactorySummary]:
1877
+ if not summaries.wrappers:
1878
+ return {}
1737
1879
  counts = _suite_binding_counts(tree.body)
1738
1880
  narrowing_helpers = _narrowing_helpers(tree, imports, counts)
1739
1881
  factories: dict[str, _WrapperFactorySummary] = {}
1740
1882
  for function in tree.body:
1741
1883
  if not isinstance(function, ast.FunctionDef) or counts[function.name] != 1:
1742
1884
  continue
1743
- returned = _direct_factory_return(function)
1744
- if returned is None or not isinstance(returned.func, ast.Name):
1745
- continue
1746
- wrapper = summaries.wrappers.get(returned.func.id)
1747
- if wrapper is None:
1748
- continue
1749
- arguments = _factory_constructor_arguments(returned, wrapper)
1750
- if arguments is None:
1751
- continue
1752
- scope = _own_scope(function)
1753
- resolver = _OriginResolver(
1754
- imports,
1755
- summaries,
1756
- frozenset(),
1757
- frozenset(),
1758
- frozenset(),
1759
- {},
1760
- _function_local_bound_names(function, scope),
1761
- scope,
1762
- )
1763
- origins = {
1764
- accessor: resolver.factory_argument_origins(arguments[wrapper.fields[field]], narrowing_helpers)
1765
- for accessor, field in wrapper.accessors.items()
1766
- }
1767
- if any(origins.values()):
1768
- factories[function.name] = _WrapperFactorySummary(wrapper, origins)
1885
+ summary = _wrapper_factory_summary(function, imports, summaries, narrowing_helpers)
1886
+ if summary is not None:
1887
+ factories[function.name] = summary
1769
1888
  return factories
1770
1889
 
1771
1890
 
1772
- def _direct_factory_return(function: ast.FunctionDef) -> ast.Call | None:
1891
+ def _wrapper_factory_summary(
1892
+ function: ast.FunctionDef,
1893
+ imports: ImportIndex,
1894
+ summaries: _ModuleSummaries,
1895
+ narrowing_helpers: frozenset[str],
1896
+ ) -> _WrapperFactorySummary | None:
1897
+ body = _factory_body(function)
1898
+ if body is None:
1899
+ return None
1900
+ returned, bindings = body
1901
+ if not isinstance(returned.func, ast.Name):
1902
+ return None
1903
+ scope = _own_scope(function)
1904
+ local_names = _function_local_bound_names(function, scope)
1905
+ wrapper = summaries.wrappers.get(returned.func.id)
1906
+ if wrapper is None or returned.func.id in local_names:
1907
+ return None
1908
+ arguments = _factory_constructor_arguments(returned, wrapper)
1909
+ if arguments is None:
1910
+ return None
1911
+ resolver = _OriginResolver(
1912
+ imports,
1913
+ summaries,
1914
+ frozenset(),
1915
+ frozenset(),
1916
+ _factory_response_names(bindings, imports, local_names),
1917
+ bindings,
1918
+ local_names,
1919
+ scope,
1920
+ )
1921
+ if not _factory_expressions_are_safe(function, returned, resolver, narrowing_helpers):
1922
+ return None
1923
+ origins = {
1924
+ accessor: resolver.factory_argument_origins(arguments[wrapper.fields[stored]], narrowing_helpers)
1925
+ for accessor, stored in wrapper.accessors.items()
1926
+ }
1927
+ parameters = frozenset(
1928
+ argument.arg for argument in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
1929
+ )
1930
+ forwarded = {
1931
+ accessor: _factory_forwarded_parameters(
1932
+ arguments[wrapper.fields[stored]], bindings, parameters, narrowing_helpers
1933
+ )
1934
+ for accessor, stored in wrapper.accessors.items()
1935
+ }
1936
+ if not any(origins.values()) and not any(forwarded.values()):
1937
+ return None
1938
+ return _WrapperFactorySummary(wrapper, origins, forwarded, function.args, narrowing_helpers)
1939
+
1940
+
1941
+ def _factory_body(function: ast.FunctionDef) -> tuple[ast.Call, dict[str, ast.expr]] | None:
1773
1942
  statements = function.body[1:] if ast.get_docstring(function, clean=False) is not None else function.body
1774
1943
  if (
1775
1944
  function.decorator_list
1776
- or _parameter_positions(function)
1777
- or any((function.args.kwonlyargs, function.args.vararg, function.args.kwarg, function.type_params))
1778
- or len(statements) != 1
1945
+ or any((function.args.defaults, function.args.vararg, function.args.kwarg, function.type_params))
1946
+ or any(default is not None for default in function.args.kw_defaults)
1947
+ or not statements
1948
+ or not isinstance(statements[-1], ast.Return)
1779
1949
  ):
1780
1950
  return None
1781
- statement = statements[0]
1782
- if not isinstance(statement, ast.Return) or not isinstance(statement.value, ast.Call):
1951
+ bindings = _factory_bindings(function, statements[:-1])
1952
+ if bindings is None:
1953
+ return None
1954
+ returned = statements[-1].value
1955
+ while isinstance(returned, ast.Name) and returned.id in bindings:
1956
+ returned = bindings[returned.id]
1957
+ if not isinstance(returned, ast.Call):
1783
1958
  return None
1784
- if any(isinstance(node, ast.NamedExpr | ast.Yield | ast.YieldFrom | ast.Await) for node in ast.walk(statement)):
1959
+ return returned, bindings
1960
+
1961
+
1962
+ def _factory_bindings(function: ast.FunctionDef, statements: list[ast.stmt]) -> dict[str, ast.expr] | None:
1963
+ parameters = {
1964
+ argument.arg for argument in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
1965
+ }
1966
+ bindings: dict[str, ast.expr] = {}
1967
+ all_locals = _assigned_names(_own_scope(function))
1968
+ for statement in statements:
1969
+ if (
1970
+ isinstance(statement, ast.Assign)
1971
+ and len(statement.targets) == 1
1972
+ and isinstance(statement.targets[0], ast.Name)
1973
+ ):
1974
+ name, value = statement.targets[0].id, statement.value
1975
+ elif (
1976
+ isinstance(statement, ast.AnnAssign)
1977
+ and isinstance(statement.target, ast.Name)
1978
+ and statement.value is not None
1979
+ ):
1980
+ name, value = statement.target.id, statement.value
1981
+ else:
1982
+ return None
1983
+ if not isinstance(value, ast.Name | ast.Constant | ast.Call | ast.Dict | ast.List | ast.Tuple | ast.Set):
1984
+ return None
1985
+ unavailable = all_locals - bindings.keys()
1986
+ if (
1987
+ name in bindings
1988
+ or name in parameters
1989
+ or any(isinstance(child, ast.Name) and child.id in unavailable for child in ast.walk(value))
1990
+ ):
1991
+ return None
1992
+ bindings[name] = value
1993
+ return bindings
1994
+
1995
+
1996
+ def _factory_expressions_are_safe(
1997
+ function: ast.FunctionDef,
1998
+ constructor: ast.Call,
1999
+ resolver: _OriginResolver,
2000
+ narrowing_helpers: frozenset[str],
2001
+ ) -> bool:
2002
+ for statement in function.body:
2003
+ for node in ast.walk(statement):
2004
+ if isinstance(
2005
+ node,
2006
+ ast.NamedExpr
2007
+ | ast.Yield
2008
+ | ast.YieldFrom
2009
+ | ast.Await
2010
+ | ast.Lambda
2011
+ | ast.comprehension
2012
+ | ast.BinOp
2013
+ | ast.BoolOp
2014
+ | ast.UnaryOp
2015
+ | ast.Compare
2016
+ | ast.IfExp,
2017
+ ):
2018
+ return False
2019
+ if not isinstance(node, ast.Call) or node is constructor:
2020
+ continue
2021
+ if resolver.factory_call_is_known(node):
2022
+ continue
2023
+ if _factory_http_request(node, resolver.imports, resolver.local_bound_names):
2024
+ continue
2025
+ if _is_narrowing_call(node, narrowing_helpers, resolver.local_bound_names):
2026
+ continue
2027
+ return False
2028
+ return True
2029
+
2030
+
2031
+ def _factory_http_request(call: ast.Call, imports: ImportIndex, local_names: frozenset[str]) -> bool:
2032
+ return not any(isinstance(node, ast.Name) and node.id in local_names for node in ast.walk(call.func)) and any(
2033
+ imports.resolves(call.func, sources=_HTTP_MODULES, symbol=method) for method in _HTTP_METHODS
2034
+ )
2035
+
2036
+
2037
+ def _factory_response_names(
2038
+ bindings: dict[str, ast.expr], imports: ImportIndex, local_names: frozenset[str]
2039
+ ) -> frozenset[str]:
2040
+ responses: set[str] = set()
2041
+ for name, value in bindings.items():
2042
+ if (isinstance(value, ast.Call) and _factory_http_request(value, imports, local_names)) or (
2043
+ isinstance(value, ast.Name) and value.id in responses
2044
+ ):
2045
+ responses.add(name)
2046
+ return frozenset(responses)
2047
+
2048
+
2049
+ def _factory_forwarded_parameters(
2050
+ value: ast.expr,
2051
+ bindings: dict[str, ast.expr],
2052
+ parameters: frozenset[str],
2053
+ narrowing_helpers: frozenset[str],
2054
+ ) -> frozenset[str]:
2055
+ if isinstance(value, ast.Name):
2056
+ if value.id in parameters:
2057
+ return frozenset({value.id})
2058
+ binding = bindings.get(value.id)
2059
+ if binding is not None:
2060
+ return _factory_forwarded_parameters(binding, bindings, parameters, narrowing_helpers)
2061
+ if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id in narrowing_helpers:
2062
+ return _factory_forwarded_parameters(value.args[0], bindings, parameters, narrowing_helpers)
2063
+ return frozenset()
2064
+
2065
+
2066
+ def _factory_call_arguments(call: ast.Call, signature: ast.arguments) -> dict[str, ast.expr] | None:
2067
+ positional = tuple(argument.arg for argument in (*signature.posonlyargs, *signature.args))
2068
+ keyword_names = {argument.arg for argument in (*signature.args, *signature.kwonlyargs)}
2069
+ if len(call.args) > len(positional) or any(isinstance(argument, ast.Starred) for argument in call.args):
1785
2070
  return None
1786
- return statement.value
2071
+ arguments = dict(zip(positional, call.args, strict=False))
2072
+ for keyword in call.keywords:
2073
+ if keyword.arg not in keyword_names or keyword.arg in arguments:
2074
+ return None
2075
+ arguments[keyword.arg] = keyword.value
2076
+ return arguments if len(arguments) == len(positional) + len(signature.kwonlyargs) else None
1787
2077
 
1788
2078
 
1789
2079
  def _factory_constructor_arguments(call: ast.Call, wrapper: _WrapperSummary) -> dict[str, ast.expr] | None: