funcnodes-basic 0.2.2__tar.gz → 0.2.4__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 (31) hide show
  1. {funcnodes_basic-0.2.2/src/funcnodes_basic.egg-info → funcnodes_basic-0.2.4}/PKG-INFO +2 -2
  2. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/pyproject.toml +9 -2
  3. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/__init__.py +3 -2
  4. funcnodes_basic-0.2.4/src/funcnodes_basic/dataclass.py +64 -0
  5. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/logic.py +29 -6
  6. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4/src/funcnodes_basic.egg-info}/PKG-INFO +2 -2
  7. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/SOURCES.txt +3 -0
  8. funcnodes_basic-0.2.4/src/funcnodes_basic.egg-info/requires.txt +2 -0
  9. funcnodes_basic-0.2.4/tests/test_dataclass.py +141 -0
  10. funcnodes_basic-0.2.4/tests/test_flows.py +26 -0
  11. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_logic.py +5 -1
  12. funcnodes_basic-0.2.2/src/funcnodes_basic.egg-info/requires.txt +0 -2
  13. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/LICENSE +0 -0
  14. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/MANIFEST.in +0 -0
  15. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/README.md +0 -0
  16. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/setup.cfg +0 -0
  17. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/dicts.py +0 -0
  18. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/input.py +0 -0
  19. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/lists.py +0 -0
  20. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/math_nodes.py +0 -0
  21. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic/strings.py +0 -0
  22. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/dependency_links.txt +0 -0
  23. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/entry_points.txt +0 -0
  24. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/src/funcnodes_basic.egg-info/top_level.txt +0 -0
  25. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_all_nodes_pytest.py +0 -0
  26. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_dict.py +0 -0
  27. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_import.py +0 -0
  28. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_inputs.py +0 -0
  29. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_lists.py +0 -0
  30. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_math.py +0 -0
  31. {funcnodes_basic-0.2.2 → funcnodes_basic-0.2.4}/tests/test_strings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcnodes-basic
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Basic functionalities for funcnodes
5
5
  Author-email: Julian Kimmig <julian.kimmig@linkdlab.de>
6
6
  License: AGPL-3.0
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
12
12
  Requires-Python: >=3.11
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: funcnodes-core>=0.3.9
15
+ Requires-Dist: funcnodes-core>=0.4.1
16
16
  Requires-Dist: funcnodes
17
17
  Dynamic: license-file
18
18
 
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "funcnodes-basic"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Basic functionalities for funcnodes"
5
5
  readme = "README.md"
6
6
  classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
7
7
  requires-python = ">=3.11"
8
8
  dependencies = [
9
- "funcnodes-core>=0.3.9",
9
+ "funcnodes-core>=0.4.1",
10
10
  "funcnodes",
11
11
  ]
12
12
  authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}]
@@ -43,3 +43,10 @@ shelf = "funcnodes_basic:NODE_SHELF"
43
43
 
44
44
  [tool.setuptools.packages.find]
45
45
  where = [ "src",]
46
+
47
+ [tool.commitizen]
48
+ name = "cz_conventional_commits"
49
+ tag_format = "v$version"
50
+ version_scheme = "pep440"
51
+ version_provider = "uv"
52
+ update_changelog_on_bump = true
@@ -5,9 +5,9 @@ from .lists import NODE_SHELF as lists_shelf
5
5
  from .strings import NODE_SHELF as strings_shelf
6
6
  from .dicts import NODE_SHELF as dicts_shelf
7
7
  from .input import NODE_SHELF as input_shelf
8
+ from .dataclass import NODE_SHELF as dataclass_shelf
8
9
 
9
-
10
- __version__ = "0.2.2"
10
+ __version__ = "0.2.3"
11
11
 
