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__
|
# C functions often use generic names like "object" in __text_signature__
|
||||||
# while docstrings use descriptive names like "string", "cls", etc.
|
# while docstrings use descriptive names like "string", "cls", etc.
|
||||||
doc_param_list = list(param_types.items())
|
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 = []
|
params = []
|
||||||
for i, (pname, param) in enumerate(sig_param_list):
|
for i, (pname, param) in enumerate(sig_param_list):
|
||||||
@ -1223,6 +1225,17 @@ def _fix_dunder_signatures(
|
|||||||
pass
|
pass
|
||||||
continue
|
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
|
# __setitem__: refine value type from __getitem__ return type
|
||||||
if name == "__setitem__":
|
if name == "__setitem__":
|
||||||
try:
|
try:
|
||||||
@ -1381,6 +1394,22 @@ def introspect_class(cls: type[object], module_name: str) -> StructData:
|
|||||||
if dunder_methods:
|
if dunder_methods:
|
||||||
_fix_dunder_signatures(cls, 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 {
|
return {
|
||||||
"name": cls.__name__,
|
"name": cls.__name__,
|
||||||
"doc": class_doc,
|
"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)
|
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__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Blender type stubs generator")
|
parser = argparse.ArgumentParser(description="Blender type stubs generator")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -403,6 +465,11 @@ if __name__ == "__main__":
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Type-check generated stubs instead of generating",
|
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(
|
parser.add_argument(
|
||||||
"versions",
|
"versions",
|
||||||
nargs="*",
|
nargs="*",
|
||||||
@ -412,6 +479,8 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if args.typecheck_stubs:
|
if args.typecheck_stubs:
|
||||||
typecheck_stubs(args.versions or None)
|
typecheck_stubs(args.versions or None)
|
||||||
|
elif args.conformance:
|
||||||
|
conformance_check(args.versions or None)
|
||||||
else:
|
else:
|
||||||
if not args.versions:
|
if not args.versions:
|
||||||
parser.error("versions are required for generation")
|
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]
|
[tool.basedpyright]
|
||||||
typeCheckingMode = "strict"
|
typeCheckingMode = "strict"
|
||||||
extraPaths = ["stubs"]
|
extraPaths = ["stubs"]
|
||||||
exclude = ["downloads/", ".venv/", "dist/"]
|
exclude = ["downloads/", ".venv/", "dist/", "conformance/"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
exclude = ["dist/"]
|
exclude = ["dist/"]
|
||||||
@ -40,6 +40,7 @@ format = { cmd = "black .", help = "Format all Python files" }
|
|||||||
lint = { cmd = "ruff check .", help = "Lint all Python files" }
|
lint = { cmd = "ruff check .", help = "Lint all Python files" }
|
||||||
typecheck = { cmd = "basedpyright", help = "Run type checker on source code" }
|
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)" }
|
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" }
|
test = { cmd = "python -m unittest discover -s tests -v", help = "Run unit tests" }
|
||||||
|
|
||||||
[tool.poe.tasks.generate]
|
[tool.poe.tasks.generate]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user