diff --git a/nodes.py b/nodes.py index e1e8eeb64..0775a78f2 100644 --- a/nodes.py +++ b/nodes.py @@ -2202,12 +2202,45 @@ def _node_source_from_parent(module_parent: str) -> str: NODE_STARTUP_ERRORS: dict[str, dict] = {} +def _read_pyproject_metadata(module_path: str) -> dict | None: + """Best-effort extraction of node-pack identity from pyproject.toml. + + Returns a dict with the Comfy Registry-style identity (pack_id, + display_name, publisher_id, version, repository) when the module + directory contains a pyproject.toml. Returns None when no toml is + present or parsing fails for any reason — startup-error tracking + must never itself raise. + """ + if not module_path or not os.path.isdir(module_path): + return None + toml_path = os.path.join(module_path, "pyproject.toml") + if not os.path.isfile(toml_path): + return None + try: + from comfy_config import config_parser + + cfg = config_parser.extract_node_configuration(module_path) + if cfg is None: + return None + meta = { + "pack_id": cfg.project.name or None, + "display_name": cfg.tool_comfy.display_name or None, + "publisher_id": cfg.tool_comfy.publisher_id or None, + "version": cfg.project.version or None, + "repository": cfg.project.urls.repository or None, + } + # Drop empty fields so the API payload stays compact. + return {k: v for k, v in meta.items() if v} + except Exception: + return None + + def record_node_startup_error( *, module_path: str, source: str, phase: str, error: BaseException, tb: str ) -> None: """Record a startup error for a node module so it can be exposed via the API.""" module_name = get_module_name(module_path) - NODE_STARTUP_ERRORS[f"{source}:{module_name}"] = { + entry = { "source": source, "module_name": module_name, "module_path": module_path, @@ -2215,6 +2248,10 @@ def record_node_startup_error( "traceback": tb, "phase": phase, } + pyproject = _read_pyproject_metadata(module_path) + if pyproject: + entry["pyproject"] = pyproject + NODE_STARTUP_ERRORS[f"{source}:{module_name}"] = entry def get_module_name(module_path: str) -> str: diff --git a/tests-unit/node_startup_errors_test.py b/tests-unit/node_startup_errors_test.py index 2a86b7653..533ba78e7 100644 --- a/tests-unit/node_startup_errors_test.py +++ b/tests-unit/node_startup_errors_test.py @@ -97,6 +97,47 @@ async def test_load_custom_node_collision_across_sources(tmp_path): ) +@pytest.mark.asyncio +async def test_load_custom_node_attaches_pyproject_metadata(tmp_path): + pack_dir = tmp_path / "MyCoolPack" + pack_dir.mkdir() + (pack_dir / "__init__.py").write_text("raise RuntimeError('boom')\n") + (pack_dir / "pyproject.toml").write_text(textwrap.dedent("""\ + [project] + name = "comfyui-mycoolpack" + version = "1.2.3" + + [project.urls] + Repository = "https://github.com/example/comfyui-mycoolpack" + + [tool.comfy] + PublisherId = "example" + DisplayName = "My Cool Pack" + """)) + + success = await nodes.load_custom_node(str(pack_dir), module_parent="custom_nodes") + assert success is False + + entry = nodes.NODE_STARTUP_ERRORS["custom_node:MyCoolPack"] + assert "pyproject" in entry, entry + py = entry["pyproject"] + assert py["pack_id"] == "comfyui-mycoolpack" + assert py["display_name"] == "My Cool Pack" + assert py["publisher_id"] == "example" + assert py["version"] == "1.2.3" + assert py["repository"] == "https://github.com/example/comfyui-mycoolpack" + + +@pytest.mark.asyncio +async def test_load_custom_node_no_pyproject_skips_metadata(tmp_path): + # Single-file extras-style module: no pyproject.toml exists alongside it, + # so the entry must not contain a 'pyproject' key. + module_path = _write_broken_module(tmp_path, "lonely") + assert await nodes.load_custom_node(module_path, module_parent="comfy_extras") is False + entry = nodes.NODE_STARTUP_ERRORS["comfy_extra:lonely"] + assert "pyproject" not in entry + + def test_unknown_module_parent_defaults_to_custom_node(): assert nodes._node_source_from_parent("custom_nodes") == "custom_node" assert nodes._node_source_from_parent("comfy_extras") == "comfy_extra"