sarj-python-lint 0.87.5__tar.gz → 0.87.8__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.5 → sarj_python_lint-0.87.8}/PKG-INFO +2 -2
  2. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/pyproject.toml +3 -3
  3. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-0.87.8/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +225 -0
  5. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/.gitignore +0 -0
  6. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/LICENSE +0 -0
  7. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/README.md +0 -0
  8. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/_filesystem.py +0 -0
  12. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/_python_target.py +0 -0
  13. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  14. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/_secret_names.py +0 -0
  15. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/_version.py +0 -0
  16. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/json_boundary.py +0 -0
  17. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/py.typed +0 -0
  18. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/ratchet.py +0 -0
  19. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rule_base.py +0 -0
  20. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/__init__.py +0 -0
  21. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  22. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  23. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  24. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_comments.py +0 -0
  25. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  26. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  27. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_first_party.py +0 -0
  28. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  29. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_imports.py +0 -0
  30. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_logging.py +0 -0
  31. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_paths.py +0 -0
  32. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_project_index.py +0 -0
  33. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  34. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_pytest.py +0 -0
  35. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_sql.py +0 -0
  36. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  37. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  38. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  39. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  40. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  41. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  42. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  43. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  44. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  45. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  46. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  47. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  48. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  49. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  50. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  51. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  52. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  53. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  54. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  55. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  56. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  57. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  58. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  59. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  60. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  61. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  62. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  63. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  64. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  65. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  66. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  67. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  68. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  69. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  70. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  71. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  72. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  73. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  74. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  75. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  76. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  77. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  78. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  79. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  80. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  81. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  82. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  83. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  84. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  85. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  86. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  87. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  88. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  89. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  90. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  91. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  92. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  93. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  94. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  95. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  96. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  97. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  98. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  99. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  100. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  101. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  102. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  103. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  104. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  105. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  106. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  107. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  108. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  109. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  110. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  111. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  112. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  113. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  114. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  115. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  116. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  117. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  118. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  119. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  120. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  121. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  122. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  123. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  124. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  125. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  126. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  127. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  128. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  129. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  130. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  131. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  132. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  133. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  134. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
  135. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  136. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/stepdown.py +0 -0
  137. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  138. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  139. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  140. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  141. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  142. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  143. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  144. {sarj_python_lint-0.87.5 → sarj_python_lint-0.87.8}/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.5
3
+ Version: 0.87.8
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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.16.8
22
- Requires-Dist: sqlglot==30.18.0
22
+ Requires-Dist: sqlglot==30.19.0
23
23
  Requires-Dist: typer==0.27.2
24
24
  Description-Content-Type: text/markdown
25
25
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.87.5"
3
+ version = "0.87.8"
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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["packaging==26.3", "ruff>=0.16.8", "sqlglot==30.18.0", "typer==0.27.2"]
18
+ dependencies = ["packaging==26.3", "ruff>=0.16.8", "sqlglot==30.19.0", "typer==0.27.2"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -36,7 +36,7 @@ dev = [
36
36
  ]
37
37
 
38
38
  [build-system]
39
- requires = ["hatchling==1.32.0"]
39
+ requires = ["hatchling==1.32.4"]
40
40
  build-backend = "hatchling.build"
41
41
 
42
42
  [tool.hatch.build.targets.wheel]
@@ -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