inferlet 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. inferlet-0.3.0/.gitignore +251 -0
  2. inferlet-0.3.0/PKG-INFO +70 -0
  3. inferlet-0.3.0/README.md +44 -0
  4. inferlet-0.3.0/pyproject.toml +56 -0
  5. inferlet-0.3.0/scripts/generate_bindings.py +68 -0
  6. inferlet-0.3.0/scripts/validate_imports.py +175 -0
  7. inferlet-0.3.0/src/inferlet/__init__.py +150 -0
  8. inferlet-0.3.0/src/inferlet/_async.py +37 -0
  9. inferlet-0.3.0/src/inferlet/adapter.py +64 -0
  10. inferlet-0.3.0/src/inferlet/bindings/__init__.py +1 -0
  11. inferlet-0.3.0/src/inferlet/bindings/componentize_py_async_support/__init__.py +529 -0
  12. inferlet-0.3.0/src/inferlet/bindings/componentize_py_async_support/futures.py +93 -0
  13. inferlet-0.3.0/src/inferlet/bindings/componentize_py_async_support/streams.py +189 -0
  14. inferlet-0.3.0/src/inferlet/bindings/componentize_py_runtime.pyi +34 -0
  15. inferlet-0.3.0/src/inferlet/bindings/componentize_py_types.py +19 -0
  16. inferlet-0.3.0/src/inferlet/bindings/poll_loop.py +441 -0
  17. inferlet-0.3.0/src/inferlet/bindings/wit_world/__init__.py +15 -0
  18. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/__init__.py +0 -0
  19. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/adapter.py +49 -0
  20. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/chat.py +81 -0
  21. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/client.py +107 -0
  22. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/context.py +147 -0
  23. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/error.py +56 -0
  24. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/inference.py +287 -0
  25. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/insecure.py +39 -0
  26. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/insecure_seed.py +40 -0
  27. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/messaging.py +65 -0
  28. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/model.py +80 -0
  29. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/pie_core_types.py +56 -0
  30. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/pie_mcp_types.py +34 -0
  31. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/poll.py +72 -0
  32. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/preopens.py +18 -0
  33. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/random.py +43 -0
  34. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/reasoning.py +57 -0
  35. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/runtime.py +33 -0
  36. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/scheduling.py +42 -0
  37. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/session.py +33 -0
  38. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/streams.py +330 -0
  39. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/tool_use.py +70 -0
  40. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/wall_clock.py +61 -0
  41. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/wasi_filesystem_types.py +594 -0
  42. inferlet-0.3.0/src/inferlet/bindings/wit_world/imports/zo.py +26 -0
  43. inferlet-0.3.0/src/inferlet/chat.py +175 -0
  44. inferlet-0.3.0/src/inferlet/context.py +379 -0
  45. inferlet-0.3.0/src/inferlet/forward.py +394 -0
  46. inferlet-0.3.0/src/inferlet/generation.py +551 -0
  47. inferlet-0.3.0/src/inferlet/grammar.py +307 -0
  48. inferlet-0.3.0/src/inferlet/mcp.py +83 -0
  49. inferlet-0.3.0/src/inferlet/messaging.py +78 -0
  50. inferlet-0.3.0/src/inferlet/model.py +65 -0
  51. inferlet-0.3.0/src/inferlet/reasoning.py +111 -0
  52. inferlet-0.3.0/src/inferlet/runtime.py +29 -0
  53. inferlet-0.3.0/src/inferlet/sample.py +176 -0
  54. inferlet-0.3.0/src/inferlet/scheduling.py +44 -0
  55. inferlet-0.3.0/src/inferlet/session.py +54 -0
  56. inferlet-0.3.0/src/inferlet/spec.py +59 -0
  57. inferlet-0.3.0/src/inferlet/tools.py +150 -0
  58. inferlet-0.3.0/src/inferlet/zo.py +59 -0
  59. inferlet-0.3.0/tests/conftest.py +594 -0
  60. inferlet-0.3.0/tests/test_sdk.py +543 -0
