sarj-python-lint 0.87.9__tar.gz → 0.88.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 (145) hide show
  1. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_registry.py +2 -2
  4. sarj_python_lint-0.88.0/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +322 -0
  5. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +21 -1
  6. sarj_python_lint-0.87.9/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -214
  7. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/.gitignore +0 -0
  8. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/LICENSE +0 -0
  9. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/README.md +0 -0
  10. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/json_boundary.py +0 -0
  19. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/py.typed +0 -0
  20. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/ratchet.py +0 -0
  21. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rule_base.py +0 -0
  22. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  23. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  24. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  25. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  26. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  27. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  28. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  29. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  30. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  31. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  32. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  33. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  34. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  35. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  36. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  37. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  38. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  39. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  40. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  41. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  42. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  43. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  44. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  45. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  46. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  47. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  48. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  49. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  50. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  51. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  52. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  53. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  54. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  55. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  56. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  57. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  58. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  59. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  60. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  61. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  62. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  63. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  64. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  65. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  66. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  67. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  68. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  69. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  70. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  71. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  72. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  73. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  74. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  75. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  76. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  77. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  78. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  79. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  80. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  81. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  82. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  83. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  84. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  85. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  86. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  87. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  88. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  89. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  90. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  91. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  92. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  93. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  94. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  95. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  96. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  97. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  98. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  99. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  100. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  101. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  102. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  103. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  104. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  105. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  106. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  107. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  108. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  109. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  110. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  111. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  112. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  113. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  114. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  115. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  116. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  117. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  118. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  119. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  120. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  121. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  122. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  123. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  124. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  125. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  126. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  127. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  128. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  129. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  130. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  131. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  132. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  133. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  134. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  135. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  136. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  137. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  138. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  139. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  140. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  141. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  142. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  143. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  144. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  145. {sarj_python_lint-0.87.9 → sarj_python_lint-0.88.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: 0.87.9
3
+ Version: 0.88.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 = "0.87.9"
3
+ version = "0.88.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" }]
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from types import MappingProxyType
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from sarj_python_lint.rules.async_mock_call_without_await_assertion import AsyncMockCallWithoutAwaitAssertion
6
7
  from sarj_python_lint.rules.complex_postgres_query_requires_architecture_review import (
7
8
  ComplexPostgresQueryRequiresArchitectureReview,
8
9
  )
@@ -158,7 +159,6 @@ from sarj_python_lint.rules.require_pydantic_for_structured_payload import Requi
158
159
  from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
159
160
  RequirePydanticOrdinalLowerBound,
160
161
  )
161
- from sarj_python_lint.rules.require_typed_http_test_response import RequireTypedHttpTestResponse
162
162
  from sarj_python_lint.rules.restated_test_docstring import RestatedTestDocstring
163
163
  from sarj_python_lint.rules.stepdown import Stepdown
164
164
  from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDelegatesToBulkRead
@@ -180,6 +180,7 @@ if TYPE_CHECKING:
180
180
 
