From a513a0a9214b0ee56913bc13a3f038a053ebc482 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:43:17 -0300 Subject: [PATCH 01/10] github actions: allow manual runs --- .github/workflows/build-pipeline.yml | 2 ++ .github/workflows/validate.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/build-pipeline.yml b/.github/workflows/build-pipeline.yml index f9bdcbd..9b1e41d 100644 --- a/.github/workflows/build-pipeline.yml +++ b/.github/workflows/build-pipeline.yml @@ -8,6 +8,8 @@ on: branches: - master - main + workflow_dispatch: # make manually launchable (on demand) + jobs: build: runs-on: ${{ matrix.os }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 94cc78d..0b97ca2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,6 +5,7 @@ on: branches: - master - main + workflow_dispatch: # make manually launchable (on demand) jobs: validate: From 1d02165bcbbcb84696d5667f9186de748f988ef4 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:56:40 -0300 Subject: [PATCH 02/10] github actions: prevent unwanted `publish_node.yml` runs on forks --- .github/workflows/publish_node.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish_node.yml b/.github/workflows/publish_node.yml index eb334c1..49e25b5 100644 --- a/.github/workflows/publish_node.yml +++ b/.github/workflows/publish_node.yml @@ -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 From c8717e602ddc30519205e832cd1366502ab9efbf Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:49:43 -0300 Subject: [PATCH 03/10] support for immutable dicts --- src/basic_data_handling/dict_nodes.py | 38 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 2393634..deb71c2 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -1,5 +1,6 @@ from typing import Any from inspect import cleandoc +from itertools import chain try: from comfy.comfy_types.node_typing import IO, ComfyNodeABC @@ -624,17 +625,18 @@ def INPUT_TYPES(cls): FUNCTION = "merge" def merge(self, dict1: dict, dict2=None, dict3=None, dict4=None) -> tuple[dict]: - result = dict1.copy() - - if dict2 is not None: - result.update(dict2) - - if dict3 is not None: - result.update(dict3) - - if dict4 is not None: - result.update(dict4) - + extra_dicts = [x for x in (dict2, dict3, dict4) if x is not None] + if not extra_dicts: + return (dict1,) + + # dict1 might be something like frozendict or other immutable mapping class. + # Thus, we shouldn't just copy-and-update, + # but instead we should build a new instance of the same type: + dict1_type = type(dict1) + result = dict1_type(chain( + dict1.items(), + *(x.items() for x in extra_dicts), + )) return (result,) @@ -806,8 +808,11 @@ def INPUT_TYPES(cls): FUNCTION = "set" def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]: - result = input_dict.copy() - result[key] = value + # input_dict might be something like frozendict or other immutable mapping class. + # Thus, we shouldn't just copy-and-update, + # but instead we should build a new instance of the same type: + dict_type = type(input_dict) + result = dict_type(chain(input_dict.items(), (key, value))) return (result,) @@ -864,8 +869,11 @@ def INPUT_TYPES(cls): FUNCTION = "update" def update(self, dict1: dict, dict2: dict) -> tuple[dict]: - result = dict1.copy() - result.update(dict2) + # dict1 might be something like frozendict or other immutable mapping class. + # Thus, we shouldn't just copy-and-update, + # but instead we should build a new instance of the same type: + dict1_type = type(dict1) + result = dict1_type(chain(dict1.items(), dict2.items())) return (result,) From adb91b188efb7786b6fd1103f6d60705cbbb4533 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:13:34 -0300 Subject: [PATCH 04/10] + type hints --- src/basic_data_handling/dict_nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index deb71c2..d3a72fa 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -624,7 +624,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "merge" - def merge(self, dict1: dict, dict2=None, dict3=None, dict4=None) -> tuple[dict]: + def merge(self, dict1: dict, dict2: dict = None, dict3: dict = None, dict4: dict = None) -> tuple[dict]: extra_dicts = [x for x in (dict2, dict3, dict4) if x is not None] if not extra_dicts: return (dict1,) From 3a14652dc9a3365e8fd32daf12b5327a779a66c8 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:08:00 -0300 Subject: [PATCH 05/10] Update all the remaining dict-modifying nodes + change approach to intermediate dict -> preserve type --- src/basic_data_handling/dict_nodes.py | 192 ++++++++++++++++++-------- 1 file changed, 137 insertions(+), 55 deletions(-) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index d3a72fa..eddfaa3 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -1,6 +1,7 @@ -from typing import Any +from typing import Any, Mapping, Type + from inspect import cleandoc -from itertools import chain +import random try: from comfy.comfy_types.node_typing import IO, ComfyNodeABC @@ -17,6 +18,24 @@ class IO: from ._dynamic_input import ContainsDynamicDict +def _output_dict_preserving_type(result: dict, in_type: Type[Mapping]): + 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. @@ -351,7 +370,30 @@ def INPUT_TYPES(cls): 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} + 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,) @@ -377,7 +419,30 @@ def INPUT_TYPES(cls): 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} + if not(input_dict and keys): + return (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,) @@ -522,11 +587,13 @@ def INPUT_TYPES(cls): def invert(self, input_dict: dict) -> tuple[dict, 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): """ @@ -629,14 +696,14 @@ def merge(self, dict1: dict, dict2: dict = None, dict3: dict = None, dict4: dict if not extra_dicts: return (dict1,) - # dict1 might be something like frozendict or other immutable mapping class. - # Thus, we shouldn't just copy-and-update, - # but instead we should build a new instance of the same type: - dict1_type = type(dict1) - result = dict1_type(chain( - dict1.items(), - *(x.items() for x in extra_dicts), - )) + # 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,) @@ -668,17 +735,21 @@ def INPUT_TYPES(cls): FUNCTION = "pop" def pop(self, input_dict: dict, key: str, default_value=None) -> tuple[dict, Any]: - result = input_dict.copy() + 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): """ @@ -686,7 +757,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): @@ -703,15 +775,18 @@ def INPUT_TYPES(cls): FUNCTION = "popitem" def popitem(self, input_dict: dict) -> tuple[dict, str, Any, bool]: - result = input_dict.copy() + 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): @@ -741,17 +816,18 @@ 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() + 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): @@ -778,11 +854,14 @@ def INPUT_TYPES(cls): 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 + 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): @@ -808,11 +887,10 @@ def INPUT_TYPES(cls): FUNCTION = "set" def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]: - # input_dict might be something like frozendict or other immutable mapping class. - # Thus, we shouldn't just copy-and-update, - # but instead we should build a new instance of the same type: - dict_type = type(input_dict) - result = dict_type(chain(input_dict.items(), (key, value))) + result = dict(input_dict) + result[key] = value + + result = _output_dict_preserving_type(result, type(input_dict)) return (result,) @@ -840,9 +918,11 @@ def INPUT_TYPES(cls): 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: dict, key: str, default_value: Any = None) -> tuple[dict, Any]: + result = dict(input_dict) value = result.setdefault(key, default_value) + + result = _output_dict_preserving_type(result, type(input_dict)) return result, value @@ -869,11 +949,13 @@ def INPUT_TYPES(cls): FUNCTION = "update" def update(self, dict1: dict, dict2: dict) -> tuple[dict]: - # dict1 might be something like frozendict or other immutable mapping class. - # Thus, we shouldn't just copy-and-update, - # but instead we should build a new instance of the same type: - dict1_type = type(dict1) - result = dict1_type(chain(dict1.items(), dict2.items())) + if not dict2: + return (dict1,) + + result = dict(dict1) + result.update(dict2) + + result = _output_dict_preserving_type(result, type(dict1)) return (result,) From 48983f226819066663a19f3d0bd16d830bb1d56e Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:59:15 -0300 Subject: [PATCH 06/10] updated type-hints (keeping track of key/value type) --- src/basic_data_handling/dict_nodes.py | 69 ++++++++++++++++++--------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index eddfaa3..2b75b31 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -1,4 +1,7 @@ -from typing import Any, Mapping, Type +from typing import ( + Any, Dict, List, Mapping, Tuple, Type, TypeVar, + Union as _U, Optional as _O +) from inspect import cleandoc import random @@ -18,7 +21,20 @@ class IO: from ._dynamic_input import ContainsDynamicDict -def _output_dict_preserving_type(result: dict, in_type: Type[Mapping]): +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]]: if in_type is dict: return result @@ -277,7 +293,7 @@ def INPUT_TYPES(cls): 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,) @@ -369,7 +385,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "exclude_keys" - def exclude_keys(self, input_dict: dict, keys_to_exclude: list) -> tuple[dict]: + 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,) @@ -418,7 +434,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "filter_by_keys" - def filter_by_keys(self, input_dict: dict, keys: list) -> tuple[dict]: + def filter_by_keys(self, input_dict: Mapping[KT, VT], keys: list) -> tuple[Mapping[KT, VT]]: if not(input_dict and keys): return (input_dict,) @@ -470,7 +486,7 @@ def INPUT_TYPES(cls): 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),) @@ -500,7 +516,7 @@ def INPUT_TYPES(cls): 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),) @@ -525,7 +541,7 @@ def INPUT_TYPES(cls): 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 @@ -557,7 +573,10 @@ def INPUT_TYPES(cls): 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,) @@ -584,7 +603,7 @@ def INPUT_TYPES(cls): 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()} except Exception: @@ -615,7 +634,7 @@ def INPUT_TYPES(cls): 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()),) @@ -638,7 +657,7 @@ def INPUT_TYPES(cls): 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()),) @@ -662,7 +681,7 @@ def INPUT_TYPES(cls): 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),) @@ -691,7 +710,13 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "merge" - def merge(self, dict1: dict, dict2: dict = None, dict3: dict = None, dict4: dict = None) -> tuple[dict]: + 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,) @@ -734,7 +759,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" - def pop(self, input_dict: dict, key: str, default_value=None) -> tuple[dict, Any]: + 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 @@ -774,7 +799,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "popitem" - def popitem(self, input_dict: dict) -> tuple[dict, str, Any, bool]: + 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 @@ -815,7 +840,7 @@ def INPUT_TYPES(cls): 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]: + 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 @@ -853,7 +878,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "remove" - def remove(self, input_dict: dict, key: str) -> tuple[dict, bool]: + 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 @@ -886,7 +911,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set" - def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]: + 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 @@ -918,7 +943,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "setdefault" - def setdefault(self, input_dict: dict, key: str, default_value: Any = None) -> tuple[dict, Any]: + 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) @@ -948,7 +973,7 @@ def INPUT_TYPES(cls): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "update" - def update(self, dict1: dict, dict2: dict) -> tuple[dict]: + def update(self, dict1: Mapping[KT, VT], dict2: Mapping[KT2, VT2]) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]]]: if not dict2: return (dict1,) @@ -978,7 +1003,7 @@ def INPUT_TYPES(cls): 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()),) From 682f6142892ce298260aede6c1247068fedecaa9 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:04:55 -0300 Subject: [PATCH 07/10] + docstring --- src/basic_data_handling/dict_nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 2b75b31..89b3d03 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -35,6 +35,9 @@ class IO: 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 From e7a041e7d6f1bb14ac971aeb76d2767b5a528c20 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:47:15 -0300 Subject: [PATCH 08/10] fix failed test --- src/basic_data_handling/dict_nodes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 89b3d03..0f01ebd 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -438,8 +438,10 @@ def INPUT_TYPES(cls): FUNCTION = "filter_by_keys" def filter_by_keys(self, input_dict: Mapping[KT, VT], keys: list) -> tuple[Mapping[KT, VT]]: - if not(input_dict and keys): + 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) From 0b401a15e43dae22468533fda8010380970332c7 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:49:11 -0300 Subject: [PATCH 09/10] `frozendict` dev-dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 211a825..f0fd4d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 From b90ca0696e91e573c5339a64171f4b67fff5e774 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:52:32 -0300 Subject: [PATCH 10/10] Tests update: also check for `frozendict` input --- tests/test_dict_nodes.py | 368 +++++++++++++++++++++++---------------- 1 file changed, 220 insertions(+), 148 deletions(-) diff --git a/tests/test_dict_nodes.py b/tests/test_dict_nodes.py index f314a0d..81e2417 100644 --- a/tests/test_dict_nodes.py +++ b/tests/test_dict_nodes.py @@ -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 @@ 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}" + + +@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() + 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_update(): - 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,) - - -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,) + + +@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 = 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_merge(): - 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,) - - -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) - - -def test_dict_filter_by_keys(): + 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}" + + +@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"]) == ({},) - - -def test_dict_exclude_keys(): + 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}" + + +@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, [], [], [])