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:
2026-03-26 15:07:35 +01:00
co-authored by Claude Opus 4.6
parent 852a5de700
commit 6ea41f3b4d
6 changed files with 253 additions and 22 deletions
+183 -8
View File
@@ -483,11 +483,12 @@ def clean_type_str(type_str: str) -> str:
r"Sequence[tuple[\1, \2]]",
type_str,
)
# list[X, Y, ...] with >1 type args -> treat as list[X] (drop extras)
def _fix_multi_arg_list(m: re.Match[str]) -> str:
inner = m.group(1)
# If it contains nested generics like list[float], keep first one
parts = []
parts: list[str] = []
depth = 0
current: list[str] = []
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."""
# Determine base class (skip object and internal bases)
bases = [
@@ -1216,11 +1217,134 @@ def infer_getter_return_types(functions: list[FunctionData]) -> None:
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:
"""Introspect a module and return its full metadata as a dict."""
if module_name == "bpy.types":
return introspect_rna_types()
if module_name == "bpy.ops":
return introspect_ops_module()
module = importlib.import_module(module_name)
# 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])
# but not stdlib re-exports (Iterable, Sequence, etc.)
elif hasattr(obj, "__origin__") and n not in {
"Callable", "Collection", "Generator", "Iterable",
"Iterator", "Mapping", "MutableMapping", "MutableSequence",
"MutableSet", "Sequence", "Set", "FrozenSet",
"Dict", "List", "Tuple", "Type", "Optional", "Union",
"Callable",
"Collection",
"Generator",
"Iterable",
"Iterator",
"Mapping",
"MutableMapping",
"MutableSequence",
"MutableSet",
"Sequence",
"Set",
"FrozenSet",
"Dict",
"List",
"Tuple",
"Type",
"Optional",
"Union",
}:
names_set.add(n)
public_names: list[str] = sorted(names_set)
@@ -1282,7 +1420,7 @@ def introspect_module(module_name: str) -> ModuleData:
variables.append(
{
"name": name,
"type": f"TypeAlias",
"type": "TypeAlias",
"value": type_repr,
}
)
@@ -1810,7 +1948,26 @@ def introspect_rna_types() -> ModuleData:
# Introspect bpy_struct (base of all RNA types)
bpy_struct_cls = getattr(_bpy_types, "bpy_struct", 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"]:
cls = getattr(_bpy_types, cls_name, None)
@@ -1893,6 +2050,24 @@ def introspect_rna_types() -> ModuleData:
struct["properties"].extend(screen_props)
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 {
"module": "bpy.types",
"doc": "Blender RNA type definitions.",