From ed1cef099e25494824bb0e894a497d207963c7d2 Mon Sep 17 00:00:00 2001 From: HowToSD Date: Wed, 5 Mar 2025 03:38:01 -0800 Subject: [PATCH] Added Pandas Load Excel node to read Excel file and added dependency on openpyxl package. --- .gitignore | 4 +- __init__.py | 3 + docs/reference/node_reference.md | 1 + docs/reference/pandas_load_excel.md | 23 +++++++ examples/datasets/test_file.xlsx | Bin 0 -> 13025 bytes modules/pandas_wrapper/pandas_load_excel.py | 55 +++++++++++++++ pyproject.toml | 2 +- requirements.txt | 3 +- .../pandas_wrapper/pandas_load_excel_test.py | 64 ++++++++++++++++++ 9 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 docs/reference/pandas_load_excel.md create mode 100644 examples/datasets/test_file.xlsx create mode 100644 modules/pandas_wrapper/pandas_load_excel.py create mode 100644 tests/pandas_wrapper/pandas_load_excel_test.py diff --git a/.gitignore b/.gitignore index 17a60fc..8c82aed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ __pycache__/ **/__pycache__/ -datasets/ models/ *.pth +/datasets/ +!*/foo/ +examples/datasets/.~lock* \ No newline at end of file diff --git a/__init__.py b/__init__.py index baeddf4..8d8af09 100644 --- a/__init__.py +++ b/__init__.py @@ -67,6 +67,7 @@ from .modules.pandas_wrapper.pandas_kurtosis import PandasKurtosis from .modules.pandas_wrapper.pandas_load_csv import PandasLoadCSV from .modules.pandas_wrapper.pandas_load_csv_with_encoding import PandasLoadCSVWithEncoding from .modules.pandas_wrapper.pandas_load_csv_with_index import PandasLoadCSVWithIndex +from .modules.pandas_wrapper.pandas_load_excel import PandasLoadExcel from .modules.pandas_wrapper.pandas_load_html import PandasLoadHTML from .modules.pandas_wrapper.pandas_load_json import PandasLoadJSON from .modules.pandas_wrapper.pandas_loc_cell_str import PandasLocCellStr @@ -300,6 +301,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasLoadCSV": PandasLoadCSV, "PandasLoadCSVWithEncoding": PandasLoadCSVWithEncoding, "PandasLoadCSVWithIndex": PandasLoadCSVWithIndex, + "PandasLoadExcel": PandasLoadExcel, "PandasLoadHTML": PandasLoadHTML, "PandasLoadJSON": PandasLoadJSON, "PandasLocCellStr": PandasLocCellStr, @@ -536,6 +538,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasLoadCSV": "Pandas Load CSV", "PandasLoadCSVWithEncoding": "Pandas Load CSV With Encoding", "PandasLoadCSVWithIndex": "Pandas Load CSV With Index", + "PandasLoadExcel": "Pandas Load Excel", "PandasLoadHTML": "Pandas Load HTML", "PandasLoadJSON": "Pandas Load JSON", "PandasLocCellStr": "Pandas Loc Cell Str", diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index 1623b38..6bc1a96 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -109,6 +109,7 @@ | [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. | +| [Pandas Load Excel](pandas_load_excel.md) | Loads Excel files into a pandas DataFrame. | | [Pandas Load HTML](pandas_load_html.md) | Scans an HTML file or webpage for tables and returns the number of DataFrames | | [Pandas Load JSON](pandas_load_json.md) | Loads JSON files into a pandas DataFrame. | | [Pandas Save CSV](pandas_save_csv.md) | Saves a PandasDataFrame to a CSV file. | diff --git a/docs/reference/pandas_load_excel.md b/docs/reference/pandas_load_excel.md new file mode 100644 index 0000000..1371ee6 --- /dev/null +++ b/docs/reference/pandas_load_excel.md @@ -0,0 +1,23 @@ +# Pandas Load Excel +Loads Excel files into a pandas DataFrame. + +This node uses read_excel method. +See [Pandas documentation](https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html) for details. + +If sheet_name is not specified, first sheet will be read. + +## Input +| Name | Data type | +|---|---| +| file_path | String | +| sheet_name | String | + +## Output +| Data type | +|---| +| DataFrame | + +
+Category: IO + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/examples/datasets/test_file.xlsx b/examples/datasets/test_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d0466d8e231dbd54a8b463ce4fd76cae12dbc2b9 GIT binary patch literal 13025 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 literal 0 HcmV?d00001 diff --git a/modules/pandas_wrapper/pandas_load_excel.py b/modules/pandas_wrapper/pandas_load_excel.py new file mode 100644 index 0000000..00f92b0 --- /dev/null +++ b/modules/pandas_wrapper/pandas_load_excel.py @@ -0,0 +1,55 @@ +from typing import Any, Dict +from io import StringIO +import pandas as pd + + +class PandasLoadExcel: + """ + Loads Excel files into a pandas DataFrame. + + This node uses read_excel method. + See [Pandas documentation](https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html) for details. + + If sheet_name is not specified, first sheet will be read. + + category: IO + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `load_csv` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "file_path": ("STRING", {"multiline": False}), + "sheet_name": ("STRING", {"default": ""}), + + } + } + + RETURN_TYPES: tuple = ("DATAFRAME",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, file_path: str, sheet_name: str) -> tuple: + """ + Loads an Excel file into a pandas DataFrame. + + Args: + file_path (str): The path to the Excel file. + + Returns: + tuple: A tuple containing a DataFrame. + """ + if len(sheet_name) == 0: + sheet_name = 0 # Read first sheet + df = pd.read_excel(file_path, sheet_name=sheet_name) + return (df,) diff --git a/pyproject.toml b/pyproject.toml index 50e24d1..bd99782 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "data-analysis" description = "Data analysis custom modules for ComfyUI – Integrates Pandas, Seaborn and Matplotlib into ComfyUI, featuring extensive PyTorch wrapper nodes for seamless tensor operations. Experimental PyTorch model training was added in v2.0." -version = "2.0.4" +version = "2.0.5" license = {file = "LICENSE"} [project.urls] diff --git a/requirements.txt b/requirements.txt index 58e2f64..b504a51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ matplotlib>=3.10.0 pandas>=2.2.3 seaborn>=0.13.2 scipy>=1.14.1 -scikit-learn>=1.6.1 \ No newline at end of file +scikit-learn>=1.6.1 +openpyxl>=3.1.5 \ No newline at end of file diff --git a/tests/pandas_wrapper/pandas_load_excel_test.py b/tests/pandas_wrapper/pandas_load_excel_test.py new file mode 100644 index 0000000..fd481db --- /dev/null +++ b/tests/pandas_wrapper/pandas_load_excel_test.py @@ -0,0 +1,64 @@ +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_load_excel import PandasLoadExcel + + +class TestPandasLoadExcel(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PandasLoadExcel() + self.test_file_path = os.path.join( + os.path.dirname(__file__), "..", "..", + "examples", "datasets", "test_file.xlsx") + + self.test_data_list = [ + ( + "Sheet1", + pd.DataFrame( + { + "Item": ["Bat", "Racquet", "Glove", "Hockey stick", "Soccer ball"], + "Qty": [3, 4, 5, 6, 7] + }, + index = [0, 1, 2, 3, 4] + ), + ), + ( + "FruitPrice", + pd.DataFrame( + { + "Fruit": ["Apple", "Banana", "Coconut", "Donut"], + "Price": [1.23, 2.34, 3.45, 4.56] + }, + index = [0, 1, 2, 3] + ) + ), + ( + "", # Leaving the sheet name blank should read the first sheet. + pd.DataFrame( + { + "Item": ["Bat", "Racquet", "Glove", "Hockey stick", "Soccer ball"], + "Qty": [3, 4, 5, 6, 7] + }, + index = [0, 1, 2, 3, 4] + ), + ), + ] + def test_read_excel(self): + """Reads multiple sheets from an excel file. + """ + for test_data in self.test_data_list: + with self.subTest(test_data=test_data): + sheet_name = test_data[0] + expected = test_data[1] + df = self.node.f(self.test_file_path, sheet_name=sheet_name)[0] + pd.testing.assert_frame_equal(df, expected) + + +if __name__ == "__main__": + unittest.main()