diff --git a/litellm/proxy/dev_reload.py b/litellm/proxy/dev_reload.py index cf97bc2ff08..693ba44578a 100644 --- a/litellm/proxy/dev_reload.py +++ b/litellm/proxy/dev_reload.py @@ -1,3 +1,4 @@ +import importlib import logging import sys from collections.abc import Callable, Sequence @@ -48,7 +49,9 @@ def multi_worker_reload_supported() -> bool: def load_app_or_exit(import_path: str) -> Callable[..., object]: + module_name: Final = import_path.partition(":")[0] try: + importlib.import_module(module_name) return ASGI_APP.validate_python(import_path) except Exception: LOGGER.exception("Error loading ASGI app %s", import_path) diff --git a/tests/unit/proxy/test_dev_reload.py b/tests/unit/proxy/test_dev_reload.py index f6baf11bd3b..5ebf7c152d9 100644 --- a/tests/unit/proxy/test_dev_reload.py +++ b/tests/unit/proxy/test_dev_reload.py @@ -1,5 +1,7 @@ import contextlib import importlib +import logging +import logging.handlers import os import re import signal @@ -7,6 +9,7 @@ import socket import subprocess import sys import time +import traceback from collections.abc import Callable from pathlib import Path from typing import Final @@ -84,6 +87,33 @@ def test_a_broken_app_exits_the_worker_as_a_startup_failure( assert exit_info.value.code == uvicorn_config.STARTUP_FAILURE +@pytest.mark.parametrize( + ("module", "source"), + ( + ("reload_import_error_app", "import os\nfrom os import missing_name\n"), + ("reload_value_error_app", "import os\nraise ValueError('broken edit')\n"), + ), +) +def test_a_broken_import_logs_the_line_that_raised( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, module: str, source: str +) -> None: + app_file: Final = tmp_path / f"{module}.py" + app_file.write_text(source) + monkeypatch.syspath_prepend(str(tmp_path)) + uvicorn_logger: Final = logging.getLogger("uvicorn.error") + captured: Final = logging.handlers.BufferingHandler(capacity=POOL_WORKERS) + uvicorn_logger.addHandler(captured) + try: + with pytest.raises(SystemExit): + load_app_or_exit(f"{module}:app") + finally: + uvicorn_logger.removeHandler(captured) + (record,) = captured.buffer + assert record.exc_info is not None + raised_at: Final = frozenset((frame.filename, frame.lineno) for frame in traceback.extract_tb(record.exc_info[2])) + assert (str(app_file), 2) in raised_at, record.exc_text + + def test_a_working_app_loads_as_the_module_defines_it(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: (tmp_path / "reload_working_app.py").write_text("async def app(scope, receive, send):\n return None\n") monkeypatch.syspath_prepend(str(tmp_path))