diff --git a/ruff-tests.toml b/ruff-tests.toml index ff29bcff313..de0931f5e69 100644 --- a/ruff-tests.toml +++ b/ruff-tests.toml @@ -32,6 +32,10 @@ # PT017 an `assert` on the caught error inside `except`. Nothing runs the handler when # the call stops raising, so the test goes green on the exact regression it was # written to catch. `pytest.raises` fails when the call succeeds +# RUF043 a `match=` pattern carrying regex metacharacters in a plain string. `match=` is +# `re.search`, so a `.` copied out of an error message is a wildcard and the block +# accepts messages the author never meant to accept. Mark a real regex raw, wrap a +# literal message in `re.escape`, and the pattern says which one it is # # No target-version here on purpose: it resolves from requires-python (>=3.10), so # 3.11-only builtins like BaseExceptionGroup are correctly flagged in a tree that @@ -53,4 +57,5 @@ lint.select = [ "PT017", "PLR0133", "PLW0127", + "RUF043", ] diff --git a/tests/local_testing/test_embedding.py b/tests/local_testing/test_embedding.py index ee9d4cdd915..677f8b10cca 100644 --- a/tests/local_testing/test_embedding.py +++ b/tests/local_testing/test_embedding.py @@ -1,5 +1,6 @@ import json import os +import re import sys import traceback @@ -536,13 +537,13 @@ def test_demo_tokens_as_input_to_embeddings_fails_for_titan(): with pytest.raises( litellm.BadRequestError, - match='litellm.BadRequestError: BedrockException - {"message":"Malformed input request: expected type: String, found: JSONArray, please reformat your input and try again."}', + match=re.escape('litellm.BadRequestError: BedrockException - {"message":"Malformed input request: expected type: String, found: JSONArray, please reformat your input and try again."}'), ): litellm.embedding(model="amazon.titan-embed-text-v1", input=[[1]]) with pytest.raises( litellm.BadRequestError, - match='litellm.BadRequestError: BedrockException - {"message":"Malformed input request: expected type: String, found: Integer, please reformat your input and try again."}', + match=re.escape('litellm.BadRequestError: BedrockException - {"message":"Malformed input request: expected type: String, found: Integer, please reformat your input and try again."}'), ): litellm.embedding( model="amazon.titan-embed-text-v1", diff --git a/tests/multi_instance_e2e_tests/test_update_team_e2e.py b/tests/multi_instance_e2e_tests/test_update_team_e2e.py index 13091fd3df6..ce88e976ce0 100644 --- a/tests/multi_instance_e2e_tests/test_update_team_e2e.py +++ b/tests/multi_instance_e2e_tests/test_update_team_e2e.py @@ -143,7 +143,7 @@ async def test_team_blocking_behavior_multi_instance(): assert team_info_4001["blocked"] is True, "Team should be blocked after update" # 8. Make a chat completion request on port 4000 with a new prompt; expect it to be blocked. - with pytest.raises(Exception, match="(?i)blocked") as excinfo: + with pytest.raises(Exception, match=r"(?i)blocked") as excinfo: await chat_completion_on_port( session, key=key, @@ -157,7 +157,7 @@ async def test_team_blocking_behavior_multi_instance(): ), f"Expected error indicating team blocked, got: {error_msg}" # 9. Make a chat completion request on port 4000 with a new prompt; expect it to be blocked. - with pytest.raises(Exception, match="(?i)blocked") as excinfo: + with pytest.raises(Exception, match=r"(?i)blocked") as excinfo: await chat_completion_on_port( session, key=key, @@ -171,7 +171,7 @@ async def test_team_blocking_behavior_multi_instance(): ), f"Expected error indicating team blocked, got: {error_msg}" # 9. Repeat the chat completion request with another new prompt; expect it to be blocked. - with pytest.raises(Exception, match="(?i)blocked") as excinfo_second: + with pytest.raises(Exception, match=r"(?i)blocked") as excinfo_second: await chat_completion_on_port( session, key=key, diff --git a/tests/router_unit_tests/test_router_helper_utils.py b/tests/router_unit_tests/test_router_helper_utils.py index 755405c8b21..82bdbd7bfc9 100644 --- a/tests/router_unit_tests/test_router_helper_utils.py +++ b/tests/router_unit_tests/test_router_helper_utils.py @@ -1832,7 +1832,7 @@ def test_init_auto_router_deployment_duplicate_model_name(mock_auto_router, mode ) with pytest.raises( - ValueError, match="Auto-router deployment test-auto-router with tags .* already exists" + ValueError, match=r"Auto-router deployment test-auto-router with tags .* already exists" ): router.init_auto_router_deployment(deployment) diff --git a/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py b/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py index dd97de24df3..4a15da87c89 100644 --- a/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py +++ b/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py @@ -1,5 +1,6 @@ import json import os +import re import sys from unittest.mock import MagicMock, patch @@ -158,7 +159,7 @@ def test_bitbucket_client_get_file_content_access_denied(mock_get): client = BitBucketClient(config) - with pytest.raises(Exception, match="Access denied to file 'test.prompt'"): + with pytest.raises(Exception, match=re.escape("Access denied to file 'test.prompt'")): client.get_file_content("test.prompt") diff --git a/tests/test_litellm/integrations/gitlab/test_gitlab_prompt_manager.py b/tests/test_litellm/integrations/gitlab/test_gitlab_prompt_manager.py index 1f7706882f6..adccd94141f 100644 --- a/tests/test_litellm/integrations/gitlab/test_gitlab_prompt_manager.py +++ b/tests/test_litellm/integrations/gitlab/test_gitlab_prompt_manager.py @@ -1,4 +1,5 @@ import os +import re import sys from unittest.mock import MagicMock, patch @@ -172,7 +173,7 @@ def test_gitlab_client_get_file_content_access_denied(mock_get): mock_get.side_effect = err client = GitLabClient({"project": "g/s/r", "access_token": "tok"}) - with pytest.raises(Exception, match="Access denied to file 'test.prompt'"): + with pytest.raises(Exception, match=re.escape("Access denied to file 'test.prompt'")): client.get_file_content("test.prompt") diff --git a/tests/test_litellm/integrations/test_openmeter.py b/tests/test_litellm/integrations/test_openmeter.py index 539e3f99cdc..b9da99b6fa9 100644 --- a/tests/test_litellm/integrations/test_openmeter.py +++ b/tests/test_litellm/integrations/test_openmeter.py @@ -33,7 +33,7 @@ class TestOpenMeterIntegration: def test_openmeter_logger_missing_api_key(self): """Test that OpenMeterLogger raises exception when API key is missing""" os.environ.pop("OPENMETER_API_KEY", None) - with pytest.raises(Exception, match="Missing keys.*OPENMETER_API_KEY"): + with pytest.raises(Exception, match=r"Missing keys.*OPENMETER_API_KEY"): OpenMeterLogger() def test_common_logic_with_string_user(self): diff --git a/tests/test_litellm/litellm_core_utils/test_url_utils.py b/tests/test_litellm/litellm_core_utils/test_url_utils.py index 751b548adcd..aaaa43a0dc4 100644 --- a/tests/test_litellm/litellm_core_utils/test_url_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_url_utils.py @@ -100,12 +100,12 @@ class TestEncodeUrlPathSegment: @pytest.mark.parametrize("value", ["", ".", "..", None]) def test_rejects_empty_and_dot_segments(self, value): - with pytest.raises(ValueError, match="resource_id (is required|cannot be a dot path segment)"): + with pytest.raises(ValueError, match=r"resource_id (is required|cannot be a dot path segment)"): encode_url_path_segment(value, field_name="resource_id") @pytest.mark.parametrize("value", ["../model", "model/../other", "/model"]) def test_rejects_dot_segments_in_multi_segment_paths(self, value): - with pytest.raises(ValueError, match="model (is required|cannot be a dot path segment)"): + with pytest.raises(ValueError, match=r"model (is required|cannot be a dot path segment)"): encode_url_path_segments(value, field_name="model") diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index 25739a978d0..e519fab896a 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -929,7 +929,7 @@ class TestValidateEnvironmentAuthToken: config = AnthropicModelInfo() with mock_patch.dict("os.environ", {}, clear=True): with pytest.raises( - Exception, match="ANTHROPIC_API_KEY.*ANTHROPIC_AUTH_TOKEN" + Exception, match=r"ANTHROPIC_API_KEY.*ANTHROPIC_AUTH_TOKEN" ): config.validate_environment( headers={}, diff --git a/tests/test_litellm/llms/oci/chat/test_oci_generic_chat.py b/tests/test_litellm/llms/oci/chat/test_oci_generic_chat.py index a4a5f111513..0a47852d085 100644 --- a/tests/test_litellm/llms/oci/chat/test_oci_generic_chat.py +++ b/tests/test_litellm/llms/oci/chat/test_oci_generic_chat.py @@ -106,7 +106,7 @@ class TestGenericToolCallErrors: ) def test_non_string_id_raises(self): - with pytest.raises(OCIError, match="id.*must be a string"): + with pytest.raises(OCIError, match=r"id.*must be a string"): adapt_messages_to_generic_oci_standard_tool_call( "assistant", [ @@ -126,7 +126,7 @@ class TestGenericToolCallErrors: ) def test_non_string_function_name_raises(self): - with pytest.raises(OCIError, match="function.name.*must be a string"): + with pytest.raises(OCIError, match=r"function\.name.*must be a string"): adapt_messages_to_generic_oci_standard_tool_call( "assistant", [ @@ -139,7 +139,7 @@ class TestGenericToolCallErrors: ) def test_non_string_arguments_raises(self): - with pytest.raises(OCIError, match="arguments.*must be a JSON string"): + with pytest.raises(OCIError, match=r"arguments.*must be a JSON string"): adapt_messages_to_generic_oci_standard_tool_call( "assistant", [ diff --git a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_handler.py b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_handler.py index 5e854bbad70..cf52a5a856d 100644 --- a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_handler.py +++ b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_handler.py @@ -3,6 +3,7 @@ Test Vertex AI files handler functionality """ import asyncio +import re from types import MappingProxyType import pytest from unittest.mock import AsyncMock, patch @@ -180,7 +181,7 @@ class TestVertexAIFilesHandler: # Should raise ValueError for failed download with pytest.raises( ValueError, - match="Failed to download file from GCS: gs://test-bucket/litellm-vertex-files/uploads/abc-test-file.txt", + match=re.escape("Failed to download file from GCS: gs://test-bucket/litellm-vertex-files/uploads/abc-test-file.txt"), ): await self.handler.afile_content( file_content_request=file_content_request, diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index 813264c1feb..39a06c68913 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -33,7 +33,7 @@ def test_validate_vertex_location_accepts_valid(location): ["attacker.example/", "evil.com#", "us.attacker.example", "us/../..", "US", "us_central1", "-us", "", None], ) def test_validate_vertex_location_rejects_invalid(location): - with pytest.raises(ValueError, match="vertex_location is required|Invalid vertex_location format"): + with pytest.raises(ValueError, match=r"vertex_location is required|Invalid vertex_location format"): validate_vertex_location(location) diff --git a/tests/test_litellm/proxy/client/cli/test_agents.py b/tests/test_litellm/proxy/client/cli/test_agents.py index c2858c84c6d..e504dd6e8a0 100644 --- a/tests/test_litellm/proxy/client/cli/test_agents.py +++ b/tests/test_litellm/proxy/client/cli/test_agents.py @@ -255,7 +255,7 @@ class TestRunAgent: assert calls["args"] == ("claude", "--resume") def test_missing_binary_raises_with_install_hint(self): - with pytest.raises(AgentRunError, match="claude.*Install it first"): + with pytest.raises(AgentRunError, match=r"claude.*Install it first"): run_agent( "http://localhost:4000", "sk-key", diff --git a/tests/test_litellm/proxy/db/test_db_url_settings.py b/tests/test_litellm/proxy/db/test_db_url_settings.py index ee4cf7fbb05..0ceec49de12 100644 --- a/tests/test_litellm/proxy/db/test_db_url_settings.py +++ b/tests/test_litellm/proxy/db/test_db_url_settings.py @@ -504,7 +504,7 @@ def test_apply_to_env_rejects_pinned_sqlite_direct_url(monkeypatch): monkeypatch.setenv("DATABASE_URL", "postgresql://u:p@writer.example.com:5432/db") monkeypatch.setenv("DIRECT_URL", "sqlite:///data/litellm.db") - with pytest.raises(RuntimeError, match="DIRECT_URL.*sqlite"): + with pytest.raises(RuntimeError, match=r"DIRECT_URL.*sqlite"): _apply() @@ -514,7 +514,7 @@ def test_apply_to_env_rejects_pinned_non_postgres_reader(monkeypatch): "DATABASE_URL_READ_REPLICA", "mysql://u:p@reader.example.com:3306/db" ) - with pytest.raises(RuntimeError, match="DATABASE_URL_READ_REPLICA.*mysql"): + with pytest.raises(RuntimeError, match=r"DATABASE_URL_READ_REPLICA.*mysql"): _apply() diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index 47f01fe096d..b0b2c68e30d 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -10,6 +10,7 @@ from __future__ import annotations import json import os +import re from types import SimpleNamespace from typing import Any, Dict from unittest.mock import AsyncMock, MagicMock @@ -303,7 +304,7 @@ def test_resolve_routing_plugins_rejects_non_routing_plugin(tmp_path): plugin_file = tmp_path / "bad_rs_plugin.py" plugin_file.write_text("not_a_plugin = object()\n") - with pytest.raises(ValueError, match="router_settings.plugins"): + with pytest.raises(ValueError, match=re.escape("router_settings.plugins")): resolve_routing_plugins( plugin_paths=["bad_rs_plugin.not_a_plugin"], config_file_path=str(tmp_path / "config.yaml"), diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 76aeea894e1..a0ca33da737 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -2,6 +2,7 @@ import asyncio import importlib import json import os +import re import socket import subprocess import sys @@ -2683,7 +2684,7 @@ async def test_get_config_from_file(tmp_path, monkeypatch): with open(empty_file, "w") as f: f.write("") # Write empty content which will result in None when loaded - with pytest.raises(Exception, match="Config cannot be None or Empty."): + with pytest.raises(Exception, match=re.escape("Config cannot be None or Empty.")): await proxy_config._get_config_from_file(str(empty_file)) # Test Case 5: Using global user_config_file_path when no config_file_path provided diff --git a/tests/test_litellm/sandbox/test_opensandbox_sandbox.py b/tests/test_litellm/sandbox/test_opensandbox_sandbox.py index 0d7bcbe1e53..2928dea100e 100644 --- a/tests/test_litellm/sandbox/test_opensandbox_sandbox.py +++ b/tests/test_litellm/sandbox/test_opensandbox_sandbox.py @@ -490,7 +490,7 @@ async def test_create_waits_for_endpoint_resolution(monkeypatch): async def test_create_raises_when_endpoint_is_missing(): client = FakeHTTPClient(endpoint_json={"headers": {"X": "y"}}) - with pytest.raises(TimeoutError, match="execd endpoint.*not ready"): + with pytest.raises(TimeoutError, match=r"execd endpoint.*not ready"): await OpenSandboxSandboxConfig().acreate_sandbox( api_key="", api_base=TEST_API_BASE, ready_timeout=0, client=client ) diff --git a/tests/test_litellm/test_router_model_cost_isolation.py b/tests/test_litellm/test_router_model_cost_isolation.py index 4fdb5faf305..1580ec7f437 100644 --- a/tests/test_litellm/test_router_model_cost_isolation.py +++ b/tests/test_litellm/test_router_model_cost_isolation.py @@ -9,6 +9,7 @@ should still use the built-in pricing. import copy import os +import re import sys from unittest.mock import patch @@ -1892,7 +1893,7 @@ def test_a_reservation_without_a_declared_id_is_refused(): duplicate is permanent.""" anonymous = {k: v for k, v in _PTU_MODEL_INFO.items() if k != "id"} - with pytest.raises(ValueError, match="model_info.id is required"): + with pytest.raises(ValueError, match=re.escape("model_info.id is required")): _ptu_router(model_info=anonymous) @@ -1976,7 +1977,7 @@ def test_a_bare_yaml_date_bound_does_not_escape_the_id_rule(): windowed = {k: v for k, v in _PTU_MODEL_INFO.items() if k != "id"} - with pytest.raises(ValueError, match="model_info.id is required"): + with pytest.raises(ValueError, match=re.escape("model_info.id is required")): _ptu_router(model_info={**windowed, "ptu_effective_to": _dt.date(2027, 1, 1)})