funcnodes-basic 0.2.3__tar.gz → 1.0.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 (33) hide show
  1. {funcnodes_basic-0.2.3/src/funcnodes_basic.egg-info → funcnodes_basic-1.0.0}/PKG-INFO +2 -2
  2. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/pyproject.toml +9 -2
  3. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/__init__.py +3 -1
  4. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/logic.py +29 -6
  5. funcnodes_basic-1.0.0/src/funcnodes_basic/pyobjects.py +183 -0
  6. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0/src/funcnodes_basic.egg-info}/PKG-INFO +2 -2
  7. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic.egg-info/SOURCES.txt +3 -0
  8. funcnodes_basic-1.0.0/src/funcnodes_basic.egg-info/requires.txt +2 -0
  9. funcnodes_basic-1.0.0/tests/test_flows.py +26 -0
  10. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_logic.py +5 -1
  11. funcnodes_basic-1.0.0/tests/test_pyobjects.py +124 -0
  12. funcnodes_basic-0.2.3/src/funcnodes_basic.egg-info/requires.txt +0 -2
  13. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/LICENSE +0 -0
  14. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/MANIFEST.in +0 -0
  15. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/README.md +0 -0
  16. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/setup.cfg +0 -0
  17. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/dataclass.py +0 -0
  18. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/dicts.py +0 -0
  19. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/input.py +0 -0
  20. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/lists.py +0 -0
  21. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/math_nodes.py +0 -0
  22. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic/strings.py +0 -0
  23. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic.egg-info/dependency_links.txt +0 -0
  24. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic.egg-info/entry_points.txt +0 -0
  25. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/src/funcnodes_basic.egg-info/top_level.txt +0 -0
  26. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_all_nodes_pytest.py +0 -0
  27. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_dataclass.py +0 -0
  28. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_dict.py +0 -0
  29. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_import.py +0 -0
  30. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_inputs.py +0 -0
  31. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_lists.py +0 -0
  32. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/tests/test_math.py +0 -0
  33. {funcnodes_basic-0.2.3 → funcnodes_basic-1.0.0}/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.3
3
+ Version: 1.0.0
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>=1.0.5
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.3"
3
+ version = "1.0.0"
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>=1.0.5",
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
@@ -6,8 +6,9 @@ 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
8
  from .dataclass import NODE_SHELF as dataclass_shelf
9
+ from .pyobjects import NODE_SHELF as pyobjects_shelf
9
10
 
10
- __version__ = "0.2.3"
11
+ __version__ = "1.0.0"
11
12
 
