--feature-flag: strict bool coercion + drop invalid values

Per @rattus128 review on PR #13685: silently coercing typo'd bool values
(e.g. `--feature-flag show_signin_button=ture`) to `False` was a confusing
UX. Make bool coercion strict and drop unparseable flags entirely.

- `_coerce_bool`: accept only `true`/`false` (case-insensitive); raise
  `ValueError` for anything else (`ture`, `yes`, `1`, ``).
- `_coerce_flag_value`: no longer swallows exceptions; raises on bad
  coercion so the caller decides what to do.
- `_parse_cli_feature_flags`: catches `ValueError`/`TypeError`, logs a
  warning ("dropping flag"), and omits the flag from the result. ComfyUI
  still starts; `SERVER_FEATURE_FLAGS` retains the registered default;
  other valid `--feature-flag` entries on the same command line are
  unaffected.

Tests:
- `test_bool_typo_raises`: `ture`/`yes`/`1`/`""` all raise ValueError.
- `test_failed_int_coercion_raises`: replaces the old "falls back to
  string" test now that coercion failures propagate.
- `test_invalid_bool_value_dropped`: parser drops the bad flag and logs
  a warning, while a valid sibling flag still parses.

19/19 unit tests pass.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019df5a8-36be-7107-a4af-c7e4f51687df
This commit is contained in:
Jedrzej Kosinski 2026-05-04 18:19:04 -07:00
parent 0b5da2af97
commit 7d4fb0929c
2 changed files with 60 additions and 15 deletions

View File

