nodebpy 520.35.1__tar.gz → 520.37.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nodebpy-520.35.1 → nodebpy-520.37.0}/PKG-INFO +17 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/README.md +16 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/pyproject.toml +5 -1
- nodebpy-520.37.0/src/nodebpy/_lookup.py +215 -0
- nodebpy-520.37.0/src/nodebpy/_skill.py +53 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/assets/_library.py +15 -3
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/__init__.py +2 -0
- nodebpy-520.37.0/src/nodebpy/builder/_combine.py +80 -0
- nodebpy-520.37.0/src/nodebpy/builder/_infer.py +139 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/_utils.py +62 -2
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/items.py +3 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/node.py +33 -2
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/tree.py +82 -3
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/codegen.py +220 -70
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/DESIGN.md +4 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/balancing.py +24 -18
- nodebpy-520.37.0/src/nodebpy/layout/feeders.py +93 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/pipeline.py +6 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/placement.py +9 -3
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/sugiyama.py +4 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/_mixins.py +263 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/color.py +97 -63
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/converter.py +189 -336
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/distort.py +14 -11
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/filter.py +201 -126
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/input.py +25 -19
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/interface.py +1 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/manual.py +101 -3
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/matte.py +48 -36
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/output.py +2 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/attribute.py +2 -2
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/color.py +9 -6
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/converter.py +124 -128
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/geometry.py +120 -110
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/grid.py +16 -16
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/input.py +27 -25
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/interface.py +5 -5
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/manual.py +121 -91
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/output.py +3 -2
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/texture.py +26 -26
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/vector.py +11 -8
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/zone.py +1 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/__init__.py +5 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/color.py +25 -16
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/converter.py +6 -6
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/grid.py +2 -2
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/input.py +101 -7
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/manual.py +29 -84
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/output.py +5 -5
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/script.py +1 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/shader.py +44 -34
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/texture.py +9 -9
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/vector.py +9 -9
- nodebpy-520.37.0/src/nodebpy/skills/nodebpy/SKILL.md +167 -0
- nodebpy-520.37.0/src/nodebpy/skills/nodebpy/references/existing-trees.md +156 -0
- nodebpy-520.37.0/src/nodebpy/skills/nodebpy/references/shader-compositor.md +99 -0
- nodebpy-520.37.0/src/nodebpy/skills/nodebpy/references/verifying.md +86 -0
- nodebpy-520.37.0/src/nodebpy/skills/nodebpy/references/writing-trees.md +148 -0
- nodebpy-520.37.0/src/nodebpy/skills/nodebpy/references/zones-and-items.md +121 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/__main__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/assets/__main__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/assets/_codegen.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/assets/_pipeline.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/assets/_textconv.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/_socket_order.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/_floats.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/parity.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/plot.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/__init__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/api.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/apply.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/build.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/common.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/config.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/digraph.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/dna.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/edits.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/extract.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/long_links.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/model.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/node_size.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/ordering.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/packing.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/priority.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/ranking.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/realize.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/reroutes.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/routing.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/snapping.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/spacing.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/stacking.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/layout/zones.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/live.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/__init__.py +1 -1
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/assets.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/py.typed +0 -0
- {nodebpy-520.35.1 → nodebpy-520.37.0}/src/nodebpy/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: nodebpy
|
|
3
|
-
Version: 520.
|
|
3
|
+
Version: 520.37.0
|
|
4
4
|
Summary: Build nodes trees in Blender more elegantly with code
|
|
5
5
|
Author: Brady Johnston
|
|
6
6
|
Author-email: Brady Johnston <brady.johnston@me.com>
|
|
@@ -60,6 +60,22 @@ with g.tree("AnotherTree", collapse=True, is_modifier=True) as tree:
|
|
|
60
60
|
- Python's arithmetic, comparison and boolean operators create the matching `Math` / `VectorMath` / `Compare` / `BooleanMath` nodes: `g.Value(1.0) * 2 + (0, 0, 1)`.
|
|
61
61
|
- Existing node trees — including ones wired up by hand in the GUI — can be exported back to `nodebpy` source with `to_python()`.
|
|
62
62
|
|
|
63
|
+
## Agent skill
|
|
64
|
+
|
|
65
|
+
nodebpy ships an [Agent Skill](https://agentskills.io) that teaches AI coding agents to
|
|
66
|
+
write, check and port node trees with it. Install it into any skills-aware agent
|
|
67
|
+
(Claude Code, Codex, Cursor, Copilot, OpenCode and others) from the repository:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npx skills add BradyAJohnston/nodebpy
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Claude Code can also take it as a plugin (`/plugin marketplace add BradyAJohnston/nodebpy`,
|
|
74
|
+
then `/plugin install nodebpy@nodebpy`), and the installed package carries the same
|
|
75
|
+
skill, matched to its version: `nodebpy skill install` copies it into `~/.claude/skills`
|
|
76
|
+
and `nodebpy skill path` prints where it is. `nodebpy lookup search|show|socket` is the
|
|
77
|
+
lookup the skill uses to find classes, sockets and options.
|
|
78
|
+
|
|
63
79
|
## Documentation
|
|
64
80
|
|
|
65
81
|
Guides and the full node reference live at [bradyajohnston.github.io/nodebpy](https://bradyajohnston.github.io/nodebpy):
|
|
@@ -43,6 +43,22 @@ with g.tree("AnotherTree", collapse=True, is_modifier=True) as tree:
|
|
|
43
43
|
- Python's arithmetic, comparison and boolean operators create the matching `Math` / `VectorMath` / `Compare` / `BooleanMath` nodes: `g.Value(1.0) * 2 + (0, 0, 1)`.
|
|
44
44
|
- Existing node trees — including ones wired up by hand in the GUI — can be exported back to `nodebpy` source with `to_python()`.
|
|
45
45
|
|
|
46
|
+
## Agent skill
|
|
47
|
+
|
|
48
|
+
nodebpy ships an [Agent Skill](https://agentskills.io) that teaches AI coding agents to
|
|
49
|
+
write, check and port node trees with it. Install it into any skills-aware agent
|
|
50
|
+
(Claude Code, Codex, Cursor, Copilot, OpenCode and others) from the repository:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx skills add BradyAJohnston/nodebpy
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Claude Code can also take it as a plugin (`/plugin marketplace add BradyAJohnston/nodebpy`,
|
|
57
|
+
then `/plugin install nodebpy@nodebpy`), and the installed package carries the same
|
|
58
|
+
skill, matched to its version: `nodebpy skill install` copies it into `~/.claude/skills`
|
|
59
|
+
and `nodebpy skill path` prints where it is. `nodebpy lookup search|show|socket` is the
|
|
60
|
+
lookup the skill uses to find classes, sockets and options.
|
|
61
|
+
|
|
46
62
|
## Documentation
|
|
47
63
|
|
|
48
64
|
Guides and the full node reference live at [bradyajohnston.github.io/nodebpy](https://bradyajohnston.github.io/nodebpy):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nodebpy"
|
|
3
|
-
version = "520.
|
|
3
|
+
version = "520.37.0"
|
|
4
4
|
description = "Build nodes trees in Blender more elegantly with code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -65,6 +65,10 @@ not-subscriptable = "ignore"
|
|
|
65
65
|
invalid-assignment = "ignore"
|
|
66
66
|
invalid-argument-type = "ignore"
|
|
67
67
|
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
# The agent skill's Markdown keeps its examples compact and aligned by hand.
|
|
70
|
+
extend-exclude = ["src/nodebpy/skills", "skills"]
|
|
71
|
+
|
|
68
72
|
[tool.ruff.lint]
|
|
69
73
|
ignore = ["SIM117", "RUF059"]
|
|
70
74
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""``nodebpy lookup``: find node classes and show their API compactly.
|
|
2
|
+
|
|
3
|
+
``search <term>`` matches the term (case-insensitive, words may be separated
|
|
4
|
+
by anything) against class names, Blender ``bl_idname``s and the first
|
|
5
|
+
docstring line. ``show <ClassName>`` prints the constructor parameters, enum
|
|
6
|
+
options, class-method variants and output sockets without the long Union
|
|
7
|
+
types ``inspect.signature`` prints. ``socket [Type]`` lists the methods and
|
|
8
|
+
properties on a socket type (what ``node.o.<name>`` or
|
|
9
|
+
``tree.inputs.<type>(...)`` returns), or the socket types themselves.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import inspect
|
|
16
|
+
import re
|
|
17
|
+
import typing
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
|
|
20
|
+
_PREFIX = {"geometry": "g", "shader": "s", "compositor": "c"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _modules() -> dict[str, object]:
|
|
24
|
+
import nodebpy # noqa: F401 # before bpy: the project's own copy wins
|
|
25
|
+
from nodebpy import compositor, geometry, shader
|
|
26
|
+
|
|
27
|
+
return {"geometry": geometry, "shader": shader, "compositor": compositor}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _classes(module) -> Iterator[tuple[str, type]]:
|
|
31
|
+
for name in dir(module):
|
|
32
|
+
obj = getattr(module, name)
|
|
33
|
+
if name[0].isupper() and inspect.isclass(obj) and hasattr(obj, "_bl_idname"):
|
|
34
|
+
yield name, obj
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _summary(cls) -> str:
|
|
38
|
+
doc = inspect.getdoc(cls) or ""
|
|
39
|
+
return doc.strip().splitlines()[0] if doc.strip() else ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def search(term: str, trees: list[str]) -> list[str]:
|
|
43
|
+
words = re.split(r"[\s_]+", term.strip())
|
|
44
|
+
pattern = re.compile(".*".join(re.escape(w) for w in words), re.IGNORECASE)
|
|
45
|
+
modules = _modules()
|
|
46
|
+
lines = []
|
|
47
|
+
for tree in trees:
|
|
48
|
+
for name, cls in _classes(modules[tree]):
|
|
49
|
+
idname = getattr(cls, "_bl_idname", "")
|
|
50
|
+
summary = _summary(cls)
|
|
51
|
+
if (
|
|
52
|
+
pattern.search(name)
|
|
53
|
+
or pattern.search(idname)
|
|
54
|
+
or pattern.search(summary)
|
|
55
|
+
):
|
|
56
|
+
lines.append(f"{_PREFIX[tree]}.{name:32} {idname:40} {summary[:70]}")
|
|
57
|
+
return lines or [f"no node class matches {term!r}"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _short_type(annotation) -> str:
|
|
61
|
+
if typing.get_origin(annotation) is typing.Literal:
|
|
62
|
+
return " | ".join(repr(a) for a in typing.get_args(annotation))
|
|
63
|
+
text = str(annotation)
|
|
64
|
+
match = re.search(r"ForwardRef\('(\w+Socket)'\)", text)
|
|
65
|
+
return match.group(1) if match else text.replace("typing.", "")[:40]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _signature_lines(fn, names_only: bool = False) -> list[str]:
|
|
69
|
+
"""One entry per parameter, with a ``*`` marker before keyword-only ones."""
|
|
70
|
+
try:
|
|
71
|
+
sig = inspect.signature(fn)
|
|
72
|
+
except (TypeError, ValueError): # pragma: no cover - C-implemented callables
|
|
73
|
+
return []
|
|
74
|
+
hints: dict = {}
|
|
75
|
+
if not names_only:
|
|
76
|
+
try:
|
|
77
|
+
hints = typing.get_type_hints(fn)
|
|
78
|
+
except Exception: # noqa: BLE001 - forward references that do not resolve
|
|
79
|
+
hints = {}
|
|
80
|
+
entries, keyword_only = [], False
|
|
81
|
+
for param in sig.parameters.values():
|
|
82
|
+
if param.name == "self":
|
|
83
|
+
continue
|
|
84
|
+
if param.kind is param.KEYWORD_ONLY and not keyword_only:
|
|
85
|
+
entries.append("*")
|
|
86
|
+
keyword_only = True
|
|
87
|
+
if param.kind in (param.VAR_KEYWORD, param.VAR_POSITIONAL):
|
|
88
|
+
entries.append(
|
|
89
|
+
f"{'**' if param.kind is param.VAR_KEYWORD else '*'}{param.name}"
|
|
90
|
+
)
|
|
91
|
+
continue
|
|
92
|
+
if names_only:
|
|
93
|
+
entries.append(param.name)
|
|
94
|
+
continue
|
|
95
|
+
default = "" if param.default is param.empty else f" = {param.default!r}"
|
|
96
|
+
kind = _short_type(hints.get(param.name, param.annotation))
|
|
97
|
+
entries.append(f"{param.name}: {kind}{default}")
|
|
98
|
+
return entries
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def show(name: str, trees: list[str]) -> list[str]:
|
|
102
|
+
modules = _modules()
|
|
103
|
+
for tree in trees:
|
|
104
|
+
cls = getattr(modules[tree], name, None)
|
|
105
|
+
if cls is None:
|
|
106
|
+
continue
|
|
107
|
+
lines = [
|
|
108
|
+
f"{tree}.{name} ({getattr(cls, '_bl_idname', '?')})",
|
|
109
|
+
f" {_summary(cls)}",
|
|
110
|
+
"",
|
|
111
|
+
" constructor:",
|
|
112
|
+
]
|
|
113
|
+
for entry in _signature_lines(cls.__init__):
|
|
114
|
+
lines.append(
|
|
115
|
+
" * (keyword-only properties)" if entry == "*" else f" {entry}"
|
|
116
|
+
)
|
|
117
|
+
methods, factories = [], {}
|
|
118
|
+
for attr in sorted(dir(cls)):
|
|
119
|
+
if attr.startswith("_") or not attr.islower():
|
|
120
|
+
continue
|
|
121
|
+
static = inspect.getattr_static(cls, attr, None)
|
|
122
|
+
if isinstance(static, classmethod) and attr != "create_group":
|
|
123
|
+
methods.append(attr)
|
|
124
|
+
elif not isinstance(static, (property, staticmethod)) and not callable(
|
|
125
|
+
static
|
|
126
|
+
):
|
|
127
|
+
# factory objects: g.Compare.float.less_than(), g.StoreNamedAttribute.point.float()
|
|
128
|
+
subs = tuple(s for s in dir(static) if not s.startswith("_"))
|
|
129
|
+
if subs:
|
|
130
|
+
factories.setdefault(subs, []).append(attr)
|
|
131
|
+
if methods:
|
|
132
|
+
lines.append("")
|
|
133
|
+
lines.append(" variants:")
|
|
134
|
+
for m in methods:
|
|
135
|
+
params = ", ".join(_signature_lines(getattr(cls, m), names_only=True))
|
|
136
|
+
lines.append(f" {name}.{m}({params})")
|
|
137
|
+
for subs, attrs in factories.items():
|
|
138
|
+
left = attrs[0] if len(attrs) == 1 else "{" + ",".join(attrs) + "}"
|
|
139
|
+
lines.append("")
|
|
140
|
+
lines.append(f" variants: {name}.{left}.{{{','.join(subs)}}}()")
|
|
141
|
+
doc = inspect.getdoc(cls) or ""
|
|
142
|
+
outputs = doc.split("Outputs\n-------", 1)
|
|
143
|
+
if len(outputs) == 2:
|
|
144
|
+
names = [
|
|
145
|
+
ln.strip() for ln in outputs[1].splitlines() if ln.startswith("o.")
|
|
146
|
+
]
|
|
147
|
+
lines.append("")
|
|
148
|
+
lines.append(" outputs: " + ", ".join(names))
|
|
149
|
+
return lines
|
|
150
|
+
return [f"no class {name!r}; try: nodebpy lookup search {name}"]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def socket(kind: str | None) -> list[str]:
|
|
154
|
+
from nodebpy.builder import socket as sockets
|
|
155
|
+
|
|
156
|
+
base = set(dir(sockets.Socket))
|
|
157
|
+
classes = {
|
|
158
|
+
n.removesuffix("Socket"): c
|
|
159
|
+
for n, c in vars(sockets).items()
|
|
160
|
+
if inspect.isclass(c)
|
|
161
|
+
and n.endswith("Socket")
|
|
162
|
+
and n not in ("Socket", "BaseSocket")
|
|
163
|
+
}
|
|
164
|
+
if not kind:
|
|
165
|
+
return ["socket types: " + ", ".join(sorted(classes))]
|
|
166
|
+
wanted = kind.lower().removesuffix("socket")
|
|
167
|
+
key = next((k for k in classes if k.lower() == wanted), None)
|
|
168
|
+
if key is None:
|
|
169
|
+
return [f"no socket type {kind!r}; types: {', '.join(sorted(classes))}"]
|
|
170
|
+
cls = classes[key]
|
|
171
|
+
lines = [f"{key}Socket"]
|
|
172
|
+
for attr in sorted(set(dir(cls)) - base):
|
|
173
|
+
if attr.startswith("_"):
|
|
174
|
+
continue
|
|
175
|
+
static = inspect.getattr_static(cls, attr, None)
|
|
176
|
+
doc = (inspect.getdoc(static) or "").strip().splitlines()
|
|
177
|
+
first = doc[0] if doc else ""
|
|
178
|
+
if isinstance(static, property):
|
|
179
|
+
lines.append(f" .{attr:22} {first[:80]}")
|
|
180
|
+
elif callable(static) or isinstance(static, (classmethod, staticmethod)):
|
|
181
|
+
params = _signature_lines(getattr(cls, attr), names_only=True)
|
|
182
|
+
lines.append(f" .{attr}({', '.join(params)})".ljust(40) + f" {first[:70]}")
|
|
183
|
+
else: # pragma: no cover - a plain factory object on a socket class
|
|
184
|
+
subs = [x for x in dir(static) if not x.startswith("_")]
|
|
185
|
+
lines.append(f" .{attr}.{{{','.join(subs)}}}()"[:110])
|
|
186
|
+
return lines
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def add_parser(sub) -> None:
|
|
190
|
+
parser = sub.add_parser(
|
|
191
|
+
"lookup",
|
|
192
|
+
help="Find node classes and show their API: search, show, socket.",
|
|
193
|
+
description=__doc__,
|
|
194
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument("what", choices=["search", "show", "socket"])
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"term", nargs="?", help="search term, class name or socket type"
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--tree", choices=list(_PREFIX), default=None, help="limit to one tree type"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def run(args: argparse.Namespace) -> None:
|
|
206
|
+
trees = [args.tree] if args.tree else list(_PREFIX)
|
|
207
|
+
if args.what == "socket":
|
|
208
|
+
lines = socket(args.term)
|
|
209
|
+
elif not args.term:
|
|
210
|
+
raise SystemExit(f"nodebpy lookup {args.what} needs a term")
|
|
211
|
+
elif args.what == "search":
|
|
212
|
+
lines = search(args.term, trees)
|
|
213
|
+
else:
|
|
214
|
+
lines = show(args.term, trees)
|
|
215
|
+
print("\n".join(lines))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""``nodebpy skill``: the agent skill bundled with the package.
|
|
2
|
+
|
|
3
|
+
The skill (``nodebpy/skills/nodebpy/SKILL.md`` and its references) teaches
|
|
4
|
+
AI coding agents to write node trees with the installed nodebpy, so it
|
|
5
|
+
always matches the installed version. ``path`` prints its directory;
|
|
6
|
+
``install`` copies it into an agent's skills directory (Claude Code's
|
|
7
|
+
``~/.claude/skills`` by default). Other agents are covered by
|
|
8
|
+
``npx skills add BradyAJohnston/nodebpy``, which reads the same folder from
|
|
9
|
+
the repository.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
SKILL_DIR = Path(__file__).parent / "skills" / "nodebpy"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def install(target: Path) -> Path:
|
|
22
|
+
"""Copy the skill into ``target`` (a skills directory), replacing an
|
|
23
|
+
existing ``nodebpy`` skill there, and return the installed directory."""
|
|
24
|
+
destination = Path(target).expanduser() / SKILL_DIR.name
|
|
25
|
+
if destination.exists():
|
|
26
|
+
shutil.rmtree(destination)
|
|
27
|
+
shutil.copytree(
|
|
28
|
+
SKILL_DIR, destination, ignore=shutil.ignore_patterns("__pycache__")
|
|
29
|
+
)
|
|
30
|
+
return destination
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_parser(sub) -> None:
|
|
34
|
+
parser = sub.add_parser(
|
|
35
|
+
"skill",
|
|
36
|
+
help="Locate or install the bundled agent skill.",
|
|
37
|
+
description=__doc__,
|
|
38
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("what", choices=["path", "install"])
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--to",
|
|
43
|
+
type=Path,
|
|
44
|
+
default=Path("~/.claude/skills"),
|
|
45
|
+
help="skills directory to install into (default: ~/.claude/skills)",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run(args: argparse.Namespace) -> None:
|
|
50
|
+
if args.what == "path":
|
|
51
|
+
print(SKILL_DIR)
|
|
52
|
+
else:
|
|
53
|
+
print(f"installed {install(args.to)}")
|
|
@@ -1773,6 +1773,10 @@ def _parse_args(argv: list[str] | None = None):
|
|
|
1773
1773
|
),
|
|
1774
1774
|
)
|
|
1775
1775
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
1776
|
+
from nodebpy import _lookup, _skill
|
|
1777
|
+
|
|
1778
|
+
_lookup.add_parser(sub)
|
|
1779
|
+
_skill.add_parser(sub)
|
|
1776
1780
|
|
|
1777
1781
|
dump = sub.add_parser(
|
|
1778
1782
|
"dump",
|
|
@@ -2020,7 +2024,7 @@ def _parse_args(argv: list[str] | None = None):
|
|
|
2020
2024
|
)
|
|
2021
2025
|
|
|
2022
2026
|
args = parser.parse_args(argv)
|
|
2023
|
-
if args.command not in ("plot", "textconv"):
|
|
2027
|
+
if args.command not in ("plot", "textconv", "lookup", "skill"):
|
|
2024
2028
|
# Fill positionals and flags from the nearest pyproject's
|
|
2025
2029
|
# [tool.nodebpy.assets] table; explicit arguments always win.
|
|
2026
2030
|
apply_config(args)
|
|
@@ -2073,9 +2077,17 @@ def _build_command(args) -> None:
|
|
|
2073
2077
|
|
|
2074
2078
|
def main(argv: list[str] | None = None) -> None: # pragma: no cover - CLI wrapper
|
|
2075
2079
|
"""CLI entry point for the ``dump``, ``build``, ``ensure``, ``check``,
|
|
2076
|
-
``plot`` and ``
|
|
2080
|
+
``plot``, ``textconv``, ``lookup`` and ``skill`` subcommands."""
|
|
2077
2081
|
args = _parse_args(argv)
|
|
2078
|
-
if args.command == "
|
|
2082
|
+
if args.command == "lookup":
|
|
2083
|
+
from nodebpy import _lookup
|
|
2084
|
+
|
|
2085
|
+
_lookup.run(args)
|
|
2086
|
+
elif args.command == "skill":
|
|
2087
|
+
from nodebpy import _skill
|
|
2088
|
+
|
|
2089
|
+
_skill.run(args)
|
|
2090
|
+
elif args.command == "dump":
|
|
2079
2091
|
_dump_command(args)
|
|
2080
2092
|
elif args.command == "ensure":
|
|
2081
2093
|
if is_stale(args.blend, args.source, args.resources, stamp_options(args)):
|
|
@@ -81,6 +81,7 @@ from .tree import (
|
|
|
81
81
|
PanelContext,
|
|
82
82
|
SocketContext,
|
|
83
83
|
TreeBuilder,
|
|
84
|
+
WorldBuilder,
|
|
84
85
|
default_split_inputs,
|
|
85
86
|
)
|
|
86
87
|
|
|
@@ -157,6 +158,7 @@ __all__ = [
|
|
|
157
158
|
"VectorSocket",
|
|
158
159
|
"VectorSocketGrid",
|
|
159
160
|
"VectorSocketList",
|
|
161
|
+
"WorldBuilder",
|
|
160
162
|
"arrange",
|
|
161
163
|
"asset_group_base",
|
|
162
164
|
"build_from_source",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Feed a vector, rotation or color input from a tuple that holds sockets.
|
|
2
|
+
|
|
3
|
+
``g.Cube(size=(1, 1, height))`` can't store ``height`` in the socket's
|
|
4
|
+
default, since it is only known when the tree runs, so the tuple is built into
|
|
5
|
+
a Combine XYZ (or Combine Color) node whose output is linked in instead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from bpy.types import NodeSocket
|
|
13
|
+
|
|
14
|
+
from ._infer import _source_socket
|
|
15
|
+
from ._utils import SocketError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .tree import TreeBuilder
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _show(values: list[Any]) -> str:
|
|
22
|
+
"""The tuple as written, with sockets named by their node."""
|
|
23
|
+
parts = []
|
|
24
|
+
for v in values:
|
|
25
|
+
socket = _source_socket(v)
|
|
26
|
+
node = getattr(socket, "node", None)
|
|
27
|
+
parts.append(f"{node.name}'s {socket.name}" if socket and node else repr(v))
|
|
28
|
+
return f"({', '.join(parts)})"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def holds_sockets(value: Any) -> bool:
|
|
32
|
+
"""Whether ``value`` is a tuple or list with a socket or node in it."""
|
|
33
|
+
return isinstance(value, (tuple, list)) and any(
|
|
34
|
+
_source_socket(v) is not None for v in value
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def link_combined(tree: TreeBuilder[Any], socket: NodeSocket, value: Any) -> None:
|
|
39
|
+
"""Link a Combine XYZ / Combine Color built from ``value`` into ``socket``."""
|
|
40
|
+
from ..nodes import compositor, geometry, shader
|
|
41
|
+
|
|
42
|
+
nodes: Any = {"ShaderNodeTree": shader, "CompositorNodeTree": compositor}.get(
|
|
43
|
+
tree.tree.bl_idname, geometry
|
|
44
|
+
)
|
|
45
|
+
values = list(value)
|
|
46
|
+
if socket.type in ("VECTOR", "ROTATION"):
|
|
47
|
+
# a 2D vector socket takes (x, y); an Euler rotation is converted
|
|
48
|
+
size = len(getattr(socket, "default_value", ())) or 3
|
|
49
|
+
if socket.type == "ROTATION" or size not in (2, 3):
|
|
50
|
+
size = 3
|
|
51
|
+
if len(values) != size:
|
|
52
|
+
raise SocketError(
|
|
53
|
+
f"'{socket.name}' takes {size} components, not {len(values)}: "
|
|
54
|
+
f"{_show(values)}."
|
|
55
|
+
)
|
|
56
|
+
combined = nodes.CombineXYZ(*values)
|
|
57
|
+
elif socket.type == "RGBA":
|
|
58
|
+
if len(values) not in (3, 4):
|
|
59
|
+
raise SocketError(
|
|
60
|
+
f"'{socket.name}' takes a color of 3 or 4 components, not "
|
|
61
|
+
f"{len(values)}: {_show(values)}."
|
|
62
|
+
)
|
|
63
|
+
red, green, blue, *alpha = values
|
|
64
|
+
kwargs: dict[str, Any] = {"red": red, "green": green, "blue": blue}
|
|
65
|
+
if alpha and "alpha" in nodes.CombineColor.__init__.__code__.co_varnames:
|
|
66
|
+
kwargs["alpha"] = alpha[0]
|
|
67
|
+
elif alpha and not (isinstance(alpha[0], (int, float)) and alpha[0] == 1):
|
|
68
|
+
# shader colors carry no alpha through links (and ``==`` on a
|
|
69
|
+
# socket would build a Compare node rather than test it)
|
|
70
|
+
raise SocketError(
|
|
71
|
+
f"Shader colors have no alpha to set from {_show(alpha)[1:-1]}; "
|
|
72
|
+
f"pass (red, green, blue) to '{socket.name}'."
|
|
73
|
+
)
|
|
74
|
+
combined = nodes.CombineColor(**kwargs)
|
|
75
|
+
else:
|
|
76
|
+
raise SocketError(
|
|
77
|
+
f"'{socket.name}' is a {socket.type} input, which a tuple of sockets "
|
|
78
|
+
"can't feed."
|
|
79
|
+
)
|
|
80
|
+
tree.link(combined._default_output_socket, socket)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Pick a multi-type node's data type from the values it is given.
|
|
2
|
+
|
|
3
|
+
Switch, Index Switch and Menu Switch need their type set before anything is
|
|
4
|
+
linked. Left at the default (Float), a node passed in links whichever of its
|
|
5
|
+
outputs converts to a float, so ``g.Switch(cond, g.Cube(), g.UVSphere())``
|
|
6
|
+
silently switches between the two UV Maps instead of the meshes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
from bpy.types import Node, NodeSocket
|
|
15
|
+
|
|
16
|
+
from ._utils import SocketError
|
|
17
|
+
|
|
18
|
+
# the data_type / input_type spelling of a socket type, where they differ
|
|
19
|
+
_DATA_TYPE = {"VALUE": "FLOAT"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _source_socket(value: Any) -> NodeSocket | None:
|
|
23
|
+
"""The socket ``value`` would link from, or ``None`` for a plain value."""
|
|
24
|
+
if value is None or value is ...:
|
|
25
|
+
return None
|
|
26
|
+
if isinstance(value, NodeSocket):
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, Node):
|
|
29
|
+
return next((s for s in value.outputs if s.enabled), None)
|
|
30
|
+
socket = getattr(value, "socket", None)
|
|
31
|
+
if isinstance(socket, NodeSocket):
|
|
32
|
+
return socket
|
|
33
|
+
try:
|
|
34
|
+
# nodes and item handles link from their default output
|
|
35
|
+
default = getattr(value, "_default_output_socket", None)
|
|
36
|
+
except (IndexError, ValueError, AttributeError):
|
|
37
|
+
return None
|
|
38
|
+
return default if isinstance(default, NodeSocket) else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _literal_type(value: Any) -> str | None:
|
|
42
|
+
if isinstance(value, bool):
|
|
43
|
+
return "BOOLEAN"
|
|
44
|
+
if isinstance(value, int):
|
|
45
|
+
return "INT"
|
|
46
|
+
if isinstance(value, float):
|
|
47
|
+
return "FLOAT"
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
return "STRING"
|
|
50
|
+
if isinstance(value, (tuple, list)) and all(
|
|
51
|
+
isinstance(v, (int, float)) for v in value
|
|
52
|
+
):
|
|
53
|
+
return {3: "VECTOR", 4: "RGBA"}.get(len(value))
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _describe(socket: NodeSocket) -> str:
|
|
58
|
+
return f"{getattr(socket.node, 'name', '?')}'s {socket.name}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def infer_node_type(
|
|
62
|
+
node: Node, prop: str, values: Iterable[Any], *, default: str = "FLOAT"
|
|
63
|
+
) -> str:
|
|
64
|
+
"""``prop``'s value for ``node`` switching between ``values``, set on it.
|
|
65
|
+
|
|
66
|
+
The types a switch takes depend on the tree (Integer Vector only in the
|
|
67
|
+
compositor), which only the node itself knows, so the inferred type is
|
|
68
|
+
tried on the node and the error lists the ones it accepts.
|
|
69
|
+
"""
|
|
70
|
+
name = cast(Any, node.bl_rna).name
|
|
71
|
+
data_type = infer_data_type(name, values, keyword=prop, default=default)
|
|
72
|
+
try:
|
|
73
|
+
setattr(node, prop, data_type)
|
|
74
|
+
except TypeError:
|
|
75
|
+
raise SocketError(
|
|
76
|
+
f"{name} can't switch {data_type} values in this tree; it takes "
|
|
77
|
+
f"{', '.join(_accepted(node, prop))}."
|
|
78
|
+
) from None
|
|
79
|
+
return data_type
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _accepted(node: Node, prop: str) -> list[str]:
|
|
83
|
+
"""The values of ``prop`` that ``node`` accepts where it is."""
|
|
84
|
+
current = getattr(node, prop)
|
|
85
|
+
accepted = []
|
|
86
|
+
for item in cast(Any, node.bl_rna).properties[prop].enum_items:
|
|
87
|
+
try:
|
|
88
|
+
setattr(node, prop, item.identifier)
|
|
89
|
+
except TypeError:
|
|
90
|
+
continue
|
|
91
|
+
accepted.append(item.identifier)
|
|
92
|
+
setattr(node, prop, current)
|
|
93
|
+
return accepted
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def infer_data_type(
|
|
97
|
+
node_name: str,
|
|
98
|
+
values: Iterable[Any],
|
|
99
|
+
*,
|
|
100
|
+
keyword: str,
|
|
101
|
+
default: str = "FLOAT",
|
|
102
|
+
) -> str:
|
|
103
|
+
"""The data type for a node switching between ``values``.
|
|
104
|
+
|
|
105
|
+
Sockets and nodes decide it: each links from one socket, and they must all
|
|
106
|
+
agree. Plain Python values only decide when nothing is linked (``1`` and
|
|
107
|
+
``2.5`` together make a float). Raises :class:`SocketError` when the linked
|
|
108
|
+
values disagree.
|
|
109
|
+
"""
|
|
110
|
+
values = list(values)
|
|
111
|
+
sources = [s for s in (_source_socket(v) for v in values) if s is not None]
|
|
112
|
+
|
|
113
|
+
if not sources:
|
|
114
|
+
literals = {t for t in map(_literal_type, values) if t is not None}
|
|
115
|
+
if literals <= {"INT", "FLOAT"} and len(literals) == 2:
|
|
116
|
+
literals = {"FLOAT"}
|
|
117
|
+
return literals.pop() if len(literals) == 1 else default
|
|
118
|
+
|
|
119
|
+
found: dict[str, list[str]] = {}
|
|
120
|
+
for socket in sources:
|
|
121
|
+
data_type = _DATA_TYPE.get(socket.type, socket.type)
|
|
122
|
+
found.setdefault(data_type, []).append(_describe(socket))
|
|
123
|
+
if len(found) > 1:
|
|
124
|
+
listing = "; ".join(
|
|
125
|
+
f"{data_type} from {', '.join(names)}" for data_type, names in found.items()
|
|
126
|
+
)
|
|
127
|
+
raise SocketError(
|
|
128
|
+
f"Can't tell which type of {node_name} to build: the values are "
|
|
129
|
+
f"{listing}. Use a typed variant (e.g. "
|
|
130
|
+
f"{node_name.replace(' ', '')}.{_variant(next(iter(found)))}(...)) "
|
|
131
|
+
f"or pass {keyword}=."
|
|
132
|
+
)
|
|
133
|
+
(data_type,) = found
|
|
134
|
+
return data_type
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _variant(data_type: str) -> str:
|
|
138
|
+
"""The class-method name for a data type (``RGBA`` is ``color``)."""
|
|
139
|
+
return {"RGBA": "color", "INT": "integer"}.get(data_type, data_type.lower())
|