Fix 4 bugs from third code review

- Fix delete_proj not persisting cleared current_project to config:
  page reload after deleting active project restored deleted name,
  silently breaking all DB sync
- Fix sync_to_db crash on non-dict batch_data items: add isinstance
  guard matching import_json_file
- Fix output_types ignored in load_dynamic: parse declared types and
  use to_int()/to_float() to coerce values, so downstream ComfyUI
  nodes receive correct types even when API returns strings
- Fix backward-compat comma-split for types not trimming whitespace:
  legacy workflows with "STRING, INT" got types " INT" breaking
  ComfyUI connection type-matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 21:38:37 +01:00
parent b499eb4dfd
commit c4d107206f
5 changed files with 40 additions and 4 deletions

View File

@@ -142,10 +142,24 @@ class ProjectLoaderDynamic:
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
keys = [k.strip() for k in output_keys.split(",") if k.strip()] keys = [k.strip() for k in output_keys.split(",") if k.strip()]
# Parse types for coercion
types = []
if output_types:
try:
types = json.loads(output_types)
except (json.JSONDecodeError, TypeError):
types = [t.strip() for t in output_types.split(",")]
results = [] results = []
for key in keys: for i, key in enumerate(keys):
val = data.get(key, "") val = data.get(key, "")
if isinstance(val, bool): declared_type = types[i] if i < len(types) else ""
# Coerce based on declared output type when possible
if declared_type == "INT":
results.append(to_int(val))
elif declared_type == "FLOAT":
results.append(to_float(val))
elif isinstance(val, bool):
results.append(str(val).lower()) results.append(str(val).lower())
elif isinstance(val, int): elif isinstance(val, int):
results.append(val) results.append(val)

View File

@@ -119,6 +119,10 @@ def render_projects_tab(state: AppState):
state.db.delete_project(name) state.db.delete_project(name)
if state.current_project == name: if state.current_project == name:
state.current_project = '' state.current_project = ''
state.config['current_project'] = ''
save_config(state.current_dir,
state.config.get('favorites', []),
state.config)
ui.notify(f'Deleted project "{name}"', type='positive') ui.notify(f'Deleted project "{name}"', type='positive')
render_project_list.refresh() render_project_list.refresh()

View File

@@ -100,6 +100,22 @@ class TestProjectLoaderDynamic:
assert result[0] == "comma_val" assert result[0] == "comma_val"
assert result[1] == "ok" assert result[1] == "ok"
def test_load_dynamic_type_coercion(self):
"""output_types should coerce values to declared types."""
import json as _json
data = {"seed": "42", "cfg": "1.5", "prompt": "hello"}
node = ProjectLoaderDynamic()
keys_json = _json.dumps(["seed", "cfg", "prompt"])
types_json = _json.dumps(["INT", "FLOAT", "STRING"])
with patch("project_loader._fetch_data", return_value=data):
result = node.load_dynamic(
"http://localhost:8080", "proj1", "batch_i2v", 1,
output_keys=keys_json, output_types=types_json
)
assert result[0] == 42 # string "42" coerced to int
assert result[1] == 1.5 # string "1.5" coerced to float
assert result[2] == "hello" # string stays string
def test_load_dynamic_empty_keys(self): def test_load_dynamic_empty_keys(self):
node = ProjectLoaderDynamic() node = ProjectLoaderDynamic()
with patch("project_loader._fetch_data", return_value={"prompt": "hello"}): with patch("project_loader._fetch_data", return_value={"prompt": "hello"}):

View File

@@ -202,6 +202,8 @@ def sync_to_db(db, project_name: str, file_path: Path, data: dict) -> None:
if isinstance(batch_data, list): if isinstance(batch_data, list):
db.conn.execute("DELETE FROM sequences WHERE data_file_id = ?", (df_id,)) db.conn.execute("DELETE FROM sequences WHERE data_file_id = ?", (df_id,))
for item in batch_data: for item in batch_data:
if not isinstance(item, dict):
continue
seq_num = int(item.get(KEY_SEQUENCE_NUMBER, 0)) seq_num = int(item.get(KEY_SEQUENCE_NUMBER, 0))
now = __import__('time').time() now = __import__('time').time()
db.conn.execute( db.conn.execute(

View File

@@ -154,7 +154,7 @@ app.registerExtension({
let types = []; let types = [];
if (otWidget?.value) { if (otWidget?.value) {
try { types = JSON.parse(otWidget.value); } catch (_) { try { types = JSON.parse(otWidget.value); } catch (_) {
types = otWidget.value.split(","); types = otWidget.value.split(",").map(t => t.trim()).filter(Boolean);
} }
} }
@@ -162,7 +162,7 @@ app.registerExtension({
// On load, LiteGraph already restored serialized outputs with links. // On load, LiteGraph already restored serialized outputs with links.
// Rename and set types to match stored state (preserves links). // Rename and set types to match stored state (preserves links).
for (let i = 0; i < this.outputs.length && i < keys.length; i++) { for (let i = 0; i < this.outputs.length && i < keys.length; i++) {
this.outputs[i].name = keys[i].trim(); this.outputs[i].name = keys[i];
if (types[i]) this.outputs[i].type = types[i]; if (types[i]) this.outputs[i].type = types[i];
} }