Added PandasExcelFileSheetName and PyListToString nodes as well fixing ShowText.js. Added dependency on lxml to address broken html node without lxml

This commit is contained in:
HowToSD
2025-06-10 20:53:28 -07:00
parent b7affbf1af
commit e642a7493f
15 changed files with 305 additions and 223 deletions
+6
View File
@@ -13,6 +13,7 @@ from .modules.cda.py_int_to_string import PyIntToString
from .modules.cda.py_kv_float_create import PyKvFloatCreate
from .modules.cda.py_kv_int_create import PyKvIntCreate
from .modules.cda.py_kv_string_create import PyKvStringCreate
from .modules.cda.py_list_to_string import PyListToString
from .modules.cda.py_string_to_datetime import PyStringToDatetime
from .modules.cda.py_string_to_dict import PyStringToDict
from .modules.cda.py_string_to_float import PyStringToFloat
@@ -71,6 +72,7 @@ from .modules.pandas_wrapper.pandas_dropna import PandasDropNA
from .modules.pandas_wrapper.pandas_eq import PandasEq
from .modules.pandas_wrapper.pandas_eq_scalar_float import PandasEqScalarFloat
from .modules.pandas_wrapper.pandas_eq_scalar_int import PandasEqScalarInt
from .modules.pandas_wrapper.pandas_excel_file_sheet_names import PandasExcelFileSheetNames
from .modules.pandas_wrapper.pandas_exp import PandasExp
from .modules.pandas_wrapper.pandas_feature_split_to_numpy import PandasFeatureSplitToNumpy
from .modules.pandas_wrapper.pandas_feature_split_to_pt import PandasFeatureSplitToPt
@@ -253,6 +255,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = {
"PandasEq": PandasEq,
"PandasEqScalarFloat": PandasEqScalarFloat,
"PandasEqScalarInt": PandasEqScalarInt,
"PandasExcelFileSheetNames": PandasExcelFileSheetNames,
"PandasExp": PandasExp,
"PandasFeatureSplitToNumpy": PandasFeatureSplitToNumpy,
"PandasFeatureSplitToPt": PandasFeatureSplitToPt,
@@ -361,6 +364,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = {
"PyKvFloatCreate": PyKvFloatCreate,
"PyKvIntCreate": PyKvIntCreate,
"PyKvStringCreate": PyKvStringCreate,
"PyListToString": PyListToString,
"PyStringToDatetime": PyStringToDatetime,
"PyStringToDict": PyStringToDict,
"PyStringToFloat": PyStringToFloat,
@@ -446,6 +450,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {
"PandasEq": "Pandas Eq",
"PandasEqScalarFloat": "Pandas Eq Scalar Float",
"PandasEqScalarInt": "Pandas Eq Scalar Int",
"PandasExcelFileSheetNames": "Pandas Excel File Sheet Names",
"PandasExp": "Pandas Exp",
"PandasFeatureSplitToNumpy": "Pandas Feature Split To Numpy",
"PandasFeatureSplitToPt": "Pandas Feature Split To Pt",
@@ -554,6 +559,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {
"PyKvFloatCreate": "Py Kv Float Create",
"PyKvIntCreate": "Py Kv Int Create",
"PyKvStringCreate": "Py Kv String Create",
"PyListToString": "Py List To String",
"PyStringToDatetime": "Py String To Datetime",
"PyStringToDict": "Py String To Dict",
"PyStringToFloat": "Py String To Float",
+2
View File
@@ -118,6 +118,7 @@
| [Pandas Create Series From List](pandas_create_series_from_list.md) | Creates a Pandas Series from a Python list. |
| [Pandas Create Series From List Index List](pandas_create_series_from_list_index_list.md) | Creates a Pandas Series from a Python list of cell values and a list for index. |
| [Pandas Create With Index](pandas_create_with_index.md) | Creates a pandas DataFrame using values entered in the text field. Input data is assumed to have an index. |
| [Pandas Excel File Sheet Names](pandas_excel_file_sheet_names.md) | Returns the string list of sheet names contained in the Excel file. |
| [Pandas Load CSV](pandas_load_csv.md) | Loads CSV files into a pandas DataFrame. |
| [Pandas Load CSV With Encoding](pandas_load_csv_with_encoding.md) | Loads CSV files into a pandas DataFrame. |
| [Pandas Load CSV With Index](pandas_load_csv_with_index.md) | Loads CSV files into a pandas DataFrame. |
@@ -189,6 +190,7 @@
| [Py Kv Float Create](py_kv_float_create.md) | Creates a Python dictionary with a string key and a float value. |
| [Py Kv Int Create](py_kv_int_create.md) | Creates a Python dictionary with an string key and an integer value. |
| [Py Kv String Create](py_kv_string_create.md) | Creates a Python dictionary with a string key and a string value. |
| [Py List To String](py_list_to_string.md) | Converts a Python list value to a Python string |
| [Py String To Datetime](py_string_to_datetime.md) | Converts a Python string to a Python datetime.datetime. |
| [Py String To Dict](py_string_to_dict.md) | Creates a Python dictionary using the string values entered in the text field. |
| [Py String To Float](py_string_to_float.md) | Converts a Python string to a float. |
@@ -0,0 +1,23 @@
# Pandas Excel File Sheet Names
Returns the string list of sheet names contained in the Excel file.
This node uses ExcelFile method using the openpyxl package.
The openpyxl package is specified as a dependency in requirements.txt, so you should not need to manually install it.
See [Pandas documentation](https://pandas.pydata.org/docs/reference/api/pandas.ExcelFile.html) for details.
To display the list, connect output to PyListToString node to convert the list to a string, then connect the output to Pandas Show Text node to display the string.
## Input
| Name | Data type |
|---|---|
| file_path | String |
## Output
| Data type |
|---|
| Pylist |
<HR>
Category: IO
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
+17
View File
@@ -0,0 +1,17 @@
# Py List To String
Converts a Python list value to a Python string
## Input
| Name | Data type |
|---|---|
| value | Pylist |
## Output
| Data type |
|---|
| String |
<HR>
Category: Python wrapper
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
@@ -0,0 +1 @@
{"id":"d3e44069-d0c1-4d4b-b205-9aac7b243871","revision":0,"last_node_id":3,"last_link_id":2,"nodes":[{"id":2,"type":"PyListToString","pos":[743.5132446289062,244.38381958007812],"size":[214.20001220703125,26],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"value","name":"value","type":"PYLIST","link":1}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[2]}],"properties":{"Node name for S&R":"PyListToString"}},{"id":1,"type":"PandasExcelFileSheetNames","pos":[350.7860107421875,242.56568908691406],"size":[365.4000244140625,58],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"STRING","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"PYLIST","name":"PYLIST","type":"PYLIST","links":[1]}],"properties":{"Node name for S&R":"PandasExcelFileSheetNames"},"widgets_values":["custom_nodes/data-analysis/examples/datasets/test_file.xlsx"]},{"id":3,"type":"PandasShowText","pos":[987.1495361328125,242.56565856933594],"size":[210,76],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","link":2}],"outputs":[{"localized_name":"STRING","name":"STRING","shape":6,"type":"STRING","links":null}],"properties":{"Node name for S&R":"PandasShowText"},"widgets_values":["['Sheet1', 'FruitPrice']"]}],"links":[[1,1,0,2,0,"PYLIST"],[2,2,0,3,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.1000000000000005,"offset":[75.57754666842986,116.52528218135221]},"frontendVersion":"1.16.9","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}
+39
View File
@@ -0,0 +1,39 @@
from typing import Any, Dict, List
class PyListToString:
"""
Converts a Python list value to a Python string
category: Python wrapper
"""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
"""
Defines the input types for the function.
Returns:
Dict[str, Any]: A dictionary specifying required input types.
"""
return {
"required": {
"value": ("PYLIST", {})
}
}
RETURN_TYPES: tuple = ("STRING",)
FUNCTION: str = "f"
CATEGORY: str = "Data Analysis"
def f(self, value: List) -> tuple:
"""
Converts a Python list value to a Python string
Args:
value (list): List value to convert.
Returns:
tuple: A tuple containing the converted value.
"""
return (str(value),)
@@ -0,0 +1,51 @@
from typing import Any, Dict
import pandas as pd
class PandasExcelFileSheetNames:
"""
Returns the string list of sheet names contained in the Excel file.
This node uses ExcelFile method using the openpyxl package.
The openpyxl package is specified as a dependency in requirements.txt, so you should not need to manually install it.
See [Pandas documentation](https://pandas.pydata.org/docs/reference/api/pandas.ExcelFile.html) for details.
To display the list, connect output to PyListToString node to convert the list to a string, then connect the output to Pandas Show Text node to display the string.
category: IO
"""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
"""
Defines the input types for the function.
Returns:
Dict[str, Any]: A dictionary specifying required input types.
"""
return {
"required": {
"file_path": ("STRING", {"multiline": False}),
}
}
RETURN_TYPES: tuple = ("PYLIST",)
FUNCTION: str = "f"
CATEGORY: str = "Data Analysis"
@classmethod
def IS_CHANGED(cls, **kw):
return float("NaN")
def f(self, file_path: str) -> tuple:
"""
Returns the string list of sheet names contained in the Excel file.
Args:
file_path (str): The path to the Excel file.
Returns:
tuple: A tuple containing a string list of sheet names.
"""
pylist = pd.ExcelFile(file_path, engine="openpyxl").sheet_names
return (pylist,)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "data-analysis"
description = "Data analysis custom modules for ComfyUI – Integrates Pandas, Seaborn and Matplotlib into ComfyUI."
version = "3.0.7"
version = "3.1.0"
license = {file = "LICENSE"}
[project.urls]
+2 -1
View File
@@ -3,4 +3,5 @@ pandas>=2.2.3
seaborn>=0.13.2
scipy>=1.14.1
scikit-learn>=1.6.1
openpyxl>=3.1.5
openpyxl>=3.1.5
lxml>=5.4.0
+29
View File
@@ -0,0 +1,29 @@
import os
import sys
import unittest
PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))
MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules")
sys.path.append(MODULE_ROOT)
from cda.py_list_to_string import PyListToString
class TestPyListToString(unittest.TestCase):
def setUp(self):
"""Set up test data."""
self.node = PyListToString()
self.test_cases = [
([1,2,3], "[1, 2, 3]"),
(["apple", "banana"], "['apple', 'banana']"),
([], "[]")
]
def test_list_to_string(self):
"""Test converting a list to string."""
for value, expected in self.test_cases:
with self.subTest(value=value):
retval = self.node.f(value=value)[0]
self.assertEqual(retval, expected, f"Mismatch for value={value}")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,34 @@
import os
import sys
import unittest
import pandas as pd
PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))
MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules")
sys.path.append(MODULE_ROOT)
from pandas_wrapper.pandas_excel_file_sheet_names import PandasExcelFileSheetNames
class TestPandasExcelFileSheetNames(unittest.TestCase):
def setUp(self):
"""Set up test data."""
self.node = PandasExcelFileSheetNames()
self.test_file_path = os.path.join(
os.path.dirname(__file__), "..", "..",
"examples", "datasets", "test_file.xlsx")
self.test_data_list = [
"Sheet1",
"FruitPrice"
]
def test_excel_file_sheet_names(self):
"""Reads sheet names from an excel file.
"""
expected = self.test_data_list
actual = self.node.f(self.test_file_path)[0]
self.assertEqual(actual, expected)
if __name__ == "__main__":
unittest.main()
-66
View File
@@ -1,66 +0,0 @@
/*
This code is based on
https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js
See credit/credit.md for the full license.
*/
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "HowToSD.ShowDataFrame",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "PandasShowDataFrame") { // This needs to match NODE_CLASS_MAPPINGS in Python
function populate(text) {
if (this.widgets) {
// Clear all widgets on the node
for (let i = 0; i < this.widgets.length; i++) {
// if onRemove method exists, call it.
this.widgets[i].onRemove?.();
}
this.widgets.length = 0;
}
const v = [...text]; // Copy the text list (e.g. text.copy())
if (!v[0]) {
v.shift();
}
for (const list of v) {
const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1));
}
};
}
},
});
-66
View File
@@ -1,66 +0,0 @@
/*
This code is based on
https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js
See credit/credit.md for the full license.
*/
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "HowToSD.ShowIndex",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "PandasShowIndex") { // This needs to match NODE_CLASS_MAPPINGS in Python
function populate(text) {
if (this.widgets) {
// Clear all widgets on the node
for (let i = 0; i < this.widgets.length; i++) {
// if onRemove method exists, call it.
this.widgets[i].onRemove?.();
}
this.widgets.length = 0;
}
const v = [...text]; // Copy the text list (e.g. text.copy())
if (!v[0]) {
v.shift();
}
for (const list of v) {
const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1));
}
};
}
},
});
+100 -23
View File
@@ -8,57 +8,134 @@ See credit/credit.md for the full license.
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "HowToSD.ShowText",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["PandasShowText", "CDAShowInt", "CDAShowFloat"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python
// Each node needs to match NODE_CLASS_MAPPINGS in Python
const targetNodes = [
"PandasShowText",
"PandasShowIndex",
"PandasShowDataFrame",
"CDAShowInt",
"CDAShowFloat",
"NumpyShow",
"PandasShowSeries",
]
const noConvertedFieldNodes = [
"CDAShowInt",
"CDAShowFloat",
]
if (targetNodes.includes(nodeData.name)) {
function populate(text) {
// 'this' is not bound automatically — it's explicitly set via .call() in onExecuted.
if (this.widgets) {
for (let i = 1; i < this.widgets.length; i++) {
// On older frontend versions there is a hidden converted-widget
// Legacy version: const isConvertedWidget = +!!this.inputs?.[0].widget;
const hasInputWidget = !!(this.inputs?.[0]?.widget); // Coerce to boolean: true if widget exists
let isConvertedWidget = hasInputWidget ? 1 : 0; // Convert boolean to integer (1 or 0)
if (noConvertedFieldNodes.includes(nodeData.name)){ // Remove all widgets
isConvertedWidget = 0;
}
// Remove all dynamically added widgets, preserving the auto-converted input widget if present
for (let i = isConvertedWidget; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = 1;
this.widgets.length = isConvertedWidget;
}
const v = [...text];
if (!v[0]) {
const v = [...text]; // Shallow copy of text array into v
// If the first element is empty or undefined, remove it from the array
if (!v[0]) {
v.shift();
}
for (const list of v) {
const w = ComfyWidgets["STRING"](this, "text2", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
for (let row of v) { // Iterate over v. v can be 1d or 2d.
if (!(row instanceof Array)) row = [row];
for (const elem of row) {
// Create a new STRING widget
// Use field name like "text_0", "text_1", accounting for existing widgets
const fieldName = "text_" + (this.widgets?.length ?? 0);
const w = ComfyWidgets["STRING"](
this,
fieldName,
["STRING", { multiline: true }],
app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = elem;
}
}
/*
Defer until the next animation frame so all widgets are rendered and layout is stable.
Then compute the node size needed to fit new content.
If computeSize underestimates it, use the current node size as the floor to preserve layout.
*/
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
const sz = this.computeSize(); // Compute size based on current widgets
// Adjust to ensure the node fits its current visual width/height
if (sz[0] < this.size[0]) sz[0] = this.size[0]; // Fit existing node width
if (sz[1] < this.size[1]) sz[1] = this.size[1]; // Fit existing node height
this.onResize?.(sz); // Apply the size adjustment
app.graph.setDirtyCanvas(true, false); // Trigger a redraw of the canvas
});
}
// **onExecute** setup
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
// Call the original onExecuted handler (if one exists), forwarding all arguments
onExecuted?.apply(this, arguments);
// Call our populate function, binding 'this' to the current node
populate.call(this, message.text);
};
// **configure** setup
const VALUES = Symbol(); // Unique internal key to store widget values
const configure = nodeType.prototype.configure;
nodeType.prototype.configure = function () {
// Store unmodified widget values as they get removed on configure by new frontend
// This value is applied in onConfigure
this[VALUES] = arguments[0]?.widgets_values;
// Call the original configure handler, if it exists
return configure?.apply(this, arguments);
};
// **onConfigure** setup
// Populate node's widgets using the saved values in Configure
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1));
// Read saved widget values
const widgets_values = this[VALUES];
if (widgets_values?.length) {
// In newer frontend there seems to be a delay in creating the initial widget
requestAnimationFrame(() => {
// Legacy code
// populate.call(this,
// widgets_values.slice(+(widgets_values.length > 1 && this.inputs?.[0].widget)));
const hasAutoWidget = widgets_values.length > 1 && this.inputs?.[0]?.widget;
let startIndex = hasAutoWidget ? 1 : 0;
if (noConvertedFieldNodes.includes(nodeData.name)){ // Remove all widgets
startIndex = 0;
}
populate.call(this, widgets_values.slice(startIndex));
});
}
};
}
},
});
});
-66
View File
@@ -1,66 +0,0 @@
/*
This code is based on
https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js
See credit/credit.md for the full license.
*/
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
// Displays input text on a node
app.registerExtension({
name: "HowToSD.ShowTextGeneric",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["NumpyShow", "PandasShowSeries"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python
function populate(text) {
if (this.widgets) {
// Clear all widgets on the node
for (let i = 0; i < this.widgets.length; i++) {
// if onRemove method exists, call it.
this.widgets[i].onRemove?.();
}
this.widgets.length = 0;
}
const v = [...text]; // Copy the text list (e.g. text.copy())
if (!v[0]) {
v.shift();
}
for (const list of v) {
const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1));
}
};
}
},
});