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:
parent
6ecd996f35
commit
441631d69a
@ -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("")
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user