diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index 60fc7746..f0de3a69 100644 --- a/comfy_cli/registry/config_parser.py +++ b/comfy_cli/registry/config_parser.py @@ -6,6 +6,7 @@ import tomlkit import tomlkit.exceptions +import tomlkit.items import typer from comfy_cli import ui @@ -44,6 +45,49 @@ ) +# Uncommentable hint emitted below "[tool.comfy].includes". +_INCLUDES_HINT = """ +# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility +""" + +# Uncommentable hint emitted below "[project].license", for OS/GPU support. +_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 _add_commented_hint(table: tomlkit.items.Table, hint: str) -> None: + """Append `hint` to `table` as standalone comment lines. + + tomlkit's `Item.comment()` takes a single line by contract — it raises + `ValueError` on line breaks as of tomlkit 0.15.1 — and renders inline, which + would leave the hint impossible to uncomment in place. So emit one standalone + comment per line instead. `tomlkit.comment()` re-adds the `#` marker, hence + the strip. Hints must be added while `table` is still being built, before any + sub-table: a comment appended after one renders *inside* that sub-table. + """ + # `strip()` (not `strip("\n")`) so a CRLF checkout of this file — the repo has + # no `.gitattributes` and `core.autocrlf=true` is the Windows default — doesn't + # leave a leading `\r` that `splitlines()` would turn into a stray empty `#`. + for line in hint.strip().splitlines(): + table.add(tomlkit.comment(line.removeprefix("#").removeprefix(" "))) + + def create_comfynode_config(): # Create the initial structure of the TOML document document = tomlkit.document() @@ -55,6 +99,11 @@ def create_comfynode_config(): project["dependencies"] = tomlkit.aot() project["license"] = "MIT" + # Attach the classifiers hint below "license", before the "urls" sub-table is + # added — `initialize_project_config` reparses this file and only rewrites + # values, so this is the one place the hint can be positioned correctly. + _add_commented_hint(project, _CLASSIFIERS_HINT) + urls = tomlkit.table() urls["Repository"] = "" @@ -72,9 +121,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 -""") + _add_commented_hint(comfy, _INCLUDES_HINT) tool.add("comfy", comfy) document.add("tool", tool) @@ -236,35 +283,9 @@ 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 -# ] -""") + # The [project].classifiers hint is emitted by `create_comfynode_config` above, + # which is the only place it can be positioned below "license" (see + # `_add_commented_hint`). Reassigning "license" here preserves it. tool = document.get("tool", tomlkit.table()) comfy = tool.get("comfy", tomlkit.table()) diff --git a/pyproject.toml b/pyproject.toml index f91d096e..cfdc4f7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "rich", "ruff", "semver~=3.0.2", - "tomlkit", + "tomlkit>=0.13,<0.16", "typer>=0.12.5", "typing-extensions>=4.7", "uv>=0.11.15", diff --git a/tests/comfy_cli/registry/test_config_parser.py b/tests/comfy_cli/registry/test_config_parser.py index 07365d43..057289e2 100644 --- a/tests/comfy_cli/registry/test_config_parser.py +++ b/tests/comfy_cli/registry/test_config_parser.py @@ -5,7 +5,9 @@ import tomlkit from comfy_cli.registry.config_parser import ( + _add_commented_hint, _strip_url_credentials, + create_comfynode_config, extract_node_configuration, initialize_project_config, validate_and_extract_accelerator_classifiers, @@ -1066,3 +1068,64 @@ 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"] + + +# The `[tool.comfy]` version-compatibility hint is emitted as a standalone comment. +# tomlkit's `Item.comment()` is single-line by contract (it raises on line breaks as +# of 0.15.1) and renders inline, which would leave the hint un-uncommentable. + + +def test_create_comfynode_config_emits_uncommentable_version_hint(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + create_comfynode_config() + rendered = (tmp_path / "pyproject.toml").read_text() + + # The hint sits on its own line beneath `includes`, not trailing it inline. + assert '\nincludes = []\n# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility\n' in rendered + + # It is inert until uncommented... + assert "requires-comfyui" not in tomlkit.parse(rendered)["tool"]["comfy"] + + # ...and uncommenting it in place yields a valid key. + uncommented = rendered.replace('# "requires-comfyui"', '"requires-comfyui"', 1) + assert tomlkit.parse(uncommented)["tool"]["comfy"]["requires-comfyui"] == ">=1.0.0" + + +def test_initialize_project_config_emits_uncommentable_classifiers_hint(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + _init_git_repo_with_reqs(tmp_path, "") + initialize_project_config() + rendered = (tmp_path / "pyproject.toml").read_text() + + # The hint sits under `license`, and critically *before* the `[project.urls]` + # sub-table — appending it afterwards would scope `classifiers` to the wrong table. + assert '\nlicense = "MIT"\n# classifiers = [\n' in rendered + assert rendered.index("# classifiers = [") < rendered.index("[project.urls]") + assert '# "Operating System :: OS Independent",\n' in rendered + assert "\n# ]\n" in rendered + + # It is inert until uncommented... + assert "classifiers" not in tomlkit.parse(rendered)["project"] + + # ...and uncommenting the block in place yields a valid `[project].classifiers`. + hint_start = rendered.index("# classifiers = [") + hint_end = rendered.index("# ]", hint_start) + len("# ]") + block = rendered[hint_start:hint_end] + uncommented = "\n".join(line[1:].lstrip(" ") if line.strip() != "#" else "" for line in block.splitlines()) + parsed = tomlkit.parse(rendered[:hint_start] + uncommented + rendered[hint_end:]) + assert "Operating System :: OS Independent" in parsed["project"]["classifiers"] + assert "Environment :: GPU :: Apple Metal" in parsed["project"]["classifiers"] + + +def test_add_commented_hint_tolerates_crlf_source(): + # If this source file is checked out with CRLF endings (no `.gitattributes` + + # Windows `core.autocrlf=true`), the hint literals contain `\r\n`. A leading + # `\r` surviving into `splitlines()` would emit a stray empty `#` comment above + # the hint. Simulate that by feeding a CRLF hint and asserting no bare `#` line. + table = tomlkit.table() + _add_commented_hint(table, '\r\n# "requires-comfyui" = ">=1.0.0" # note\r\n') + rendered = tomlkit.document() + rendered["tool"] = table + lines = tomlkit.dumps(rendered).splitlines() + assert "#" not in [line.strip() for line in lines] + assert '# "requires-comfyui" = ">=1.0.0" # note' in rendered.as_string()