funcnodes-core 2.3.2__tar.gz → 2.4.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 (96) hide show
  1. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/CHANGELOG.md +11 -0
  2. funcnodes_core-2.4.0/CONTRIBUTING.md +49 -0
  3. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/PKG-INFO +26 -3
  4. funcnodes_core-2.4.0/README.md +25 -0
  5. funcnodes_core-2.4.0/context7.json +4 -0
  6. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/pyproject.toml +1 -1
  7. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/io.py +11 -5
  8. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_config.py +1 -1
  9. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_nodeclass.py +174 -0
  10. funcnodes_core-2.4.0/tests/test_trigger_propagation_matrix.py +353 -0
  11. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/uv.lock +1 -1
  12. funcnodes_core-2.3.2/README.md +0 -2
  13. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.coveragerc +0 -0
  14. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.flake8 +0 -0
  15. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.github/actions/install_package/action.yml +0 -0
  16. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.github/workflows/py_test.yml +0 -0
  17. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.github/workflows/version_publish_main.yml +0 -0
  18. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.gitignore +0 -0
  19. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/.pre-commit-config.yaml +0 -0
  20. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/LICENSE +0 -0
  21. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/MANIFEST.in +0 -0
  22. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/THIRD_PARTY_NOTICES.md +0 -0
  23. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/conftest.py +0 -0
  24. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/cz.toml +0 -0
  25. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/pytest.ini +0 -0
  26. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/__init__.py +0 -0
  27. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/_logging.py +0 -0
  28. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/_setup.py +0 -0
  29. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/config.py +0 -0
  30. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/data.py +0 -0
  31. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/datapath.py +0 -0
  32. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/decorator/__init__.py +0 -0
  33. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/eventmanager.py +0 -0
  34. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/exceptions.py +0 -0
  35. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/graph.py +0 -0
  36. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/grouping_logic.py +0 -0
  37. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/lib/__init__.py +0 -0
  38. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/lib/lib.py +0 -0
  39. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/lib/libfinder.py +0 -0
  40. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/lib/libparser.py +0 -0
  41. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/node.py +0 -0
  42. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/nodemaker.py +0 -0
  43. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/nodespace.py +0 -0
  44. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/testing.py +0 -0
  45. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/triggerstack.py +0 -0
  46. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/__init__.py +0 -0
  47. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/cache.py +0 -0
  48. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/data.py +0 -0
  49. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/deprecations.py +0 -0
  50. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/files.py +0 -0
  51. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/functions.py +0 -0
  52. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/modules.py +0 -0
  53. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/nodetqdm.py +0 -0
  54. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/nodeutils.py +0 -0
  55. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/plugins.py +0 -0
  56. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/plugins_types.py +0 -0
  57. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/saving.py +0 -0
  58. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/serialization.py +0 -0
  59. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/special_types.py +0 -0
  60. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/src/funcnodes_core/utils/wrapper.py +0 -0
  61. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/__init__.py +0 -0
  62. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/conftest.py +0 -0
  63. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_cache_utils.py +0 -0
  64. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_data.py +0 -0
  65. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_datapaths.py +0 -0
  66. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_decorator.py +0 -0
  67. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_eventmanager.py +0 -0
  68. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_exceptions.py +0 -0
  69. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_functions.py +0 -0
  70. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_graph.py +0 -0
  71. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_grouping_logic.py +0 -0
  72. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_lib.py +0 -0
  73. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_libfinder.py +0 -0
  74. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_nodeclassmixin.py +0 -0
  75. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_nodeio.py +0 -0
  76. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_nodemaker.py +0 -0
  77. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_nodespace.py +0 -0
  78. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_nodeutils.py +0 -0
  79. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_public_api.py +0 -0
  80. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_setup.py +0 -0
  81. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_triggering.py +0 -0
  82. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_triggerstack.py +0 -0
  83. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/__init__.py +0 -0
  84. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_datautils.py +0 -0
  85. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_deprecations.py +0 -0
  86. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_files.py +0 -0
  87. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_logging.py +0 -0
  88. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_modules.py +0 -0
  89. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_nodetqdm.py +0 -0
  90. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_plugins.py +0 -0
  91. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_plugins_types.py +0 -0
  92. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_saving.py +0 -0
  93. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_serialization.py +0 -0
  94. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_special_types.py +0 -0
  95. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_testing.py +0 -0
  96. {funcnodes_core-2.3.2 → funcnodes_core-2.4.0}/tests/test_utils/test_wrapper.py +0 -0