@ -28,8 +28,22 @@ CLI_FEATURE_FLAG_REGISTRY: dict[str, FeatureFlagInfo] = {
} }
def _coerce_bool(v: str) -> bool:
"""Strict bool coercion: only 'true'/'false' (case-insensitive).
Anything else raises ValueError so the caller can warn and drop the flag,
rather than silently treating typos like 'ture' or 'yes' as False.
"""
lower = v.lower()
if lower == "true":
return True
if lower == "false":
return False
raise ValueError(f"expected 'true' or 'false', got {v!r}")
_COERCE_FNS: dict[str, Any] = { _COERCE_FNS: dict[str, Any] = {
"bool": lambda v: v.lower() == "true", "bool": _coerce_bool,
"int": lambda v: int(v), "int": lambda v: int(v),
"float": lambda v: float(v), "float": lambda v: float(v),
} }
@ -38,8 +52,9 @@ _COERCE_FNS: dict[str, Any] = {
def _coerce_flag_value(key: str, raw_value: str) -> Any: def _coerce_flag_value(key: str, raw_value: str) -> Any:
"""Coerce a raw string value using the registry type, or keep as string. """Coerce a raw string value using the registry type, or keep as string.
Returns the raw string if the key is unregistered, the type is unknown, Returns the raw string if the key is unregistered or the type is unknown.
or coercion fails (with a warning logged in the failure case). Raises ValueError/TypeError if the key is registered with a known type but
the value cannot be coerced; callers are expected to warn and drop the flag.
""" """
info = CLI_FEATURE_FLAG_REGISTRY.get(key) info = CLI_FEATURE_FLAG_REGISTRY.get(key)
if info is None: if info is None:
@ -47,20 +62,16 @@ def _coerce_flag_value(key: str, raw_value: str) -> Any:
coerce = _COERCE_FNS.get(info["type"]) coerce = _COERCE_FNS.get(info["type"])
if coerce is None: if coerce is None:
return raw_value return raw_value
try: return coerce(raw_value)
return coerce(raw_value)
except (ValueError, TypeError):
logging.warning(
"Could not coerce --feature-flag %s=%r to %s; using raw string.",
key, raw_value, info["type"],
)
return raw_value
def _parse_cli_feature_flags() -> dict[str, Any]: def _parse_cli_feature_flags() -> dict[str, Any]:
"""Parse --feature-flag key=value pairs from CLI args into a dict. """Parse --feature-flag key=value pairs from CLI args into a dict.
Items without '=' default to the value 'true' (bare flag form). Items without '=' default to the value 'true' (bare flag form).
Flags whose value cannot be coerced to the registered type are dropped
with a warning, so a typo like '--feature-flag some_bool=ture' does not
silently take effect as the wrong value.
""" """
result: dict[str, Any] = {} result: dict[str, Any] = {}
for item in getattr(args, "feature_flag", []): for item in getattr(args, "feature_flag", []):
@ -70,7 +81,14 @@ def _parse_cli_feature_flags() -> dict[str, Any]:
continue continue
if not sep: if not sep:
raw_value = "true" raw_value = "true"
result[key] = _coerce_flag_value(key, raw_value.strip()) try:
result[key] = _coerce_flag_value(key, raw_value.strip())
except (ValueError, TypeError) as e:
info = CLI_FEATURE_FLAG_REGISTRY.get(key, {})
logging.warning(
"Could not coerce --feature-flag %s=%r to %s (%s); dropping flag.",
key, raw_value.strip(), info.get("type", "?"), e,
)
return result return result

View File

@ -1,5 +1,7 @@
"""Tests for feature flags functionality.""" """Tests for feature flags functionality."""
import pytest
from comfy_api.feature_flags import ( from comfy_api.feature_flags import (
get_connection_feature, get_connection_feature,
supports_feature, supports_feature,
@ -116,14 +118,26 @@ class TestCoerceFlagValue:
assert _coerce_flag_value("unknown_flag", "true") == "true" assert _coerce_flag_value("unknown_flag", "true") == "true"
assert _coerce_flag_value("unknown_flag", "42") == "42" assert _coerce_flag_value("unknown_flag", "42") == "42"
def test_failed_coercion_falls_back_to_string(self, monkeypatch): def test_bool_typo_raises(self):
"""Malformed values for typed flags must not crash; raw string is returned.""" """Strict bool: typos like 'ture' or 'yes' must raise so the flag can be dropped."""
with pytest.raises(ValueError):
_coerce_flag_value("show_signin_button", "ture")
with pytest.raises(ValueError):
_coerce_flag_value("show_signin_button", "yes")
with pytest.raises(ValueError):
_coerce_flag_value("show_signin_button", "1")
with pytest.raises(ValueError):
_coerce_flag_value("show_signin_button", "")
def test_failed_int_coercion_raises(self, monkeypatch):
"""Malformed values for typed flags must raise; caller decides what to do."""
monkeypatch.setitem( monkeypatch.setitem(
CLI_FEATURE_FLAG_REGISTRY, CLI_FEATURE_FLAG_REGISTRY,
"test_int_flag", "test_int_flag",
{"type": "int", "default": 0, "description": "test"}, {"type": "int", "default": 0, "description": "test"},
) )
assert _coerce_flag_value("test_int_flag", "not_a_number") == "not_a_number" with pytest.raises(ValueError):
_coerce_flag_value("test_int_flag", "not_a_number")
class TestParseCliFeatureFlags: class TestParseCliFeatureFlags:
@ -145,6 +159,19 @@ class TestParseCliFeatureFlags:
result = _parse_cli_feature_flags() result = _parse_cli_feature_flags()
assert result == {"valid": "1"} assert result == {"valid": "1"}
def test_invalid_bool_value_dropped(self, monkeypatch, caplog):
"""A typo'd bool value must be dropped entirely, not silently set to False
and not stored as a raw string. A warning must be logged."""
monkeypatch.setattr(
"comfy_api.feature_flags.args",
type("Args", (), {"feature_flag": ["show_signin_button=ture", "valid=1"]})(),
)
with caplog.at_level("WARNING"):
result = _parse_cli_feature_flags()
assert result == {"valid": "1"}
assert "show_signin_button" not in result
assert any("show_signin_button" in r.message and "drop" in r.message.lower() for r in caplog.records)
class TestCliFeatureFlagRegistry: class TestCliFeatureFlagRegistry:
"""Test suite for the CLI feature flag registry.""" """Test suite for the CLI feature flag registry."""