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:
@@ -8,6 +8,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
workflow_dispatch: # make manually launchable (on demand)
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
workflow_dispatch: # make manually launchable (on demand)
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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, [], [], [])
|
||||
|
||||
Reference in New Issue
Block a user