Skip to content

Commit 119699e

Browse files
committed
review comments
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
1 parent dbef526 commit 119699e

5 files changed

Lines changed: 200 additions & 77 deletions

File tree

mellea/helpers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
wait_for_all_mots,
1717
)
1818
from .event_loop_helper import _run_async_in_thread
19+
from .imports import get_unauthorized_imports
1920
from .openai_compatible_helpers import (
2021
chat_completion_delta_merge,
2122
extract_model_tool_requests,
@@ -36,6 +37,7 @@
3637
"chat_completion_delta_merge",
3738
"extract_model_tool_requests",
3839
"get_current_event_loop",
40+
"get_unauthorized_imports",
3941
"is_vllm_server_with_structured_output",
4042
"message_to_openai_message",
4143
"messages_to_docs",

mellea/helpers/imports.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Utilities for analyzing Python code imports."""
2+
3+
import ast
4+
5+
6+
def get_unauthorized_imports(
7+
code: str, allowed_imports: list[str] | None = None
8+
) -> list[str]:
9+
r"""Extract unauthorized imports from Python code.
10+
11+
Parses Python code and returns a sorted list of top-level modules that are
12+
imported but not in the allowed list. Handles both `import X` and `from X import Y`
13+
statements, extracting the root module name (e.g., "numpy" from "numpy.random").
14+
15+
Args:
16+
code: Python source code to analyze.
17+
allowed_imports: Allowlist of permitted top-level modules. If None, allows all imports.
18+
19+
Returns:
20+
Sorted list of unauthorized module names found in code. Empty list if code
21+
has syntax errors or if allowed_imports is None.
22+
23+
Example:
24+
>>> code = "import numpy\nimport os\nimport forbidden_lib"
25+
>>> get_unauthorized_imports(code, ["os", "sys"])
26+
['numpy', 'forbidden_lib']
27+
"""
28+
if allowed_imports is None:
29+
return []
30+
31+
unauthorized: set[str] = set()
32+
try:
33+
tree = ast.parse(code)
34+
except (SyntaxError, ValueError):
35+
return []
36+
37+
for node in ast.walk(tree):
38+
if isinstance(node, ast.Import):
39+
for alias in node.names:
40+
module = alias.name.split(".")[0]
41+
if module not in allowed_imports:
42+
unauthorized.add(module)
43+
elif isinstance(node, ast.ImportFrom) and node.module:
44+
module = node.module.split(".")[0]
45+
if module not in allowed_imports:
46+
unauthorized.add(module)
47+
48+
return sorted(unauthorized)

mellea/stdlib/requirements/python_tools.py

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from pathlib import Path
5757

5858
from ...core import Context, Requirement, ValidationResult
59+
from ...helpers import get_unauthorized_imports
5960
from .python_reqs import _has_python_code_listing
6061

6162

@@ -81,41 +82,6 @@ def _extract_code(ctx: Context) -> str | None:
8182
return None
8283

8384

84-
def _get_unauthorized_imports(
85-
code: str, allowed_imports: list[str] | None
86-
) -> list[str]:
87-
"""Return list of imports in code that are not in allowed_imports.
88-
89-
Args:
90-
code: Python code to analyze
91-
allowed_imports: Allowlist of permitted top-level modules (None = allow all)
92-
93-
Returns:
94-
List of unauthorized import module names, or empty list if all allowed.
95-
"""
96-
if allowed_imports is None:
97-
return []
98-
99-
unauthorized = []
100-
try:
101-
tree = ast.parse(code)
102-
for node in ast.walk(tree):
103-
if isinstance(node, ast.Import):
104-
for alias in node.names:
105-
module_name = alias.name.split(".")[0]
106-
if module_name not in allowed_imports:
107-
unauthorized.append(module_name)
108-
elif isinstance(node, ast.ImportFrom):
109-
if node.module:
110-
module_name = node.module.split(".")[0]
111-
if module_name not in allowed_imports:
112-
unauthorized.append(module_name)
113-
except (SyntaxError, ValueError):
114-
pass
115-
116-
return list(set(unauthorized))
117-
118-
11985
def _code_parses(code: str) -> tuple[bool, str | None]:
12086
"""Check if code parses as valid Python.
12187
@@ -261,7 +227,7 @@ def validate(ctx: Context) -> ValidationResult:
261227
result=False, reason="Could not extract Python code"
262228
)
263229

264-
unauthorized = _get_unauthorized_imports(code, allowed_imports)
230+
unauthorized = get_unauthorized_imports(code, allowed_imports)
265231
if unauthorized:
266232
allowed_str = ", ".join(sorted(set(allowed_imports)))
267233
return ValidationResult(

mellea/stdlib/tools/interpreter.py

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import Any
2121

2222
from ...core import MelleaLogger
23+
from ...helpers import get_unauthorized_imports
2324

2425
logger = MelleaLogger.get_logger()
2526

@@ -148,7 +149,7 @@ def execute(self, code: str, timeout: int) -> ExecutionResult:
148149
)
149150

150151
if self.allowed_imports:
151-
unauthorized = _get_unauthorized_imports(code, self.allowed_imports)
152+
unauthorized = get_unauthorized_imports(code, self.allowed_imports)
152153
if unauthorized:
153154
return ExecutionResult(
154155
success=False,
@@ -185,7 +186,7 @@ def execute(self, code: str, timeout: int) -> ExecutionResult:
185186
unexpected error occurs.
186187
"""
187188
if self.allowed_imports:
188-
unauthorized = _get_unauthorized_imports(code, self.allowed_imports)
189+
unauthorized = get_unauthorized_imports(code, self.allowed_imports)
189190
if unauthorized:
190191
return ExecutionResult(
191192
success=False,
@@ -259,7 +260,7 @@ def execute(self, code: str, timeout: int) -> ExecutionResult:
259260
flag, or a skipped result on import violation or sandbox error.
260261
"""
261262
if self.allowed_imports:
262-
unauthorized = _get_unauthorized_imports(code, self.allowed_imports)
263+
unauthorized = get_unauthorized_imports(code, self.allowed_imports)
263264
if unauthorized:
264265
return ExecutionResult(
265266
success=False,
@@ -301,37 +302,9 @@ def execute(self, code: str, timeout: int) -> ExecutionResult:
301302
)
302303

303304

304-
def _get_unauthorized_imports(code: str, allowed_imports: list[str]) -> list[str]:
305-
"""Get list of unauthorized imports used in code."""
306-
unauthorized: list[str] = []
307-
try:
308-
tree = ast.parse(code)
309-
except SyntaxError:
310-
return unauthorized
311-
312-
for node in ast.walk(tree):
313-
if isinstance(node, ast.Import):
314-
for alias in node.names:
315-
base_module = alias.name.split(".")[0]
316-
if (
317-
base_module not in allowed_imports
318-
and base_module not in unauthorized
319-
):
320-
unauthorized.append(base_module)
321-
elif isinstance(node, ast.ImportFrom):
322-
if node.module:
323-
base_module = node.module.split(".")[0]
324-
if (
325-
base_module not in allowed_imports
326-
and base_module not in unauthorized
327-
):
328-
unauthorized.append(base_module)
329-
return unauthorized
330-
331-
332305
def _check_allowed_imports(code: str, allowed_imports: list[str]) -> bool:
333306
"""Check if code only uses allowed imports."""
334-
return len(_get_unauthorized_imports(code, allowed_imports)) == 0
307+
return len(get_unauthorized_imports(code, allowed_imports)) == 0
335308

336309

337310
def code_interpreter(code: str) -> ExecutionResult:

0 commit comments

Comments
 (0)