Skip to content
Merged

Test #29

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "funcnodes-basic"
version = "0.2.2"
version = "0.2.3"
description = "Basic functionalities for funcnodes"
readme = "README.md"
classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
Expand Down
5 changes: 3 additions & 2 deletions src/funcnodes_basic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
from .strings import NODE_SHELF as strings_shelf
from .dicts import NODE_SHELF as dicts_shelf
from .input import NODE_SHELF as input_shelf
from .dataclass import NODE_SHELF as dataclass_shelf


__version__ = "0.2.2"
__version__ = "0.2.3"

NODE_SHELF = Shelf(
nodes=[],
subshelves=[
input_shelf,
lists_shelf,
dicts_shelf,
dataclass_shelf,
strings_shelf,
math_shelf,
logic_shelf,
Expand Down
64 changes: 64 additions & 0 deletions src/funcnodes_basic/dataclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import dataclasses
import funcnodes_core as fn
from typing import Any, Dict


@fn.NodeDecorator(
id="dataclass.to_dict",
)
def dataclass_to_dict(instance: Any) -> Dict[str, Any]:
"""
Convert a dataclass instance to a dictionary.

Args:
instance (object): The dataclass instance to convert.

Returns:
dict: The dictionary representation of the dataclass instance.
"""
if not dataclasses.is_dataclass(instance):
raise TypeError(f"Expected a dataclass instance, got {type(instance)}")

return dataclasses.asdict(instance)


@fn.NodeDecorator(
id="dataclass.get_field",
default_io_options={
"instance": {
"on": {
"after_set_value": fn.decorator.update_other_io_value_options(
"field_name",
lambda result: {
"options": [field.name for field in dataclasses.fields(result)]
if dataclasses.is_dataclass(result)
else None,
},
)
}
}
},
)
def dataclass_get_field(instance: Any, field_name: str) -> Any:
"""
Get a field value from a dataclass instance.
"""
if not dataclasses.is_dataclass(instance):
raise TypeError(f"Expected a dataclass instance, got {type(instance)}")

if not hasattr(instance, field_name):
raise AttributeError(
f"{instance.__class__.__name__} has no field '{field_name}'"
)

return getattr(instance, field_name)


NODE_SHELF = fn.Shelf(
nodes=[
dataclass_to_dict,
dataclass_get_field,
],
name="dataclass",
description="Nodes for working with dataclasses",
)
141 changes: 141 additions & 0 deletions tests/test_dataclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from funcnodes_basic import dataclass as dc_nodes
import pytest_funcnodes
import pytest
import funcnodes_core as fn
from dataclasses import dataclass


@dataclass
class SimpleDataClass:
name: str
value: int
is_active: bool = True


@dataclass
class NestedDataClass:
id: int
data: SimpleDataClass


@pytest_funcnodes.nodetest(dc_nodes.dataclass_to_dict)
async def test_dataclass_to_dict():
node = dc_nodes.dataclass_to_dict()

# Test with a simple dataclass
instance1 = SimpleDataClass(name="Test1", value=100)
node.inputs["instance"].value = instance1
await node
assert node.outputs["out"].value == {
"name": "Test1",
"value": 100,
"is_active": True,
}

# Test with a nested dataclass
instance2 = NestedDataClass(
id=1, data=SimpleDataClass(name="Nested", value=200, is_active=False)
)
node.inputs["instance"].value = instance2
await node
assert node.outputs["out"].value == {
"id": 1,
"data": {"name": "Nested", "value": 200, "is_active": False},
}

# Test with non-dataclass input
node.inputs["instance"].value = {"not": "a dataclass"}
with pytest.raises(fn.NodeTriggerError) as excinfo:
await node
assert "Expected a dataclass instance" in str(excinfo.value)

node.inputs["instance"].value = 123
with pytest.raises(fn.NodeTriggerError) as excinfo:
await node
assert "Expected a dataclass instance" in str(excinfo.value)


@pytest_funcnodes.nodetest(dc_nodes.dataclass_get_field)
async def test_dataclass_get_field():
node = dc_nodes.dataclass_get_field()

instance_simple = SimpleDataClass(name="TestSimple", value=123)
instance_nested = NestedDataClass(
id=1, data=SimpleDataClass(name="Nested", value=200, is_active=False)
)

# Test setting instance updates field_name options
node.inputs["instance"].value = instance_simple
await node # Initial trigger to process instance and update options
assert node.inputs["field_name"].value_options["options"] == [
"name",
"value",
"is_active",
]

# Test getting a valid field 'name'
node.inputs["field_name"].value = "name"
await node
assert node.outputs["out"].value == "TestSimple"

# Test getting a valid field 'value'
node.inputs["field_name"].value = "value"
await node
assert node.outputs["out"].value == 123

# Test getting a valid field 'is_active'
node.inputs["field_name"].value = "is_active"
await node
assert node.outputs["out"].value is True

# Test with nested dataclass, first update options
node.inputs["instance"].value = instance_nested

assert node.inputs["field_name"].value_options["options"] == ["id", "data"]

# Test getting 'id' from nested
node.inputs["field_name"].value = "id"
await node
assert node.outputs["out"].value == 1

# Test getting 'data' (which is another dataclass)
node.inputs["field_name"].value = "data"
await node
assert node.outputs["out"].value == SimpleDataClass(
name="Nested", value=200, is_active=False
)

# Test getting non-existent field
node.inputs["instance"].value = instance_simple
node.inputs["field_name"].value = "non_existent_field"
with pytest.raises(fn.NodeTriggerError) as excinfo:
await node
assert "has no field 'non_existent_field'" in str(excinfo.value)

# Test with non-dataclass input
node.inputs["instance"].value = {"not": "a dataclass"}
node.inputs["field_name"].value = "some_field" # field_name options will be None
assert node.inputs["field_name"].value_options["options"] is None

with pytest.raises(fn.NodeTriggerError) as excinfo:
await node # trigger the func with invalid instance
assert "Expected a dataclass instance" in str(excinfo.value)

# Test dynamic update of options when instance changes
node.inputs["instance"].value = instance_nested
assert node.inputs["field_name"].value_options["options"] == ["id", "data"]
node.inputs["field_name"].value = "id"
await node
assert node.outputs["out"].value == 1

node.inputs["instance"].value = instance_simple
assert node.inputs["field_name"].value_options["options"] == [
"name",
"value",
"is_active",
]
node.inputs[
"field_name"
].value = "name" # previous 'id' is no longer valid for options, but value remains
await node
assert node.outputs["out"].value == "TestSimple"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.