sarj-python-lint 0.87.4__tar.gz → 0.87.7__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 (144) hide show
  1. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_ast_index.py +9 -15
  4. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_registry.py +2 -0
  5. sarj_python_lint-0.87.7/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +225 -0
  6. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +3 -3
  7. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +4 -3
  8. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +4 -4
  9. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/.gitignore +0 -0
  10. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/LICENSE +0 -0
  11. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/README.md +0 -0
  12. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/_analysis_session.py +0 -0
  15. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  29. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  30. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_first_party.py +0 -0
  31. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  32. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_imports.py +0 -0
  33. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_logging.py +0 -0
  34. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_paths.py +0 -0
  35. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_project_index.py +0 -0
  36. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  37. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_pytest.py +0 -0
  38. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_sql.py +0 -0
  39. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  40. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  41. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  42. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  43. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  44. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  45. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  46. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  47. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  48. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  49. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  50. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  51. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  52. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  53. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  54. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  55. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  56. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  57. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  58. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  59. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  60. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  61. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  62. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  63. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  64. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  65. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  66. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  67. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  68. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  69. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  70. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  71. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  72. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  73. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  74. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  75. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  76. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  77. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  78. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  79. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  80. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  81. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  82. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  83. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  84. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  85. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  86. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  87. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  88. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  89. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  90. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  91. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  92. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  93. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  94. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  95. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  96. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  97. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  98. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  99. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  100. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  101. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  102. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  103. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  104. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  105. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  106. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  107. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  108. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  109. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  110. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  111. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  112. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  113. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  114. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  115. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  116. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  117. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  118. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  119. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  120. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  121. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  122. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  123. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  124. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  125. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  126. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  127. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  128. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  129. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  130. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  131. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  132. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  133. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  134. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
  135. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  136. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/stepdown.py +0 -0
  137. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  138. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  139. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  140. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  141. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  142. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  143. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  144. {sarj_python_lint-0.87.4 → sarj_python_lint-0.87.7}/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.4
3
+ Version: 0.87.7
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.4"
3
+ version = "0.87.7"
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,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
- from typing import TYPE_CHECKING, final
6
+ from typing import TYPE_CHECKING, TypeIs, final
7
7
 
8
8
 
9
9
  if TYPE_CHECKING:
@@ -13,12 +13,16 @@ if TYPE_CHECKING:
13
13
  _AST = ast.AST
14
14
 
15
15
 
16
+ def object_list(value: object) -> TypeIs[list[object]]:
17
+ return isinstance(value, list)
18
+
19
+
16
20
  def children(node: ast.AST) -> list[ast.AST]:
17
21
  out: list[ast.AST] = []
18
22
  for name in node._fields:
19
23
  value: object = getattr(node, name, None)
20
- if isinstance(value, list):
21
- out += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
24
+ if object_list(value):
25
+ out += [item for item in value if isinstance(item, _AST)]
22
26
  elif isinstance(value, _AST):
23
27
  out.append(value)
24
28
  return out
@@ -30,12 +34,7 @@ def walk(node: ast.AST) -> Iterator[ast.AST]:
30
34
  while i < len(queue):
31
35
  current = queue[i]
32
36
  i += 1
33
- for name in current._fields:
34
- value: object = getattr(current, name, None)
35
- if isinstance(value, list):
36
- queue += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
37
- elif isinstance(value, _AST):
38
- queue.append(value)
37
+ queue += children(current)
39
38
  yield current
40
39
 
41
40
 
@@ -56,12 +55,7 @@ class _NodeIndex:
56
55
  buckets[cls] = [current]
57
56
  else:
58
57
  bucket.append(current)
59
- for name in cls._fields:
60
- value: object = getattr(current, name, None)
61
- if isinstance(value, list):
62
- flat += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
63
- elif isinstance(value, _AST):
64
- flat.append(value)
58
+ flat += children(current)
65
59
  self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
66
60
  self._flat: list[ast.AST] = flat
67
61
  self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
