Merge pull request #46 from Lex-DRL/dict-immutable-support

Support for arbitrary mappings and immutable dicts as inputs for dict nodes
This commit is contained in:
StableLlama
2026-03-22 21:17:54 +01:00
committed by GitHub
6 changed files with 401 additions and 204 deletions
+2
View File
@@ -8,6 +8,8 @@ on:
branches:
- master
- main
workflow_dispatch: # make manually launchable (on demand)
jobs:
build:
runs-on: ${{ matrix.os }}
+1
View File
@@ -13,6 +13,7 @@ permissions:
jobs:
publish-node:
name: Publish Custom Node to registry
if: github.repository_owner == 'StableLlama' # prevent unwanted runs on forks
runs-on: ubuntu-latest
steps:
- name: ♻️ Check out code
+1
View File
@@ -5,6 +5,7 @@ on:
branches:
- master
- main
workflow_dispatch: # make manually launchable (on demand)
jobs:
validate:
+1
View File
@@ -37,6 +37,7 @@ dependencies = []
dev = [
"bump-my-version",
"coverage", # testing
"frozendict", # an example not-a-dict-but-mapping class for dict nodes
"mypy", # linting
"pre-commit", # runs linting on commit
"pytest", # testing
+186 -66
View File
@@ -1,5 +1,10 @@
from typing import Any
from typing import (
Any, Dict, List, Mapping, Tuple, Type, TypeVar,
Union as _U, Optional as _O
)
from inspect import cleandoc
import random
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
@@ -16,6 +21,40 @@ except:
from ._dynamic_input import ContainsDynamicDict
T = TypeVar('T') # Any type.
KT = TypeVar('KT') # Key type.
KT2 = TypeVar('KT2')
KT3 = TypeVar('KT3')
KT4 = TypeVar('KT4')
VT = TypeVar('VT') # Value type.
VT2 = TypeVar('VT2')
VT3 = TypeVar('VT3')
VT4 = TypeVar('VT4')
def _output_dict_preserving_type(result: Dict[KT, VT], in_type: Type[Mapping]) -> _U[Dict[KT, VT], Mapping[KT, VT]]:
"""Internal function:
try returning the output dict as an instance of the same class as input dict.
"""
if in_type is dict:
return result
# Support other dict-like classes:
# noinspection PyBroadException
try:
return in_type(result)
except Exception:
pass
# noinspection PyBroadException
try:
return in_type(result.items())
except Exception:
return result
class DictCreate(ComfyNodeABC):
"""
Creates a new empty dictionary.
@@ -257,7 +296,7 @@ class DictCreateFromLists(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_from_lists"
def create_from_lists(self, keys: list, values: list) -> tuple[dict]:
def create_from_lists(self, keys: List[KT], values: List[VT]) -> tuple[Dict[KT, VT]]:
# Pair keys with values up to the length of the shorter list
result = dict(zip(keys, values))
return (result,)
@@ -349,8 +388,31 @@ class DictExcludeKeys(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "exclude_keys"
def exclude_keys(self, input_dict: dict, keys_to_exclude: list) -> tuple[dict]:
result = {k: v for k, v in input_dict.items() if k not in keys_to_exclude}
def exclude_keys(self, input_dict: Mapping[KT, VT], keys_to_exclude: list) -> tuple[Mapping[KT, VT]]:
if not(input_dict and keys_to_exclude):
return (input_dict,)
# `in` check is faster with sets + remove duplicates:
keys_to_exclude_set = set(keys_to_exclude)
if len(input_dict) <= len(keys_to_exclude_set):
# It's faster to rebuild the dict with filter.
result = {
k: v for k, v in input_dict.items()
if k not in keys_to_exclude_set
}
else:
# It's faster to duplicate the dict, then pop all the exclusions one-by-one.
result = dict(input_dict)
for key in keys_to_exclude_set:
if key in result:
result.pop(key)
if len(result) == len(input_dict):
# No changes made
return (input_dict,)
result = _output_dict_preserving_type(result, type(input_dict))
return (result,)
@@ -375,8 +437,33 @@ class DictFilterByKeys(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "filter_by_keys"
def filter_by_keys(self, input_dict: dict, keys: list) -> tuple[dict]:
result = {k: input_dict[k] for k in keys if k in input_dict}
def filter_by_keys(self, input_dict: Mapping[KT, VT], keys: list) -> tuple[Mapping[KT, VT]]:
if not input_dict:
return (input_dict,)
if not keys:
return (_output_dict_preserving_type({}, type(input_dict)),)
# `in` check is faster with sets + remove duplicates:
keys_set = set(keys)
if len(keys_set) <= len(input_dict):
# It's faster to iterate over preserved keys
result = {
k: input_dict[k] for k in keys_set
if k in input_dict
}
else:
# It's faster to iterate over the input dict items
result = {
k: v for k, v in input_dict.items()
if k in keys_set
}
if len(result) == len(input_dict):
# No changes made
return (input_dict,)
result = _output_dict_preserving_type(result, type(input_dict))
return (result,)
@@ -404,7 +491,7 @@ class DictFromKeys(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "from_keys"
def from_keys(self, keys: list, value=None) -> tuple[dict]:
def from_keys(self, keys: List[KT], value: VT = None) -> tuple[Dict[KT, VT]]:
return (dict.fromkeys(keys, value),)
@@ -434,7 +521,7 @@ class DictGet(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get"
def get(self, input_dict: dict, key: str, default=None) -> tuple[Any]:
def get(self, input_dict: Mapping[Any, VT], key: Any, default: T = None) -> tuple[_U[VT, T]]:
return (input_dict.get(key, default),)
@@ -459,7 +546,7 @@ class DictGetKeysValues(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_keys_values"
def get_keys_values(self, input_dict: dict) -> tuple[list, list]:
def get_keys_values(self, input_dict: Mapping[KT, VT]) -> tuple[List[KT], List[VT]]:
keys = list(input_dict.keys())
values = list(input_dict.values())
return keys, values
@@ -491,7 +578,10 @@ class DictGetMultiple(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_multiple"
def get_multiple(self, input_dict: dict, keys: list, default=None) -> tuple[list]:
def get_multiple(
self,
input_dict: Mapping[Any, VT], keys: list, default: T = None
) -> tuple[List[_U[VT, T]]]:
values = [input_dict.get(key, default) for key in keys]
return (values,)
@@ -518,14 +608,16 @@ class DictInvert(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "invert"
def invert(self, input_dict: dict) -> tuple[dict, bool]:
def invert(self, input_dict: Mapping[KT, VT]) -> tuple[Mapping[VT, KT], bool]:
try:
inverted = {v: k for k, v in input_dict.items()}
return inverted, True
except Exception:
# Return original dictionary if inversion fails (e.g., unhashable values)
return input_dict, False
inverted = _output_dict_preserving_type(inverted, type(input_dict))
return inverted, True
class DictItems(ComfyNodeABC):
"""
@@ -547,7 +639,7 @@ class DictItems(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "items"
def items(self, input_dict: dict) -> tuple[list]:
def items(self, input_dict: Mapping[KT, VT]) -> tuple[List[Tuple[KT, VT]]]:
return (list(input_dict.items()),)
@@ -570,7 +662,7 @@ class DictKeys(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "keys"
def keys(self, input_dict: dict) -> tuple[list]:
def keys(self, input_dict: Mapping[KT, Any]) -> tuple[List[KT]]:
return (list(input_dict.keys()),)
@@ -594,7 +686,7 @@ class DictLength(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "length"
def length(self, input_dict: dict) -> tuple[int]:
def length(self, input_dict: Mapping) -> tuple[int]:
return (len(input_dict),)
@@ -623,18 +715,25 @@ class DictMerge(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "merge"
def merge(self, dict1: dict, dict2=None, dict3=None, dict4=None) -> tuple[dict]:
result = dict1.copy()
def merge(
self,
dict1: Mapping[KT, VT],
dict2: Mapping[KT2, VT2] = None,
dict3: Mapping[KT3, VT3] = None,
dict4: Mapping[KT4, VT4] = None,
) -> tuple[Mapping[_U[KT, KT2, KT3, KT4], _U[VT, VT2, VT3, VT4]]]:
extra_dicts = [x for x in (dict2, dict3, dict4) if x is not None]
if not extra_dicts:
return (dict1,)
if dict2 is not None:
result.update(dict2)
if dict3 is not None:
result.update(dict3)
if dict4 is not None:
result.update(dict4)
# dict1 might be something like `frozendict` or other immutable mapping class.
# Thus, we shouldn't do `dict.copy()`,
# we must explicitly construct a `dict` object:
result = dict(dict1)
for extra in extra_dicts:
result.update(extra)
result = _output_dict_preserving_type(result, type(dict1))
return (result,)
@@ -665,18 +764,22 @@ class DictPop(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "pop"
def pop(self, input_dict: dict, key: str, default_value=None) -> tuple[dict, Any]:
result = input_dict.copy()
def pop(self, input_dict: Mapping[KT, VT], key: Any, default_value: T = None) -> tuple[Mapping[KT, VT], _U[VT, T]]:
if key not in input_dict:
return input_dict, default_value
# input_dict might be something like `frozendict` or other immutable mapping class.
# Thus, we shouldn't do `dict.copy()`,
# we must explicitly construct a `dict` object:
result = dict(input_dict)
try:
if key in result:
value = result.pop(key)
return result, value
else:
return result, default_value
value = result.pop(key)
except Exception as e:
raise ValueError(f"Error popping key from dictionary: {str(e)}")
result = _output_dict_preserving_type(result, type(input_dict))
return result, value
class DictPopItem(ComfyNodeABC):
"""
@@ -684,7 +787,8 @@ class DictPopItem(ComfyNodeABC):
This node takes a dictionary as input, removes an arbitrary key-value pair,
and returns the modified dictionary along with the removed key and value.
If the dictionary is empty, returns an error.
If operation fails, no error is thrown, but the last argument is `False`,
and the dictionary is returned intact.
"""
@classmethod
def INPUT_TYPES(cls):
@@ -700,16 +804,19 @@ class DictPopItem(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "popitem"
def popitem(self, input_dict: dict) -> tuple[dict, str, Any, bool]:
result = input_dict.copy()
def popitem(self, input_dict: Mapping[KT, VT]) -> tuple[Mapping[KT, VT], _U[KT, str], _O[VT], bool]:
if not input_dict:
return input_dict, "", None, False
result = dict(input_dict)
# noinspection PyBroadException
try:
if result:
key, value = result.popitem()
return result, key, value, True
else:
return result, "", None, False
except:
return result, "", None, False
key, value = result.popitem()
except Exception:
return input_dict, "", None, False
result = _output_dict_preserving_type(result, type(input_dict))
return result, key, value, True
class DictPopRandom(ComfyNodeABC):
@@ -738,18 +845,19 @@ class DictPopRandom(ComfyNodeABC):
def IS_CHANGED(cls, **kwargs):
return float("NaN") # Not equal to anything -> trigger recalculation
def pop_random(self, input_dict: dict) -> tuple[dict, str, Any, bool]:
import random
result = input_dict.copy()
def pop_random(self, input_dict: Mapping[KT, VT]) -> tuple[Mapping[KT, VT], _U[KT, str], _O[VT], bool]:
if not input_dict:
return input_dict, "", None, False
result = dict(input_dict)
try:
if result:
random_key = random.choice(list(result.keys()))
random_value = result.pop(random_key)
return result, random_key, random_value, True
else:
return result, "", None, False
except:
return result, "", None, False
random_key = random.choice(list(result.keys()))
random_value = result.pop(random_key)
except Exception:
return input_dict, "", None, False
result = _output_dict_preserving_type(result, type(input_dict))
return result, random_key, random_value, True
class DictRemove(ComfyNodeABC):
@@ -775,12 +883,15 @@ class DictRemove(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "remove"
def remove(self, input_dict: dict, key: str) -> tuple[dict, bool]:
result = input_dict.copy()
if key in result:
del result[key]
return result, True
return result, False
def remove(self, input_dict: Mapping[KT, VT], key: Any) -> tuple[Mapping[KT, VT], bool]:
if key not in input_dict:
return input_dict, False
result = dict(input_dict)
del result[key]
result = _output_dict_preserving_type(result, type(input_dict))
return result, True
class DictSet(ComfyNodeABC):
@@ -805,9 +916,11 @@ class DictSet(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "set"
def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]:
result = input_dict.copy()
def set(self, input_dict: Mapping[KT, VT], key: KT2, value: VT2) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]]]:
result = dict(input_dict)
result[key] = value
result = _output_dict_preserving_type(result, type(input_dict))
return (result,)
@@ -835,9 +948,11 @@ class DictSetDefault(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "setdefault"
def setdefault(self, input_dict: dict, key: str, default_value=None) -> tuple[dict, Any]:
result = input_dict.copy()
def setdefault(self, input_dict: Mapping[KT, VT], key: KT2, default_value: VT2 = None) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]], _U[VT, VT2]]:
result = dict(input_dict)
value = result.setdefault(key, default_value)
result = _output_dict_preserving_type(result, type(input_dict))
return result, value
@@ -863,9 +978,14 @@ class DictUpdate(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "update"
def update(self, dict1: dict, dict2: dict) -> tuple[dict]:
result = dict1.copy()
def update(self, dict1: Mapping[KT, VT], dict2: Mapping[KT2, VT2]) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]]]:
if not dict2:
return (dict1,)
result = dict(dict1)
result.update(dict2)
result = _output_dict_preserving_type(result, type(dict1))
return (result,)
@@ -888,7 +1008,7 @@ class DictValues(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "values"
def values(self, input_dict: dict) -> tuple[list]:
def values(self, input_dict: Mapping[Any, VT]) -> tuple[List[VT]]:
return (list(input_dict.values()),)
+210 -138
View File
@@ -1,4 +1,5 @@
#import pytest
from frozendict import frozendict
import pytest
from src.basic_data_handling.dict_nodes import (
DictCompare,
DictContainsKey,
@@ -29,28 +30,49 @@ from src.basic_data_handling.dict_nodes import (
DictValues,
)
# frozendict - as an example of not-a-dict-subclass-yet-mapping class:
_tested_dict_types = (dict, frozendict)
_dict_x1 = {"key1": "value1"}
_dict_x2 = {"key1": "value1", "key2": "value2"}
_dict_x3 = {"key1": "value1", "key2": "value2", "key3": "value3"}
_dict_b = {"key2": "value2"}
def test_dict_create():
node = DictCreate()
assert node.create() == ({},) # Creates an empty dictionary
def test_dict_get():
@pytest.mark.parametrize(
"in_dict, key, default, expected, message", [
(_dict_x3, "key1", None, "value1", "existing key"),
(_dict_x3, "non_existent", None, None, "missing key, no default"),
(_dict_x3, "key99", "default_value", "default_value", "missing key with default"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_get(dict_type, in_dict, key, default, expected, message):
node = DictGet()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.get(my_dict, "key1") == ("value1",)
assert node.get(my_dict, "key3", default="default_value") == ("default_value",)
# Test with no default specified
assert node.get(my_dict, "non_existent") == (None,)
my_dict = dict_type(in_dict)
assert node.get(my_dict, key, default=default) == (expected,), f"Wrong result: {message}"
def test_dict_set():
@pytest.mark.parametrize(
"in_dict, key, value, expected, message", [
(_dict_x1, "key2", "value2", _dict_x2, "base case"),
(_dict_x1, "key1", "new_value", {"key1": "new_value"}, "overwriting existing key"),
({}, "key", "value", {"key": "value"}, "empty dict"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_set(dict_type, in_dict, key, value, expected, message):
node = DictSet()
my_dict = {"key1": "value1"}
assert node.set(my_dict, "key2", "value2") == ({"key1": "value1", "key2": "value2"},)
# Test overwriting existing key
assert node.set(my_dict, "key1", "new_value") == ({"key1": "new_value"},)
# Test with empty dict
assert node.set({}, "key", "value") == ({"key": "value"},)
my_dict = dict_type(in_dict)
result = node.set(my_dict, key, value)
assert result == (dict_type(expected),), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_create_from_boolean():
node = DictCreateFromBoolean()
@@ -83,15 +105,16 @@ def test_dict_create_from_string():
node = DictCreateFromString()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0="value1", key_1="key2", value_1="value2", key_2="", value_2="")
assert result == ({"key1": "value1", "key2": "value2"},)
assert result == (_dict_x2,)
# Test with empty inputs
assert node.create() == ({},)
def test_dict_pop_random():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_pop_random(dict_type):
node = DictPopRandom()
# Test with non-empty dictionary
my_dict = {"key1": "value1", "key2": "value2"}
my_dict = dict_type(_dict_x2)
result_dict, key, value, success = node.pop_random(my_dict)
# Check that operation was successful
@@ -100,53 +123,57 @@ def test_dict_pop_random():
assert len(result_dict) == len(my_dict) - 1
# Check that removed key is not in result dict
assert key not in result_dict
assert type(result_dict) == dict_type
# Check that the original key-value pair matches
assert my_dict[key] == value
# Test with empty dictionary
empty_result_dict, empty_key, empty_value, empty_success = node.pop_random({})
assert empty_result_dict == {}
empty_result_dict, empty_key, empty_value, empty_success = node.pop_random(dict_type({}))
assert empty_result_dict == dict_type({})
assert type(empty_result_dict) == dict_type
assert empty_key == ""
assert empty_value is None
assert empty_success is False
def test_dict_keys():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_keys(dict_type):
node = DictKeys()
my_dict = {"key1": "value1", "key2": "value2"}
my_dict = dict_type(_dict_x2)
assert node.keys(my_dict) == (["key1", "key2"],)
# Test with empty dict
assert node.keys({}) == ([],)
assert node.keys(dict_type({})) == ([],)
def test_dict_values():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_values(dict_type):
node = DictValues()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.values(my_dict) == (["value1", "value2"],)
assert node.values(dict_type(_dict_x2)) == (["value1", "value2"],)
# Test with empty dict
assert node.values({}) == ([],)
assert node.values(dict_type({})) == ([],)
def test_dict_items():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_items(dict_type):
node = DictItems()
my_dict = {"key1": "value1", "key2": "value2"}
my_dict = dict_type(_dict_x2)
# Note that the order might not be preserved, so we check if items are in the result
items = node.items(my_dict)[0]
assert len(items) == 2
assert ("key1", "value1") in items
assert ("key2", "value2") in items
# Test with empty dict
assert node.items({}) == ([],)
assert node.items(dict_type({})) == ([],)
def test_dict_contains_key():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_contains_key(dict_type):
node = DictContainsKey()
my_dict = {"key1": "value1"}
my_dict = dict_type(_dict_x1)
assert node.contains_key(my_dict, "key1") == (True,)
assert node.contains_key(my_dict, "key2") == (False,)
# Test with empty dict
assert node.contains_key({}, "any_key") == (False,)
assert node.contains_key(dict_type({}), "any_key") == (False,)
def test_dict_from_keys():
@@ -159,22 +186,31 @@ def test_dict_from_keys():
assert node.from_keys([]) == ({},)
def test_dict_pop():
@pytest.mark.parametrize(
"in_dict, key, default, out_dict, pop_value, message", [
(_dict_x2, "key1", None, _dict_b, "value1", "base case"),
(_dict_x2, "non_existent", "default", _dict_x2, "default", "non-existent key (with default)"),
({"a": 1}, "b", None, {"a": 1}, None, "non-existent key (no default)"),
({}, "key", None, {}, None, "empty dict"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_pop(dict_type, in_dict, key, default, out_dict, pop_value, message):
node = DictPop()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.pop(my_dict, "key1") == ({"key2": "value2"}, "value1")
# Test with default value for non-existent key
assert node.pop(my_dict, "non_existent", default_value="default") == (my_dict, "default")
# Test with no default for non-existent key - should not modify dict
assert node.pop({"a": 1}, "b") == ({"a": 1}, None)
my_dict = dict_type(in_dict)
result = node.pop(my_dict, key, default_value=default)
assert result == (dict_type(out_dict), pop_value), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_pop_item():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_pop_item(dict_type):
node = DictPopItem()
my_dict = {"key1": "value1"}
my_dict = dict_type(_dict_x1)
result = node.popitem(my_dict)
# Since we only have one item, we know what should be popped
assert result[0] == {} # remaining dict is empty
assert result[0] == dict_type({}) # remaining dict is empty
assert type(result[0]) == dict_type
assert result[1] == "key1" # popped key
assert result[2] == "value1" # popped value
assert result[3] is True # success
@@ -182,107 +218,139 @@ def test_dict_pop_item():
assert node.popitem({}) == ({}, "", None, False)
def test_dict_set_default():
@pytest.mark.parametrize(
"in_dict, key, default, out_dict, out_value, message", [
(_dict_x1, "key2", "default", {"key1": "value1", "key2": "default"}, "default", "key that doesn't exist"),
(_dict_x1, "key1", "new_default", _dict_x1, "value1", "key that already exists"),
({}, "key", "value", {"key": "value"}, "value", "empty dict"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_set_default(dict_type, in_dict, key, default, out_dict, out_value, message):
node = DictSetDefault()
my_dict = {"key1": "value1"}
# Test setting a key that doesn't exist
assert node.setdefault(my_dict, "key2", "default") == ({"key1": "value1", "key2": "default"}, "default")
# Test with a key that already exists
assert node.setdefault(my_dict, "key1", "new_default") == ({"key1": "value1"}, "value1")
# Test with empty dict
assert node.setdefault({}, "key", "value") == ({"key": "value"}, "value")
my_dict = dict_type(in_dict)
result = node.setdefault(my_dict, key, default)
assert result == (dict_type(out_dict), out_value), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_update():
@pytest.mark.parametrize(
"in_dict, update_dict, expected, message", [
(_dict_x1, _dict_b, _dict_x2, "base case"),
({"a": 1, "b": 2}, {"b": 3, "c": 4}, {"a": 1, "b": 3, "c": 4}, "overlapping keys"),
(_dict_x1, {}, _dict_x1, "empty update dict"),
({}, _dict_b, _dict_b, "empty original dict"),
]
)
@pytest.mark.parametrize("update_type", _tested_dict_types)
@pytest.mark.parametrize("in_type", _tested_dict_types)
def test_dict_update(in_type, update_type, in_dict, update_dict, expected, message):
node = DictUpdate()
my_dict = {"key1": "value1"}
update_dict = {"key2": "value2"}
assert node.update(my_dict, update_dict) == ({"key1": "value1", "key2": "value2"},)
# Test updating with overlapping keys
assert node.update({"a": 1, "b": 2}, {"b": 3, "c": 4}) == ({"a": 1, "b": 3, "c": 4},)
# Test with empty update dict
assert node.update(my_dict, {}) == (my_dict,)
# Test with empty original dict
assert node.update({}, update_dict) == (update_dict,)
result = node.update(in_type(in_dict), update_type(update_dict))
assert result == (in_type(expected),), f"Wrong result: {message}"
assert type(result[0]) == in_type, f"Wrong type: {message}"
def test_dict_length():
@pytest.mark.parametrize("num", [0, 3, 6, 9, 12, 15])
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_length(dict_type, num):
node = DictLength()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.length(my_dict) == (2,)
# Test with empty dict
assert node.length({}) == (0,)
my_dict = dict_type({f"key{x}": f"value{x}" for x in range(1, num+1)})
assert node.length(my_dict) == (num,)
def test_dict_merge():
@pytest.mark.parametrize(
"dict_a, other_dicts, expected, message", [
(_dict_x1, [_dict_b], _dict_x2, "basic merge"),
({"a": 1}, [{"a": 2}], {"a": 2}, "overlapping keys (later dicts override earlier ones)"),
({"a": 1}, [{"b": 2}, {"c": 3}], {"a": 1, "b": 2, "c": 3}, "more than two dicts"),
({}, [], {}, "empty dict - v1"),
(_dict_x1, [], _dict_x1, "empty dict - v2"),
(_dict_x1, [{}], _dict_x1, "empty dict - v3"),
(_dict_x1, [{}, {}, {}], _dict_x1, "empty dict - v4"),
({}, [_dict_x1], _dict_x1, "empty dict - v5"),
({}, [_dict_x1, _dict_b], _dict_x2, "empty first dict"),
]
)
@pytest.mark.parametrize("type_b", _tested_dict_types)
@pytest.mark.parametrize("type_a", _tested_dict_types)
def test_dict_merge(type_a, type_b, dict_a, other_dicts, expected, message):
node = DictMerge()
dict1 = {"key1": "value1"}
dict2 = {"key2": "value2"}
# Test basic merge
assert node.merge(dict1, dict2) == ({"key1": "value1", "key2": "value2"},)
# Test with overlapping keys (later dicts override earlier ones)
assert node.merge({"a": 1}, {"a": 2}) == ({"a": 2},)
# Test with more than two dicts
result = node.merge({"a": 1}, {"b": 2}, {"c": 3})
assert result == ({"a": 1, "b": 2, "c": 3},)
# Test with empty dicts
assert node.merge(dict1, {}) == (dict1,)
assert node.merge({}, dict1) == (dict1,)
dict1 = type_a(dict_a)
dicts_extra = tuple(type_b(d) for d in other_dicts)
result = node.merge(dict1, *dicts_extra)
assert result == (type_a(expected),), f"Wrong result: {message}"
assert type(result[0]) == type_a, f"Wrong type: {message}"
def test_dict_get_keys_values():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_get_keys_values(dict_type):
node = DictGetKeysValues()
my_dict = {"key1": "value1", "key2": "value2"}
my_dict = dict_type(_dict_x2)
keys, values = node.get_keys_values(my_dict)
# Check keys and values contents (order may vary)
assert set(keys) == {"key1", "key2"}
assert set(values) == {"value1", "value2"}
# Test with empty dict
assert node.get_keys_values({}) == ([], [])
assert node.get_keys_values(dict_type({})) == ([], [])
def test_dict_remove():
@pytest.mark.parametrize(
"in_dict, key, expected, success, message", [
(_dict_x2, "key1", _dict_b, True, "successful removal"),
(_dict_x2, "non_existent", _dict_x2, False, "removal of non-existent key"),
({}, "any_key", {}, False, "empty dict"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_remove(dict_type, in_dict, key, expected, success, message):
node = DictRemove()
my_dict = {"key1": "value1", "key2": "value2"}
# Test successful removal
assert node.remove(my_dict, "key1") == ({"key2": "value2"}, True)
# Test removal of non-existent key
assert node.remove(my_dict, "non_existent") == (my_dict, False)
# Test with empty dict
assert node.remove({}, "any_key") == ({}, False)
my_dict = dict_type(in_dict)
result = node.remove(my_dict, key)
assert result == (dict_type(expected), success), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_filter_by_keys():
@pytest.mark.parametrize(
"in_dict, keys, expected, message", [
(_dict_x3, ["key1", "key3"], {"key1": "value1", "key3": "value3"}, "subset of keys"),
(_dict_x3, ["key1", "non_existent"], _dict_x1, "non-existent keys"),
(_dict_x3, [], {}, "empty keys list"),
(_dict_x3, list(_dict_x3.keys()), _dict_x3, "all keys"),
({}, ["any_key"], {}, "empty dict"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_filter_by_keys(dict_type, in_dict, keys, expected, message):
node = DictFilterByKeys()
my_dict = {"key1": "value1", "key2": "value2", "key3": "value3"}
# Test with subset of keys
assert node.filter_by_keys(my_dict, ["key1", "key3"]) == ({"key1": "value1", "key3": "value3"},)
# Test with non-existent keys
assert node.filter_by_keys(my_dict, ["key1", "non_existent"]) == ({"key1": "value1"},)
# Test with empty keys list
assert node.filter_by_keys(my_dict, []) == ({},)
# Test with empty dict
assert node.filter_by_keys({}, ["any_key"]) == ({},)
my_dict = dict_type(in_dict)
result = node.filter_by_keys(my_dict, keys)
assert result == (dict_type(expected),), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_exclude_keys():
@pytest.mark.parametrize(
"in_dict, keys, expected, message", [
(_dict_x3, ["key1", "key3"], _dict_b, "excluding some keys"),
(_dict_x3, list(_dict_x3.keys()), {}, "excluding all keys"),
(_dict_x3, ["non_existent"], _dict_x3, "excluding non-existent keys"),
(_dict_x3, [], _dict_x3, "empty exclude list"),
({}, ["any_key"], {}, "empty dict"),
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_exclude_keys(dict_type, in_dict, keys, expected, message):
node = DictExcludeKeys()
my_dict = {"key1": "value1", "key2": "value2", "key3": "value3"}
# Test excluding some keys
assert node.exclude_keys(my_dict, ["key1", "key3"]) == ({"key2": "value2"},)
# Test excluding non-existent keys
assert node.exclude_keys(my_dict, ["non_existent"]) == (my_dict,)
# Test excluding all keys
assert node.exclude_keys(my_dict, ["key1", "key2", "key3"]) == ({},)
# Test with empty exclude list
assert node.exclude_keys(my_dict, []) == (my_dict,)
# Test with empty dict
assert node.exclude_keys({}, ["any_key"]) == ({},)
my_dict = dict_type(in_dict)
result = node.exclude_keys(my_dict, keys)
assert result == (dict_type(expected),), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_get_multiple():
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_get_multiple(dict_type):
node = DictGetMultiple()
my_dict = {"key1": "value1", "key2": "value2"}
my_dict = dict_type(_dict_x2)
# Test getting existing keys
assert node.get_multiple(my_dict, ["key1", "key2"]) == (["value1", "value2"],)
# Test with mix of existing and non-existent keys
@@ -292,21 +360,23 @@ def test_dict_get_multiple():
# Test with empty keys list
assert node.get_multiple(my_dict, []) == ([],)
# Test with empty dict
assert node.get_multiple({}, ["key1"], default="default") == (["default"],)
assert node.get_multiple(dict_type({}), ["key1"], default="default") == (["default"],)
def test_dict_invert():
@pytest.mark.parametrize(
"in_dict, out_dict, success, message", [
(_dict_x2, {"value1": "key1", "value2": "key2"}, True, "basic inversion"),
({"key1": "value", "key2": "value"}, {"value": "key2"}, True, "duplicated values - last key wins"),
({}, {}, True, "empty dict"),
# TODO: `False` success
]
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_invert(dict_type, in_dict, out_dict, success, message):
node = DictInvert()
my_dict = {"key1": "value1", "key2": "value2"}
# Test basic inversion
assert node.invert(my_dict) == ({"value1": "key1", "value2": "key2"}, True)
# Test with duplicated values
result, success = node.invert({"key1": "value", "key2": "value"})
# With duplicate values, the last key wins
assert result == {"value": "key2"}
assert success is True
# Test with empty dict
assert node.invert({}) == ({}, True)
result = node.invert(dict_type(in_dict))
assert result == (dict_type(out_dict), success), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_create_from_lists():
@@ -314,37 +384,39 @@ def test_dict_create_from_lists():
keys = ["key1", "key2", "key3"]
values = ["value1", "value2", "value3"]
# Test with matching length lists
assert node.create_from_lists(keys, values) == ({"key1": "value1", "key2": "value2", "key3": "value3"},)
assert node.create_from_lists(keys, values) == (_dict_x3,)
# Test with more keys than values
assert node.create_from_lists(keys, ["value1", "value2"]) == ({"key1": "value1", "key2": "value2"},)
assert node.create_from_lists(keys, ["value1", "value2"]) == (_dict_x2,)
# Test with more values than keys
assert node.create_from_lists(["key1", "key2"], values) == ({"key1": "value1", "key2": "value2"},)
assert node.create_from_lists(["key1", "key2"], values) == (_dict_x2,)
# Test with empty lists
assert node.create_from_lists([], []) == ({},)
def test_dict_compare():
@pytest.mark.parametrize("b_type", _tested_dict_types)
@pytest.mark.parametrize("a_type", _tested_dict_types)
def test_dict_compare(a_type, b_type):
node = DictCompare()
# Test identical dictionaries
dict1 = {"key1": "value1", "key2": "value2"}
dict2 = {"key1": "value1", "key2": "value2"}
assert node.compare(dict1, dict2) == (True, [], [], [])
dict_a = a_type(_dict_x2)
dict_b = b_type(_dict_x2)
assert node.compare(dict_a, dict_b) == (True, [], [], [])
# Test dictionaries with different values
dict3 = {"key1": "value1", "key2": "different"}
are_equal, only_in_1, only_in_2, diff_values = node.compare(dict1, dict3)
dict_b = b_type({"key1": "value1", "key2": "different"})
are_equal, only_in_1, only_in_2, diff_values = node.compare(dict_a, dict_b)
assert are_equal is False
assert only_in_1 == []
assert only_in_2 == []
assert "key2" in diff_values
# Test dictionaries with different keys
dict4 = {"key1": "value1", "key3": "value3"}
are_equal, only_in_1, only_in_2, diff_values = node.compare(dict1, dict4)
dict_b = b_type({"key1": "value1", "key3": "value3"})
are_equal, only_in_1, only_in_2, diff_values = node.compare(dict_a, dict_b)
assert are_equal is False
assert "key2" in only_in_1
assert "key3" in only_in_2
assert diff_values == []
# Test empty dictionaries
assert node.compare({}, {}) == (True, [], [], [])
assert node.compare(a_type({}), b_type({})) == (True, [], [], [])