RNA enum properties support
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"` | [](https://pypi.org/project/blender-python-stubs/5.2.1.1/) |
|
||||
| 5.1 | `pip install "blender-python-stubs>=5.1,<5.2"` | [](https://pypi.org/project/blender-python-stubs/5.1.0.1/) |
|
||||
| 5.0 | `pip install "blender-python-stubs>=5.0,<5.1"` | [](https://pypi.org/project/blender-python-stubs/5.0.1.0/) |
|
||||
| 4.5 | `pip install "blender-python-stubs>=4.5,<4.6"` | [](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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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] = [
|
||||
{
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user