Skip to content

Commit 47d55b8

Browse files
authored
add support to read pyproject.toml from custom node (#8357)
* add support to read pyproject.toml from custom node * sf * use pydantic instead * sf * use pydantic_settings * remove unnecessary try/catch and handle single-file python node * sf
1 parent 310f4b6 commit 47d55b8

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

comfy_config/config_parser.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import os
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
from pydantic_settings import PydanticBaseSettingsSource, TomlConfigSettingsSource
6+
7+
from comfy_config.types import (
8+
ComfyConfig,
9+
ProjectConfig,
10+
PyProjectConfig,
11+
PyProjectSettings
12+
)
13+
14+
"""
15+
Extract configuration from a custom node directory's pyproject.toml file or a Python file.
16+
17+
This function reads and parses the pyproject.toml file in the specified directory
18+
to extract project and ComfyUI-specific configuration information. If no
19+
pyproject.toml file is found, it creates a minimal configuration using the
20+
folder name as the project name. If a Python file is provided, it uses the
21+
file name (without extension) as the project name.
22+
23+
Args:
24+
path (str): Path to the directory containing the pyproject.toml file, or
25+
path to a .py file. If pyproject.toml doesn't exist in a directory,
26+
the folder name will be used as the default project name. If a .py
27+
file is provided, the filename (without .py extension) will be used
28+
as the project name.
29+
30+
Returns:
31+
Optional[PyProjectConfig]: A PyProjectConfig object containing:
32+
- project: Basic project information (name, version, dependencies, etc.)
33+
- tool_comfy: ComfyUI-specific configuration (publisher_id, models, etc.)
34+
Returns None if configuration extraction fails or if the provided file
35+
is not a Python file.
36+
37+
Notes:
38+
- If pyproject.toml is missing in a directory, creates a default config with folder name
39+
- If a .py file is provided, creates a default config with filename (without extension)
40+
- Returns None for non-Python files
41+
42+
Example:
43+
>>> from comfy_config import config_parser
44+
>>> # For directory
45+
>>> custom_node_dir = os.path.dirname(os.path.realpath(__file__))
46+
>>> project_config = config_parser.extract_node_configuration(custom_node_dir)
47+
>>> print(project_config.project.name) # "my_custom_node" or name from pyproject.toml
48+
>>>
49+
>>> # For single-file Python node file
50+
>>> py_file_path = os.path.realpath(__file__) # "/path/to/my_node.py"
51+
>>> project_config = config_parser.extract_node_configuration(py_file_path)
52+
>>> print(project_config.project.name) # "my_node"
53+
"""
54+
def extract_node_configuration(path) -> Optional[PyProjectConfig]:
55+
if os.path.isfile(path):
56+
file_path = Path(path)
57+
58+
if file_path.suffix.lower() != '.py':
59+
return None
60+
61+
project_name = file_path.stem
62+
project = ProjectConfig(name=project_name)
63+
comfy = ComfyConfig()
64+
return PyProjectConfig(project=project, tool_comfy=comfy)
65+
66+
folder_name = os.path.basename(path)
67+
toml_path = Path(path) / "pyproject.toml"
68+
69+
if not toml_path.exists():
70+
project = ProjectConfig(name=folder_name)
71+
comfy = ComfyConfig()
72+
return PyProjectConfig(project=project, tool_comfy=comfy)
73+
74+
raw_settings = load_pyproject_settings(toml_path)
75+
76+
project_data = raw_settings.project
77+
78+
tool_data = raw_settings.tool
79+
comfy_data = tool_data.get("comfy", {}) if tool_data else {}
80+
81+
return PyProjectConfig(project=project_data, tool_comfy=comfy_data)
82+
83+
84+
def load_pyproject_settings(toml_path: Path) -> PyProjectSettings:
85+
class PyProjectLoader(PyProjectSettings):
86+
@classmethod
87+
def settings_customise_sources(
88+
cls,
89+
settings_cls,
90+
init_settings: PydanticBaseSettingsSource,
91+
env_settings: PydanticBaseSettingsSource,
92+
dotenv_settings: PydanticBaseSettingsSource,
93+
file_secret_settings: PydanticBaseSettingsSource,
94+
):
95+
return (TomlConfigSettingsSource(settings_cls, toml_path),)
96+
97+
return PyProjectLoader()

comfy_config/types.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from pydantic import BaseModel, Field
2+
from pydantic_settings import BaseSettings, SettingsConfigDict
3+
from typing import List, Optional
4+
5+
# IMPORTANT: The type definitions specified in pyproject.toml for custom nodes
6+
# must remain synchronized with the corresponding files in the https://github.com/Comfy-Org/comfy-cli/blob/main/comfy_cli/registry/types.py.
7+
# Any changes to one must be reflected in the other to maintain consistency.
8+
9+
class NodeVersion(BaseModel):
10+
changelog: str
11+
dependencies: List[str]
12+
deprecated: bool
13+
id: str
14+
version: str
15+
download_url: str
16+
17+
18+
class Node(BaseModel):
19+
id: str
20+
name: str
21+
description: str
22+
author: Optional[str] = None
23+
license: Optional[str] = None
24+
icon: Optional[str] = None
25+
repository: Optional[str] = None
26+
tags: List[str] = Field(default_factory=list)
27+
latest_version: Optional[NodeVersion] = None
28+
29+
30+
class PublishNodeVersionResponse(BaseModel):
31+
node_version: NodeVersion
32+
signedUrl: str
33+
34+
35+
class URLs(BaseModel):
36+
homepage: str = Field(default="", alias="Homepage")
37+
documentation: str = Field(default="", alias="Documentation")
38+
repository: str = Field(default="", alias="Repository")
39+
issues: str = Field(default="", alias="Issues")
40+
41+
42+
class Model(BaseModel):
43+
location: str
44+
model_url: str
45+
46+
47+
class ComfyConfig(BaseModel):
48+
publisher_id: str = Field(default="", alias="PublisherId")
49+
display_name: str = Field(default="", alias="DisplayName")
50+
icon: str = Field(default="", alias="Icon")
51+
models: List[Model] = Field(default_factory=list, alias="Models")
52+
includes: List[str] = Field(default_factory=list)
53+
54+
55+
class License(BaseModel):
56+
file: str = ""
57+
text: str = ""
58+
59+
60+
class ProjectConfig(BaseModel):
61+
name: str = ""
62+
description: str = ""
63+
version: str = "1.0.0"
64+
requires_python: str = Field(default=">= 3.9", alias="requires-python")
65+
dependencies: List[str] = Field(default_factory=list)
66+
license: License = Field(default_factory=License)
67+
urls: URLs = Field(default_factory=URLs)
68+
69+
70+
class PyProjectConfig(BaseModel):
71+
project: ProjectConfig = Field(default_factory=ProjectConfig)
72+
tool_comfy: ComfyConfig = Field(default_factory=ComfyConfig)
73+
74+
75+
class PyProjectSettings(BaseSettings):
76+
project: dict = Field(default_factory=dict)
77+
78+
tool: dict = Field(default_factory=dict)
79+
80+
model_config = SettingsConfigDict()

0 commit comments

Comments
 (0)