Skip to content

Commit 29cbbcc

Browse files
refactor: harden readability contracts
1 parent 412ee4b commit 29cbbcc

12 files changed

Lines changed: 373 additions & 95 deletions

comfyui_to_python/generator/embedded_modules.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,46 @@
1414

1515
import ast
1616
from pathlib import Path
17-
from typing import Any
18-
1917

2018
# Modules whose top-level definitions should be embedded in generated scripts.
21-
# Order matters: dependencies first, so functions are defined before callers reference them.
19+
# Order matters: dependencies first, so functions are defined before callers use them.
2220
_SOURCE_FILES: list[str] = [
2321
"runtime/module_loader.py", # _load_module, _bootstrap_import — no internal deps
24-
"runtime/path_discovery.py", # get_comfyui_path, find_path, etc. — uses stdlib only
25-
"runtime/bootstrap.py", # _discover_cli_options, _filter_args — depends on module_loader
26-
"node_runtime.py", # public API facade + bootstrap/cleanup — imports from all above
22+
"runtime/path_discovery.py", # get_comfyui_path, find_path, etc.
23+
"runtime/bootstrap.py", # CLI filtering; depends on module_loader
24+
"node_runtime.py", # public API facade + bootstrap/cleanup
2725
]
2826

2927

28+
APPROVED_EMBEDDED_NAMES: frozenset[str] = frozenset(
29+
{
30+
"_apply_device_settings",
31+
"_apply_directory_overrides",
32+
"_bootstrap_import",
33+
"_discover_comfyui_cli_options",
34+
"_filter_comfyui_args",
35+
"_find_file",
36+
"_find_from_extension_location",
37+
"_get_base_option",
38+
"_init_extra_nodes",
39+
"_is_comfyui_directory",
40+
"_load_custom_node_modules",
41+
"_load_module",
42+
"_load_module_temp",
43+
"_parse_parser_actions",
44+
"add_comfyui_directory_to_sys_path",
45+
"add_extra_model_paths",
46+
"bootstrap_comfyui_runtime",
47+
"cleanup_comfyui_runtime",
48+
"find_path",
49+
"get_comfyui_path",
50+
"get_node_class_mappings",
51+
"get_value_at_index",
52+
"import_custom_nodes",
53+
}
54+
)
55+
56+
3057
def _strip_imports(source: str) -> str:
3158
"""Remove all top-level import statements from Python source code.
3259
@@ -99,6 +126,22 @@ def get_embedded_helpers() -> str:
99126
return "\n".join(parts)
100127

101128

129+
def verify_embedded_surface_matches_manifest() -> list[str]:
130+
"""Return embedded helper names that differ from the approved surface."""
131+
actual = list_embedded_names()
132+
differences = [
133+
*(
134+
f"missing approved embedded name: {name}"
135+
for name in sorted(APPROVED_EMBEDDED_NAMES - actual)
136+
),
137+
*(
138+
f"unexpected embedded name: {name}"
139+
for name in sorted(actual - APPROVED_EMBEDDED_NAMES)
140+
),
141+
]
142+
return differences
143+
144+
102145
def list_embedded_names() -> set[str]:
103146
"""Return the set of all top-level names that will be embedded.
104147
@@ -194,7 +237,8 @@ def verify_no_missing_cross_calls() -> list[str]:
194237
is_nested = _is_nested_or_local_def(filepath, caller_name)
195238
if not is_nested:
196239
unresolved.append(
197-
f"{rel_path}: calls '{caller_name}' (not embedded, not builtin)"
240+
f"{rel_path}: calls '{caller_name}' "
241+
"(not embedded, not builtin)"
198242
)
199243

200244
return unresolved

