Skip to content

Commit d76b8ba

Browse files
Copilotsingankit
andauthored
[evaluation] Fix lazy loading for optional dependency imports to avoid noisy messages (#41852)
* Initial plan * Fix lazy loading for optional dependency imports to avoid noisy messages Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Refactor lazy import mechanism to use generalized function - Replace separate _try_import_aiagentconverter and _try_import_skagentconverter functions with a single _create_lazy_import function - The new function accepts class_name, module_path, and dependency_name as parameters - Reduces code duplication and makes the pattern more maintainable - Maintains exact same functionality and error messages - Updates corresponding unit tests to reflect the new implementation Addresses feedback from @singankit to generalize the lazy import pattern. Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Remove [INFO] prefix from error messages in lazy imports Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Simplify lazy import error handling to let ImportError propagate naturally Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Implement lazy loading for red_team module to avoid noisy import messages Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Revert red_team module to traditional import pattern as requested Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Change red_team module to raise ImportError instead of print statement Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Apply black formatting to fix code style issues Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Remove AIAgentConverter test since azure-ai-projects is always in dev requirements Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> * Skip lazy import tests when semantic-kernel is installed Co-authored-by: singankit <30610298+singankit@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: singankit <30610298+singankit@users.noreply.github.com>
1 parent e646d9e commit d76b8ba

File tree

3 files changed

+184
-14
lines changed

3 files changed

+184
-14
lines changed

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/__init__.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,46 @@
5353
# The converter from the AI service to the evaluator schema requires a dependency on
5454
# ai.projects, but we also don't want to force users installing ai.evaluations to pull
5555
# in ai.projects. So we only import it if it's available and the user has ai.projects.
56-
try:
57-
from ._converters._ai_services import AIAgentConverter
56+
# We use lazy loading to avoid printing messages during import unless the classes are actually used.
57+
_lazy_imports = {}
5858

59-
_patch_all.append("AIAgentConverter")
60-
except ImportError:
61-
print(
62-
"[INFO] Could not import AIAgentConverter. Please install the dependency with `pip install azure-ai-projects`."
63-
)
6459

65-
try:
66-
from ._converters._sk_services import SKAgentConverter
60+
def _create_lazy_import(class_name, module_path, dependency_name):
61+
"""Create a lazy import function for optional dependencies.
6762
68-
_patch_all.append("SKAgentConverter")
69-
except ImportError:
70-
print("[INFO] Could not import SKAgentConverter. Please install the dependency with `pip install semantic-kernel`.")
63+
Args:
64+
class_name: Name of the class to import
65+
module_path: Module path to import from
66+
dependency_name: Name of the dependency package for error message
67+
68+
Returns:
69+
A function that performs the lazy import when called
70+
"""
71+
72+
def lazy_import():
73+
try:
74+
module = __import__(module_path, fromlist=[class_name])
75+
cls = getattr(module, class_name)
76+
_patch_all.append(class_name)
77+
return cls
78+
except ImportError:
79+
raise ImportError(
80+
f"Could not import {class_name}. Please install the dependency with `pip install {dependency_name}`."
81+
)
82+
83+
return lazy_import
84+
85+
86+
_lazy_imports["AIAgentConverter"] = _create_lazy_import(
87+
"AIAgentConverter",
88+
"azure.ai.evaluation._converters._ai_services",
89+
"azure-ai-projects",
90+
)
91+
_lazy_imports["SKAgentConverter"] = _create_lazy_import(
92+
"SKAgentConverter",
93+
"azure.ai.evaluation._converters._sk_services",
94+
"semantic-kernel",
95+
)
7196

7297
__all__ = [
7398
"evaluate",
@@ -110,6 +135,16 @@
110135
"AzureOpenAIStringCheckGrader",
111136
"AzureOpenAITextSimilarityGrader",
112137
"AzureOpenAIScoreModelGrader",
138+
# Include lazy imports in __all__ so they appear as available
139+
"AIAgentConverter",
140+
"SKAgentConverter",
113141
]
114142

115143
__all__.extend([p for p in _patch_all if p not in __all__])
144+
145+
146+
def __getattr__(name):
147+
"""Handle lazy imports for optional dependencies."""
148+
if name in _lazy_imports:
149+
return _lazy_imports[name]()
150+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/red_team/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from ._attack_objective_generator import RiskCategory
99
from ._red_team_result import RedTeamResult
1010
except ImportError:
11-
print(
12-
"[INFO] Could not import Pyrit. Please install the dependency with `pip install azure-ai-evaluation[redteam]`."
11+
raise ImportError(
12+
"Could not import Pyrit. Please install the dependency with `pip install azure-ai-evaluation[redteam]`."
1313
)
1414

1515

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Tests for lazy import behavior of optional dependencies."""
2+
3+
import sys
4+
import unittest
5+
from io import StringIO
6+
import importlib
7+
import pytest
8+
9+
try:
10+
import semantic_kernel
11+
12+
has_semantic_kernel = True
13+
except ImportError:
14+
has_semantic_kernel = False
15+
16+
17+
class TestLazyImports(unittest.TestCase):
18+
"""Test lazy import behavior for optional dependencies."""
19+
20+
@pytest.mark.unittest
21+
@pytest.mark.skipif(has_semantic_kernel, reason="semantic-kernel is installed")
22+
def test_no_messages_during_module_import(self):
23+
"""Test that no messages are printed when importing the main module."""
24+
# Capture stderr to check for unwanted messages
25+
captured_stderr = StringIO()
26+
original_stderr = sys.stderr
27+
sys.stderr = captured_stderr
28+
29+
try:
30+
# Test imports that would normally fail with missing dependencies
31+
# Since we can't easily control the dependency availability in the test environment,
32+
# we test the lazy import setup directly
33+
34+
# This should not print any messages during setup
35+
_lazy_imports = {}
36+
_patch_all = []
37+
38+
def _create_lazy_import(class_name, module_path, dependency_name):
39+
"""Create a lazy import function for optional dependencies."""
40+
41+
def lazy_import():
42+
try:
43+
module = __import__(module_path, fromlist=[class_name])
44+
cls = getattr(module, class_name)
45+
_patch_all.append(class_name)
46+
return cls
47+
except ImportError:
48+
raise ImportError(
49+
f"Could not import {class_name}. Please install the dependency with `pip install {dependency_name}`."
50+
)
51+
52+
return lazy_import
53+
54+
# Setting up lazy imports should not print any messages
55+
_lazy_imports["SKAgentConverter"] = _create_lazy_import(
56+
"SKAgentConverter",
57+
"azure.ai.evaluation._converters._sk_services",
58+
"semantic-kernel",
59+
)
60+
61+
# Check that no messages were printed during setup
62+
stderr_output = captured_stderr.getvalue()
63+
self.assertEqual(
64+
stderr_output,
65+
"",
66+
"No messages should be printed during lazy import setup",
67+
)
68+
69+
finally:
70+
sys.stderr = original_stderr
71+
72+
@pytest.mark.unittest
73+
@pytest.mark.skipif(has_semantic_kernel, reason="semantic-kernel is installed")
74+
def test_message_shown_when_accessing_missing_dependency(self):
75+
"""Test that appropriate message is shown when accessing a class with missing dependency."""
76+
# Test the __getattr__ functionality
77+
_lazy_imports = {}
78+
79+
def _create_lazy_import(class_name, module_path, dependency_name):
80+
"""Create a lazy import function for optional dependencies."""
81+
82+
def lazy_import():
83+
try:
84+
# This should fail in most test environments
85+
module = __import__(module_path, fromlist=[class_name])
86+
cls = getattr(module, class_name)
87+
return cls
88+
except ImportError:
89+
raise ImportError(
90+
f"Could not import {class_name}. Please install the dependency with `pip install {dependency_name}`."
91+
)
92+
93+
return lazy_import
94+
95+
_lazy_imports["SKAgentConverter"] = _create_lazy_import(
96+
"SKAgentConverter",
97+
"azure.ai.evaluation._converters._sk_services",
98+
"semantic-kernel",
99+
)
100+
101+
def mock_getattr(name):
102+
"""Mock __getattr__ function like the one in __init__.py"""
103+
if name in _lazy_imports:
104+
return _lazy_imports[name]()
105+
raise AttributeError(f"module has no attribute '{name}'")
106+
107+
# This should raise ImportError directly
108+
with self.assertRaises(ImportError) as cm:
109+
mock_getattr("SKAgentConverter")
110+
111+
# Check that the ImportError message contains the expected information
112+
error_message = str(cm.exception)
113+
self.assertIn("Could not import SKAgentConverter", error_message)
114+
self.assertIn("pip install semantic-kernel", error_message)
115+
116+
@pytest.mark.unittest
117+
def test_getattr_with_non_existent_attribute(self):
118+
"""Test __getattr__ behavior with non-existent attributes."""
119+
_lazy_imports = {}
120+
121+
def mock_getattr(name):
122+
"""Mock __getattr__ function like the one in __init__.py"""
123+
if name in _lazy_imports:
124+
return _lazy_imports[name]()
125+
raise AttributeError(f"module has no attribute '{name}'")
126+
127+
# Test with a non-existent attribute
128+
with self.assertRaises(AttributeError) as cm:
129+
mock_getattr("NonExistentClass")
130+
131+
self.assertIn("has no attribute 'NonExistentClass'", str(cm.exception))
132+
133+
134+
if __name__ == "__main__":
135+
unittest.main()

0 commit comments

Comments
 (0)