12
12
  NODE_SHELF = Shelf(
13
13
  nodes=[],
@@ -15,6 +15,7 @@ NODE_SHELF = Shelf(
15
15
  input_shelf,
16
16
  lists_shelf,
17
17
  dicts_shelf,
18
+ dataclass_shelf,
18
19
  strings_shelf,
19
20
  math_shelf,
20
21
  logic_shelf,
@@ -0,0 +1,64 @@
1
+ import dataclasses
2
+ import funcnodes_core as fn
3
+ from typing import Any, Dict
4
+
5
+
6
+ @fn.NodeDecorator(
7
+ id="dataclass.to_dict",
8
+ )
9
+ def dataclass_to_dict(instance: Any) -> Dict[str, Any]:
10
+ """
11
+ Convert a dataclass instance to a dictionary.
12
+
13
+ Args:
14
+ instance (object): The dataclass instance to convert.
15
+
16
+ Returns:
17
+ dict: The dictionary representation of the dataclass instance.
18
+ """
19
+ if not dataclasses.is_dataclass(instance):
20
+ raise TypeError(f"Expected a dataclass instance, got {type(instance)}")
21
+
22
+ return dataclasses.asdict(instance)
23
+
24
+
25
+ @fn.NodeDecorator(
26
+ id="dataclass.get_field",
27
+ default_io_options={
28
+ "instance": {
29
+ "on": {
30
+ "after_set_value": fn.decorator.update_other_io_value_options(
31
+ "field_name",
32
+ lambda result: {
33
+ "options": [field.name for field in dataclasses.fields(result)]
34
+ if dataclasses.is_dataclass(result)
35
+ else None,
36
+ },
37
+ )
38
+ }
39
+ }
40
+ },
41
+ )
42
+ def dataclass_get_field(instance: Any, field_name: str) -> Any:
43
+ """
44
+ Get a field value from a dataclass instance.
45
+ """
46
+ if not dataclasses.is_dataclass(instance):
47
+ raise TypeError(f"Expected a dataclass instance, got {type(instance)}")
48
+
49
+ if not hasattr(instance, field_name):
50
+ raise AttributeError(
51
+ f"{instance.__class__.__name__} has no field '{field_name}'"
52
+ )
53
+
54
+ return getattr(instance, field_name)
55
+
56
+
57
+ NODE_SHELF = fn.Shelf(
58
+ nodes=[
59
+ dataclass_to_dict,
60
+ dataclass_get_field,
61
+ ],
62
+ name="dataclass",
63
+ description="Nodes for working with dataclasses",
64
+ )
@@ -1,6 +1,6 @@
1
1
  """Logic Nodes for control flow and decision making."""
2
2
 
3
- from funcnodes_core.node import Node, TriggerStack
3
+ from funcnodes_core.node import Node
4
4
  from typing import Any, List, Optional
5
5
  from funcnodes_core.io import NodeInput, NodeOutput, NoValue
6
6
  import asyncio
@@ -35,8 +35,19 @@ class WhileNode(Node):
35
35
  async def func(self, condition: bool, input: Any) -> None:
36
36
  if self.inputs["condition"].value:
37
37
  self.outputs["do"].value = input
38
- triggerstack = TriggerStack()
39
- await self.outputs["do"].trigger(triggerstack)
38
+ datapaths = [
39
+ ip.datapath
40
+ for ip in self.outputs["do"].connections
41
+ if ip.datapath is not None
42
+ ]
43
+
44
+ while datapaths:
45
+ for dp in datapaths:
46
+ if dp.done(breaking_nodes=[self]):
47
+ datapaths.remove(dp)
48
+ break
49
+ else:
50
+ await asyncio.sleep(0.1)
40
51
  self.request_trigger()
41
52
  else:
42
53
  self.outputs["done"].value = input
@@ -94,9 +105,21 @@ class ForNode(Node):
94
105
  pass
95
106
 
96
107
  for i in self.progress(input, desc="Iterating", unit="it", total=iplen):
97
- self.outputs["do"].set_value(i, does_trigger=False)
98
- triggerstack = TriggerStack()
99
- await self.outputs["do"].trigger(triggerstack)
108
+ self.outputs["do"].set_value(i, does_trigger=True)
109
+ datapaths = [
110
+ ip.datapath
111
+ for ip in self.outputs["do"].connections
112
+ if ip.datapath is not None
113
+ ]
114
+
115
+ while datapaths:
116
+ for dp in datapaths:
117
+ if dp.done(breaking_nodes=[self]):
118
+ datapaths.remove(dp)
119
+ break
120
+ else:
121
+ await asyncio.sleep(0.1)
122
+
100
123
  v = self.inputs["collector"].value
101
124
  if v is not NoValue:
102
125
  results.append(v)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcnodes-basic
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Basic functionalities for funcnodes
5
5
  Author-email: Julian Kimmig <julian.kimmig@linkdlab.de>
6
6
  License: AGPL-3.0
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
12
12
  Requires-Python: >=3.11
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: funcnodes-core>=0.3.9
15
+ Requires-Dist: funcnodes-core>=0.4.1
16
16
  Requires-Dist: funcnodes
17
17
  Dynamic: license-file
18
18
 
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  src/funcnodes_basic/__init__.py
6
+ src/funcnodes_basic/dataclass.py
6
7
  src/funcnodes_basic/dicts.py
7
8
  src/funcnodes_basic/input.py
8
9
  src/funcnodes_basic/lists.py
@@ -16,7 +17,9 @@ src/funcnodes_basic.egg-info/entry_points.txt
16
17
  src/funcnodes_basic.egg-info/requires.txt
17
18
  src/funcnodes_basic.egg-info/top_level.txt
18
19
  tests/test_all_nodes_pytest.py
20
+ tests/test_dataclass.py
19
21
  tests/test_dict.py
22
+ tests/test_flows.py
20
23
  tests/test_import.py
21
24
  tests/test_inputs.py
22
25
  tests/test_lists.py
@@ -0,0 +1,2 @@
1
+ funcnodes-core>=0.4.1
2
+ funcnodes
@@ -0,0 +1,141 @@
1
+ from funcnodes_basic import dataclass as dc_nodes
2
+ import pytest_funcnodes
3
+ import pytest
4
+ import funcnodes_core as fn
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class SimpleDataClass:
10
+ name: str
11
+ value: int
12
+ is_active: bool = True
13
+
14
+
15
+ @dataclass
16
+ class NestedDataClass:
17
+ id: int
18
+ data: SimpleDataClass
19
+
20
+
21
+ @pytest_funcnodes.nodetest(dc_nodes.dataclass_to_dict)
22
+ async def test_dataclass_to_dict():
23
+ node = dc_nodes.dataclass_to_dict()
24
+
25
+ # Test with a simple dataclass
26
+ instance1 = SimpleDataClass(name="Test1", value=100)
27
+ node.inputs["instance"].value = instance1
28
+ await node
29
+ assert node.outputs["out"].value == {
30
+ "name": "Test1",
31
+ "value": 100,
32
+ "is_active": True,
33
+ }
34
+
35
+ # Test with a nested dataclass
36
+ instance2 = NestedDataClass(
37
+ id=1, data=SimpleDataClass(name="Nested", value=200, is_active=False)
38
+ )
39
+ node.inputs["instance"].value = instance2
40
+ await node
41
+ assert node.outputs["out"].value == {
42
+ "id": 1,
43
+ "data": {"name": "Nested", "value": 200, "is_active": False},
44
+ }
45
+
46
+ # Test with non-dataclass input
47
+ node.inputs["instance"].value = {"not": "a dataclass"}
48
+ with pytest.raises(fn.NodeTriggerError) as excinfo:
49
+ await node
50
+ assert "Expected a dataclass instance" in str(excinfo.value)
51
+
52
+ node.inputs["instance"].value = 123
53
+ with pytest.raises(fn.NodeTriggerError) as excinfo:
54
+ await node
55
+ assert "Expected a dataclass instance" in str(excinfo.value)
56
+
57
+
58
+ @pytest_funcnodes.nodetest(dc_nodes.dataclass_get_field)
59
+ async def test_dataclass_get_field():
60
+ node = dc_nodes.dataclass_get_field()
61
+
62
+ instance_simple = SimpleDataClass(name="TestSimple", value=123)
63
+ instance_nested = NestedDataClass(
64
+ id=1, data=SimpleDataClass(name="Nested", value=200, is_active=False)
65
+ )
66
+
67
+ # Test setting instance updates field_name options
68
+ node.inputs["instance"].value = instance_simple
69
+ await node # Initial trigger to process instance and update options
70
+ assert node.inputs["field_name"].value_options["options"] == [
71
+ "name",
72
+ "value",
73
+ "is_active",
74
+ ]
75
+
76
+ # Test getting a valid field 'name'
77
+ node.inputs["field_name"].value = "name"
78
+ await node
79
+ assert node.outputs["out"].value == "TestSimple"
80
+
81
+ # Test getting a valid field 'value'
82
+ node.inputs["field_name"].value = "value"
83
+ await node
84
+ assert node.outputs["out"].value == 123
85
+
86
+ # Test getting a valid field 'is_active'
87
+ node.inputs["field_name"].value = "is_active"
88
+ await node
89
+ assert node.outputs["out"].value is True
90
+
91
+ # Test with nested dataclass, first update options
92
+ node.inputs["instance"].value = instance_nested
93
+
94
+ assert node.inputs["field_name"].value_options["options"] == ["id", "data"]
95
+
96
+ # Test getting 'id' from nested
97
+ node.inputs["field_name"].value = "id"
98
+ await node
99
+ assert node.outputs["out"].value == 1
100
+
101
+ # Test getting 'data' (which is another dataclass)
102
+ node.inputs["field_name"].value = "data"
103
+ await node
104
+ assert node.outputs["out"].value == SimpleDataClass(
105
+ name="Nested", value=200, is_active=False
106
+ )
107
+
108
+ # Test getting non-existent field
109
+ node.inputs["instance"].value = instance_simple
110
+ node.inputs["field_name"].value = "non_existent_field"
111
+ with pytest.raises(fn.NodeTriggerError) as excinfo:
112
+ await node
113
+ assert "has no field 'non_existent_field'" in str(excinfo.value)
114
+
115
+ # Test with non-dataclass input
116
+ node.inputs["instance"].value = {"not": "a dataclass"}
117
+ node.inputs["field_name"].value = "some_field" # field_name options will be None
118
+ assert node.inputs["field_name"].value_options["options"] is None
119
+
120
+ with pytest.raises(fn.NodeTriggerError) as excinfo:
121
+ await node # trigger the func with invalid instance
122
+ assert "Expected a dataclass instance" in str(excinfo.value)
123
+
124
+ # Test dynamic update of options when instance changes
125
+ node.inputs["instance"].value = instance_nested
126
+ assert node.inputs["field_name"].value_options["options"] == ["id", "data"]
127
+ node.inputs["field_name"].value = "id"
128
+ await node
129
+ assert node.outputs["out"].value == 1
130
+
131
+ node.inputs["instance"].value = instance_simple
132
+ assert node.inputs["field_name"].value_options["options"] == [
133
+ "name",
134
+ "value",
135
+ "is_active",
136
+ ]
137
+ node.inputs[
138
+ "field_name"
139
+ ].value = "name" # previous 'id' is no longer valid for options, but value remains
140
+ await node
141
+ assert node.outputs["out"].value == "TestSimple"
@@ -0,0 +1,26 @@
1
+ from funcnodes_basic import logic, strings
2
+ import pytest_funcnodes
3
+ import asyncio
4
+
5
+
6
+ @pytest_funcnodes.nodetest(logic.ForNode)
7
+ async def test_if_node_flow1():
8
+ fornode = logic.ForNode()
9
+ endswith = strings.string_endswith()
10
+ ifnode = logic.IfNode()
11
+
12
+ fornode.outputs["do"].connect(endswith.inputs["s"])
13
+ endswith.outputs["ends_with"].connect(ifnode.inputs["condition"])
14
+ fornode.outputs["do"].connect(ifnode.inputs["input"])
15
+ ifnode.outputs["on_true"].connect(fornode.inputs["collector"])
16
+
17
+ endswith.inputs["suffix"].value = ".txt"
18
+ fornode.inputs["input"].value = ["a.txt", "b.xls", "c.xls", "d.txt", "e.xls"]
19
+
20
+ # await fn.run_until_complete(fornode, ifnode, endswith)
21
+ await asyncio.sleep(1.5)
22
+ print("fornode:", fornode.in_trigger)
23
+ print("ifnode:", ifnode.in_trigger)
24
+ print("endswith:", endswith.in_trigger)
25
+
26
+ assert fornode.outputs["done"].value == ["a.txt", "d.txt"]
@@ -42,8 +42,12 @@ async def test_wait_node():
42
42
  @pytest_funcnodes.nodetest(logic.ForNode)
43
43
  async def test_for_node():
44
44
  node = logic.ForNode()
45
+ waitnode = logic.WaitNode()
46
+ waitnode.inputs["delay"].value = 0.5
47
+ waitnode.inputs["input"].connect(node.outputs["do"])
48
+ waitnode.outputs["output"].connect(node.inputs["collector"])
45
49
  node.inputs["input"].value = "hello"
46
- node.outputs["do"].connect(node.inputs["collector"])
50
+
47
51
  await node
48
52
 
49
53
  assert node.outputs["done"].value == ["h", "e", "l", "l", "o"]
@@ -1,2 +0,0 @@
1
- funcnodes-core>=0.3.9
2
- funcnodes
File without changes