RNA enum properties support

This commit is contained in:
2026-09-21 11:18:10 +02:00
parent f575a84c13
commit f2efa629ba
8 changed files with 134 additions and 8 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
These instructions apply to any AI coding agent working in this repository.
Type stubs generator for the Blender Python API (`bpy`, `mathutils`, `bmesh`, `gpu`, etc.). Introspects Blender's embedded Python in `--background` mode. Supported: Blender 4.0–5.1.
Type stubs generator for the Blender Python API (`bpy`, `mathutils`, `bmesh`, `gpu`, etc.). Introspects Blender's embedded Python in `--background` mode. Supported: Blender 4.0–5.2.
## Commands
+6 -3
View File
@@ -17,6 +17,7 @@ The version matches your target Blender version:
| Blender Version | Install Command | PyPI |
| --------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 5.2 | `pip install "blender-python-stubs>=5.2,<5.3"` | [![PyPI](https://img.shields.io/badge/PyPI-5.2-blue?logo=pypi)](https://pypi.org/project/blender-python-stubs/5.2.1.1/) |
| 5.1 | `pip install "blender-python-stubs>=5.1,<5.2"` | [![PyPI](https://img.shields.io/badge/PyPI-5.1-blue?logo=pypi)](https://pypi.org/project/blender-python-stubs/5.1.0.1/) |
| 5.0 | `pip install "blender-python-stubs>=5.0,<5.1"` | [![PyPI](https://img.shields.io/badge/PyPI-5.0-blue?logo=pypi)](https://pypi.org/project/blender-python-stubs/5.0.1.0/) |
| 4.5 | `pip install "blender-python-stubs>=4.5,<4.6"` | [![PyPI](https://img.shields.io/badge/PyPI-4.5-blue?logo=pypi)](https://pypi.org/project/blender-python-stubs/4.5.8.1/) |
@@ -56,6 +57,8 @@ gpu.state.blend_set('ALPHA')
- **Context members** — `bpy.context.active_object`, `selected_objects`, `edit_object`, and ~100 other screen context attributes are properly typed
- **Constructor signatures** — GPU types, mathutils types (`Vector`, `Matrix`, `Euler`, etc.) have typed `__init__` methods
- **Literal enum types** — string parameters like `gpu.state.blend_set(mode)` use `Literal["NONE", "ALPHA", ...]` instead of plain `str`
- **RNA enum properties** — class properties such as `bpy.types.Panel.bl_space_type` derive their `Literal[...]` values directly from Blender RNA metadata
- **Reusable enum aliases** — annotate addon class attributes with `bpy.types.SpaceType` or `bpy.types.RegionType` instead of repeating every allowed literal
- **Docstrings** — inline documentation on properties, methods, and functions
- **Classmethod detection** — `Matrix.Identity()`, `Vector.Fill()`, etc. correctly typed as `@classmethod`
- **Dunder methods** — `__getitem__`, `__len__`, `__add__`, `__matmul__`, and other operators introspected with correct return types (e.g. `Matrix[0]` returns `Vector`)
@@ -101,20 +104,20 @@ bpystubgen generates stubs from Blender's Sphinx documentation, not from runtime
## Supported Versions
Blender 4.0 through 5.1. Each Blender version gets its own stub package version.
Blender 4.0 through 5.2. Each Blender version gets its own stub package version.
## Generating Stubs
To generate stubs for a specific Blender version, run:
```bash
uv run poe generate 5.1
uv run poe generate 5.2
```
This will automatically download the corresponding Blender executable and run the introspection and stub generation process. You can also type-check the generated stubs:
```bash
uv run poe typecheck-stubs 5.1
uv run poe typecheck-stubs 5.2
```
## Contributing
+17 -1
View File
@@ -603,6 +603,19 @@ def generate_types_stub(
structs: list[StructData], python_version: str = "3.11", doc: str = ""
) -> str:
"""Generate the complete bpy/types.pyi content."""
enum_aliases: dict[str, str] = {}
enum_property_aliases = {
"bl_space_type": "SpaceType",
"bl_region_type": "RegionType",
}
for struct in structs:
for prop in struct["properties"]:
alias_name = enum_property_aliases.get(prop["name"])
if alias_name is None or not prop["type"].startswith("Literal["):
continue
enum_aliases.setdefault(alias_name, prop["type"])
prop["type"] = alias_name
# Collect all type strings to detect needed imports
all_type_strs_parts: list[str] = []
for s in structs:
@@ -616,7 +629,7 @@ def generate_types_stub(
all_type_strs_parts.append(m["return_type"])
all_type_strs = " ".join(all_type_strs_parts)
typing_imports = ["Generic", "TypedDict", "TypeVar"]
typing_imports = ["Generic", "TypedDict", "TypeAlias", "TypeVar"]
if "Literal[" in all_type_strs:
typing_imports.append("Literal")
if re.search(r"\bSelf\b", all_type_strs):
@@ -657,6 +670,9 @@ def generate_types_stub(
'_T = TypeVar("_T")',
]
)
for alias_name, alias_type in enum_aliases.items():
parts.append("")
parts.append(f"{alias_name}: TypeAlias = {alias_type}")
sorted_structs = topological_sort_structs(structs)
inherited_map = collect_inherited_info(sorted_structs)
+36 -1
View File
@@ -1555,7 +1555,7 @@ def _property_data_from_member(
is_readonly = raw.fset is None
else:
is_readonly = not _is_getset_writable(cls, name)
prop_type = rtype or "object"
prop_type = rna_enum_literal_type(cls, name) or rtype or "object"
result: PropertyData = {
"name": name,
"type": prop_type,
@@ -1567,6 +1567,41 @@ def _property_data_from_member(
return result
def rna_enum_literal_type(cls: type[object], name: str) -> str | None:
"""Return a Literal annotation for an RNA enum property, when available.
RNA metadata is the authoritative source for enum identifiers. Blender's
Python docstrings do not consistently include the enum items for class
properties, so relying on the docstring can leave properties such as
``Panel.bl_space_type`` typed as plain ``str``.
"""
rna = getattr(cls, "bl_rna", None)
if rna is None:
return None
properties = getattr(rna, "properties", None)
if properties is None:
return None
try:
prop = properties[name]
except (KeyError, IndexError, TypeError):
return None
if getattr(prop, "type", None) != "ENUM":
return None
values = [
str(getattr(item, "identifier", "")) for item in getattr(prop, "enum_items", [])
]
values = [value for value in values if value]
if not values:
return None
quoted = ", ".join(f'"{value}"' for value in values)
return f"Literal[{quoted}]"
def _introspect_declared_class_members(
cls: type[object],
) -> tuple[list[PropertyData], list[FunctionData]]:
+2 -2
View File
@@ -41,13 +41,13 @@ exclude = ["dist/", "conformance/"]
format = { cmd = "black .", help = "Format all Python files" }
lint = { cmd = "ruff check .", help = "Lint all Python files" }
typecheck = { cmd = "basedpyright", help = "Run type checker on source code" }
typecheck-stubs = { cmd = "python -m main --typecheck-stubs", help = "Type-check generated stubs (e.g., poe typecheck-stubs 4.0 4.1)" }
typecheck-stubs = { cmd = "python -m main --typecheck-stubs", help = "Type-check generated stubs (e.g., poe typecheck-stubs 5.2)" }
conformance = { cmd = "python -m main --conformance", help = "Run conformance tests against generated stubs (e.g., poe conformance 5.0)" }
test = { cmd = "python -m unittest discover -s tests -v", help = "Run unit tests" }
[tool.poe.tasks.generate]
cmd = "python -m main"
help = "Generate stubs for Blender versions (e.g., poe generate 5.0 4.3)"
help = "Generate stubs for Blender versions (e.g., poe generate 5.2 5.1)"
[tool.poe.tasks.publish]
cmd = "python -m publish"
+11
View File
@@ -19,6 +19,17 @@ from blender_downloader import (
class TestGetDownloadUrl(unittest.TestCase):
def test_builds_correct_blender_5_2_url(self) -> None:
with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"):
with patch(
"blender_downloader.get_archive_extension", return_value=".tar.xz"
):
url = get_download_url("5.2.0")
self.assertEqual(
url,
"https://download.blender.org/release/Blender5.2/blender-5.2.0-linux-x64.tar.xz",
)
def test_builds_correct_url(self) -> None:
with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"):
with patch(
+35
View File
@@ -719,6 +719,41 @@ class TestCollectAllMethods(unittest.TestCase):
class TestGenerateTypesStub(unittest.TestCase):
def test_rna_enum_aliases_are_reusable(self) -> None:
structs = [
{
"name": "Panel",
"doc": "",
"base": "bpy_struct",
"properties": [
{
"name": "bl_space_type",
"type": 'Literal["EMPTY", "PROPERTIES"]',
"is_readonly": False,
"description": "",
},
{
"name": "bl_region_type",
"type": 'Literal["WINDOW", "HEADER"]',
"is_readonly": False,
"description": "",
},
],
"methods": [],
}
]
content = generate_types_stub(structs)
self.assertIn(
'SpaceType: TypeAlias = Literal["EMPTY", "PROPERTIES"]', content
)
self.assertIn(
'RegionType: TypeAlias = Literal["WINDOW", "HEADER"]', content
)
self.assertIn("bl_space_type: SpaceType", content)
self.assertIn("bl_region_type: RegionType", content)
def test_includes_header(self) -> None:
structs: list[StructData] = [
{
+26
View File
@@ -21,6 +21,7 @@ from introspect import (
parse_docstring_types,
python_type_name,
rna_property_to_type,
rna_enum_literal_type,
)
@@ -254,6 +255,31 @@ class TestParseDocstringTypes(unittest.TestCase):
self.assertEqual(clean_type_str("SettingsGroup"), "SettingsGroup")
class TestRnaEnumLiteralType(unittest.TestCase):
def test_reads_enum_identifiers_from_rna_metadata(self) -> None:
enum_property = SimpleNamespace(
type="ENUM",
enum_items=[
SimpleNamespace(identifier="EMPTY"),
SimpleNamespace(identifier="VIEW_3D"),
],
)
rna = SimpleNamespace(properties={"bl_space_type": enum_property})
cls = cast(type[object], SimpleNamespace(bl_rna=rna))
self.assertEqual(
rna_enum_literal_type(cls, "bl_space_type"),
'Literal["EMPTY", "VIEW_3D"]',
)
def test_returns_none_for_non_enum_properties(self) -> None:
prop = SimpleNamespace(type="STRING")
rna = SimpleNamespace(properties={"name": prop})
cls = cast(type[object], SimpleNamespace(bl_rna=rna))
self.assertIsNone(rna_enum_literal_type(cls, "name"))
class TestRefineTypesByContext(unittest.TestCase):
def test_bool_vector_default(self) -> None:
param_types = {"default": "Sequence"}