181
181
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
182
182
  {
183
+ AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
183
184
  ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
184
185
  ExcessiveCommentary.id: ExcessiveCommentary,
185
186
  PreferWalrusAwaitedNoneGuard.id: PreferWalrusAwaitedNoneGuard,
@@ -283,7 +284,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
283
284
  NoPsycopgExecutionOutsideInjectedOwner.id: NoPsycopgExecutionOutsideInjectedOwner,
284
285
  RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
285
286
  RequirePydanticForStructuredPayload.id: RequirePydanticForStructuredPayload,
286
- RequireTypedHttpTestResponse.id: RequireTypedHttpTestResponse,
287
287
  RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
288
288
  NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
289
289
  NoWholeRequestResponsePayloadInLog.id: NoWholeRequestResponsePayloadInLog,
@@ -0,0 +1,322 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from types import MappingProxyType
6
+ from typing import TYPE_CHECKING, ClassVar, Final, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ is_suppressed,
19
+ parse_or_none,
20
+ )
21
+ from sarj_python_lint.rules._imports import ImportIndex
22
+ from sarj_python_lint.rules._paths import is_generated
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Iterator
27
+ from pathlib import Path
28
+
29
+
30
+ _CALL_ASSERTIONS: Final = frozenset(
31
+ {"assert_called", "assert_called_once", "assert_called_with", "assert_called_once_with"}
32
+ )
33
+ _AWAIT_ASSERTIONS: Final = frozenset(
34
+ {
35
+ "assert_awaited",
36
+ "assert_awaited_once",
37
+ "assert_awaited_with",
38
+ "assert_awaited_once_with",
39
+ "assert_any_await",
40
+ "assert_has_awaits",
41
+ }
42
+ )
43
+ _AWAIT_STATE: Final = frozenset({"await_count", "await_args", "await_args_list"})
44
+ _CONSTRUCTOR_KEYWORDS: Final = frozenset({"return_value", "side_effect", "name", "wraps", "unsafe", "spec", "spec_set"})
45
+ _MUTATORS: Final = frozenset({"reset_mock", "configure_mock", "attach_mock", "mock_add_spec"})
46
+ _IMMEDIATE_CHILD_PARTS: Final = 2
47
+ _MOCK_ATTRIBUTES: Final = (
48
+ _MUTATORS
49
+ | _AWAIT_STATE
50
+ | frozenset(
51
+ {
52
+ "return_value",
53
+ "side_effect",
54
+ "called",
55
+ "call_count",
56
+ "call_args",
57
+ "call_args_list",
58
+ "mock_calls",
59
+ "method_calls",
60
+ }
61
+ )
62
+ )
63
+ type _TestFunction = ast.FunctionDef | ast.AsyncFunctionDef
64
+ type _Reference = tuple[str, ...]
65
+
66
+
67
+ @final
68
+ class AsyncMockCallWithoutAwaitAssertion(Rule):
69
+ id = "async-mock-call-without-await-assertion"
70
+ code = "SARJ456"
71
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
72
+ default_level=Severity.WARNING,
73
+ summary="Pair positive AsyncMock call assertions with evidence that the mock was awaited.",
74
+ rationale=(
75
+ "Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can "
76
+ "therefore pass when production code silently drops the coroutine. Await assertions cover that gap, "
77
+ "while call assertions still detect extra calls whose coroutines were never awaited."
78
+ ),
79
+ remediation=(
80
+ "Keep the call assertion and add an appropriate assert_awaited assertion. For a test that deliberately "
81
+ "checks scheduling before execution, use an exact SARJ456 suppression explaining that contract."
82
+ ),
83
+ category=RuleCategory.TESTING,
84
+ autofix=AutofixPolicy.NONE,
85
+ limitations=(
86
+ "Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.",
87
+ "Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
88
+ "Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
89
+ "Any direct positive await assertion or explicit await-state assertion on the same mock is accepted; argument equality and assertion strength are not compared.",
90
+ "Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
91
+ ),
92
+ examples=(
93
+ RuleExample(
94
+ example_id="call-only-async-mock",
95
+ title="A call assertion does not prove awaiting",
96
+ outcome=ExampleOutcome.MATCH,
97
+ files=(
98
+ ExampleFile.python(
99
+ "tests/test_delivery.py",
100
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n",
101
+ ),
102
+ ),
103
+ focus_path=PurePosixPath("tests/test_delivery.py"),
104
+ expected_count=1,
105
+ public=True,
106
+ ),
107
+ RuleExample(
108
+ example_id="call-and-await-async-mock",
109
+ title="Call and await assertions protect different contracts",
110
+ outcome=ExampleOutcome.NO_MATCH,
111
+ files=(
112
+ ExampleFile.python(
113
+ "tests/test_delivery.py",
114
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n send.assert_awaited_once_with('item')\n",
115
+ ),
116
+ ),
117
+ focus_path=PurePosixPath("tests/test_delivery.py"),
118
+ expected_count=0,
119
+ public=True,
120
+ ),
121
+ ),
122
+ )
123
+ description = documentation.summary
124
+
125
+ @override
126
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
127
+ if (
128
+ path.suffix != ".py"
129
+ or "AsyncMock" not in source
130
+ or "assert_called" not in source
131
+ or not (path.stem.startswith("test_") or path.stem.endswith("_test"))
132
+ or is_generated(path, source)
133
+ ):
134
+ return []
135
+ tree = parse_or_none(path, source)
136
+ if tree is None:
137
+ return []
138
+ imports = ImportIndex.from_tree(tree, module_scope_only=True)
139
+ writes = {
140
+ _reference(node)
141
+ for node in ast.walk(tree)
142
+ if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
143
+ }
144
+ lines = source.splitlines()
145
+ return [
146
+ Diagnostic(
147
+ path=path,
148
+ line=call.lineno,
149
+ col=call.col_offset + 1,
150
+ code=self.code,
151
+ severity=Severity.WARNING,
152
+ message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
153
+ )
154
+ for test in _collected_tests(tree.body)
155
+ if not _is_pytest_fixture(test, imports)
156
+ for call in _uncovered_calls(_test_imports(imports, test), test, writes)
157
+ if not is_suppressed(lines, call.lineno, self.code)
158
+ ]
159
+
160
+
161
+ def _collected_tests(statements: list[ast.stmt]) -> Iterator[_TestFunction]:
162
+ for statement in statements:
163
+ match statement:
164
+ case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
165
+ yield statement
166
+ case ast.ClassDef() if statement.name.startswith("Test"):
167
+ yield from _collected_tests(statement.body)
168
+ case _:
169
+ pass
170
+
171
+
172
+ def _is_pytest_fixture(test: _TestFunction, imports: ImportIndex) -> bool:
173
+ return any(
174
+ imports.resolved_qualified_name(decorator.func if isinstance(decorator, ast.Call) else decorator)
175
+ == "pytest.fixture"
176
+ for decorator in test.decorator_list
177
+ )
178
+
179
+
180
+ def _test_imports(module: ImportIndex, test: _TestFunction) -> ImportIndex:
181
+ local = ImportIndex.from_tree(ast.Module(body=[test, *test.body], type_ignores=[]))
182
+ bindings = {name: target for name, target in module.bindings.items() if name not in local.shadowed_names}
183
+ bindings.update(local.bindings)
184
+ return ImportIndex(MappingProxyType(bindings), module.shadowed_names | local.shadowed_names)
185
+
186
+
187
+ def _uncovered_calls(imports: ImportIndex, test: _TestFunction, writes: set[_Reference]) -> Iterator[ast.Call]:
188
+ nodes = tuple(ast.walk(test))
189
+ bindings = _mock_bindings(imports, test.body, writes)
190
+ covered = _await_oracles(test.body)
191
+ reported: set[_Reference] = set()
192
+ for statement in test.body:
193
+ match statement:
194
+ case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method)) as call) if (
195
+ method in _CALL_ASSERTIONS
196
+ ):
197
+ reference = _reference(receiver)
198
+ if not reference or reference in covered or reference in reported:
199
+ continue
200
+ binding = bindings.get(reference[0])
201
+ if (
202
+ binding is None
203
+ or not _is_async_reference(reference, binding[1])
204
+ or any(_invalidates_reference(node, reference, binding[0]) for node in nodes)
205
+ ):
206
+ continue
207
+ reported.add(reference)
208
+ yield call
209
+ case _:
210
+ pass
211
+
212
+
213
+ def _mock_bindings(
214
+ imports: ImportIndex, statements: list[ast.stmt], writes: set[_Reference]
215
+ ) -> dict[str, tuple[ast.Name, ast.Call]]:
216
+ bindings: dict[str, tuple[ast.Name, ast.Call]] = {}
217
+ for statement in statements:
218
+ match statement:
219
+ case (
220
+ ast.Assign(targets=[ast.Name() as target], value=ast.Call() as call)
221
+ | ast.AnnAssign(target=ast.Name() as target, value=ast.Call() as call)
222
+ ):
223
+ if _is_async_constructor(call.func, imports, writes) and all(
224
+ keyword.arg in _CONSTRUCTOR_KEYWORDS for keyword in call.keywords
225
+ ):
226
+ bindings[target.id] = (target, call)
227
+ case _:
228
+ pass
229
+ return bindings
230
+
231
+
232
+ def _is_async_constructor(node: ast.expr, imports: ImportIndex, writes: set[_Reference]) -> bool:
233
+ reference = _reference(node)
234
+ if any(reference[:length] in writes for length in range(1, len(reference) + 1)):
235
+ return False
236
+ return imports.resolved_qualified_name(node) == "unittest.mock.AsyncMock" or (
237
+ isinstance(node, ast.Attribute)
238
+ and node.attr == "AsyncMock"
239
+ and imports.resolved_qualified_name(node.value) == "unittest.mock"
240
+ )
241
+
242
+
243
+ def _reference(node: ast.AST) -> _Reference:
244
+ match node:
245
+ case ast.Name(id=name):
246
+ return (name,)
247
+ case ast.Attribute(value=parent, attr=attribute):
248
+ prefix = _reference(parent)
249
+ return (*prefix, attribute) if prefix else ()
250
+ case _:
251
+ return ()
252
+
253
+
254
+ def _is_async_reference(reference: _Reference, constructor: ast.Call) -> bool:
255
+ if len(reference) == 1:
256
+ return True
257
+ return (
258
+ len(reference) == _IMMEDIATE_CHILD_PARTS
259
+ and not constructor.args
260
+ and not any(keyword.arg in {"spec", "spec_set", "wraps"} for keyword in constructor.keywords)
261
+ and reference[1] not in _MOCK_ATTRIBUTES
262
+ and not reference[1].startswith(("_", "assert"))
263
+ )
264
+
265
+
266
+ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
267
+ covered: set[_Reference] = set()
268
+ for statement in statements:
269
+ match statement:
270
+ case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method))) if (
271
+ method in _AWAIT_ASSERTIONS
272
+ ):
273
+ covered.add(_reference(receiver))
274
+ case ast.Assert(test=condition):
275
+ covered.update(
276
+ _reference(node.value)
277
+ for node in ast.walk(condition)
278
+ if isinstance(node, ast.Attribute) and node.attr in _AWAIT_STATE
279
+ )
280
+ case _:
281
+ pass
282
+ return covered
283
+
284
+
285
+ def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Name) -> bool:
286
+ match node:
287
+ case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
288
+ return node is not binding and name == reference[0]
289
+ case ast.arg(arg=name):
290
+ return name == reference[0]
291
+ case ast.Global(names=names) | ast.Nonlocal(names=names):
292
+ return reference[0] in names
293
+ case ast.Attribute(ctx=ast.Store() | ast.Del()):
294
+ changed = _reference(node)
295
+ return bool(changed) and (
296
+ reference[: len(changed)] == changed or (changed[:-1] == reference and changed[-1].startswith("assert"))
297
+ )
298
+ case ast.Call(func=ast.Attribute(value=receiver, attr=method)) if method in _MUTATORS:
299
+ changed = _reference(receiver)
300
+ return bool(changed) and reference[: len(changed)] == changed
301
+ case ast.Call(func=ast.Name(id="setattr" | "delattr"), args=[receiver, *_]):
302
+ return _reference(receiver)[:1] == reference[:1]
303
+ case ast.Assign(value=value) | ast.AnnAssign(value=value) if value is not None:
304
+ return reference[0] in _alias_roots(value)
305
+ case _:
306
+ return False
307
+
308
+
309
+ def _alias_roots(node: ast.expr) -> Iterator[str]:
310
+ match node:
311
+ case ast.Name() | ast.Attribute():
312
+ if reference := _reference(node):
313
+ yield reference[0]
314
+ case ast.Tuple(elts=elements) | ast.List(elts=elements) | ast.Set(elts=elements):
315
+ for element in elements:
316
+ yield from _alias_roots(element)
317
+ case ast.Dict(keys=keys, values=values):
318
+ for element in (*keys, *values):
319
+ if element is not None:
320
+ yield from _alias_roots(element)
321
+ case _:
322
+ pass
@@ -118,6 +118,7 @@ class NoRepeatedTestBody(Rule):
118
118
  "At least three consecutive call-and-literal-equality cases with corroborating behavior names are compared even with only two statements.",
