Skip to content

Commit 8dbf554

Browse files
committed
add supported builtin modules with integration test
1 parent d9df5bf commit 8dbf554

File tree

5 files changed

+296
-20
lines changed

5 files changed

+296
-20
lines changed

CONTRIBUTING.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@ Thank you for your interest in contributing to designer-plugin! This document pr
3131

3232
### Running Tests
3333

34-
Run the full test suite:
34+
Run unit tests (default):
3535
```bash
3636
uv run pytest
3737
```
3838

39-
Run tests with verbose output:
39+
Run integration tests (requires a running d3 instance):
4040
```bash
41-
uv run pytest -v
41+
uv run pytest -m integration
42+
```
43+
44+
Run all tests:
45+
```bash
46+
uv run pytest -m ""
4247
```
4348

4449
Run specific test file:

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ python_classes = ["Test*"]
109109
python_functions = ["test_*"]
110110
addopts = [
111111
"-v",
112+
"-m", "not integration",
112113
"--strict-markers",
113114
"--strict-config",
114115
]
116+
markers = [
117+
"integration: tests that require a running d3 instance",
118+
]
119+

src/designer_plugin/d3sdk/ast_utils.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
from pydantic import BaseModel, Field
1515

16+
from designer_plugin.d3sdk.builtin_modules import SUPPORTED_MODULES
17+
1618

