diff --git a/.gitignore b/.gitignore index fffdff1..61e15be 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ __pycache__/ .vscode/settings.json .idea/ .vscode/ +*.code-workspace +.history # Byte-compiled / optimized / DLL files __pycache__/ @@ -45,6 +47,9 @@ share/python-wheels/ *.manifest *.spec +# temporary files created by tests +tests/temp/ + venv/ bisect_state.json diff --git a/.pylintrc b/.pylintrc index 22987f1..a31e900 100644 --- a/.pylintrc +++ b/.pylintrc @@ -16,6 +16,8 @@ disable= W0212, # protected-access C0301, # line-too-long C0103, # invalid-name + W1510, # subprocess-run-check + W0707, # raise-missing-from # TODO W3101, # missing timeout on request diff --git a/DEV_README.md b/DEV_README.md index fee7e17..aba94e3 100644 --- a/DEV_README.md +++ b/DEV_README.md @@ -83,6 +83,26 @@ def remove(name: str): - Use `rich` for all console output - For progress reporting, use either [`rich.progress`](https://rich.readthedocs.io/en/stable/progress.html) +## Develop comfy-cli and ComfyUI-Manager (cm-cli) together +### Make changes to both +1. Fork your own branches of `comfy-cli` and `ComfyUI-Manager`, make changes +2. Be sure to commit any changes to `ComfyUI-Manager` to a new branch, and push to remote + +### Try out changes to both +1. clone the changed branch of `comfy-cli`, then live install `comfy-cli`: + - `pip install -e comfy-cli` +2. Go to a test dir and run: + - `comfy --here install --manager-url=` +3. Run: + - `cd ComfyUI/custom_nodes/ComfyUI-Manager/ && git checkout && cd -` +4. Further changes can be pulled into these copies of the `comfy-cli` and `ComfyUI-Manager` repos + +### Debug both simultaneously +1. Follow instructions above to get working install with changes +2. Add breakpoints directly to code: `import ipdb; ipdb.set_trace()` +3. Execute relevant `comfy-cli` command + + ## Contact If you have any questions or need further assistance, please contact the project maintainer at [???](mailto:???@drip.art). diff --git a/comfy_cli/cmdline.py b/comfy_cli/cmdline.py index 32c3e39..e13dc68 100644 --- a/comfy_cli/cmdline.py +++ b/comfy_cli/cmdline.py @@ -219,6 +219,14 @@ def install( commit: Annotated[ Optional[str], typer.Option(help="Specify commit hash for ComfyUI") ] = None, + fast_deps: Annotated[ + Optional[bool], + typer.Option( + "--fast-deps", + show_default=False, + help="Use new fast dependency installer", + ), + ] = False, ): check_for_updates() checker = EnvChecker() @@ -260,6 +268,7 @@ def install( plat=platform, skip_torch_or_directml=skip_torch_or_directml, skip_requirement=skip_requirement, + fast_deps=fast_deps, ) print(f"ComfyUI is installed at: {comfy_path}") return None @@ -331,6 +340,7 @@ def install( plat=platform, skip_torch_or_directml=skip_torch_or_directml, skip_requirement=skip_requirement, + fast_deps=fast_deps, ) print(f"ComfyUI is installed at: {comfy_path}") diff --git a/comfy_cli/command/custom_nodes/cm_cli_util.py b/comfy_cli/command/custom_nodes/cm_cli_util.py index 1fcccda..c17c944 100644 --- a/comfy_cli/command/custom_nodes/cm_cli_util.py +++ b/comfy_cli/command/custom_nodes/cm_cli_util.py @@ -9,12 +9,18 @@ from rich import print from comfy_cli.config_manager import ConfigManager +from comfy_cli.uv import DependencyCompiler from comfy_cli.workspace_manager import WorkspaceManager workspace_manager = WorkspaceManager() +# set of commands that invalidate (ie require an update of) dependencies after they are run +_dependency_cmds = { + 'install', + 'reinstall', +} -def execute_cm_cli(args, channel=None, mode=None) -> str | None: +def execute_cm_cli(args, channel=None, fast_deps=False, mode=None) -> str | None: _config_manager = ConfigManager() workspace_path = workspace_manager.workspace_path @@ -34,9 +40,13 @@ def execute_cm_cli(args, channel=None, mode=None) -> str | None: raise typer.Exit(code=1) cmd = [sys.executable, cm_cli_path] + args + if channel is not None: cmd += ["--channel", channel] + if fast_deps: + cmd += ["--no-deps"] + if mode is not None: cmd += ["--mode", mode] @@ -54,6 +64,12 @@ def execute_cm_cli(args, channel=None, mode=None) -> str | None: cmd, env=new_env, check=True, capture_output=True, text=True ) print(result.stdout) + + if fast_deps and args[0] in _dependency_cmds: + # we're using the fast_deps behavior and just ran a command that invalidated the dependencies + depComp = DependencyCompiler(cwd=workspace_path) + depComp.install_comfy_deps() + return result.stdout except subprocess.CalledProcessError as e: if e.returncode == 1: diff --git a/comfy_cli/command/custom_nodes/command.py b/comfy_cli/command/custom_nodes/command.py index 218b796..823a1a2 100644 --- a/comfy_cli/command/custom_nodes/command.py +++ b/comfy_cli/command/custom_nodes/command.py @@ -363,7 +363,7 @@ def show( validate_mode(mode) - execute_cm_cli(["show", arg], channel, mode) + execute_cm_cli(["show", arg], channel=channel, mode=mode) @app.command("simple-show", help="Show node list (simple mode)") @@ -402,7 +402,7 @@ def simple_show( validate_mode(mode) - execute_cm_cli(["simple-show", arg], channel, mode) + execute_cm_cli(["simple-show", arg], channel=channel, mode=mode) # install, reinstall, uninstall @@ -420,6 +420,14 @@ def install( autocompletion=channel_completer, ), ] = None, + fast_deps: Annotated[ + Optional[bool], + typer.Option( + "--fast-deps", + show_default=False, + help="Use new fast dependency installer", + ), + ] = False, mode: str = typer.Option( None, help="[remote|local|cache]", @@ -432,7 +440,7 @@ def install( validate_mode(mode) - execute_cm_cli(["install"] + nodes, channel, mode) + execute_cm_cli(["install"] + nodes, channel=channel, fast_deps=fast_deps, mode=mode) @app.command(help="Reinstall custom nodes") @@ -449,6 +457,14 @@ def reinstall( autocompletion=channel_completer, ), ] = None, + fast_deps: Annotated[ + Optional[bool], + typer.Option( + "--fast-deps", + show_default=False, + help="Use new fast dependency installer", + ), + ] = False, mode: str = typer.Option( None, help="[remote|local|cache]", @@ -461,7 +477,7 @@ def reinstall( validate_mode(mode) - execute_cm_cli(["reinstall"] + nodes, channel, mode) + execute_cm_cli(["reinstall"] + nodes, channel=channel, fast_deps=fast_deps, mode=mode) @app.command(help="Uninstall custom nodes") @@ -490,7 +506,7 @@ def uninstall( validate_mode(mode) - execute_cm_cli(["uninstall"] + nodes, channel, mode) + execute_cm_cli(["uninstall"] + nodes, channel=channel, mode=mode) def update_node_id_cache(): @@ -544,7 +560,7 @@ def update( ): validate_mode(mode) - execute_cm_cli(["update"] + nodes, channel, mode) + execute_cm_cli(["update"] + nodes, channel=channel, mode=mode) update_node_id_cache() @@ -573,7 +589,7 @@ def disable( ): validate_mode(mode) - execute_cm_cli(["disable"] + nodes, channel, mode) + execute_cm_cli(["disable"] + nodes, channel=channel, mode=mode) @app.command(help="Enable custom nodes") @@ -600,7 +616,7 @@ def enable( ): validate_mode(mode) - execute_cm_cli(["enable"] + nodes, channel, mode) + execute_cm_cli(["enable"] + nodes, channel=channel, mode=mode) @app.command(help="Fix dependencies of custom nodes") @@ -627,7 +643,7 @@ def fix( ): validate_mode(mode) - execute_cm_cli(["fix"] + nodes, channel, mode) + execute_cm_cli(["fix"] + nodes, channel=channel, mode=mode) @app.command( @@ -685,7 +701,7 @@ def install_deps( else: deps_file = os.path.abspath(os.path.expanduser(deps)) - execute_cm_cli(["install-deps", deps_file], channel, mode) + execute_cm_cli(["install-deps", deps_file], channel=channel, mode=mode) if tmp_path is not None and os.path.exists(tmp_path): os.remove(tmp_path) diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index 553046e..f4c8f0d 100644 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -9,6 +9,7 @@ from comfy_cli import constants, ui, utils from comfy_cli.command.custom_nodes.command import update_node_id_cache from comfy_cli.constants import GPU_OPTION +from comfy_cli.uv import DependencyCompiler from comfy_cli.workspace_manager import WorkspaceManager, check_comfy_repo workspace_manager = WorkspaceManager() @@ -20,7 +21,7 @@ def get_os_details(): return os_name, os_version -def install_comfyui_dependencies( +def pip_install_comfyui_dependencies( repo_dir, gpu: GPU_OPTION, plat: constants.OS, @@ -150,7 +151,7 @@ def install_comfyui_dependencies( # install requirements for manager -def install_manager_dependencies(repo_dir): +def pip_install_manager_dependencies(repo_dir): os.chdir(os.path.join(repo_dir, "custom_nodes", "ComfyUI-Manager")) subprocess.run( [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True @@ -169,6 +170,7 @@ def execute( plat: constants.OS = None, skip_torch_or_directml: bool = False, skip_requirement: bool = False, + fast_deps: bool = False, *args, **kwargs, ): @@ -192,7 +194,6 @@ def execute( if "@" in url: # clone specific branch url, branch = url.rsplit("@", 1) - subprocess.run(["git", "clone", "-b", branch, url, repo_dir], check=True) else: subprocess.run(["git", "clone", url, repo_dir], check=True) @@ -208,9 +209,10 @@ def execute( os.chdir(repo_dir) subprocess.run(["git", "checkout", commit], check=True) - install_comfyui_dependencies( - repo_dir, gpu, plat, cuda_version, skip_torch_or_directml, skip_requirement - ) + if not fast_deps: + pip_install_comfyui_dependencies( + repo_dir, gpu, plat, cuda_version, skip_torch_or_directml, skip_requirement + ) WorkspaceManager().set_recent_workspace(repo_dir) workspace_manager.setup_workspace_manager(specified_workspace=repo_dir) @@ -224,8 +226,8 @@ def execute( manager_repo_dir = os.path.join(repo_dir, "custom_nodes", "ComfyUI-Manager") if os.path.exists(manager_repo_dir): - if restore: - install_manager_dependencies(repo_dir) + if restore and not fast_deps: + pip_install_manager_dependencies(repo_dir) else: print( f"Directory {manager_repo_dir} already exists. Skipping installation of ComfyUI-Manager.\nIf you want to restore dependencies, add the '--restore' option." @@ -236,13 +238,18 @@ def execute( if "@" in manager_url: # clone specific branch manager_url, manager_branch = manager_url.rsplit("@", 1) - subprocess.run(["git", "clone", "-b", manager_branch, manager_url, manager_repo_dir], check=True) else: subprocess.run(["git", "clone", manager_url, manager_repo_dir], check=True) - install_manager_dependencies(repo_dir) + if not fast_deps: + pip_install_manager_dependencies(repo_dir) + + if fast_deps: + depComp = DependencyCompiler(cwd=repo_dir, gpu=gpu) + depComp.install_comfy_deps() + if not skip_manager: update_node_id_cache() os.chdir(repo_dir) diff --git a/comfy_cli/ui.py b/comfy_cli/ui.py index c79fc23..77054b0 100644 --- a/comfy_cli/ui.py +++ b/comfy_cli/ui.py @@ -36,9 +36,33 @@ def show_progress(iterable, total, description="Downloading..."): ChoiceType = Union[str, Choice, Dict[str, Any]] +def prompt_autocomplete( + question: str, + choices: List[ChoiceType], + default: ChoiceType = "", + force_prompting: bool = False +) -> Optional[ChoiceType]: + """ + Asks a single select question using questionary and returns the selected response. + + Args: + question (str): The question to display to the user. + choices (List[ChoiceType]): A list of choices the user can autocomplete from. + default (ChoiceType): Default choice. + force_prompting (bool): Whether to force prompting even if skip_prompting is set. + + Returns: + Optional[ChoiceType]: The selected choice from the user, or None if skipping prompts. + """ + if workspace_manager.skip_prompting and not force_prompting: + return None + return questionary.autocomplete(question, choices=choices, default=default).ask() + + def prompt_select( question: str, choices: List[ChoiceType], + default: ChoiceType = "", force_prompting: bool = False ) -> Optional[ChoiceType]: """ @@ -47,6 +71,7 @@ def prompt_select( Args: question (str): The question to display to the user. choices (List[ChoiceType]): A list of choices for the user to select from. + default (ChoiceType): Default choice. force_prompting (bool): Whether to force prompting even if skip_prompting is set. Returns: @@ -54,7 +79,7 @@ def prompt_select( """ if workspace_manager.skip_prompting and not force_prompting: return None - return questionary.select(question, choices=choices).ask() + return questionary.select(question, choices=choices, default=default).ask() E = TypeVar('E', bound=Enum) diff --git a/comfy_cli/uv.py b/comfy_cli/uv.py new file mode 100644 index 0000000..4f0a66a --- /dev/null +++ b/comfy_cli/uv.py @@ -0,0 +1,368 @@ +from importlib import metadata +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +from textwrap import dedent +from typing import Any, Optional, Union, cast + +from comfy_cli.constants import GPU_OPTION +from comfy_cli import ui + +PathLike = Union[os.PathLike[str], str] + +def _run(cmd: list[str], cwd: PathLike) -> subprocess.CompletedProcess[Any]: + return subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + check=True + ) + +def _check_call(cmd: list[str], cwd: Optional[PathLike] = None): + """uses check_call to run pip, as reccomended by the pip maintainers. + see https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program""" + + subprocess.check_call(cmd, cwd=cwd) + +_req_name_re: re.Pattern[str] = re.compile(r"require\s([\w-]+)") + +def _req_re_closure(name: str) -> re.Pattern[str]: + return re.compile(rf"({name}\S+)") + +def parse_uv_compile_error(err: str) -> tuple[str, list[str]]: + """takes in stderr from a run of `uv pip compile` that failed due to requirement conflict and spits out + a tuple of (reqiurement_name, [requirement_spec_in_conflict_a, requirement_spec_in_conflict_b]). Will probably + fail for stderr produced from other kinds of errors + """ + if reqNameMatch := _req_name_re.search(err): + reqName = reqNameMatch[1] + else: + raise ValueError + + reqRe = _req_re_closure(reqName) + + return reqName, cast(list[str], reqRe.findall(err)) + +class DependencyCompiler: + rocmPytorchUrl = "https://download.pytorch.org/whl/rocm6.0" + nvidiaPytorchUrl = "https://download.pytorch.org/whl/cu121" + + overrideGpu = dedent(""" + # ensure usage of {gpu} version of pytorch + --extra-index-url {gpuUrl} + torch + torchsde + torchvision + """).strip() + + reqNames = { + "requirements.txt", + "pyproject.toml", + "setup.cfg", + "setup.py", + } + + @staticmethod + def Find_Req_Files(*ders: PathLike) -> list[Path]: + return [file + for der in ders + for file in Path(der).absolute().iterdir() + if file.name in DependencyCompiler.reqNames + ] + + @staticmethod + def Install_Build_Deps(): + """Use pip to install bare minimum requirements for uv to do its thing + """ + if shutil.which("uv") is None: + cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--upgrade", + "pip", + "uv" + ] + + _check_call(cmd=cmd) + + @staticmethod + def Compile( + cwd: PathLike, + reqFiles: list[PathLike], + override: Optional[PathLike] = None, + out: Optional[PathLike] = None, + index_strategy: Optional[str] = "unsafe-best-match", + resolve_strategy: Optional[str] = None, + ) -> subprocess.CompletedProcess[Any]: + cmd = [ + sys.executable, + "-m", + "uv", + "pip", + "compile", + ] + + for reqFile in reqFiles: + cmd.append(str(reqFile)) + + # ensures that eg tqdm is latest version, even though an old tqdm is on the amd url + # see https://github.com/astral-sh/uv/blob/main/PIP_COMPATIBILITY.md#packages-that-exist-on-multiple-indexes and https://github.com/astral-sh/uv/issues/171 + if index_strategy is not None: + cmd.extend([ + "--index-strategy", + "unsafe-best-match", + ]) + + if override is not None: + cmd.extend([ + "--override", + str(override), + ]) + + if out is not None: + cmd.extend([ + "-o", + str(out), + ]) + + try: + return _run(cmd, cwd) + except subprocess.CalledProcessError as e: + print(e.__class__.__name__) + print(e) + print(f"STDOUT:\n{e.stdout}") + print(f"STDERR:\n{e.stderr}") + + if resolve_strategy == "ask": + name, reqs = parse_uv_compile_error(e.stderr) + vers = [req.split(name)[1].strip(",") for req in reqs] + + ver = ui.prompt_select( + "Please pick one of the conflicting version specs (or pick latest):", + choices=vers + ["latest"], + default=vers[0], + ) + + if ver == "latest": + req = name + else: + req = name + ver + + e.req = req + elif resolve_strategy is not None: + # no other resolve_strategy options implemented yet + raise ValueError + + raise e + + @staticmethod + def Install( + cwd: PathLike, + reqFile: list[PathLike], + override: Optional[PathLike] = None, + extraUrl: Optional[str] = None, + index_strategy: Optional[str] = "unsafe-best-match", + dry: bool = False + ) -> subprocess.CompletedProcess[Any]: + cmd = [ + sys.executable, + "-m", + "uv", + "pip", + "install", + "-r", + str(reqFile), + ] + + if index_strategy is not None: + cmd.extend([ + "--index-strategy", + "unsafe-best-match", + ]) + + if extraUrl is not None: + cmd.extend([ + "--extra-index-url", + extraUrl, + ]) + + if override is not None: + cmd.extend([ + "--override", + str(override), + ]) + + if dry: + cmd.append("--dry-run") + + return _check_call(cmd, cwd) + + @staticmethod + def Sync( + cwd: PathLike, + reqFile: list[PathLike], + extraUrl: Optional[str] = None, + index_strategy: Optional[str] = "unsafe-best-match", + dry: bool = False + ) -> subprocess.CompletedProcess[Any]: + cmd = [ + sys.executable, + "-m", + "uv", + "pip", + "sync", + str(reqFile), + ] + + if index_strategy is not None: + cmd.extend([ + "--index-strategy", + "unsafe-best-match", + ]) + + if extraUrl is not None: + cmd.extend([ + "--extra-index-url", + extraUrl, + ]) + + if dry: + cmd.append("--dry-run") + + return _check_call(cmd, cwd) + + @staticmethod + def Resolve_Gpu(gpu: Union[str, None]): + if gpu is None: + try: + tver = metadata.version("torch") + if "+cu" in tver: + return GPU_OPTION.NVIDIA + elif "+rocm" in tver: + return GPU_OPTION.AMD + else: + return None + except metadata.PackageNotFoundError: + return None + else: + return gpu + + def __init__( + self, + cwd: PathLike = ".", + reqFilesCore: Optional[list[PathLike]] = None, + reqFilesExt: Optional[list[PathLike]] = None, + gpu: Optional[str] = None, + outName: str = "requirements.compiled", + ): + self.cwd = Path(cwd) + self.reqFiles = [Path(reqFile) for reqFile in reqFilesExt] if reqFilesExt is not None else None + self.gpu = DependencyCompiler.Resolve_Gpu(gpu) + + self.gpuUrl = DependencyCompiler.nvidiaPytorchUrl if self.gpu == GPU_OPTION.NVIDIA else DependencyCompiler.rocmPytorchUrl if self.gpu == GPU_OPTION.AMD else None + self.out = self.cwd / outName + self.override = self.cwd / "override.txt" + + self.reqFilesCore = reqFilesCore if reqFilesCore is not None else self.find_core_reqs() + self.reqFilesExt = reqFilesExt if reqFilesExt is not None else self.find_ext_reqs() + + def find_core_reqs(self): + return DependencyCompiler.Find_Req_Files(self.cwd) + + def find_ext_reqs(self): + extDirs = [d for d in (self.cwd / "custom_nodes").iterdir() if d.is_dir() and d.name != "__pycache__"] + return DependencyCompiler.Find_Req_Files(*extDirs) + + def make_override(self): + #clean up + self.override.unlink(missing_ok=True) + + with open(self.override, "w") as f: + if self.gpu is not None: + f.write(DependencyCompiler.overrideGpu.format(gpu=self.gpu, gpuUrl=self.gpuUrl)) + f.write("\n\n") + + completed = DependencyCompiler.Compile( + cwd=self.cwd, + reqFiles=self.reqFilesCore, + override=self.override + ) + + with open(self.override, "a") as f: + f.write("# ensure that core comfyui deps take precedence over any 3rd party extension deps\n") + for line in completed.stdout: + f.write(line) + f.write("\n") + + def compile_core_plus_ext(self): + #clean up + self.out.unlink(missing_ok=True) + + while True: + try: + DependencyCompiler.Compile( + cwd=self.cwd, + reqFiles=(self.reqFilesCore + self.reqFilesExt), + override=self.override, + out=self.out, + resolve_strategy="ask", + ) + + break + except subprocess.CalledProcessError as e: + if hasattr(e, "req"): + with open(self.override, "a") as f: + f.write(e.req + "\n") + else: + raise AttributeError + + def install_core_plus_ext(self): + DependencyCompiler.Install( + cwd=self.cwd, + reqFile=self.out, + override=self.override, + extraUrl=self.gpuUrl, + ) + + def sync_core_plus_ext(self): + DependencyCompiler.Sync( + cwd=self.cwd, + reqFile=self.out, + extraUrl=self.gpuUrl, + ) + + def handle_opencv(self): + """as per the opencv docs, you should only have exactly one opencv package. + headless is more suitable for comfy than the gui version, so remove gui if + headless is present. TODO: add support for contrib pkgs. see: https://github.com/opencv/opencv-python""" + + with open(self.out, "r") as f: + lines = f.readlines() + + guiFound, headlessFound = False, False + for line in lines: + if "opencv-python==" in line: + guiFound = True + elif "opencv-python-headless==" in line: + headlessFound = True + + if headlessFound and guiFound: + with open(self.out, "w") as f: + for line in lines: + if "opencv-python==" not in line: + f.write(line) + + def install_comfy_deps(self): + DependencyCompiler.Install_Build_Deps() + + self.make_override() + self.compile_core_plus_ext() + self.handle_opencv() + + self.install_core_plus_ext() diff --git a/tests/uv/mock_requirements/core_reqs.txt b/tests/uv/mock_requirements/core_reqs.txt new file mode 100644 index 0000000..d69c960 --- /dev/null +++ b/tests/uv/mock_requirements/core_reqs.txt @@ -0,0 +1 @@ +tqdm==4.66.4 diff --git a/tests/uv/mock_requirements/requirements.compiled b/tests/uv/mock_requirements/requirements.compiled new file mode 100644 index 0000000..2ff3d84 --- /dev/null +++ b/tests/uv/mock_requirements/requirements.compiled @@ -0,0 +1,22 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile /home/tel/git/comfy-cli/tests/uv/mock_requirements/core_reqs.txt /home/tel/git/comfy-cli/tests/uv/mock_requirements/x_reqs.txt /home/tel/git/comfy-cli/tests/uv/mock_requirements/y_reqs.txt --index-strategy unsafe-best-match --override /home/tel/git/comfy-cli/tests/temp/test_uv/override.txt -o /home/tel/git/comfy-cli/tests/temp/test_uv/requirements.compiled +mpmath==1.3.0 + # via + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/y_reqs.txt + # sympy +numpy==2.0.0 + # via + # --override override.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/x_reqs.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/y_reqs.txt +sympy==1.13.0 + # via + # --override override.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/x_reqs.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/y_reqs.txt +tqdm==4.66.4 + # via + # --override override.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/core_reqs.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/x_reqs.txt + # -r /home/tel/git/comfy-cli/tests/uv/mock_requirements/y_reqs.txt diff --git a/tests/uv/mock_requirements/x_reqs.txt b/tests/uv/mock_requirements/x_reqs.txt new file mode 100644 index 0000000..a22c58d --- /dev/null +++ b/tests/uv/mock_requirements/x_reqs.txt @@ -0,0 +1,3 @@ +numpy>=2.0.0 +sympy<=1.10.1 +tqdm==1.0 diff --git a/tests/uv/mock_requirements/y_reqs.txt b/tests/uv/mock_requirements/y_reqs.txt new file mode 100644 index 0000000..a3e0604 --- /dev/null +++ b/tests/uv/mock_requirements/y_reqs.txt @@ -0,0 +1,4 @@ +mpmath==1.3.0 +numpy<=1.5.0 +sympy>=1.13.0 +tqdm==2.0.0 diff --git a/tests/uv/test_uv.py b/tests/uv/test_uv.py new file mode 100644 index 0000000..e7d5e07 --- /dev/null +++ b/tests/uv/test_uv.py @@ -0,0 +1,42 @@ +from pathlib import Path +import pytest +import shutil + +from comfy_cli.uv import DependencyCompiler +from comfy_cli import ui + +hereDir = Path(__file__).parent.resolve() +reqsDir = hereDir/"mock_requirements" + +# set up a temp dir to write files to +testsDir = hereDir.parent.resolve() +temp = testsDir/"temp"/"test_uv" +shutil.rmtree(temp, ignore_errors=True) +temp.mkdir(exist_ok=True, parents=True) + +@pytest.fixture +def mock_prompt_select(monkeypatch): + mockChoices = ["==1.13.0", "==2.0.0"] + def _mock_prompt_select(*args, **kwargs): + return mockChoices.pop(0) + + monkeypatch.setattr(ui, "prompt_select", _mock_prompt_select) + +def test_compile(mock_prompt_select): + depComp = DependencyCompiler( + cwd=temp, + reqFilesCore=[reqsDir/"core_reqs.txt"], + reqFilesExt=[reqsDir/"x_reqs.txt", reqsDir/"y_reqs.txt"], + ) + + DependencyCompiler.Install_Build_Deps() + depComp.make_override() + depComp.compile_core_plus_ext() + + with open(reqsDir/"requirements.compiled", "r") as known, open(temp/"requirements.compiled", "r") as test: + # compare all non-commented lines in generated file vs reference file + knownLines, testLines = [ + [line for line in known.readlines() if line.strip()[0]!="#"], + [line for line in test.readlines() if line.strip()[0]!="#"], + ] + assert knownLines == testLines