@@ -1,3 +1,14 @@
1
+ ## v2.4.0 (2026-03-17)
2
+
3
+ ### Feat
4
+
5
+ - **context**: add context7 configuration file with URL and public key
6
+
7
+ ### Fix
8
+
9
+ - **io**: preserve trigger propagation semantics across node chains
10
+ - **tests**: correct typo in test function name for clarity
11
+
1
12
  ## v2.3.2 (2025-12-24)
2
13
 
3
14
  ### Fix
@@ -0,0 +1,49 @@
1
+ # Contributing to funcnodes-core
2
+
3
+ This repository contains the **core runtime** (nodes, IO, nodespace, library, config, serialization).
4
+
5
+ ## Development setup (Python)
6
+
7
+ Prereqs:
8
+
9
+ - Python **3.11+**
10
+ - `uv` (https://github.com/astral-sh/uv)
11
+
12
+ Recommended environment variables (keep caches/config local):
13
+
14
+ - `UV_CACHE_DIR=.cache/uv`
15
+ - `FUNCNODES_CONFIG_DIR=.funcnodes`
16
+
17
+ Install dev dependencies:
18
+
19
+ ```bash
20
+ cd funcnodes_core
21
+ UV_CACHE_DIR=.cache/uv uv sync --group dev
22
+ ```
23
+
24
+ Run tests:
25
+
26
+ ```bash
27
+ cd funcnodes_core
28
+ FUNCNODES_CONFIG_DIR=.funcnodes UV_CACHE_DIR=.cache/uv uv run pytest
29
+ ```
30
+
31
+ ## Code style & hooks
32
+
33
+ Run pre-commit:
34
+
35
+ ```bash
36
+ cd funcnodes_core
37
+ UV_CACHE_DIR=.cache/uv uv run pre-commit install
38
+ UV_CACHE_DIR=.cache/uv uv run pre-commit run -a
39
+ ```
40
+
41
+ ## TDD expectations
42
+
43
+ - Write tests first; add edge cases as separate tests.
44
+ - Avoid mocks unless simulating external resources.
45
+
46
+ ## Pull requests
47
+
48
+ - Work on a feature branch (direct commits to `main`/`master`/`test` are blocked by pre-commit).
49
+ - Keep changes scoped: core is widely used across FuncNodes packages.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcnodes-core
3
- Version: 2.3.2
3
+ Version: 2.4.0
4
4
  Summary: core package for funcnodes
5
5
  Project-URL: homepage, https://github.com/Linkdlab/funcnodes_core
6
6
  Project-URL: source, https://github.com/Linkdlab/funcnodes_core
@@ -37,5 +37,28 @@ Requires-Dist: vulture>=2.14; extra == 'dev'
37
37
  Requires-Dist: yappi>=1.6.10; extra == 'dev'
38
38
  Description-Content-Type: text/markdown
39
39
 
40
- Core package for funcnodes.
41
- for detailed instructions go to the funcnodes repo
40
+ # FuncNodes Core
41
+
42
+ This is the core package for **FuncNodes**, a flexible and modular framework for building and managing computational graphs.
43
+
44
+ `funcnodes_core` contains the fundamental logic for Nodes, NodeSpaces, and the event-driven architecture that powers the system.
45
+
46
+ > [!NOTE]
47
+ > If you are looking for the full application, including the web interface and worker management, please visit the main [FuncNodes repository](https://github.com/Linkdlab/FuncNodes) or install the `funcnodes` package.
48
+
49
+ ## Installation
50
+
51
+ If you are developing a custom implementation or only need the core logic:
52
+
53
+ ```bash
54
+ pip install funcnodes-core
55
+ ```
56
+
57
+ For the full experience:
58
+ ```bash
59
+ pip install funcnodes
60
+ ```
61
+
62
+ ## Documentation
63
+
64
+ For detailed instructions and documentation, please visit the [FuncNodes Documentation](https://linkdlab.github.io/FuncNodes).
@@ -0,0 +1,25 @@
1
+ # FuncNodes Core
2
+
3
+ This is the core package for **FuncNodes**, a flexible and modular framework for building and managing computational graphs.
4
+
5
+ `funcnodes_core` contains the fundamental logic for Nodes, NodeSpaces, and the event-driven architecture that powers the system.
6
+
7
+ > [!NOTE]
8
+ > If you are looking for the full application, including the web interface and worker management, please visit the main [FuncNodes repository](https://github.com/Linkdlab/FuncNodes) or install the `funcnodes` package.
9
+
10
+ ## Installation
11
+
12
+ If you are developing a custom implementation or only need the core logic:
13
+
14
+ ```bash
15
+ pip install funcnodes-core
16
+ ```
17
+
18
+ For the full experience:
19
+ ```bash
20
+ pip install funcnodes
21
+ ```
22
+
23
+ ## Documentation
24
+
25
+ For detailed instructions and documentation, please visit the [FuncNodes Documentation](https://linkdlab.github.io/FuncNodes).
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "https://context7.com/linkdlab/funcnodes_core",
3
+ "public_key": "pk_jr5hlTznUKh2bFkliKxXR"
4
+ }
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "funcnodes-core"
3
3
 
4
- version = "2.3.2"
4
+ version = "2.4.0"
5
5
 
6
6
  description = "core package for funcnodes"
7
7
  authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}]
@@ -1039,10 +1039,11 @@ class NodeInput(NodeIO, Generic[NodeIOType]):
1039
1039
 
1040
1040
  self.datapath = new_datapath
1041
1041
 
1042
+ resolved_does_trigger = does_trigger
1042
1043
  if self.node is not None:
1043
- if does_trigger is None:
1044
- does_trigger = self.does_trigger
1045
- if does_trigger:
1044
+ if resolved_does_trigger is None:
1045
+ resolved_does_trigger = self.does_trigger
1046
+ if resolved_does_trigger:
1046
1047
  self.node.request_trigger()
1047
1048
 
1048
1049
  for other in self._forwards:
@@ -1153,7 +1154,7 @@ class NodeInput(NodeIO, Generic[NodeIOType]):
1153
1154
  self._forwards.add(other)
1154
1155
  other.forwards_from(self, replace=replace)
1155
1156
 
1156
- other.set_value(self.value)
1157
+ other.set_value(self.value, does_trigger=False)
1157
1158
 
1158
1159
  return [
1159
1160
  self.node.uuid if self.node else None,
@@ -1304,6 +1305,7 @@ class NodeOutput(NodeIO):
1304
1305
  else:
1305
1306
  datapath = None
1306
1307
  # input_paths = []
1308
+ propagated_does_trigger = False if does_trigger is False else None
1307
1309
  for other in self.connections:
1308
1310
  # if self.node is not None:
1309
1311
  # for input_path in input_paths:
@@ -1311,7 +1313,11 @@ class NodeOutput(NodeIO):
1311
1313
  # else:
1312
1314
  # datapath = None
1313
1315
 
1314
- other.set_value(value, does_trigger=does_trigger, datapath=datapath)
1316
+ other.set_value(
1317
+ value,
1318
+ does_trigger=propagated_does_trigger,
1319
+ datapath=datapath,
1320
+ )
1315
1321
 
1316
1322
  def post_connect(self, other: NodeIO):
1317
1323
  """Called after a connection is made.
@@ -42,7 +42,7 @@ def test_no_deprecation_warning():
42
42
  warnings.simplefilter("error", DeprecationWarning)
43
43
 
44
44
 
45
- def test_config_not_laoded():
45
+ def test_config_not_loaded():
46
46
  try:
47
47
  assert not fn.config._CONFIG_CHANGED, "Expected _CONFIG_CHANGED to be False"
48
48
 
@@ -37,6 +37,25 @@ class DummyNode(Node):
37
37
  return input
38
38
 
39
39
 
40
+ def make_counter_node(node_id: str, *, does_trigger: bool = True):
41
+ resolved_node_id = node_id
42
+
43
+ class CounterNode(Node):
44
+ node_id = resolved_node_id
45
+ value = NodeInput(id="value", type=int, does_trigger=does_trigger)
46
+ output = NodeOutput(id="output", type=int)
47
+
48
+ def __init__(self, *args, **kwargs):
49
+ super().__init__(*args, pretrigger_delay=0.0, **kwargs)
50
+ self.call_count = 0
51
+
52
+ async def func(self, value: int):
53
+ self.call_count += 1
54
+ self.outputs["output"].value = value
55
+
56
+ return CounterNode
57
+
58
+
40
59
  @funcnodes_test
41
60
  async def test_nodeclass_initialization():
42
61
  with pytest.raises(TypeError):
@@ -128,6 +147,161 @@ async def test_trigger_stack():
128
147
  assert not trigger_stack.done()
129
148
 
130
149
 
150
+ @funcnodes_test
151
+ async def test_forwarded_input_respects_target_does_trigger_flag():
152
+ ForwardSourceNode = make_counter_node("forward_source_node_test")
153
+ ForwardTargetNode = make_counter_node(
154
+ "forward_target_node_test", does_trigger=False
155
+ )
156
+
157
+ node_a = ForwardSourceNode()
158
+ node_b = ForwardTargetNode()
159
+ node_a.inputs["value"].connect(node_b.inputs["value"])
160
+
161
+ node_a.inputs["value"].value = 42
162
+ await fn.run_until_complete(node_a, node_b)
163
+
164
+ assert node_a.inputs["value"].value == 42
165
+ assert node_b.inputs["value"].value == 42
166
+ assert node_a.call_count == 1
167
+ assert node_b.call_count == 0
168
+ assert node_a.outputs["output"].value == 42
169
+ assert node_b.outputs["output"].value is fn.NoValue
170
+
171
+
172
+ @funcnodes_test
173
+ async def test_forwarded_input_uses_target_flag_when_source_flag_disables_self_only():
174
+ ForwardSourceNode = make_counter_node(
175
+ "forward_source_node_no_trigger_test", does_trigger=False
176
+ )
177
+ ForwardTargetNode = make_counter_node("forward_target_node_allow_test")
178
+
179
+ node_a = ForwardSourceNode()
180
+ node_b = ForwardTargetNode()
181
+ node_a.inputs["value"].connect(node_b.inputs["value"])
182
+
183
+ node_a.inputs["value"].value = 42
184
+ await fn.run_until_complete(node_a, node_b)
185
+
186
+ assert node_a.call_count == 0
187
+ assert node_b.call_count == 1
188
+ assert node_b.inputs["value"].value == 42
189
+ assert node_b.outputs["output"].value == 42
190
+
191
+
192
+ @funcnodes_test
193
+ async def test_forwarded_input_propagates_explicit_false_downstream():
194
+ ForwardSourceNode = make_counter_node("forward_source_node_explicit_false_test")
195
+ ForwardTargetNode = make_counter_node("forward_target_node_explicit_false_test")
196
+
197
+ node_a = ForwardSourceNode()
198
+ node_b = ForwardTargetNode()
199
+ node_a.inputs["value"].connect(node_b.inputs["value"])
200
+
201
+ node_a.inputs["value"].set_value(42, does_trigger=False)
202
+ await fn.run_until_complete(node_a, node_b)
203
+
204
+ assert node_a.call_count == 0
205
+ assert node_b.call_count == 0
206
+ assert node_b.inputs["value"].value == 42
207
+ assert node_b.outputs["output"].value is fn.NoValue
208
+
209
+
210
+ @funcnodes_test
211
+ async def test_forwarded_input_triggers_downstream_when_both_allow_triggering():
212
+ ForwardSourceNode = make_counter_node("forward_source_node_trigger_test")
213
+ ForwardTargetNode = make_counter_node("forward_target_node_trigger_test")
214
+
215
+ node_a = ForwardSourceNode()
216
+ node_b = ForwardTargetNode()
217
+ node_a.inputs["value"].connect(node_b.inputs["value"])
218
+
219
+ node_a.inputs["value"].value = 42
220
+ await fn.run_until_complete(node_a, node_b)
221
+
222
+ assert node_a.call_count == 1
223
+ assert node_b.call_count == 1
224
+ assert node_b.outputs["output"].value == 42
225
+
226
+
227
+ @funcnodes_test
228
+ async def test_forward_chain_skips_non_triggering_middle_node_but_triggers_downstream():
229
+ NodeA = make_counter_node("forward_chain_source_node_test")
230
+ NodeB = make_counter_node("forward_chain_middle_node_test", does_trigger=False)
231
+ NodeC = make_counter_node("forward_chain_target_node_test")
232
+
233
+ node_a = NodeA()
234
+ node_b = NodeB()
235
+ node_c = NodeC()
236
+
237
+ node_a.inputs["value"].connect(node_b.inputs["value"])
238
+ node_b.inputs["value"].connect(node_c.inputs["value"])
239
+
240
+ node_a.inputs["value"].value = 42
241
+ await fn.run_until_complete(node_a, node_b, node_c)
242
+
243
+ assert node_a.call_count == 1
244
+ assert node_b.call_count == 0
245
+ assert node_c.call_count == 1
246
+ assert node_b.inputs["value"].value == 42
247
+ assert node_c.inputs["value"].value == 42
248
+ assert node_b.outputs["output"].value is fn.NoValue
249
+ assert node_c.outputs["output"].value == 42
250
+
251
+
252
+ @funcnodes_test
253
+ async def test_output_set_value_respects_explicit_false_downstream():
254
+ SourceNode = make_counter_node("output_source_node_explicit_false_test")
255
+ TargetNode = make_counter_node("output_target_node_explicit_false_test")
256
+
257
+ node_a = SourceNode()
258
+ node_b = TargetNode()
259
+ node_a.outputs["output"].connect(node_b.inputs["value"])
260
+
261
+ node_a.outputs["output"].set_value(42, does_trigger=False)
262
+ await fn.run_until_complete(node_a, node_b)
263
+
264
+ assert node_b.inputs["value"].value == 42
265
+ assert node_b.call_count == 0
266
+ assert node_b.outputs["output"].value is fn.NoValue
267
+
268
+
269
+ @funcnodes_test
270
+ async def test_output_set_value_uses_target_does_trigger_flag_when_unspecified():
271
+ SourceNode = make_counter_node("output_source_node_unspecified_test")
272
+ TargetNode = make_counter_node(
273
+ "output_target_node_unspecified_false_test", does_trigger=False
274
+ )
275
+
276
+ node_a = SourceNode()
277
+ node_b = TargetNode()
278
+ node_a.outputs["output"].connect(node_b.inputs["value"])
279
+
280
+ node_a.outputs["output"].set_value(42)
281
+ await fn.run_until_complete(node_a, node_b)
282
+
283
+ assert node_b.inputs["value"].value == 42
284
+ assert node_b.call_count == 0
285
+ assert node_b.outputs["output"].value is fn.NoValue
286
+
287
+
288
+ @funcnodes_test
289
+ async def test_output_set_value_triggers_target_when_target_allows_triggering():
290
+ SourceNode = make_counter_node("output_source_node_trigger_test")
291
+ TargetNode = make_counter_node("output_target_node_trigger_test")
292
+
293
+ node_a = SourceNode()
294
+ node_b = TargetNode()
295
+ node_a.outputs["output"].connect(node_b.inputs["value"])
296
+
297
+ node_a.outputs["output"].set_value(42)
298
+ await fn.run_until_complete(node_a, node_b)
299
+
300
+ assert node_b.inputs["value"].value == 42
301
+ assert node_b.call_count == 1
302
+ assert node_b.outputs["output"].value == 42
303
+
304
+
131
305
  @funcnodes_test
132
306
  def test_nodeclass_string():
133
307
  test_node = DummyNode(uuid="test_uuid")
@@ -0,0 +1,353 @@
1
+ import pytest
2
+
3
+ import funcnodes_core as fn
4
+ from funcnodes_core.node import Node, NodeInput, NodeOutput
5
+ from pytest_funcnodes import funcnodes_test
6
+
7
+
8
+ UNSPECIFIED = object()
9
+
10
+
11
+ def make_counter_node(node_id: str, *, does_trigger: bool = True):
12
+ resolved_node_id = node_id
13
+
14
+ class CounterNode(Node):
15
+ node_id = resolved_node_id
16
+ value = NodeInput(id="value", type=int, does_trigger=does_trigger)
17
+ output = NodeOutput(id="output", type=int)
18
+
19
+ def __init__(self, *args, **kwargs):
20
+ super().__init__(*args, pretrigger_delay=0.0, **kwargs)
21
+ self.call_count = 0
22
+
23
+ async def func(self, value: int):
24
+ self.call_count += 1
25
+ self.outputs["output"].value = value
26
+
27
+ return CounterNode
28
+
29
+
30
+ def build_nodes(scenario_name: str, node_flags: dict[str, bool]) -> dict[str, Node]:
31
+ nodes: dict[str, Node] = {}
32
+ for name, does_trigger in node_flags.items():
33
+ node_cls = make_counter_node(
34
+ f"trigger_matrix_{scenario_name.lower()}_{name.lower()}",
35
+ does_trigger=does_trigger,
36
+ )
37
+ nodes[name] = node_cls()
38
+ return nodes
39
+
40
+
41
+ def connect_nodes(
42
+ nodes: dict[str, Node], connections: list[tuple[str, str, str]]
43
+ ) -> None:
44
+ for connection_type, src, dst in connections:
45
+ if connection_type == "forward":
46
+ nodes[src].inputs["value"].connect(nodes[dst].inputs["value"])
47
+ elif connection_type == "output":
48
+ nodes[src].outputs["output"].connect(nodes[dst].inputs["value"])
49
+ else:
50
+ raise ValueError(f"Unknown connection type: {connection_type}")
51
+
52
+
53
+ def apply_action(nodes: dict[str, Node], action: dict[str, object]) -> None:
54
+ node = nodes[action["node"]] # type: ignore[index]
55
+ value = action["value"]
56
+ does_trigger = action.get("does_trigger", UNSPECIFIED)
57
+
58
+ if action["io"] == "input":
59
+ setter = node.inputs["value"].set_value
60
+ elif action["io"] == "output":
61
+ setter = node.outputs["output"].set_value
62
+ else:
63
+ raise ValueError(f"Unknown io type: {action['io']}")
64
+
65
+ if does_trigger is UNSPECIFIED:
66
+ setter(value)
67
+ else:
68
+ setter(value, does_trigger=does_trigger) # type: ignore[arg-type]
69
+
70
+
71
+ async def run_scenario(scenario: dict[str, object]) -> None:
72
+ nodes = build_nodes(scenario["name"], scenario["node_flags"]) # type: ignore[arg-type]
73
+ connect_nodes(nodes, scenario["connections"]) # type: ignore[arg-type]
74
+ apply_action(nodes, scenario["action"]) # type: ignore[arg-type]
75
+ await fn.run_until_complete(*nodes.values())
76
+
77
+ expected_counts = scenario["expected_counts"] # type: ignore[assignment]
78
+ for name, expected_count in expected_counts.items():
79
+ assert nodes[name].call_count == expected_count
80
+
81
+
82
+ INPUT_FORWARDING_SCENARIOS = [
83
+ {
84
+ "name": "input_forward_tt_unspecified",
85
+ "node_flags": {"A": True, "B": True},
86
+ "connections": [("forward", "A", "B")],
87
+ "action": {"node": "A", "io": "input", "value": 1},
88
+ "expected_counts": {"A": 1, "B": 1},
89
+ },
90
+ {
91
+ "name": "input_forward_ft_unspecified",
92
+ "node_flags": {"A": False, "B": True},
93
+ "connections": [("forward", "A", "B")],
94
+ "action": {"node": "A", "io": "input", "value": 1},
95
+ "expected_counts": {"A": 0, "B": 1},
96
+ },
97
+ {
98
+ "name": "input_forward_tf_unspecified",
99
+ "node_flags": {"A": True, "B": False},
100
+ "connections": [("forward", "A", "B")],
101
+ "action": {"node": "A", "io": "input", "value": 1},
102
+ "expected_counts": {"A": 1, "B": 0},
103
+ },
104
+ {
105
+ "name": "input_forward_ff_unspecified",
106
+ "node_flags": {"A": False, "B": False},
107
+ "connections": [("forward", "A", "B")],
108
+ "action": {"node": "A", "io": "input", "value": 1},
109
+ "expected_counts": {"A": 0, "B": 0},
110
+ },
111
+ {
112
+ "name": "input_forward_tt_explicit_false",
113
+ "node_flags": {"A": True, "B": True},
114
+ "connections": [("forward", "A", "B")],
115
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": False},
116
+ "expected_counts": {"A": 0, "B": 0},
117
+ },
118
+ {
119
+ "name": "input_forward_chain_tft_unspecified",
120
+ "node_flags": {"A": True, "B": False, "C": True},
121
+ "connections": [("forward", "A", "B"), ("forward", "B", "C")],
122
+ "action": {"node": "A", "io": "input", "value": 1},
123
+ "expected_counts": {"A": 1, "B": 0, "C": 1},
124
+ },
125
+ {
126
+ "name": "input_forward_chain_tft_explicit_false",
127
+ "node_flags": {"A": True, "B": False, "C": True},
128
+ "connections": [("forward", "A", "B"), ("forward", "B", "C")],
129
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": False},
130
+ "expected_counts": {"A": 0, "B": 0, "C": 0},
131
+ },
132
+ {
133
+ "name": "input_forward_chain_ftft_unspecified",
134
+ "node_flags": {"A": False, "B": True, "C": False, "D": True},
135
+ "connections": [
136
+ ("forward", "A", "B"),
137
+ ("forward", "B", "C"),
138
+ ("forward", "C", "D"),
139
+ ],
140
+ "action": {"node": "A", "io": "input", "value": 1},
141
+ "expected_counts": {"A": 0, "B": 1, "C": 0, "D": 1},
142
+ },
143
+ {
144
+ "name": "input_forward_fanout_t_tf_unspecified",
145
+ "node_flags": {"A": True, "B": True, "C": False},
146
+ "connections": [("forward", "A", "B"), ("forward", "A", "C")],
147
+ "action": {"node": "A", "io": "input", "value": 1},
148
+ "expected_counts": {"A": 1, "B": 1, "C": 0},
149
+ },
150
+ {
151
+ "name": "input_forward_fanout_t_tt_explicit_false",
152
+ "node_flags": {"A": True, "B": True, "C": True},
153
+ "connections": [("forward", "A", "B"), ("forward", "A", "C")],
154
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": False},
155
+ "expected_counts": {"A": 0, "B": 0, "C": 0},
156
+ },
157
+ {
158
+ "name": "input_forward_tt_explicit_true",
159
+ "node_flags": {"A": True, "B": True},
160
+ "connections": [("forward", "A", "B")],
161
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
162
+ "expected_counts": {"A": 1, "B": 1},
163
+ },
164
+ {
165
+ "name": "input_forward_tf_explicit_true",
166
+ "node_flags": {"A": True, "B": False},
167
+ "connections": [("forward", "A", "B")],
168
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
169
+ "expected_counts": {"A": 1, "B": 1},
170
+ },
171
+ {
172
+ "name": "input_forward_ft_explicit_true",
173
+ "node_flags": {"A": False, "B": True},
174
+ "connections": [("forward", "A", "B")],
175
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
176
+ "expected_counts": {"A": 1, "B": 1},
177
+ },
178
+ {
179
+ "name": "input_forward_ff_explicit_true",
180
+ "node_flags": {"A": False, "B": False},
181
+ "connections": [("forward", "A", "B")],
182
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
183
+ "expected_counts": {"A": 1, "B": 1},
184
+ },
185
+ {
186
+ "name": "input_forward_chain_tft_explicit_true",
187
+ "node_flags": {"A": True, "B": False, "C": True},
188
+ "connections": [("forward", "A", "B"), ("forward", "B", "C")],
189
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
190
+ "expected_counts": {"A": 1, "B": 1, "C": 1},
191
+ },
192
+ {
193
+ "name": "input_forward_chain_tff_explicit_true",
194
+ "node_flags": {"A": True, "B": False, "C": False},
195
+ "connections": [("forward", "A", "B"), ("forward", "B", "C")],
196
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
197
+ "expected_counts": {"A": 1, "B": 1, "C": 1},
198
+ },
199
+ {
200
+ "name": "input_forward_fanout_t_ft_explicit_true",
201
+ "node_flags": {"A": True, "B": False, "C": True},
202
+ "connections": [("forward", "A", "B"), ("forward", "A", "C")],
203
+ "action": {"node": "A", "io": "input", "value": 1, "does_trigger": True},
204
+ "expected_counts": {"A": 1, "B": 1, "C": 1},
205
+ },
206
+ {
207
+ "name": "input_forward_diamond",
208
+ "node_flags": {"A": True, "B": True, "C": True, "D": True, "E": False},
209
+ "connections": [
210
+ ("forward", "A", "B"),
211
+ ("forward", "A", "C"),
212
+ ("forward", "B", "D"),
213
+ ("forward", "C", "E"),
214
+ ],
215
+ "action": {"node": "A", "io": "input", "value": 1},
216
+ "expected_counts": {"A": 1, "B": 1, "C": 1, "D": 1, "E": 0},
217
+ },
218
+ ]
219
+
220
+
221
+ OUTPUT_PROPAGATION_SCENARIOS = [
222
+ {
223
+ "name": "output_to_input_t_unspecified",
224
+ "node_flags": {"A": True, "B": True},
225
+ "connections": [("output", "A", "B")],
226
+ "action": {"node": "A", "io": "output", "value": 1},
227
+ "expected_counts": {"A": 0, "B": 1},
228
+ },
229
+ {
230
+ "name": "output_to_input_f_unspecified",
231
+ "node_flags": {"A": True, "B": False},
232
+ "connections": [("output", "A", "B")],
233
+ "action": {"node": "A", "io": "output", "value": 1},
234
+ "expected_counts": {"A": 0, "B": 0},
235
+ },
236
+ {
237
+ "name": "output_to_input_t_explicit_false",
238
+ "node_flags": {"A": True, "B": True},
239
+ "connections": [("output", "A", "B")],
240
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": False},
241
+ "expected_counts": {"A": 0, "B": 0},
242
+ },
243
+ {
244
+ "name": "output_forward_chain_ft_unspecified",
245
+ "node_flags": {"A": True, "B": False, "C": True},
246
+ "connections": [("output", "A", "B"), ("forward", "B", "C")],
247
+ "action": {"node": "A", "io": "output", "value": 1},
248
+ "expected_counts": {"A": 0, "B": 0, "C": 1},
249
+ },
250
+ {
251
+ "name": "output_forward_chain_ft_explicit_false",
252
+ "node_flags": {"A": True, "B": False, "C": True},
253
+ "connections": [("output", "A", "B"), ("forward", "B", "C")],
254
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": False},
255
+ "expected_counts": {"A": 0, "B": 0, "C": 0},
256
+ },
257
+ {
258
+ "name": "output_to_input_t_explicit_true",
259
+ "node_flags": {"A": True, "B": True},
260
+ "connections": [("output", "A", "B")],
261
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": True},
262
+ "expected_counts": {"A": 0, "B": 1},
263
+ },
264
+ {
265
+ "name": "output_to_input_f_explicit_true",
266
+ "node_flags": {"A": True, "B": False},
267
+ "connections": [("output", "A", "B")],
268
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": True},
269
+ "expected_counts": {"A": 0, "B": 0},
270
+ },
271
+ {
272
+ "name": "output_forward_chain_ft_explicit_true",
273
+ "node_flags": {"A": True, "B": False, "C": True},
274
+ "connections": [("output", "A", "B"), ("forward", "B", "C")],
275
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": True},
276
+ "expected_counts": {"A": 0, "B": 0, "C": 1},
277
+ },
278
+ {
279
+ "name": "output_forward_chain_tf_explicit_true",
280
+ "node_flags": {"A": True, "B": True, "C": False},
281
+ "connections": [("output", "A", "B"), ("forward", "B", "C")],
282
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": True},
283
+ "expected_counts": {"A": 0, "B": 1, "C": 0},
284
+ },
285
+ {
286
+ "name": "output_forward_chain_ff_explicit_true",
287
+ "node_flags": {"A": True, "B": False, "C": False},
288
+ "connections": [("output", "A", "B"), ("forward", "B", "C")],
289
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": True},
290
+ "expected_counts": {"A": 0, "B": 0, "C": 0},
291
+ },
292
+ {
293
+ "name": "output_fanout_explicit_true",
294
+ "node_flags": {"A": True, "B": False, "C": True},
295
+ "connections": [("output", "A", "B"), ("output", "A", "C")],
296
+ "action": {"node": "A", "io": "output", "value": 1, "does_trigger": True},
297
+ "expected_counts": {"A": 0, "B": 0, "C": 1},
298
+ },
299
+ ]
300
+
301
+
302
+ MIXED_PROPAGATION_SCENARIOS = [
303
+ {
304
+ "name": "execution_chain_output_output",
305
+ "node_flags": {"A": True, "B": True, "C": True},
306
+ "connections": [("output", "A", "B"), ("output", "B", "C")],
307
+ "action": {"node": "A", "io": "output", "value": 1},
308
+ "expected_counts": {"A": 0, "B": 1, "C": 1},
309
+ },
310
+ {
311
+ "name": "input_then_execution_chain_output_false",
312
+ "node_flags": {"A": True, "B": True, "C": False},
313
+ "connections": [("forward", "A", "B"), ("output", "B", "C")],
314
+ "action": {"node": "A", "io": "input", "value": 1},
315
+ "expected_counts": {"A": 1, "B": 1, "C": 0},
316
+ },
317
+ {
318
+ "name": "mixed_output_diamond",
319
+ "node_flags": {"A": True, "B": False, "C": True, "D": True, "E": True},
320
+ "connections": [
321
+ ("output", "A", "B"),
322
+ ("output", "A", "C"),
323
+ ("forward", "B", "D"),
324
+ ("output", "C", "E"),
325
+ ],
326
+ "action": {"node": "A", "io": "output", "value": 1},
327
+ "expected_counts": {"A": 0, "B": 0, "C": 1, "D": 1, "E": 1},
328
+ },
329
+ ]
330
+
331
+
332
+ @pytest.mark.parametrize(
333
+ "scenario", INPUT_FORWARDING_SCENARIOS, ids=lambda s: s["name"]
334
+ )
335
+ @funcnodes_test
336
+ async def test_input_forwarding_trigger_matrix(scenario):
337
+ await run_scenario(scenario)
338
+
339
+
340
+ @pytest.mark.parametrize(
341
+ "scenario", OUTPUT_PROPAGATION_SCENARIOS, ids=lambda s: s["name"]
342
+ )
343
+ @funcnodes_test
344
+ async def test_output_propagation_trigger_matrix(scenario):
345
+ await run_scenario(scenario)
346
+
347
+
348
+ @pytest.mark.parametrize(
349
+ "scenario", MIXED_PROPAGATION_SCENARIOS, ids=lambda s: s["name"]
350
+ )
351
+ @funcnodes_test
352
+ async def test_mixed_trigger_propagation_matrix(scenario):
353
+ await run_scenario(scenario)
@@ -457,7 +457,7 @@ wheels = [
457
457
 
458
458
  [[package]]
459
459
  name = "funcnodes-core"
460
- version = "2.3.2"
460
+ version = "2.4.0"
461
461
  source = { editable = "." }
462
462
  dependencies = [
463
463
  { name = "dill" },
@@ -1,2 +0,0 @@
1
- Core package for funcnodes.
2
- for detailed instructions go to the funcnodes repo
File without changes
File without changes
File without changes