Clean up dead code, fix type errors, and add publish workflow

Remove unused _append_ops_call_method, fix _property_data_from_member
call signature, replace sys.version_info guard with hasattr check for
__buffer__ protocol, fix implicit string concatenations, add msgbus
overrides for 4.0-4.4, add conformance tests, and add publish script.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 11:07:52 +02:00
co-authored by Claude Opus 4.6
parent 673b9e54bc
commit f4df542dd1
17 changed files with 954 additions and 165 deletions
+353 -83
View File
@@ -176,7 +176,11 @@ class ParamData(TypedDict):
kind: str
class FunctionData(TypedDict):
class _FunctionDataOptional(TypedDict, total=False):
is_override: bool
class FunctionData(_FunctionDataOptional):
name: str
doc: str
params: list[ParamData]
@@ -494,6 +498,12 @@ _CLEAN_TYPE_ALIAS_RULES: tuple[RegexRule, ...] = (
(re.compile(r"\bfunction\b"), "Callable[..., object]"),
(re.compile(r"\bgenerator\b"), "Generator"),
(re.compile(r"\bsequence\b"), "Sequence"),
# Old typing module names -> modern builtins
(re.compile(r"\bDict\b"), "dict"),
(re.compile(r"\bList\b"), "list"),
(re.compile(r"\bTuple\b"), "tuple"),
(re.compile(r"\bSet\b(?!tings)"), "set"),
(re.compile(r"\bFrozenSet\b"), "frozenset"),
)
_CLEAN_TYPE_GENERIC_RULES: tuple[RegexRule, ...] = (
@@ -501,7 +511,8 @@ _CLEAN_TYPE_GENERIC_RULES: tuple[RegexRule, ...] = (
(re.compile(r"\s*\|\s*\d+\b"), ""),
(re.compile(r"(\w)\[\]"), r"\1"),
(re.compile(r"\bCallable\b(?!\[)"), "Callable[..., object]"),
(re.compile(r"Callable\[\[[^\]]*\.\.\.[^\]]*\]"), "Callable[...,"),
# Callable[[...], X] -> Callable[..., X] (consume trailing comma)
(re.compile(r"Callable\[\[[^\]]*\.\.\.[^\]]*\],?\s*"), "Callable[..., "),
(re.compile(r"\bdict\b(?!\[)"), "dict[str, object]"),
(re.compile(r"\blist\b(?!\[)"), "list[object]"),
(re.compile(r"\btuple\b(?!\[)"), "tuple[object, ...]"),
@@ -662,7 +673,7 @@ def clean_type_str(type_str: str) -> str:
type_str = re.sub(rf"\b{re.escape(undef)}\b(\.\w+)*", "object", type_str)
# Map known unqualified Python stdlib types
type_str = re.sub(r"\bModule\b", "types.ModuleType", type_str)
type_str = re.sub(r"\b[Mm]odule\b", "types.ModuleType", type_str)
# Fix GPU types wrongly referenced as bpy.types.GPU* (Blender docstring bug in 4.x)
type_str = re.sub(r"\bbpy\.types\.(GPU\w+)\b", r"gpu.types.\1", type_str)
@@ -959,6 +970,20 @@ def _annotation_to_type_str(ann: object) -> str:
s = s.replace("typing.", "")
# NoneType -> None
s = re.sub(r"\bNoneType\b", "None", s)
# Old typing aliases -> modern builtins
s = re.sub(r"\bDict\b", "dict", s)
s = re.sub(r"\bList\b", "list", s)
s = re.sub(r"\bTuple\b", "tuple", s)
s = re.sub(r"\bSet\b(?!tings)", "set", s)
s = re.sub(r"\bFrozenSet\b", "frozenset", s)
# Bare module type -> types.ModuleType
s = re.sub(r"\bmodule\b", "types.ModuleType", s)
# Parameterize bare generics (e.g., bare set -> set[object])
s = re.sub(r"\bdict\b(?!\[)", "dict[str, object]", s)
s = re.sub(r"\blist\b(?!\[)", "list[object]", s)
s = re.sub(r"\bset\b(?!\[)", "set[object]", s)
s = re.sub(r"\bfrozenset\b(?!\[)", "frozenset[object]", s)
s = re.sub(r"\btuple\b(?!\[)", "tuple[object, ...]", 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)
@@ -1517,7 +1542,6 @@ def _append_callable_method(
def _property_data_from_member(
cls: type[object],
name: str,
obj: object,
raw: object,
) -> PropertyData:
"""Build PropertyData from a property/getset descriptor class member."""
@@ -1559,7 +1583,7 @@ def _introspect_declared_class_members(
_append_callable_method(methods, obj, name)
continue
if isinstance(raw, property) or type(raw).__name__ == "getset_descriptor":
properties.append(_property_data_from_member(cls, name, obj, raw))
properties.append(_property_data_from_member(cls, name, raw))
continue
properties.append(
{
@@ -1606,28 +1630,23 @@ def _refine_and_synthesize_dunders(
}
)
if sys.version_info >= (3, 12) and "__buffer__" not in method_names:
try:
instance = cls()
memoryview(instance)
methods.append(
{
"name": "__buffer__",
"doc": "",
"params": [
{
"name": "flags",
"type": "int",
"default": None,
"kind": "POSITIONAL_ONLY",
}
],
"return_type": "memoryview",
"is_classmethod": False,
}
)
except Exception:
pass
if "__buffer__" not in method_names and hasattr(cls, "__buffer__"):
methods.append(
{
"name": "__buffer__",
"doc": "",
"params": [
{
"name": "flags",
"type": "int",
"default": None,
"kind": "POSITIONAL_ONLY",
}
],
"return_type": "memoryview",
"is_classmethod": False,
}
)
def _collect_public_module_names(module: ModuleType, module_name: str) -> list[str]:
@@ -1757,6 +1776,12 @@ def introspect_class(cls: type[object], module_name: str) -> StructData:
_insert_constructor_from_doc(cls, class_doc, methods)
_refine_and_synthesize_dunders(cls, methods)
# Mark __eq__/__ne__ as overrides (they override object.__eq__/__ne__)
_OBJECT_OVERRIDES = {"__eq__", "__ne__"}
for method in methods:
if method["name"] in _OBJECT_OVERRIDES:
method["is_override"] = True
return {
"name": cls.__name__,
"doc": class_doc,
@@ -1859,64 +1884,219 @@ def _apply_ops_property_and_method_fixups(op_struct: StructData) -> None:
method["return_type"] = "bpy.types.Struct"
def _append_ops_call_method(op_struct: StructData) -> None:
"""Append the callable operator wrapper method definition."""
op_struct["methods"].append(
{
"name": "__call__",
"doc": "Execute the operator.",
"params": [
{
"name": "args",
"type": "object",
"default": None,
"kind": "VAR_POSITIONAL",
},
{
"name": "kwargs",
"type": "object",
"default": None,
"kind": "VAR_KEYWORD",
},
],
"return_type": "set[str]",
"is_classmethod": False,
}
)
_RAW_RNA_TYPE_MAP: dict[str, str] = {
"BOOLEAN": "bool",
"INT": "int",
"FLOAT": "float",
"STRING": "str",
"ENUM": "str",
}
_VECTOR_SUBTYPES = {
"TRANSLATION",
"DIRECTION",
"VELOCITY",
"ACCELERATION",
"XYZ",
"XYZ_LENGTH",
}
def _build_ops_submodule_struct(operator_type_name: str) -> StructData:
"""Build synthetic bpy.ops submodule structure for attribute access."""
return {
"name": "_OpsSubModule",
"doc": "Operator submodule (e.g. bpy.ops.mesh).",
"base": None,
"properties": [],
"methods": [
{
"name": "__getattr__",
"doc": "",
"params": [
{
"name": "name",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": operator_type_name,
"is_classmethod": False,
def _raw_rna_prop_to_type(prop: object) -> str:
"""Convert a raw RNA Property to a type string (for operator params)."""
ptype: str = getattr(prop, "type", "")
is_array: bool = getattr(prop, "is_array", False)
array_length: int = getattr(prop, "array_length", 0)
subtype: str = getattr(prop, "subtype", "NONE")
if ptype == "ENUM":
items = list(getattr(prop, "enum_items", []))
if items:
values = [str(getattr(i, "identifier", "")) for i in items]
if values:
quoted = ", ".join(f'"{v}"' for v in values)
return f"Literal[{quoted}]"
return "str"
if ptype == "POINTER":
fixed = getattr(prop, "fixed_type", None)
if fixed is not None:
return f"bpy.types.{getattr(fixed, 'identifier', 'object')} | None"
return "object | None"
if ptype == "COLLECTION":
return "object"
if ptype in ("FLOAT", "INT", "BOOLEAN") and is_array:
base = _RAW_RNA_TYPE_MAP.get(ptype, "object")
if ptype == "FLOAT" and subtype in _VECTOR_SUBTYPES:
return "mathutils.Vector | collections.abc.Sequence[float]"
if ptype == "FLOAT" and subtype == "EULER":
return "mathutils.Euler | collections.abc.Sequence[float]"
if (
ptype == "FLOAT"
and subtype in ("COLOR", "COLOR_GAMMA")
and array_length == 3
):
return "mathutils.Color | collections.abc.Sequence[float]"
if ptype == "FLOAT" and subtype == "MATRIX":
return "mathutils.Matrix | collections.abc.Sequence[float]"
return f"collections.abc.Sequence[{base}]"
return _RAW_RNA_TYPE_MAP.get(ptype, "object")
def _rna_prop_default(prop: object) -> str:
"""Extract the default value from an RNA property as a string for stubs."""
ptype: str = getattr(prop, "type", "")
is_array: bool = getattr(prop, "is_array", False)
try:
if ptype == "ENUM":
is_flag = getattr(prop, "is_enum_flag", False)
if is_flag:
# Enum flags are sets — use ellipsis since the type is Literal, not set
return "..."
val = str(getattr(prop, "default", ""))
# If the default is not in the enum items, use ellipsis
items = {
str(getattr(i, "identifier", ""))
for i in getattr(prop, "enum_items", [])
}
],
if val and val in items:
return repr(val)
return "..."
if is_array:
arr = getattr(prop, "default_array", None)
if arr is not None:
return repr(list(arr))
return "..."
val = getattr(prop, "default", None)
if val is None:
return "None"
return repr(val)
except Exception:
return "..."
def _introspect_operator(op: object) -> FunctionData | None:
"""Introspect a single bpy.ops operator and return its typed signature."""
get_rna = getattr(op, "get_rna_type", None)
if get_rna is None or not callable(get_rna):
return None
try:
rna = get_rna()
except Exception:
return None
idname_fn = getattr(op, "idname_py", None)
if idname_fn is None:
return None
try:
py_name: str = idname_fn()
except Exception:
return None
# Extract the function name (e.g. "mesh.primitive_cube_add" -> "primitive_cube_add")
func_name = py_name.split(".")[-1] if "." in py_name else py_name
doc: str = getattr(rna, "description", "") or ""
# All operators accept an optional execution context as the first positional arg
params: list[ParamData] = [
{
"name": "execution_context",
"type": "Literal['INVOKE_DEFAULT', 'INVOKE_REGION_WIN', 'INVOKE_REGION_CHANNELS', 'INVOKE_REGION_PREVIEW', 'INVOKE_AREA', 'INVOKE_SCREEN', 'EXEC_DEFAULT', 'EXEC_REGION_WIN', 'EXEC_REGION_CHANNELS', 'EXEC_REGION_PREVIEW', 'EXEC_AREA', 'EXEC_SCREEN'] | None",
"default": "None",
"kind": "POSITIONAL_ONLY",
},
]
for prop in getattr(rna, "properties", []):
pid: str = getattr(prop, "identifier", "")
if pid == "rna_type":
continue
param_type = _raw_rna_prop_to_type(prop)
default = _rna_prop_default(prop)
params.append(
{
"name": pid,
"type": param_type,
"default": default,
"kind": "KEYWORD_ONLY",
}
)
return {
"name": func_name,
"doc": doc,
"params": params,
"return_type": "set[str]",
"is_classmethod": False,
}
def _build_ops_submodule_variables(submodule_names: list[str]) -> list[VariableData]:
"""Build typed bpy.ops submodule variable declarations."""
return [
{"name": sub_name, "type": "_OpsSubModule", "value": "..."}
for sub_name in submodule_names
]
def _introspect_ops_submodule(
ops_mod: object, sub_name: str, op_base_name: str
) -> tuple[StructData, list[StructData]]:
"""Introspect an ops submodule.
Returns the module-level struct (_OpsModule_*) and a list of per-operator
structs that inherit from the operator base class (e.g. BPyOpFunction).
Each operator is a class with a typed __call__ so that both
``bpy.ops.mesh.subdivide(number_cuts=3)`` and
``bpy.ops.mesh.subdivide.poll()`` type-check correctly.
"""
sub = getattr(ops_mod, sub_name)
op_structs: list[StructData] = []
op_props: list[PropertyData] = []
for op_name in sorted(dir(sub)):
if op_name.startswith("_"):
continue
op = getattr(sub, op_name, None)
if op is None:
continue
func_data = _introspect_operator(op)
if func_data is None:
continue
# Create a per-operator class inheriting from the base operator type
op_class_name = f"_Op_{sub_name}_{op_name}"
call_method: FunctionData = {
"name": "__call__",
"doc": func_data["doc"],
"params": func_data["params"],
"return_type": "set[str]",
"is_classmethod": False,
}
op_struct: StructData = {
"name": op_class_name,
"doc": func_data["doc"],
"base": op_base_name,
"properties": [],
"methods": [call_method],
}
op_structs.append(op_struct)
op_props.append(
{
"name": op_name,
"type": op_class_name,
"is_readonly": False,
"description": func_data["doc"],
}
)
class_name = f"_OpsModule_{sub_name}"
module_struct: StructData = {
"name": class_name,
"doc": f"Operators in bpy.ops.{sub_name}.",
"base": None,
"properties": op_props,
"methods": [],
}
return module_struct, op_structs
def introspect_ops_module() -> ModuleData:
@@ -1950,17 +2130,37 @@ def introspect_ops_module() -> ModuleData:
_apply_ops_method_return_fixups(op_struct, sample_op)
_apply_ops_property_and_method_fixups(op_struct)
_append_ops_call_method(op_struct)
sub_struct = _build_ops_submodule_struct(op_struct["name"])
variables = _build_ops_submodule_variables(submodule_names)
# Introspect each ops submodule with typed operator classes
structs: list[StructData] = [op_struct]
variables: list[VariableData] = []
print(" Introspecting bpy.ops operators...", file=sys.stderr, flush=True)
total_ops = 0
for sub_name in submodule_names:
module_struct, per_op_structs = _introspect_ops_submodule(
ops_mod, sub_name, op_struct["name"]
)
# Add per-operator classes first (they're referenced by the module struct)
structs.extend(per_op_structs)
structs.append(module_struct)
total_ops += len(per_op_structs)
variables.append(
{"name": sub_name, "type": module_struct["name"], "value": "..."}
)
print(
f" Introspected {total_ops} operators "
+ f"in {len(submodule_names)} submodules",
file=sys.stderr,
flush=True,
)
return {
"module": "bpy.ops",
"doc": "Blender operator access.",
"functions": [],
"variables": variables,
"structs": [op_struct, sub_struct],
"structs": structs,
}
@@ -2531,12 +2731,29 @@ def _merge_missing_c_methods(
structs: list[StructData], bpy_types_module: ModuleType
) -> None:
"""Add C-level methods missing from RNA metadata for each struct."""
# Build parent method/property lookup for override detection
struct_by_name = {s["name"]: s for s in structs}
def _get_all_parent_names(struct: StructData) -> set[str]:
names: set[str] = set()
base = struct.get("base")
if base:
# Strip generic params (e.g. "bpy_prop_collection[Object]" -> "bpy_prop_collection")
base_name = base.split("[")[0]
parent = struct_by_name.get(base_name)
if parent:
names |= {m["name"] for m in parent["methods"]}
names |= {p["name"] for p in parent["properties"]}
names |= _get_all_parent_names(parent)
return names
for struct in structs:
cls = getattr(bpy_types_module, struct["name"], None)
if cls is None:
continue
existing = {m["name"] for m in struct["methods"]}
existing |= {p["name"] for p in struct["properties"]}
parent_names = _get_all_parent_names(struct)
for attr_name in sorted(cls.__dict__):
if attr_name.startswith("_") or attr_name in existing:
continue
@@ -2554,12 +2771,16 @@ def _merge_missing_c_methods(
func_data = introspect_callable(bound, attr_name)
if func_data:
func_data["is_classmethod"] = True
if attr_name in parent_names:
func_data["is_override"] = True
struct["methods"].append(func_data)
elif raw_type in ("method_descriptor", "builtin_function_or_method"):
obj = getattr(cls, attr_name)
if callable(obj):
func_data = introspect_callable(obj, attr_name)
if func_data:
if attr_name in parent_names:
func_data["is_override"] = True
struct["methods"].append(func_data)
elif raw_type == "function":
# Python functions with RST docstrings are API methods
@@ -2568,6 +2789,8 @@ def _merge_missing_c_methods(
if ":rtype:" in doc or ":type " in doc:
func_data = introspect_callable(raw, attr_name)
if func_data:
if attr_name in parent_names:
func_data["is_override"] = True
struct["methods"].append(func_data)
@@ -2738,6 +2961,53 @@ def introspect_rna_types() -> ModuleData:
bpy_app = importlib.import_module("bpy.app")
blender_version: tuple[int, ...] = getattr(bpy_app, "version", (0, 0, 0))
temp_override = ctx.__class__.__dict__.get("temp_override")
if temp_override is not None and blender_version < (4, 4, 0):
# On Blender < 4.4, probing temp_override() hangs, but the docstring
# still references ContextTempOverride. Generate a minimal stub.
if "ContextTempOverride" not in known:
stub: StructData = {
"name": "ContextTempOverride",
"doc": "Context manager for context overrides.",
"base": None,
"properties": [],
"methods": [
{
"name": "__enter__",
"doc": "",
"params": [],
"return_type": "Context",
"is_classmethod": False,
},
{
"name": "__exit__",
"doc": "",
"params": [
{
"name": "exc_type",
"type": "object",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "exc_val",
"type": "object",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "exc_tb",
"type": "object",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": "None",
"is_classmethod": False,
},
],
}
structs.append(stub)
known.add("ContextTempOverride")
if (
temp_override is not None
and type(temp_override).__name__ == "method_descriptor"