comfyui_to_python/generator/planner.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,7 @@ def build_plan(
5151
input_value_types = self.get_input_value_types(input_types)
5252
class_def = self.node_class_mappings[class_type]()
5353

54-
missing_required_variable = False
55-
if "required" in input_types.keys():
56-
for required in input_types["required"]:
57-
if required not in inputs.keys():
58-
missing_required_variable = True
59-
if missing_required_variable:
54+
if self._node_has_missing_required_inputs(input_types, inputs):
6055
continue
6156

6257
if class_type not in initialized_objects:
@@ -85,22 +80,7 @@ def build_plan(
8580
if no_params or key in class_def_params
8681
}
8782

88-
hidden_inputs = input_types.get("hidden", {})
89-
if "unique_id" in hidden_inputs and (
90-
no_params or "unique_id" in class_def_params
91-
):
92-
inputs["unique_id"] = random.randint(1, 2**64)
93-
if "prompt" in hidden_inputs and (
94-
no_params or "prompt" in class_def_params
95-
):
96-
inputs["prompt"] = {"variable_name": "prompt"}
97-
if "extra_pnginfo" in hidden_inputs and (
98-
no_params or "extra_pnginfo" in class_def_params
99-
):
100-
inputs["extra_pnginfo"] = {"variable_name": "extra_pnginfo"}
101-
if "hidden" not in input_types and class_def_params is not None:
102-
if "unique_id" in class_def_params:
103-
inputs["unique_id"] = random.randint(1, 2**64)
83+
self._apply_hidden_inputs(inputs, input_types, class_def_params)
10484

10585
executed_variables[idx] = (
10686
f"{self.clean_variable_name(class_type)}_"
@@ -135,6 +115,34 @@ def build_plan(
135115
custom_nodes=custom_nodes,
136116
)
137117

118+
@staticmethod
119+
def _node_has_missing_required_inputs(input_types: dict, inputs: dict) -> bool:
120+
"""Return True when a node is missing any required input."""
121+
return any(
122+
required not in inputs for required in input_types.get("required", {})
123+
)
124+
125+
@staticmethod
126+
def _apply_hidden_inputs(
127+
inputs: dict, input_types: dict, class_def_params: list | None
128+
) -> None:
129+
"""Inject ComfyUI hidden runtime inputs accepted by the node function."""
130+
hidden_inputs = input_types.get("hidden", {})
131+
no_params = class_def_params is None
132+
if "unique_id" in hidden_inputs and (
133+
no_params or "unique_id" in class_def_params
134+
):
135+
inputs["unique_id"] = random.randint(1, 2**64)
136+
if "prompt" in hidden_inputs and (no_params or "prompt" in class_def_params):
137+
inputs["prompt"] = {"variable_name": "prompt"}
138+
if "extra_pnginfo" in hidden_inputs and (
139+
no_params or "extra_pnginfo" in class_def_params
140+
):
141+
inputs["extra_pnginfo"] = {"variable_name": "extra_pnginfo"}
142+
if "hidden" not in input_types and class_def_params is not None:
143+
if "unique_id" in class_def_params:
144+
inputs["unique_id"] = random.randint(1, 2**64)
145+
138146
def create_function_call_code(
139147
self,
140148
obj_name: str,
@@ -164,12 +172,16 @@ def create_prompt_seed_sync_code(
164172
for key in ("seed", "noise_seed"):
165173
if key not in inputs:
166174
continue
167-
randomized_seed_variable = f"node_{self.sanitize_node_id(str(node_id))}_{self.clean_variable_name(key)}"
175+
randomized_seed_variable = (
176+
f"node_{self.sanitize_node_id(str(node_id))}_"
177+
f"{self.clean_variable_name(key)}"
178+
)
168179
randomized_seed_code = self.get_randomized_seed_code(
169180
input_value_types.get(key)
170181
)
171182
seed_sync_lines.append(
172-
f'{randomized_seed_variable} = prompt["{node_id}"]["inputs"]["{key}"] = {randomized_seed_code}'
183+
f'{randomized_seed_variable} = prompt["{node_id}"]["inputs"]'
184+
f'["{key}"] = {randomized_seed_code}'
173185
)
174186
inputs[key] = {"variable_name": randomized_seed_variable}
175187

@@ -256,7 +268,9 @@ def update_inputs(self, inputs: dict, executed_variables: dict) -> dict:
256268
isinstance(inputs[key], list)
257269
and inputs[key][0] in executed_variables.keys()
258270
):
259-
inputs[key] = {
260-
"variable_name": f"get_value_at_index({executed_variables[inputs[key][0]]}, {inputs[key][1]})"
261-
}
271+
variable_name = (
272+
f"get_value_at_index({executed_variables[inputs[key][0]]}, "
273+
f"{inputs[key][1]})"
274+
)
275+
inputs[key] = {"variable_name": variable_name}
262276
return inputs

comfyui_to_python/generator/render.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,23 @@ class WorkflowRenderer:
1616
"""Render a generation plan into the final standalone Python source."""
1717

1818
def render(self, plan: GenerationPlan) -> str:
19-
workflow_literal = self.format_python_literal(plan.workflow_data)
20-
if plan.metadata_workflow_data is None:
21-
extra_pnginfo_literal = "None"
22-
else:
23-
extra_pnginfo_literal = self.format_python_literal(
24-
{"workflow": plan.metadata_workflow_data}
25-
)
19+
final_code = "\n".join(
20+
self._build_static_imports(plan)
21+
+ [""]
22+
+ self._build_workflow_section(plan)
23+
+ [""]
24+
+ self._build_execution_section(plan)
25+
+ [""]
26+
+ self._build_entrypoint_section()
27+
)
28+
return black.format_str(final_code, mode=black.Mode())
2629

30+
def _build_static_imports(self, plan: GenerationPlan) -> list[str]:
31+
"""Build imports and embedded helper definitions for standalone scripts."""
2732
# Auto-discover all helpers from contributing runtime modules.
2833
# Reads source files, strips imports, embeds definitions — so internal
2934
# cross-calls always resolve (no NameError from missing __all__ entries).
3035
embedded_helpers = get_embedded_helpers()
31-
3236
static_imports = [
3337
"# Imports",
3438
"import gc",
@@ -42,20 +46,22 @@ def render(self, plan: GenerationPlan) -> str:
4246
"from typing import Sequence, Mapping, Any, Union",
4347
"",
4448
"log = logging.getLogger(__name__)",
45-
] + [embedded_helpers]
46-
49+
embedded_helpers,
50+
]
4751
if plan.custom_nodes:
4852
static_imports.append(f"\n{inspect.getsource(import_custom_nodes)}\n")
49-
custom_nodes_call = "import_custom_nodes()"
50-
else:
51-
custom_nodes_call = None
53+
return static_imports
5254

53-
imports_code = []
54-
for module_name in sorted(plan.import_statements.keys()):
55-
class_names = ", ".join(sorted(plan.import_statements[module_name]))
56-
imports_code.append(f"from {module_name} import {class_names}")
57-
58-
workflow_section = [
55+
def _build_workflow_section(self, plan: GenerationPlan) -> list[str]:
56+
"""Build workflow and PNG metadata literals."""
57+
workflow_literal = self.format_python_literal(plan.workflow_data)
58+
if plan.metadata_workflow_data is None:
59+
extra_pnginfo_literal = "None"
60+
else:
61+
extra_pnginfo_literal = self.format_python_literal(
62+
{"workflow": plan.metadata_workflow_data}
63+
)
64+
return [
5965
"# Workflow data",
6066
"def build_workflow() -> dict[str, Any]:",
6167
f" return {workflow_literal}",
@@ -68,17 +74,22 @@ def render(self, plan: GenerationPlan) -> str:
6874
"extra_pnginfo = build_extra_pnginfo()",
6975
]
7076

77+
def _build_execution_section(self, plan: GenerationPlan) -> list[str]:
78+
"""Build the bootstrap, import, loop, and cleanup body."""
7179
execution_section = [
7280
"# Workflow execution",
7381
"def main(unload_models: bool | None = None):",
7482
" bootstrap_comfyui_runtime()",
7583
" add_extra_model_paths()",
7684
]
77-
if custom_nodes_call:
78-
execution_section.append(f" {custom_nodes_call}")
85+
if plan.custom_nodes:
86+
execution_section.append(" import_custom_nodes()")
87+
88+
imports_code = self._build_imports_code(plan)
7989
if imports_code:
8090
execution_section.extend(["", " # Node imports"])
8191
execution_section.extend(f" {line}" for line in imports_code)
92+
8293
execution_section.extend(
8394
[
8495
"",
@@ -105,24 +116,26 @@ def render(self, plan: GenerationPlan) -> str:
105116
" cleanup_comfyui_runtime(unload_models=unload_models)",
106117
]
107118
)
119+
return execution_section
108120

109-
entrypoint_section = [
121+
@staticmethod
122+
def _build_imports_code(plan: GenerationPlan) -> list[str]:
123+
"""Build sorted node import statements for the main function."""
124+
imports_code = []
125+
for module_name in sorted(plan.import_statements.keys()):
126+
class_names = ", ".join(sorted(plan.import_statements[module_name]))
127+
imports_code.append(f"from {module_name} import {class_names}")
128+
return imports_code
129+
130+
@staticmethod
131+
def _build_entrypoint_section() -> list[str]:
132+
"""Build the standalone script entrypoint."""
133+
return [
110134
"# Entrypoint",
111135
'if __name__ == "__main__":',
112136
" main()",
113137
]
114138

115-
final_code = "\n".join(
116-
static_imports
117-
+ [""]
118-
+ workflow_section
119-
+ [""]
120-
+ execution_section
121-
+ [""]
122-
+ entrypoint_section
123-
)
124-
return black.format_str(final_code, mode=black.Mode())
125-
126139
@staticmethod
127140
def format_python_literal(value: Any) -> str:
128141
return pformat(value, sort_dicts=False)

comfyui_to_python/node_runtime.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,19 @@ def bootstrap_comfyui_runtime() -> None:
165165
# argparse crashes when bootstrap runs inside a subprocess (e.g. test
166166
# runner) where sys.argv contains non-ComfyUI arguments.
167167
original_argv = sys.argv
168-
sys.argv = _filter_comfyui_args(sys.argv)
168+
try:
169+
sys.argv = _filter_comfyui_args(sys.argv)
169170

170-
# Load via _bootstrap_import() for namespace-package-safe imports.
171-
options_mod = _bootstrap_import("comfy.options")
172-
if options_mod is not None:
173-
options_mod.enable_args_parsing()
171+
# Load via _bootstrap_import() for namespace-package-safe imports.
172+
options_mod = _bootstrap_import("comfy.options")
173+
if options_mod is not None:
174+
options_mod.enable_args_parsing()
174175

175-
cli_args_mod = _bootstrap_import("comfy.cli_args")
176+
cli_args_mod = _bootstrap_import("comfy.cli_args")
177+
finally:
178+
# Restore original argv so downstream and error paths see real inputs.
179+
sys.argv = original_argv
176180

177-
# Restore original argv so that downstream code sees what was actually passed
178-
sys.argv = original_argv
179181
args = getattr(cli_args_mod, "args", None) if cli_args_mod else None
180182

181183
if os.name == "nt":

tests/runtime/generated/text-to-image.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -602,17 +602,19 @@ def bootstrap_comfyui_runtime() -> None:
602602
# argparse crashes when bootstrap runs inside a subprocess (e.g. test
603603
# runner) where sys.argv contains non-ComfyUI arguments.
604604
original_argv = sys.argv
605-
sys.argv = _filter_comfyui_args(sys.argv)
605+
try:
606+
sys.argv = _filter_comfyui_args(sys.argv)
606607

607-
# Load via _bootstrap_import() for namespace-package-safe imports.
608-
options_mod = _bootstrap_import("comfy.options")
609-
if options_mod is not None:
610-
options_mod.enable_args_parsing()
608+
# Load via _bootstrap_import() for namespace-package-safe imports.
609+
options_mod = _bootstrap_import("comfy.options")
610+
if options_mod is not None:
611+
options_mod.enable_args_parsing()
611612

612-
cli_args_mod = _bootstrap_import("comfy.cli_args")
613+
cli_args_mod = _bootstrap_import("comfy.cli_args")
614+
finally:
615+
# Restore original argv so downstream and error paths see real inputs.
616+
sys.argv = original_argv
613617

614-
# Restore original argv so that downstream code sees what was actually passed
615-
sys.argv = original_argv
616618
args = getattr(cli_args_mod, "args", None) if cli_args_mod else None
617619

618620
if os.name == "nt":

0 commit comments

Comments
 (0)