diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index 60fc7746..f67e6789 100644 --- a/comfy_cli/registry/config_parser.py +++ b/comfy_cli/registry/config_parser.py @@ -7,6 +7,7 @@ import tomlkit import tomlkit.exceptions import typer +from tomlkit.items import Comment, Trivia from comfy_cli import ui from comfy_cli.registry.types import ( @@ -44,6 +45,40 @@ ) +# Hint blocks the generated pyproject.toml carries as commented-out TOML for the user to +# uncomment. Each line is the comment BODY — tomlkit renders the leading "# ". They are +# emitted one comment item per line because tomlkit >= 0.15 rejects a comment containing +# line breaks (`ValueError: Comment cannot contain line breaks`). +_REQUIRES_COMFYUI_HINT = '"requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility' + +_CLASSIFIERS_HINT = """\ +classifiers = [ + # For OS-independent nodes (works on all operating systems) + "Operating System :: OS Independent", + + # OR for OS-specific nodes, specify the supported systems: + "Operating System :: Microsoft :: Windows", # Windows specific + "Operating System :: POSIX :: Linux", # Linux specific + "Operating System :: MacOS", # macOS specific + + # GPU Accelerator support. Pick the ones that are supported by your extension. + "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support + "Environment :: GPU :: AMD ROCm", # AMD ROCm support + "Environment :: GPU :: Intel Arc", # Intel Arc support + "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support + "Environment :: GPU :: Apple Metal", # Apple Metal support +]""" + + +def _append_hint(table: tomlkit.items.Table, hint: str) -> None: + """Append a hint block to `table` as one comment item per line.""" + for line in hint.split("\n"): + # Blank separators are emitted as a bare "#" so the block stays one contiguous + # comment. tomlkit.comment("") would render "# " and leave trailing whitespace + # in the file we hand the user. + table.add(tomlkit.comment(line) if line.strip() else Comment(Trivia(comment_ws="", comment="#"))) + + def create_comfynode_config(): # Create the initial structure of the TOML document document = tomlkit.document() @@ -72,9 +107,7 @@ def create_comfynode_config(): comfy["includes"] = tomlkit.array() # Add uncommentable hint for ComfyUI version compatibility, below of "[tool.comfy].includes" field. - comfy["includes"].comment(""" -# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility -""") + _append_hint(comfy, _REQUIRES_COMFYUI_HINT) tool.add("comfy", comfy) document.add("tool", tool) @@ -228,7 +261,6 @@ def initialize_project_config(): urls["Documentation"] = git_remote_url + "/wiki" urls["Bug Tracker"] = git_remote_url + "/issues" - project["urls"] = urls project["name"] = sanitize_node_name(repo_name) project["description"] = "" project["version"] = "1.0.0" @@ -236,35 +268,13 @@ def initialize_project_config(): # Use PEP 639 SPDX license identifier project["license"] = "MIT" - # [project].classifiers Classifiers uncommentable hint for OS/GPU support - # Attach classifiers comments to the project, below of "license" field. - # will generate a comment like this: - # - # [project] - # ... - # license = "MIT" - # # classifiers = [ - # # # For OS-independent nodes (works on all operating systems) - # ... - - project["license"].comment(""" -# classifiers = [ -# # For OS-independent nodes (works on all operating systems) -# "Operating System :: OS Independent", -# -# # OR for OS-specific nodes, specify the supported systems: -# "Operating System :: Microsoft :: Windows", # Windows specific -# "Operating System :: POSIX :: Linux", # Linux specific -# "Operating System :: MacOS", # macOS specific -# -# # GPU Accelerator support. Pick the ones that are supported by your extension. -# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support -# "Environment :: GPU :: AMD ROCm", # AMD ROCm support -# "Environment :: GPU :: Intel Arc", # Intel Arc support -# "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support -# "Environment :: GPU :: Apple Metal", # Apple Metal support -# ] -""") + # [project].classifiers uncommentable hint for OS/GPU support, below of "license" + # field. `urls` is removed and re-added so the hint lands between the two rather + # than after the [project.urls] sub-table, which is where an append would put it. + if "urls" in project: + del project["urls"] + _append_hint(project, _CLASSIFIERS_HINT) + project["urls"] = urls tool = document.get("tool", tomlkit.table()) comfy = tool.get("comfy", tomlkit.table()) diff --git a/tests/comfy_cli/registry/test_config_parser.py b/tests/comfy_cli/registry/test_config_parser.py index 07365d43..aceee638 100644 --- a/tests/comfy_cli/registry/test_config_parser.py +++ b/tests/comfy_cli/registry/test_config_parser.py @@ -6,6 +6,7 @@ from comfy_cli.registry.config_parser import ( _strip_url_credentials, + create_comfynode_config, extract_node_configuration, initialize_project_config, validate_and_extract_accelerator_classifiers, @@ -1066,3 +1067,80 @@ def test_initialize_project_config_vcs_with_inline_comment(tmp_path, monkeypatch data = tomlkit.parse(f.read()) deps = [str(d) for d in data["project"]["dependencies"]] assert deps == ["git+https://github.com/org/mono.git#subdirectory=pkg"] + + +def test_create_comfynode_config_emits_requires_comfyui_hint(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + create_comfynode_config() + content = (tmp_path / "pyproject.toml").read_text() + + hint = '# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility' + assert hint in content + # The hint documents [tool.comfy].includes, so it must render below that field. + assert content.index("includes = [") < content.index(hint) + tomlkit.parse(content) + + +def test_initialize_project_config_emits_classifiers_hint(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + subprocess.run( + ["git", "remote", "add", "origin", "https://github.com/user/ComfyUI-MyNode.git"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + initialize_project_config() + content = (tmp_path / "pyproject.toml").read_text() + + for line in ( + "# classifiers = [", + '# "Operating System :: OS Independent",', + '# "Environment :: GPU :: Apple Metal", # Apple Metal support', + "# ]", + ): + assert line in content + # The hint documents [project].classifiers, so it must render below "license" and + # still inside the [project] table. + assert content.index('license = "MIT"') < content.index("# classifiers = [") < content.index("[project.urls]") + tomlkit.parse(content) + + +def test_classifiers_hint_is_valid_toml_once_uncommented(tmp_path, monkeypatch): + """The hint's whole promise is that a user can uncomment it in place.""" + monkeypatch.chdir(tmp_path) + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + subprocess.run( + ["git", "remote", "add", "origin", "https://github.com/user/ComfyUI-MyNode.git"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + initialize_project_config() + content = (tmp_path / "pyproject.toml").read_text() + + uncommented = [] + for line in content.splitlines(): + if line.startswith("# classifiers = [") or (uncommented and not uncommented[-1].endswith("]")): + uncommented.append(line.removeprefix("# ").removeprefix("#")) + data = tomlkit.parse("[project]\n" + "\n".join(uncommented)) + assert "Operating System :: OS Independent" in data["project"]["classifiers"] + + +def test_generated_pyproject_has_no_trailing_whitespace(tmp_path, monkeypatch): + """Blank hint separators must render as a bare "#", not "# " — user repos commonly + run a trailing-whitespace pre-commit hook over the file we generate.""" + monkeypatch.chdir(tmp_path) + subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True) + subprocess.run( + ["git", "remote", "add", "origin", "https://github.com/user/ComfyUI-MyNode.git"], + cwd=tmp_path, + check=True, + capture_output=True, + ) + initialize_project_config() + content = (tmp_path / "pyproject.toml").read_text() + + assert [line for line in content.splitlines() if line != line.rstrip()] == [] + # The separators must still be comments, so the block stays one uncommentable unit. + assert "#\n# # OR for OS-specific nodes" in content