Skip to content

Commit f5e175a

Browse files
committed
docs(api): publish generated Python contract inventory and drift gates
1 parent 990ec7a commit f5e175a

4 files changed

Lines changed: 42611 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,21 @@ See also: [https://www.comfy.org/](https://www.comfy.org/)
399399

400400
> _psst — we're hiring!_ Help build ComfyUI: [comfy.org/careers](https://www.comfy.org/careers)
401401
402+
## V2 Custom Node API
403+
404+
Import `io` and `sdk` from `comfy_api.latest` for node schemas, typed resource
405+
handles, and context services. The API works independently of an optional
406+
sandbox overlay; service availability and permissions depend on the installed
407+
provider.
408+
409+
- [Python typing contract](comfy_api/latest/_sdk_public.pyi)
410+
- [Machine-readable API inventory](comfy_api/latest/api-spec.json), including
411+
nested schema types, signatures, argument kinds, and async operations
412+
413+
Regenerate with `python -m comfy_api.generate_api_spec`; verify freshness with
414+
`python -m comfy_api.generate_api_spec --check`. Neither command requires the
415+
private sandbox overlay.
416+
402417
## Frontend Development
403418

404419
As of August 15, 2024, we have transitioned to a new frontend, which is now hosted in a separate repository: [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend). The compiled JS files (from TS/Vue) are published to [pypi](https://pypi.org/project/comfyui-frontend-package) and installed as a dependency in ComfyUI.

‎comfy_api/generate_api_spec.py‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""Generate the public V2 API inventory without requiring a runtime overlay.
2+
3+
Run from the backend checkout: python -m comfy_api.generate_api_spec
4+
Check the published artifact: python -m comfy_api.generate_api_spec --check
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import argparse
10+
import ast
11+
import importlib
12+
import inspect
13+
import json
14+
from pathlib import Path
15+
16+
MODULES = (
17+
("io", "comfy_api.latest._io_public"),
18+
("sdk", "comfy_api.latest._sdk_public"),
19+
)
20+
ARTIFACT = Path(__file__).parent / "latest" / "api-spec.json"
21+
22+
23+
class _AnnotationSource:
24+
def __init__(self, text: str) -> None:
25+
self.text = text
26+
27+
def __repr__(self) -> str:
28+
return self.text
29+
30+
31+
def _annotation(value) -> str:
32+
return value if isinstance(value, str) else inspect.formatannotation(value)
33+
34+
35+
def _signature(function) -> str:
36+
try:
37+
signature = inspect.signature(function)
38+
except (ValueError, TypeError):
39+
return "(...)"
40+
parameters = []
41+
for parameter in signature.parameters.values():
42+
annotation = parameter.annotation
43+
if isinstance(annotation, str):
44+
annotation = _AnnotationSource(annotation)
45+
default = parameter.default
46+
if default is not inspect.Parameter.empty:
47+
try:
48+
ast.parse(repr(default), mode="eval")
49+
except (SyntaxError, ValueError):
50+
default = _AnnotationSource("...")
51+
parameters.append(parameter.replace(annotation=annotation, default=default))
52+
returns = signature.return_annotation
53+
if isinstance(returns, str):
54+
returns = _AnnotationSource(returns)
55+
return str(signature.replace(parameters=parameters, return_annotation=returns))
56+
57+
58+
def _callable(function, kind: str) -> dict:
59+
declaration = {
60+
"kind": kind,
61+
"async": inspect.iscoroutinefunction(function),
62+
"signature": _signature(function),
63+
}
64+
try:
65+
signature = inspect.signature(function)
66+
except (ValueError, TypeError):
67+
return declaration
68+
declaration["parameters"] = [
69+
{
70+
"name": parameter.name,
71+
"kind": parameter.kind.name,
72+
"annotation": None
73+
if parameter.annotation is inspect.Parameter.empty
74+
else _annotation(parameter.annotation),
75+
"required": parameter.default is inspect.Parameter.empty
76+
and parameter.kind
77+
not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD),
78+
}
79+
for parameter in signature.parameters.values()
80+
]
81+
declaration["returns"] = (
82+
None
83+
if signature.return_annotation is inspect.Signature.empty
84+
else _annotation(signature.return_annotation)
85+
)
86+
return declaration
87+
88+
89+
def _class(cls, seen: frozenset[type] = frozenset()) -> dict:
90+
if cls in seen:
91+
return {"kind": "class", "reference": cls.__qualname__}
92+
seen = seen | {cls}
93+
members = {}
94+
for name in sorted(dir(cls)):
95+
if name.startswith("_") and name != "__init__":
96+
continue
97+
member = inspect.getattr_static(cls, name)
98+
kind = "method"
99+
if isinstance(member, (classmethod, staticmethod)):
100+
kind = "classmethod" if isinstance(member, classmethod) else "staticmethod"
101+
member = member.__func__
102+
if inspect.isfunction(member):
103+
members[name] = _callable(member, kind)
104+
elif isinstance(member, property):
105+
members[name] = {
106+
"kind": "property",
107+
"writable": member.fset is not None,
108+
"getter": _callable(member.fget, "method"),
109+
}
110+
elif inspect.isclass(member) and member.__module__.startswith("comfy_api."):
111+
members[name] = _class(member, seen)
112+
annotations = {}
113+
for base in reversed(cls.__mro__):
114+
annotations.update(
115+
{
116+
name: _annotation(value)
117+
for name, value in vars(base).get("__annotations__", {}).items()
118+
if not name.startswith("_")
119+
}
120+
)
121+
return {"kind": "class", "members": members, "annotations": annotations}
122+
123+
124+
def generate_manifest() -> str:
125+
modules = {}
126+
for title, module_name in MODULES:
127+
module = importlib.import_module(module_name)
128+
names = getattr(module, "__all__", None) or [
129+
name for name in vars(module) if not name.startswith("_")
130+
]
131+
exports = {}
132+
for name in sorted(names):
133+
value = getattr(module, name)
134+
if inspect.isclass(value):
135+
exports[name] = _class(value)
136+
elif callable(value):
137+
exports[name] = _callable(value, "function")
138+
elif not inspect.ismodule(value):
139+
exports[name] = {"kind": "constant", "type": type(value).__name__}
140+
modules[title] = exports
141+
return (
142+
json.dumps(
143+
{
144+
"format": "comfy-api-v2",
145+
"import": "comfy_api.latest",
146+
"modules": modules,
147+
},
148+
indent=2,
149+
sort_keys=True,
150+
)
151+
+ "\n"
152+
)
153+
154+
155+
def main() -> int:
156+
parser = argparse.ArgumentParser(description=__doc__)
157+
parser.add_argument("--check", action="store_true")
158+
arguments = parser.parse_args()
159+
generated = generate_manifest()
160+
if arguments.check:
161+
if not ARTIFACT.is_file() or ARTIFACT.read_text() != generated:
162+
parser.exit(
163+
1, "api-spec.json is stale; run python -m comfy_api.generate_api_spec\n"
164+
)
165+
return 0
166+
ARTIFACT.write_text(generated)
167+
return 0
168+
169+
170+
if __name__ == "__main__":
171+
raise SystemExit(main())

0 commit comments

Comments
 (0)