1719
###############################################################################
1820
# Package info models
@@ -22,7 +24,9 @@ class ImportAlias(BaseModel):
2224
Mirrors the structure of ast.alias for Pydantic compatibility.
2325
"""
2426

25-
name: str = Field(description="The imported name (e.g., 'Path' in 'from pathlib import Path')")
27+
name: str = Field(
28+
description="The imported name (e.g., 'Path' in 'from pathlib import Path')"
29+
)
2630
asname: str | None = Field(
2731
default=None,
2832
description="The alias (e.g., 'np' in 'import numpy as np')",
@@ -54,15 +58,11 @@ def to_import_statement(self) -> str:
5458
if self.methods:
5559
node = ast.ImportFrom(
5660
module=self.package,
57-
names=[
58-
ast.alias(name=m.name, asname=m.asname) for m in self.methods
59-
],
61+
names=[ast.alias(name=m.name, asname=m.asname) for m in self.methods],
6062
level=0,
6163
)
6264
else:
63-
node = ast.Import(
64-
names=[ast.alias(name=self.package, asname=self.alias)]
65-
)
65+
node = ast.Import(names=[ast.alias(name=self.package, asname=self.alias)])
6666
return ast.unparse(node)
6767

6868

@@ -453,21 +453,17 @@ def _collect_used_names(func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> se
453453
return names
454454

455455

456-
# Shared exclusion constants
457-
_EXCLUDED_PACKAGES: set[str] = {"d3blobgen", "typing"}
458-
459-
460456
def _is_type_checking_block(node: ast.If) -> bool:
461457
"""Check if an if statement is ``if TYPE_CHECKING:``."""
462458
return isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING"
463459

464460

465-
def _is_excluded_package(module_name: str) -> bool:
466-
"""Check if a module name matches any excluded package."""
467-
return any(excluded in module_name for excluded in _EXCLUDED_PACKAGES)
461+
def _is_builtin_package(module_name: str) -> bool:
462+
"""Check if a module name matches python builtin package."""
463+
return module_name in SUPPORTED_MODULES
468464

469465

470-
@functools.lru_cache(maxsize=None)
466+
@functools.cache
471467
def _get_module_ast(module: types.ModuleType) -> ast.Module | None:
472468
"""Return the parsed AST for *module*, cached by module identity."""
473469
try:
@@ -526,8 +522,9 @@ def find_imports_for_function(func: Callable[..., Any]) -> list[PackageInfo]:
526522

527523
if isinstance(node, ast.Import):
528524
for alias in node.names:
529-
if _is_excluded_package(alias.name):
525+
if not _is_builtin_package(alias.name):
530526
continue
527+
531528
# The name used in code is the alias if present, otherwise the module name
532529
effective_name = alias.asname if alias.asname else alias.name
533530
if effective_name in used_names:
@@ -541,7 +538,7 @@ def find_imports_for_function(func: Callable[..., Any]) -> list[PackageInfo]:
541538
elif isinstance(node, ast.ImportFrom):
542539
if not node.module:
543540
continue
544-
if _is_excluded_package(node.module):
541+
if not _is_builtin_package(node.module):
545542
continue
546543

547544
# Filter to only methods actually used by the function
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
SUPPORTED_MODULES: frozenset[str] = frozenset(
2+
[
3+
"Bastion",
4+
"ConfigParser",
5+
"Cookie",
6+
"HTMLParser",
7+
"SocketServer",
8+
"StringIO",
9+
"UserDict",
10+
"UserList",
11+
"_winreg",
12+
"abc",
13+
"aifc",
14+
"anydbm",
15+
"array",
16+
"ast",
17+
"atexit",
18+
"audioop",
19+
"base64",
20+
"binascii",
21+
"bisect",
22+
"bz2",
23+
"cPickle",
24+
"cStringIO",
25+
"chunk",
26+
"cmath",
27+
"cmd",
28+
"codecs",
29+
"codeop",
30+
"collections",
31+
"copy",
32+
"copy_reg",
33+
"csv",
34+
"ctypes",
35+
"datetime",
36+
"difflib",
37+
"dircache",
38+
"dis",
39+
"dumbdbm",
40+
"dummy_thread",
41+
"errno",
42+
"filecmp",
43+
"fnmatch",
44+
"functools",
45+
"future_builtins",
46+
"gc",
47+
"getopt",
48+
"hashlib",
49+
"heapq",
50+
"hmac",
51+
"htmlentitydefs",
52+
"imghdr",
53+
"imp",
54+
"importlib",
55+
"inspect",
56+
"io",
57+
"itertools",
58+
"json",
59+
"keyword",
60+
"linecache",
61+
"locale",
62+
"logging",
63+
"mailcap",
64+
"marshal",
65+
"math",
66+
"mmap",
67+
"msvcrt",
68+
"mutex",
69+
"netrc",
70+
"new",
71+
"nntplib",
72+
"numbers",
73+
"operator",
74+
"os",
75+
"parser",
76+
"pkgutil",
77+
"plistlib",
78+
"poplib",
79+
"pprint",
80+
"quopri",
81+
"random",
82+
"re",
83+
"repr",
84+
"rfc822",
85+
"rlcompleter",
86+
"sched",
87+
"select",
88+
"sets",
89+
"sgmllib",
90+
"sha",
91+
"shelve",
92+
"shlex",
93+
"shutil",
94+
"signal",
95+
"site",
96+
"sndhdr",
97+
"socket",
98+
"sqlite3",
99+
"stat",
100+
"statvfs",
101+
"string",
102+
"stringprep",
103+
"struct",
104+
"subprocess",
105+
"sunau",
106+
"symbol",
107+
"symtable",
108+
"sysconfig",
109+
"tempfile",
110+
"textwrap",
111+
"thread",
112+
"time",
113+
"token",
114+
"tokenize",
115+
"traceback",
116+
"types",
117+
"unicodedata",
118+
"unittest",
119+
"urlparse",
120+
"uuid",
121+
"warnings",
122+
"weakref",
123+
"webbrowser",
124+
"winsound",
125+
"wsgiref",
126+
"xdrlib",
127+
"xml",
128+
"zipfile",
129+
"zipimport",
130+
"zlib",
131+
]
132+
)
133+
134+
NOT_SUPPORTED_MODULES: frozenset[str] = frozenset(
135+
[
136+
"BaseHTTPServer",
137+
"CGIHTTPServer",
138+
"DocXMLRPCServer",
139+
"Queue",
140+
"ScrolledText",
141+
"SimpleHTTPServer",
142+
"SimpleXMLRPCServer",
143+
"Tix",
144+
"Tkinter",
145+
"UserString",
146+
"argparse",
147+
"asynchat",
148+
"asyncore",
149+
"bdb",
150+
"binhex",
151+
"bsddb",
152+
"calendar",
153+
"cgi",
154+
"cgitb",
155+
"code",
156+
"colorsys",
157+
"compileall",
158+
"compiler",
159+
"contextlib",
160+
"cookielib",
161+
"dbhash",
162+
"dbm",
163+
"decimal",
164+
"distutils",
165+
"doctest",
166+
"dummy_threading",
167+
"email",
168+
"ensurepip",
169+
"fileinput",
170+
"formatter",
171+
"fractions",
172+
"ftplib",
173+
"getpass",
174+
"gettext",
175+
"glob",
176+
"gzip",
177+
"htmllib",
178+
"httplib",
179+
"imaplib",
180+
"mailbox",
181+
"mhlib",
182+
"mimetools",
183+
"mimetypes",
184+
"mimify",
185+
"modulefinder",
186+
"msilib",
187+
"multiprocessing",
188+
"optparse",
189+
"pdb",
190+
"pickle",
191+
"pickletools",
192+
"platform",
193+
"popen2",
194+
"profile",
195+
"py_compile",
196+
"pyclbr",
197+
"pydoc",
198+
"robotparser",
199+
"runpy",
200+
"smtpd",
201+
"smtplib",
202+
"ssl",
203+
"sys",
204+
"tabnanny",
205+
"tarfile",
206+
"telnetlib",
207+
"test",
208+
"threading",
209+
"timeit",
210+
"trace",
211+
"ttk",
212+
"turtle",
213+
"urllib",
214+
"urllib2",
215+
"uu",
216+
"wave",
217+
"whichdb",
218+
"xmlrpclib",
219+
]
220+
)

tests/test_supported_modules.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from designer_plugin.d3sdk import d3function, D3AsyncSession
2+
from designer_plugin.d3sdk.builtin_modules import SUPPORTED_MODULES, NOT_SUPPORTED_MODULES
3+
4+
import pytest
5+
import asyncio
6+
7+
@d3function('test_supported_modules')
8+
def check_import(module_str) -> bool:
9+
try:
10+
module = __import__(module_str)
11+
return True
12+
except ImportError as e:
13+
return False
14+
15+
class TestSupportedModules:
16+
"""
17+
Test if supported and not supported modules are handled properly on Deisgner side.
18+
This is integration test so Designer must be running to pass the test.
19+
"""
20+
21+
@pytest.mark.integration
22+
def test_supported_modules(self):
23+
"""Test if all supported modules are able to be imported on Designer side."""
24+
async def run():
25+
failed = []
26+
async with D3AsyncSession("localhost", 80) as session:
27+
for module_str in SUPPORTED_MODULES:
28+
import_success: bool = await session.rpc(
29+
check_import.payload(module_str)
30+
)
31+
if not import_success:
32+
failed.append(module_str)
33+
assert not failed, f"Failed to import: {failed}"
34+
asyncio.run(run())
35+
36+
@pytest.mark.integration
37+
def test_not_supported_modules(self):
38+
"""Test if all not supported modules are not importable on Designer side."""
39+
async def run():
40+
failed = []
41+
async with D3AsyncSession("localhost", 80) as session:
42+
for module_str in NOT_SUPPORTED_MODULES:
43+
import_success: bool = await session.rpc(
44+
check_import.payload(module_str)
45+
)
46+
if import_success:
47+
failed.append(module_str)
48+
assert not failed, f"Unexpectedly imported: {failed}"
49+
asyncio.run(run())

0 commit comments

Comments
 (0)