Skip to content

Commit a79f9e9

Browse files
committed
Update: Add inputs/outputs types for classes derived from bpy.types.Node (#341)
1 parent f316767 commit a79f9e9

3 files changed

Lines changed: 344 additions & 3 deletions

File tree

docs/generate_modules.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,14 @@ Or, you can download .zip file from GitHub.
219219
cd fake-${TARGET}-module/src
220220

221221
mkdir -p mods/generated_mods
222-
${BLENDER_BIN}/blender --background --factory-startup -noaudio --python-exit-code 1 --python gen_modfile/gen_external_modules_modfile.py -- -m addon_utils -o mods/generated_mods/gen_modules_modfile -f json
223-
${BLENDER_BIN}/blender --background --factory-startup -noaudio --python-exit-code 1 --python gen_modfile/gen_external_modules_modfile.py -- -m keyingsets_builtins -a -o mods/generated_mods/gen_startup_modfile -f json
222+
${BLENDER_BIN}/blender --background --factory-startup -noaudio --python-exit-code 1 --python gen_modfile/gen_external_modules_modfile.py -- -m addon_utils -o mods/generated_mods/gen_modules_modfile -f rst
223+
${BLENDER_BIN}/blender --background --factory-startup -noaudio --python-exit-code 1 --python gen_modfile/gen_external_modules_modfile.py -- -m keyingsets_builtins -a -o mods/generated_mods/gen_startup_modfile -f rst
224+
225+
mkdir -p mods/generated_mods/gen_node_inputs_outputs_for_node_class
226+
${BLENDER_BIN}/blender --background --factory-startup -noaudio --python-exit-code 1 --python gen_modfile/gen_node_inputs_outputs_for_node_class.py -- -o mods/generated_mods/gen_node_inputs_outputs_for_node_class -f rst
224227

225228
mkdir -p mods/generated_mods/gen_bgl_modfile
226-
python gen_modfile/gen_bgl_modfile.py -i ${BLENDER_SRC}/source/blender/python/generic/bgl.cc -o mods/generated_mods/gen_bgl_modfile/bgl.json -f json
229+
python gen_modfile/gen_bgl_modfile.py -i ${BLENDER_SRC}/source/blender/python/generic/bgl.cc -o mods/generated_mods/gen_bgl_modfile/bgl.rst -f rst
227230
```
228231
<!-- markdownlint-enable MD013 -->
229232

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
##############################################################################
2+
#
3+
# gen_node_inputs_outputs_for_node_class.py
4+
#
5+
# Description:
6+
# gen_node_inputs_outputs_for_node_class.py generates
7+
# definition to add inputs and outputs argument and their class
8+
# as a modfile format.
9+
#
10+
# Note:
11+
# This script needs to run from blender.
12+
# So, you need to download blender binary from blender's official website.
13+
#
14+
# Usage:
15+
# blender -noaudio --factory-startup --background --python \
16+
# gen_node_inputs_outputs_for_node_class.py --
17+
# -o <output_dir> -f <output_format>
18+
#
19+
# output_dir:
20+
# Generated definitions are output to files which will be located to
21+
# specified directory.
22+
# [Ex] gen_modules_modfile.generated
23+
#
24+
# output_format:
25+
# Output format. Supported formats are "rst" and "json".
26+
#
27+
##############################################################################
28+
29+
# ruff: noqa: UP006, UP032, UP035, PTH103
30+
# ruff: noqa: PTH123
31+
32+
import argparse
33+
import json
34+
import os
35+
import sys
36+
from typing import Dict, TextIO
37+
38+
import bpy # pylint: disable=E0401
39+
40+
41+
class GenerationConfig:
42+
first_import_module_name = None
43+
output_dir = None
44+
output_alias = False
45+
output_format = "rst"
46+
47+
48+
# pylint: disable=C0209
49+
def analyze() -> Dict:
50+
results = {
51+
"new": [],
52+
"append": [],
53+
}
54+
55+
for node_base_class in bpy.types.NodeInternal.__subclasses__():
56+
node_baseclass_name = node_base_class.__name__
57+
nodetree_class_name = "{}Tree".format(node_baseclass_name)
58+
59+
try:
60+
node_group = bpy.data.node_groups.new(
61+
nodetree_class_name, nodetree_class_name)
62+
except TypeError:
63+
continue
64+
65+
for node_class_name in dir(bpy.types):
66+
if not node_class_name.startswith(node_baseclass_name):
67+
continue
68+
69+
try:
70+
node = node_group.nodes.new(node_class_name)
71+
except RuntimeError:
72+
continue
73+
74+
append_class_def = {
75+
"name": node_class_name,
76+
"base_classes": [],
77+
"description": None,
78+
"type": "class",
79+
"module": "bpy.types",
80+
"methods": [],
81+
"attributes": [],
82+
}
83+
84+
if node.inputs:
85+
# Add fake class for node inputs.
86+
fake_class_name = "_{}_NodeInputs".format(node_class_name)
87+
new_class_def = {
88+
"name": fake_class_name,
89+
"base_classes": ["bpy.types.NodeInputs"],
90+
"description": None,
91+
"type": "class",
92+
"module": "bpy.types",
93+
"methods": [],
94+
"attributes": [],
95+
}
96+
97+
for i, node_input in enumerate(node.inputs):
98+
new_func_def = {
99+
"name": "__getitem__",
100+
"description": None,
101+
"type": "method",
102+
"parameters": ["key"],
103+
"parameter_details": [],
104+
"return": {},
105+
"options": ["overload"],
106+
}
107+
new_func_def["parameter_details"].append({
108+
"name": "key",
109+
"description": None,
110+
"data_type": (
111+
'typing.Literal[{}] | typing.Literal["{}"]'
112+
.format(i, node_input.name)
113+
),
114+
"mod_option": "skip-refine",
115+
})
116+
new_func_def["return"] = {
117+
"type": "return",
118+
"description": None,
119+
"data_type": (
120+
":class:`bpy.types.{}`"
121+
.format(node_input.__class__.__name__)
122+
),
123+
}
124+
new_class_def["methods"].append(new_func_def)
125+
126+
results["new"].append(new_class_def)
127+
128+
# Add attribute for input.
129+
new_attribute = {
130+
"type": "attribute",
131+
"name": "inputs",
132+
"description": None,
133+
"class": node_class_name,
134+
"module": "bpy.types",
135+
"data_type": (
136+
":class:`bpy.types.{}`"
137+
.format(fake_class_name)
138+
),
139+
"mod_option": "skip-refine",
140+
}
141+
append_class_def["attributes"].append(new_attribute)
142+
results["append"].append(append_class_def)
143+
144+
if node.outputs:
145+
# Add fake class for node outputs.
146+
fake_class_name = "_{}_NodeOutputs".format(node_class_name)
147+
new_class_def = {
148+
"name": fake_class_name,
149+
"base_classes": ["bpy.types.NodeOutputs"],
150+
"description": None,
151+
"type": "class",
152+
"module": "bpy.types",
153+
"methods": [],
154+
"attributes": [],
155+
}
156+
157+
for i, node_output in enumerate(node.outputs):
158+
new_func_def = {
159+
"name": "__getitem__",
160+
"description": None,
161+
"type": "method",
162+
"parameters": ["key"],
163+
"parameter_details": [],
164+
"return": {},
165+
"options": ["overload"],
166+
}
167+
new_func_def["parameter_details"].append({
168+
"name": "key",
169+
"description": None,
170+
"data_type": (
171+
'typing.Literal[{}] | typing.Literal["{}"]'
172+
.format(i, node_output.name)
173+
),
174+
"mod_option": "skip-refine",
175+
})
176+
new_func_def["return"] = {
177+
"type": "return",
178+
"description": None,
179+
"data_type": (
180+
":class:`bpy.types.{}`"
181+
.format(node_output.__class__.__name__)
182+
),
183+
}
184+
new_class_def["methods"].append(new_func_def)
185+
186+
results["new"].append(new_class_def)
187+
188+
# Add attribute for output.
189+
new_attribute_def = {
190+
"type": "attribute",
191+
"name": "outputs",
192+
"description": None,
193+
"class": node_class_name,
194+
"module": "bpy.types",
195+
"data_type": (
196+
":class:`bpy.types.{}`"
197+
.format(fake_class_name)
198+
),
199+
}
200+
append_class_def["attributes"].append(new_attribute_def)
201+
results["append"].append(append_class_def)
202+
203+
return results
204+
205+
206+
# pylint: disable=C0209
207+
def write_class_info(f: TextIO, mod_kind: str, class_info: Dict) -> None:
208+
f.write(".. mod-type:: {}\n\n".format(mod_kind))
209+
f.write(".. module:: {}\n\n".format(class_info["module"]))
210+
if len(class_info["base_classes"]) != 0:
211+
f.write("base classes --- {}\n\n".format(
212+
', '.join(class_info["base_classes"])))
213+
f.write(".. class:: {}\n\n".format(class_info["name"]))
214+
for attr_info in class_info["attributes"]:
215+
f.write(" .. attribute:: {}\n\n".format(attr_info["name"]))
216+
f.write(" :type: {}\n\n".format(attr_info["data_type"]))
217+
for func_info in class_info["methods"]:
218+
f.write(" .. {}:: {}({})\n\n".format(
219+
func_info["type"], func_info["name"],
220+
", ".join(func_info["parameters"])))
221+
if "overload" in func_info["options"]:
222+
f.write(" :option function: overload\n")
223+
for param_info in func_info["parameter_details"]:
224+
f.write(" :type {}: {}\n"
225+
.format(param_info["name"], param_info["data_type"]))
226+
f.write(" :mod-option arg {}: {}\n"
227+
.format(param_info["name"], param_info["mod_option"]))
228+
ret_info = func_info["return"]
229+
f.write(" :rtype: {}\n\n".format(ret_info["data_type"]))
230+
f.write("\n")
231+
232+
233+
# pylint: disable=C0209
234+
def write_to_rst_modfile(data: Dict, config: 'GenerationConfig') -> None:
235+
os.makedirs(config.output_dir, exist_ok=True)
236+
237+
for info in data["new"]:
238+
assert info["type"] == "class"
239+
240+
output_dir = "{}/new".format(config.output_dir)
241+
os.makedirs(output_dir, exist_ok=True)
242+
243+
mod_filename = "{}/{}.{}.mod.rst".format(
244+
output_dir, info["module"], info["name"])
245+
with open(mod_filename, "w", encoding="utf-8") as f:
246+
write_class_info(f, "new", info)
247+
for info in data["append"]:
248+
assert info["type"] == "class"
249+
250+
output_dir = "{}/append".format(config.output_dir)
251+
os.makedirs(output_dir, exist_ok=True)
252+
253+
mod_filename = "{}/{}.{}.mod.rst".format(
254+
output_dir, info["module"], info["name"])
255+
with open(mod_filename, "w", encoding="utf-8") as f:
256+
write_class_info(f, "append", info)
257+
258+
259+
# pylint: disable=C0209
260+
def write_to_json_modfile(data: Dict, config: 'GenerationConfig') -> None:
261+
os.makedirs(config.output_dir, exist_ok=True)
262+
263+
for info in data["new"]:
264+
output_dir = "{}/new".format(config.output_dir)
265+
os.makedirs(output_dir, exist_ok=True)
266+
mod_filename = "{}/{}.{}.mod.json".format(
267+
output_dir, info["module"], info["name"])
268+
with open(mod_filename, "w", encoding="utf-8") as f:
269+
json.dump(info, f, indent=4, sort_keys=True, separators=(",", ": "))
270+
271+
for info in data["append"]:
272+
output_dir = "{}/append".format(config.output_dir)
273+
os.makedirs(output_dir, exist_ok=True)
274+
mod_filename = "{}/{}.{}.mod.json".format(
275+
output_dir, info["module"], info["name"])
276+
with open(mod_filename, "w", encoding="utf-8") as f:
277+
json.dump(info, f, indent=4, sort_keys=True, separators=(",", ": "))
278+
279+
280+
def write_to_modfile(data: Dict, config: 'GenerationConfig') -> None:
281+
if config.output_format == "rst":
282+
write_to_rst_modfile(data, config)
283+
elif config.output_format == "json":
284+
write_to_json_modfile(data, config)
285+
286+
287+
# pylint: disable=C0209
288+
def parse_options() -> 'GenerationConfig':
289+
# Start after "--" option if we run this script from blender binary.
290+
argv = sys.argv
291+
try:
292+
index = argv.index("--") + 1
293+
except: # noqa: E722 # pylint: disable=W0702
294+
index = len(argv)
295+
argv = argv[index:]
296+
297+
usage = (
298+
"Usage: blender -noaudio --factory-startup --background "
299+
"--python {} -- [-f <output_format>] "
300+
"[-o <output_dir>]".format(__file__)
301+
)
302+
parser = argparse.ArgumentParser(usage)
303+
parser.add_argument(
304+
"-o", dest="output_dir", type=str, help="Output directory.",
305+
required=True
306+
)
307+
parser.add_argument("-f", dest="output_format", type=str,
308+
help="Output format (rst, json).", required=True)
309+
args = parser.parse_args(argv)
310+
311+
config = GenerationConfig()
312+
config.output_dir = args.output_dir
313+
config.output_format = args.output_format
314+
315+
if config.output_format not in ["rst", "json"]:
316+
raise ValueError(
317+
"Unsupported output format: {}".format(config.output_format))
318+
319+
return config
320+
321+
322+
def main() -> None:
323+
config = parse_options()
324+
325+
# Analyze modules.
326+
results = analyze()
327+
328+
# Write module info to file.
329+
write_to_modfile(results, config)
330+
331+
332+
if __name__ == "__main__":
333+
main()

src/gen_module.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ if [[ "${generated_mod_dir}/gen_startup_modfile" -ot "${SCRIPT_DIR}/gen_modfile/
241241
touch "${generated_mod_dir}/gen_startup_modfile"
242242
fi
243243

244+
if [[ "${generated_mod_dir}/gen_node_inputs_outputs_for_node_class" -ot "${SCRIPT_DIR}/gen_modfile/gen_node_inputs_outputs_for_node_class.py" ]]; then
245+
${blender_bin} --background --factory-startup -noaudio --python-exit-code 1 --python "${SCRIPT_DIR}/gen_modfile/gen_node_inputs_outputs_for_node_class.py" -- -o "${generated_mod_dir}/gen_node_inputs_outputs_for_node_class" -f rst
246+
touch "${generated_mod_dir}/gen_node_inputs_outputs_for_node_class"
247+
fi
248+
244249
# generate bgl modfile if gen_bgl_modfile.py and source is newer
245250
bgl_c_file="${source_dir}/source/blender/python/generic/bgl.c"
246251
if [ ! -e "${bgl_c_file}" ]; then

0 commit comments

Comments
 (0)