diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0cb8270..98fcbf4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -10,14 +10,14 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: "0.6.14" + rev: "0.9.4" hooks: # Update the uv lockfile - id: uv-lock - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.6 + rev: v0.14.1 hooks: # Run the linter. - id: ruff @@ -26,7 +26,13 @@ repos: - id: ruff-format - repo: https://github.com/pycqa/flake8 - rev: 7.2.0 # pick a git hash / tag to point to + rev: 7.3.0 # pick a git hash / tag to point to hooks: - id: flake8 args: ["--config=.flake8"] + + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.9.1 + hooks: + - id: commitizen + stages: [commit-msg] diff --git a/CHANGELOG.md b/CHANGELOG.md index f2c48b5..c926c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v1.1.0 (2025-11-21) + +### Feat + +- **logic**: enhance ForNode to support async iterables + +### Fix + +- **dicts**: improve key retrieval logic in DictGetNode + ## v1.0.0 (2025-09-16) ### Feat diff --git a/cz.toml b/cz.toml new file mode 100644 index 0000000..e1d8a7b --- /dev/null +++ b/cz.toml @@ -0,0 +1,6 @@ +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version_provider = "uv" +update_changelog_on_bump = true diff --git a/pyproject.toml b/pyproject.toml index 8a717b5..b395acb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "funcnodes-basic" -version = "1.0.0" +version = "1.1.0" description = "Basic functionalities for funcnodes" readme = "README.md" classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",] diff --git a/src/funcnodes_basic/dicts.py b/src/funcnodes_basic/dicts.py index d90b632..604aeb8 100644 --- a/src/funcnodes_basic/dicts.py +++ b/src/funcnodes_basic/dicts.py @@ -30,7 +30,13 @@ def _update_keys(self, **kwargs): self._keymap = keymap async def func(self, dictionary: dict, key: str) -> None: - v = dictionary.get(self._keymap[key], fn.NoValue) + try: + v = dictionary[self._keymap[key]] + except KeyError: + try: + v = dictionary[key] + except KeyError: + v = fn.NoValue self.outputs["value"].value = v return v diff --git a/src/funcnodes_basic/logic.py b/src/funcnodes_basic/logic.py index 012856b..e05c4cf 100644 --- a/src/funcnodes_basic/logic.py +++ b/src/funcnodes_basic/logic.py @@ -3,6 +3,7 @@ from funcnodes_core.node import Node from typing import Any, List, Optional from funcnodes_core.io import NodeInput, NodeOutput, NoValue +from collections.abc import AsyncIterable import asyncio import funcnodes_core as fn @@ -104,8 +105,8 @@ async def func(self, input: list, collector: Optional[Any] = None) -> None: except Exception: pass - for i in self.progress(input, desc="Iterating", unit="it", total=iplen): - self.outputs["do"].set_value(i, does_trigger=True) + async def _iterate_value(value: Any): + self.outputs["do"].set_value(value, does_trigger=True) datapaths = [ ip.datapath for ip in self.outputs["do"].connections @@ -124,6 +125,16 @@ async def func(self, input: list, collector: Optional[Any] = None) -> None: if v is not NoValue: results.append(v) self.inputs["collector"].value = NoValue + + if isinstance(input, AsyncIterable): + # handle async iterables/generators without forcing them into sync iteration + with self.progress(desc="Iterating", unit="it", total=iplen) as pbar: + async for i in input: + await _iterate_value(i) + pbar.update(1) + else: + for i in self.progress(input, desc="Iterating", unit="it", total=iplen): + await _iterate_value(i) self.outputs["done"].value = results diff --git a/src/funcnodes_basic/pyobjects.py b/src/funcnodes_basic/pyobjects.py index 23e53ea..0b4afd1 100644 --- a/src/funcnodes_basic/pyobjects.py +++ b/src/funcnodes_basic/pyobjects.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, List, Annotated, Dict +from typing import Any, List, Annotated import funcnodes_core as fn from funcnodes_core.io import InputMeta, OutputMeta @@ -46,7 +46,7 @@ def get_attribute( "attribute", _list_public_attributes, ) - } + }, ), ], attribute: Annotated[ @@ -146,7 +146,7 @@ def delete_attribute( "attribute", _list_public_attributes, ) - } + }, ), ], attribute: Annotated[ diff --git a/tests/test_logic.py b/tests/test_logic.py index c043fd5..b5ba3b2 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -53,6 +53,72 @@ async def test_for_node(): assert node.outputs["done"].value == ["h", "e", "l", "l", "o"] +@pytest_funcnodes.nodetest(logic.ForNode) +async def test_for_node_async_generator(): + async def async_generator(): + yield "hello" + yield "world" + yield "foo" + yield "bar" + + node = logic.ForNode() + waitnode = logic.WaitNode() + waitnode.inputs["delay"].value = 0.5 + waitnode.inputs["input"].connect(node.outputs["do"]) + waitnode.outputs["output"].connect(node.inputs["collector"]) + node.outputs["do"].connect(waitnode.inputs["input"]) + node.inputs["input"].value = async_generator() + + await node + assert node.outputs["done"].value == ["hello", "world", "foo", "bar"] + + +@pytest_funcnodes.nodetest(logic.ForNode) +async def test_for_node_async_iterable_object(): + class AsyncIterable: + def __init__(self, values): + self._values = values + + def __aiter__(self): + self._iter = iter(self._values) + return self + + async def __anext__(self): + try: + return next(self._iter) + except StopIteration: + raise StopAsyncIteration + + node = logic.ForNode() + waitnode = logic.WaitNode() + waitnode.inputs["delay"].value = 0.1 + waitnode.inputs["input"].connect(node.outputs["do"]) + waitnode.outputs["output"].connect(node.inputs["collector"]) + node.outputs["do"].connect(waitnode.inputs["input"]) + node.inputs["input"].value = AsyncIterable(["a", "b", "c"]) + + await node + + assert node.outputs["done"].value == ["a", "b", "c"] + + async def async_generator(): + yield "d" + yield "e" + yield "f" + + node = logic.ForNode() + waitnode = logic.WaitNode() + waitnode.inputs["delay"].value = 0.1 + waitnode.inputs["input"].connect(node.outputs["do"]) + waitnode.outputs["output"].connect(node.inputs["collector"]) + node.outputs["do"].connect(waitnode.inputs["input"]) + node.inputs["input"].value = async_generator() + + await node + + assert node.outputs["done"].value == ["d", "e", "f"] + + @pytest_funcnodes.nodetest(logic.CollectorNode) async def test_collector_node(): node = logic.CollectorNode() diff --git a/uv.lock b/uv.lock index 7fc807e..50d9485 100644 --- a/uv.lock +++ b/uv.lock @@ -380,7 +380,7 @@ wheels = [ [[package]] name = "funcnodes-basic" -version = "0.2.4" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "funcnodes" },