diff --git a/app/frontend_management.py b/app/frontend_management.py index 308f71da6..4b7dfbb98 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -11,33 +11,43 @@ from dataclasses import dataclass from functools import cached_property from pathlib import Path from typing import TypedDict, Optional +from importlib.metadata import version import requests from typing_extensions import NotRequired from comfy.cli_args import DEFAULT_VERSION_STRING +# The path to the requirements.txt file +req_path = Path(__file__).parents[1] / "requirements.txt" def frontend_install_warning_message(): - req_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'requirements.txt')) + """The warning message to display when the frontend version is not up to date.""" + extra = "" if sys.flags.no_user_site: extra = "-s " return f"Please install the updated requirements.txt file by running:\n{sys.executable} {extra}-m pip install -r {req_path}\n\nThis error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead.\n\nIf you are on the portable package you can run: update\\update_comfyui.bat to solve this problem" -try: - import comfyui_frontend_package -except ImportError: - # TODO: Remove the check after roll out of 0.3.16 - logging.error(f"\n\n********** ERROR ***********\n\ncomfyui-frontend-package is not installed. {frontend_install_warning_message()}\n********** ERROR **********\n") - exit(-1) +def check_frontend_version(): + """Check if the frontend version is up to date.""" + + def parse_version(version: str) -> tuple[int, int, int]: + return tuple(map(int, version.split("."))) + + try: + frontend_version_str = version("comfyui-frontend-package") + frontend_version = parse_version(frontend_version_str) + with open(req_path, "r", encoding="utf-8") as f: + required_frontend = parse_version(f.readline().split("=")[-1]) + if frontend_version < required_frontend: + logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) + else: + logging.info("ComfyUI frontend version: {}".format(frontend_version_str)) + except Exception as e: + logging.error(f"Failed to check frontend version: {e}") -try: - frontend_version = tuple(map(int, comfyui_frontend_package.__version__.split("."))) -except: - frontend_version = (0,) - pass REQUEST_TIMEOUT = 10 # seconds @@ -133,9 +143,17 @@ def download_release_asset_zip(release: Release, destination_path: str) -> None: class FrontendManager: - DEFAULT_FRONTEND_PATH = str(importlib.resources.files(comfyui_frontend_package) / "static") CUSTOM_FRONTENDS_ROOT = str(Path(__file__).parents[1] / "web_custom_versions") + @classmethod + def default_frontend_path(cls) -> str: + try: + import comfyui_frontend_package + return str(importlib.resources.files(comfyui_frontend_package) / "static") + except ImportError: + logging.error(f"\n\n********** ERROR ***********\n\ncomfyui-frontend-package is not installed. {frontend_install_warning_message()}\n********** ERROR **********\n") + sys.exit(-1) + @classmethod def parse_version_string(cls, value: str) -> tuple[str, str, str]: """ @@ -172,7 +190,8 @@ class FrontendManager: main error source might be request timeout or invalid URL. """ if version_string == DEFAULT_VERSION_STRING: - return cls.DEFAULT_FRONTEND_PATH + check_frontend_version() + return cls.default_frontend_path() repo_owner, repo_name, version = cls.parse_version_string(version_string) @@ -225,4 +244,5 @@ class FrontendManager: except Exception as e: logging.error("Failed to initialize frontend: %s", e) logging.info("Falling back to the default frontend.") - return cls.DEFAULT_FRONTEND_PATH + check_frontend_version() + return cls.default_frontend_path() diff --git a/main.py b/main.py index 6fa1cfb0f..c6f5c3c1e 100644 --- a/main.py +++ b/main.py @@ -139,7 +139,6 @@ from server import BinaryEventTypes import nodes import comfy.model_management import comfyui_version -import app.frontend_management def cuda_malloc_warning(): @@ -293,28 +292,13 @@ def start_comfyui(asyncio_loop=None): return asyncio_loop, prompt_server, start_all -def warn_frontend_version(frontend_version): - try: - required_frontend = (0,) - req_path = os.path.join(os.path.dirname(__file__), 'requirements.txt') - with open(req_path, 'r') as f: - required_frontend = tuple(map(int, f.readline().split('=')[-1].split('.'))) - if frontend_version < required_frontend: - logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), app.frontend_management.frontend_install_warning_message())) - except: - pass - - if __name__ == "__main__": # Running directly, just start ComfyUI. logging.info("ComfyUI version: {}".format(comfyui_version.__version__)) - frontend_version = app.frontend_management.frontend_version - logging.info("ComfyUI frontend version: {}".format('.'.join(map(str, frontend_version)))) event_loop, _, start_all_func = start_comfyui() try: x = start_all_func() - warn_frontend_version(frontend_version) event_loop.run_until_complete(x) except KeyboardInterrupt: logging.info("\nStopped server") diff --git a/nodes.py b/nodes.py index bbf49915c..e43c29295 100644 --- a/nodes.py +++ b/nodes.py @@ -1785,14 +1785,7 @@ class LoadImageOutput(LoadImage): DESCRIPTION = "Load an image from the output folder. When the refresh button is clicked, the node will update the image list and automatically select the first image, allowing for easy iteration." EXPERIMENTAL = True - FUNCTION = "load_image_output" - - def load_image_output(self, image): - return self.load_image(f"{image} [output]") - - @classmethod - def VALIDATE_INPUTS(s, image): - return True + FUNCTION = "load_image" class ImageScale: diff --git a/tests-unit/app_test/frontend_manager_test.py b/tests-unit/app_test/frontend_manager_test.py index a8df52484..ce67df6c6 100644 --- a/tests-unit/app_test/frontend_manager_test.py +++ b/tests-unit/app_test/frontend_manager_test.py @@ -70,7 +70,7 @@ def test_get_release_invalid_version(mock_provider): def test_init_frontend_default(): version_string = DEFAULT_VERSION_STRING frontend_path = FrontendManager.init_frontend(version_string) - assert frontend_path == FrontendManager.DEFAULT_FRONTEND_PATH + assert frontend_path == FrontendManager.default_frontend_path() def test_init_frontend_invalid_version(): @@ -84,24 +84,29 @@ def test_init_frontend_invalid_provider(): with pytest.raises(HTTPError): FrontendManager.init_frontend_unsafe(version_string) + @pytest.fixture def mock_os_functions(): - with patch('app.frontend_management.os.makedirs') as mock_makedirs, \ - patch('app.frontend_management.os.listdir') as mock_listdir, \ - patch('app.frontend_management.os.rmdir') as mock_rmdir: + with ( + patch("app.frontend_management.os.makedirs") as mock_makedirs, + patch("app.frontend_management.os.listdir") as mock_listdir, + patch("app.frontend_management.os.rmdir") as mock_rmdir, + ): mock_listdir.return_value = [] # Simulate empty directory yield mock_makedirs, mock_listdir, mock_rmdir + @pytest.fixture def mock_download(): - with patch('app.frontend_management.download_release_asset_zip') as mock: + with patch("app.frontend_management.download_release_asset_zip") as mock: mock.side_effect = Exception("Download failed") # Simulate download failure yield mock + def test_finally_block(mock_os_functions, mock_download, mock_provider): # Arrange mock_makedirs, mock_listdir, mock_rmdir = mock_os_functions - version_string = 'test-owner/test-repo@1.0.0' + version_string = "test-owner/test-repo@1.0.0" # Act & Assert with pytest.raises(Exception): @@ -128,3 +133,42 @@ def test_parse_version_string_invalid(): version_string = "invalid" with pytest.raises(argparse.ArgumentTypeError): FrontendManager.parse_version_string(version_string) + + +def test_init_frontend_default_with_mocks(): + # Arrange + version_string = DEFAULT_VERSION_STRING + + # Act + with ( + patch("app.frontend_management.check_frontend_version") as mock_check, + patch.object( + FrontendManager, "default_frontend_path", return_value="/mocked/path" + ), + ): + frontend_path = FrontendManager.init_frontend(version_string) + + # Assert + assert frontend_path == "/mocked/path" + mock_check.assert_called_once() + + +def test_init_frontend_fallback_on_error(): + # Arrange + version_string = "test-owner/test-repo@1.0.0" + + # Act + with ( + patch.object( + FrontendManager, "init_frontend_unsafe", side_effect=Exception("Test error") + ), + patch("app.frontend_management.check_frontend_version") as mock_check, + patch.object( + FrontendManager, "default_frontend_path", return_value="/default/path" + ), + ): + frontend_path = FrontendManager.init_frontend(version_string) + + # Assert + assert frontend_path == "/default/path" + mock_check.assert_called_once()