mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-08 00:02:31 +08:00
Merge branch 'master' into feature/deploy-environment-header
This commit is contained in:
commit
bcfbf8a33a
@ -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. Use KEY=VALUE to set an explicit value, or bare KEY to set it to true. Can be specified multiple times. Boolean values (true/false) and numbers are auto-converted. Examples: --feature-flag show_signin_button=true or --feature-flag show_signin_button")
|
||||||
|
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()
|
||||||
|
|||||||
@ -721,13 +721,15 @@ def load_models_gpu(models, memory_required=0, force_patch_weights=False, minimu
|
|||||||
else:
|
else:
|
||||||
minimum_memory_required = max(inference_memory, minimum_memory_required + extra_reserved_memory())
|
minimum_memory_required = max(inference_memory, minimum_memory_required + extra_reserved_memory())
|
||||||
|
|
||||||
models_temp = set()
|
# Order-preserving dedup. A plain set() would randomize iteration order across runs
|
||||||
|
models_temp = {}
|
||||||
for m in models:
|
for m in models:
|
||||||
models_temp.add(m)
|
models_temp[m] = None
|
||||||
for mm in m.model_patches_models():
|
for mm in m.model_patches_models():
|
||||||
models_temp.add(mm)
|
models_temp[mm] = None
|
||||||
|
|
||||||
models = models_temp
|
models = list(models_temp)
|
||||||
|
models.reverse()
|
||||||
|
|
||||||
models_to_load = []
|
models_to_load = []
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,8 @@ def prefetch_queue_pop(queue, device, module):
|
|||||||
consumed = queue.pop(0)
|
consumed = queue.pop(0)
|
||||||
if consumed is not None:
|
if consumed is not None:
|
||||||
offload_stream, prefetch_state = consumed
|
offload_stream, prefetch_state = consumed
|
||||||
offload_stream.wait_stream(comfy.model_management.current_stream(device))
|
if offload_stream is not None:
|
||||||
|
offload_stream.wait_stream(comfy.model_management.current_stream(device))
|
||||||
_, comfy_modules = prefetch_state
|
_, comfy_modules = prefetch_state
|
||||||
if comfy_modules is not None:
|
if comfy_modules is not None:
|
||||||
cleanup_prefetched_modules(comfy_modules)
|
cleanup_prefetched_modules(comfy_modules)
|
||||||
|
|||||||
@ -253,6 +253,9 @@ def resolve_cast_module_with_vbar(s, dtype, device, bias_dtype, compute_dtype, w
|
|||||||
if bias is not None:
|
if bias is not None:
|
||||||
bias = post_cast(s, "bias", bias, bias_dtype, prefetch["resident"], update_weight)
|
bias = post_cast(s, "bias", bias, bias_dtype, prefetch["resident"], update_weight)
|
||||||
|
|
||||||
|
if prefetch["signature"] is not None:
|
||||||
|
prefetch["resident"] = True
|
||||||
|
|
||||||
return weight, bias
|
return weight, bias
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -89,7 +89,8 @@ def get_additional_models(conds, dtype):
|
|||||||
gligen += get_models_from_cond(conds[k], "gligen")
|
gligen += get_models_from_cond(conds[k], "gligen")
|
||||||
add_models += get_models_from_cond(conds[k], "additional_models")
|
add_models += get_models_from_cond(conds[k], "additional_models")
|
||||||
|
|
||||||
control_nets = set(cnets)
|
# Order-preserving dedup. A plain set() would randomize iteration order across runs
|
||||||
|
control_nets = list(dict.fromkeys(cnets))
|
||||||
|
|
||||||
inference_memory = 0
|
inference_memory = 0
|
||||||
control_models = []
|
control_models = []
|
||||||
|
|||||||
@ -5,12 +5,95 @@ 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
|
import logging
|
||||||
|
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 _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] = {
|
||||||
|
"bool": _coerce_bool,
|
||||||
|
"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.
|
||||||
|
|
||||||
|
Returns the raw string if the key is unregistered or the type is unknown.
|
||||||
|
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)
|
||||||
|
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.
|
||||||
|
|
||||||
|
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] = {}
|
||||||
|
for item in getattr(args, "feature_flag", []):
|
||||||
|
key, sep, raw_value = item.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
if not sep:
|
||||||
|
raw_value = "true"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
# 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 +101,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]],
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class OpenAIVideoSora2(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="OpenAIVideoSora2",
|
node_id="OpenAIVideoSora2",
|
||||||
display_name="OpenAI Sora - Video (Deprecated)",
|
display_name="OpenAI Sora - Video (DEPRECATED)",
|
||||||
category="api node/video/Sora",
|
category="api node/video/Sora",
|
||||||
description=(
|
description=(
|
||||||
"OpenAI video and audio generation.\n\n"
|
"OpenAI video and audio generation.\n\n"
|
||||||
|
|||||||
@ -199,6 +199,9 @@ class FILMNet(nn.Module):
|
|||||||
def get_dtype(self):
|
def get_dtype(self):
|
||||||
return self.extract.extract_sublevels.convs[0][0].conv.weight.dtype
|
return self.extract.extract_sublevels.convs[0][0].conv.weight.dtype
|
||||||
|
|
||||||
|
def memory_used_forward(self, shape, dtype):
|
||||||
|
return 1700 * shape[1] * shape[2] * dtype.itemsize
|
||||||
|
|
||||||
def _build_warp_grids(self, H, W, device):
|
def _build_warp_grids(self, H, W, device):
|
||||||
"""Pre-compute warp grids for all pyramid levels."""
|
"""Pre-compute warp grids for all pyramid levels."""
|
||||||
if (H, W) in self._warp_grids:
|
if (H, W) in self._warp_grids:
|
||||||
|
|||||||
@ -74,6 +74,9 @@ class IFNet(nn.Module):
|
|||||||
def get_dtype(self):
|
def get_dtype(self):
|
||||||
return self.encode.cnn0.weight.dtype
|
return self.encode.cnn0.weight.dtype
|
||||||
|
|
||||||
|
def memory_used_forward(self, shape, dtype):
|
||||||
|
return 300 * shape[1] * shape[2] * dtype.itemsize
|
||||||
|
|
||||||
def _build_warp_grids(self, H, W, device):
|
def _build_warp_grids(self, H, W, device):
|
||||||
if (H, W) in self._warp_grids:
|
if (H, W) in self._warp_grids:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -37,7 +37,7 @@ class FrameInterpolationModelLoader(io.ComfyNode):
|
|||||||
model = cls._detect_and_load(sd)
|
model = cls._detect_and_load(sd)
|
||||||
dtype = torch.float16 if model_management.should_use_fp16(model_management.get_torch_device()) else torch.float32
|
dtype = torch.float16 if model_management.should_use_fp16(model_management.get_torch_device()) else torch.float32
|
||||||
model.eval().to(dtype)
|
model.eval().to(dtype)
|
||||||
patcher = comfy.model_patcher.ModelPatcher(
|
patcher = comfy.model_patcher.CoreModelPatcher(
|
||||||
model,
|
model,
|
||||||
load_device=model_management.get_torch_device(),
|
load_device=model_management.get_torch_device(),
|
||||||
offload_device=model_management.unet_offload_device(),
|
offload_device=model_management.unet_offload_device(),
|
||||||
@ -78,7 +78,7 @@ class FrameInterpolate(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="FrameInterpolate",
|
node_id="FrameInterpolate",
|
||||||
display_name="Frame Interpolate",
|
display_name="Frame Interpolate",
|
||||||
category="image/video",
|
category="video",
|
||||||
search_aliases=["rife", "film", "frame interpolation", "slow motion", "interpolate frames", "vfi"],
|
search_aliases=["rife", "film", "frame interpolation", "slow motion", "interpolate frames", "vfi"],
|
||||||
inputs=[
|
inputs=[
|
||||||
FrameInterpolationModel.Input("interp_model"),
|
FrameInterpolationModel.Input("interp_model"),
|
||||||
@ -98,16 +98,13 @@ class FrameInterpolate(io.ComfyNode):
|
|||||||
if num_frames < 2 or multiplier < 2:
|
if num_frames < 2 or multiplier < 2:
|
||||||
return io.NodeOutput(images)
|
return io.NodeOutput(images)
|
||||||
|
|
||||||
model_management.load_model_gpu(interp_model)
|
|
||||||
device = interp_model.load_device
|
device = interp_model.load_device
|
||||||
dtype = interp_model.model_dtype()
|
dtype = interp_model.model_dtype()
|
||||||
inference_model = interp_model.model
|
inference_model = interp_model.model
|
||||||
|
activation_mem = inference_model.memory_used_forward(images.shape, dtype)
|
||||||
# Free VRAM for inference activations (model weights + ~20x a single frame's worth)
|
model_management.load_models_gpu([interp_model], memory_required=activation_mem)
|
||||||
H, W = images.shape[1], images.shape[2]
|
|
||||||
activation_mem = H * W * 3 * images.element_size() * 20
|
|
||||||
model_management.free_memory(activation_mem, device)
|
|
||||||
align = getattr(inference_model, "pad_align", 1)
|
align = getattr(inference_model, "pad_align", 1)
|
||||||
|
H, W = images.shape[1], images.shape[2]
|
||||||
|
|
||||||
# Prepare a single padded frame on device for determining output dimensions
|
# Prepare a single padded frame on device for determining output dimensions
|
||||||
def prepare_frame(idx):
|
def prepare_frame(idx):
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class ImageCompare(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageCompare",
|
node_id="ImageCompare",
|
||||||
display_name="Image Compare",
|
display_name="Compare Images",
|
||||||
description="Compares two images side by side with a slider.",
|
description="Compares two images side by side with a slider.",
|
||||||
category="image",
|
category="image",
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class ImageCrop(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageCrop",
|
node_id="ImageCrop",
|
||||||
search_aliases=["trim"],
|
search_aliases=["trim"],
|
||||||
display_name="Image Crop (Deprecated)",
|
display_name="Crop Image (DEPRECATED)",
|
||||||
category="image/transform",
|
category="image/transform",
|
||||||
is_deprecated=True,
|
is_deprecated=True,
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
@ -56,7 +56,7 @@ class ImageCropV2(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageCropV2",
|
node_id="ImageCropV2",
|
||||||
search_aliases=["trim"],
|
search_aliases=["trim"],
|
||||||
display_name="Image Crop",
|
display_name="Crop Image",
|
||||||
category="image/transform",
|
category="image/transform",
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
has_intermediate_output=True,
|
has_intermediate_output=True,
|
||||||
@ -109,6 +109,7 @@ class RepeatImageBatch(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="RepeatImageBatch",
|
node_id="RepeatImageBatch",
|
||||||
search_aliases=["duplicate image", "clone image"],
|
search_aliases=["duplicate image", "clone image"],
|
||||||
|
display_name="Repeat Image Batch",
|
||||||
category="image/batch",
|
category="image/batch",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
@ -131,6 +132,7 @@ class ImageFromBatch(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageFromBatch",
|
node_id="ImageFromBatch",
|
||||||
search_aliases=["select image", "pick from batch", "extract image"],
|
search_aliases=["select image", "pick from batch", "extract image"],
|
||||||
|
display_name="Get Image from Batch",
|
||||||
category="image/batch",
|
category="image/batch",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
@ -157,7 +159,8 @@ class ImageAddNoise(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageAddNoise",
|
node_id="ImageAddNoise",
|
||||||
search_aliases=["film grain"],
|
search_aliases=["film grain"],
|
||||||
category="image",
|
display_name="Add Noise to Image",
|
||||||
|
category="image/postprocessing",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
IO.Int.Input(
|
IO.Int.Input(
|
||||||
@ -259,7 +262,7 @@ class ImageStitch(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageStitch",
|
node_id="ImageStitch",
|
||||||
search_aliases=["combine images", "join images", "concatenate images", "side by side"],
|
search_aliases=["combine images", "join images", "concatenate images", "side by side"],
|
||||||
display_name="Image Stitch",
|
display_name="Stitch Images",
|
||||||
description="Stitches image2 to image1 in the specified direction.\n"
|
description="Stitches image2 to image1 in the specified direction.\n"
|
||||||
"If image2 is not provided, returns image1 unchanged.\n"
|
"If image2 is not provided, returns image1 unchanged.\n"
|
||||||
"Optional spacing can be added between images.",
|
"Optional spacing can be added between images.",
|
||||||
@ -434,6 +437,7 @@ class ResizeAndPadImage(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ResizeAndPadImage",
|
node_id="ResizeAndPadImage",
|
||||||
search_aliases=["fit to size"],
|
search_aliases=["fit to size"],
|
||||||
|
display_name="Resize And Pad Image",
|
||||||
category="image/transform",
|
category="image/transform",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
@ -485,6 +489,7 @@ class SaveSVGNode(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="SaveSVGNode",
|
node_id="SaveSVGNode",
|
||||||
search_aliases=["export vector", "save vector graphics"],
|
search_aliases=["export vector", "save vector graphics"],
|
||||||
|
display_name="Save SVG",
|
||||||
description="Save SVG files on disk.",
|
description="Save SVG files on disk.",
|
||||||
category="image/save",
|
category="image/save",
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -591,7 +596,7 @@ class ImageRotate(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageRotate",
|
node_id="ImageRotate",
|
||||||
display_name="Image Rotate",
|
display_name="Rotate Image",
|
||||||
search_aliases=["turn", "flip orientation"],
|
search_aliases=["turn", "flip orientation"],
|
||||||
category="image/transform",
|
category="image/transform",
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
@ -624,6 +629,7 @@ class ImageFlip(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageFlip",
|
node_id="ImageFlip",
|
||||||
search_aliases=["mirror", "reflect"],
|
search_aliases=["mirror", "reflect"],
|
||||||
|
display_name="Flip Image",
|
||||||
category="image/transform",
|
category="image/transform",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
@ -650,6 +656,7 @@ class ImageScaleToMaxDimension(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageScaleToMaxDimension",
|
node_id="ImageScaleToMaxDimension",
|
||||||
|
display_name="Scale Image to Max Dimension",
|
||||||
category="image/upscaling",
|
category="image/upscaling",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
|
|||||||
@ -80,7 +80,8 @@ class ImageCompositeMasked(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ImageCompositeMasked",
|
node_id="ImageCompositeMasked",
|
||||||
search_aliases=["paste image", "overlay", "layer"],
|
search_aliases=["overlay", "layer", "paste image", "images composition"],
|
||||||
|
display_name="Image Composite Masked",
|
||||||
category="image",
|
category="image",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("destination"),
|
IO.Image.Input("destination"),
|
||||||
@ -201,6 +202,7 @@ class InvertMask(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="InvertMask",
|
node_id="InvertMask",
|
||||||
search_aliases=["reverse mask", "flip mask"],
|
search_aliases=["reverse mask", "flip mask"],
|
||||||
|
display_name="Invert Mask",
|
||||||
category="mask",
|
category="mask",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mask.Input("mask"),
|
IO.Mask.Input("mask"),
|
||||||
@ -222,6 +224,7 @@ class CropMask(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="CropMask",
|
node_id="CropMask",
|
||||||
search_aliases=["cut mask", "extract mask region", "mask slice"],
|
search_aliases=["cut mask", "extract mask region", "mask slice"],
|
||||||
|
display_name="Crop Mask",
|
||||||
category="mask",
|
category="mask",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mask.Input("mask"),
|
IO.Mask.Input("mask"),
|
||||||
@ -247,7 +250,8 @@ class MaskComposite(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="MaskComposite",
|
node_id="MaskComposite",
|
||||||
search_aliases=["combine masks", "blend masks", "layer masks"],
|
search_aliases=["combine masks", "blend masks", "layer masks", "masks composition"],
|
||||||
|
display_name="Combine Masks",
|
||||||
category="mask",
|
category="mask",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mask.Input("destination"),
|
IO.Mask.Input("destination"),
|
||||||
@ -298,6 +302,7 @@ class FeatherMask(IO.ComfyNode):
|
|||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="FeatherMask",
|
node_id="FeatherMask",
|
||||||
search_aliases=["soft edge mask", "blur mask edges", "gradient mask edge"],
|
search_aliases=["soft edge mask", "blur mask edges", "gradient mask edge"],
|
||||||
|
display_name="Feather Mask",
|
||||||
category="mask",
|
category="mask",
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Mask.Input("mask"),
|
IO.Mask.Input("mask"),
|
||||||
|
|||||||
@ -59,7 +59,8 @@ class ImageRGBToYUV(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="ImageRGBToYUV",
|
node_id="ImageRGBToYUV",
|
||||||
search_aliases=["color space conversion"],
|
search_aliases=["color space conversion"],
|
||||||
category="image/batch",
|
display_name="Image RGB to YUV",
|
||||||
|
category="image/color",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input("image"),
|
io.Image.Input("image"),
|
||||||
],
|
],
|
||||||
@ -81,7 +82,8 @@ class ImageYUVToRGB(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="ImageYUVToRGB",
|
node_id="ImageYUVToRGB",
|
||||||
search_aliases=["color space conversion"],
|
search_aliases=["color space conversion"],
|
||||||
category="image/batch",
|
display_name="Image YUV to RGB",
|
||||||
|
category="image/color",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input("Y"),
|
io.Image.Input("Y"),
|
||||||
io.Image.Input("U"),
|
io.Image.Input("U"),
|
||||||
|
|||||||
@ -20,7 +20,8 @@ class Blend(io.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="ImageBlend",
|
node_id="ImageBlend",
|
||||||
display_name="Image Blend",
|
search_aliases=["mix images"],
|
||||||
|
display_name="Blend Images",
|
||||||
category="image/postprocessing",
|
category="image/postprocessing",
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -224,6 +225,7 @@ class ImageScaleToTotalPixels(io.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="ImageScaleToTotalPixels",
|
node_id="ImageScaleToTotalPixels",
|
||||||
|
display_name="Scale Image to Total Pixels",
|
||||||
category="image/upscaling",
|
category="image/upscaling",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input("image"),
|
io.Image.Input("image"),
|
||||||
@ -568,7 +570,7 @@ class BatchImagesNode(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="BatchImagesNode",
|
node_id="BatchImagesNode",
|
||||||
display_name="Batch Images",
|
display_name="Batch Images",
|
||||||
category="image",
|
category="image/batch",
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
search_aliases=["batch", "image batch", "batch images", "combine images", "merge images", "stack images"],
|
search_aliases=["batch", "image batch", "batch images", "combine images", "merge images", "stack images"],
|
||||||
inputs=[
|
inputs=[
|
||||||
|
|||||||
@ -17,7 +17,8 @@ class SaveWEBM(io.ComfyNode):
|
|||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="SaveWEBM",
|
node_id="SaveWEBM",
|
||||||
search_aliases=["export webm"],
|
search_aliases=["export webm"],
|
||||||
category="image/video",
|
display_name="Save WEBM",
|
||||||
|
category="video",
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input("images"),
|
io.Image.Input("images"),
|
||||||
@ -72,7 +73,7 @@ class SaveVideo(io.ComfyNode):
|
|||||||
node_id="SaveVideo",
|
node_id="SaveVideo",
|
||||||
search_aliases=["export video"],
|
search_aliases=["export video"],
|
||||||
display_name="Save Video",
|
display_name="Save Video",
|
||||||
category="image/video",
|
category="video",
|
||||||
essentials_category="Basics",
|
essentials_category="Basics",
|
||||||
description="Saves the input images to your ComfyUI output directory.",
|
description="Saves the input images to your ComfyUI output directory.",
|
||||||
inputs=[
|
inputs=[
|
||||||
@ -121,7 +122,7 @@ class CreateVideo(io.ComfyNode):
|
|||||||
node_id="CreateVideo",
|
node_id="CreateVideo",
|
||||||
search_aliases=["images to video"],
|
search_aliases=["images to video"],
|
||||||
display_name="Create Video",
|
display_name="Create Video",
|
||||||
category="image/video",
|
category="video",
|
||||||
description="Create a video from images.",
|
description="Create a video from images.",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input("images", tooltip="The images to create a video from."),
|
io.Image.Input("images", tooltip="The images to create a video from."),
|
||||||
@ -146,7 +147,7 @@ class GetVideoComponents(io.ComfyNode):
|
|||||||
node_id="GetVideoComponents",
|
node_id="GetVideoComponents",
|
||||||
search_aliases=["extract frames", "split video", "video to images", "demux"],
|
search_aliases=["extract frames", "split video", "video to images", "demux"],
|
||||||
display_name="Get Video Components",
|
display_name="Get Video Components",
|
||||||
category="image/video",
|
category="video",
|
||||||
description="Extracts all components from a video: frames, audio, and framerate.",
|
description="Extracts all components from a video: frames, audio, and framerate.",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Video.Input("video", tooltip="The video to extract components from."),
|
io.Video.Input("video", tooltip="The video to extract components from."),
|
||||||
@ -174,7 +175,7 @@ class LoadVideo(io.ComfyNode):
|
|||||||
node_id="LoadVideo",
|
node_id="LoadVideo",
|
||||||
search_aliases=["import video", "open video", "video file"],
|
search_aliases=["import video", "open video", "video file"],
|
||||||
display_name="Load Video",
|
display_name="Load Video",
|
||||||
category="image/video",
|
category="video",
|
||||||
essentials_category="Basics",
|
essentials_category="Basics",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Combo.Input("file", options=sorted(files), upload=io.UploadType.video),
|
io.Combo.Input("file", options=sorted(files), upload=io.UploadType.video),
|
||||||
@ -216,7 +217,7 @@ class VideoSlice(io.ComfyNode):
|
|||||||
"frame load cap",
|
"frame load cap",
|
||||||
"start time",
|
"start time",
|
||||||
],
|
],
|
||||||
category="image/video",
|
category="video",
|
||||||
essentials_category="Video Tools",
|
essentials_category="Video Tools",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Video.Input("video"),
|
io.Video.Input("video"),
|
||||||
|
|||||||
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 CLI_FEATURE_FLAG_REGISTRY
|
||||||
|
print(json.dumps(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
|
||||||
setup_logger(log_level=args.verbose, use_stdout=args.log_stdout)
|
setup_logger(log_level=args.verbose, use_stdout=args.log_stdout)
|
||||||
|
|
||||||
|
|||||||
25
nodes.py
25
nodes.py
@ -1887,7 +1887,7 @@ class ImageInvert:
|
|||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
FUNCTION = "invert"
|
FUNCTION = "invert"
|
||||||
|
|
||||||
CATEGORY = "image"
|
CATEGORY = "image/color"
|
||||||
|
|
||||||
def invert(self, image):
|
def invert(self, image):
|
||||||
s = 1.0 - image
|
s = 1.0 - image
|
||||||
@ -1903,7 +1903,7 @@ class ImageBatch:
|
|||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
FUNCTION = "batch"
|
FUNCTION = "batch"
|
||||||
|
|
||||||
CATEGORY = "image"
|
CATEGORY = "image/batch"
|
||||||
DEPRECATED = True
|
DEPRECATED = True
|
||||||
|
|
||||||
def batch(self, image1, image2):
|
def batch(self, image1, image2):
|
||||||
@ -1960,7 +1960,7 @@ class ImagePadForOutpaint:
|
|||||||
RETURN_TYPES = ("IMAGE", "MASK")
|
RETURN_TYPES = ("IMAGE", "MASK")
|
||||||
FUNCTION = "expand_image"
|
FUNCTION = "expand_image"
|
||||||
|
|
||||||
CATEGORY = "image"
|
CATEGORY = "image/transform"
|
||||||
|
|
||||||
def expand_image(self, image, left, top, right, bottom, feathering):
|
def expand_image(self, image, left, top, right, bottom, feathering):
|
||||||
d1, d2, d3, d4 = image.size()
|
d1, d2, d3, d4 = image.size()
|
||||||
@ -2103,7 +2103,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"ConditioningSetArea": "Conditioning (Set Area)",
|
"ConditioningSetArea": "Conditioning (Set Area)",
|
||||||
"ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
|
"ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
|
||||||
"ConditioningSetMask": "Conditioning (Set Mask)",
|
"ConditioningSetMask": "Conditioning (Set Mask)",
|
||||||
"ControlNetApply": "Apply ControlNet (OLD)",
|
"ControlNetApply": "Apply ControlNet (DEPRECATED)",
|
||||||
"ControlNetApplyAdvanced": "Apply ControlNet",
|
"ControlNetApplyAdvanced": "Apply ControlNet",
|
||||||
# Latent
|
# Latent
|
||||||
"VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
|
"VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
|
||||||
@ -2121,6 +2121,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"LatentFromBatch" : "Latent From Batch",
|
"LatentFromBatch" : "Latent From Batch",
|
||||||
"RepeatLatentBatch": "Repeat Latent Batch",
|
"RepeatLatentBatch": "Repeat Latent Batch",
|
||||||
# Image
|
# Image
|
||||||
|
"EmptyImage": "Empty Image",
|
||||||
"SaveImage": "Save Image",
|
"SaveImage": "Save Image",
|
||||||
"PreviewImage": "Preview Image",
|
"PreviewImage": "Preview Image",
|
||||||
"LoadImage": "Load Image",
|
"LoadImage": "Load Image",
|
||||||
@ -2128,15 +2129,15 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"LoadImageOutput": "Load Image (from Outputs)",
|
"LoadImageOutput": "Load Image (from Outputs)",
|
||||||
"ImageScale": "Upscale Image",
|
"ImageScale": "Upscale Image",
|
||||||
"ImageScaleBy": "Upscale Image By",
|
"ImageScaleBy": "Upscale Image By",
|
||||||
"ImageInvert": "Invert Image",
|
"ImageInvert": "Invert Image Colors",
|
||||||
"ImagePadForOutpaint": "Pad Image for Outpainting",
|
"ImagePadForOutpaint": "Pad Image for Outpainting",
|
||||||
"ImageBatch": "Batch Images",
|
"ImageBatch": "Batch Images (DEPRECATED)",
|
||||||
"ImageCrop": "Image Crop",
|
"ImageCrop": "Crop Image",
|
||||||
"ImageStitch": "Image Stitch",
|
"ImageStitch": "Stitch Images",
|
||||||
"ImageBlend": "Image Blend",
|
"ImageBlend": "Blend Images",
|
||||||
"ImageBlur": "Image Blur",
|
"ImageBlur": "Blur Image",
|
||||||
"ImageQuantize": "Image Quantize",
|
"ImageQuantize": "Quantize Image",
|
||||||
"ImageSharpen": "Image Sharpen",
|
"ImageSharpen": "Sharpen Image",
|
||||||
"ImageScaleToTotalPixels": "Scale Image to Total Pixels",
|
"ImageScaleToTotalPixels": "Scale Image to Total Pixels",
|
||||||
"GetImageSize": "Get Image Size",
|
"GetImageSize": "Get Image Size",
|
||||||
# _for_testing
|
# _for_testing
|
||||||
|
|||||||
40
openapi.yaml
40
openapi.yaml
@ -1999,6 +1999,26 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: List of node IDs to execute (partial graph execution)
|
description: List of node IDs to execute (partial graph execution)
|
||||||
|
workflow_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
x-runtime: [cloud]
|
||||||
|
description: |
|
||||||
|
UUID identifying a hosted-cloud workflow entity to associate with this
|
||||||
|
job. Local ComfyUI doesn't track workflow entities and returns `null`
|
||||||
|
(or omits the field). The `x-runtime: [cloud]` extension marks this
|
||||||
|
as populated only by the hosted-cloud runtime; absence of the tag
|
||||||
|
means a field is populated by all runtimes.
|
||||||
|
workflow_version_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
x-runtime: [cloud]
|
||||||
|
description: |
|
||||||
|
UUID identifying a hosted-cloud workflow version to associate with
|
||||||
|
this job. Local ComfyUI returns `null` (or omits the field). See
|
||||||
|
`workflow_id` above for `x-runtime` semantics.
|
||||||
|
|
||||||
PromptResponse:
|
PromptResponse:
|
||||||
type: object
|
type: object
|
||||||
@ -2347,7 +2367,12 @@ components:
|
|||||||
description: Device type (cuda, mps, cpu, etc.)
|
description: Device type (cuda, mps, cpu, etc.)
|
||||||
index:
|
index:
|
||||||
type: number
|
type: number
|
||||||
description: Device index
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Device index within its type (e.g. CUDA ordinal for `cuda:0`,
|
||||||
|
`cuda:1`). `null` for devices with no index, including the CPU
|
||||||
|
device returned in `--cpu` mode (PyTorch's `torch.device('cpu').index`
|
||||||
|
is `None`).
|
||||||
vram_total:
|
vram_total:
|
||||||
type: number
|
type: number
|
||||||
description: Total VRAM in bytes
|
description: Total VRAM in bytes
|
||||||
@ -2503,7 +2528,18 @@ components:
|
|||||||
description: Alternative search terms for finding this node
|
description: Alternative search terms for finding this node
|
||||||
essentials_category:
|
essentials_category:
|
||||||
type: string
|
type: string
|
||||||
description: Category override used by the essentials pack
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Category override used by the essentials pack. The
|
||||||
|
`essentials_category` key may be present with a string value,
|
||||||
|
present and `null`, or absent entirely:
|
||||||
|
|
||||||
|
- V1 nodes: `essentials_category` is **omitted** when the node
|
||||||
|
class doesn't define an `ESSENTIALS_CATEGORY` attribute, and
|
||||||
|
**`null`** if the attribute is explicitly set to `None`.
|
||||||
|
- V3 nodes (`comfy_api.latest.io`): `essentials_category` is
|
||||||
|
**always present**, and **`null`** for nodes whose `Schema`
|
||||||
|
doesn't populate it.
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# Models
|
# Models
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
"""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,
|
||||||
get_server_features,
|
get_server_features,
|
||||||
|
CLI_FEATURE_FLAG_REGISTRY,
|
||||||
SERVER_FEATURE_FLAGS,
|
SERVER_FEATURE_FLAGS,
|
||||||
|
_coerce_flag_value,
|
||||||
|
_parse_cli_feature_flags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -96,3 +101,83 @@ 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"
|
||||||
|
|
||||||
|
def test_bool_typo_raises(self):
|
||||||
|
"""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(
|
||||||
|
CLI_FEATURE_FLAG_REGISTRY,
|
||||||
|
"test_int_flag",
|
||||||
|
{"type": "int", "default": 0, "description": "test"},
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_coerce_flag_value("test_int_flag", "not_a_number")
|
||||||
|
|
||||||
|
|
||||||
|
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_defaults_to_true(self, monkeypatch):
|
||||||
|
"""Bare flag without '=' is treated as the string 'true' (and coerced if registered)."""
|
||||||
|
monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["show_signin_button", "valid=1"]})())
|
||||||
|
result = _parse_cli_feature_flags()
|
||||||
|
assert result == {"show_signin_button": True, "valid": "1"}
|
||||||
|
|
||||||
|
def test_empty_key_skipped(self, monkeypatch):
|
||||||
|
monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["=value", "valid=1"]})())
|
||||||
|
result = _parse_cli_feature_flags()
|
||||||
|
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:
|
||||||
|
"""Test suite for the CLI feature flag registry."""
|
||||||
|
|
||||||
|
def test_registry_entries_have_required_fields(self):
|
||||||
|
for key, info in 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