@@ -47,6 +47,7 @@ from sarj_python_lint.rules.no_file_level_escape_hatch_suppression import (
47
47
  from sarj_python_lint.rules.no_first_party_private_import import (
48
48
  NoFirstPartyPrivateImport,
49
49
  )
50
+ from sarj_python_lint.rules.no_fixed_sleep_before_assert import NoFixedSleepBeforeAssert
50
51
  from sarj_python_lint.rules.no_frozen_after_validator_field_write import (
51
52
  NoFrozenAfterValidatorFieldWrite,
52
53
  )
@@ -204,6 +205,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
204
205
  NoRawSourceTextTestOracle.id: NoRawSourceTextTestOracle,
205
206
  IacSourceCoupledTest.id: IacSourceCoupledTest,
206
207
  NoRawConnectionInTests.id: NoRawConnectionInTests,
208
+ NoFixedSleepBeforeAssert.id: NoFixedSleepBeforeAssert,
207
209
  NoAnalyticalAggregationInPostgresStore.id: NoAnalyticalAggregationInPostgresStore,
208
210
  NoSelectStar.id: NoSelectStar,
209
211
  NoServiceBehaviorInSettings.id: NoServiceBehaviorInSettings,
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from itertools import pairwise
5
+ from pathlib import PurePosixPath
6
+ import re
7
+ from typing import TYPE_CHECKING, ClassVar, Final, final, override
8
+
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ Rule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
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
+ _ASYNC_SLEEPS: Final = frozenset({"asyncio.sleep", "anyio.sleep", "trio.sleep"})
31
+ _DURATION_KEYWORDS: Final = frozenset({"delay", "seconds"})
32
+ _CONSTANT_NAME: Final = re.compile(r"_*[A-Z][A-Z0-9_]*")
33
+ _MESSAGE: Final = (
34
+ "fixed sleep before an assertion guesses how long the code under test needs; await its signal or poll "
35
+ "the condition with a deadline"
36
+ )
37
+
38
+ type _TestFunction = ast.FunctionDef | ast.AsyncFunctionDef
39
+
40
+
41
+ @final
42
+ class NoFixedSleepBeforeAssert(Rule):
43
+ id = "no-fixed-sleep-before-assert"
44
+ code = "SARJ455"
45
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
46
+ default_level=Severity.WARNING,
47
+ summary="Do not sleep for a fixed interval and then assert in a test.",
48
+ rationale=(
49
+ "A fixed sleep followed by an assertion guesses how long the code under test needs. On a loaded CI "
50
+ "runner the guess is too short and the test flakes; on an idle one every run pays the whole interval."
51
+ ),
52
+ remediation=(
53
+ "Await the signal the code exposes, such as an event, future, queue item, or fake callback, or poll the "
54
+ "condition with a deadline. When the test deliberately proves that nothing happens during a quiet "
55
+ "window, keep the sleep with an exact SARJ455 suppression that names the window."
56
+ ),
57
+ category=RuleCategory.TESTING,
58
+ autofix=AutofixPolicy.NONE,
59
+ limitations=(
60
+ "Only pytest-collected test functions in test_*.py and *_test.py modules are inspected; fixtures, helpers, nested functions, and conftest.py are not.",
61
+ "A sleep is reported only when the next statement in the same block is an assert, so an assert placed after the enclosing with, if, or try block is not reported.",
62
+ "A duration is fixed only when built from numeric literals and UPPER_CASE constants; durations read from other names or from calls are not reported.",
63
+ "Sleeps inside loops are treated as polling, and time.sleep in an async test is left to Ruff ASYNC251.",
64
+ ),
65
+ examples=(
66
+ RuleExample(
67
+ example_id="test-sleeps-then-asserts",
68
+ title="A test waits a guessed interval before asserting",
69
+ outcome=ExampleOutcome.MATCH,
70
+ files=(
71
+ ExampleFile.python(
72
+ "tests/test_monitor.py",
73
+ "import asyncio\n\n\nasync def test_reports_silence(monitor):\n monitor.start()\n"
74
+ " await asyncio.sleep(0.5)\n assert monitor.reason == 'silence'\n",
75
+ ),
76
+ ),
77
+ focus_path=PurePosixPath("tests/test_monitor.py"),
78
+ expected_count=1,
79
+ public=True,
80
+ ),
81
+ RuleExample(
82
+ example_id="test-awaits-signal-then-asserts",
83
+ title="A test awaits the signal with a deadline before asserting",
84
+ outcome=ExampleOutcome.NO_MATCH,
85
+ files=(
86
+ ExampleFile.python(
87
+ "tests/test_monitor.py",
88
+ "import asyncio\n\n\nasync def test_reports_silence(monitor):\n monitor.start()\n"
89
+ " await asyncio.wait_for(monitor.fired.wait(), timeout=5)\n"
90
+ " assert monitor.reason == 'silence'\n",
91
+ ),
92
+ ),
93
+ focus_path=PurePosixPath("tests/test_monitor.py"),
94
+ expected_count=0,
95
+ public=True,
96
+ ),
97
+ RuleExample(
98
+ example_id="test-blocks-then-asserts",
99
+ title="A synchronous test blocks for a guessed interval before asserting",
100
+ outcome=ExampleOutcome.MATCH,
101
+ files=(
102
+ ExampleFile.python(
103
+ "tests/test_worker.py",
104
+ "import time\n\n\ndef test_worker_drains_queue(worker, queue):\n worker.start()\n"
105
+ " time.sleep(1)\n assert queue.pending() == 0\n",
106
+ ),
107
+ ),
108
+ focus_path=PurePosixPath("tests/test_worker.py"),
109
+ expected_count=1,
110
+ scenario="blocking-sleep",
111
+ public=True,
112
+ ),
113
+ RuleExample(
114
+ example_id="test-polls-with-deadline-then-asserts",
115
+ title="A synchronous test polls the condition with a deadline before asserting",
116
+ outcome=ExampleOutcome.NO_MATCH,
117
+ files=(
118
+ ExampleFile.python(
119
+ "tests/test_worker.py",
120
+ "import time\n\n\ndef test_worker_drains_queue(worker, queue):\n worker.start()\n"
121
+ " deadline = time.monotonic() + 5\n"
122
+ " while queue.pending() and time.monotonic() < deadline:\n time.sleep(0.01)\n"
123
+ " assert queue.pending() == 0\n",
124
+ ),
125
+ ),
126
+ focus_path=PurePosixPath("tests/test_worker.py"),
127
+ expected_count=0,
128
+ scenario="blocking-sleep",
129
+ public=True,
130
+ ),
131
+ ),
132
+ )
133
+ description = documentation.summary
134
+
135
+ @override
136
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
137
+ if not _is_collected_test_module(path) or "sleep" not in source or is_generated(path, source):
138
+ return []
139
+ tree = parse_or_none(path, source)
140
+ if tree is None:
141
+ return []
142
+ imports = ImportIndex.from_tree(tree)
143
+ diagnostics = [
144
+ Diagnostic(path=path, line=statement.lineno, col=statement.col_offset + 1, code=self.code, message=_MESSAGE)
145
+ for test in _collected_tests(tree.body)
146
+ for statement, following in _adjacent_statements(test.body)
147
+ if isinstance(following, ast.Assert) and _is_fixed_sleep(statement, test, imports)
148
+ ]
149
+ return sorted(diagnostics, key=lambda item: (item.line, item.col))
150
+
151
+
152
+ def _is_collected_test_module(path: Path) -> bool:
153
+ return path.suffix == ".py" and (path.stem.startswith("test_") or path.stem.endswith("_test"))
154
+
155
+
156
+ def _collected_tests(statements: list[ast.stmt]) -> Iterator[_TestFunction]:
157
+ for statement in statements:
158
+ match statement:
159
+ case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
160
+ yield statement
161
+ case ast.ClassDef() if statement.name.startswith("Test"):
162
+ yield from _collected_tests(statement.body)
163
+ case _:
164
+ pass
165
+
166
+
167
+ def _adjacent_statements(block: list[ast.stmt]) -> Iterator[tuple[ast.stmt, ast.stmt]]:
168
+ yield from pairwise(block)
169
+ for statement in block:
170
+ for nested in _straight_line_blocks(statement):
171
+ yield from _adjacent_statements(nested)
172
+
173
+
174
+ def _straight_line_blocks(statement: ast.stmt) -> list[list[ast.stmt]]:
175
+ match statement:
176
+ case ast.If(body=body, orelse=orelse):
177
+ return [body, orelse]
178
+ case ast.With(body=body) | ast.AsyncWith(body=body):
179
+ return [body]
180
+ case (
181
+ ast.Try(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody)
182
+ | ast.TryStar(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody)
183
+ ):
184
+ return [body, *(handler.body for handler in handlers), orelse, finalbody]
185
+ case ast.Match(cases=cases):
186
+ return [case.body for case in cases]
187
+ case _:
188
+ return []
189
+
190
+
191
+ def _is_fixed_sleep(statement: ast.stmt, test: _TestFunction, imports: ImportIndex) -> bool:
192
+ match statement:
193
+ case ast.Expr(value=ast.Await(value=ast.Call() as call)):
194
+ return imports.resolved_qualified_name(call.func) in _ASYNC_SLEEPS and _has_fixed_duration(call)
195
+ case ast.Expr(value=ast.Call() as call) if isinstance(test, ast.FunctionDef):
196
+ return imports.resolved_qualified_name(call.func) == "time.sleep" and _has_fixed_duration(call)
197
+ case _:
198
+ return False
199
+
200
+
201
+ def _has_fixed_duration(call: ast.Call) -> bool:
202
+ duration = (
203
+ call.args[0]
204
+ if call.args
205
+ else next((keyword.value for keyword in call.keywords if keyword.arg in _DURATION_KEYWORDS), None)
206
+ )
207
+ match duration:
208
+ case None | ast.Constant(value=0):
209
+ return False
210
+ case _:
211
+ return _is_fixed(duration)
212
+
213
+
214
+ def _is_fixed(node: ast.expr) -> bool:
215
+ match node:
216
+ case ast.Constant(value=int() | float()):
217
+ return True
218
+ case ast.Name(id=name) | ast.Attribute(attr=name):
219
+ return _CONSTANT_NAME.fullmatch(name) is not None
220
+ case ast.UnaryOp(op=ast.UAdd() | ast.USub(), operand=operand):
221
+ return _is_fixed(operand)
222
+ case ast.BinOp(left=left, op=ast.Add() | ast.Sub() | ast.Mult() | ast.Div() | ast.FloorDiv(), right=right):
223
+ return _is_fixed(left) and _is_fixed(right)
224
+ case _:
225
+ return False
@@ -19,7 +19,7 @@ from sarj_python_lint.rule_base import (
19
19
  Severity,
20
20
  parse_or_none,
21
21
  )
