Fix all typecheck errors across all 8 Blender versions (4.0-5.1)

- Fix Sequence name clash in bpy.types by never importing it directly
- Qualify all bare Sequence[ to collections.abc.Sequence[ in bpy.types
- Force writable properties to readonly when overriding parent @property
- Auto-detect classes used as generics and add Generic[_T] base
- Include struct bases in type string collection for import detection
- Strip RST backslash-space before generic brackets in docstrings
- Convert "string in ['X', 'Y']" docstring pattern to Literal types
- Strip single backtick RST markup from type annotations
- Fix Callable[[object, ...], X] -> Callable[..., X]
- Add Callable import to bpy.types generator
- Map ContextTempOverride to object, handle BMVertSkin removal in 5.1
- Fix Literal[...] = False incompatible defaults
- Update test for collect_inherited_info rename

Result: 0 errors on all versions (4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 5.0, 5.1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph HENRY 2026-03-27 12:41:38 +01:00
parent 6ecd996f35
commit 441631d69a
3 changed files with 83 additions and 25 deletions

View File

@ -84,6 +84,8 @@ def collect_all_type_strings(module_data: ModuleData) -> list[str]:
for var in module_data["variables"]:
all_types.append(map_type(var["type"]))
for struct in module_data.get("structs", []):
if struct.get("base"):
all_types.append(struct["base"])
for prop in struct["properties"]:
all_types.append(prop["type"])
for method in struct["methods"]:
@ -126,6 +128,8 @@ def collect_imports(module_data: ModuleData) -> set[str]:
imports.add("from typing import Self")
if _has_type("TypeAlias"):
imports.add("from typing import TypeAlias")
if _has_type("Generic"):
imports.add("from typing import Generic, TypeVar")
# Detect module-qualified references (e.g. bpy.types.Object, mathutils.Vector)
# Detect qualified module references
@ -507,25 +511,45 @@ def topological_sort_structs(structs: list[StructData]) -> list[StructData]:
return result
def collect_all_methods(
structs: list[StructData],
) -> dict[str, set[str]]:
"""Build a map of struct name -> all method names inherited from ancestors."""
by_name: dict[str, StructData] = {s["name"]: s for s in structs}
cache: dict[str, set[str]] = {}
class _InheritedInfo:
methods: set[str]
readonly_props: set[str]
def get_inherited(name: str) -> set[str]:
def __init__(self, methods: set[str], readonly_props: set[str]):
self.methods = methods
self.readonly_props = readonly_props
def collect_inherited_info(
structs: list[StructData],
) -> dict[str, _InheritedInfo]:
"""Build a map of struct name -> inherited method and readonly property names."""
by_name: dict[str, StructData] = {s["name"]: s for s in structs}
cache: dict[str, _InheritedInfo] = {}
def get_inherited(name: str) -> _InheritedInfo:
if name in cache:
return cache[name]
struct = by_name.get(name)
if not struct or not struct["base"]:
cache[name] = set()
cache[name] = _InheritedInfo(set(), set())
return cache[name]
base = struct["base"]
base_own: set[str] = (
{m["name"] for m in by_name[base]["methods"]} if base in by_name else set()
if base in by_name:
base_methods = {m["name"] for m in by_name[base]["methods"]}
base_ro_props = {
p["name"]
for p in by_name[base]["properties"]
if p["is_readonly"]
}
else:
base_methods = set()
base_ro_props = set()
parent = get_inherited(base)
cache[name] = _InheritedInfo(
base_methods | parent.methods,
base_ro_props | parent.readonly_props,
)
cache[name] = base_own | get_inherited(base)
return cache[name]
for struct in structs:
@ -560,9 +584,8 @@ def generate_types_stub(
abc_imports.append("Callable")
if "MutableSequence[" in all_type_strs:
abc_imports.append("MutableSequence")
# Only import bare Sequence if used without collections.abc. prefix
if re.search(r"(?<!\.)\bSequence\[", all_type_strs):
abc_imports.append("Sequence")
# Don't import Sequence directly — bpy.types.Sequence (video sequencer strip)
# shadows it. Always use collections.abc.Sequence via the qualified import.
imports: list[str] = [
f"from collections.abc import {', '.join(abc_imports)}",
@ -591,17 +614,25 @@ def generate_types_stub(
)
sorted_structs = topological_sort_structs(structs)
inherited_map = collect_all_methods(sorted_structs)
inherited_map = collect_inherited_info(sorted_structs)
for struct in sorted_structs:
info = inherited_map.get(struct["name"], _InheritedInfo(set(), set()))
# Force writable properties to readonly when they override a parent's
# readonly @property (avoids reportIncompatibleMethodOverride)
for prop in struct["properties"]:
if not prop["is_readonly"] and prop["name"] in info.readonly_props:
prop["is_readonly"] = True
parts.append("")
parts.append(
generate_struct_stub(struct, inherited_map.get(struct["name"], set()))
)
parts.append(generate_struct_stub(struct, info.methods))
result = "\n".join(parts)
class_names = {s["name"] for s in structs}
result = strip_self_module_prefix(result, "bpy.types", class_names)
# Qualify all bare Sequence[ references to avoid shadowing by bpy.types.Sequence
result = re.sub(
r"(?<!\.)(?<!\w)\bSequence\[", "collections.abc.Sequence[", result
)
return prune_unused_imports(result)
@ -675,6 +706,19 @@ def generate_module_stub(
module_data["structs"], python_version, module_data["doc"]
)
# Detect classes used as generics (e.g. BMElemSeq[BMVert]) and add
# Generic[_T] as base if they don't already have one.
# Must happen before collect_imports so Generic/TypeVar get imported.
all_types = collect_all_type_strings(module_data)
joined_types = " ".join(all_types)
struct_names = {s["name"] for s in module_data.get("structs", [])}
for struct in module_data.get("structs", []):
name = struct["name"]
if name in struct_names and re.search(rf"\b{re.escape(name)}\[", joined_types):
base = struct.get("base") or ""
if "Generic[" not in base:
struct["base"] = f"{base}, Generic[_T]" if base else "Generic[_T]"
parts: list[str] = []
# Module docstring
@ -703,6 +747,14 @@ def generate_module_stub(
parts.append(imp)
parts.append("")
# Add TypeVar if any struct uses Generic[_T]
if any(
"Generic[_T]" in (s.get("base") or "")
for s in module_data.get("structs", [])
):
parts.append('_T = TypeVar("_T")')
parts.append("")
# Variables
if module_data["variables"]:
parts.append("")

View File

@ -320,6 +320,8 @@ def clean_type_str(type_str: str) -> str:
type_str = re.sub(r",?\s*\(readonly\)", "", type_str)
type_str = re.sub(r",?\s*\(never None\)", "", type_str)
type_str = re.sub(r":class:`([^`]+)`", r"\1", type_str)
# Strip RST escaped backslash + space before generic brackets (e.g. "Foo\ [Bar]" -> "Foo[Bar]")
type_str = re.sub("\\\\\\s*\\[", "[", type_str)
# Remove double backtick RST markup
type_str = re.sub(r"``([^`]+)``", r"\1", type_str)
# Remove single backtick markup (` int ` → int)
@ -702,8 +704,12 @@ def clean_type_str(type_str: str) -> str:
_UNDEFINED_TYPE_MAP = {
"ContextTempOverride": "object",
}
if type_str in _UNDEFINED_TYPE_MAP:
return _UNDEFINED_TYPE_MAP[type_str]
for undef_name, replacement in _UNDEFINED_TYPE_MAP.items():
type_str = re.sub(rf"\b{undef_name}\b", replacement, type_str)
# Replace undefined types in generic arguments with object
_UNDEFINED_TYPES = {"BMVertSkin"}
for undef in _UNDEFINED_TYPES:
type_str = re.sub(rf"(?:\w+\.)*{undef}", "object", type_str)
# Final balance check — catch any remaining malformed types
if type_str.count("[") != type_str.count("]") or type_str.count(

View File

@ -5,7 +5,7 @@ import unittest
from generate_stubs import (
ParamOverrides,
apply_overrides,
collect_all_methods,
collect_inherited_info,
generate_function_stub,
generate_method_stub,
generate_property_stub,
@ -674,10 +674,10 @@ class TestCollectAllMethods(unittest.TestCase):
"methods": [method],
},
]
result = collect_all_methods(structs)
self.assertEqual(result["Node"], set())
self.assertIn("is_registered_node_type", result["TextureNode"])
self.assertIn("is_registered_node_type", result["TextureNodeValToRGB"])
result = collect_inherited_info(structs)
self.assertEqual(result["Node"].methods, set())
self.assertIn("is_registered_node_type", result["TextureNode"].methods)
self.assertIn("is_registered_node_type", result["TextureNodeValToRGB"].methods)
class TestGenerateTypesStub(unittest.TestCase):