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:
Joseph HENRY 2026-03-26 17:45:01 +01:00
parent e4739448c2
commit 9fa2f1c200
5 changed files with 126 additions and 2 deletions

View 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)

View File

@ -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
View File

@ -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")

View File

@ -0,0 +1,7 @@
{
"Matrix.Translation": {
"params": {
"vector": "Sequence[float] | Vector"
}
}
}

View File

@ -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]