diff --git a/.gitignore b/.gitignore index 98e7af855..7465bbb28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +main.exp +main.lib +main.obj .DS_Store /[Oo]utput/ /[Ii]nput/ diff --git a/comfy/cli_args_types.py b/comfy/cli_args_types.py index 95409ee45..285aef9b9 100644 --- a/comfy/cli_args_types.py +++ b/comfy/cli_args_types.py @@ -124,7 +124,7 @@ class Configuration(dict): self._observers: List[ConfigObserver] = [] self.config_files = [] self.cwd: Optional[str] = None - self.base_paths: list[Path] = [] + self.base_paths: list[str] = [] self.listen: str = "127.0.0.1" self.port: int = 8188 self.enable_cors_header: Optional[str] = None diff --git a/comfy/client/embedded_comfy_client.py b/comfy/client/embedded_comfy_client.py index e4896d61e..0f83e6bcc 100644 --- a/comfy/client/embedded_comfy_client.py +++ b/comfy/client/embedded_comfy_client.py @@ -37,7 +37,7 @@ def _execute_prompt( configuration: Configuration | None) -> dict: execution_context = current_execution_context() if len(execution_context.folder_names_and_paths) == 0 or configuration is not None: - init_default_paths(execution_context.folder_names_and_paths, configuration) + init_default_paths(execution_context.folder_names_and_paths, configuration, replace_existing=True) span_context: Context = propagate.extract(span_context) token = attach(span_context) try: @@ -54,6 +54,8 @@ def __execute_prompt( progress_handler: ExecutorToClientProgress | None, configuration: Configuration | None) -> dict: from .. import options + from ..cmd.execution import PromptExecutor + progress_handler = progress_handler or ServerStub() try: @@ -66,8 +68,7 @@ def __execute_prompt( args.clear() args.update(configuration) - from ..cmd.execution import PromptExecutor - with tracer.start_as_current_span("Initialize Prompt Executor", context=span_context) as span: + with tracer.start_as_current_span("Initialize Prompt Executor", context=span_context): prompt_executor = PromptExecutor(progress_handler, lru_size=configuration.cache_lru if configuration is not None else 0) prompt_executor.raise_exceptions = True _prompt_executor.executor = prompt_executor diff --git a/comfy/cmd/folder_paths.py b/comfy/cmd/folder_paths.py index 275b00a74..cccec169f 100644 --- a/comfy/cmd/folder_paths.py +++ b/comfy/cmd/folder_paths.py @@ -5,6 +5,7 @@ import mimetypes import os import time from contextlib import nullcontext +from functools import reduce from pathlib import Path, PurePosixPath from typing import Optional, List, Literal @@ -51,46 +52,60 @@ def _resolve_path_with_compatibility(path: Path | str) -> PurePosixPath | Path: return Path(path).resolve() -def init_default_paths(folder_names_and_paths: FolderNames, configuration: Optional[Configuration] = None, create_all_directories=False): +def init_default_paths(folder_names_and_paths: FolderNames, configuration: Optional[Configuration] = None, create_all_directories=False, replace_existing=True): """ Populates the folder names and paths object with the default, upstream model directories and custom_nodes directory. :param folder_names_and_paths: the object to populate with paths :param configuration: a configuration whose base_paths and other path settings will be used to set the values on this object :param create_all_directories: create all the possible directories by calling create_directories() after the object is populated + :param replace_existing: when true, removes existing model paths objects for the built-in folder names; and, replaces the base paths :return: """ from ..cmd.main_pre import args configuration = configuration or args + base_paths = [Path(configuration.cwd) if configuration.cwd is not None else None] + configuration.base_paths - base_paths = [path for path in base_paths if path is not None] + base_paths = [Path(path) for path in base_paths if path is not None] if len(base_paths) == 0: base_paths = [Path(os.getcwd())] + base_paths = reduce(lambda uniq_list, item: uniq_list.append(item) or uniq_list if item not in uniq_list else uniq_list, base_paths, []) + if replace_existing: + folder_names_and_paths.base_paths.clear() for base_path in base_paths: folder_names_and_paths.add_base_path(base_path) - folder_names_and_paths.add(ModelPaths(["checkpoints"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["configs"], additional_absolute_directory_paths={get_package_as_path("comfy.configs")}, supported_extensions={".yaml"})) - folder_names_and_paths.add(ModelPaths(["vae"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["clip"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["loras"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(folder_names=["diffusion_models", "unet"], supported_extensions=set(supported_pt_extensions), folder_names_are_relative_directory_paths_too=True)) - folder_names_and_paths.add(ModelPaths(["clip_vision"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["style_models"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["embeddings"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["diffusers"], supported_extensions=set())) - folder_names_and_paths.add(ModelPaths(["vae_approx"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(folder_names=["controlnet", "t2i_adapter"], supported_extensions=set(supported_pt_extensions), folder_names_are_relative_directory_paths_too=True)) - folder_names_and_paths.add(ModelPaths(["gligen"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["upscale_models"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["custom_nodes"], folder_name_base_path_subdir=construct_path(""), supported_extensions=set())) - folder_names_and_paths.add(ModelPaths(["hypernetworks"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["photomaker"], supported_extensions=set(supported_pt_extensions))) - folder_names_and_paths.add(ModelPaths(["classifiers"], supported_extensions=set())) - folder_names_and_paths.add(ModelPaths(["huggingface"], supported_extensions=set())) hf_cache_paths = ModelPaths(["huggingface_cache"], supported_extensions=set()) # TODO: explore if there is a better way to do this if "HF_HUB_CACHE" in os.environ: hf_cache_paths.additional_absolute_directory_paths.add(os.environ.get("HF_HUB_CACHE")) - folder_names_and_paths.add(hf_cache_paths) + + model_paths_to_add = [ + ModelPaths(["checkpoints"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["configs"], additional_absolute_directory_paths={get_package_as_path("comfy.configs")}, supported_extensions={".yaml"}), + ModelPaths(["vae"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["clip"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["loras"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(folder_names=["diffusion_models", "unet"], supported_extensions=set(supported_pt_extensions), folder_names_are_relative_directory_paths_too=True), + ModelPaths(["clip_vision"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["style_models"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["embeddings"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["diffusers"], supported_extensions=set()), + ModelPaths(["vae_approx"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(folder_names=["controlnet", "t2i_adapter"], supported_extensions=set(supported_pt_extensions), folder_names_are_relative_directory_paths_too=True), + ModelPaths(["gligen"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["upscale_models"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["custom_nodes"], folder_name_base_path_subdir=construct_path(""), supported_extensions=set()), + ModelPaths(["hypernetworks"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["photomaker"], supported_extensions=set(supported_pt_extensions)), + ModelPaths(["classifiers"], supported_extensions=set()), + ModelPaths(["huggingface"], supported_extensions=set()), + hf_cache_paths, + ] + for model_paths in model_paths_to_add: + if replace_existing: + for folder_name in model_paths.folder_names: + del folder_names_and_paths[folder_name] + folder_names_and_paths.add(model_paths) + if create_all_directories: create_directories(folder_names_and_paths) diff --git a/comfy/cmd/folder_paths.pyi b/comfy/cmd/folder_paths.pyi index 995f869e7..dbc7471f7 100644 --- a/comfy/cmd/folder_paths.pyi +++ b/comfy/cmd/folder_paths.pyi @@ -17,11 +17,7 @@ supported_pt_extensions: set[str] # Functions -def init_default_paths( - folder_names_and_paths: FolderNames, - configuration: Optional[Configuration] = ..., - create_all_directories: bool = ... -) -> None: ... +def init_default_paths(folder_names_and_paths: FolderNames, configuration: Optional[Configuration] = None, create_all_directories: bool = ..., replace_existing: bool = ...): ... def map_legacy(folder_name: str) -> str: ... diff --git a/comfy/component_model/folder_path_types.py b/comfy/component_model/folder_path_types.py index c3d14e3d8..49cb7d55d 100644 --- a/comfy/component_model/folder_path_types.py +++ b/comfy/component_model/folder_path_types.py @@ -6,7 +6,7 @@ import os import typing import weakref from abc import ABC, abstractmethod -from pathlib import Path, PurePosixPath +from pathlib import Path from typing import Any, NamedTuple, Optional, Iterable from .platform_path import construct_path @@ -452,9 +452,16 @@ class FolderNames: def __delitem__(self, key): to_remove: list[AbstractPaths] = [] + if isinstance(key, str): + folder_names = [key] + else: + iter(key) + folder_names = key + for model_paths in self.contents: - if model_paths.has_folder_name(key): - to_remove.append(model_paths) + for folder_name in folder_names: + if model_paths.has_folder_name(folder_name): + to_remove.append(model_paths) for model_paths in to_remove: self.contents.remove(model_paths) diff --git a/tests/distributed/test_distributed_queue.py b/tests/distributed/test_distributed_queue.py index 511967189..ac5f6e528 100644 --- a/tests/distributed/test_distributed_queue.py +++ b/tests/distributed/test_distributed_queue.py @@ -1,5 +1,6 @@ import asyncio import logging + logging.basicConfig(level=logging.ERROR) import uuid @@ -22,7 +23,6 @@ from comfy.distributed.process_pool_executor import ProcessPoolExecutor from comfy.distributed.server_stub import ServerStub - def create_test_prompt() -> QueueItem: from comfy.cmd.execution import validate_prompt @@ -41,7 +41,7 @@ async def test_sign_jwt_auth_none(): assert user_token["sub"] == client_id -_executor_factories: tuple[Executor] = (ContextVarExecutor,) +_executor_factories: tuple[Executor] = (ContextVarExecutor, ProcessPoolExecutor) @pytest.mark.asyncio diff --git a/tests/issues/__init__.py b/tests/issues/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/issues/__test_25_respect_cwd_param.py b/tests/issues/__test_25_respect_cwd_param.py new file mode 100644 index 000000000..de94e6914 --- /dev/null +++ b/tests/issues/__test_25_respect_cwd_param.py @@ -0,0 +1,36 @@ +import os.path +import tempfile +from importlib.resources import files + +import pytest + +from comfy.api.components.schema.prompt import Prompt +from comfy.cli_args_types import Configuration +from comfy.client.embedded_comfy_client import EmbeddedComfyClient + +_TEST_WORKFLOW = { + "0": { + "inputs": {}, + "class_type": "TestPath", + "_meta": { + "title": "" + } + } +} + + +@pytest.mark.asyncio +async def test_respect_cwd_param(): + with tempfile.TemporaryDirectory() as tmp_dir: + cwd = str(tmp_dir) + config = Configuration(cwd=cwd) + # for finding the custom nodes + config.base_paths = [files(__package__)] + from comfy.cmd.folder_paths import models_dir + assert os.path.commonpath([os.getcwd(), models_dir]) == os.getcwd(), "at the time models_dir is accessed, the cwd should be the actual cwd, since there is no other configuration" + + client = EmbeddedComfyClient(config) + prompt = Prompt.validate(_TEST_WORKFLOW) + outputs = await client.queue_prompt_api(prompt) + path_as_imported = outputs.outputs["0"]["path"][0] + assert os.path.commonpath([path_as_imported, cwd]) == cwd, "at the time the node is imported, the cwd should be the temporary directory" diff --git a/tests/issues/custom_nodes/issue_25/__init__.py b/tests/issues/custom_nodes/issue_25/__init__.py new file mode 100644 index 000000000..e9e848c98 --- /dev/null +++ b/tests/issues/custom_nodes/issue_25/__init__.py @@ -0,0 +1,23 @@ +import os + +from comfy.cmd import folder_paths +from comfy.nodes.package_typing import CustomNode, InputTypes, FunctionReturnsUIVariables + +TEST_PATH = os.path.join(folder_paths.models_dir, "test", "path") + + +class TestPath(CustomNode): + @classmethod + def INPUT_TYPES(cls) -> InputTypes: + return {"required": {}} + + FUNCTION = "execute" + OUTPUT_NODE = True + + def execute(self) -> FunctionReturnsUIVariables: + return {"ui": {"path": [TEST_PATH]}} + + +NODE_CLASS_MAPPINGS = { + "TestPath": TestPath +}