Read Python type annotations from function signatures

Functions with type hints (e.g. bpy_extras.anim_utils) now have their
parameter and return types picked up from inspect.signature annotations,
falling back from docstring :type:/:rtype: directives.

- Add _annotation_to_type_str to clean annotation strings
- Replace _bpy_types.X with bpy.types.X, Union[X, Y] with X | Y
- Qualify bare mathutils types (Vector, Matrix, etc.)
- Normalize NoneType to None

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph HENRY 2026-03-30 17:22:21 +02:00
parent f0affebaa3
commit b4158d8ca2

View File

@ -944,6 +944,26 @@ def _widen_mathutils_params(params: list[ParamData]) -> None:
param["type"] = f"{ptype} | Sequence[float]"
def _annotation_to_type_str(ann: object) -> str:
"""Convert a Python annotation object to a clean type string for stubs."""
s = ann if isinstance(ann, str) else str(ann)
# Clean up internal module references
s = s.replace("_bpy_types.", "bpy.types.")
# typing.Union[X, Y] -> X | Y
s = re.sub(r"\bUnion\[([^\]]+)\]", lambda m: " | ".join(m.group(1).split(", ")), s)
# typing.Optional[X] -> X | None
s = re.sub(r"\bOptional\[([^\]]+)\]", r"\1 | None", s)
# <class 'int'> -> int
s = re.sub(r"<class '([^']+)'>", r"\1", s)
s = s.replace("typing.", "")
# NoneType -> None
s = re.sub(r"\bNoneType\b", "None", s)
# Qualify bare mathutils types (avoid double-qualifying already-qualified ones)
for mt in ("Vector", "Matrix", "Euler", "Quaternion", "Color"):
s = re.sub(rf"(?<!\.)(?<!\w)\b{mt}\b", f"mathutils.{mt}", s)
return s
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 ""
@ -1032,6 +1052,10 @@ def introspect_callable(func: Callable[..., object], name: str) -> FunctionData
type_str = doc_type
actual_name = doc_name
# Fall back to signature annotations (Python functions with type hints)
if type_str is None and param.annotation is not inspect.Parameter.empty:
type_str = _annotation_to_type_str(param.annotation)
if default == "None" and type_str and not re.search(r"\| None\b", type_str):
type_str = type_str + " | None"
@ -1044,6 +1068,10 @@ def introspect_callable(func: Callable[..., object], name: str) -> FunctionData
}
)
# Fall back to signature return annotation
if return_type is None and sig.return_annotation is not inspect.Signature.empty:
return_type = _annotation_to_type_str(sig.return_annotation)
_widen_mathutils_params(params)
return {
"name": name,