From 331cf9f1665213b65660bb362db885b2c7b698b0 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:28:40 -0300 Subject: [PATCH] `nodes_dict`: `DictAddAny` --- __meta.py | 2 ++ __typing.py | 4 ++- _io_custom.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ nodes_dict.py | 76 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 _io_custom.py create mode 100644 nodes_dict.py diff --git a/__meta.py b/__meta.py index 0714032..eac65e6 100644 --- a/__meta.py +++ b/__meta.py @@ -5,3 +5,5 @@ Metadata-related module. category: str = "String Constructor" category_dict: str = f"{category}/Format-Dict" +category_old: str = f"{category}/_old" +pack_id_suffix: str = " [String Constructor]" diff --git a/__typing.py b/__typing.py index 5ae9d6b..dec7f96 100644 --- a/__typing.py +++ b/__typing.py @@ -2,7 +2,9 @@ """ """ -from typing import TypeVar as _TypeVar +import typing as _t +from typing import Any as _A, Optional as _O, Union as _U, TypeVar as _TypeVar T = _TypeVar('T') T2 = _TypeVar('T2') +FormatDict = _TypeVar('FormatDict', bound=_U[_t.Dict[str, _A], _t.Mapping[str, _A]]) diff --git a/_io_custom.py b/_io_custom.py new file mode 100644 index 0000000..f3f9bf6 --- /dev/null +++ b/_io_custom.py @@ -0,0 +1,85 @@ +# encoding: utf-8 +""" +""" + +from dataclasses import fields as __dataclass_fields + +from comfy_api.latest import io as _io + +from .__meta import category_old as _category_old +from .__typing import _t, T as _T + + +_DICT = _io.Custom("DICT") +_DICT_INPUT_OPTIONAL = _DICT.Input( + 'dict', + optional=True, + tooltip="An (optional) Format-Dictionary to work with." +) +_DICT_OUTPUT = _DICT.Output( + 'DICT', + display_name='dict' +) + +_T_Input = _t.TypeVar('T_Input', bound=_io.Input) + + +def __dataclass_fields_dict(obj) -> _t.Dict[str, _t.Any]: + """Return all the field values from a dataclass - as dict. No deep-copy.""" + field_names = (field.name for field in __dataclass_fields(obj)) + return { + attr: getattr(obj, attr) + for attr in field_names + } + + +def _dataclass_override(obj: _T, **overrides) -> _T: + """Copy a dataclass instance with optional overrides.""" + obj_dict = __dataclass_fields_dict(obj) + obj_dict.update(overrides) + obj_type = type(obj) + return obj_type(**obj_dict) + + +def _input_override(input_obj: _T_Input, **overrides) -> _T_Input: + """Copy an ``io.Input`` instance with optional overrides.""" + new_id = input_obj.id + if 'id' in overrides: + new_id = overrides.pop('id') + + input_args = input_obj.as_dict() + if 'id' in input_args: + del input_args['id'] + input_args.update(overrides) + + input_type: _t.Type[_T_Input] = type(input_obj) + return input_type(new_id, **input_args) + + +def _schema_old_name(schema: _io.Schema, node_id: str, **overrides): + """Build a deprecated schema for old node-ID.""" + schema_dict = __dataclass_fields_dict(schema) + if 'node_id' in schema_dict: + del schema_dict['node_id'] + name_old = schema_dict.pop('display_name') + + schema_dict.update(dict( + category=_category_old, + display_name=f"{name_old} [OLD]", + is_deprecated=True, + is_dev_only=True, + )) + schema_dict.update(overrides) + return _io.Schema(node_id, **schema_dict) + + +# noinspection PyAbstractClass +class _BaseNode(_io.ComfyNode): + """Shared base class for all the nodes in pack. + Stores schema in internal class attribute. + """ + _schema: _io.Schema = None + + @classmethod + def define_schema(cls) -> _io.Schema: + return cls._schema diff --git a/nodes_dict.py b/nodes_dict.py new file mode 100644 index 0000000..066349b --- /dev/null +++ b/nodes_dict.py @@ -0,0 +1,76 @@ +# encoding: utf-8 +"""Service nodes for dict preparation.""" + +from inspect import cleandoc as _cleandoc + +from frozendict import frozendict as _frozendict + +from comfy_api.latest import io as _io, ui as _ui + +from .__meta import ( + category_dict as _category_dict, + pack_id_suffix as _pack_id +) +from .__typing import _A, _U, _O, _t, T as _T, FormatDict as _FormatDict +from ._dict import _new_dict_with_updated_key +from ._io_custom import ( + _BaseNode, + _DICT_INPUT_OPTIONAL, _DICT_OUTPUT, + _input_override, _schema_old_name +) +from .docstring_formatter import format_docstring as _format_docstring + +_dict = dict + +# ---------------------------------------------------------- + +class DictAddAny(_BaseNode): + """Add/update any-type item to a Format-Dict.""" + _schema = _io.Schema( + node_id=f'DictAddAny{_pack_id}', + display_name='Add ANY to Dict', + category=_category_dict, + description=_format_docstring(_cleandoc(__doc__)), + inputs=[ + _io.String.Input( + 'name', + tooltip=( + "Name (key) of the item inserted into the dict. " + "It must comprise only of latin letters, digits and underscores + it can't start with a digit." + ), + ), + + _DICT_INPUT_OPTIONAL, + _io.AnyType.Input( + 'value', + optional=True, + tooltip="The actual any-type item to add into the dict." + ), + ], + outputs=[_DICT_OUTPUT], + ) + + @classmethod + def execute(cls, + name: str, dict: _O[_FormatDict] = None, value: _A = None + ) -> _io.NodeOutput: + """Update/append an item of any type to the dict.""" + if value is None: + if dict is None: + dict = _frozendict() + # No need to create another dict instance if we add nothing: + result = dict + else: + result = _new_dict_with_updated_key(dict, name, value) + return _io.NodeOutput(result) + + +# ========================================================== +# Deprecated nodes with old IDs (for backwards compatibility) + + +class DictAddAnyOld1(DictAddAny): + _schema = _schema_old_name( + DictAddAny._schema, + 'StringConstructorDictAddAny' + )