Skip to content

Commit 1e631b6

Browse files
Fix: Issue with arrange() import of networkx (#101)
* run test set without networkx * fix macos name * fix networkx removal * tests + fix for networkx missing * udpate changelog and version number * test for grid_positions
1 parent 7acaaa0 commit 1e631b6

8 files changed

Lines changed: 192 additions & 69 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: ["*"]
88

99
jobs:
10-
build:
10+
test:
1111
runs-on: ${{ matrix.os }}
1212
strategy:
1313
max-parallel: 4

docs/changelog.qmd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
---
22
title: Changelog
33
---
4+
5+
## v520.0.1 - 2026-06-05
6+
7+
### Enhancements
8+
### Fixes
9+
- Import and usage of the `arrange()` function properly handles the optional `netowrkx` dependency
10+
411
## v520.0.0 - 2026-06-04
512

613
### Enhancements

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "nodebpy"
3-
version = "520.0.0"
3+
version = "520.0.1"
44
description = "Build nodes trees in Blender more elegantly with code"
55
readme = "README.md"
66
authors = [
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# SPDX-License-Identifier: GPL-2.0-or-later

tests/__snapshots__/test_usecases.ambr

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@
6868
```
6969
'''
7070
# ---
71+
# name: test_active_grid_positions
72+
'''
73+
```{mermaid}
74+
graph LR
75+
N0("Group Input"):::default-node
76+
N1("Grid to Points"):::geometry-node
77+
N2("Combine XYZ"):::converter-node
78+
N3("Store Named Attribute"):::default-node
79+
N4("Store Named Attribute"):::default-node
80+
N5("Delete Geometry"):::geometry-node
81+
N6("Group Output"):::default-node
82+
N0 -->|"Grid->Grid"| N1
83+
N1 -->|"X->X"| N2
84+
N1 -->|"Y->Y"| N2
85+
N1 -->|"Z->Z"| N2
86+
N2 -->|"Vector->Value"| N3
87+
N1 -->|"Points->Geometry"| N3
88+
N1 -->|"Value->Value"| N4
89+
N3 -->|"Geometry->Geometry"| N4
90+
N1 -->|"Is Tile->Selection"| N5
91+
N4 -->|"Geometry->Geometry"| N5
92+
N5 -->|"Geometry->Points"| N6
93+
```
94+
'''
95+
# ---
7196
# name: test_bundle_path_filter
7297
'''
7398
```{mermaid}

tests/test_arrange_no_networkx.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Regression tests for building node trees when networkx is unavailable.
2+
3+
When nodebpy is vendored into a Blender extension, the optional ``networkx``
4+
dependency is frequently absent. The Sugiyama layout should then fall back to
5+
the simple arrangement instead of crashing.
6+
7+
The subtle failure mode this guards against is order-dependent: the *first*
8+
arrange attempt raises a clean ``ImportError`` (which is caught and falls back),
9+
but a *second* attempt used to surface a raw ``KeyError`` from the namespace
10+
package machinery, escaping the ``except ImportError`` guard. So we must build
11+
more than one tree in the same process to exercise the real bug.
12+
"""
13+
14+
import sys
15+
16+
import pytest
17+
18+
from nodebpy import TreeBuilder
19+
from nodebpy import geometry as g
20+
21+
22+
@pytest.fixture
23+
def no_networkx():
24+
"""Simulate a vendored install where networkx cannot be imported.
25+
26+
Setting ``sys.modules['networkx'] = None`` makes ``import networkx`` raise
27+
``ImportError``. We also evict the cached ``nodebpy.lib.nodearrange``
28+
modules so the import is genuinely re-attempted (rebuilding the namespace
29+
package path), matching the user's fresh-process scenario.
30+
"""
31+
blocked = "networkx"
32+
saved = {
33+
k: v
34+
for k, v in sys.modules.items()
35+
if k == blocked
36+
or k.startswith(blocked + ".")
37+
or k.startswith("nodebpy.lib.nodearrange")
38+
}
39+
for key in saved:
40+
del sys.modules[key]
41+
sys.modules[blocked] = None # force ImportError on `import networkx`
42+
try:
43+
yield
44+
finally:
45+
del sys.modules[blocked]
46+
sys.modules.update(saved)
47+
48+
49+
def _build(name: str) -> TreeBuilder:
50+
with TreeBuilder.geometry(name) as tree: # default arrange="sugiyama"
51+
geo = tree.inputs.geometry()
52+
out = tree.outputs.geometry()
53+
_ = geo >> g.SetPosition() >> g.RealizeInstances() >> out
54+
return tree
55+
56+
57+
def test_fallback_without_networkx(no_networkx):
58+
"""Two sequential sugiyama builds must degrade gracefully without networkx."""
59+
# First tree: clean ImportError -> warns + falls back. This works today.
60+
with pytest.warns(UserWarning, match="networkx"):
61+
_build("FirstNoNX")
62+
63+
# Second tree: this is where the stale namespace path used to raise
64+
# KeyError: 'nodebpy.lib.nodearrange', escaping `except ImportError`.
65+
second = _build("SecondNoNX")
66+
67+
# The tree should still have been built and arranged (simple fallback).
68+
assert len(second.tree.nodes) > 0

tests/test_usecases.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,3 +1024,24 @@ def test_geometryscript_city_builder(snapshot):
10241024
g.JoinGeometry((curve_mesh, buildings)) >> tree.outputs.geometry("Result")
10251025

10261026
assert snapshot == tree._repr_markdown_()
1027+
1028+
1029+
def test_active_grid_positions(snapshot):
1030+
with g.tree("Active Grid Positions", arrange='simple') as tree:
1031+
tree.tree.show_modifier_manage_panel = True
1032+
1033+
grid = tree.inputs.float("Grid", hide_value=True, structure_type="GRID")
1034+
points_output = tree.outputs.geometry("Points")
1035+
1036+
points = g.GridToPoints.float(grid)
1037+
indices = g.CombineXYZ(points.o.x, points.o.y, points.o.z).o.vector
1038+
1039+
(
1040+
points.o.points
1041+
>> g.StoreNamedAttribute.point.vector(name="ix", value=indices)
1042+
>> g.StoreNamedAttribute.point.boolean(name="value", value=points.o.value)
1043+
>> g.DeleteGeometry(selection=points.o.is_tile)
1044+
>> points_output
1045+
)
1046+
1047+
assert snapshot == tree._repr_markdown_()

0 commit comments

Comments
 (0)