mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-11 16:26:07 +00:00
Merge pull request #21396 from BerriAI/fix/conftest-deprecation-warnings
fix(tests): improve conftest isolation and remove deprecation warnings
This commit is contained in:
@@ -824,6 +824,7 @@ def log_guardrail_information(func):
|
||||
"""
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
def _infer_event_type_from_function_name(
|
||||
func_name: str,
|
||||
@@ -904,7 +905,7 @@ def log_guardrail_information(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
if inspect.iscoroutinefunction(func):
|
||||
return async_wrapper(*args, **kwargs)
|
||||
return sync_wrapper(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Union
|
||||
@@ -270,7 +271,7 @@ def track_llm_api_timing():
|
||||
verbose_logger.debug(f"Error in service logging: {str(e)}")
|
||||
|
||||
# Check if the function is async or sync
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
if inspect.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import litellm
|
||||
@@ -101,8 +102,8 @@ def perform_redaction(model_call_details: dict, result):
|
||||
# Redact result
|
||||
if result is not None:
|
||||
# Check if result is a coroutine, async generator, or other async object - these cannot be deepcopied
|
||||
if (asyncio.iscoroutine(result) or
|
||||
asyncio.iscoroutinefunction(result) or
|
||||
if (asyncio.iscoroutine(result) or
|
||||
inspect.iscoroutinefunction(result) or
|
||||
hasattr(result, '__aiter__') or # async generator
|
||||
hasattr(result, '__anext__')): # async iterator
|
||||
# For async objects, return a simple redacted response without deepcopy
|
||||
|
||||
@@ -12,6 +12,7 @@ import asyncio
|
||||
import atexit
|
||||
import cProfile
|
||||
import functools
|
||||
import inspect
|
||||
import threading
|
||||
from pathlib import Path as PathLib
|
||||
from typing import Any, Callable, Optional
|
||||
@@ -100,7 +101,7 @@ def profile_endpoint(sampling_rate: float = 1.0):
|
||||
global _last_profile_file_path
|
||||
_last_profile_file_path = path
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
if inspect.iscoroutinefunction(func):
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
is_sampling = _start_profiling_for_request(sampling_rate)
|
||||
|
||||
@@ -11,6 +11,7 @@ All /key management endpoints
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
@@ -1105,7 +1106,7 @@ async def generate_key_fn(
|
||||
)
|
||||
|
||||
if user_custom_key_generate is not None:
|
||||
if asyncio.iscoroutinefunction(user_custom_key_generate):
|
||||
if inspect.iscoroutinefunction(user_custom_key_generate):
|
||||
result = await user_custom_key_generate(data) # type: ignore
|
||||
else:
|
||||
raise ValueError("user_custom_key_generate must be a coroutine")
|
||||
@@ -1257,7 +1258,7 @@ async def generate_service_account_key_fn(
|
||||
verbose_proxy_logger.debug("entered /key/generate")
|
||||
|
||||
if user_custom_key_generate is not None:
|
||||
if asyncio.iscoroutinefunction(user_custom_key_generate):
|
||||
if inspect.iscoroutinefunction(user_custom_key_generate):
|
||||
result = await user_custom_key_generate(data) # type: ignore
|
||||
else:
|
||||
raise ValueError("user_custom_key_generate must be a coroutine")
|
||||
|
||||
@@ -11,6 +11,7 @@ Has all /sso/* routes
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import inspect
|
||||
import os
|
||||
import secrets
|
||||
from copy import deepcopy
|
||||
@@ -2258,7 +2259,7 @@ class SSOAuthenticationHandler:
|
||||
user_defined_values: Optional[SSOUserDefinedValues] = None
|
||||
|
||||
if user_custom_sso is not None:
|
||||
if asyncio.iscoroutinefunction(user_custom_sso):
|
||||
if inspect.iscoroutinefunction(user_custom_sso):
|
||||
user_defined_values = await user_custom_sso(result) # type: ignore
|
||||
else:
|
||||
raise ValueError("user_custom_sso must be a coroutine function")
|
||||
|
||||
@@ -193,6 +193,7 @@ plugins = "pydantic.mypy"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
markers = [
|
||||
"asyncio: mark test as an asyncio test",
|
||||
"limit_leaks: mark test with memory limit for leak detection (e.g., '40 MB')",
|
||||
|
||||
@@ -650,12 +650,12 @@ class TestBackgroundStreamingModule:
|
||||
|
||||
def test_background_streaming_task_is_async(self):
|
||||
"""Test that background_streaming_task is an async function"""
|
||||
import asyncio
|
||||
import inspect
|
||||
from litellm.proxy.response_polling.background_streaming import (
|
||||
background_streaming_task,
|
||||
)
|
||||
|
||||
assert asyncio.iscoroutinefunction(background_streaming_task)
|
||||
|
||||
assert inspect.iscoroutinefunction(background_streaming_task)
|
||||
|
||||
|
||||
class TestProviderResolutionForPolling:
|
||||
|
||||
+165
-39
@@ -1,9 +1,15 @@
|
||||
# conftest.py
|
||||
# conftest.py - IMPROVED VERSION
|
||||
#
|
||||
# Key changes:
|
||||
# 1. Changed module reload from 'module' scope to 'function' scope for better isolation
|
||||
# 2. Made cache flushing happen per-function instead of per-module
|
||||
# 3. Removed manual event loop creation (let pytest-asyncio handle it)
|
||||
# 4. Added proper cleanup in fixtures
|
||||
# 5. Added worker-specific isolation for parallel execution
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
@@ -14,66 +20,186 @@ import asyncio
|
||||
import litellm
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def isolate_litellm_state():
|
||||
"""
|
||||
Per-function isolation fixture (changed from module scope).
|
||||
|
||||
This ensures better isolation when running tests in parallel:
|
||||
- Each test function gets a clean litellm state
|
||||
- Cache is flushed before each test
|
||||
- No module reloading during parallel execution
|
||||
|
||||
Note: Module reloading at function scope is safer for parallel execution
|
||||
but adds overhead. Consider removing reload entirely if tests can work without it.
|
||||
"""
|
||||
# Get worker ID if running with pytest-xdist
|
||||
worker_id = os.environ.get('PYTEST_XDIST_WORKER', 'master')
|
||||
|
||||
# Store original callback state (all callback lists)
|
||||
original_state = {}
|
||||
if hasattr(litellm, 'callbacks'):
|
||||
original_state['callbacks'] = litellm.callbacks.copy() if litellm.callbacks else []
|
||||
if hasattr(litellm, 'success_callback'):
|
||||
original_state['success_callback'] = litellm.success_callback.copy() if litellm.success_callback else []
|
||||
if hasattr(litellm, 'failure_callback'):
|
||||
original_state['failure_callback'] = litellm.failure_callback.copy() if litellm.failure_callback else []
|
||||
if hasattr(litellm, '_async_success_callback'):
|
||||
original_state['_async_success_callback'] = litellm._async_success_callback.copy() if litellm._async_success_callback else []
|
||||
if hasattr(litellm, '_async_failure_callback'):
|
||||
original_state['_async_failure_callback'] = litellm._async_failure_callback.copy() if litellm._async_failure_callback else []
|
||||
|
||||
# Flush cache before test (critical for respx mocks)
|
||||
if hasattr(litellm, "in_memory_llm_clients_cache"):
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
# Clear success/failure callbacks to prevent chaining
|
||||
if hasattr(litellm, 'success_callback'):
|
||||
litellm.success_callback = []
|
||||
if hasattr(litellm, 'failure_callback'):
|
||||
litellm.failure_callback = []
|
||||
if hasattr(litellm, '_async_success_callback'):
|
||||
litellm._async_success_callback = []
|
||||
if hasattr(litellm, '_async_failure_callback'):
|
||||
litellm._async_failure_callback = []
|
||||
|
||||
yield
|
||||
|
||||
# Cleanup after test
|
||||
if hasattr(litellm, "in_memory_llm_clients_cache"):
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
# Restore all callback lists to original state
|
||||
for attr_name, original_value in original_state.items():
|
||||
if hasattr(litellm, attr_name):
|
||||
setattr(litellm, attr_name, original_value)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def setup_and_teardown():
|
||||
"""
|
||||
This fixture reloads litellm before every function. To speed up testing by removing callbacks being chained.
|
||||
Module-scoped setup/teardown for heavy initialization.
|
||||
|
||||
Use this sparingly - most state should be handled by isolate_litellm_state.
|
||||
Only reload modules here if absolutely necessary.
|
||||
"""
|
||||
curr_dir = os.getcwd() # Get the current working directory
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../..")
|
||||
) # Adds the project directory to the system path
|
||||
)
|
||||
|
||||
import litellm
|
||||
from litellm import Router
|
||||
|
||||
importlib.reload(litellm)
|
||||
# Only reload if NOT running in parallel (module reload + parallel = bad)
|
||||
worker_id = os.environ.get('PYTEST_XDIST_WORKER', None)
|
||||
if worker_id is None:
|
||||
# Single process mode - safe to reload
|
||||
importlib.reload(litellm)
|
||||
|
||||
try:
|
||||
if hasattr(litellm, "proxy") and hasattr(litellm.proxy, "proxy_server"):
|
||||
import litellm.proxy.proxy_server
|
||||
try:
|
||||
if hasattr(litellm, "proxy") and hasattr(litellm.proxy, "proxy_server"):
|
||||
import litellm.proxy.proxy_server
|
||||
importlib.reload(litellm.proxy.proxy_server)
|
||||
except Exception as e:
|
||||
print(f"Error reloading litellm.proxy.proxy_server: {e}")
|
||||
|
||||
importlib.reload(litellm.proxy.proxy_server)
|
||||
except Exception as e:
|
||||
print(f"Error reloading litellm.proxy.proxy_server: {e}")
|
||||
# Flush cache after reload (prevents stale client instances)
|
||||
if hasattr(litellm, "in_memory_llm_clients_cache"):
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
print(f"[conftest] Module setup complete (worker: {worker_id or 'master'})")
|
||||
|
||||
import asyncio
|
||||
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
print(litellm)
|
||||
# from litellm import Router, completion, aembedding, acompletion, embedding
|
||||
yield
|
||||
|
||||
# Teardown code (executes after the yield point)
|
||||
loop.close() # Close the loop created earlier
|
||||
asyncio.set_event_loop(None) # Remove the reference to the loop
|
||||
# Teardown - no need to manually manage event loops with pytest-asyncio auto mode
|
||||
print(f"[conftest] Module teardown complete (worker: {worker_id or 'master'})")
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
# Separate tests in 'test_amazing_proxy_custom_logger.py' and other tests
|
||||
custom_logger_tests = [
|
||||
item for item in items if "custom_logger" in item.parent.name
|
||||
]
|
||||
other_tests = [item for item in items if "custom_logger" not in item.parent.name]
|
||||
"""
|
||||
Customize test collection order.
|
||||
|
||||
# Sort tests based on their names
|
||||
- Separate tests marked with 'no_parallel' from parallelizable tests
|
||||
- Sort custom_logger tests first (they tend to interfere with other tests)
|
||||
"""
|
||||
# Separate no_parallel tests
|
||||
no_parallel_tests = [
|
||||
item for item in items
|
||||
if any(mark.name == "no_parallel" for mark in item.iter_markers())
|
||||
]
|
||||
|
||||
# Separate custom_logger tests
|
||||
custom_logger_tests = [
|
||||
item for item in items
|
||||
if "custom_logger" in item.parent.name
|
||||
and item not in no_parallel_tests
|
||||
]
|
||||
|
||||
# Everything else
|
||||
other_tests = [
|
||||
item for item in items
|
||||
if item not in no_parallel_tests and item not in custom_logger_tests
|
||||
]
|
||||
|
||||
# Sort each group
|
||||
custom_logger_tests.sort(key=lambda x: x.name)
|
||||
other_tests.sort(key=lambda x: x.name)
|
||||
no_parallel_tests.sort(key=lambda x: x.name)
|
||||
|
||||
# Reorder the items list
|
||||
items[:] = custom_logger_tests + other_tests
|
||||
# Reorder: custom_logger first (isolated), then other tests, then no_parallel tests last
|
||||
items[:] = custom_logger_tests + other_tests + no_parallel_tests
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
Configure pytest with custom settings.
|
||||
"""
|
||||
# Add marker for flaky tests (for documentation purposes)
|
||||
config.addinivalue_line(
|
||||
"markers", "flaky: mark test as potentially flaky (should use --reruns)"
|
||||
)
|
||||
|
||||
# Detect if running in CI
|
||||
is_ci = os.environ.get('CI') == 'true' or os.environ.get('LITELLM_CI') == 'true'
|
||||
if is_ci:
|
||||
print("[conftest] Running in CI mode - enabling stricter test isolation")
|
||||
|
||||
|
||||
# Optional: Add a fixture for tests that need even stricter isolation
|
||||
@pytest.fixture
|
||||
def strict_isolation():
|
||||
"""
|
||||
Use this fixture for tests that need extra strict isolation.
|
||||
|
||||
Example:
|
||||
def test_something(strict_isolation):
|
||||
# Test code with guaranteed clean state
|
||||
pass
|
||||
"""
|
||||
# Force flush all caches
|
||||
if hasattr(litellm, "in_memory_llm_clients_cache"):
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
# Reset all global state
|
||||
if hasattr(litellm, "disable_aiohttp_transport"):
|
||||
original_aiohttp = litellm.disable_aiohttp_transport
|
||||
litellm.disable_aiohttp_transport = False
|
||||
else:
|
||||
original_aiohttp = None
|
||||
|
||||
if hasattr(litellm, "set_verbose"):
|
||||
original_verbose = litellm.set_verbose
|
||||
litellm.set_verbose = False
|
||||
else:
|
||||
original_verbose = None
|
||||
|
||||
yield
|
||||
|
||||
# Restore original state
|
||||
if original_aiohttp is not None:
|
||||
litellm.disable_aiohttp_transport = original_aiohttp
|
||||
if original_verbose is not None:
|
||||
litellm.set_verbose = original_verbose
|
||||
|
||||
# Final cache flush
|
||||
if hasattr(litellm, "in_memory_llm_clients_cache"):
|
||||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
Reference in New Issue
Block a user