@@ -0,0 +1,251 @@
1
+ # Generated by Cargo
2
+ # will have compiled files and executables
3
+ debug/
4
+ target/
5
+
6
+ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7
+ # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8
+ Cargo.lock
9
+
10
+ # These are backup files generated by rustfmt
11
+ **/*.rs.bk
12
+
13
+ # MSVC Windows builds of rustc generate these, which store debugging information
14
+ *.pdb
15
+
16
+ # RustRover
17
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
20
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
21
+ .idea/
22
+
23
+ # VSCode
24
+ # .vscode is not sensitive to security, so we can keep it in the repo. Useful for Pylint configuration.
25
+ *.code-workspace
26
+ .vscode
27
+
28
+ # Other local ide/environment
29
+ .claude
30
+ .codex
31
+ .kilocode
32
+
33
+ # Byte-compiled / optimized / DLL files
34
+ __pycache__/
35
+ *.py[cod]
36
+ *$py.class
37
+
38
+ # C extensions
39
+ *.so
40
+
41
+ # Distribution / packaging
42
+ .Python
43
+ build/
44
+ develop-eggs/
45
+ dist/
46
+ downloads/
47
+ eggs/
48
+ .eggs/
49
+ lib64/
50
+ parts/
51
+ sdist/
52
+ var/
53
+ wheels/
54
+ share/python-wheels/
55
+ *.egg-info/
56
+ .installed.cfg
57
+ *.egg
58
+ MANIFEST
59
+
60
+ # PyInstaller
61
+ # Usually these files are written by a python script from a template
62
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
63
+ *.manifest
64
+ *.spec
65
+
66
+ # Installer logs
67
+ pip-log.txt
68
+ pip-delete-this-directory.txt
69
+
70
+ # Unit test / coverage reports
71
+ htmlcov/
72
+ .tox/
73
+ .nox/
74
+ .coverage
75
+ .coverage.*
76
+ .cache
77
+ nosetests.xml
78
+ coverage.xml
79
+ *.cover
80
+ *.py,cover
81
+ .hypothesis/
82
+ .pytest_cache/
83
+ cover/
84
+
85
+ # Translations
86
+ *.mo
87
+ *.pot
88
+
89
+ # Django stuff:
90
+ local_settings.py
91
+ db.sqlite3
92
+ db.sqlite3-journal
93
+
94
+ # Flask stuff:
95
+ .webassets-cache
96
+
97
+ # Scrapy stuff:
98
+ .scrapy
99
+
100
+ # Sphinx documentation
101
+ docs/_build/
102
+ docs/plans/
103
+
104
+ # PyBuilder
105
+ .pybuilder/
106
+ target/
107
+
108
+ # Jupyter Notebook
109
+ .ipynb_checkpoints
110
+
111
+ # IPython
112
+ profile_default/
113
+ ipython_config.py
114
+
115
+ # pyenv
116
+ # For a library or package, you might want to ignore these files since the code is
117
+ # intended to run in multiple environments; otherwise, check them in:
118
+ # .python-version
119
+
120
+ # pipenv
121
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
122
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
123
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
124
+ # install all needed dependencies.
125
+ #Pipfile.lock
126
+
127
+ # UV
128
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
129
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
130
+ # commonly ignored for libraries.
131
+ #uv.lock
132
+
133
+ # poetry
134
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
135
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
136
+ # commonly ignored for libraries.
137
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
138
+ #poetry.lock
139
+
140
+ # pdm
141
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
142
+ #pdm.lock
143
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
144
+ # in version control.
145
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
146
+ .pdm.toml
147
+ .pdm-python
148
+ .pdm-build/
149
+
150
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
151
+ __pypackages__/
152
+
153
+ # Celery stuff
154
+ celerybeat-schedule
155
+ celerybeat.pid
156
+
157
+ # SageMath parsed files
158
+ *.sage.py
159
+
160
+ # Environments
161
+ .env
162
+ .venv
163
+ env/
164
+ venv/
165
+ ENV/
166
+ env.bak/
167
+ venv.bak/
168
+
169
+ # Spyder project settings
170
+ .spyderproject
171
+ .spyproject
172
+
173
+ # Rope project settings
174
+ .ropeproject
175
+
176
+ # mkdocs documentation
177
+ /site
178
+
179
+ # mypy
180
+ .mypy_cache/
181
+ .dmypy.json
182
+ dmypy.json
183
+
184
+ # Pyre type checker
185
+ .pyre/
186
+
187
+ # pytype static type analyzer
188
+ .pytype/
189
+
190
+ # Cython debug symbols
191
+ cython_debug/
192
+
193
+ # Ruff stuff:
194
+ .ruff_cache/
195
+
196
+ # PyPI configuration file
197
+ .pypirc
198
+
199
+ # Prompt files
200
+ *.prompt
201
+
202
+ .DS_Store
203
+
204
+ program_cache/
205
+
206
+ *.model
207
+
208
+ .run/
209
+
210
+ backend/backend-pytorch
211
+
212
+ bin/
213
+ *.wasm
214
+
215
+ # logs and temporary files
216
+ logs
217
+ *.log
218
+ .plan
219
+ *profiling_result.json
220
+ /docs/
221
+
222
+ # No MCP files
223
+ .memory-bank
224
+
225
+ # raw eval data
226
+ cuda-protocol-tests*
227
+ metal-protocol-tests*
228
+ cuda_artifacts.tar*
229
+ tensor_references
230
+
231
+ # Github Spec-kit
232
+ .spec-kit/
233
+
234
+ # Node.js
235
+ node_modules/
236
+ /package-lock.json
237
+ !website/package-lock.json
238
+ wandb/
239
+
240
+ # Website (Docusaurus)
241
+ website/build/
242
+ website/.docusaurus/
243
+ website/.cache-loader/
244
+ website/src/data/models.json
245
+
246
+ *.pt
247
+ *.zt
248
+
249
+ PIEX.md
250
+ pie/rollout_logs/
251
+ *.out
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: inferlet
3
+ Version: 0.3.0
4
+ Summary: Python SDK for writing Pie inferlets
5
+ Author: Pie Project
6
+ License: Apache-2.0
7
+ Keywords: inference,inferlet,llm,pie,wasm
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: jinja2>=3.0
17
+ Provides-Extra: build
18
+ Requires-Dist: componentize-py>=0.19; extra == 'build'
19
+ Provides-Extra: dev
20
+ Requires-Dist: componentize-py>=0.19; extra == 'dev'
21
+ Requires-Dist: mypy>=1.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.1; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # inferlet Python SDK
28
+
29
+ Python API for writing Pie inferlets.
30
+
31
+ ```python
32
+ from inferlet import Context, Model, Sampler, runtime
33
+
34
+ async def main(input: dict) -> str:
35
+ model = Model.load(runtime.models()[0])
36
+ ctx = Context(model)
37
+
38
+ ctx.system("You are helpful.").user(input["prompt"])
39
+
40
+ return await ctx.generate(
41
+ Sampler.top_p(0.6, 0.95),
42
+ max_tokens=256,
43
+ ).collect_text()
44
+ ```
45
+
46
+ ## Main pieces
47
+
48
+ - `Context`: owns KV-cache state and chat/raw token buffers.
49
+ - `Forward`: runs one explicit forward pass with samplers, probes, masks, and
50
+ manual page control.
51
+ - `Generator`: multi-step generation loop with stop conditions, constraints,
52
+ speculation, adapters, and JSON collection.
53
+ - `chat`, `reasoning`, `tools`: optional decoders and helpers for model-native
54
+ formats.
55
+ - `runtime`, `session`, `messaging`, `mcp`: host services exposed to inferlets.
56
+
57
+ ## Build notes
58
+
59
+ Python inferlets are packaged as Wasm components. Pure-Python dependencies can
60
+ be bundled; native extensions such as `numpy`, `orjson`, `msgspec`, or
61
+ `pydantic_core` cannot be loaded in the Wasm runtime.
62
+
63
+ Build through Bakery:
64
+
65
+ ```bash
66
+ pie build ./my-python-inferlet -o out.wasm
67
+ ```
68
+
69
+ For constrained decoding details, see
70
+ [`sdk/CONSTRAINED_DECODING.md`](../CONSTRAINED_DECODING.md).
@@ -0,0 +1,44 @@
1
+ # inferlet Python SDK
2
+
3
+ Python API for writing Pie inferlets.
4
+
5
+ ```python
6
+ from inferlet import Context, Model, Sampler, runtime
7
+
8
+ async def main(input: dict) -> str:
9
+ model = Model.load(runtime.models()[0])
10
+ ctx = Context(model)
11
+
12
+ ctx.system("You are helpful.").user(input["prompt"])
13
+
14
+ return await ctx.generate(
15
+ Sampler.top_p(0.6, 0.95),
16
+ max_tokens=256,
17
+ ).collect_text()
18
+ ```
19
+
20
+ ## Main pieces
21
+
22
+ - `Context`: owns KV-cache state and chat/raw token buffers.
23
+ - `Forward`: runs one explicit forward pass with samplers, probes, masks, and
24
+ manual page control.
25
+ - `Generator`: multi-step generation loop with stop conditions, constraints,
26
+ speculation, adapters, and JSON collection.
27
+ - `chat`, `reasoning`, `tools`: optional decoders and helpers for model-native
28
+ formats.
29
+ - `runtime`, `session`, `messaging`, `mcp`: host services exposed to inferlets.
30
+
31
+ ## Build notes
32
+
33
+ Python inferlets are packaged as Wasm components. Pure-Python dependencies can
34
+ be bundled; native extensions such as `numpy`, `orjson`, `msgspec`, or
35
+ `pydantic_core` cannot be loaded in the Wasm runtime.
36
+
37
+ Build through Bakery:
38
+
39
+ ```bash
40
+ pie build ./my-python-inferlet -o out.wasm
41
+ ```
42
+
43
+ For constrained decoding details, see
44
+ [`sdk/CONSTRAINED_DECODING.md`](../CONSTRAINED_DECODING.md).
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "inferlet"
3
+ version = "0.3.0"
4
+ description = "Python SDK for writing Pie inferlets"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = {text = "Apache-2.0"}
8
+ authors = [
9
+ {name = "Pie Project"}
10
+ ]
11
+ keywords = ["pie", "inferlet", "wasm", "llm", "inference"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+
22
+ dependencies = [
23
+ "jinja2>=3.0", # For chat template formatting
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ build = [
28
+ "componentize-py>=0.19", # WASM component compiler
29
+ ]
30
+ dev = [
31
+ "componentize-py>=0.19",
32
+ "pytest>=7.0",
33
+ "pytest-asyncio>=0.21",
34
+ "mypy>=1.0",
35
+ "ruff>=0.1",
36
+ ]
37
+
38
+ [build-system]
39
+ requires = ["hatchling"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/inferlet"]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py310"
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I", "N", "W"]
51
+
52
+ [tool.mypy]
53
+ python_version = "3.10"
54
+ warn_return_any = true
55
+ warn_unused_ignores = true
56
+ disallow_untyped_defs = true
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate WIT bindings for inferlet using componentize-py.
4
+
5
+ Usage:
6
+ python scripts/generate_bindings.py
7
+
8
+ This generates Python bindings in src/inferlet/bindings/ that provide
9
+ typed interfaces to the WIT imports (inferlet:core/runtime, etc.).
10
+ """
11
+
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def main() -> int:
18
+ # Get paths relative to script location
19
+ script_dir = Path(__file__).parent
20
+ project_root = script_dir.parent
21
+ sdk_root = project_root.parent
22
+ pie_root = sdk_root.parent
23
+
24
+ wit_path = pie_root / "runtime" / "wit"
25
+ output_path = project_root / "src" / "inferlet" / "bindings"
26
+
27
+ if not wit_path.exists():
28
+ print(f"Error: WIT directory not found at {wit_path}", file=sys.stderr)
29
+ return 1
30
+
31
+ # Ensure output directory exists
32
+ output_path.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Generate bindings using componentize-py
35
+ # The world is "inferlet" as defined in runtime/wit/world.wit
36
+ cmd = [
37
+ "componentize-py",
38
+ "-d",
39
+ str(wit_path),
40
+ "-w",
41
+ "inferlet",
42
+ "bindings",
43
+ str(output_path),
44
+ ]
45
+
46
+ print(f"Generating bindings from {wit_path}...")
47
+ print(f"Output: {output_path}")
48
+ print(f"Command: {' '.join(cmd)}")
49
+
50
+ try:
51
+ result = subprocess.run(cmd, capture_output=True, text=True)
52
+ if result.returncode != 0:
53
+ print(f"Error generating bindings:", file=sys.stderr)
54
+ print(result.stderr, file=sys.stderr)
55
+ return result.returncode
56
+ print(result.stdout)
57
+ print("Bindings generated successfully!")
58
+ return 0
59
+ except FileNotFoundError:
60
+ print(
61
+ "Error: componentize-py not found. Install with: pip install componentize-py",
62
+ file=sys.stderr,
63
+ )
64
+ return 1
65
+
66
+
67
+ if __name__ == "__main__":
68
+ sys.exit(main())
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate that user Python code doesn't import forbidden modules.
4
+
5
+ This script checks for imports that won't work in the WASM environment:
6
+ - Network libraries (requests, urllib3, httpx, aiohttp, etc.)
7
+ - Web frameworks (flask, django, fastapi, etc.)
8
+ - System libraries (os.*, subprocess, multiprocessing, etc.)
9
+ - Database drivers (sqlite3, psycopg2, pymysql, etc.)
10
+
11
+ Usage:
12
+ python scripts/validate_imports.py <path_to_python_file>
13
+ """
14
+
15
+ import ast
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ # Modules that are forbidden in WASM environment
21
+ FORBIDDEN_MODULES = {
22
+ # Network libraries
23
+ "requests",
24
+ "urllib3",
25
+ "httpx",
26
+ "aiohttp",
27
+ "websockets",
28
+ "socket",
29
+ "ssl",
30
+ # Web frameworks
31
+ "flask",
32
+ "django",
33
+ "fastapi",
34
+ "starlette",
35
+ "tornado",
36
+ "bottle",
37
+ # System libraries
38
+ "subprocess",
39
+ "multiprocessing",
40
+ "threading", # Limited support
41
+ "ctypes",
42
+ "cffi",
43
+ # File system (limited)
44
+ "pathlib", # Some operations may work
45
+ "shutil",
46
+ "tempfile",
47
+ # Database drivers
48
+ "sqlite3",
49
+ "psycopg2",
50
+ "pymysql",
51
+ "pymongo",
52
+ "redis",
53
+ # Other
54
+ "tkinter",
55
+ "pygame",
56
+ "numpy", # Native extension
57
+ "pandas", # Native extension
58
+ "scipy", # Native extension
59
+ }
60
+
61
+ # Submodules of os that are forbidden
62
+ FORBIDDEN_OS_SUBMODULES = {
63
+ "os.system",
64
+ "os.popen",
65
+ "os.spawn",
66
+ "os.fork",
67
+ "os.exec",
68
+ }
69
+
70
+
71
+ class ImportVisitor(ast.NodeVisitor):
72
+ """AST visitor to collect imports."""
73
+
74
+ def __init__(self) -> None:
75
+ self.imports: list[tuple[str, int]] = [] # (module, line_number)
76
+
77
+ def visit_Import(self, node: ast.Import) -> None:
78
+ for alias in node.names:
79
+ self.imports.append((alias.name, node.lineno))
80
+ self.generic_visit(node)
81
+
82
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
83
+ if node.module:
84
+ self.imports.append((node.module, node.lineno))
85
+ # Also check for specific imports like "from os import system"
86
+ for alias in node.names:
87
+ full_name = f"{node.module}.{alias.name}"
88
+ self.imports.append((full_name, node.lineno))
89
+ self.generic_visit(node)
90
+
91
+
92
+ def validate_imports(source_path: Path) -> list[str]:
93
+ """
94
+ Validate imports in a Python source file.
95
+
96
+ Returns a list of warning messages for forbidden imports.
97
+ """
98
+ warnings: list[str] = []
99
+
100
+ try:
101
+ source = source_path.read_text()
102
+ tree = ast.parse(source, filename=str(source_path))
103
+ except SyntaxError as e:
104
+ return [f"Syntax error in {source_path}: {e}"]
105
+
106
+ visitor = ImportVisitor()
107
+ visitor.visit(tree)
108
+
109
+ for module, lineno in visitor.imports:
110
+ # Check exact match
111
+ base_module = module.split(".")[0]
112
+ if base_module in FORBIDDEN_MODULES:
113
+ warnings.append(
114
+ f"{source_path}:{lineno}: Forbidden import '{module}' - "
115
+ f"This module won't work in WASM."
116
+ )
117
+ # Check os submodules
118
+ elif module in FORBIDDEN_OS_SUBMODULES:
119
+ warnings.append(
120
+ f"{source_path}:{lineno}: Forbidden import '{module}' - "
121
+ f"This function won't work in WASM."
122
+ )
123
+
124
+ return warnings
125
+
126
+
127
+ def main() -> int:
128
+ if len(sys.argv) < 2:
129
+ print("Usage: python validate_imports.py <path>", file=sys.stderr)
130
+ print(" path can be a Python file or directory", file=sys.stderr)
131
+ return 1
132
+
133
+ target = Path(sys.argv[1])
134
+ if not target.exists():
135
+ print(f"Error: {target} does not exist", file=sys.stderr)
136
+ return 1
137
+
138
+ # Collect Python files
139
+ if target.is_file():
140
+ if not target.suffix == ".py":
141
+ print(f"Error: {target} is not a Python file", file=sys.stderr)
142
+ return 1
143
+ files = [target]
144
+ else:
145
+ files = list(target.rglob("*.py"))
146
+
147
+ if not files:
148
+ print(f"No Python files found in {target}", file=sys.stderr)
149
+ return 1
150
+
151
+ # Validate all files
152
+ all_warnings: list[str] = []
153
+ for f in files:
154
+ # Skip test files and __pycache__
155
+ if "__pycache__" in str(f) or "test" in f.stem.lower():
156
+ continue
157
+ warnings = validate_imports(f)
158
+ all_warnings.extend(warnings)
159
+
160
+ if all_warnings:
161
+ print(f"Found {len(all_warnings)} potential issues:\n")
162
+ for warning in all_warnings:
163
+ print(f" - {warning}")
164
+ print(
165
+ "\nNote: Some of these may work with polyfills or alternatives. "
166
+ "Review each import carefully."
167
+ )
168
+ return 1
169
+ else:
170
+ print(f"Validated {len(files)} file(s) - no forbidden imports found.")
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ sys.exit(main())