Widen mathutils param types, fix getset_descriptor writability, add conformance tests

- Widen bare mathutils type params (Vector, Euler, etc.) to also accept
  Sequence[float], matching Blender's mathutils_array_parse C behavior
- Fix getset_descriptor readonly detection by probing __set__ on the
  descriptor instead of checking fset (which doesn't exist on C descriptors)
- Accept int | slice keys in __getitem__/__setitem__/__delitem__
- Accept Sequence[element_type] values in __setitem__ for slice assignment
- Add mathutils overrides for Matrix.Translation and Matrix.Scale
- Extend apply_overrides to support ClassName.method_name keys
- Add conformance test files from Blender docs examples
- Disable reportUnusedExpression in conformance checks

Remaining known conformance issues:
- draw_handler_add missing from SpaceView3D
- Vector not nominally Sequence[float] (buffer protocol, swizzle setters)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 18:06:11 +01:00
co-authored by Claude Opus 4.6
parent 9fa2f1c200
commit c2876bc184
10 changed files with 327 additions and 12 deletions
+64 -9
View File
@@ -226,7 +226,9 @@ def parse_docstring_types(docstring: str) -> tuple[dict[str, str], str | None]:
# Match :type param: ... up to the next RST directive (:arg, :type, :rtype, :return)
# but NOT :class: or :func: which appear inside type annotations
directive_lookahead = r"(?=\n\s*:(?:arg|param|type|rtype|return|returns|raises)[\s:]|$)"
directive_lookahead = (
r"(?=\n\s*:(?:arg|param|type|rtype|return|returns|raises)[\s:]|$)"
)
for match in re.finditer(
rf":type\s+(\w+):\s*(.+?){directive_lookahead}", docstring, re.DOTALL
):
@@ -890,6 +892,29 @@ def parse_rst_function_sig(
return result
# Bare mathutils types that Blender's C code accepts interchangeably with
# Sequence[float] via mathutils_array_parse. When a param is typed as one
# of these in a docstring, widen it to also accept Sequence[float].
_MATHUTILS_ARRAY_TYPES = {
"mathutils.Vector",
"mathutils.Euler",
"mathutils.Quaternion",
"mathutils.Color",
"Vector",
"Euler",
"Quaternion",
"Color",
}
def _widen_mathutils_params(params: list[ParamData]) -> None:
"""Widen bare mathutils type params to also accept Sequence[float]."""
for param in params:
ptype = param.get("type")
if ptype and ptype in _MATHUTILS_ARRAY_TYPES:
param["type"] = f"{ptype} | Sequence[float]"
def introspect_callable(func: Callable[..., object], name: str) -> FunctionData | None:
"""Introspect a callable (function or builtin) and return its metadata."""
docstring = inspect.getdoc(func) or ""
@@ -947,6 +972,7 @@ def introspect_callable(func: Callable[..., object], name: str) -> FunctionData
"kind": "POSITIONAL_OR_KEYWORD",
}
)
_widen_mathutils_params(params)
return {
"name": name,
"doc": clean_docstring(docstring),
@@ -959,9 +985,7 @@ 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 = [
(n, p) for n, p in sig.parameters.items() if n != "self"
]
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):
@@ -991,6 +1015,7 @@ def introspect_callable(func: Callable[..., object], name: str) -> FunctionData
}
)
_widen_mathutils_params(params)
return {
"name": name,
"doc": clean_docstring(docstring),
@@ -1099,6 +1124,7 @@ def _parse_class_constructor(class_doc: str, cls: type) -> FunctionData | None:
if not params:
return None
_widen_mathutils_params(params)
return {
"name": "__init__",
"doc": "",
@@ -1192,7 +1218,7 @@ def _fix_dunder_signatures(
method["params"] = [
{
"name": "key",
"type": "int",
"type": "int | slice",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
@@ -1216,7 +1242,7 @@ def _fix_dunder_signatures(
method["params"] = [
{
"name": "key",
"type": "int",
"type": "int | slice",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
@@ -1244,13 +1270,13 @@ def _fix_dunder_signatures(
method["params"] = [
{
"name": "key",
"type": "int",
"type": "int | slice",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "value",
"type": vtype,
"type": f"{vtype} | Sequence[{vtype}]",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
@@ -1304,6 +1330,31 @@ def _fix_dunder_signatures(
continue
def _is_getset_writable(cls: type[object], attr_name: str) -> bool:
"""Test if a C getset_descriptor property is writable.
C getset_descriptors that have a setter implement __set__.
Those without a setter raise AttributeError on __set__ calls.
We test this by checking if __set__ raises on a dummy call.
"""
descriptor = cls.__dict__.get(attr_name)
if descriptor is None:
return False
# If the descriptor doesn't implement __set__ at all, it's read-only
if not hasattr(descriptor, "__set__"):
return False
# Try calling __set__ with None — a writable descriptor will attempt
# the set (and fail on None), while a read-only one raises AttributeError
try:
descriptor.__set__(None, None)
except AttributeError:
return False
except Exception:
# Any other error means __set__ exists (it just didn't like our args)
return True
return True
def introspect_class(cls: type[object], module_name: str) -> StructData:
"""Introspect a class (C extension or Python) and return StructData."""
# Determine base class (skip object and internal bases)
@@ -1361,7 +1412,11 @@ def introspect_class(cls: type[object], module_name: str) -> StructData:
elif isinstance(raw, property) or type(raw).__name__ == "getset_descriptor":
doc = inspect.getdoc(raw) or ""
_, rtype = parse_docstring_types(doc)
is_readonly = not hasattr(raw, "fset") or raw.fset is None
if isinstance(raw, property):
is_readonly = raw.fset is None
else:
# C getset_descriptors don't expose fset; probe at runtime
is_readonly = not _is_getset_writable(cls, name)
properties.append(
{
"name": name,