22
- from sarj_python_lint.rules._ast_index import nodes
22
+ from sarj_python_lint.rules._ast_index import nodes, object_list
23
23
  from sarj_python_lint.rules._logging import LOG_METHODS, is_logger_expr
24
24
 
25
25
 
@@ -308,9 +308,9 @@ def _analysis_facts(tree: ast.Module) -> _AnalysisFacts:
308
308
 
309
309
 
310
310
  def _statement_list(value: object) -> list[ast.stmt] | None:
311
- if not isinstance(value, list):
311
+ if not object_list(value):
312
312
  return None
313
- ast_items: list[ast.AST] = [item for item in value if isinstance(item, ast.AST)] # pyright: ignore[reportUnknownVariableType] — narrowed by isinstance
313
+ ast_items: list[ast.AST] = [item for item in value if isinstance(item, ast.AST)]
314
314
  if not ast_items or not all(isinstance(item, ast.stmt) for item in ast_items):
315
315
  return None
316
316
  return [item for item in ast_items if isinstance(item, ast.stmt)]
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
20
20
  is_suppressed,
21
21
  parse_or_none,
22
22
  )
23
+ from sarj_python_lint.rules._ast_index import object_list
23
24
  from sarj_python_lint.rules._paths import is_generated
24
25
 
25
26
 
