Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 43 additions & 33 deletions comfy_cli/registry/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -228,43 +261,20 @@ 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"

# 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())
Expand Down
78 changes: 78 additions & 0 deletions tests/comfy_cli/registry/test_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Loading