Skip to content

Commit 6f125af

Browse files
author
Thomas Baker
committed
verify types test
1 parent 41d81e2 commit 6f125af

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ jobs:
3737
run: poetry run pytest .
3838
env:
3939
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
40+
- name: Verify types
41+
run: poetry run python tests/verify_types.py
4042

4143
publish:
4244
needs: [compile, test]

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ jobs:
4141
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
4242
run: |
4343
poetry run pytest .
44+
45+
- name: Verify types
46+
run: poetry run python tests/verify_types.py

tests/verify_types.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Script to verify all types in src/elevenlabs/types/ can be imported and instantiated.
4+
Allows pydantic ValidationError but catches other errors (like circular imports).
5+
Each type is tested in a separate Python process to avoid import cache issues.
6+
"""
7+
import subprocess
8+
import sys
9+
from pathlib import Path
10+
from concurrent.futures import ThreadPoolExecutor, as_completed
11+
12+
13+
def get_class_name_from_file(file_path: Path) -> str | None:
14+
"""Convert a file name to the expected class name using PascalCase."""
15+
# Remove .py extension
16+
name = file_path.stem
17+
18+
# Skip special files
19+
if name.startswith('__'):
20+
return None
21+
22+
# Convert snake_case to PascalCase
23+
parts = name.split('_')
24+
class_name = ''.join(word.capitalize() for word in parts)
25+
26+
return class_name
27+
28+
29+
def verify_type_import_in_subprocess(module_name: str, class_name: str) -> str | None:
30+
"""
31+
Test importing and instantiating a type in a separate subprocess.
32+
Returns error_message if failed, None if successful.
33+
"""
34+
# Get project root (parent of tests directory)
35+
project_root = Path(__file__).parent.parent
36+
37+
# Python code to test import in isolation
38+
test_code = f"""
39+
import sys
40+
from pydantic import ValidationError as PydanticValidationError
41+
42+
try:
43+
# Try to import the type
44+
from elevenlabs.types.{module_name} import {class_name}
45+
46+
# Try to instantiate with no args (expect ValidationError)
47+
try:
48+
{class_name}()
49+
except PydanticValidationError:
50+
pass # Expected - type requires arguments
51+
except TypeError:
52+
pass # Some types might not be Pydantic models (enums, etc.)
53+
54+
sys.exit(0) # Success
55+
56+
except ImportError as e:
57+
print(f"ImportError: {{e}}", file=sys.stderr)
58+
sys.exit(1)
59+
except Exception as e:
60+
print(f"{{type(e).__name__}}: {{e}}", file=sys.stderr)
61+
sys.exit(1)
62+
"""
63+
64+
try:
65+
result = subprocess.run(
66+
["poetry", "run", "python", "-c", test_code],
67+
capture_output=True,
68+
text=True,
69+
timeout=10,
70+
cwd=str(project_root)
71+
)
72+
73+
if result.returncode == 0:
74+
return None
75+
else:
76+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
77+
return error_msg
78+
79+
except subprocess.TimeoutExpired:
80+
return "Timeout: Import took longer than 10 seconds"
81+
except Exception as e:
82+
return f"Subprocess error: {str(e)}"
83+
84+
85+
def main():
86+
# Get project root (parent of tests directory)
87+
project_root = Path(__file__).parent.parent
88+
types_dir = project_root / "src/elevenlabs/types"
89+
90+
if not types_dir.exists():
91+
print(f"Error: {types_dir} does not exist", file=sys.stderr)
92+
sys.exit(1)
93+
94+
# Get all Python files
95+
py_files = sorted([f for f in types_dir.glob("*.py") if not f.name.startswith("__")])
96+
97+
print(f"Testing {len(py_files)} type files in parallel...\n")
98+
99+
failures = []
100+
successes = []
101+
102+
# Test types in parallel for speed
103+
with ThreadPoolExecutor(max_workers=10) as executor:
104+
future_to_file = {}
105+
106+
for py_file in py_files:
107+
module_name = py_file.stem
108+
class_name = get_class_name_from_file(py_file)
109+
110+
if class_name is None:
111+
continue # Skip special files
112+
113+
future = executor.submit(verify_type_import_in_subprocess, module_name, class_name)
114+
future_to_file[future] = (module_name, class_name)
115+
116+
for future in as_completed(future_to_file):
117+
module_name, class_name = future_to_file[future]
118+
try:
119+
error = future.result()
120+
121+
if error is None:
122+
successes.append((module_name, class_name))
123+
print(f"✓ {module_name}.{class_name}")
124+
else:
125+
failures.append((module_name, class_name, error))
126+
print(f"✗ {module_name}.{class_name}")
127+
128+
except Exception as e:
129+
failures.append((module_name, class_name, f"Test execution failed: {str(e)}"))
130+
print(f"✗ {module_name}.{class_name}")
131+
132+
# Print summary
133+
print(f"\n{'='*80}")
134+
print(f"Summary: {len(successes)} passed, {len(failures)} failed")
135+
print(f"{'='*80}")
136+
137+
if failures:
138+
print("\nFailed imports:")
139+
for module_name, class_name, error in failures:
140+
print(f"\n{module_name}.{class_name}:")
141+
# Indent error messages
142+
for line in error.split('\n'):
143+
print(f" {line}")
144+
sys.exit(1)
145+
else:
146+
print("\nAll types can be imported successfully!")
147+
sys.exit(0)
148+
149+
150+
if __name__ == "__main__":
151+
main()

0 commit comments

Comments
 (0)