sarj-python-lint 1.26.3__tar.gz → 1.27.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.
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_registry.py +4 -0
- sarj_python_lint-1.27.0/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +273 -0
- sarj_python_lint-1.27.0/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +162 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/.gitignore +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/LICENSE +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/README.md +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.27.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/
|
|
@@ -53,6 +53,7 @@ from sarj_python_lint.rules.no_first_party_private_import import (
|
|
|
53
53
|
NoFirstPartyPrivateImport,
|
|
54
54
|
)
|
|
55
55
|
from sarj_python_lint.rules.no_fixed_sleep_before_assert import NoFixedSleepBeforeAssert
|
|
56
|
+
from sarj_python_lint.rules.no_fixed_sleep_in_loop import NoFixedSleepInLoop
|
|
56
57
|
from sarj_python_lint.rules.no_frozen_after_validator_field_write import (
|
|
57
58
|
NoFrozenAfterValidatorFieldWrite,
|
|
58
59
|
)
|
|
@@ -169,6 +170,7 @@ from sarj_python_lint.rules.repeated_test_composition import RepeatedTestComposi
|
|
|
169
170
|
from sarj_python_lint.rules.require_explicit_contract_implementation import RequireExplicitContractImplementation
|
|
170
171
|
from sarj_python_lint.rules.require_explicit_psycopg_transaction import RequireExplicitPsycopgTransaction
|
|
171
172
|
from sarj_python_lint.rules.require_explicit_service_contract import RequireExplicitServiceContract
|
|
173
|
+
from sarj_python_lint.rules.require_injectable_retry_sleep import RequireInjectableRetrySleep
|
|
172
174
|
from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
|
|
173
175
|
RequireKeywordOnlySwapProneParams,
|
|
174
176
|
)
|
|
@@ -208,6 +210,8 @@ if TYPE_CHECKING:
|
|
|
208
210
|
|
|
209
211
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
210
212
|
{
|
|
213
|
+
RequireInjectableRetrySleep.id: RequireInjectableRetrySleep,
|
|
214
|
+
NoFixedSleepInLoop.id: NoFixedSleepInLoop,
|
|
211
215
|
NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
|
|
212
216
|
NoUnusedCopyResult.id: NoUnusedCopyResult,
|
|
213
217
|
NoBeforeValidatorInputMutation.id: NoBeforeValidatorInputMutation,
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
import re
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar, Final, NamedTuple, 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
|
+
is_suppressed,
|
|
20
|
+
)
|
|
21
|
+
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
|
|
27
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_ASYNC_SLEEPS: Final = frozenset({"asyncio.sleep", "anyio.sleep", "trio.sleep"})
|
|
31
|
+
_BLOCKING_SLEEP: Final = "time.sleep"
|
|
32
|
+
_DURATION_KEYWORDS: Final = frozenset({"delay", "seconds"})
|
|
33
|
+
_CONSTANT_NAME: Final = re.compile(r"_*[A-Z][A-Z0-9_]*")
|
|
34
|
+
_LOOPS: Final = (ast.For, ast.AsyncFor, ast.While)
|
|
35
|
+
_FUNCTIONS: Final = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
36
|
+
_SCOPES: Final = (*_FUNCTIONS, ast.Lambda, ast.ClassDef)
|
|
37
|
+
_MESSAGE: Final = (
|
|
38
|
+
"fixed sleep inside a loop makes every iteration wait on the wall clock, including under test; take the "
|
|
39
|
+
"interval or the sleeper from an injected dependency"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@final
|
|
44
|
+
class NoFixedSleepInLoop(Rule):
|
|
45
|
+
id = "no-fixed-sleep-in-loop"
|
|
46
|
+
code = "SARJ485"
|
|
47
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
48
|
+
default_level=Severity.WARNING,
|
|
49
|
+
summary="Do not sleep for a fixed literal interval inside a production loop.",
|
|
50
|
+
rationale=(
|
|
51
|
+
"A polling or cleanup loop that sleeps for a literal interval gives tests no seam: every test that "
|
|
52
|
+
"drives the loop pays the whole interval in real time, and replacing the module's sleep is a "
|
|
53
|
+
"monkeypatch that SARJ445 forbids. Suites slow down one hardcoded interval at a time."
|
|
54
|
+
),
|
|
55
|
+
remediation=(
|
|
56
|
+
"Route the interval through a constructor or configuration parameter, or call an injected sleeper or "
|
|
57
|
+
"clock, so tests can pass a zero interval or a recording fake. Keep a literal only behind an exact "
|
|
58
|
+
"SARJ485 suppression that names why the interval must never change under test."
|
|
59
|
+
),
|
|
60
|
+
category=RuleCategory.TESTING,
|
|
61
|
+
autofix=AutofixPolicy.NONE,
|
|
62
|
+
limitations=(
|
|
63
|
+
"Only awaited asyncio.sleep, anyio.sleep, and trio.sleep, and time.sleep in synchronous functions, are inspected; import aliases are followed and shadowed names abstain. time.sleep in an async function is left to Ruff ASYNC251.",
|
|
64
|
+
"A duration is fixed when it folds with +, -, *, /, and // from numeric literals and UPPER_CASE constants bound once at module scope in the same file to such a literal; zero and negative results are yields, not waits.",
|
|
65
|
+
"Imported, reassigned, or shadowed constants, lowercase names, attributes including class constants, subscripts, and calls are treated as configurable and are not reported.",
|
|
66
|
+
"The sleep must sit in the body of a for, async for, or while loop inside the same function; loop else clauses, module-level and class-level loops, and loops in an enclosing function across a nested def, lambda, or class are not reported.",
|
|
67
|
+
"Test modules, test directories, conftest.py, test-support paths, and generated files are excluded.",
|
|
68
|
+
),
|
|
69
|
+
examples=(
|
|
70
|
+
RuleExample(
|
|
71
|
+
example_id="polling-loop-sleeps-literal",
|
|
72
|
+
title="A polling loop sleeps for a hardcoded interval",
|
|
73
|
+
outcome=ExampleOutcome.MATCH,
|
|
74
|
+
files=(
|
|
75
|
+
ExampleFile.python(
|
|
76
|
+
"app/jobs.py",
|
|
77
|
+
"import asyncio\n\n\nasync def wait_until_done(job):\n while not await job.done():\n"
|
|
78
|
+
" await asyncio.sleep(2)\n",
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
focus_path=PurePosixPath("app/jobs.py"),
|
|
82
|
+
expected_count=1,
|
|
83
|
+
public=True,
|
|
84
|
+
),
|
|
85
|
+
RuleExample(
|
|
86
|
+
example_id="polling-loop-sleeps-injected-interval",
|
|
87
|
+
title="A polling loop sleeps for an injected interval",
|
|
88
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
89
|
+
files=(
|
|
90
|
+
ExampleFile.python(
|
|
91
|
+
"app/jobs.py",
|
|
92
|
+
"import asyncio\n\n\nclass JobWaiter:\n def __init__(self, poll_seconds: float) -> None:\n"
|
|
93
|
+
" self._poll_seconds = poll_seconds\n\n async def wait_until_done(self, job):\n"
|
|
94
|
+
" while not await job.done():\n await asyncio.sleep(self._poll_seconds)\n",
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
focus_path=PurePosixPath("app/jobs.py"),
|
|
98
|
+
expected_count=0,
|
|
99
|
+
public=True,
|
|
100
|
+
),
|
|
101
|
+
RuleExample(
|
|
102
|
+
example_id="blocking-loop-sleeps-literal",
|
|
103
|
+
title="A synchronous retry loop blocks for a hardcoded interval",
|
|
104
|
+
outcome=ExampleOutcome.MATCH,
|
|
105
|
+
files=(
|
|
106
|
+
ExampleFile.python(
|
|
107
|
+
"app/health.py",
|
|
108
|
+
"import time\n\n\ndef wait_for_health(probe):\n for _ in range(30):\n"
|
|
109
|
+
" if probe():\n return\n time.sleep(0.5)\n",
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
focus_path=PurePosixPath("app/health.py"),
|
|
113
|
+
expected_count=1,
|
|
114
|
+
scenario="blocking-sleep",
|
|
115
|
+
public=True,
|
|
116
|
+
),
|
|
117
|
+
RuleExample(
|
|
118
|
+
example_id="blocking-loop-calls-injected-sleeper",
|
|
119
|
+
title="A synchronous retry loop calls an injected sleeper",
|
|
120
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
121
|
+
files=(
|
|
122
|
+
ExampleFile.python(
|
|
123
|
+
"app/health.py",
|
|
124
|
+
"from collections.abc import Callable\n\n\n"
|
|
125
|
+
"def wait_for_health(probe, sleep: Callable[[float], None]):\n for _ in range(30):\n"
|
|
126
|
+
" if probe():\n return\n sleep(0.5)\n",
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
focus_path=PurePosixPath("app/health.py"),
|
|
130
|
+
expected_count=0,
|
|
131
|
+
scenario="blocking-sleep",
|
|
132
|
+
public=True,
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
description = documentation.summary
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
140
|
+
path = context.path
|
|
141
|
+
if (
|
|
142
|
+
"sleep" not in context.source
|
|
143
|
+
or is_test_path(path)
|
|
144
|
+
or is_test_support_path(path)
|
|
145
|
+
or context.generated
|
|
146
|
+
or context.tree is None
|
|
147
|
+
):
|
|
148
|
+
return []
|
|
149
|
+
sleeps = [
|
|
150
|
+
sleep
|
|
151
|
+
for call in context.nodes(ast.Call)
|
|
152
|
+
if (sleep := _sleep(context, call)) is not None and _in_function_loop_body(context, sleep.reported)
|
|
153
|
+
]
|
|
154
|
+
if not sleeps:
|
|
155
|
+
return []
|
|
156
|
+
constants = _module_constants(context)
|
|
157
|
+
findings = [
|
|
158
|
+
Diagnostic(
|
|
159
|
+
path=path,
|
|
160
|
+
line=reported.lineno,
|
|
161
|
+
col=reported.col_offset + 1,
|
|
162
|
+
code=self.code,
|
|
163
|
+
message=_MESSAGE,
|
|
164
|
+
severity=Severity.WARNING,
|
|
165
|
+
)
|
|
166
|
+
for reported, duration in sleeps
|
|
167
|
+
if (seconds := _folded_seconds(duration, constants)) is not None
|
|
168
|
+
and seconds > 0
|
|
169
|
+
and not is_suppressed(context.source_lines, reported.lineno, self.code)
|
|
170
|
+
]
|
|
171
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _Sleep(NamedTuple):
|
|
175
|
+
reported: ast.expr
|
|
176
|
+
duration: ast.expr | None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _sleep(context: PythonFileContext, call: ast.Call) -> _Sleep | None:
|
|
180
|
+
name = context.imports.resolved_qualified_name(call.func)
|
|
181
|
+
parent = context.parents.get(call)
|
|
182
|
+
if name in _ASYNC_SLEEPS and isinstance(parent, ast.Await):
|
|
183
|
+
return _Sleep(parent, _duration(call))
|
|
184
|
+
if name == _BLOCKING_SLEEP and isinstance(_owner(context, call), ast.FunctionDef):
|
|
185
|
+
return _Sleep(call, _duration(call))
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _duration(call: ast.Call) -> ast.expr | None:
|
|
190
|
+
if call.args:
|
|
191
|
+
return call.args[0]
|
|
192
|
+
return next((keyword.value for keyword in call.keywords if keyword.arg in _DURATION_KEYWORDS), None)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _module_constants(context: PythonFileContext) -> dict[str, float]:
|
|
196
|
+
tree = context.tree
|
|
197
|
+
if tree is None:
|
|
198
|
+
return {}
|
|
199
|
+
assignments: dict[str, ast.expr] = {}
|
|
200
|
+
for statement in tree.body:
|
|
201
|
+
match statement:
|
|
202
|
+
case (
|
|
203
|
+
ast.Assign(targets=[ast.Name(id=name)], value=value)
|
|
204
|
+
| ast.AnnAssign(target=ast.Name(id=name), value=ast.expr() as value)
|
|
205
|
+
) if _CONSTANT_NAME.fullmatch(name):
|
|
206
|
+
assignments[name] = value
|
|
207
|
+
case _:
|
|
208
|
+
pass
|
|
209
|
+
stores = Counter(node.id for node in context.nodes(ast.Name) if not isinstance(node.ctx, ast.Load))
|
|
210
|
+
rebound = {
|
|
211
|
+
*(node.arg for node in context.nodes(ast.arg)),
|
|
212
|
+
*(name for node in context.nodes(ast.Global, ast.Nonlocal) for name in node.names),
|
|
213
|
+
*(node.name for node in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)),
|
|
214
|
+
*context.imports.bindings,
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
name: seconds
|
|
218
|
+
for name, value in assignments.items()
|
|
219
|
+
if stores[name] == 1 and name not in rebound and (seconds := _folded_seconds(value, {})) is not None
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _folded_seconds(node: ast.expr | None, constants: Mapping[str, float]) -> float | None:
|
|
224
|
+
match node:
|
|
225
|
+
case ast.Constant(value=bool()):
|
|
226
|
+
return None
|
|
227
|
+
case ast.Constant(value=int() | float() as value):
|
|
228
|
+
return value
|
|
229
|
+
case ast.Name(id=name):
|
|
230
|
+
return constants.get(name)
|
|
231
|
+
case ast.UnaryOp(op=ast.UAdd(), operand=operand):
|
|
232
|
+
return _folded_seconds(operand, constants)
|
|
233
|
+
case ast.UnaryOp(op=ast.USub(), operand=operand):
|
|
234
|
+
value = _folded_seconds(operand, constants)
|
|
235
|
+
return None if value is None else -value
|
|
236
|
+
case ast.BinOp(left=left, op=op, right=right):
|
|
237
|
+
lhs, rhs = _folded_seconds(left, constants), _folded_seconds(right, constants)
|
|
238
|
+
return None if lhs is None or rhs is None else _arithmetic(op, lhs, rhs)
|
|
239
|
+
case _:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _arithmetic(op: ast.operator, lhs: float, rhs: float) -> float | None:
|
|
244
|
+
match op:
|
|
245
|
+
case ast.Add():
|
|
246
|
+
return lhs + rhs
|
|
247
|
+
case ast.Sub():
|
|
248
|
+
return lhs - rhs
|
|
249
|
+
case ast.Mult():
|
|
250
|
+
return lhs * rhs
|
|
251
|
+
case ast.Div() if rhs:
|
|
252
|
+
return lhs / rhs
|
|
253
|
+
case ast.FloorDiv() if rhs:
|
|
254
|
+
return lhs // rhs
|
|
255
|
+
case _:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _owner(context: PythonFileContext, node: ast.AST) -> ast.AST | None:
|
|
260
|
+
current = context.parents.get(node)
|
|
261
|
+
while current is not None and not isinstance(current, _SCOPES):
|
|
262
|
+
current = context.parents.get(current)
|
|
263
|
+
return current
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _in_function_loop_body(context: PythonFileContext, node: ast.AST) -> bool:
|
|
267
|
+
in_loop = False
|
|
268
|
+
child, parent = node, context.parents.get(node)
|
|
269
|
+
while parent is not None and not isinstance(parent, _SCOPES):
|
|
270
|
+
if isinstance(parent, _LOOPS) and any(statement is child for statement in parent.body):
|
|
271
|
+
in_loop = True
|
|
272
|
+
child, parent = parent, context.parents.get(parent)
|
|
273
|
+
return in_loop and isinstance(parent, _FUNCTIONS)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, Final, 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
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
24
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_POLICIES: Final = frozenset(
|
|
28
|
+
{"tenacity.retry", "tenacity.Retrying", "tenacity.AsyncRetrying", "tenacity.asyncio.AsyncRetrying"}
|
|
29
|
+
)
|
|
30
|
+
_WAIT_MODULES: Final = frozenset({"tenacity", "tenacity.wait"})
|
|
31
|
+
_WALL_CLOCK_SLEEPS: Final = frozenset(
|
|
32
|
+
{"asyncio.sleep", "anyio.sleep", "trio.sleep", "time.sleep", "tenacity.nap.sleep"}
|
|
33
|
+
)
|
|
34
|
+
_MESSAGE: Final = (
|
|
35
|
+
"this tenacity policy waits between attempts on the wall clock without a sleep= seam; pass an injected sleep "
|
|
36
|
+
"so tests exercise retries without real delays"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@final
|
|
41
|
+
class RequireInjectableRetrySleep(Rule):
|
|
42
|
+
id = "require-injectable-retry-sleep"
|
|
43
|
+
code = "SARJ486"
|
|
44
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
45
|
+
default_level=Severity.WARNING,
|
|
46
|
+
summary="Pass an injectable sleep to a tenacity retry policy that waits between attempts.",
|
|
47
|
+
rationale=(
|
|
48
|
+
"A tenacity policy built with a backoff wait and no sleep argument sleeps on the wall clock. Every test "
|
|
49
|
+
"that exercises a transient failure pays the backoff, and exhausting an exponential policy costs the "
|
|
50
|
+
"sum of every interval. Replacing tenacity's sleep from a test is a monkeypatch that SARJ445 forbids."
|
|
51
|
+
),
|
|
52
|
+
remediation=(
|
|
53
|
+
"Build the policy in a factory or constructor that accepts a sleep callable and pass it as sleep=, or "
|
|
54
|
+
"accept the wait strategy itself as a dependency. Tests then inject a no-op or recording sleeper, or "
|
|
55
|
+
"wait_none()."
|
|
56
|
+
),
|
|
57
|
+
category=RuleCategory.TESTING,
|
|
58
|
+
autofix=AutofixPolicy.NONE,
|
|
59
|
+
limitations=(
|
|
60
|
+
"Only calls to tenacity.retry, tenacity.Retrying, and tenacity.AsyncRetrying resolved through imports are inspected; bare @retry, positional arguments, and **kwargs expansion abstain.",
|
|
61
|
+
"A wait is reported only when it is built inline from tenacity wait_* calls, optionally combined with +; wait_none() and wait_fixed(0) do not wait. A wait read from a name, attribute, or other call is treated as injected.",
|
|
62
|
+
"Any sleep= argument other than a wall-clock sleep imported from asyncio, anyio, trio, time, or tenacity.nap counts as a seam; the rule does not prove that tests replace it.",
|
|
63
|
+
"The backoff library has no sleep parameter, so its decorators are not inspected. Test modules, test directories, conftest.py, test-support paths, and generated files are excluded.",
|
|
64
|
+
),
|
|
65
|
+
examples=(
|
|
66
|
+
RuleExample(
|
|
67
|
+
example_id="retry-backoff-without-sleep",
|
|
68
|
+
title="A module-level retry decorator backs off on the wall clock",
|
|
69
|
+
outcome=ExampleOutcome.MATCH,
|
|
70
|
+
files=(
|
|
71
|
+
ExampleFile.python(
|
|
72
|
+
"app/retry.py",
|
|
73
|
+
"from tenacity import retry, stop_after_attempt, wait_exponential\n\n"
|
|
74
|
+
"client_retry = retry(stop=stop_after_attempt(4), wait=wait_exponential(multiplier=2))\n",
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
focus_path=PurePosixPath("app/retry.py"),
|
|
78
|
+
expected_count=1,
|
|
79
|
+
public=True,
|
|
80
|
+
),
|
|
81
|
+
RuleExample(
|
|
82
|
+
example_id="retry-backoff-with-injected-sleep",
|
|
83
|
+
title="A retry factory passes an injected sleep",
|
|
84
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
85
|
+
files=(
|
|
86
|
+
ExampleFile.python(
|
|
87
|
+
"app/retry.py",
|
|
88
|
+
"from collections.abc import Awaitable, Callable\n\n"
|
|
89
|
+
"from tenacity import AsyncRetrying, stop_after_attempt, wait_exponential\n\n\n"
|
|
90
|
+
"def client_retrying(sleep: Callable[[float], Awaitable[None]]) -> AsyncRetrying:\n"
|
|
91
|
+
" return AsyncRetrying(sleep=sleep, stop=stop_after_attempt(4), "
|
|
92
|
+
"wait=wait_exponential(multiplier=2))\n",
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
focus_path=PurePosixPath("app/retry.py"),
|
|
96
|
+
expected_count=0,
|
|
97
|
+
public=True,
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
description = documentation.summary
|
|
102
|
+
|
|
103
|
+
@override
|
|
104
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
105
|
+
path = context.path
|
|
106
|
+
if (
|
|
107
|
+
"tenacity" not in context.source
|
|
108
|
+
or is_test_path(path)
|
|
109
|
+
or is_test_support_path(path)
|
|
110
|
+
or context.generated
|
|
111
|
+
or context.tree is None
|
|
112
|
+
):
|
|
113
|
+
return []
|
|
114
|
+
imports = context.imports
|
|
115
|
+
findings = [
|
|
116
|
+
Diagnostic(
|
|
117
|
+
path=path,
|
|
118
|
+
line=call.lineno,
|
|
119
|
+
col=call.col_offset + 1,
|
|
120
|
+
code=self.code,
|
|
121
|
+
message=_MESSAGE,
|
|
122
|
+
severity=Severity.WARNING,
|
|
123
|
+
)
|
|
124
|
+
for call in context.nodes(ast.Call)
|
|
125
|
+
if imports.resolved_qualified_name(call.func) in _POLICIES
|
|
126
|
+
and _waits_without_seam(call, imports)
|
|
127
|
+
and not is_suppressed(context.source_lines, call.lineno, self.code)
|
|
128
|
+
]
|
|
129
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _waits_without_seam(call: ast.Call, imports: ImportIndex) -> bool:
|
|
133
|
+
if call.args or any(keyword.arg is None for keyword in call.keywords):
|
|
134
|
+
return False
|
|
135
|
+
arguments = {keyword.arg: keyword.value for keyword in call.keywords}
|
|
136
|
+
wait = arguments.get("wait")
|
|
137
|
+
if wait is None or _wait_strategy(wait, imports) is not True:
|
|
138
|
+
return False
|
|
139
|
+
sleep = arguments.get("sleep")
|
|
140
|
+
return sleep is None or imports.resolved_qualified_name(sleep) in _WALL_CLOCK_SLEEPS
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _wait_strategy(node: ast.expr, imports: ImportIndex) -> bool | None:
|
|
144
|
+
match node:
|
|
145
|
+
case ast.BinOp(left=left, op=ast.Add(), right=right):
|
|
146
|
+
parts = (_wait_strategy(left, imports), _wait_strategy(right, imports))
|
|
147
|
+
return None if None in parts else any(parts)
|
|
148
|
+
case ast.Call(func=func, args=args, keywords=keywords):
|
|
149
|
+
module, _, strategy = (imports.resolved_qualified_name(func) or "").rpartition(".")
|
|
150
|
+
if module not in _WAIT_MODULES or not strategy.startswith("wait_"):
|
|
151
|
+
return None
|
|
152
|
+
match strategy, args, keywords:
|
|
153
|
+
case "wait_none", [], []:
|
|
154
|
+
return False
|
|
155
|
+
case "wait_fixed", [ast.Constant(value=0)], []:
|
|
156
|
+
return False
|
|
157
|
+
case "wait_fixed", [], [ast.keyword(arg="wait", value=ast.Constant(value=0))]:
|
|
158
|
+
return False
|
|
159
|
+
case _:
|
|
160
|
+
return True
|
|
161
|
+
case _:
|
|
162
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/_test_provenance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.26.3 → sarj_python_lint-1.27.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|