From 401d252a42160ed95ea0b7d657301c29da5580f5 Mon Sep 17 00:00:00 2001 From: coderfromthenorth93 Date: Fri, 6 Jun 2025 20:15:43 -0400 Subject: [PATCH 1/4] Parse the supporting metadata and send to backend while publishing a node --- comfy_cli/registry/api.py | 8 +++ comfy_cli/registry/config_parser.py | 97 ++++++++++++++++++++++++++++- comfy_cli/registry/types.py | 5 +- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/comfy_cli/registry/api.py b/comfy_cli/registry/api.py index fe0375dd..f7535833 100644 --- a/comfy_cli/registry/api.py +++ b/comfy_cli/registry/api.py @@ -54,10 +54,18 @@ def publish_node_version(self, node_config: PyProjectConfig, token) -> PublishNo "name": node_config.tool_comfy.display_name, "license": license_json, "repository": node_config.project.urls.repository, + "supported_os": node_config.project.supported_os, + "supported_accelerators": node_config.project.supported_accelerators, + "supported_comfyui_version": node_config.project.supported_comfyui_version, + "supported_comfyui_frontend_version": node_config.project.supported_comfyui_frontend_version, }, "node_version": { "version": node_config.project.version, "dependencies": node_config.project.dependencies, + "supported_os": node_config.project.supported_os, + "supported_accelerators": node_config.project.supported_accelerators, + "supported_comfyui_version": node_config.project.supported_comfyui_version, + "supported_comfyui_frontend_version": node_config.project.supported_comfyui_frontend_version, }, } print(request_body) diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index 1a243943..981de7d3 100644 --- a/comfy_cli/registry/config_parser.py +++ b/comfy_cli/registry/config_parser.py @@ -1,4 +1,5 @@ import os +import re import subprocess from typing import Optional @@ -87,6 +88,76 @@ def sanitize_node_name(name: str) -> str: return name +def validate_and_extract_os_classifiers(classifiers: list) -> list: + os_classifiers = [c for c in classifiers if c.startswith("Operating System :: ")] + if not os_classifiers: + return [] + + os_values = [c[len("Operating System :: "):] for c in os_classifiers] + valid_os_prefixes = {"Microsoft", "POSIX", "MacOS", "OS Independent"} + + for os_value in os_values: + if not any(os_value.startswith(prefix) for prefix in valid_os_prefixes): + typer.echo( + 'Warning: Invalid Operating System classifier found. Operating System classifiers must start with one of: "Microsoft", "POSIX", "MacOS", "OS Independent". ' + 'Examples: "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: OS Independent". ' + 'No OS information will be populated.' + ) + return [] + + return os_values + + +def validate_and_extract_accelerator_classifiers(classifiers: list) -> list: + accelerator_classifiers = [c for c in classifiers if c.startswith("Environment ::")] + if not accelerator_classifiers: + return [] + + accelerator_values = [c[len("Environment :: "):] for c in accelerator_classifiers] + + valid_accelerators = { + "GPU :: NVIDIA CUDA", + "GPU :: AMD ROCm", + "GPU :: Intel Arc", + "NPU :: Huawei Ascend", + "GPU :: Apple Metal", + } + + for accelerator_value in accelerator_values: + if accelerator_value not in valid_accelerators: + typer.echo( + 'Warning: Invalid Environment classifier found. Environment classifiers must be one of: ' + '"Environment :: GPU :: NVIDIA CUDA", "Environment :: GPU :: AMD ROCm", "Environment :: GPU :: Intel Arc", ' + '"Environment :: NPU :: Huawei Ascend", "Environment :: GPU :: Apple Metal". ' + 'No accelerator information will be populated.' + ) + return [] + + return accelerator_values + + +def validate_version(version: str, field_name: str) -> str: + if not version: + return version + + version_pattern = r'^(?:(==|>=|<=|!=|~=|>|<|<>|=)\s*)?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+)?)?$' + + version_parts = [part.strip() for part in version.split(',')] + for part in version_parts: + if not re.match(version_pattern, part): + typer.echo( + f'Warning: Invalid {field_name} format: "{version}". ' + f'Each version part must follow the pattern: [operator][version] where operator is optional (==, >=, <=, !=, ~=, >, <, <>, =) ' + f'and version is in format major.minor.patch[-suffix]. ' + f'Multiple versions can be comma-separated. ' + f'Examples: ">=1.0.0", "==2.1.0-beta", "1.5.2", ">=1.0.0,<2.0.0". ' + f'No {field_name} will be populated.' + ) + return "" + + return version + + def initialize_project_config(): create_comfynode_config() @@ -157,6 +228,26 @@ def extract_node_configuration( urls_data = project_data.get("urls", {}) comfy_data = data.get("tool", {}).get("comfy", {}) + dependencies = project_data.get("dependencies", []) + supported_comfyui_frontend_version = "" + for dep in dependencies: + if isinstance(dep, str) and dep.startswith("comfyui-frontend-package"): + supported_comfyui_frontend_version = dep.removeprefix("comfyui-frontend-package") + break + + # Remove the ComfyUI-frontend dependency from the dependencies list + dependencies = [ + dep for dep in dependencies if not (isinstance(dep, str) and dep.startswith("comfyui-frontend-package")) + ] + + supported_comfyui_version = data.get("tool", {}).get("requires-comfyui", "") + + classifiers = project_data.get("classifiers", []) + supported_os = validate_and_extract_os_classifiers(classifiers) + supported_accelerators = validate_and_extract_accelerator_classifiers(classifiers) + supported_comfyui_version = validate_version(supported_comfyui_version, "requires-comfyui") + supported_comfyui_frontend_version = validate_version(supported_comfyui_frontend_version, "comfyui-frontend-package") + license_data = project_data.get("license", {}) if isinstance(license_data, str): license = License(text=license_data) @@ -182,7 +273,7 @@ def extract_node_configuration( description=project_data.get("description", ""), version=project_data.get("version", ""), requires_python=project_data.get("requires-python", ""), - dependencies=project_data.get("dependencies", []), + dependencies=dependencies, license=license, urls=URLs( homepage=urls_data.get("Homepage", ""), @@ -190,6 +281,10 @@ def extract_node_configuration( repository=urls_data.get("Repository", ""), issues=urls_data.get("Issues", ""), ), + supported_os=supported_os, + supported_accelerators=supported_accelerators, + supported_comfyui_version=supported_comfyui_version, + supported_comfyui_frontend_version=supported_comfyui_frontend_version, ) comfy = ComfyConfig( diff --git a/comfy_cli/registry/types.py b/comfy_cli/registry/types.py index b3f94b2b..0077cc2b 100644 --- a/comfy_cli/registry/types.py +++ b/comfy_cli/registry/types.py @@ -69,7 +69,10 @@ class ProjectConfig: dependencies: List[str] = field(default_factory=list) license: License = field(default_factory=License) urls: URLs = field(default_factory=URLs) - + supported_os: List[str] = field(default_factory=list) + supported_accelerators: List[str] = field(default_factory=list) + supported_comfyui_version: str = "" + supported_comfyui_frontend_version: str = "" @dataclass class PyProjectConfig: From 5715763d983db7ab49362de5713e26201262f564 Mon Sep 17 00:00:00 2001 From: coderfromthenorth93 Date: Fri, 6 Jun 2025 20:27:07 -0400 Subject: [PATCH 2/4] Add/update tests --- .../comfy_cli/registry/test_config_parser.py | 193 +++++++++++++++++- 1 file changed, 192 insertions(+), 1 deletion(-) diff --git a/tests/comfy_cli/registry/test_config_parser.py b/tests/comfy_cli/registry/test_config_parser.py index fdebeaef..86b14ff5 100644 --- a/tests/comfy_cli/registry/test_config_parser.py +++ b/tests/comfy_cli/registry/test_config_parser.py @@ -2,7 +2,12 @@ import pytest -from comfy_cli.registry.config_parser import extract_node_configuration +from comfy_cli.registry.config_parser import ( + extract_node_configuration, + validate_and_extract_accelerator_classifiers, + validate_and_extract_os_classifiers, + validate_version, +) from comfy_cli.registry.types import ( License, Model, @@ -127,3 +132,189 @@ def test_extract_license_incorrect_format(): assert result is not None, "Expected PyProjectConfig, got None" assert isinstance(result, PyProjectConfig) assert result.project.license == License(text="MIT") + +def test_extract_node_configuration_with_os_classifiers(): + mock_data = { + "project": { + "classifiers": [ + "Operating System :: OS Independent", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Topic :: Software Development", + ] + } + } + with ( + patch("os.path.isfile", return_value=True), + patch("builtins.open", mock_open()), + patch("tomlkit.load", return_value=mock_data), + ): + result = extract_node_configuration("fake_path.toml") + + assert result is not None + assert len(result.project.supported_os) == 2 + assert "OS Independent" in result.project.supported_os + assert "Microsoft :: Windows" in result.project.supported_os + + +def test_extract_node_configuration_with_accelerator_classifiers(): + mock_data = { + "project": { + "classifiers": [ + "Environment :: GPU :: NVIDIA CUDA", + "Environment :: GPU :: AMD ROCm", + "Environment :: GPU :: Intel Arc", + "Environment :: NPU :: Huawei Ascend", + "Environment :: GPU :: Apple Metal", + "Programming Language :: Python :: 3", + "Topic :: Software Development", + ] + } + } + with ( + patch("os.path.isfile", return_value=True), + patch("builtins.open", mock_open()), + patch("tomlkit.load", return_value=mock_data), + ): + result = extract_node_configuration("fake_path.toml") + + assert result is not None + assert len(result.project.supported_accelerators) == 5 + assert "GPU :: NVIDIA CUDA" in result.project.supported_accelerators + assert "GPU :: AMD ROCm" in result.project.supported_accelerators + assert "GPU :: Intel Arc" in result.project.supported_accelerators + assert "NPU :: Huawei Ascend" in result.project.supported_accelerators + assert "GPU :: Apple Metal" in result.project.supported_accelerators + + +def test_extract_node_configuration_with_comfyui_version(): + mock_data = {"project": {"dependencies": ["packge1>=2.0.0", "comfyui-frontend-package>=1.2.3", "package2>=1.0.0"]}} + with ( + patch("os.path.isfile", return_value=True), + patch("builtins.open", mock_open()), + patch("tomlkit.load", return_value=mock_data), + ): + result = extract_node_configuration("fake_path.toml") + + assert result is not None + assert result.project.supported_comfyui_frontend_version == ">=1.2.3" + assert len(result.project.dependencies) == 2 + assert "comfyui-frontend-package>=1.2.3" not in result.project.dependencies + assert "packge1>=2.0.0" in result.project.dependencies + assert "package2>=1.0.0" in result.project.dependencies + + +def test_extract_node_configuration_with_requires_comfyui(): + mock_data = {"project": {}, "tool": {"requires-comfyui": "2.0.0"}} + with ( + patch("os.path.isfile", return_value=True), + patch("builtins.open", mock_open()), + patch("tomlkit.load", return_value=mock_data), + ): + result = extract_node_configuration("fake_path.toml") + + assert result is not None + assert result.project.supported_comfyui_version == "2.0.0" + +def test_validate_and_extract_os_classifiers_valid(): + """Test OS validation with valid classifiers.""" + classifiers = [ + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + ] + result = validate_and_extract_os_classifiers(classifiers) + expected = ["Microsoft :: Windows", "POSIX :: Linux", "MacOS", "OS Independent"] + assert result == expected + +@patch('typer.echo') +def test_validate_and_extract_os_classifiers_invalid(mock_echo): + """Test OS validation with invalid classifiers.""" + classifiers = [ + "Operating System :: Microsoft :: Windows", + "Operating System :: Linux", # Invalid - should be "POSIX :: Linux" + "Programming Language :: Python :: 3", + ] + result = validate_and_extract_os_classifiers(classifiers) + assert result == [] + mock_echo.assert_called_once() + assert "Invalid Operating System classifier found" in mock_echo.call_args[0][0] + + +def test_validate_and_extract_accelerator_classifiers_valid(): + """Test accelerator validation with valid classifiers.""" + classifiers = [ + "Environment :: GPU :: NVIDIA CUDA", + "Environment :: GPU :: AMD ROCm", + "Environment :: GPU :: Intel Arc", + "Environment :: NPU :: Huawei Ascend", + "Environment :: GPU :: Apple Metal", + "Programming Language :: Python :: 3", + ] + result = validate_and_extract_accelerator_classifiers(classifiers) + expected = ["GPU :: NVIDIA CUDA", "GPU :: AMD ROCm", "GPU :: Intel Arc", "NPU :: Huawei Ascend", "GPU :: Apple Metal"] + assert result == expected + + +@patch('typer.echo') +def test_validate_and_extract_accelerator_classifiers_invalid(mock_echo): + """Test accelerator validation with invalid classifiers.""" + classifiers = [ + "Environment :: GPU :: NVIDIA CUDA", + "Environment :: GPU :: Invalid GPU", # Invalid + "Programming Language :: Python :: 3", + ] + result = validate_and_extract_accelerator_classifiers(classifiers) + assert result == [] + mock_echo.assert_called_once() + assert "Invalid Environment classifier found" in mock_echo.call_args[0][0] + + +def test_validate_version_valid(): + """Test version validation with valid versions.""" + valid_versions = [ + "1.1.1", + ">=1.0.0", + "==2.1.0-beta", + "1.5.2", + "~=3.0.0", + "!=1.2.3", + ">2.0.0", + "<3.0.0", + "<=4.0.0", + "<>1.0.0", + "=1.0.0", + "1.0.0-alpha1", + ">=1.0.0,<2.0.0", + "==1.2.3,!=1.2.4", + ">=1.0.0,<=2.0.0,!=1.5.0", + "1.0.0,2.0.0", + ">1.0.0,<2.0.0,!=1.5.0-beta", + ] + + for version in valid_versions: + result = validate_version(version, "test_field") + assert result == version, f"Version {version} should be valid" + + +@patch('typer.echo') +def test_validate_version_invalid(mock_echo): + """Test version validation with invalid versions.""" + invalid_versions = [ + "1.0", # Missing patch version + ">=abc", # Invalid version format + "invalid-version", # Completely invalid + "1.0.0.0", # Too many version parts + ">>1.0.0", # Invalid operator + ">=1.0.0,invalid", + "1.0,2.0.0", + ">=1.0.0,>=abc", + ] + + for version in invalid_versions: + result = validate_version(version, "test_field") + assert result == "", f"Version {version} should be invalid" + + assert mock_echo.call_count == len(invalid_versions) From f7acab82fbac26c79b80a31b8f63c3b06e57a155 Mon Sep 17 00:00:00 2001 From: coderfromthenorth93 Date: Fri, 6 Jun 2025 20:37:16 -0400 Subject: [PATCH 3/4] fix ruff formatting --- comfy_cli/registry/config_parser.py | 38 ++++++++++--------- comfy_cli/registry/types.py | 1 + .../comfy_cli/registry/test_config_parser.py | 23 +++++++---- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index 981de7d3..e7f3dc39 100644 --- a/comfy_cli/registry/config_parser.py +++ b/comfy_cli/registry/config_parser.py @@ -93,32 +93,32 @@ def validate_and_extract_os_classifiers(classifiers: list) -> list: if not os_classifiers: return [] - os_values = [c[len("Operating System :: "):] for c in os_classifiers] + os_values = [c[len("Operating System :: ") :] for c in os_classifiers] valid_os_prefixes = {"Microsoft", "POSIX", "MacOS", "OS Independent"} - + for os_value in os_values: if not any(os_value.startswith(prefix) for prefix in valid_os_prefixes): typer.echo( 'Warning: Invalid Operating System classifier found. Operating System classifiers must start with one of: "Microsoft", "POSIX", "MacOS", "OS Independent". ' 'Examples: "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: OS Independent". ' - 'No OS information will be populated.' + "No OS information will be populated." ) return [] return os_values -def validate_and_extract_accelerator_classifiers(classifiers: list) -> list: +def validate_and_extract_accelerator_classifiers(classifiers: list) -> list: accelerator_classifiers = [c for c in classifiers if c.startswith("Environment ::")] if not accelerator_classifiers: return [] - - accelerator_values = [c[len("Environment :: "):] for c in accelerator_classifiers] - + + accelerator_values = [c[len("Environment :: ") :] for c in accelerator_classifiers] + valid_accelerators = { "GPU :: NVIDIA CUDA", "GPU :: AMD ROCm", - "GPU :: Intel Arc", + "GPU :: Intel Arc", "NPU :: Huawei Ascend", "GPU :: Apple Metal", } @@ -126,10 +126,10 @@ def validate_and_extract_accelerator_classifiers(classifiers: list) -> list: for accelerator_value in accelerator_values: if accelerator_value not in valid_accelerators: typer.echo( - 'Warning: Invalid Environment classifier found. Environment classifiers must be one of: ' + "Warning: Invalid Environment classifier found. Environment classifiers must be one of: " '"Environment :: GPU :: NVIDIA CUDA", "Environment :: GPU :: AMD ROCm", "Environment :: GPU :: Intel Arc", ' '"Environment :: NPU :: Huawei Ascend", "Environment :: GPU :: Apple Metal". ' - 'No accelerator information will be populated.' + "No accelerator information will be populated." ) return [] @@ -140,21 +140,21 @@ def validate_version(version: str, field_name: str) -> str: if not version: return version - version_pattern = r'^(?:(==|>=|<=|!=|~=|>|<|<>|=)\s*)?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+)?)?$' + version_pattern = r"^(?:(==|>=|<=|!=|~=|>|<|<>|=)\s*)?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+)?)?$" - version_parts = [part.strip() for part in version.split(',')] + version_parts = [part.strip() for part in version.split(",")] for part in version_parts: if not re.match(version_pattern, part): typer.echo( f'Warning: Invalid {field_name} format: "{version}". ' - f'Each version part must follow the pattern: [operator][version] where operator is optional (==, >=, <=, !=, ~=, >, <, <>, =) ' - f'and version is in format major.minor.patch[-suffix]. ' - f'Multiple versions can be comma-separated. ' + f"Each version part must follow the pattern: [operator][version] where operator is optional (==, >=, <=, !=, ~=, >, <, <>, =) " + f"and version is in format major.minor.patch[-suffix]. " + f"Multiple versions can be comma-separated. " f'Examples: ">=1.0.0", "==2.1.0-beta", "1.5.2", ">=1.0.0,<2.0.0". ' - f'No {field_name} will be populated.' + f"No {field_name} will be populated." ) return "" - + return version @@ -246,7 +246,9 @@ def extract_node_configuration( supported_os = validate_and_extract_os_classifiers(classifiers) supported_accelerators = validate_and_extract_accelerator_classifiers(classifiers) supported_comfyui_version = validate_version(supported_comfyui_version, "requires-comfyui") - supported_comfyui_frontend_version = validate_version(supported_comfyui_frontend_version, "comfyui-frontend-package") + supported_comfyui_frontend_version = validate_version( + supported_comfyui_frontend_version, "comfyui-frontend-package" + ) license_data = project_data.get("license", {}) if isinstance(license_data, str): diff --git a/comfy_cli/registry/types.py b/comfy_cli/registry/types.py index 0077cc2b..980a42ab 100644 --- a/comfy_cli/registry/types.py +++ b/comfy_cli/registry/types.py @@ -74,6 +74,7 @@ class ProjectConfig: supported_comfyui_version: str = "" supported_comfyui_frontend_version: str = "" + @dataclass class PyProjectConfig: project: ProjectConfig = field(default_factory=ProjectConfig) diff --git a/tests/comfy_cli/registry/test_config_parser.py b/tests/comfy_cli/registry/test_config_parser.py index 86b14ff5..739b7a82 100644 --- a/tests/comfy_cli/registry/test_config_parser.py +++ b/tests/comfy_cli/registry/test_config_parser.py @@ -133,6 +133,7 @@ def test_extract_license_incorrect_format(): assert isinstance(result, PyProjectConfig) assert result.project.license == License(text="MIT") + def test_extract_node_configuration_with_os_classifiers(): mock_data = { "project": { @@ -216,6 +217,7 @@ def test_extract_node_configuration_with_requires_comfyui(): assert result is not None assert result.project.supported_comfyui_version == "2.0.0" + def test_validate_and_extract_os_classifiers_valid(): """Test OS validation with valid classifiers.""" classifiers = [ @@ -229,7 +231,8 @@ def test_validate_and_extract_os_classifiers_valid(): expected = ["Microsoft :: Windows", "POSIX :: Linux", "MacOS", "OS Independent"] assert result == expected -@patch('typer.echo') + +@patch("typer.echo") def test_validate_and_extract_os_classifiers_invalid(mock_echo): """Test OS validation with invalid classifiers.""" classifiers = [ @@ -254,11 +257,17 @@ def test_validate_and_extract_accelerator_classifiers_valid(): "Programming Language :: Python :: 3", ] result = validate_and_extract_accelerator_classifiers(classifiers) - expected = ["GPU :: NVIDIA CUDA", "GPU :: AMD ROCm", "GPU :: Intel Arc", "NPU :: Huawei Ascend", "GPU :: Apple Metal"] + expected = [ + "GPU :: NVIDIA CUDA", + "GPU :: AMD ROCm", + "GPU :: Intel Arc", + "NPU :: Huawei Ascend", + "GPU :: Apple Metal", + ] assert result == expected -@patch('typer.echo') +@patch("typer.echo") def test_validate_and_extract_accelerator_classifiers_invalid(mock_echo): """Test accelerator validation with invalid classifiers.""" classifiers = [ @@ -293,13 +302,13 @@ def test_validate_version_valid(): "1.0.0,2.0.0", ">1.0.0,<2.0.0,!=1.5.0-beta", ] - + for version in valid_versions: result = validate_version(version, "test_field") assert result == version, f"Version {version} should be valid" -@patch('typer.echo') +@patch("typer.echo") def test_validate_version_invalid(mock_echo): """Test version validation with invalid versions.""" invalid_versions = [ @@ -312,9 +321,9 @@ def test_validate_version_invalid(mock_echo): "1.0,2.0.0", ">=1.0.0,>=abc", ] - + for version in invalid_versions: result = validate_version(version, "test_field") assert result == "", f"Version {version} should be invalid" - + assert mock_echo.call_count == len(invalid_versions) From c4370bcd7733feded2fca57d4a6c95b51b1e3508 Mon Sep 17 00:00:00 2001 From: coderfromthenorth93 Date: Sat, 7 Jun 2025 21:54:38 -0400 Subject: [PATCH 4/4] fix bug --- comfy_cli/registry/config_parser.py | 2 +- tests/comfy_cli/registry/test_config_parser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index e7f3dc39..4d93d7bc 100644 --- a/comfy_cli/registry/config_parser.py +++ b/comfy_cli/registry/config_parser.py @@ -240,7 +240,7 @@ def extract_node_configuration( dep for dep in dependencies if not (isinstance(dep, str) and dep.startswith("comfyui-frontend-package")) ] - supported_comfyui_version = data.get("tool", {}).get("requires-comfyui", "") + supported_comfyui_version = data.get("tool", {}).get("comfy", {}).get("requires-comfyui", "") classifiers = project_data.get("classifiers", []) supported_os = validate_and_extract_os_classifiers(classifiers) diff --git a/tests/comfy_cli/registry/test_config_parser.py b/tests/comfy_cli/registry/test_config_parser.py index 739b7a82..bf54312a 100644 --- a/tests/comfy_cli/registry/test_config_parser.py +++ b/tests/comfy_cli/registry/test_config_parser.py @@ -206,7 +206,7 @@ def test_extract_node_configuration_with_comfyui_version(): def test_extract_node_configuration_with_requires_comfyui(): - mock_data = {"project": {}, "tool": {"requires-comfyui": "2.0.0"}} + mock_data = {"project": {}, "tool": {"comfy": {"requires-comfyui": "2.0.0"}}} with ( patch("os.path.isfile", return_value=True), patch("builtins.open", mock_open()),