From 5564db721b358099e2f5199728d1848cf4614d13 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Fri, 17 Jul 2026 00:06:53 -0700 Subject: [PATCH 1/2] fix(registry): emit pyproject hints as standalone comments for tomlkit 0.15 tomlkit 0.15.1 made `Item.comment()` validate its argument, raising ValueError("Comment cannot contain line breaks") on any `\n`. Two call sites in config_parser passed multi-line strings, so every call to `create_comfynode_config()` raised and `comfy node init` was broken at runtime, not just in tests (9 tests red on main; tomlkit was unpinned so CI resolved the newest release). Emit each hint line as a standalone `tomlkit.comment()` instead, which keeps the hints on their own lines and uncommentable in place. The classifiers hint moves into `create_comfynode_config`, the only point where it can be positioned under `license` and before the `[project.urls]` sub-table. Rendered output is byte-identical to the pre-break version apart from dropped trailing whitespace. Also pin `tomlkit>=0.13,<0.16` to stop silent upstream breakage; the fix is verified against 0.13.0, 0.13.2, 0.14.0 and 0.15.1. --- comfy_cli/registry/config_parser.py | 82 +++++++++++-------- pyproject.toml | 2 +- .../comfy_cli/registry/test_config_parser.py | 48 +++++++++++ 3 files changed, 99 insertions(+), 33 deletions(-) diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index 60fc7746..dae00a99 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,46 @@ ) +# 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. + """ + for line in hint.strip("\n").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 +96,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 +118,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 +280,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..a8f2f5a2 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,50 @@ 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"] From 7932c01018b3a7144e903bd3c18ab5e78f0eedeb Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sat, 18 Jul 2026 19:03:34 -0700 Subject: [PATCH 2/2] fix(registry): strip CRLF from pyproject hints so no stray empty # comment Under a CRLF checkout (no .gitattributes + Windows core.autocrlf=true) the hint literals contain \r\n. hint.strip("\n") left a leading \r that splitlines() turned into an empty first element, emitting a bare # comment above each hint. Use hint.strip(). Add a CRLF regression test. Addresses cursor-review (gemini-3.1-pro edge-case) on PR #536. Co-Authored-By: Claude Opus 4.8 --- comfy_cli/registry/config_parser.py | 5 ++++- tests/comfy_cli/registry/test_config_parser.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/comfy_cli/registry/config_parser.py b/comfy_cli/registry/config_parser.py index dae00a99..f0de3a69 100644 --- a/comfy_cli/registry/config_parser.py +++ b/comfy_cli/registry/config_parser.py @@ -81,7 +81,10 @@ def _add_commented_hint(table: tomlkit.items.Table, hint: str) -> None: 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. """ - for line in hint.strip("\n").splitlines(): + # `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(" "))) diff --git a/tests/comfy_cli/registry/test_config_parser.py b/tests/comfy_cli/registry/test_config_parser.py index a8f2f5a2..057289e2 100644 --- a/tests/comfy_cli/registry/test_config_parser.py +++ b/tests/comfy_cli/registry/test_config_parser.py @@ -5,6 +5,7 @@ import tomlkit from comfy_cli.registry.config_parser import ( + _add_commented_hint, _strip_url_credentials, create_comfynode_config, extract_node_configuration, @@ -1114,3 +1115,17 @@ def test_initialize_project_config_emits_uncommentable_classifiers_hint(tmp_path 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()