12
13
  NODE_SHELF = Shelf(
13
14
  nodes=[],
@@ -15,6 +16,7 @@ NODE_SHELF = Shelf(
15
16
  input_shelf,
16
17
  lists_shelf,
17
18
  dicts_shelf,
19
+ pyobjects_shelf,
18
20
  dataclass_shelf,
19
21
  strings_shelf,
20
22
  math_shelf,
@@ -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)
@@ -0,0 +1,183 @@
1
+ """Utilities that expose common Python object interactions as nodes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, List, Annotated, Dict
6
+
7
+ import funcnodes_core as fn
8
+ from funcnodes_core.io import InputMeta, OutputMeta
9
+
10
+
11
+ def _list_public_attributes(obj: Any) -> List[str]:
12
+ """Return a sorted list of the object's non-private attribute names."""
13
+ if obj is None:
14
+ return []
15
+ try:
16
+ attributes = dir(obj)
17
+ except Exception: # pragma: no cover - dir() rarely raises but stay defensive
18
+ return []
19
+ # Sorting keeps the dropdown stable for UI components that rely on deterministic
20
+ # value ordering across reruns.
21
+ return sorted(attr for attr in attributes if not attr.startswith("_"))
22
+
23
+
24
+ def _ensure_non_private(attribute: str) -> None:
25
+ """Reject attribute names that appear private."""
26
+
27
+ if attribute.startswith("_"):
28
+ raise AttributeError(
29
+ f"Access to private attribute '{attribute}' is not permitted by this node."
30
+ )
31
+
32
+
33
+ @fn.NodeDecorator(
34
+ id="pyobject_get_attribute",
35
+ name="Get Attribute",
36
+ description="Retrieve the value of a non-private attribute from a Python object.",
37
+ # default_io_options=_attribute_io_options(),
38
+ )
39
+ def get_attribute(
40
+ obj: Annotated[
41
+ Any,
42
+ InputMeta(
43
+ description="Python object that exposes the desired attribute.",
44
+ on={
45
+ "after_set_value": fn.decorator.update_other_io_options(
46
+ "attribute",
47
+ _list_public_attributes,
48
+ )
49
+ }
50
+ ),
51
+ ],
52
+ attribute: Annotated[
53
+ str,
54
+ InputMeta(
55
+ description="Name of the attribute to retrieve; private attributes are rejected.",
56
+ ),
57
+ ],
58
+ ) -> Annotated[
59
+ Any,
60
+ OutputMeta(
61
+ description="Value read from the requested attribute.",
62
+ ),
63
+ ]:
64
+ """Return the value of the selected non-private attribute for the provided object."""
65
+ _ensure_non_private(attribute)
66
+ if not hasattr(obj, attribute):
67
+ raise AttributeError(
68
+ f"Attribute '{attribute}' is not available on object of type {type(obj).__name__}."
69
+ )
70
+ # getattr performs the actual attribute retrieval once validation is complete.
71
+ return getattr(obj, attribute)
72
+
73
+
74
+ @fn.NodeDecorator(
75
+ id="pyobject_has_attribute",
76
+ name="Has Attribute",
77
+ description="Check whether an object exposes a given attribute.",
78
+ )
79
+ def has_attribute(
80
+ obj: Annotated[
81
+ Any,
82
+ InputMeta(
83
+ description="Python object that may expose the attribute.",
84
+ ),
85
+ ],
86
+ attribute: Annotated[
87
+ str,
88
+ InputMeta(
89
+ description="Attribute name to probe; private attributes are rejected.",
90
+ ),
91
+ ],
92
+ ) -> Annotated[
93
+ bool,
94
+ OutputMeta(description="True if the attribute exists on the object."),
95
+ ]:
96
+ """Return True when the object defines the requested attribute."""
97
+
98
+ return hasattr(obj, attribute)
99
+
100
+
101
+ @fn.NodeDecorator(
102
+ id="pyobject_set_attribute",
103
+ name="Set Attribute",
104
+ description="Assign a new value to a non-private attribute on a Python object.",
105
+ )
106
+ def set_attribute(
107
+ obj: Annotated[
108
+ Any,
109
+ InputMeta(
110
+ description="Python object whose attribute should be updated.",
111
+ ),
112
+ ],
113
+ attribute: Annotated[
114
+ str,
115
+ InputMeta(
116
+ description="Attribute name that will receive the new value.",
117
+ ),
118
+ ],
119
+ value: Annotated[
120
+ Any,
121
+ InputMeta(description="Value to assign to the attribute."),
122
+ ],
123
+ ) -> Annotated[
124
+ Any,
125
+ OutputMeta(description="The original object after assignment."),
126
+ ]:
127
+ """Set an attribute on the provided object and return the object for chaining."""
128
+
129
+ _ensure_non_private(attribute)
130
+ setattr(obj, attribute, value)
131
+ return obj
132
+
133
+
134
+ @fn.NodeDecorator(
135
+ id="pyobject_delete_attribute",
136
+ name="Delete Attribute",
137
+ description="Remove a non-private attribute from a Python object.",
138
+ )
139
+ def delete_attribute(
140
+ obj: Annotated[
141
+ Any,
142
+ InputMeta(
143
+ description="Python object whose attribute should be removed.",
144
+ on={
145
+ "after_set_value": fn.decorator.update_other_io_options(
146
+ "attribute",
147
+ _list_public_attributes,
148
+ )
149
+ }
150
+ ),
151
+ ],
152
+ attribute: Annotated[
153
+ str,
154
+ InputMeta(
155
+ description="Attribute name to remove from the object.",
156
+ ),
157
+ ],
158
+ ) -> Annotated[
159
+ Any,
160
+ OutputMeta(description="The original object after deletion."),
161
+ ]:
162
+ """Delete an attribute from the object and return the mutated object."""
163
+
164
+ _ensure_non_private(attribute)
165
+ if not hasattr(obj, attribute):
166
+ raise AttributeError(
167
+ f"Attribute '{attribute}' is not available on object of type {type(obj).__name__}."
168
+ )
169
+ delattr(obj, attribute)
170
+ return obj
171
+
172
+
173
+ NODE_SHELF = fn.Shelf(
174
+ nodes=[
175
+ get_attribute,
176
+ has_attribute,
177
+ set_attribute,
178
+ delete_attribute,
179
+ ],
180
+ name="Python Objects",
181
+ description="Access and transform general Python objects.",
182
+ subshelves=[],
183
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcnodes-basic
3
- Version: 0.2.3
3
+ Version: 1.0.0
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>=1.0.5
16
16
  Requires-Dist: funcnodes
17
17
  Dynamic: license-file
18
18
 
@@ -9,6 +9,7 @@ src/funcnodes_basic/input.py
9
9
  src/funcnodes_basic/lists.py
10
10
  src/funcnodes_basic/logic.py
11
11
  src/funcnodes_basic/math_nodes.py
12
+ src/funcnodes_basic/pyobjects.py
12
13
  src/funcnodes_basic/strings.py
13
14
  src/funcnodes_basic.egg-info/PKG-INFO
14
15
  src/funcnodes_basic.egg-info/SOURCES.txt
@@ -19,9 +20,11 @@ src/funcnodes_basic.egg-info/top_level.txt
19
20
  tests/test_all_nodes_pytest.py
20
21
  tests/test_dataclass.py
21
22
  tests/test_dict.py
23
+ tests/test_flows.py
22
24
  tests/test_import.py
23
25
  tests/test_inputs.py
24
26
  tests/test_lists.py
25
27
  tests/test_logic.py
26
28
  tests/test_math.py
29
+ tests/test_pyobjects.py
27
30
  tests/test_strings.py
@@ -0,0 +1,2 @@
1
+ funcnodes-core>=1.0.5
2
+ funcnodes
@@ -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"]
@@ -0,0 +1,124 @@
1
+ import funcnodes_core as fn
2
+ import pytest
3
+ import pytest_funcnodes
4
+
5
+ from funcnodes_basic import pyobjects
6
+
7
+
8
+ class Sample:
9
+ class_attr = "class-level"
10
+
11
+ def __init__(self) -> None:
12
+ self.public = "value"
13
+ self._private = "hidden"
14
+
15
+ @property
16
+ def public_property(self) -> str:
17
+ return "prop"
18
+
19
+ def public_method(self) -> str:
20
+ return "method"
21
+
22
+
23
+ @pytest_funcnodes.nodetest(pyobjects.get_attribute)
24
+ async def test_get_attribute_node_exposes_public_attributes():
25
+ node = pyobjects.get_attribute()
26
+ sample = Sample()
27
+
28
+ # Setting the object input triggers the attribute dropdown update.
29
+ node.inputs["obj"].value = sample
30
+
31
+ options = node.inputs["attribute"].value_options["options"]
32
+ assert options == [
33
+ "class_attr",
34
+ "public",
35
+ "public_method",
36
+ "public_property",
37
+ ]
38
+
39
+ node.inputs["attribute"].value = "public"
40
+ await node
41
+ assert node.outputs["out"].value == "value"
42
+
43
+ node.inputs["attribute"].value = "public_property"
44
+ await node
45
+ assert node.outputs["out"].value == "prop"
46
+
47
+ node.inputs["attribute"].value = "public_method"
48
+ await node
49
+ method = node.outputs["out"].value
50
+ assert callable(method)
51
+ assert method() == "method"
52
+
53
+ with pytest.raises(fn.NodeTriggerError):
54
+ node.inputs["attribute"].value = "_private"
55
+ await node
56
+
57
+ with pytest.raises(fn.NodeTriggerError):
58
+ node.inputs["attribute"].value = "missing"
59
+ await node
60
+
61
+
62
+ @pytest_funcnodes.nodetest(pyobjects.has_attribute)
63
+ async def test_has_attribute_node_checks_presence():
64
+ node = pyobjects.has_attribute()
65
+ sample = Sample()
66
+
67
+ node.inputs["obj"].value = sample
68
+
69
+ node.inputs["attribute"].value = "public_property"
70
+ await node
71
+ assert node.outputs["out"].value is True
72
+
73
+ node.inputs["attribute"].value = "missing"
74
+ await node
75
+ assert node.outputs["out"].value is False
76
+
77
+ node.inputs["attribute"].value = "_private"
78
+ await node
79
+ assert node.outputs["out"].value is True
80
+
81
+
82
+ @pytest_funcnodes.nodetest(pyobjects.set_attribute)
83
+ async def test_set_attribute_node_updates_value():
84
+ node = pyobjects.set_attribute()
85
+ sample = Sample()
86
+
87
+ node.inputs["obj"].value = sample
88
+ node.inputs["attribute"].value = "public"
89
+ node.inputs["value"].value = "updated"
90
+ await node
91
+
92
+ assert sample.public == "updated"
93
+ assert node.outputs["out"].value is sample
94
+
95
+ node.inputs["attribute"].value = "new_attr"
96
+ node.inputs["value"].value = 42
97
+ await node
98
+ assert getattr(sample, "new_attr") == 42
99
+
100
+ with pytest.raises(fn.NodeTriggerError):
101
+ node.inputs["attribute"].value = "_private"
102
+ node.inputs["value"].value = "secret"
103
+ await node
104
+
105
+
106
+ @pytest_funcnodes.nodetest(pyobjects.delete_attribute)
107
+ async def test_delete_attribute_node_removes_value():
108
+ node = pyobjects.delete_attribute()
109
+ sample = Sample()
110
+
111
+ node.inputs["obj"].value = sample
112
+ node.inputs["attribute"].value = "public"
113
+ await node
114
+
115
+ assert not hasattr(sample, "public")
116
+ assert node.outputs["out"].value is sample
117
+
118
+ with pytest.raises(fn.NodeTriggerError):
119
+ node.inputs["attribute"].value = "_private"
120
+ await node
121
+
122
+ with pytest.raises(fn.NodeTriggerError):
123
+ node.inputs["attribute"].value = "missing"
124
+ await node
@@ -1,2 +0,0 @@
1
- funcnodes-core>=0.3.9
2
- funcnodes
File without changes