Skip to content
Merged

Test #39

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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## v1.2.0 (2026-01-12)

### Feat

- **basic**: add dict utilities and list/json helpers
- **dicts**: add nodes to set missing or existing keys
- **input**: add json input node

## v1.1.0 (2025-11-21)

### Feat
Expand Down
6 changes: 6 additions & 0 deletions commit_message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
feat(basic): add dict utilities and list/json helpers

Add dict helpers for deep get/set, merge, key selection/rename, and
pop/delete with dynamic key value options.

Add json_dump plus list flatten/unique nodes with tests.
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 = "1.1.0"
version = "1.2.0"
description = "Basic functionalities for funcnodes"
readme = "README.md"
classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
Expand Down
229 changes: 228 additions & 1 deletion src/funcnodes_basic/dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

import funcnodes_core as fn
from typing import Any, List, Tuple
from typing import Any, List, Tuple, Annotated, Literal


class DictGetNode(fn.Node):
Expand Down Expand Up @@ -96,6 +96,224 @@ def dict_to_list(dictionary: dict) -> Tuple[List[Any], List[Any]]:
return list(keys), list(values)


@fn.NodeDecorator(
id="dict_set_default",
name="Dict Set Default",
)
def dict_set_default(dictionary: dict, key: Any, value: Any) -> dict:
result = dict(dictionary)
if key not in result:
result[key] = value
return result


@fn.NodeDecorator(
id="dict_set_key",
name="Dict Set Key",
)
def dict_set_key(dictionary: dict, key: Any, value: Any) -> dict:
result = dict(dictionary)
result[key] = value
return result


@fn.NodeDecorator(
id="dict_merge",
name="Dict Merge",
)
def dict_merge(
a: dict,
b: dict,
prefer: Annotated[Literal["a", "b"], fn.InputMeta(hidden=True)] = "b",
) -> dict:
if prefer == "a":
return {**b, **a}
return {**a, **b}


@fn.NodeDecorator(
id="dict_select_keys",
name="Dict Select Keys",
)
def dict_select_keys(
dictionary: Annotated[
dict,
fn.InputMeta(
on={
"after_set_value": fn.decorator.update_other_io_options(
"keys", lambda x: list(x.keys())
)
}
),
],
keys: List[Any],
ignore_missing: bool = True,
) -> dict:
selected: dict = {}
for key in keys:
if key in dictionary:
selected[key] = dictionary[key]
elif not ignore_missing:
raise KeyError(key)
return selected


@fn.NodeDecorator(
id="dict_rename_key",
name="Dict Rename Key",
)
def dict_rename_key(
dictionary: Annotated[
dict,
fn.InputMeta(
on={
"after_set_value": fn.decorator.update_other_io_options(
"old_key", lambda x: list(x.keys())
)
}
),
],
old_key: Any,
new_key: Any,
overwrite: bool = False,
) -> dict:
if old_key == new_key:
return dict(dictionary)

result = dict(dictionary)
if old_key not in result:
raise KeyError(old_key)
if new_key in result and not overwrite:
raise KeyError(new_key)

result[new_key] = result.pop(old_key)
return result


@fn.NodeDecorator(
id="dict_delete_key",
name="Dict Delete Key",
)
def dict_delete_key(
dictionary: Annotated[
dict,
fn.InputMeta(
on={
"after_set_value": fn.decorator.update_other_io_options(
"key", lambda x: list(x.keys())
)
}
),
],
key: Any,
ignore_missing: bool = True,
) -> dict:
result = dict(dictionary)
if key in result:
del result[key]
elif not ignore_missing:
raise KeyError(key)
return result


@fn.NodeDecorator(
id="dict_pop",
name="Dict Pop",
outputs=[
{"name": "new_dict"},
{"name": "value"},
],
)
def dict_pop(
dictionary: Annotated[
dict,
fn.InputMeta(
on={
"after_set_value": fn.decorator.update_other_io_options(
"key", lambda x: list(x.keys())
)
}
),
],
key: Any,
default: Any = fn.NoValue,
) -> Tuple[dict, Any]:
result = dict(dictionary)
if key in result:
value = result.pop(key)
return result, value
if default is fn.NoValue or default == str(fn.NoValue):
raise KeyError(key)
return result, default


