diff --git a/pyproject.toml b/pyproject.toml index c490d60..de28374 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.4.6" +version = "0.4.7" description = """Basic Python functions for manipulating data that every programmer is used to, lightweight with no additional dependencies. Supported data types: diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index fb0fb5a..21c80c1 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -308,6 +308,9 @@ class ExecutionOrder(ComfyNodeABC): the execution order of nodes in the workflow. You only need to chain this node with the other execution order nodes in the desired order and add any output of the nodes you want to force execution order on. + + This node also passes through any input connected to "any node output" as + its second output. """ @classmethod def INPUT_TYPES(cls): @@ -318,13 +321,16 @@ def INPUT_TYPES(cls): } } - RETURN_TYPES = ("E/O",) + RETURN_TYPES = ("E/O", IO.ANY) + RETURN_NAMES = ("E/O", "passthrough") + FUNCTION = "execution_order" CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" - def execute(self, **kwargs) -> tuple[Any]: - return (None,) + def execute(self, **kwargs: list[Any]) -> tuple[None, Any]: + any_node_output = kwargs.get('any node output', []) + return (None, any_node_output) NODE_CLASS_MAPPINGS = { diff --git a/src/basic_data_handling/path_nodes.py b/src/basic_data_handling/path_nodes.py index 4e6fb9a..c70248c 100644 --- a/src/basic_data_handling/path_nodes.py +++ b/src/basic_data_handling/path_nodes.py @@ -28,13 +28,15 @@ def load_image_helper(path: str): pass if not os.path.exists(path): - raise FileNotFoundError(f"Basic data handling: Image file not found: {path}") + return None # Open and process the image - img = Image.open(path) - img = ImageOps.exif_transpose(img) - - return img + try: + img = Image.open(path) + img = ImageOps.exif_transpose(img) + return img + except Exception: + return None def extract_mask_from_alpha(img): @@ -675,19 +677,33 @@ def INPUT_TYPES(cls): }, } - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("text",) + RETURN_TYPES = (IO.STRING, IO.BOOLEAN) + RETURN_NAMES = ("text", "exists") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_text" + @classmethod + def IS_CHANGED(cls, path): + try: + if os.path.exists(path): + return os.path.getmtime(path) + except Exception: + pass + return float("NaN") # Return NaN if file doesn't exist or can't access modification time + def load_text(self, path: str): - if not os.path.exists(path): - raise FileNotFoundError(f"Basic data handling: String file not found: {path}") + exists = os.path.exists(path) - with open(path, "r", encoding="utf-8") as f: - text = f.read() - return (text,) + if not exists: + return ("", False) + + try: + with open(path, "r", encoding="utf-8") as f: + text = f.read() + return (text, True) + except Exception: + return ("", False) class PathLoadImageRGB(ComfyNodeABC): @@ -705,18 +721,32 @@ def INPUT_TYPES(cls): }, } - RETURN_TYPES = (IO.IMAGE,) - RETURN_NAMES = ("image",) + RETURN_TYPES = (IO.IMAGE, IO.BOOLEAN) + RETURN_NAMES = ("image", "exists") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_image_rgb" + @classmethod + def IS_CHANGED(cls, path): + try: + if os.path.exists(path): + return os.path.getmtime(path) + except Exception: + pass + return float("NaN") # Return NaN if file doesn't exist or can't access modification time + def load_image_rgb(self, path: str): import numpy as np import torch img = load_image_helper(path) + if img is None: + # Create an empty 1x1 image + empty_tensor = torch.zeros((1, 1, 1, 3), dtype=torch.float32) + return (empty_tensor, False) + # Convert to RGB (removing alpha if present) img_rgb = img.convert("RGB") @@ -724,7 +754,7 @@ def load_image_rgb(self, path: str): image_tensor = np.array(img_rgb).astype(np.float32) / 255.0 image_tensor = torch.from_numpy(image_tensor)[None,] - return (image_tensor,) + return (image_tensor, True) class PathLoadImageRGBA(ComfyNodeABC): @@ -743,18 +773,33 @@ def INPUT_TYPES(cls): }, } - RETURN_TYPES = (IO.IMAGE, IO.MASK) - RETURN_NAMES = ("image", "mask") + RETURN_TYPES = (IO.IMAGE, IO.MASK, IO.BOOLEAN) + RETURN_NAMES = ("image", "mask", "exists") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_image_rgba" + @classmethod + def IS_CHANGED(cls, path): + try: + if os.path.exists(path): + return os.path.getmtime(path) + except Exception: + pass + return float("NaN") # Return NaN if file doesn't exist or can't access modification time + def load_image_rgba(self, path: str): import numpy as np import torch img = load_image_helper(path) + if img is None: + # Create empty 1x1 image and mask + empty_image = torch.zeros((1, 1, 1, 3), dtype=torch.float32) + empty_mask = torch.zeros((1, 1, 1), dtype=torch.float32) + return (empty_image, empty_mask, False) + # Convert to RGB for the image img_rgb = img.convert("RGB") @@ -765,7 +810,7 @@ def load_image_rgba(self, path: str): # Extract alpha channel as mask mask_tensor = extract_mask_from_alpha(img) - return (image_tensor, mask_tensor) + return (image_tensor, mask_tensor, True) class PathLoadMaskFromAlpha(ComfyNodeABC): @@ -784,16 +829,33 @@ def INPUT_TYPES(cls): }, } - RETURN_TYPES = (IO.MASK,) - RETURN_NAMES = ("mask",) + RETURN_TYPES = (IO.MASK, IO.BOOLEAN) + RETURN_NAMES = ("mask", "exists") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_mask_from_alpha" + @classmethod + def IS_CHANGED(cls, path): + try: + if os.path.exists(path): + return os.path.getmtime(path) + except Exception: + pass + return float("NaN") # Return NaN if file doesn't exist or can't access modification time + def load_mask_from_alpha(self, path: str): + import torch + img = load_image_helper(path) + + if img is None: + # Return empty 1x1 mask + empty_mask = torch.zeros((1, 1, 1), dtype=torch.float32) + return (empty_mask, False) + mask_tensor = extract_mask_from_alpha(img) - return (mask_tensor,) + return (mask_tensor, True) class PathLoadMaskFromGreyscale(ComfyNodeABC): @@ -815,21 +877,38 @@ def INPUT_TYPES(cls): }, } - RETURN_TYPES = (IO.MASK,) - RETURN_NAMES = ("mask",) + RETURN_TYPES = (IO.MASK, IO.BOOLEAN) + RETURN_NAMES = ("mask", "exists") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_mask_from_greyscale" + @classmethod + def IS_CHANGED(cls, path): + try: + if os.path.exists(path): + return os.path.getmtime(path) + except Exception: + pass + return float("NaN") # Return NaN if file doesn't exist or can't access modification time + def load_mask_from_greyscale(self, path: str, invert: bool = False): + import torch + img = load_image_helper(path) + + if img is None: + # Return empty 1x1 mask + empty_mask = torch.zeros((1, 1, 1), dtype=torch.float32) + return (empty_mask, False) + mask_tensor = extract_mask_from_greyscale(img) # Optionally invert the mask (1.0 - mask) if invert: mask_tensor = 1.0 - mask_tensor - return (mask_tensor,) + return (mask_tensor, True) class PathSaveStringFile(ComfyNodeABC): @@ -852,8 +931,8 @@ def INPUT_TYPES(cls): } } - RETURN_TYPES = (IO.BOOLEAN) - RETURN_NAMES = ("success") + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("success",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "save_text" @@ -861,6 +940,7 @@ def INPUT_TYPES(cls): def save_text(self, text: str, path: str, create_dirs: bool = True, encoding: str = "utf-8"): if not path: + print("Basic data handling: Save failed - no path specified") return (False,) try: @@ -872,6 +952,7 @@ def save_text(self, text: str, path: str, create_dirs: bool = True, encoding: st with open(path, "w", encoding=encoding) as f: f.write(text) + print(f"Basic data handling: Successfully saved text to {path}") return (True,) except Exception as e: print(f"Basic data handling: Error saving text file: {e}") @@ -899,8 +980,8 @@ def INPUT_TYPES(cls): } } - RETURN_TYPES = (IO.BOOLEAN) - RETURN_NAMES = ("success") + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("success",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "save_image" @@ -908,6 +989,7 @@ def INPUT_TYPES(cls): def save_image(self, images, path: str, format: str = "png", quality: int = 95, create_dirs: bool = True): if not path: + print("Basic data handling: Save failed - no path specified") return (False,) # If the path doesn't have an extension or it doesn't match the format, add it @@ -957,6 +1039,7 @@ def save_image(self, images, path: str, format: str = "png", quality: int = 95, else: pil_img.save(path, format=format.upper()) + print(f"Basic data handling: Successfully saved image to {path}") return (True,) except Exception as e: print(f"Basic data handling: Error saving image: {e}") @@ -987,8 +1070,8 @@ def INPUT_TYPES(cls): } } - RETURN_TYPES = (IO.BOOLEAN) - RETURN_NAMES = ("success") + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("success",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "save_image_with_mask" @@ -998,6 +1081,7 @@ def save_image_with_mask(self, images, mask, path: str, format: str = "png", quality: int = 95, invert_mask: bool = False, create_dirs: bool = True): if not path: + print("Basic data handling: Save failed - no path specified") return (False,) # Check format compatibility - needs to support alpha channel @@ -1065,6 +1149,7 @@ def save_image_with_mask(self, images, mask, path: str, format: str = "png", else: pil_img_rgba.save(path, format=format.upper()) + print(f"Basic data handling: Successfully saved image with mask to {path}") return (True,) except Exception as e: print(f"Basic data handling: Error saving image with mask: {e}") diff --git a/tests/test_path_nodes.py b/tests/test_path_nodes.py index dab0b9c..66c5396 100644 --- a/tests/test_path_nodes.py +++ b/tests/test_path_nodes.py @@ -41,7 +41,7 @@ def test_path_load_save_string_file(tmp_path): # Test loading the string back load_node = PathLoadStringFile() loaded_string = load_node.load_text(file_path) - assert loaded_string == (test_string,) + assert loaded_string == (test_string, True) # Test creating directories when saving nested_path = str(tmp_path / "nested" / "dir" / "test.txt") @@ -52,12 +52,10 @@ def test_path_load_save_string_file(tmp_path): utf8_text = "UTF-8 text with special chars: 你好, ñ, é, ö" utf8_path = str(tmp_path / "utf8_test.txt") assert save_node.save_text(utf8_text, utf8_path, encoding="utf-8") == (True,) - assert load_node.load_text(utf8_path) == (utf8_text,) + assert load_node.load_text(utf8_path) == (utf8_text, True) # Test error handling - with pytest.raises(FileNotFoundError): - load_node.load_text(str(tmp_path / "nonexistent.txt")) - + assert load_node.load_text(str(tmp_path / "nonexistent.txt")) == ("", False) def test_path_abspath(): @@ -304,7 +302,7 @@ def mock_extract_mask_from_greyscale(img): # Verify the mask shape assert isinstance(alpha_mask, tuple) - assert len(alpha_mask) == 1 + assert len(alpha_mask) == 2 assert isinstance(alpha_mask[0], torch.Tensor) assert alpha_mask[0].shape == (1, img_size[1], img_size[0]) @@ -314,7 +312,7 @@ def mock_extract_mask_from_greyscale(img): # Verify the mask shape assert isinstance(gray_mask, tuple) - assert len(gray_mask) == 1 + assert len(gray_mask) == 2 assert isinstance(gray_mask[0], torch.Tensor) assert gray_mask[0].shape == (1, img_size[1], img_size[0]) @@ -352,7 +350,7 @@ def mock_extract_mask_from_alpha(img): # Test loading an image with alpha load_node = PathLoadImageRGBA() - loaded_img, loaded_mask = load_node.load_image_rgba(img_path) + loaded_img, loaded_mask, success = load_node.load_image_rgba(img_path) # Verify that the returned objects are tensors with the right shapes assert isinstance(loaded_img, torch.Tensor) @@ -410,7 +408,7 @@ def mock_load_image_helper(path): # Verify that the returned object is a tensor with the right shape assert isinstance(loaded_img, tuple) - assert len(loaded_img) == 1 + assert len(loaded_img) == 2 assert isinstance(loaded_img[0], torch.Tensor) assert loaded_img[0].shape == (1, img_size[1], img_size[0], 3) # (batch, height, width, channels)