Add conformance tests and fix introspection issues
- Add conformance test framework with poe conformance task - Add test files for mathutils, bpy, and gpu API patterns - Fix self param leaking into signatures from C method_descriptors - Synthesize __iter__ for classes with __getitem__ but no explicit __iter__ - Add __iter__ runtime probing to discover Iterator[element_type] - Add :raises to directive lookahead to fix rtype parsing - Add 5.1 override for Matrix.Translation to accept Vector Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4739448c2
commit
9fa2f1c200
18
conformance/test_mathutils.py
Normal file
18
conformance/test_mathutils.py
Normal file
@ -0,0 +1,18 @@
|
||||
import mathutils
|
||||
from math import radians
|
||||
|
||||
vec = mathutils.Vector((1.0, 2.0, 3.0))
|
||||
|
||||
mat_rot = mathutils.Matrix.Rotation(radians(90.0), 4, "X")
|
||||
mat_trans = mathutils.Matrix.Translation(vec)
|
||||
|
||||
mat = mat_trans @ mat_rot
|
||||
mat.invert()
|
||||
|
||||
mat3 = mat.to_3x3()
|
||||
quat1 = mat.to_quaternion()
|
||||
quat2 = mat3.to_quaternion()
|
||||
|
||||
quat_diff = quat1.rotation_difference(quat2)
|
||||
|
||||
print(quat_diff.angle)
|
||||
@ -959,7 +959,9 @@ def introspect_callable(func: Callable[..., object], name: str) -> FunctionData
|
||||
# C functions often use generic names like "object" in __text_signature__
|
||||
# while docstrings use descriptive names like "string", "cls", etc.
|
||||
doc_param_list = list(param_types.items())
|
||||
sig_param_list = list(sig.parameters.items())
|
||||
sig_param_list = [
|
||||
(n, p) for n, p in sig.parameters.items() if n != "self"
|
||||
]
|
||||
|
||||
params = []
|
||||
for i, (pname, param) in enumerate(sig_param_list):
|
||||
@ -1223,6 +1225,17 @@ def _fix_dunder_signatures(
|
||||
pass
|
||||
continue
|
||||
|
||||
# __iter__: return Iterator[element_type] based on __getitem__
|
||||
if name == "__iter__":
|
||||
try:
|
||||
result = instance[0]
|
||||
etype = _type_name(result)
|
||||
method["return_type"] = f"Iterator[{etype}]"
|
||||
method["params"] = []
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
# __setitem__: refine value type from __getitem__ return type
|
||||
if name == "__setitem__":
|
||||
try:
|
||||
@ -1381,6 +1394,22 @@ def introspect_class(cls: type[object], module_name: str) -> StructData:
|
||||
if dunder_methods:
|
||||
_fix_dunder_signatures(cls, dunder_methods)
|
||||
|
||||
# Synthesize __iter__ if the class supports iteration via __getitem__
|
||||
# but doesn't define __iter__ explicitly (old-style C iteration protocol).
|
||||
method_names = {m["name"] for m in methods}
|
||||
if "__getitem__" in method_names and "__iter__" not in method_names:
|
||||
getitem = next(m for m in methods if m["name"] == "__getitem__")
|
||||
elem_type = getitem["return_type"] or "object"
|
||||
methods.append(
|
||||
{
|
||||
"name": "__iter__",
|
||||
"doc": "",
|
||||
"params": [],
|
||||
"return_type": f"Iterator[{elem_type}]",
|
||||
"is_classmethod": False,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"name": cls.__name__,
|
||||
"doc": class_doc,
|
||||
|
||||
69
main.py
69
main.py
@ -396,6 +396,68 @@ def typecheck_stubs(versions: list[str] | None = None) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def conformance_check(versions: list[str] | None = None) -> None:
|
||||
"""Type-check conformance test files against generated stubs."""
|
||||
dist_dir = SCRIPT_DIR / "dist"
|
||||
conformance_dir = SCRIPT_DIR / "conformance"
|
||||
|
||||
if not conformance_dir.exists():
|
||||
print("No conformance/ directory found.")
|
||||
sys.exit(1)
|
||||
|
||||
if versions:
|
||||
missing = [v for v in versions if not (dist_dir / v).is_dir()]
|
||||
if missing:
|
||||
print(f"Missing stubs for: {', '.join(missing)}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
versions = sorted(
|
||||
d.name
|
||||
for d in dist_dir.iterdir()
|
||||
if d.is_dir() and not d.name.startswith(".")
|
||||
)
|
||||
|
||||
if not versions:
|
||||
print("No generated stubs found in dist/.")
|
||||
sys.exit(1)
|
||||
|
||||
failed = False
|
||||
for version in versions:
|
||||
version_dir = dist_dir / version
|
||||
print(f"=== Conformance check against Blender {version} stubs ===")
|
||||
|
||||
python_version_file = version_dir / ".python-version"
|
||||
python_version = (
|
||||
python_version_file.read_text().strip()
|
||||
if python_version_file.exists()
|
||||
else "3.11"
|
||||
)
|
||||
|
||||
config = version_dir / "pyrightconfig.conformance.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"include": [str(conformance_dir)],
|
||||
"extraPaths": [str(version_dir)],
|
||||
"typeCheckingMode": "strict",
|
||||
"pythonVersion": python_version,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
["basedpyright", "--project", str(config)],
|
||||
)
|
||||
|
||||
config.unlink()
|
||||
|
||||
if result.returncode != 0:
|
||||
failed = True
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Blender type stubs generator")
|
||||
parser.add_argument(
|
||||
@ -403,6 +465,11 @@ if __name__ == "__main__":
|
||||
action="store_true",
|
||||
help="Type-check generated stubs instead of generating",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--conformance",
|
||||
action="store_true",
|
||||
help="Run conformance tests against generated stubs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"versions",
|
||||
nargs="*",
|
||||
@ -412,6 +479,8 @@ if __name__ == "__main__":
|
||||
|
||||
if args.typecheck_stubs:
|
||||
typecheck_stubs(args.versions or None)
|
||||
elif args.conformance:
|
||||
conformance_check(args.versions or None)
|
||||
else:
|
||||
if not args.versions:
|
||||
parser.error("versions are required for generation")
|
||||
|
||||
7
overrides/5.1/mathutils.json
Normal file
7
overrides/5.1/mathutils.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Matrix.Translation": {
|
||||
"params": {
|
||||
"vector": "Sequence[float] | Vector"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ target-version = ["py311"]
|
||||
[tool.basedpyright]
|
||||
typeCheckingMode = "strict"
|
||||
extraPaths = ["stubs"]
|
||||
exclude = ["downloads/", ".venv/", "dist/"]
|
||||
exclude = ["downloads/", ".venv/", "dist/", "conformance/"]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["dist/"]
|
||||
@ -40,6 +40,7 @@ format = { cmd = "black .", help = "Format all Python files" }
|
||||
lint = { cmd = "ruff check .", help = "Lint all Python files" }
|
||||
typecheck = { cmd = "basedpyright", help = "Run type checker on source code" }
|
||||
typecheck-stubs = { cmd = "python -m main --typecheck-stubs", help = "Type-check generated stubs (e.g., poe typecheck-stubs 4.0 4.1)" }
|
||||
conformance = { cmd = "python -m main --conformance", help = "Run conformance tests against generated stubs (e.g., poe conformance 5.0)" }
|
||||
test = { cmd = "python -m unittest discover -s tests -v", help = "Run unit tests" }
|
||||
|
||||
[tool.poe.tasks.generate]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user