nodebpy 520.17.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.17.0 → nodebpy-520.18.0}/PKG-INFO +1 -1
  2. {nodebpy-520.17.0 → nodebpy-520.18.0}/pyproject.toml +1 -1
  3. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/assets/_codegen.py +5 -5
  4. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/assets/_library.py +7 -6
  5. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/node.py +1 -1
  6. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/export/codegen.py +52 -24
  7. {nodebpy-520.17.0 → nodebpy-520.18.0}/README.md +0 -0
  8. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/__init__.py +0 -0
  9. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/assets/__init__.py +0 -0
  10. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/assets/__main__.py +0 -0
  11. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/__init__.py +0 -0
  12. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/_registry.py +0 -0
  13. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/_utils.py +0 -0
  14. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/accessor.py +0 -0
  15. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/arrange.py +0 -0
  16. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/asset.py +0 -0
  17. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/items.py +0 -0
  18. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/mixins.py +0 -0
  19. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/socket.py +0 -0
  20. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/builder/tree.py +0 -0
  21. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/export/__init__.py +0 -0
  22. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/export/diagram.py +0 -0
  23. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/export/parity.py +0 -0
  24. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/export/web_render.py +0 -0
  25. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  26. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  27. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  28. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  29. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  30. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  31. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  32. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  33. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  34. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  35. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  36. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  37. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  38. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/__init__.py +0 -0
  39. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/_mixins.py +0 -0
  40. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  41. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/assets.py +0 -0
  42. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  43. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  44. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  45. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  46. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  47. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  48. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  49. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  50. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  51. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  52. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  53. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  54. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/assets.py +0 -0
  55. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  56. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  57. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  58. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  59. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  60. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  61. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  62. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  63. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  64. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  65. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  66. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  67. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  68. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  69. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  70. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  71. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/assets.py +0 -0
  72. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/color.py +0 -0
  73. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  74. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  75. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/group.py +0 -0
  76. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/input.py +0 -0
  77. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  78. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/output.py +0 -0
  79. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/script.py +0 -0
  80. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  81. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  82. {nodebpy-520.17.0 → nodebpy-520.18.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  83. {nodebpy-520.17.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.17.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.17.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 = [
@@ -290,7 +290,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
290
290
  def _library_source(library: AssetLibrary) -> str:
291
291
  """Source expression that reconstructs ``library`` in the generated module."""
292
292
  if isinstance(library, BundledLibrary):
293
- return f"BundledLibrary({library.filename!r})"
293
+ return f"BundledLibrary({_fmt(library.filename)})"
294
294
  from ..builder import PackageLibrary
295
295
 
296
296
  if isinstance(library, PackageLibrary):
@@ -298,7 +298,7 @@ def _library_source(library: AssetLibrary) -> str:
298
298
  # so the generated module imports cleanly and stays cross-platform even
299
299
  # when ``relative`` was passed as a ``Path``.
300
300
  relative = Path(library.relative).as_posix()
301
- return f"PackageLibrary(__file__, {relative!r})"
301
+ return f"PackageLibrary(__file__, {_fmt(relative)})"
302
302
  raise TypeError(f"Cannot serialise asset library: {library!r}")
303
303
 
304
304
 
@@ -355,8 +355,8 @@ def _render_class(cls: _AssetClass, docstrings: bool = False) -> str:
355
355
  return f"""class {cls.class_name}({base}):
356
356
  {docstring}
357
357
 
358
- _name = {cls.asset_name!r}
359
- _asset_name = {cls.asset_name!r}
358
+ _name = {_fmt(cls.asset_name)}
359
+ _asset_name = {_fmt(cls.asset_name)}
360
360
  _library = {cls.library_source}
361
361
 
362
362
  {inputs_cls}
@@ -451,7 +451,7 @@ def interface_parts(
451
451
  if library_source:
452
452
  base = asset_group_base(cls.tree_idname).__name__
453
453
  attr_lines = [
454
- f"_asset_name = {cls.asset_name!r}",
454
+ f"_asset_name = {_fmt(cls.asset_name)}",
455
455
  f"_library = {library_source}",
456
456
  ]
457
457
 
@@ -74,6 +74,7 @@ from ..export.codegen import (
74
74
  _ID_COLLECTIONS,
75
75
  GroupInterface,
76
76
  _class_name,
77
+ _fmt,
77
78
  _format_with_ruff,
78
79
  to_python,
79
80
  )
@@ -215,7 +216,7 @@ def _id_defaults(tree) -> dict[str, set[str]]:
215
216
 
216
217
  def _dict_lines(name: str, data: dict[str, object]) -> list[str]:
217
218
  lines = [f"{name} = {{"]
218
- lines += [f" {key!r}: {value!r}," for key, value in data.items()]
219
+ lines += [f" {_fmt(key)}: {_fmt(value)}," for key, value in data.items()]
219
220
  lines.append("}")
220
221
  return lines
221
222
 
@@ -336,9 +337,9 @@ def _render_group_module(
336
337
  # Deliberately no source filename/timestamp in the header: a dump must be
337
338
  # byte-identical across a no-op round-trip so it leaves no VCS diff.
338
339
  label = {
339
- "asset": f"Node-group asset {group.name!r} ({group.bl_idname})",
340
- "shared": f"Node group {group.name!r} ({group.bl_idname})",
341
- "material": f"Material {material_name!r}",
340
+ "asset": f'Node-group asset "{group.name}" ({group.bl_idname})',
341
+ "shared": f'Node group "{group.name}" ({group.bl_idname})',
342
+ "material": f'Material "{material_name}"',
342
343
  }[kind]
343
344
  header = [
344
345
  f"# {label}, dumped by nodebpy.assets.dump_library.",
@@ -375,7 +376,7 @@ def _render_group_module(
375
376
  "",
376
377
  "",
377
378
  f"MATERIAL = {class_name}",
378
- f"MATERIAL_NAME = {material_name!r}",
379
+ f"MATERIAL_NAME = {_fmt(material_name)}",
379
380
  ]
380
381
  material = bpy.data.materials[material_name]
381
382
  props = _material_properties(material)
@@ -773,7 +774,7 @@ def _dump_appended(
773
774
  if kind == "asset":
774
775
  module_dir = (output_dir / module).parent
775
776
  relpath = Path(os.path.relpath(library_blend, module_dir)).as_posix()
776
- library_source = f"PackageLibrary(__file__, {relpath!r})"
777
+ library_source = f"PackageLibrary(__file__, {_fmt(relpath)})"
777
778
  root_interface, iface_imports = interface_parts(
778
779
  group, library_source=library_source, nodebpy_pkg=nodebpy_pkg
779
780
  )
@@ -455,7 +455,7 @@ class NodeGroupBuilder[T: bpy.types.NodeTree](BaseNode, ABC):
455
455
  ...
456
456
 
457
457
  @abstractmethod
458
- def _build_group(self, tree: TreeBuilder) -> None:
458
+ def _build_group(self, tree: TreeBuilder[T]) -> None:
459
459
  """Build the node group internals and interface."""
460
460
 
461
461
  @classmethod
@@ -485,9 +485,12 @@ def _fmt(value: Any) -> str:
485
485
  # Vectors / sequences
486
486
  try:
487
487
  items = list(value)
488
- return f"({', '.join(_fmt(v) for v in items)})"
489
488
  except TypeError:
490
489
  return repr(value)
490
+ if len(items) == 1:
491
+ # The trailing comma keeps a one-element tuple a tuple.
492
+ return f"({_fmt(items[0])},)"
493
+ return f"({', '.join(_fmt(v) for v in items)})"
491
494
 
492
495
 
493
496
  def _eq(a: Any, b: Any) -> bool:
@@ -3652,8 +3655,10 @@ def _emit_interface_lines(node_tree, ctx: EmitContext) -> list[str]:
3652
3655
 
3653
3656
 
3654
3657
  def _format_with_ruff(code: str) -> str:
3655
- """Format ``code`` with ruff if the optional ``ruff`` package is installed,
3656
- otherwise return it unchanged.
3658
+ """Run ``code`` through ``ruff check --fix-only`` and ``ruff format`` if
3659
+ the optional ``ruff`` package is installed, otherwise return it unchanged
3660
+ — so generated files come out already fixed and formatted rather than
3661
+ needing a lint pass afterwards.
3657
3662
 
3658
3663
  Uses the binary bundled with the ``ruff`` Python package (via
3659
3664
  ``ruff.find_ruff_bin``), so it works wherever the package is importable —
@@ -3665,20 +3670,24 @@ def _format_with_ruff(code: str) -> str:
3665
3670
  return code
3666
3671
  import subprocess
3667
3672
 
3668
- try:
3669
- result = subprocess.run(
3670
- [find_ruff_bin(), "format", "-"],
3671
- input=code,
3672
- capture_output=True,
3673
- # Explicit UTF-8: text mode alone uses the locale encoding, and on
3674
- # Windows (cp1252) any non-ASCII character in the source reaches
3675
- # ruff as invalid UTF-8 it errors and the code stays unformatted.
3676
- encoding="utf-8",
3677
- check=True,
3678
- )
3679
- except (OSError, subprocess.SubprocessError):
3680
- return code
3681
- return result.stdout or code
3673
+ ruff = find_ruff_bin()
3674
+ for command in (["check", "--fix-only", "-"], ["format", "-"]):
3675
+ try:
3676
+ result = subprocess.run(
3677
+ [ruff, *command],
3678
+ input=code,
3679
+ capture_output=True,
3680
+ # Explicit UTF-8: text mode alone uses the locale encoding,
3681
+ # and on Windows (cp1252) any non-ASCII character in the
3682
+ # source reaches ruff as invalid UTF-8 — it errors and the
3683
+ # code stays unformatted.
3684
+ encoding="utf-8",
3685
+ check=True,
3686
+ )
3687
+ except (OSError, subprocess.SubprocessError): # pragma: no cover
3688
+ return code
3689
+ code = result.stdout or code
3690
+ return code
3682
3691
 
3683
3692
 
3684
3693
  def to_python(
@@ -3811,10 +3820,17 @@ def to_python(
3811
3820
  for alias in ("g", "s", "c")
3812
3821
  if alias in used_aliases
3813
3822
  ]
3814
- # TreeBuilder is only referenced by the `with` block; class bodies use the
3815
- # ``tree`` parameter of ``_build_group`` instead.
3816
- import_names = import_parts + (["TreeBuilder"] if top_level == "with" else [])
3823
+ # TreeBuilder anchors the `with` block and annotates every generated
3824
+ # ``_build_group(self, tree: TreeBuilder)``, so editors type the whole
3825
+ # body of a dumped class.
3826
+ import_names = import_parts + ["TreeBuilder"]
3817
3827
  lines: list[str] = []
3828
+ if collector.tree_param_types:
3829
+ # The ``tree: TreeBuilder[GeometryNodeTree]`` annotations on emitted
3830
+ # classes reference the bpy tree types by name.
3831
+ lines.append(
3832
+ "from bpy.types import " + ", ".join(sorted(collector.tree_param_types))
3833
+ )
3818
3834
  if import_names:
3819
3835
  lines.append(f"from {nodebpy_pkg} import " + ", ".join(import_names))
3820
3836
  if collector.bases_used:
@@ -3850,9 +3866,9 @@ def to_python(
3850
3866
  lines.append("")
3851
3867
  lines.extend(_node_positions_lines(node_tree, indent=""))
3852
3868
 
3853
- # Datablock defaults (a Material/Object/Image socket value) render via
3854
- # ``repr()`` as ``bpy.data.<collection>['name']``, so the module needs a
3855
- # bare ``import bpy`` to resolve them on rebuild.
3869
+ # Datablock defaults (a Material/Object/Image socket value) render as
3870
+ # guarded ``bpy.data.<collection>.get("name")`` lookups, so the module
3871
+ # needs a bare ``import bpy`` to resolve them on rebuild.
3856
3872
  # Math-constant expressions (from _fmt_float) need the stdlib import.
3857
3873
  if any(re.search(r"\bmath\.(pi|tau|e)\b", line) for line in lines):
3858
3874
  lines.insert(0, "import math")
@@ -4035,6 +4051,8 @@ class _GroupCollector:
4035
4051
  used_names: set[str] = field(default_factory=set)
4036
4052
  bases_used: set[str] = field(default_factory=set)
4037
4053
  used_aliases: set[str] = field(default_factory=set)
4054
+ # bpy.types names the emitted ``tree: TreeBuilder[...]`` annotations need.
4055
+ tree_param_types: set[str] = field(default_factory=set)
4038
4056
 
4039
4057
  def register(self, node_tree) -> str:
4040
4058
  """Ensure a class exists for ``node_tree`` and return its name."""
@@ -4057,6 +4075,10 @@ class _GroupCollector:
4057
4075
  if interface is not None and interface.base:
4058
4076
  base = interface.base
4059
4077
  self.bases_used.add(base)
4078
+ if node_tree.bl_idname in _GROUP_BASE_FOR_TREE:
4079
+ # A tree's bl_idname doubles as its bpy.types class name, which
4080
+ # parameterizes the emitted ``tree: TreeBuilder[...]`` annotation.
4081
+ self.tree_param_types.add(node_tree.bl_idname)
4060
4082
  self.class_defs.append(
4061
4083
  _render_group_class(
4062
4084
  class_name,
@@ -4312,7 +4334,13 @@ def _render_group_class(
4312
4334
  header.append(f" _tree_properties = {{{rendered}}}")
4313
4335
  if interface is not None:
4314
4336
  header.extend(["", interface.body])
4315
- header.extend(["", " def _build_group(self, tree):"])
4337
+ # The tree's bl_idname doubles as its bpy.types class name.
4338
+ annotation = (
4339
+ f"TreeBuilder[{node_tree.bl_idname}]"
4340
+ if node_tree.bl_idname in _GROUP_BASE_FOR_TREE
4341
+ else "TreeBuilder"
4342
+ )
4343
+ header.extend(["", f" def _build_group(self, tree: {annotation}) -> None:"])
4316
4344
  inner = _assemble_tree_body(emission)
4317
4345
  # Either option needs auto-layout off (it dissolves reroutes and overwrites
4318
4346
  # locations); the disable line goes at the body's "in-block" 4-space indent,
File without changes