mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-04-24 01:12:37 +08:00
feat: add generic --feature-flag CLI arg and --list-feature-flags registry
Add --feature-flag KEY=VALUE CLI argument that allows setting arbitrary server feature flags at startup. Values are auto-converted to appropriate Python types (bool, int, float, string). CLI flags are merged into SERVER_FEATURE_FLAGS but cannot overwrite core flags. Add --list-feature-flags which prints the registry of known CLI-settable feature flags as JSON and exits, enabling launchers to discover valid flags for a specific ComfyUI version. Part of Comfy-Org/ComfyUI-Desktop-2.0-Beta#415 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d9386-54d3-74d9-a661-97e0a8d37b6b
This commit is contained in:
parent
1de83f91c3
commit
8637f6a7c3
@ -238,6 +238,8 @@ database_default_path = os.path.abspath(
|
|||||||
)
|
)
|
||||||
parser.add_argument("--database-url", type=str, default=f"sqlite:///{database_default_path}", help="Specify the database URL, e.g. for an in-memory database you can use 'sqlite:///:memory:'.")
|
parser.add_argument("--database-url", type=str, default=f"sqlite:///{database_default_path}", help="Specify the database URL, e.g. for an in-memory database you can use 'sqlite:///:memory:'.")
|
||||||
parser.add_argument("--enable-assets", action="store_true", help="Enable the assets system (API routes, database synchronization, and background scanning).")
|
parser.add_argument("--enable-assets", action="store_true", help="Enable the assets system (API routes, database synchronization, and background scanning).")
|
||||||
|
parser.add_argument("--feature-flag", type=str, action='append', default=[], metavar="KEY=VALUE", help="Set a server feature flag as a key=value pair. Can be specified multiple times. Boolean values (true/false) and numbers are auto-converted. Example: --feature-flag show_signin_button=true")
|
||||||
|
parser.add_argument("--list-feature-flags", action="store_true", help="Print the registry of known CLI-settable feature flags as JSON and exit.")
|
||||||
|
|
||||||
if comfy.options.args_parsing:
|
if comfy.options.args_parsing:
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
@ -5,12 +5,66 @@ This module handles capability negotiation between frontend and backend,
|
|||||||
allowing graceful protocol evolution while maintaining backward compatibility.
|
allowing graceful protocol evolution while maintaining backward compatibility.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureFlagInfo(TypedDict):
|
||||||
|
type: str
|
||||||
|
default: Any
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
# Registry of known CLI-settable feature flags.
|
||||||
|
# Launchers can query this via --list-feature-flags to discover valid flags.
|
||||||
|
CLI_FEATURE_FLAG_REGISTRY: dict[str, FeatureFlagInfo] = {
|
||||||
|
"show_signin_button": {
|
||||||
|
"type": "bool",
|
||||||
|
"default": False,
|
||||||
|
"description": "Show the sign-in button in the frontend even when not signed in",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cli_feature_flag_registry() -> dict[str, FeatureFlagInfo]:
|
||||||
|
"""Return the registry of known CLI-settable feature flags."""
|
||||||
|
return {k: dict(v) for k, v in CLI_FEATURE_FLAG_REGISTRY.items()}
|
||||||
|
|
||||||
|
|
||||||
|
_COERCE_FNS: dict[str, Any] = {
|
||||||
|
"bool": lambda v: v.lower() == "true",
|
||||||
|
"int": lambda v: int(v),
|
||||||
|
"float": lambda v: float(v),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_flag_value(key: str, raw_value: str) -> Any:
|
||||||
|
"""Coerce a raw string value using the registry type, or keep as string."""
|
||||||
|
info = CLI_FEATURE_FLAG_REGISTRY.get(key)
|
||||||
|
if info is None:
|
||||||
|
return raw_value
|
||||||
|
coerce = _COERCE_FNS.get(info["type"])
|
||||||
|
if coerce is None:
|
||||||
|
return raw_value
|
||||||
|
return coerce(raw_value)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cli_feature_flags() -> dict[str, Any]:
|
||||||
|
"""Parse --feature-flag key=value pairs from CLI args into a dict."""
|
||||||
|
result: dict[str, Any] = {}
|
||||||
|
for item in getattr(args, "feature_flag", []):
|
||||||
|
if "=" not in item:
|
||||||
|
continue
|
||||||
|
key, _, raw_value = item.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
if key:
|
||||||
|
result[key] = _coerce_flag_value(key, raw_value.strip())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Default server capabilities
|
# Default server capabilities
|
||||||
SERVER_FEATURE_FLAGS: dict[str, Any] = {
|
_CORE_FEATURE_FLAGS: dict[str, Any] = {
|
||||||
"supports_preview_metadata": True,
|
"supports_preview_metadata": True,
|
||||||
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
|
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
|
||||||
"extension": {"manager": {"supports_v4": True}},
|
"extension": {"manager": {"supports_v4": True}},
|
||||||
@ -18,6 +72,11 @@ SERVER_FEATURE_FLAGS: dict[str, Any] = {
|
|||||||
"assets": args.enable_assets,
|
"assets": args.enable_assets,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# CLI-provided flags cannot overwrite core flags
|
||||||
|
_cli_flags = {k: v for k, v in _parse_cli_feature_flags().items() if k not in _CORE_FEATURE_FLAGS}
|
||||||
|
|
||||||
|
SERVER_FEATURE_FLAGS: dict[str, Any] = {**_CORE_FEATURE_FLAGS, **_cli_flags}
|
||||||
|
|
||||||
|
|
||||||
def get_connection_feature(
|
def get_connection_feature(
|
||||||
sockets_metadata: dict[str, dict[str, Any]],
|
sockets_metadata: dict[str, dict[str, Any]],
|
||||||
|
|||||||
10
main.py
10
main.py
@ -1,13 +1,21 @@
|
|||||||
import comfy.options
|
import comfy.options
|
||||||
comfy.options.enable_args_parsing()
|
comfy.options.enable_args_parsing()
|
||||||
|
|
||||||
|
from comfy.cli_args import args
|
||||||
|
|
||||||
|
if args.list_feature_flags:
|
||||||
|
import json
|
||||||
|
from comfy_api.feature_flags import get_cli_feature_flag_registry
|
||||||
|
print(json.dumps(get_cli_feature_flag_registry(), indent=2)) # noqa: T201
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import shutil
|
import shutil
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import time
|
import time
|
||||||
from comfy.cli_args import args, enables_dynamic_vram
|
from comfy.cli_args import enables_dynamic_vram
|
||||||
from app.logger import setup_logger
|
from app.logger import setup_logger
|
||||||
from app.assets.seeder import asset_seeder
|
from app.assets.seeder import asset_seeder
|
||||||
from app.assets.services import register_output_files
|
from app.assets.services import register_output_files
|
||||||
|
|||||||
@ -4,7 +4,10 @@ from comfy_api.feature_flags import (
|
|||||||
get_connection_feature,
|
get_connection_feature,
|
||||||
supports_feature,
|
supports_feature,
|
||||||
get_server_features,
|
get_server_features,
|
||||||
|
get_cli_feature_flag_registry,
|
||||||
SERVER_FEATURE_FLAGS,
|
SERVER_FEATURE_FLAGS,
|
||||||
|
_coerce_flag_value,
|
||||||
|
_parse_cli_feature_flags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -96,3 +99,43 @@ class TestFeatureFlags:
|
|||||||
result = get_connection_feature(sockets_metadata, "sid1", "any_feature")
|
result = get_connection_feature(sockets_metadata, "sid1", "any_feature")
|
||||||
assert result is False
|
assert result is False
|
||||||
assert supports_feature(sockets_metadata, "sid1", "any_feature") is False
|
assert supports_feature(sockets_metadata, "sid1", "any_feature") is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoerceFlagValue:
|
||||||
|
"""Test suite for _coerce_flag_value."""
|
||||||
|
|
||||||
|
def test_registered_bool_true(self):
|
||||||
|
assert _coerce_flag_value("show_signin_button", "true") is True
|
||||||
|
assert _coerce_flag_value("show_signin_button", "True") is True
|
||||||
|
|
||||||
|
def test_registered_bool_false(self):
|
||||||
|
assert _coerce_flag_value("show_signin_button", "false") is False
|
||||||
|
assert _coerce_flag_value("show_signin_button", "FALSE") is False
|
||||||
|
|
||||||
|
def test_unregistered_key_stays_string(self):
|
||||||
|
assert _coerce_flag_value("unknown_flag", "true") == "true"
|
||||||
|
assert _coerce_flag_value("unknown_flag", "42") == "42"
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseCliFeatureFlags:
|
||||||
|
"""Test suite for _parse_cli_feature_flags."""
|
||||||
|
|
||||||
|
def test_single_flag(self, monkeypatch):
|
||||||
|
monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["show_signin_button=true"]})())
|
||||||
|
result = _parse_cli_feature_flags()
|
||||||
|
assert result == {"show_signin_button": True}
|
||||||
|
|
||||||
|
def test_missing_equals_skipped(self, monkeypatch):
|
||||||
|
monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["noequals", "valid=1"]})())
|
||||||
|
result = _parse_cli_feature_flags()
|
||||||
|
assert result == {"valid": "1"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCliFeatureFlagRegistry:
|
||||||
|
"""Test suite for the CLI feature flag registry."""
|
||||||
|
|
||||||
|
def test_registry_entries_have_required_fields(self):
|
||||||
|
for key, info in get_cli_feature_flag_registry().items():
|
||||||
|
assert "type" in info, f"{key} missing 'type'"
|
||||||
|
assert "default" in info, f"{key} missing 'default'"
|
||||||
|
assert "description" in info, f"{key} missing 'description'"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user