Add rna_type/bl_rna, GeometrySet, bpy.ops typing, publish task, and README updates
- Add rna_type and bl_rna as readonly properties on bpy_struct (fixes fake-bpy-module#419) - Discover non-RNA C classes in bpy.types like GeometrySet (fixes fake-bpy-module#436) - Introspect bpy.ops operator wrapper with poll(), idname(), get_rna_type(), bl_options - Handle builtin name shadowing (e.g. bpy.ops.object) with builtins. qualification - Add poe publish task for building and publishing to PyPI - Update project URLs to Gitea, improve generated README, add disclaimer - Fix f-string lint warning and type annotation for introspect_class Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
852a5de700
commit
6ea41f3b4d
24
README.md
24
README.md
@ -95,6 +95,20 @@ bpystubgen generates stubs from Blender's Sphinx documentation, not from runtime
|
|||||||
|
|
||||||
Blender 4.0 through 5.1. Each Blender version gets its own stub package version.
|
Blender 4.0 through 5.1. 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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Pull requests are welcome. The project uses:
|
Pull requests are welcome. The project uses:
|
||||||
@ -106,16 +120,14 @@ Pull requests are welcome. The project uses:
|
|||||||
- `ruff` for linting
|
- `ruff` for linting
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate stubs for a specific version
|
|
||||||
uv run poe generate 5.1
|
|
||||||
|
|
||||||
# Type-check generated stubs
|
|
||||||
uv run poe typecheck-stubs 5.1
|
|
||||||
|
|
||||||
# Run all checks (format, lint, typecheck, tests)
|
# Run all checks (format, lint, typecheck, tests)
|
||||||
uv run poe check
|
uv run poe check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This project was vibe coded using [Claude](https://claude.ai) by Anthropic.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE)
|
[MIT](LICENSE)
|
||||||
|
|||||||
@ -717,6 +717,21 @@ def generate_module_stub(
|
|||||||
class_names = {s["name"] for s in module_data.get("structs", [])}
|
class_names = {s["name"] for s in module_data.get("structs", [])}
|
||||||
result = strip_self_module_prefix(result, module_name, class_names)
|
result = strip_self_module_prefix(result, module_name, class_names)
|
||||||
|
|
||||||
|
# If any variable/class name shadows a Python builtin used as a type
|
||||||
|
# annotation (e.g. bpy.ops has a variable named "object"), qualify it.
|
||||||
|
_BUILTINS_USED_AS_TYPES = {"object", "type", "int", "str", "float", "bool", "set"}
|
||||||
|
var_names = {v["name"] for v in module_data.get("variables", [])}
|
||||||
|
shadowed = var_names & _BUILTINS_USED_AS_TYPES
|
||||||
|
if shadowed:
|
||||||
|
for name in shadowed:
|
||||||
|
result = re.sub(
|
||||||
|
rf"(?<=: ){name}\b",
|
||||||
|
f"builtins.{name}",
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
if "import builtins" not in result:
|
||||||
|
result = "import builtins\n" + result
|
||||||
|
|
||||||
# Qualify type names that clash with class names in the same module
|
# Qualify type names that clash with class names in the same module
|
||||||
for clash in clashing_imports:
|
for clash in clashing_imports:
|
||||||
match = re.search(r"from ([\w.]+) import (\w+)", clash)
|
match = re.search(r"from ([\w.]+) import (\w+)", clash)
|
||||||
|
|||||||
191
introspect.py
191
introspect.py
@ -483,11 +483,12 @@ def clean_type_str(type_str: str) -> str:
|
|||||||
r"Sequence[tuple[\1, \2]]",
|
r"Sequence[tuple[\1, \2]]",
|
||||||
type_str,
|
type_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
# list[X, Y, ...] with >1 type args -> treat as list[X] (drop extras)
|
# list[X, Y, ...] with >1 type args -> treat as list[X] (drop extras)
|
||||||
def _fix_multi_arg_list(m: re.Match[str]) -> str:
|
def _fix_multi_arg_list(m: re.Match[str]) -> str:
|
||||||
inner = m.group(1)
|
inner = m.group(1)
|
||||||
# If it contains nested generics like list[float], keep first one
|
# If it contains nested generics like list[float], keep first one
|
||||||
parts = []
|
parts: list[str] = []
|
||||||
depth = 0
|
depth = 0
|
||||||
current: list[str] = []
|
current: list[str] = []
|
||||||
for ch in inner:
|
for ch in inner:
|
||||||
@ -1093,7 +1094,7 @@ def _parse_class_constructor(class_doc: str, cls: type) -> FunctionData | None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def introspect_class(cls: type, module_name: str) -> StructData:
|
def introspect_class(cls: type[object], module_name: str) -> StructData:
|
||||||
"""Introspect a class (C extension or Python) and return StructData."""
|
"""Introspect a class (C extension or Python) and return StructData."""
|
||||||
# Determine base class (skip object and internal bases)
|
# Determine base class (skip object and internal bases)
|
||||||
bases = [
|
bases = [
|
||||||
@ -1216,11 +1217,134 @@ def infer_getter_return_types(functions: list[FunctionData]) -> None:
|
|||||||
func["return_type"] = setters[prefix]
|
func["return_type"] = setters[prefix]
|
||||||
|
|
||||||
|
|
||||||
|
def introspect_ops_module() -> ModuleData:
|
||||||
|
"""Introspect bpy.ops, including the operator proxy classes.
|
||||||
|
|
||||||
|
bpy.ops submodules (e.g. bpy.ops.mesh) are real Python module objects,
|
||||||
|
but individual operators (e.g. bpy.ops.mesh.primitive_cube_add) are
|
||||||
|
instances of _BPyOpsSubModOp — a C class with methods like poll(),
|
||||||
|
idname(), get_rna_type(), and bl_options.
|
||||||
|
|
||||||
|
We introspect _BPyOpsSubModOp from a live instance and fix up return
|
||||||
|
types that C methods don't expose via docstrings. For the submodule
|
||||||
|
level, we create a synthetic _OpsModule class with __getattr__ since
|
||||||
|
the actual type is just Python's builtin module.
|
||||||
|
"""
|
||||||
|
bpy = importlib.import_module("bpy")
|
||||||
|
|
||||||
|
# Get a real operator instance to discover the proxy type
|
||||||
|
ops_mod = getattr(bpy, "ops")
|
||||||
|
sub_name = next(n for n in dir(ops_mod) if not n.startswith("_"))
|
||||||
|
sub_mod = getattr(ops_mod, sub_name)
|
||||||
|
op_name = next(n for n in dir(sub_mod) if not n.startswith("_"))
|
||||||
|
op: object = getattr(sub_mod, op_name)
|
||||||
|
|
||||||
|
# Introspect _BPyOpsSubModOp (individual operator wrapper)
|
||||||
|
op_cls = type(op)
|
||||||
|
assert isinstance(op_cls, type)
|
||||||
|
op_struct = introspect_class(op_cls, "bpy.ops")
|
||||||
|
|
||||||
|
# Fix return types that introspection can't discover from docstrings.
|
||||||
|
# We call each method on a real operator to determine the actual types.
|
||||||
|
_return_type_fixups: dict[str, str] = {}
|
||||||
|
for method in op_struct["methods"]:
|
||||||
|
name = method["name"]
|
||||||
|
if method["return_type"] is not None:
|
||||||
|
continue
|
||||||
|
func = getattr(op, name, None)
|
||||||
|
if func is None or not callable(func):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
result = func()
|
||||||
|
_return_type_fixups[name] = type(result).__name__
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for method in op_struct["methods"]:
|
||||||
|
if method["name"] in _return_type_fixups:
|
||||||
|
method["return_type"] = _return_type_fixups[method["name"]]
|
||||||
|
|
||||||
|
# Fix bl_options type — it's a set of strings at runtime
|
||||||
|
for prop in op_struct["properties"]:
|
||||||
|
if prop["name"] == "bl_options":
|
||||||
|
prop["type"] = "set[str]"
|
||||||
|
|
||||||
|
# get_rna_type returns bpy.types.Struct
|
||||||
|
for method in op_struct["methods"]:
|
||||||
|
if method["name"] == "get_rna_type":
|
||||||
|
method["return_type"] = "bpy.types.Struct"
|
||||||
|
|
||||||
|
# Add __call__ — operators are callable and return a set of status strings
|
||||||
|
op_struct["methods"].append(
|
||||||
|
{
|
||||||
|
"name": "__call__",
|
||||||
|
"doc": "Execute the operator.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "args",
|
||||||
|
"type": "object",
|
||||||
|
"default": None,
|
||||||
|
"kind": "VAR_POSITIONAL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kwargs",
|
||||||
|
"type": "object",
|
||||||
|
"default": None,
|
||||||
|
"kind": "VAR_KEYWORD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"return_type": "set[str]",
|
||||||
|
"is_classmethod": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# _OpsSubModule: a class with __getattr__ returning operators.
|
||||||
|
# Each ops submodule (mesh, object, etc.) is typed as this class.
|
||||||
|
sub_struct: StructData = {
|
||||||
|
"name": "_OpsSubModule",
|
||||||
|
"doc": "Operator submodule (e.g. bpy.ops.mesh).",
|
||||||
|
"base": None,
|
||||||
|
"properties": [],
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "__getattr__",
|
||||||
|
"doc": "",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "str",
|
||||||
|
"default": None,
|
||||||
|
"kind": "POSITIONAL_OR_KEYWORD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"return_type": op_struct["name"],
|
||||||
|
"is_classmethod": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# List each ops submodule as a typed variable
|
||||||
|
variables: list[VariableData] = []
|
||||||
|
for sub_name in sorted(n for n in dir(ops_mod) if not n.startswith("_")):
|
||||||
|
variables.append({"name": sub_name, "type": "_OpsSubModule", "value": "..."})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"module": "bpy.ops",
|
||||||
|
"doc": "Blender operator access.",
|
||||||
|
"functions": [],
|
||||||
|
"variables": variables,
|
||||||
|
"structs": [op_struct, sub_struct],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def introspect_module(module_name: str) -> ModuleData:
|
def introspect_module(module_name: str) -> ModuleData:
|
||||||
"""Introspect a module and return its full metadata as a dict."""
|
"""Introspect a module and return its full metadata as a dict."""
|
||||||
if module_name == "bpy.types":
|
if module_name == "bpy.types":
|
||||||
return introspect_rna_types()
|
return introspect_rna_types()
|
||||||
|
|
||||||
|
if module_name == "bpy.ops":
|
||||||
|
return introspect_ops_module()
|
||||||
|
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
# Use __all__ as the base, but also include public callables from dir()
|
# Use __all__ as the base, but also include public callables from dir()
|
||||||
@ -1243,10 +1367,24 @@ def introspect_module(module_name: str) -> ModuleData:
|
|||||||
# Also add type aliases (e.g. FCurveKey = Tuple[str, int])
|
# Also add type aliases (e.g. FCurveKey = Tuple[str, int])
|
||||||
# but not stdlib re-exports (Iterable, Sequence, etc.)
|
# but not stdlib re-exports (Iterable, Sequence, etc.)
|
||||||
elif hasattr(obj, "__origin__") and n not in {
|
elif hasattr(obj, "__origin__") and n not in {
|
||||||
"Callable", "Collection", "Generator", "Iterable",
|
"Callable",
|
||||||
"Iterator", "Mapping", "MutableMapping", "MutableSequence",
|
"Collection",
|
||||||
"MutableSet", "Sequence", "Set", "FrozenSet",
|
"Generator",
|
||||||
"Dict", "List", "Tuple", "Type", "Optional", "Union",
|
"Iterable",
|
||||||
|
"Iterator",
|
||||||
|
"Mapping",
|
||||||
|
"MutableMapping",
|
||||||
|
"MutableSequence",
|
||||||
|
"MutableSet",
|
||||||
|
"Sequence",
|
||||||
|
"Set",
|
||||||
|
"FrozenSet",
|
||||||
|
"Dict",
|
||||||
|
"List",
|
||||||
|
"Tuple",
|
||||||
|
"Type",
|
||||||
|
"Optional",
|
||||||
|
"Union",
|
||||||
}:
|
}:
|
||||||
names_set.add(n)
|
names_set.add(n)
|
||||||
public_names: list[str] = sorted(names_set)
|
public_names: list[str] = sorted(names_set)
|
||||||
@ -1282,7 +1420,7 @@ def introspect_module(module_name: str) -> ModuleData:
|
|||||||
variables.append(
|
variables.append(
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": f"TypeAlias",
|
"type": "TypeAlias",
|
||||||
"value": type_repr,
|
"value": type_repr,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1810,7 +1948,26 @@ def introspect_rna_types() -> ModuleData:
|
|||||||
# Introspect bpy_struct (base of all RNA types)
|
# Introspect bpy_struct (base of all RNA types)
|
||||||
bpy_struct_cls = getattr(_bpy_types, "bpy_struct", None)
|
bpy_struct_cls = getattr(_bpy_types, "bpy_struct", None)
|
||||||
if bpy_struct_cls is not None:
|
if bpy_struct_cls is not None:
|
||||||
structs.append(introspect_class(bpy_struct_cls, "bpy.types"))
|
bpy_struct_data = introspect_class(bpy_struct_cls, "bpy.types")
|
||||||
|
# rna_type and bl_rna are built-in RNA meta-properties not discovered
|
||||||
|
# by introspect_class (they're injected at the C level).
|
||||||
|
bpy_struct_data["properties"].extend(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "bl_rna",
|
||||||
|
"type": "Struct",
|
||||||
|
"is_readonly": True,
|
||||||
|
"description": "RNA type definition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rna_type",
|
||||||
|
"type": "Struct",
|
||||||
|
"is_readonly": True,
|
||||||
|
"description": "RNA type definition",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
structs.append(bpy_struct_data)
|
||||||
|
|
||||||
for cls_name in ["bpy_prop_collection", "bpy_prop_array"]:
|
for cls_name in ["bpy_prop_collection", "bpy_prop_array"]:
|
||||||
cls = getattr(_bpy_types, cls_name, None)
|
cls = getattr(_bpy_types, cls_name, None)
|
||||||
@ -1893,6 +2050,24 @@ def introspect_rna_types() -> ModuleData:
|
|||||||
struct["properties"].extend(screen_props)
|
struct["properties"].extend(screen_props)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Pick up non-RNA C classes in bpy.types (e.g. GeometrySet) that
|
||||||
|
# aren't discovered by BuildRNAInfo.
|
||||||
|
import builtins as _builtins
|
||||||
|
|
||||||
|
known_names = {s["name"] for s in structs}
|
||||||
|
for name in sorted(dir(_bpy_types)):
|
||||||
|
if name.startswith("_") or name in known_names:
|
||||||
|
continue
|
||||||
|
obj = getattr(_bpy_types, name, None)
|
||||||
|
if not isinstance(obj, type):
|
||||||
|
continue
|
||||||
|
# Accept classes from bpy.types or C-level classes (builtins) that
|
||||||
|
# aren't standard Python builtins (int, str, etc.)
|
||||||
|
if obj.__module__ == "bpy.types" or (
|
||||||
|
obj.__module__ == "builtins" and not hasattr(_builtins, name)
|
||||||
|
):
|
||||||
|
structs.append(introspect_class(obj, "bpy.types"))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"module": "bpy.types",
|
"module": "bpy.types",
|
||||||
"doc": "Blender RNA type definitions.",
|
"doc": "Blender RNA type definitions.",
|
||||||
|
|||||||
23
main.py
23
main.py
@ -155,7 +155,7 @@ README_TEMPLATE = """\
|
|||||||
|
|
||||||
Type stubs for Blender {blender_version} Python API.
|
Type stubs for Blender {blender_version} Python API.
|
||||||
|
|
||||||
Provides type information for `bpy`, `mathutils`, `bmesh`, `gpu`, `freestyle`, and other Blender Python modules.
|
Provides autocomplete, type checking, and inline documentation for `bpy`, `mathutils`, `bmesh`, `gpu`, `freestyle`, and all other Blender Python modules.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -163,16 +163,31 @@ Provides type information for `bpy`, `mathutils`, `bmesh`, `gpu`, `freestyle`, a
|
|||||||
pip install "blender-python-stubs>={major_minor},<{next_minor}"
|
pip install "blender-python-stubs>={major_minor},<{next_minor}"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Features
|
||||||
|
|
||||||
Install alongside your Blender addon project for type checking with mypy, pyright, ty or your IDE.
|
- Full Blender API coverage (`bpy`, `mathutils`, `bmesh`, `gpu`, `gpu_extras`, `bpy_extras`, `freestyle`, `aud`, `blf`, `bl_math`, `imbuf`, `idprop`)
|
||||||
|
- Accurate collection types (`BlendDataObjects` instead of generic `bpy_prop_collection`)
|
||||||
|
- Readonly `@property` decorators for RNA attributes
|
||||||
|
- Typed context members (`bpy.context.active_object`, `selected_objects`, etc.)
|
||||||
|
- Constructor signatures for `mathutils`, `gpu`, and other C extension types
|
||||||
|
- Literal enum types instead of plain `str`
|
||||||
|
- Zero `typing.Any` usage
|
||||||
|
- 0 errors in basedpyright strict mode
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
# Your IDE now provides autocomplete and type checking
|
|
||||||
obj: bpy.types.Object = bpy.context.active_object
|
obj: bpy.types.Object = bpy.context.active_object
|
||||||
|
obj.location.x = 1.0
|
||||||
|
|
||||||
|
bpy.data.objects.new("Cube", bpy.data.meshes.new("Mesh"))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Generated by [blender-python-stubs](https://git.autourdeminuit.com/autour_de_minuit/blender-python-stubs).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -20,9 +20,9 @@ classifiers = [
|
|||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/autourdeminuit/blender-python-stubs"
|
Homepage = "https://git.autourdeminuit.com/autour_de_minuit/blender-python-stubs"
|
||||||
Repository = "https://github.com/autourdeminuit/blender-python-stubs"
|
Repository = "https://git.autourdeminuit.com/autour_de_minuit/blender-python-stubs"
|
||||||
Issues = "https://github.com/autourdeminuit/blender-python-stubs/issues"
|
Issues = "https://git.autourdeminuit.com/autour_de_minuit/blender-python-stubs/issues"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ["py311"]
|
target-version = ["py311"]
|
||||||
@ -46,6 +46,20 @@ test = { cmd = "python -m unittest discover -s tests -v", help = "Run unit tests
|
|||||||
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.0 4.3)"
|
||||||
|
|
||||||
|
[tool.poe.tasks.publish]
|
||||||
|
shell = "uv build dist/$version && uv publish dist/$version/dist/* --publish-url ${publish_url}"
|
||||||
|
help = "Build and publish stubs (e.g., poe publish 5.0)"
|
||||||
|
|
||||||
|
[tool.poe.tasks.publish.args.version]
|
||||||
|
positional = true
|
||||||
|
required = true
|
||||||
|
help = "Blender version to publish (e.g., 5.0)"
|
||||||
|
|
||||||
|
[tool.poe.tasks.publish.args.publish-url]
|
||||||
|
options = ["--publish-url"]
|
||||||
|
default = "https://test.pypi.org/legacy/"
|
||||||
|
help = "PyPI upload URL (defaults to Test PyPI)"
|
||||||
|
|
||||||
[tool.poe.tasks.check]
|
[tool.poe.tasks.check]
|
||||||
help = "Run all checks (format, lint, typecheck, test)"
|
help = "Run all checks (format, lint, typecheck, test)"
|
||||||
sequence = ["format", "lint", "typecheck", "test", "typecheck-stubs"]
|
sequence = ["format", "lint", "typecheck", "test", "typecheck-stubs"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user