From ce1321466bb0d31c05e76a5b4de554c6bf4e19cf Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 21 Aug 2026 22:32:24 -0700 Subject: [PATCH] test(http-handler): drop the save/restore scaffolding around litellm globals (#37839) Nine tests in test_http_handler.py captured litellm.disable_aiohttp_transport, force_ipv4, ssl_ecdh_curve or the request_timeout pair, wrapped their whole body in a try, and put the value back in a finally. monkeypatch.setattr does all of that, so the captures, the try and the finally go away and the bodies lose a level of indentation. The class-scoped restore_request_timeout fixture existed only for that same bookkeeping and goes with them. litellm.in_memory_llm_clients_cache is left alone on purpose: the eviction tests assert a handler is garbage collected, and monkeypatch holds the replaced value alive until teardown, which keeps the weakref they check from clearing. --- test-quality-budget.json | 2 +- .../llms/custom_httpx/test_http_handler.py | 258 +++++++----------- 2 files changed, 107 insertions(+), 153 deletions(-) diff --git a/test-quality-budget.json b/test-quality-budget.json index a6bc189a7cf..e5184f2049e 100644 --- a/test-quality-budget.json +++ b/test-quality-budget.json @@ -12,7 +12,7 @@ "limit": 469 }, "TQ005": { - "limit": 2514 + "limit": 2488 }, "TQ006": { "limit": 34 diff --git a/tests/test_litellm/llms/custom_httpx/test_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_http_handler.py index fa1c7308c6f..641fae12bc2 100644 --- a/tests/test_litellm/llms/custom_httpx/test_http_handler.py +++ b/tests/test_litellm/llms/custom_httpx/test_http_handler.py @@ -131,79 +131,62 @@ def test_sync_post_streaming_status_error_should_not_wait_forever_for_body( @pytest.mark.asyncio async def test_ssl_security_level(monkeypatch): # Ensure aiohttp transport is enabled for this test - original_disable = litellm.disable_aiohttp_transport - litellm.disable_aiohttp_transport = False + monkeypatch.setattr(litellm, "disable_aiohttp_transport", False) - try: - with patch.dict(os.environ, clear=True): - # Set environment variable for SSL security level - monkeypatch.setenv("SSL_SECURITY_LEVEL", "DEFAULT@SECLEVEL=1") + with patch.dict(os.environ, clear=True): + # Set environment variable for SSL security level + monkeypatch.setenv("SSL_SECURITY_LEVEL", "DEFAULT@SECLEVEL=1") - # Create async client with SSL verification disabled to isolate SSL context testing - client = AsyncHTTPHandler() + # Create async client with SSL verification disabled to isolate SSL context testing + client = AsyncHTTPHandler() - try: - # Get the transport (should be LiteLLMAiohttpTransport) - transport = client.client._transport - assert isinstance(transport, LiteLLMAiohttpTransport) + try: + # Get the transport (should be LiteLLMAiohttpTransport) + transport = client.client._transport + assert isinstance(transport, LiteLLMAiohttpTransport) - # Get the aiohttp ClientSession - client_session = transport._get_valid_client_session() + # Get the aiohttp ClientSession + client_session = transport._get_valid_client_session() - # Get the connector from the session - connector = client_session.connector - assert isinstance(connector, TCPConnector) + # Get the connector from the session + connector = client_session.connector + assert isinstance(connector, TCPConnector) - # Get the SSL context from the connector - ssl_context = connector._ssl + # Get the SSL context from the connector + ssl_context = connector._ssl - # Verify that the SSL context exists and has the correct cipher string - assert isinstance(ssl_context, ssl.SSLContext) - finally: - await client.close() - finally: - # Restore original setting - litellm.disable_aiohttp_transport = original_disable + # Verify that the SSL context exists and has the correct cipher string + assert isinstance(ssl_context, ssl.SSLContext) + finally: + await client.close() @pytest.mark.asyncio -async def test_force_ipv4_transport(): +async def test_force_ipv4_transport(monkeypatch: pytest.MonkeyPatch): """Test transport creation with force_ipv4 enabled""" - original_force_ipv4 = litellm.force_ipv4 - original_disable = litellm.disable_aiohttp_transport - litellm.force_ipv4 = True - litellm.disable_aiohttp_transport = True + monkeypatch.setattr(litellm, "force_ipv4", True) + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) - try: - transport = AsyncHTTPHandler._create_async_transport() + transport = AsyncHTTPHandler._create_async_transport() - # Should get an AsyncHTTPTransport (no real HTTP call — avoids CI hangs) - assert isinstance(transport, httpx.AsyncHTTPTransport) - finally: - litellm.force_ipv4 = original_force_ipv4 - litellm.disable_aiohttp_transport = original_disable + # Should get an AsyncHTTPTransport (no real HTTP call — avoids CI hangs) + assert isinstance(transport, httpx.AsyncHTTPTransport) @pytest.mark.asyncio -async def test_aiohttp_disabled_transport(): +async def test_aiohttp_disabled_transport(monkeypatch: pytest.MonkeyPatch): """Test transport creation with aiohttp disabled""" - original_disable = litellm.disable_aiohttp_transport - original_force_ipv4 = litellm.force_ipv4 - litellm.disable_aiohttp_transport = True - litellm.force_ipv4 = False + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + monkeypatch.setattr(litellm, "force_ipv4", False) - try: - transport = AsyncHTTPHandler._create_async_transport() + transport = AsyncHTTPHandler._create_async_transport() - # Should get None when both aiohttp is disabled and force_ipv4 is False - assert transport is None - finally: - litellm.disable_aiohttp_transport = original_disable - litellm.force_ipv4 = original_force_ipv4 + # Should get None when both aiohttp is disabled and force_ipv4 is False + assert transport is None @pytest.mark.asyncio -async def test_ssl_verification_with_aiohttp_transport(): +async def test_ssl_verification_with_aiohttp_transport(monkeypatch: pytest.MonkeyPatch): """ Test aiohttp respects ssl_verify=False @@ -213,38 +196,33 @@ async def test_ssl_verification_with_aiohttp_transport(): import aiohttp # Ensure aiohttp transport is enabled for this test - original_disable = litellm.disable_aiohttp_transport - litellm.disable_aiohttp_transport = False + monkeypatch.setattr(litellm, "disable_aiohttp_transport", False) + + litellm_async_client = AsyncHTTPHandler(ssl_verify=False) try: - litellm_async_client = AsyncHTTPHandler(ssl_verify=False) + transport = litellm_async_client.client._transport + assert isinstance(transport, LiteLLMAiohttpTransport) + transport_connector = transport._get_valid_client_session().connector + assert isinstance(transport_connector, TCPConnector) + aiohttp_session = aiohttp.ClientSession( + connector=aiohttp.TCPConnector(ssl=False) + ) try: - transport = litellm_async_client.client._transport - assert isinstance(transport, LiteLLMAiohttpTransport) - transport_connector = transport._get_valid_client_session().connector - assert isinstance(transport_connector, TCPConnector) + aiohttp_connector = aiohttp_session.connector + assert isinstance(aiohttp_connector, aiohttp.TCPConnector) - aiohttp_session = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(ssl=False) - ) - try: - aiohttp_connector = aiohttp_session.connector - assert isinstance(aiohttp_connector, aiohttp.TCPConnector) - - # assert both litellm transport and aiohttp session have ssl_verify=False - assert transport_connector._ssl == aiohttp_connector._ssl - finally: - await aiohttp_session.close() + # assert both litellm transport and aiohttp session have ssl_verify=False + assert transport_connector._ssl == aiohttp_connector._ssl finally: - await litellm_async_client.close() + await aiohttp_session.close() finally: - # Restore original setting - litellm.disable_aiohttp_transport = original_disable + await litellm_async_client.close() @pytest.mark.asyncio -async def test_ssl_verification_with_shared_session(): +async def test_ssl_verification_with_shared_session(monkeypatch: pytest.MonkeyPatch): """ Test that ssl_verify=False is respected even with shared sessions. @@ -257,67 +235,55 @@ async def test_ssl_verification_with_shared_session(): import aiohttp # Ensure aiohttp transport is enabled for this test - original_disable = litellm.disable_aiohttp_transport - litellm.disable_aiohttp_transport = False + monkeypatch.setattr(litellm, "disable_aiohttp_transport", False) + + shared_session = aiohttp.ClientSession() try: - # Create a shared session (simulating what happens in production) - shared_session = aiohttp.ClientSession() + # Create transport with shared session and ssl_verify=False + transport = AsyncHTTPHandler._create_aiohttp_transport( + ssl_verify=False, + shared_session=shared_session, + ) - try: - # Create transport with shared session and ssl_verify=False - transport = AsyncHTTPHandler._create_aiohttp_transport( - ssl_verify=False, - shared_session=shared_session, - ) + # Verify the transport uses the shared session + assert transport.client is shared_session - # Verify the transport uses the shared session - assert transport.client is shared_session - - # Verify the SSL setting is stored in the transport for per-request use - assert transport._ssl_verify is False - finally: - await shared_session.close() + # Verify the SSL setting is stored in the transport for per-request use + assert transport._ssl_verify is False finally: - # Restore original setting - litellm.disable_aiohttp_transport = original_disable + await shared_session.close() @pytest.mark.asyncio -async def test_ssl_context_with_shared_session(): +async def test_ssl_context_with_shared_session(monkeypatch: pytest.MonkeyPatch): """ Test that ssl_context is respected even with shared sessions. """ import aiohttp # Ensure aiohttp transport is enabled for this test - original_disable = litellm.disable_aiohttp_transport - litellm.disable_aiohttp_transport = False + monkeypatch.setattr(litellm, "disable_aiohttp_transport", False) + + custom_ssl_context = ssl.create_default_context() + + # Create a shared session + shared_session = aiohttp.ClientSession() try: - # Create a custom SSL context - custom_ssl_context = ssl.create_default_context() + # Create transport with shared session and custom ssl_context + transport = AsyncHTTPHandler._create_aiohttp_transport( + ssl_context=custom_ssl_context, + shared_session=shared_session, + ) - # Create a shared session - shared_session = aiohttp.ClientSession() + # Verify the transport uses the shared session + assert transport.client is shared_session - try: - # Create transport with shared session and custom ssl_context - transport = AsyncHTTPHandler._create_aiohttp_transport( - ssl_context=custom_ssl_context, - shared_session=shared_session, - ) - - # Verify the transport uses the shared session - assert transport.client is shared_session - - # Verify the SSL context is stored in the transport for per-request use - assert transport._ssl_verify is custom_ssl_context - finally: - await shared_session.close() + # Verify the SSL context is stored in the transport for per-request use + assert transport._ssl_verify is custom_ssl_context finally: - # Restore original setting - litellm.disable_aiohttp_transport = original_disable + await shared_session.close() def test_get_ssl_configuration(): @@ -563,26 +529,22 @@ def test_ssl_ecdh_curve( if env_curve: monkeypatch.setenv("SSL_ECDH_CURVE", env_curve) - original_value = litellm.ssl_ecdh_curve - try: - litellm.ssl_ecdh_curve = litellm_curve + monkeypatch.setattr(litellm, "ssl_ecdh_curve", litellm_curve) - # Create a real SSL context and patch set_ecdh_curve on it - # We need a real SSLContext instance (not a MagicMock) because _create_ssl_context - # calls methods like set_ciphers() and minimum_version that require a real context. - # We patch set_ecdh_curve specifically to verify it's called with the correct curve. - real_ssl_context = ssl.create_default_context() - with patch("ssl.create_default_context", return_value=real_ssl_context): - with patch.object(real_ssl_context, "set_ecdh_curve") as mock_set_curve: - ssl_context = get_ssl_configuration() + # Create a real SSL context and patch set_ecdh_curve on it + # We need a real SSLContext instance (not a MagicMock) because _create_ssl_context + # calls methods like set_ciphers() and minimum_version that require a real context. + # We patch set_ecdh_curve specifically to verify it's called with the correct curve. + real_ssl_context = ssl.create_default_context() + with patch("ssl.create_default_context", return_value=real_ssl_context): + with patch.object(real_ssl_context, "set_ecdh_curve") as mock_set_curve: + ssl_context = get_ssl_configuration() - if should_call: - mock_set_curve.assert_called_once_with(expected_curve) - else: - mock_set_curve.assert_not_called() - assert isinstance(ssl_context, ssl.SSLContext) - finally: - litellm.ssl_ecdh_curve = original_value + if should_call: + mock_set_curve.assert_called_once_with(expected_curve) + else: + mock_set_curve.assert_not_called() + assert isinstance(ssl_context, ssl.SSLContext) def test_default_user_agent_is_litellm_version(monkeypatch): @@ -753,46 +715,38 @@ class TestDefaultCachedClientTimeoutHonorsRequestTimeout: no per-model timeout (e.g. Bedrock) hung for 600s. """ - @pytest.fixture - def restore_request_timeout(self): - original_value = litellm.request_timeout - original_flag = litellm.request_timeout_explicitly_set - try: - yield - finally: - litellm.request_timeout = original_value - litellm.request_timeout_explicitly_set = original_flag - - def test_default_when_request_timeout_unset(self, restore_request_timeout): + def test_default_when_request_timeout_unset(self, monkeypatch: pytest.MonkeyPatch): from litellm.llms.custom_httpx.http_handler import ( _DEFAULT_TIMEOUT, _default_cached_client_timeout, ) - litellm.request_timeout = litellm.constants.DEFAULT_REQUEST_TIMEOUT_SECONDS - litellm.request_timeout_explicitly_set = False + monkeypatch.setattr( + litellm, "request_timeout", litellm.constants.DEFAULT_REQUEST_TIMEOUT_SECONDS + ) + monkeypatch.setattr(litellm, "request_timeout_explicitly_set", False) assert _default_cached_client_timeout() is _DEFAULT_TIMEOUT - def test_uses_explicit_request_timeout(self, restore_request_timeout): + def test_uses_explicit_request_timeout(self, monkeypatch: pytest.MonkeyPatch): from litellm.llms.custom_httpx.http_handler import ( _default_cached_client_timeout, ) - litellm.request_timeout = 300 - litellm.request_timeout_explicitly_set = True + monkeypatch.setattr(litellm, "request_timeout", 300) + monkeypatch.setattr(litellm, "request_timeout_explicitly_set", True) resolved = _default_cached_client_timeout() assert resolved.read == 300.0 assert resolved.connect == 5.0 def test_cached_async_client_built_with_explicit_request_timeout( - self, restore_request_timeout + self, monkeypatch: pytest.MonkeyPatch ): from litellm.caching.llm_caching_handler import LLMClientCache from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.types.utils import LlmProviders - litellm.request_timeout = 300 - litellm.request_timeout_explicitly_set = True + monkeypatch.setattr(litellm, "request_timeout", 300) + monkeypatch.setattr(litellm, "request_timeout_explicitly_set", True) litellm.in_memory_llm_clients_cache = LLMClientCache() client = get_async_httpx_client(llm_provider=LlmProviders.BEDROCK) assert client.timeout.read == 300.0