diff --git a/src/crewai/utilities/import_utils.py b/src/crewai/utilities/import_utils.py new file mode 100644 index 000000000..47e46f4ba --- /dev/null +++ b/src/crewai/utilities/import_utils.py @@ -0,0 +1,32 @@ +"""Import utilities for optional dependencies.""" + +import importlib +from types import ModuleType + + +class OptionalDependencyError(ImportError): + """Exception raised when an optional dependency is not installed.""" + + pass + + +def require(name: str, *, purpose: str) -> ModuleType: + """Import a module, raising a helpful error if it's not installed. + + Args: + name: The module name to import. + purpose: Description of what requires this dependency. + + Returns: + The imported module. + + Raises: + OptionalDependencyError: If the module is not installed. + """ + try: + return importlib.import_module(name) + except ImportError as exc: + raise OptionalDependencyError( + f"{purpose} requires the optional dependency '{name}'.\n" + f"Install it with: uv add {name}" + ) from exc diff --git a/tests/utilities/test_import_utils.py b/tests/utilities/test_import_utils.py new file mode 100644 index 000000000..535403aa4 --- /dev/null +++ b/tests/utilities/test_import_utils.py @@ -0,0 +1,42 @@ +"""Tests for import utilities.""" + +import pytest +from unittest.mock import patch + +from crewai.utilities.import_utils import require, OptionalDependencyError + + +class TestRequire: + """Test the require function.""" + + def test_require_existing_module(self): + """Test requiring a module that exists.""" + module = require("json", purpose="testing") + assert module.__name__ == "json" + + def test_require_missing_module(self): + """Test requiring a module that doesn't exist.""" + with pytest.raises(OptionalDependencyError) as exc_info: + require("nonexistent_module_xyz", purpose="testing missing module") + + error_msg = str(exc_info.value) + assert ( + "testing missing module requires the optional dependency 'nonexistent_module_xyz'" + in error_msg + ) + assert "uv add nonexistent_module_xyz" in error_msg + + def test_require_with_import_error(self): + """Test that ImportError is properly chained.""" + with patch("importlib.import_module") as mock_import: + mock_import.side_effect = ImportError("Module import failed") + + with pytest.raises(OptionalDependencyError) as exc_info: + require("some_module", purpose="testing error handling") + + assert isinstance(exc_info.value.__cause__, ImportError) + assert str(exc_info.value.__cause__) == "Module import failed" + + def test_optional_dependency_error_is_import_error(self): + """Test that OptionalDependencyError is a subclass of ImportError.""" + assert issubclass(OptionalDependencyError, ImportError)