@@ -314,11 +315,11 @@ def _statement_lists(tree: ast.AST) -> list[list[ast.stmt]]:
314
315
  result: list[list[ast.stmt]] = []
315
316
  for node in ast.walk(tree):
316
317
  for field in ("body", "orelse", "finalbody"):
317
- value = getattr(node, field, None)
318
- if not isinstance(value, list):
318
+ value: object = getattr(node, field, None)
319
+ if not object_list(value):
319
320
  continue
320
321
  statements: list[ast.stmt] = []
321
- for item in value: # pyright: ignore[reportUnknownVariableType] -- AST fields are untyped lists
322
+ for item in value:
322
323
  if not isinstance(item, ast.stmt):
323
324
  break
324
325
  statements.append(item)
@@ -18,7 +18,7 @@ from sarj_python_lint.rule_base import (
18
18
  is_suppressed,
19
19
  parse_or_none,
20
20
  )
21
- from sarj_python_lint.rules._ast_index import children, nodes, walk
21
+ from sarj_python_lint.rules._ast_index import children, nodes, object_list, walk
22
22
  from sarj_python_lint.rules._paths import is_generated, is_test_path
23
23
 
24
24
 
@@ -148,10 +148,10 @@ class PreferWalrusRegexMatch(Rule):
148
148
  )
149
149
 
150
150
  for node in walk(tree):
151
- raw_body = getattr(node, "body", None)
152
- if not isinstance(raw_body, list):
151
+ raw_body: object = getattr(node, "body", None)
152
+ if not object_list(raw_body):
153
153
  continue
154
- body: list[ast.stmt] = [st for st in raw_body if isinstance(st, ast.stmt)] # pyright: ignore[reportUnknownVariableType]
154
+ body: list[ast.stmt] = [st for st in raw_body if isinstance(st, ast.stmt)]
155
155
  diags.extend(_check_body(node, body, context))
156
156
 
157
157
  return sorted(diags, key=lambda d: (d.line, d.col))