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:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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"}):
|
||||||
|
|||||||
2
utils.py
2
utils.py
@@ -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(
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user