Use @property/@setter for writable mathutils properties

Writable mathutils properties (Vector, Euler, etc.) were typed as plain
attributes with a union type, causing the getter to also return the union.
This made `obj.location.x` fail type checking since Sequence[float] has
no `.x`. Now these properties use @property for the getter (returning the
concrete mathutils type) and @setter accepting the wider union.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph HENRY 2026-04-03 15:54:40 +02:00
parent ace18808ca
commit e4284ce7d9
3 changed files with 79 additions and 22 deletions

View File

@ -89,6 +89,9 @@ def collect_all_type_strings(module_data: ModuleData) -> list[str]:
all_types.append(base) all_types.append(base)
for prop in struct["properties"]: for prop in struct["properties"]:
all_types.append(prop["type"]) all_types.append(prop["type"])
setter_type = prop.get("setter_type")
if setter_type:
all_types.append(setter_type)
for method in struct["methods"]: for method in struct["methods"]:
for param in method["params"]: for param in method["params"]:
if param["type"]: if param["type"]:
@ -367,7 +370,9 @@ def generate_property_stub(
property_decorator: str = "@property", property_decorator: str = "@property",
) -> str: ) -> str:
"""Generate a stub for a class property.""" """Generate a stub for a class property."""
if prop["is_readonly"]: setter_type = prop.get("setter_type")
if prop["is_readonly"] or setter_type:
result = f"{indent}{property_decorator}\n" result = f"{indent}{property_decorator}\n"
result += f"{indent}def {prop['name']}(self) -> {prop['type']}:\n" result += f"{indent}def {prop['name']}(self) -> {prop['type']}:\n"
if prop["description"]: if prop["description"]:
@ -377,6 +382,11 @@ def generate_property_stub(
result += f'{indent} """{desc}"""\n' result += f'{indent} """{desc}"""\n'
else: else:
result += f"{indent} ...\n" result += f"{indent} ...\n"
if setter_type:
result += f"\n{indent}@{prop['name']}.setter\n"
result += (
f"{indent}def {prop['name']}(self, value: {setter_type}) -> None: ...\n"
)
return result return result
result = f"{indent}{prop['name']}: {prop['type']}\n" result = f"{indent}{prop['name']}: {prop['type']}\n"
@ -413,6 +423,7 @@ def fixup_shadowed_builtins(
fixed: list[PropertyData] = [] fixed: list[PropertyData] = []
for prop in properties: for prop in properties:
new_type = prop["type"] new_type = prop["type"]
new_setter_type = prop.get("setter_type")
for name in shadowed: for name in shadowed:
# Replace bare builtin type references with builtins.X # Replace bare builtin type references with builtins.X
# e.g. "int" -> "builtins.int", "list[int]" -> "list[builtins.int]" # e.g. "int" -> "builtins.int", "list[int]" -> "list[builtins.int]"
@ -421,14 +432,21 @@ def fixup_shadowed_builtins(
f"builtins.{name}", f"builtins.{name}",
new_type, new_type,
) )
fixed.append( if new_setter_type:
{ new_setter_type = re.sub(
"name": prop["name"], rf"\b{name}\b",
"type": new_type, f"builtins.{name}",
"is_readonly": prop["is_readonly"], new_setter_type,
"description": prop["description"], )
} new_prop: PropertyData = {
) "name": prop["name"],
"type": new_type,
"is_readonly": prop["is_readonly"],
"description": prop["description"],
}
if new_setter_type:
new_prop["setter_type"] = new_setter_type
fixed.append(new_prop)
return fixed return fixed

View File

@ -194,7 +194,11 @@ class VariableData(TypedDict):
value: str value: str
class PropertyData(TypedDict): class _PropertyDataOptional(TypedDict, total=False):
setter_type: str
class PropertyData(_PropertyDataOptional):
name: str name: str
type: str type: str
is_readonly: bool is_readonly: bool
@ -1552,14 +1556,15 @@ def _property_data_from_member(
else: else:
is_readonly = not _is_getset_writable(cls, name) is_readonly = not _is_getset_writable(cls, name)
prop_type = rtype or "object" prop_type = rtype or "object"
if not is_readonly and prop_type in _MATHUTILS_ARRAY_TYPES: result: PropertyData = {
prop_type = f"{prop_type} | Sequence[float]"
return {
"name": name, "name": name,
"type": prop_type, "type": prop_type,
"is_readonly": is_readonly, "is_readonly": is_readonly,
"description": doc, "description": doc,
} }
if not is_readonly and prop_type in _MATHUTILS_ARRAY_TYPES:
result["setter_type"] = f"{prop_type} | Sequence[float]"
return result
def _introspect_declared_class_members( def _introspect_declared_class_members(
@ -2661,17 +2666,16 @@ def _rna_struct_to_data(
for prop in getattr(struct_info, "properties", []): for prop in getattr(struct_info, "properties", []):
prop_type = rna_property_to_type(prop) prop_type = rna_property_to_type(prop)
is_readonly = bool(getattr(prop, "is_readonly", False)) is_readonly = bool(getattr(prop, "is_readonly", False))
prop_data: PropertyData = {
"name": str(getattr(prop, "identifier", "")),
"type": prop_type,
"is_readonly": is_readonly,
"description": str(getattr(prop, "description", "") or ""),
}
# Writable mathutils properties also accept Sequence[float] for assignment # Writable mathutils properties also accept Sequence[float] for assignment
if not is_readonly and prop_type in _MATHUTILS_ARRAY_TYPES: if not is_readonly and prop_type in _MATHUTILS_ARRAY_TYPES:
prop_type = f"{prop_type} | Sequence[float]" prop_data["setter_type"] = f"{prop_type} | Sequence[float]"
properties.append( properties.append(prop_data)
{
"name": str(getattr(prop, "identifier", "")),
"type": prop_type,
"is_readonly": is_readonly,
"description": str(getattr(prop, "description", "") or ""),
}
)
methods: list[FunctionData] = [] methods: list[FunctionData] = []
is_collection_wrapper = sid in collection_element_types is_collection_wrapper = sid in collection_element_types

View File

@ -420,6 +420,41 @@ class TestGeneratePropertyStub(unittest.TestCase):
self.assertIn("def type(self) -> str:", result) self.assertIn("def type(self) -> str:", result)
self.assertIn("...", result) self.assertIn("...", result)
def test_setter_type_generates_property_with_setter(self) -> None:
prop: PropertyData = {
"name": "location",
"type": "mathutils.Vector",
"is_readonly": False,
"description": "Object location.",
"setter_type": "mathutils.Vector | Sequence[float]",
}
result = generate_property_stub(prop)
self.assertIn("@property", result)
self.assertIn("def location(self) -> mathutils.Vector:", result)
self.assertIn('"""Object location."""', result)
self.assertIn("@location.setter", result)
self.assertIn(
"def location(self, value: mathutils.Vector | Sequence[float]) -> None: ...",
result,
)
def test_setter_type_no_description(self) -> None:
prop: PropertyData = {
"name": "rotation",
"type": "mathutils.Euler",
"is_readonly": False,
"description": "",
"setter_type": "mathutils.Euler | Sequence[float]",
}
result = generate_property_stub(prop)
self.assertIn("@property", result)
self.assertIn("def rotation(self) -> mathutils.Euler:", result)
self.assertIn("@rotation.setter", result)
self.assertIn(
"def rotation(self, value: mathutils.Euler | Sequence[float]) -> None: ...",
result,
)
class TestGenerateMethodStub(unittest.TestCase): class TestGenerateMethodStub(unittest.TestCase):
def test_simple_method(self) -> None: def test_simple_method(self) -> None: