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.
|
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
|
## Commands
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ The version matches your target Blender version:
|
|||||||
|
|
||||||
| Blender Version | Install Command | PyPI |
|
| 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.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/) |
|
| 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/) |
|
| 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
|
- **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
|
- **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`
|
- **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
|
- **Docstrings** — inline documentation on properties, methods, and functions
|
||||||
- **Classmethod detection** — `Matrix.Identity()`, `Vector.Fill()`, etc. correctly typed as `@classmethod`
|
- **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`)
|
- **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
|
## 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
|
## Generating Stubs
|
||||||
|
|
||||||
To generate stubs for a specific Blender version, run:
|
To generate stubs for a specific Blender version, run:
|
||||||
|
|
||||||
```bash
|
```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:
|
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
|
```bash
|
||||||
uv run poe typecheck-stubs 5.1
|
uv run poe typecheck-stubs 5.2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|||||||
+17
-1
@@ -603,6 +603,19 @@ def generate_types_stub(
|
|||||||
structs: list[StructData], python_version: str = "3.11", doc: str = ""
|
structs: list[StructData], python_version: str = "3.11", doc: str = ""
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate the complete bpy/types.pyi content."""
|
"""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
|
# Collect all type strings to detect needed imports
|
||||||
all_type_strs_parts: list[str] = []
|
all_type_strs_parts: list[str] = []
|
||||||
for s in structs:
|
for s in structs:
|
||||||
@@ -616,7 +629,7 @@ def generate_types_stub(
|
|||||||
all_type_strs_parts.append(m["return_type"])
|
all_type_strs_parts.append(m["return_type"])
|
||||||
all_type_strs = " ".join(all_type_strs_parts)
|
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:
|
if "Literal[" in all_type_strs:
|
||||||
typing_imports.append("Literal")
|
typing_imports.append("Literal")
|
||||||
if re.search(r"\bSelf\b", all_type_strs):
|
if re.search(r"\bSelf\b", all_type_strs):
|
||||||
@@ -657,6 +670,9 @@ def generate_types_stub(
|
|||||||
'_T = TypeVar("_T")',
|
'_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)
|
sorted_structs = topological_sort_structs(structs)
|
||||||
inherited_map = collect_inherited_info(sorted_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
|
is_readonly = raw.fset is None
|
||||||
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 = rna_enum_literal_type(cls, name) or rtype or "object"
|
||||||
result: PropertyData = {
|
result: PropertyData = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": prop_type,
|
"type": prop_type,
|
||||||
@@ -1567,6 +1567,41 @@ def _property_data_from_member(
|
|||||||
return result
|
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(
|
def _introspect_declared_class_members(
|
||||||
cls: type[object],
|
cls: type[object],
|
||||||
) -> tuple[list[PropertyData], list[FunctionData]]:
|
) -> tuple[list[PropertyData], list[FunctionData]]:
|
||||||
|
|||||||
+2
-2
@@ -41,13 +41,13 @@ exclude = ["dist/", "conformance/"]
|
|||||||
format = { cmd = "black .", help = "Format all Python files" }
|
format = { cmd = "black .", help = "Format all Python files" }
|
||||||
lint = { cmd = "ruff check .", help = "Lint all Python files" }
|
lint = { cmd = "ruff check .", help = "Lint all Python files" }
|
||||||
typecheck = { cmd = "basedpyright", help = "Run type checker on source code" }
|
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)" }
|
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" }
|
test = { cmd = "python -m unittest discover -s tests -v", help = "Run unit tests" }
|
||||||
|
|
||||||
[tool.poe.tasks.generate]
|
[tool.poe.tasks.generate]
|
||||||
cmd = "python -m main"
|
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]
|
[tool.poe.tasks.publish]
|
||||||
cmd = "python -m publish"
|
cmd = "python -m publish"
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ from blender_downloader import (
|
|||||||
|
|
||||||
|
|
||||||
class TestGetDownloadUrl(unittest.TestCase):
|
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:
|
def test_builds_correct_url(self) -> None:
|
||||||
with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"):
|
with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"):
|
||||||
with patch(
|
with patch(
|
||||||
|
|||||||
@@ -719,6 +719,41 @@ class TestCollectAllMethods(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestGenerateTypesStub(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:
|
def test_includes_header(self) -> None:
|
||||||
structs: list[StructData] = [
|
structs: list[StructData] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from introspect import (
|
|||||||
parse_docstring_types,
|
parse_docstring_types,
|
||||||
python_type_name,
|
python_type_name,
|
||||||
rna_property_to_type,
|
rna_property_to_type,
|
||||||
|
rna_enum_literal_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -254,6 +255,31 @@ class TestParseDocstringTypes(unittest.TestCase):
|
|||||||
self.assertEqual(clean_type_str("SettingsGroup"), "SettingsGroup")
|
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):
|
class TestRefineTypesByContext(unittest.TestCase):
|
||||||
def test_bool_vector_default(self) -> None:
|
def test_bool_vector_default(self) -> None:
|
||||||
param_types = {"default": "Sequence"}
|
param_types = {"default": "Sequence"}
|
||||||
|
|||||||
Reference in New Issue
Block a user