Skip to content

Commit c996540

Browse files
committed
Add mypyc build script and tooling
- Add build_mypyc.py (--clean, --bench flags) - Compile scalars, lexer, parser (~30% parsing speedup) - Add tox mypyc environment, ruff config, .gitignore *.so - Document mypyc limitations and integration plan
1 parent bbc9743 commit c996540

5 files changed

Lines changed: 1293 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ __pycache__/
2727
*.lock
2828
*.log
2929
*.py[cod]
30+
*.so

build_mypyc.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env python
2+
"""Build script for mypyc compilation of graphql modules.
3+
4+
Usage:
5+
python build_mypyc.py # Build all configured modules
6+
python build_mypyc.py --clean # Remove compiled extensions
7+
python build_mypyc.py --bench # Run benchmark comparison
8+
9+
Benchmarks show ~18x speedup for recursive functions and ~16x for loops.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
import os
16+
import shutil
17+
import sys
18+
from pathlib import Path
19+
20+
# Modules to compile with mypyc.
21+
# Start small - add more as we verify they work correctly.
22+
MYPYC_MODULES = [
23+
# Sentinel module for testing compilation
24+
"graphql_mypyc/_sentinel.py",
25+
# Phase 1: Type system (hot path during execution)
26+
# NOTE: definition.py has conditional class def - not mypyc compatible
27+
# "graphql/type/definition.py",
28+
"graphql/type/scalars.py",
29+
# Phase 2: Language (parsing hot path)
30+
"graphql/language/lexer.py",
31+
"graphql/language/parser.py",
32+
# Phase 3: Execution
33+
# NOTE: execute.py has complex async code that triggers mypyc code-gen bugs
34+
# "graphql/execution/execute.py",
35+
]
36+
37+
38+
def clean() -> None:
39+
"""Remove all compiled .so files from the source tree."""
40+
src = Path("src")
41+
if not src.exists():
42+
src = Path(".")
43+
44+
count = 0
45+
for so_file in src.rglob("*.so"):
46+
print(f"Removing {so_file}")
47+
so_file.unlink()
48+
count += 1
49+
50+
# Also clean build artifacts
51+
for dirname in ["build", ".mypy_cache"]:
52+
dirpath = Path(dirname)
53+
if dirpath.exists():
54+
print(f"Removing {dirpath}/")
55+
shutil.rmtree(dirpath)
56+
57+
print(f"Cleaned {count} .so files")
58+
59+
60+
def build() -> int:
61+
"""Compile modules with mypyc."""
62+
if not MYPYC_MODULES:
63+
print("No modules configured for compilation")
64+
return 0
65+
66+
# Change to src directory for proper module resolution
67+
original_dir = os.getcwd()
68+
src_dir = Path("src")
69+
if src_dir.exists():
70+
os.chdir(src_dir)
71+
72+
try:
73+
# Verify all modules exist
74+
for module in MYPYC_MODULES:
75+
if not Path(module).exists():
76+
print(f"Error: {module} not found")
77+
return 1
78+
79+
print(f"Compiling {len(MYPYC_MODULES)} modules with mypyc...")
80+
print("Modules:", MYPYC_MODULES)
81+
82+
try:
83+
from mypyc.build import mypycify
84+
except ImportError:
85+
print("Error: mypyc not installed. Install with: pip install mypy")
86+
return 1
87+
88+
# Use mypycify to get extension modules
89+
try:
90+
ext_modules = mypycify(
91+
MYPYC_MODULES,
92+
opt_level="3",
93+
debug_level="0",
94+
)
95+
except Exception as e:
96+
print(f"mypycify failed: {e}")
97+
return 1
98+
99+
if not ext_modules:
100+
print("No extension modules generated")
101+
return 1
102+
103+
# Build the extensions in-place
104+
from setuptools import Distribution
105+
from setuptools.command.build_ext import build_ext
106+
107+
dist = Distribution({"ext_modules": ext_modules})
108+
cmd = build_ext(dist)
109+
cmd.inplace = True
110+
cmd.ensure_finalized()
111+
112+
try:
113+
cmd.run()
114+
except Exception as e:
115+
print(f"Build failed: {e}")
116+
return 1
117+
118+
print("\nBuild successful!")
119+
# Show what was built
120+
for module in MYPYC_MODULES:
121+
path = Path(module)
122+
for so_file in path.parent.glob(f"{path.stem}*.so"):
123+
print(f" Built: {so_file}")
124+
125+
return 0
126+
127+
finally:
128+
os.chdir(original_dir)
129+
130+
131+
def benchmark() -> int:
132+
"""Run benchmark comparing interpreted vs compiled."""
133+
import time
134+
import importlib.util
135+
136+
print("=== Compiled (graphql_mypyc._sentinel) ===")
137+
138+
from graphql_mypyc import _sentinel as compiled
139+
140+
print(f"Module: {compiled.__file__}")
141+
print(f"is_compiled: {compiled.is_compiled()}")
142+
143+
# Test fibonacci
144+
n = 30
145+
start = time.perf_counter()
146+
result = compiled.fibonacci(n)
147+
elapsed_compiled_fib = time.perf_counter() - start
148+
print(f"fibonacci({n}) = {result} in {elapsed_compiled_fib:.4f}s")
149+
150+
# Test sum_squares
151+
n = 1_000_000
152+
start = time.perf_counter()
153+
result = compiled.sum_squares(n)
154+
elapsed_compiled_sq = time.perf_counter() - start
155+
print(f"sum_squares({n}) = {result} in {elapsed_compiled_sq:.6f}s")
156+
157+
print()
158+
print("=== Interpreted (graphql._sentinel) ===")
159+
160+
# Import the interpreted version directly by path
161+
spec = importlib.util.spec_from_file_location(
162+
"_sentinel_interp", "src/graphql/_sentinel.py"
163+
)
164+
interp = importlib.util.module_from_spec(spec)
165+
spec.loader.exec_module(interp) # type: ignore[union-attr]
166+
print(f"Module: {interp.__file__}")
167+
print(f"is_compiled: {interp.is_compiled()}")
168+
169+
# Test fibonacci
170+
n = 30
171+
start = time.perf_counter()
172+
result = interp.fibonacci(n)
173+
elapsed_interp_fib = time.perf_counter() - start
174+
print(f"fibonacci({n}) = {result} in {elapsed_interp_fib:.4f}s")
175+
176+
# Test sum_squares
177+
n = 1_000_000
178+
start = time.perf_counter()
179+
result = interp.sum_squares(n)
180+
elapsed_interp_sq = time.perf_counter() - start
181+
print(f"sum_squares({n}) = {result} in {elapsed_interp_sq:.6f}s")
182+
183+
print()
184+
print("=== Speedup ===")
185+
print(f"fibonacci: {elapsed_interp_fib/elapsed_compiled_fib:.1f}x faster")
186+
print(f"sum_squares: {elapsed_interp_sq/elapsed_compiled_sq:.1f}x faster")
187+
188+
return 0
189+
190+
191+
def main() -> int:
192+
"""CLI entry point."""
193+
parser = argparse.ArgumentParser(description="Build graphql modules with mypyc")
194+
parser.add_argument(
195+
"--clean", action="store_true", help="Remove compiled extensions"
196+
)
197+
parser.add_argument(
198+
"--bench", action="store_true", help="Run benchmark comparison"
199+
)
200+
args = parser.parse_args()
201+
202+
if args.clean:
203+
clean()
204+
return 0
205+
206+
if args.bench:
207+
return benchmark()
208+
209+
return build()
210+
211+
212+
if __name__ == "__main__":
213+
sys.exit(main())

0 commit comments

Comments
 (0)