119
119
  "Meaningful docstring or comment differences keep tests distinct.",
120
120
  "Two-test groups with varying literals require corroborating behavior names; long scenario prose and distinct API resources remain separate contracts.",
121
+ "Differing direct string needles in asserted membership comparisons preserve distinct contracts.",
121
122
  ),
122
123
  examples=(
123
124
  RuleExample(
@@ -486,10 +487,29 @@ def _is_fixture_document(value: object) -> bool:
486
487
 
487
488
  def _erases_contract_identity(members: list[_Shape]) -> bool:
488
489
  columns: list[tuple[object, ...]] = list(zip(*(member.literals for member in members), strict=True))
489
- return any(
490
+ if any(
490
491
  len(set(column)) > 1 and all(isinstance(value, str) and value.startswith("/") for value in column)
491
492
  for column in columns
493
+ ):
494
+ return True
495
+ needles: list[tuple[str | None, ...]] = list(
496
+ zip(*(_asserted_membership_needles(member.node) for member in members), strict=True)
492
497
  )
498
+ return any(len(set(column)) > 1 and all(isinstance(value, str) for value in column) for column in needles)
499
+
500
+
501
+ def _asserted_membership_needles(node: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[str | None, ...]:
502
+ needles: list[str | None] = []
503
+ for child in _walk(node):
504
+ if not isinstance(child, ast.Assert) or not isinstance(child.test, ast.Compare):
505
+ continue
506
+ comparison = child.test
507
+ for operand, operator in zip((comparison.left, *comparison.comparators[:-1]), comparison.ops, strict=True):
508
+ if isinstance(operator, ast.In | ast.NotIn):
509
+ needles.append(
510
+ operand.value if isinstance(operand, ast.Constant) and isinstance(operand.value, str) else None
511
+ )
512
+ return tuple(needles)
493
513
 
494
514
 
495
515
  def _has_enough_duplicate_evidence(members: list[_Shape]) -> bool:
@@ -1,214 +0,0 @@
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
- is_suppressed,
17
- parse_or_none,
18
- )
19
- from sarj_python_lint.rules._paths import is_generated, is_test_path
20
-
21
-
22
- if TYPE_CHECKING:
23
- from pathlib import Path
24
-
25
-
26
- @final
27
- class RequireTypedHttpTestResponse(Rule):
28
- id = "require-typed-http-test-response"
29
- code = "SARJ449"
30
- documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
31
- summary="HTTP tests must validate JSON response bodies into named models before asserting fields.",
32
- rationale=(
33
- "Raw dictionary assertions duplicate wire spelling and do not prove that the complete response contract validates."
34
- ),
35
- remediation=(
36
- "Parse `response.json()` once with `ResponseModel.model_validate(...)` or a Pydantic `TypeAdapter`, then assert "
37
- "typed attributes."
38
- ),
39
- category=RuleCategory.TESTING,
40
- autofix=AutofixPolicy.NONE,
41
- limitations=(
42
- "Only test files and direct or simple local-aliased HTTP JSON access are checked; branch and nested-scope aliases are excluded.",
43
- ),
44
- examples=(
45
- RuleExample(
46
- example_id="raw-http-json-assertion",
47
- title="A test indexes an unvalidated response body",
48
- outcome=ExampleOutcome.MATCH,
49
- files=(
50
- ExampleFile.python(
51
- "tests/test_cards.py",
52
- "def test_interaction(client):\n response = client.post('/actions')\n assert response.json()['interactionId']\n",
53
- ),
54
- ),
55
- focus_path=PurePosixPath("tests/test_cards.py"),
56
- expected_count=1,
57
- public=True,
58
- ),
59
- RuleExample(
60
- example_id="validated-http-json-assertion",
61
- title="A test validates its response model",
62
- outcome=ExampleOutcome.NO_MATCH,
63
- files=(
64
- ExampleFile.python(
65
- "tests/test_cards.py",
66
- "def test_interaction(client):\n response = client.post('/actions')\n body = InteractionResponse.model_validate(response.json())\n assert body.interaction_id\n",
67
- ),
68
- ),
69
- focus_path=PurePosixPath("tests/test_cards.py"),
70
- expected_count=0,
71
- public=True,
72
- ),
73
- ),
74
- )
75
- description = documentation.summary
76
-
77
- @override
78
- def check(self, path: Path, source: str) -> list[Diagnostic]:
79
- if not is_test_path(path) or is_generated(path, source):
80
- return []
81
- tree = parse_or_none(path, source)
82
- if tree is None:
83
- return []
84
- lines = source.splitlines()
85
- findings: list[Diagnostic] = []
86
- for node in ast.walk(tree):
87
- call = _raw_json_access(node)
88
- if call is None or is_suppressed(lines, call.lineno, self.code):
89
- continue
90
- findings.append(
91
- Diagnostic(
92
- path=path,
93
- line=call.lineno,
94
- col=call.col_offset + 1,
95
- code=self.code,
96
- message=(
97
- "validate the HTTP JSON body into its named Pydantic response model before asserting fields"
98
- ),
99
- )
100
- )
101
- for node in ast.walk(tree):
102
- if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
103
- continue
104
- findings.extend(_alias_findings(path, lines, node, self.code))
105
- return sorted(findings, key=lambda finding: (finding.line, finding.col))
106
-
107
-
108
- def _raw_json_access(node: ast.AST) -> ast.Call | None:
109
- value: ast.expr | None = None
110
- if isinstance(node, ast.Subscript):
111
- value = node.value
112
- elif (
113
- isinstance(node, ast.Call)
114
- and isinstance(node.func, ast.Attribute)
115
- and node.func.attr in {"get", "items", "keys"}
116
- ):
117
- value = node.func.value
118
- if not isinstance(value, ast.Call) or value.args or value.keywords:
119
- return None
120
- if not isinstance(value.func, ast.Attribute) or value.func.attr != "json":
121
- return None
122
- return value
123
-
124
-
125
- def _alias_findings(
126
- path: Path,
127
- lines: list[str],
128
- function: ast.FunctionDef | ast.AsyncFunctionDef,
129
- code: str,
130
- ) -> list[Diagnostic]:
131
- aliases: set[str] = set()
132
- response_names: set[str] = set()
133
- findings: list[Diagnostic] = []
134
- for statement in function.body:
135
- if not isinstance(statement, (ast.Assign, ast.AnnAssign, ast.Assert, ast.Expr, ast.Return)):
136
- # Branches and nested scopes need control-flow analysis before alias state can be trusted.
137
- aliases.clear()
138
- response_names.clear()
139
- continue
140
- for node in ast.walk(statement):
141
- access = _alias_access(node, aliases)
142
- if access is None or is_suppressed(lines, access.lineno, code):
143
- continue
144
- findings.append(
145
- Diagnostic(
146
- path=path,
147
- line=access.lineno,
148
- col=access.col_offset + 1,
149
- code=code,
150
- message="validate the HTTP JSON body into its named Pydantic response model before asserting fields",
151
- )
152
- )
153
- _record_alias_assignment(statement, aliases, response_names)
154
- return findings
155
-
156
-
157
- def _record_alias_assignment(statement: ast.stmt, aliases: set[str], response_names: set[str]) -> None:
158
- target: ast.expr | None = None
159
- value: ast.expr | None = None
160
- if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
161
- target, value = statement.targets[0], statement.value
162
- elif isinstance(statement, ast.AnnAssign):
163
- target, value = statement.target, statement.value
164
- if not isinstance(target, ast.Name) or value is None:
165
- return
166
- is_alias = _is_http_json_call(value, response_names) or (isinstance(value, ast.Name) and value.id in aliases)
167
- if is_alias:
168
- aliases.add(target.id)
169
- else:
170
- aliases.discard(target.id)
171
- if _is_http_response_call(value):
172
- response_names.add(target.id)
173
- else:
174
- response_names.discard(target.id)
175
-
176
-
177
- def _alias_access(node: ast.AST, aliases: set[str]) -> ast.Name | None:
178
- value: ast.expr | None = None
179
- if isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load):
180
- value = node.value
181
- elif (
182
- isinstance(node, ast.Call)
183
- and isinstance(node.func, ast.Attribute)
184
- and node.func.attr in {"get", "items", "keys"}
185
- ):
186
- value = node.func.value
187
- if isinstance(value, ast.Name) and value.id in aliases:
188
- return value
189
- return None
190
-
191
-
192
- def _is_http_json_call(value: ast.expr, response_names: set[str]) -> bool:
193
- if not isinstance(value, ast.Call) or value.args or value.keywords:
194
- return False
195
- if not isinstance(value.func, ast.Attribute) or value.func.attr != "json":
196
- return False
197
- receiver = value.func.value
198
- return (isinstance(receiver, ast.Name) and receiver.id in response_names) or _is_http_response_call(receiver)
199
-
200
-
201
- def _is_http_response_call(value: ast.expr) -> bool:
202
- if isinstance(value, ast.Await):
203
- value = value.value
204
- return (
205
- isinstance(value, ast.Call)
206
- and isinstance(value.func, ast.Attribute)
207
- and value.func.attr in {"get", "post", "put", "patch", "delete", "request"}
208
- and isinstance(value.func.value, ast.Name)
209
- and value.func.value.id.endswith("client")
210
- and bool(value.args)
211
- and isinstance(value.args[0], ast.Constant)
212
- and isinstance(value.args[0].value, str)
213
- and value.args[0].value.startswith(("/", "http://", "https://"))
214
- )