nodebpy 520.16.0__tar.gz → 520.18.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.
Files changed (83) hide show
  1. {nodebpy-520.16.0 → nodebpy-520.18.0}/PKG-INFO +1 -1
  2. {nodebpy-520.16.0 → nodebpy-520.18.0}/pyproject.toml +1 -1
  3. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/assets/__init__.py +8 -0
  4. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/assets/__main__.py +34 -2
  5. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/assets/_codegen.py +119 -6
  6. nodebpy-520.18.0/src/nodebpy/assets/_library.py +1307 -0
  7. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/__init__.py +2 -0
  8. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/_utils.py +42 -2
  9. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/asset.py +42 -0
  10. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/node.py +51 -1
  11. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/socket.py +109 -21
  12. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/tree.py +428 -12
  13. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/export/__init__.py +3 -0
  14. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/export/codegen.py +1133 -142
  15. nodebpy-520.18.0/src/nodebpy/export/parity.py +545 -0
  16. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/_mixins.py +52 -0
  17. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/attribute.py +1 -1
  18. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/converter.py +219 -178
  19. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/geometry.py +7 -6
  20. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/grid.py +287 -259
  21. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/input.py +2 -2
  22. {nodebpy-520.16.0 → nodebpy-520.18.0}/README.md +0 -0
  23. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/__init__.py +0 -0
  24. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/_registry.py +0 -0
  25. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/accessor.py +0 -0
  26. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/arrange.py +0 -0
  27. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/items.py +0 -0
  28. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/builder/mixins.py +0 -0
  29. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/export/diagram.py +0 -0
  30. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/export/web_render.py +0 -0
  31. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  32. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  33. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  34. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  35. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  36. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  37. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  38. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  39. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  40. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  41. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  42. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  43. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  44. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/__init__.py +0 -0
  45. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  46. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
  47. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  48. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  49. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  50. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  51. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  52. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  53. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  54. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  55. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  56. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  57. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  58. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  59. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
  60. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  61. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  62. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  63. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  64. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  65. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  66. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  67. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  68. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  69. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  70. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  71. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/assets.py +0 -0
  72. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/color.py +0 -0
  73. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  74. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  75. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/group.py +0 -0
  76. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/input.py +0 -0
  77. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  78. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/output.py +0 -0
  79. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/script.py +0 -0
  80. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  81. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  82. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  83. {nodebpy-520.16.0 → nodebpy-520.18.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.16.0
3
+ Version: 520.18.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "520.16.0"
3
+ version = "520.18.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -6,15 +6,23 @@ classes for the node groups in a ``.blend`` asset library into a single module;
6
6
  (``geometry.py`` / ``shader.py`` / ``compositor.py``). The bundled-essentials
7
7
  APIs generated for nodebpy itself live in ``nodebpy.nodes.{geometry,shader,
8
8
  compositor}.assets`` and are re-exported alongside the built-in nodes.
9
+
10
+ ``dump_library`` and ``build_library`` round-trip a ``.blend`` asset library
11
+ through Python source: every asset is dumped to its own ``.py`` file (the
12
+ version-controlled source of truth) and the ``.blend`` is rebuilt from them
13
+ (``python -m nodebpy.assets dump/build``).
9
14
  """
10
15
 
11
16
  from ..builder import AssetLibrary, BundledLibrary, PackageLibrary
12
17
  from ._codegen import generate_asset_api, generate_asset_modules
18
+ from ._library import build_library, dump_library
13
19
 
14
20
  __all__ = [
15
21
  "AssetLibrary",
16
22
  "BundledLibrary",
17
23
  "PackageLibrary",
24
+ "build_library",
25
+ "dump_library",
18
26
  "generate_asset_api",
19
27
  "generate_asset_modules",
20
28
  ]
@@ -5,12 +5,17 @@ libraries and writes typed classes to ``nodebpy/nodes/{geometry,shader,
5
5
  compositor}/assets.py``, where ``python -m gen`` re-exports them so they are
6
6
  available alongside the built-in nodes (e.g. ``g.SmoothByAngle()``). Run *before*
7
7
  ``python -m gen`` and through the ruff/ty post-processing (see the Makefile).
8
+
9
+ ``python -m nodebpy.assets dump <blend> <dir>`` and ``… build <dir> <blend>``
10
+ instead round-trip a ``.blend`` asset library through per-asset Python sources
11
+ (see :mod:`nodebpy.assets._library`).
8
12
  """
9
13
 
10
14
  from __future__ import annotations
11
15
 
12
16
  import argparse
13
17
  import os
18
+ import sys
14
19
  from pathlib import Path
15
20
 
16
21
  from ..builder import BundledLibrary, PackageLibrary
@@ -57,8 +62,27 @@ def generate_essentials(
57
62
  return written
58
63
 
59
64
 
60
- def parse_args() -> argparse.Namespace:
61
- parser = argparse.ArgumentParser()
65
+ def parse_args() -> argparse.Namespace: # pragma: no cover - CLI wrapper
66
+ parser = argparse.ArgumentParser(
67
+ prog="python -m nodebpy.assets",
68
+ description=(
69
+ "Generate typed nodebpy API classes for node-group assets. "
70
+ "Without a subcommand this regenerates the bundled-essentials "
71
+ "asset APIs (or, with --blend-file, generates an API module for "
72
+ "a custom asset library)."
73
+ ),
74
+ epilog=(
75
+ "subcommands:\n"
76
+ " dump <blend> <output-dir> dump every node-group asset in a "
77
+ ".blend to per-asset .py source files\n"
78
+ " build <source-dir> <blend> rebuild the .blend asset library "
79
+ "from dumped .py source files\n"
80
+ "\n"
81
+ "See 'python -m nodebpy.assets dump --help' and "
82
+ "'python -m nodebpy.assets build --help' for their options."
83
+ ),
84
+ formatter_class=argparse.RawDescriptionHelpFormatter,
85
+ )
62
86
  parser.add_argument(
63
87
  "--blend-file",
64
88
  "-b",
@@ -100,6 +124,14 @@ def parse_args() -> argparse.Namespace:
100
124
 
101
125
 
102
126
  def main() -> None: # pragma: no cover - CLI wrapper
127
+ # The dump/build subcommands (blend ↔ .py round-trip) have their own
128
+ # parser; everything else keeps the original flag-based interface.
129
+ if len(sys.argv) > 1 and sys.argv[1] in ("dump", "build"):
130
+ from ._library import main as library_main
131
+
132
+ library_main(sys.argv[1:])
133
+ return
134
+
103
135
  args = parse_args()
104
136
  output = (
105
137
  args.output
@@ -22,7 +22,8 @@ from typing import cast
22
22
  import bpy
23
23
 
24
24
  from ..builder import AssetLibrary, BundledLibrary, asset_group_base
25
- from ..builder._utils import normalize_name
25
+ from ..builder._utils import normalize_name, typed_param_names
26
+ from ..export.codegen import GroupInterface, _fmt
26
27
 
27
28
  # bl_socket_type substring → (Socket accessor class, Input* parameter type).
28
29
  # Order matters: more specific keys (IntVector before Int) come first.
@@ -90,7 +91,10 @@ def _format_default(socket: bpy.types.NodeSocket) -> str:
90
91
  if isinstance(value, int):
91
92
  return str(value)
92
93
  if isinstance(value, float):
93
- return str(round(value, 6))
94
+ # The exact float32 round-trip formatter (which also folds pi/tau/e
95
+ # constants): a lossy default here would *change* the socket value on
96
+ # every instantiation, since the typed __init__ always passes it.
97
+ return _fmt(value)
94
98
  if isinstance(value, str):
95
99
  return repr(value)
96
100
  return "None"
@@ -286,7 +290,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
286
290
  def _library_source(library: AssetLibrary) -> str:
287
291
  """Source expression that reconstructs ``library`` in the generated module."""
288
292
  if isinstance(library, BundledLibrary):
289
- return f"BundledLibrary({library.filename!r})"
293
+ return f"BundledLibrary({_fmt(library.filename)})"
290
294
  from ..builder import PackageLibrary
291
295
 
292
296
  if isinstance(library, PackageLibrary):
@@ -294,7 +298,7 @@ def _library_source(library: AssetLibrary) -> str:
294
298
  # so the generated module imports cleanly and stays cross-platform even
295
299
  # when ``relative`` was passed as a ``Path``.
296
300
  relative = Path(library.relative).as_posix()
297
- return f"PackageLibrary(__file__, {relative!r})"
301
+ return f"PackageLibrary(__file__, {_fmt(relative)})"
298
302
  raise TypeError(f"Cannot serialise asset library: {library!r}")
299
303
 
300
304
 
@@ -351,8 +355,8 @@ def _render_class(cls: _AssetClass, docstrings: bool = False) -> str:
351
355
  return f"""class {cls.class_name}({base}):
352
356
  {docstring}
353
357
 
354
- _name = {cls.asset_name!r}
355
- _asset_name = {cls.asset_name!r}
358
+ _name = {_fmt(cls.asset_name)}
359
+ _asset_name = {_fmt(cls.asset_name)}
356
360
  _library = {cls.library_source}
357
361
 
358
362
  {inputs_cls}
@@ -370,6 +374,112 @@ def _render_class(cls: _AssetClass, docstrings: bool = False) -> str:
370
374
  """
371
375
 
372
376
 
377
+ def interface_parts(
378
+ group,
379
+ *,
380
+ library_source: str | None,
381
+ nodebpy_pkg: str = "nodebpy",
382
+ ) -> tuple[GroupInterface, list[str]]:
383
+ """The typed-interface parts of a merged dump class for ``group`` (a live
384
+ ``bpy.types.NodeTree``), plus the import lines they require.
385
+
386
+ The parts — numpydoc docstring, ``_asset_name``/``_library`` attributes,
387
+ ``_Inputs``/``_Outputs`` accessors and the typed ``__init__`` — are spliced
388
+ by :func:`nodebpy.export.to_python` into the class it emits around
389
+ ``_build_group``, so one class both documents the group and carries the
390
+ recipe that regenerates it.
391
+
392
+ With ``library_source`` (a ``PackageLibrary(...)`` expression), the class
393
+ becomes an appending ``Asset*Group``; without it (shared helper groups,
394
+ which are not assets), only the typed API is added and the ``Custom*Group``
395
+ base stays.
396
+
397
+ The ``__init__`` keys its super call by socket *name* (with a
398
+ ``_named_links`` fallback for duplicate names) rather than by identifier:
399
+ identifiers are authoring-history artifacts that a tree rebuilt from
400
+ ``_build_group`` reassigns, while names round-trip. Parameter names come
401
+ from :func:`~nodebpy.builder._utils.typed_param_names` — the same mapping
402
+ codegen uses for call sites, so generated calls match the signature.
403
+ """
404
+ cls = _introspect_group(group, group.name, library_source or "")
405
+ # Align accessor/parameter names with the shared call-site mapping.
406
+ for side in (cls.inputs, cls.outputs):
407
+ params = typed_param_names(side)
408
+ for s in side:
409
+ s.attr = params[s.identifier]
410
+
411
+ docstring = f" {_class_docstring(cls)}"
412
+
413
+ params = [f"{s.attr}: {s.param_type} = {s.default}" for s in cls.inputs]
414
+ signature = (
415
+ "(\n self,\n " + ",\n ".join(params) + ",\n )"
416
+ if params
417
+ else "(self)"
418
+ )
419
+ name_counts = Counter(s.name for s in cls.inputs)
420
+ keyed = [
421
+ f"{_quote(s.name)}: {s.attr}" for s in cls.inputs if name_counts[s.name] == 1
422
+ ]
423
+ pairs = [
424
+ f"({_quote(s.name)}, {s.attr})" for s in cls.inputs if name_counts[s.name] > 1
425
+ ]
426
+ args = []
427
+ if keyed:
428
+ args.append("**{" + ", ".join(keyed) + "}")
429
+ if pairs:
430
+ args.append("_named_links=[" + ", ".join(pairs) + "]")
431
+ init = f" def __init__{signature}:\n super().__init__({', '.join(args)})"
432
+
433
+ type_checking = (
434
+ " if TYPE_CHECKING:\n"
435
+ " @property\n"
436
+ " def i(self) -> _Inputs: ...\n"
437
+ " @property\n"
438
+ " def o(self) -> _Outputs: ..."
439
+ )
440
+ body = "\n\n".join(
441
+ [
442
+ _accessor(cls.inputs, "_Inputs", docstrings=True),
443
+ _accessor(cls.outputs, "_Outputs", docstrings=True),
444
+ type_checking,
445
+ init,
446
+ ]
447
+ )
448
+
449
+ base: str | None = None
450
+ attr_lines: list[str] = []
451
+ if library_source:
452
+ base = asset_group_base(cls.tree_idname).__name__
453
+ attr_lines = [
454
+ f"_asset_name = {_fmt(cls.asset_name)}",
455
+ f"_library = {library_source}",
456
+ ]
457
+
458
+ socket_classes = sorted({s.socket_class for s in cls.inputs + cls.outputs})
459
+ input_types = sorted({s.input_type for s in cls.inputs})
460
+ typing_names = ["TYPE_CHECKING"]
461
+ if any(s.menu_items for s in cls.inputs):
462
+ typing_names.append("Literal")
463
+ builder_names = {"SocketAccessor", *socket_classes}
464
+ if library_source:
465
+ builder_names.add(library_source.partition("(")[0])
466
+ import_lines = [
467
+ f"from typing import {', '.join(typing_names)}",
468
+ f"from {nodebpy_pkg}.builder import {', '.join(sorted(builder_names))}",
469
+ ]
470
+ if any("math." in s.default for s in cls.inputs):
471
+ import_lines.insert(0, "import math")
472
+ if input_types:
473
+ import_lines.append(f"from {nodebpy_pkg}.types import {', '.join(input_types)}")
474
+
475
+ return (
476
+ GroupInterface(
477
+ docstring=docstring, body=body, base=base, attr_lines=attr_lines
478
+ ),
479
+ import_lines,
480
+ )
481
+
482
+
373
483
  def _render_module(
374
484
  classes: list[_AssetClass],
375
485
  nodebpy_pkg: str = "nodebpy",
@@ -399,6 +509,9 @@ def _render_module(
399
509
 
400
510
  lines = [
401
511
  "# Auto-generated by nodebpy.assets.generate_asset_api — do not edit manually.",
512
+ "import math"
513
+ if any("math." in s.default for c in classes for s in c.inputs)
514
+ else "",
402
515
  f"from typing import {', '.join(typing_imports)}",
403
516
  "",
404
517
  f"from {nodebpy_pkg}.builder import (\n {',\n '.join(builder_imports)},\n)",