@fn.NodeDecorator(
id="dict_deep_get",
name="Dict Deep Get",
)
def dict_deep_get(obj: Any, path: List[Any]) -> Any:
current = obj
for key in path:
if isinstance(current, dict):
if key in current:
current = current[key]
else:
return fn.NoValue
elif isinstance(current, (list, tuple)):
if isinstance(key, int):
index = key
elif isinstance(key, str) and key.lstrip("-").isdigit():
index = int(key)
else:
return fn.NoValue
if -len(current) <= index < len(current):
current = current[index]
else:
return fn.NoValue
else:
return fn.NoValue
return current


@fn.NodeDecorator(
id="dict_deep_set",
name="Dict Deep Set",
)
def dict_deep_set(
dictionary: dict,
path: List[Any],
value: Any,
create_missing: bool = True,
) -> dict:
if not path:
raise ValueError("path must not be empty")

if not isinstance(dictionary, dict):
raise TypeError("dictionary must be a dict")

result = dict(dictionary)
current = result

for key in path[:-1]:
if key in current:
next_value = current[key]
if not isinstance(next_value, dict):
raise TypeError(
f"Expected dict at path segment '{key}', got {type(next_value)}"
)
next_dict = dict(next_value)
else:
if not create_missing:
raise KeyError(key)
next_dict = {}

current[key] = next_dict
current = next_dict

current[path[-1]] = value
return result


NODE_SHELF = fn.Shelf(
nodes=[
DictGetNode,
Expand All @@ -105,6 +323,15 @@ def dict_to_list(dictionary: dict) -> Tuple[List[Any], List[Any]]:
dict_from_items,
dict_from_keys_values,
dict_to_list,
dict_set_default,
dict_set_key,
dict_merge,
dict_select_keys,
dict_rename_key,
dict_delete_key,
dict_pop,
dict_deep_get,
dict_deep_set,
],
name="Dicts",
description="Work with dictionaries",
Expand Down
30 changes: 29 additions & 1 deletion src/funcnodes_basic/input.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Union
from typing import Union, Any
import funcnodes_core as fn
import json


@fn.NodeDecorator(
Expand All @@ -14,6 +15,31 @@ def any_input(input: Union[str, float, int, bool]) -> str:
return input


@fn.NodeDecorator(
node_id="input.json",
node_name="JSON Input",
description="Input a JSON object",
outputs=[
{"name": "json"},
],
)
def json_input(input: str) -> Union[str, int, float, bool, dict]:
return json.loads(input, cls=fn.JSONDecoder)


@fn.NodeDecorator(
node_id="input.json_dump",
node_name="JSON Dump",
description="Dump a Python object as a JSON string",
outputs=[
{"name": "json"},
],
)
def json_dump(obj: Any, indent: int = 0, sort_keys: bool = False) -> str:
indent = int(indent)
return json.dumps(obj, indent=indent if indent > 0 else None, sort_keys=sort_keys)


@fn.NodeDecorator(
node_id="input.str",
node_name="String Input",
Expand Down Expand Up @@ -73,6 +99,8 @@ def bool_input(input: bool) -> bool:

NODE_SHELF = fn.Shelf(
nodes=[
json_input,
json_dump,
any_input,
str_input,
int_input,
Expand Down
40 changes: 40 additions & 0 deletions src/funcnodes_basic/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,44 @@ def list_slice_step(
return lst[start:end:step]


@fn.NodeDecorator(
id="list_flatten",
name="List Flatten",
description="Flatten a list by one level.",
)
def list_flatten(lst: List[Any]) -> List[Any]:
flattened: List[Any] = []
for item in lst:
if isinstance(item, (list, tuple)):
flattened.extend(item)
else:
flattened.append(item)
return flattened


@fn.NodeDecorator(
id="list_unique",
name="List Unique",
description="Remove duplicates while preserving order.",
)
def list_unique(lst: List[Any]) -> List[Any]:
unique: List[Any] = []
seen_hashable = set()

for item in lst:
try:
if item in seen_hashable:
continue
seen_hashable.add(item)
unique.append(item)
except TypeError:
if any(item == existing for existing in unique):
continue
unique.append(item)

return unique


NODE_SHELF = fn.Shelf(
nodes=[
contains,
Expand All @@ -252,6 +290,8 @@ def list_slice_step(
list_set,
list_slice,
list_slice_step,
list_flatten,
list_unique,
],
subshelves=[],
name="Lists",
Expand Down
Loading