From 441631d69a481340ae8e2b6449d5af44dab4e78e Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Fri, 27 Mar 2026 12:41:38 +0100 Subject: [PATCH] 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) --- generate_stubs.py | 88 ++++++++++++++++++++++++++++-------- introspect.py | 10 +++- tests/test_generate_stubs.py | 10 ++-- 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/generate_stubs.py b/generate_stubs.py index 1ea470d..b000ae4 100644 --- a/generate_stubs.py +++ b/generate_stubs.py @@ -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"(? 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( diff --git a/tests/test_generate_stubs.py b/tests/test_generate_stubs.py index 27af91e..5c9dfd8 100644 --- a/tests/test_generate_stubs.py +++ b/tests/test_generate_stubs.py @@ -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):