diff --git a/__init__.py b/__init__.py index ab85975c..60498cf6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,12 +1,16 @@ import os +import sys cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode') +is_pytest_running = 'pytest' in sys.modules -if not os.path.exists(cli_mode_flag): +if os.path.exists(cli_mode_flag): + print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n") +elif is_pytest_running: + print("\n[ComfyUI-Manager] !! Running in pytest environment !!\n") +else: from .glob import manager_server WEB_DIRECTORY = "js" -else: - print(f"\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n") NODE_CLASS_MAPPINGS = {} __all__ = ['NODE_CLASS_MAPPINGS'] diff --git a/glob/manager_core.py b/glob/manager_core.py index fc604328..4c06548e 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -176,6 +176,13 @@ def get_installed_packages(): return pip_map +def call_cli_dependencies(): + try: + result = subprocess.check_output([sys.executable, '-m', 'comfy_cli', 'dependency'], universal_newlines=True) + return result, False + except subprocess.CalledProcessError as e: + print("[ComfyUI-Manager] Failed to execute the command 'python -m comfy_cli dependency'.") + return None, True def clear_pip_cache(): global pip_map @@ -1466,6 +1473,7 @@ def write_config(): 'downgrade_blacklist': get_config()['downgrade_blacklist'], 'security_level': get_config()['security_level'], 'skip_migration_check': get_config()['skip_migration_check'], + 'use_uv_install': get_config()['use_uv_install'], } with open(config_path, 'w') as configfile: config.write(configfile) @@ -1500,7 +1508,8 @@ def read_config(): 'model_download_by_agent': default_conf['model_download_by_agent'].lower() == 'true' if 'model_download_by_agent' in default_conf else False, 'downgrade_blacklist': default_conf['downgrade_blacklist'] if 'downgrade_blacklist' in default_conf else '', 'skip_migration_check': default_conf['skip_migration_check'].lower() == 'true' if 'skip_migration_check' in default_conf else False, - 'security_level': security_level + 'security_level': security_level, + 'use_uv_install': default_conf['use_uv_install'].lower() == 'true' if 'use_uv_install' in default_conf else False, } except Exception: @@ -1519,6 +1528,7 @@ def read_config(): 'downgrade_blacklist': '', 'skip_migration_check': False, 'security_level': 'normal', + 'use_uv_install': False, } @@ -1645,7 +1655,6 @@ def __win_check_git_pull(path): def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False): - # import ipdb; ipdb.set_trace() install_script_path = os.path.join(repo_path, "install.py") requirements_path = os.path.join(repo_path, "requirements.txt") diff --git a/glob/manager_server.py b/glob/manager_server.py index c57145dd..47970b6f 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -716,13 +716,15 @@ async def install_custom_node(request): print(SECURITY_MESSAGE_GENERAL) return web.Response(status=404) + no_deps = json_data.get('noDeps', False) + node_spec = core.unified_manager.resolve_node_spec(node_spec_str) if node_spec is None: return - + node_name, version_spec, is_specified = node_spec - res = await core.unified_manager.install_by_id(node_name, version_spec, json_data['channel'], json_data['mode'], return_postinstall=skip_post_install) + res = await core.unified_manager.install_by_id(node_name, version_spec, json_data['channel'], json_data['mode'], return_postinstall=skip_post_install, no_deps=no_deps) # discard post install if skip_post_install mode if res not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']: @@ -730,6 +732,29 @@ async def install_custom_node(request): return web.Response(status=200) +@routes.post("/customnode/uv-install-deps") +async def uv_install_deps(request): + if not is_allowed_security_level('middle'): + print(SECURITY_MESSAGE_MIDDLE_OR_BELOW) + return web.Response(status=403) + + json_data = await request.json() + node_id = json_data.get('id') + pip_dependencies = json_data.get('pip', []) + + if not pip_dependencies: + return web.Response(status=400, text="No dependencies provided") + + try: + result = core.call_cli_dependencies() + if result: + return web.Response(status=200, text="Dependencies installed successfully") + else: + return web.Response(status=500, text="Failed to install dependencies") + except Exception as e: + return web.Response(status=500, text=f"Error installing dependencies: {str(e)}") + + @routes.post("/customnode/fix") async def fix_custom_node(request): @@ -856,17 +881,6 @@ async def need_to_migrate(request): return web.Response(text=str(core.need_to_migrate), status=200) -@routes.get("/manager/badge_mode") -async def badge_mode(request): - if "value" in request.rel_url.query: - set_badge_mode(request.rel_url.query['value']) - core.write_config() - else: - return web.Response(text=core.get_config()['badge_mode'], status=200) - - return web.Response(status=200) - - @routes.get("/manager/channel_url_list") async def channel_url_list(request): channels = core.get_channel_dict() @@ -890,6 +904,16 @@ async def channel_url_list(request): return web.Response(status=200) +@routes.get("/manager/uv-install") +async def uv_install(request): + if "value" in request.rel_url.query: + value = request.rel_url.query['value'].lower() == 'true' + core.get_config()['use_uv_install'] = value + core.write_config() + return web.Response(status=200) + else: + return web.json_response({"use_uv_install": core.get_config().get('use_uv_install', False)}, status=200) + def add_target_blank(html_text): pattern = r'(]*)(>)' diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 8b6910a9..c1e87e9a 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -775,6 +775,12 @@ class ManagerMenuDialog extends ComfyDialog { uc_checkbox_text.style.cursor = "pointer"; this.update_check_checkbox.checked = true; + this.use_uv_install_checkbox = $el("input", { type: 'checkbox', id: "use_uv_install" }, []) + const use_uv_checkbox_text = $el("label", { for: "use_uv_install" }, [" Use UV Install"]) + use_uv_checkbox_text.style.color = "var(--fg-color)"; + use_uv_checkbox_text.style.cursor = "pointer"; + this.use_uv_install_checkbox.checked = false; + // db mode this.datasrc_combo = document.createElement("select"); this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened."); @@ -809,10 +815,33 @@ class ManagerMenuDialog extends ComfyDialog { } }); + api.fetchApi('/manager/uv-install') + .then(response => response.json()) + .then(data => { + this.use_uv_install_checkbox.checked = data.use_uv_install; + + this.use_uv_install_checkbox.addEventListener('change', function (event) { + api.fetchApi(`/manager/uv-install?value=${event.target.checked}`) + .then(response => { + if (response.status !== 200) { + console.error("Failed to update UV Install setting"); + } + }) + .catch(error => { + console.error("Error updating UV Install setting:", error); + }); + }); + + + }) + .catch(error => { + console.error("Error fetching initial UV Install setting:", error); + }); return [ $el("div", {}, [this.update_check_checkbox, uc_checkbox_text]), + $el("div", {}, [this.use_uv_install_checkbox, use_uv_checkbox_text]), $el("br", {}, []), this.datasrc_combo, channel_combo, diff --git a/js/custom-nodes-manager.js b/js/custom-nodes-manager.js index dc53e57f..4a2d84d2 100644 --- a/js/custom-nodes-manager.js +++ b/js/custom-nodes-manager.js @@ -1229,7 +1229,7 @@ export class CustomNodesManager { const res = await api.fetchApi(`/customnode/${api_mode}`, { method: 'POST', - body: JSON.stringify(data) + body: JSON.stringify(manager_instance.use_uv_install_checkbox.checked ? { ...data, noDeps: true } : data) }); if (res.error) { @@ -1257,6 +1257,12 @@ export class CustomNodesManager { } + if (manager_instance.use_uv_install_checkbox.checked) { + const res = await api.fetchApi(`/customnode/uv-install-deps`, { + method: 'POST', + }); + } + target.classList.remove("cn-btn-loading"); if (errorMsg) { diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..796b7e69 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,8 @@ +# Pytest Unit Tests + +## Install test dependencies + +`pip install -r tests/requirements.txt` + +## Run tests +`pytest tests/` \ No newline at end of file diff --git a/tests/glob_test/__init__.py b/tests/glob_test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/glob_test/manager_core_test.py b/tests/glob_test/manager_core_test.py new file mode 100644 index 00000000..df62c234 --- /dev/null +++ b/tests/glob_test/manager_core_test.py @@ -0,0 +1,37 @@ +import pytest +import subprocess +import sys +import os + +sys.path.append(os.path.join(os.getcwd(), "glob")) +import manager_core + +def test_call_cli_dependencies_success(mocker): + """Test successful execution of call_cli_dependencies().""" + mock_check_output = mocker.patch('subprocess.check_output') + mock_check_output.return_value = "Mocked dependencies output" + + result, error_occurred = manager_core.call_cli_dependencies() + + assert result == "Mocked dependencies output" + assert error_occurred is False + mock_check_output.assert_called_once_with([sys.executable, '-m', 'comfy_cli', 'dependency'], universal_newlines=True) + + +def test_call_cli_dependencies_failure(mocker): + """Test failure case of call_cli_dependencies().""" + mock_check_output = mocker.patch('subprocess.check_output') + mock_check_output.side_effect = subprocess.CalledProcessError(1, 'cmd') + + from io import StringIO + import sys + captured_output = StringIO() + sys.stdout = captured_output + + result, error_occurred = manager_core.call_cli_dependencies() + + sys.stdout = sys.__stdout__ + + assert result is None + assert error_occurred is True + assert "[ComfyUI-Manager] Failed to execute the command 'python -m comfy_cli dependency'." in captured_output.getvalue() \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..6ab80d22 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +pytest>=7.8.0 \ No newline at end of file