diff --git a/tests/tools/test_tts_pythonpath_fallback.py b/tests/tools/test_tts_pythonpath_fallback.py new file mode 100644 index 000000000000..23a45fbb5fa0 --- /dev/null +++ b/tests/tools/test_tts_pythonpath_fallback.py @@ -0,0 +1,103 @@ +"""Regression tests for #53259. + +When TTS packages (edge-tts, elevenlabs, mistralai) installed outside the +venv but importable on sys.path (e.g. via PYTHONPATH, Docker layered +filesystems), the lazy-import helpers must fall through to the raw import +instead of re-raising lazy_deps.ensure() failures as ImportError. + +Uses sys.modules fixtures so builtins.__import__ stays intact — patching +__import__ replaces the helper's own ``from tools.lazy_deps import ...`` +and defeats the purpose of the test. +""" + +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from tools.lazy_deps import FeatureUnavailable + + +@pytest.fixture(autouse=True) +def _clean_tts_modules(): + """Remove TTS packages from sys.modules so each test starts fresh.""" + removed = {} + for name in ("edge_tts", "elevenlabs", "elevenlabs.client", + "mistralai", "mistralai.client"): + if name in sys.modules: + removed[name] = sys.modules.pop(name) + yield + for name in ("edge_tts", "elevenlabs", "elevenlabs.client", + "mistralai", "mistralai.client"): + sys.modules.pop(name, None) + sys.modules.update(removed) + + +class TestEdgeTtsPythonpathFallback: + def test_falls_through_on_lazy_deps_failure(self): + """FeatureUnavailable from ensure() must not prevent raw import.""" + mock_edge_tts = MagicMock() + with patch.dict(sys.modules, {"edge_tts": mock_edge_tts}), \ + patch("tools.lazy_deps.ensure", + side_effect=FeatureUnavailable("tts.edge", (), "test")): + from tools.tts_tool import _import_edge_tts + result = _import_edge_tts() + assert result is mock_edge_tts + + def test_raises_when_package_truly_missing(self): + """When the package is truly absent, ImportError must propagate.""" + with patch("tools.lazy_deps.ensure"), \ + patch.dict(sys.modules, {"edge_tts": None}): + from tools.tts_tool import _import_edge_tts + with pytest.raises(ImportError): + _import_edge_tts() + + +class TestElevenLabsPythonpathFallback: + def test_falls_through_on_lazy_deps_failure(self): + """FeatureUnavailable from ensure() must not prevent raw import.""" + mock_cls = MagicMock() + mock_client_pkg = MagicMock() + mock_client_pkg.ElevenLabs = mock_cls + with patch.dict(sys.modules, { + "elevenlabs": mock_client_pkg, + "elevenlabs.client": mock_client_pkg, + }), patch("tools.lazy_deps.ensure", + side_effect=FeatureUnavailable("tts.elevenlabs", (), "test")): + from tools.tts_tool import _import_elevenlabs + result = _import_elevenlabs() + assert result is mock_cls + + def test_raises_when_package_truly_missing(self): + """When the package is truly absent, ImportError must propagate.""" + with patch("tools.lazy_deps.ensure"), \ + patch.dict(sys.modules, {"elevenlabs": None, + "elevenlabs.client": None}): + from tools.tts_tool import _import_elevenlabs + with pytest.raises(ImportError): + _import_elevenlabs() + + +class TestMistralPythonpathFallback: + def test_falls_through_on_lazy_deps_failure(self): + """FeatureUnavailable from ensure() must not prevent raw import.""" + mock_cls = MagicMock() + mock_mistralai = MagicMock() + mock_mistralai.Mistral = mock_cls + with patch.dict(sys.modules, { + "mistralai": mock_mistralai, + "mistralai.client": mock_mistralai, + }), patch("tools.lazy_deps.ensure", + side_effect=FeatureUnavailable("tts.mistral", (), "test")): + from tools.tts_tool import _import_mistral_client + result = _import_mistral_client() + assert result is mock_cls + + def test_raises_when_package_truly_missing(self): + """When the package is truly absent, ImportError must propagate.""" + with patch("tools.lazy_deps.ensure"), \ + patch.dict(sys.modules, {"mistralai": None, + "mistralai.client": None}): + from tools.tts_tool import _import_mistral_client + with pytest.raises(ImportError): + _import_mistral_client() diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index 50a2ee89085f..2e155dd83cae 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -1596,7 +1596,7 @@ def _transcribe_mistral(file_path: str, model_name: str) -> Dict[str, Any]: try: from tools.lazy_deps import ensure as _lazy_ensure _lazy_ensure("stt.mistral", prompt=False) - except ImportError: + except Exception: pass from mistralai.client import Mistral diff --git a/tools/tts_tool.py b/tools/tts_tool.py index a7fddab7b075..f0973c75dd45 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -107,8 +107,8 @@ def _import_edge_tts(): _lazy_ensure("tts.edge", prompt=False) except ImportError: pass - except Exception as e: - raise ImportError(str(e)) + except Exception: + pass import edge_tts return edge_tts @@ -128,8 +128,8 @@ def _import_elevenlabs(): # lazy_deps module itself missing — fall through to the raw import # so older code paths still get a clean ImportError. pass - except Exception as e: # FeatureUnavailable or any unexpected error - raise ImportError(str(e)) + except Exception: + pass from elevenlabs.client import ElevenLabs return ElevenLabs @@ -151,8 +151,8 @@ def _import_mistral_client(): ensure("tts.mistral", prompt=False) except ImportError: pass - except Exception as e: # FeatureUnavailable or any unexpected error - raise ImportError(str(e)) + except Exception: + pass from mistralai.client import Mistral return Mistral