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:
+64
-9
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user