# Copyright 2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """`diffusers-cli schema` — print the input schema for any pipeline repo. Tries `DiffusionPipeline.config_name` first (so standard repos get their `__call__` signature introspected); falls back to `ModularPipelineBlocks.from_pretrained` for modular repos. No weights are downloaded — only the small index file (and any custom block code if `--trust-remote-code` is set). """ from __future__ import annotations import inspect import re from argparse import ArgumentParser, Namespace, _SubParsersAction from typing import Any from huggingface_hub.cli._output import OutputFormat, out from ..utils import logging from . import BaseDiffusersCLICommand logger = logging.get_logger("diffusers-cli/schema") def _schema(args: Namespace) -> None: """Print the pipeline's input schema. Tries `DiffusionPipeline.config_name` (= `model_index.json`) first; if present, introspects the declared pipeline class's `__call__` signature. Otherwise falls back to `ModularPipelineBlocks.from_pretrained` and reads the block-declared `inputs`. No weights downloaded either way. """ import diffusers try: index = diffusers.DiffusionPipeline.load_config(args.model, token=args.token, revision=args.revision) except OSError: index = None if index is not None: class_name = index.get("_class_name") if class_name is None: raise SystemExit( f"{diffusers.DiffusionPipeline.config_name} for {args.model!r} has no `_class_name` field." ) pipeline_cls = getattr(diffusers, class_name, None) if pipeline_cls is None: raise SystemExit( f"Pipeline class {class_name!r} declared in {diffusers.DiffusionPipeline.config_name} " "is not exported by the installed diffusers." ) sig = inspect.signature(pipeline_cls.__call__) descriptions = _parse_docstring_args(pipeline_cls.__call__.__doc__) if args.verbose else {} schema: list[dict[str, Any]] = [] for name, param in sig.parameters.items(): if name == "self": continue if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): continue has_default = param.default is not inspect.Parameter.empty schema.append( { "name": name, "type_hint": str(param.annotation) if param.annotation is not inspect.Parameter.empty else None, "default": param.default if has_default else None, "required": not has_default, "description": descriptions.get(name, ""), } ) else: kwargs: dict[str, Any] = {"trust_remote_code": args.trust_remote_code} if args.revision: kwargs["revision"] = args.revision if args.token: kwargs["token"] = args.token # If the repo declares custom code + external dependencies, surface them upfront so # the user knows what to install before we hit an ImportError inside from_pretrained. _warn_custom_block_requirements(args) try: blocks = diffusers.ModularPipelineBlocks.from_pretrained(args.model, **kwargs) except Exception as e: hint = "\nPass --trust-remote-code if it ships custom block code." if not args.trust_remote_code else "" raise SystemExit( f"Could not read schema for {args.model!r}: no {diffusers.DiffusionPipeline.config_name} and " f"loading as a modular pipeline failed with:\n {type(e).__name__}: {e}{hint}" ) from e class_name = type(blocks).__name__ schema = [ { "name": p.name, "type_hint": str(p.type_hint) if p.type_hint is not None else None, "default": p.default, "required": p.required, "description": p.description, } for p in blocks.inputs ] if out.mode == OutputFormat.json: out.dict({"task": "schema", "model": args.model, "pipeline_class": class_name, "inputs": schema}) elif out.mode == OutputFormat.agent: out.table(schema, headers=["name", "required", "type_hint", "default", "description"]) else: out.text(f"{class_name} ({args.model}) inputs:") for entry in schema: tag = "required" if entry["required"] else f"optional, default={entry['default']!r}" out.text(f" {entry['name']} ({tag})") if entry["type_hint"]: out.text(f" type: {entry['type_hint']}") if entry["description"]: out.text(f" desc: {entry['description']}") def _warn_custom_block_requirements(args: Namespace) -> None: """Warn upfront when a modular block ships custom code with declared external dependencies. Reads `modular_config.json` if present; if it has an `auto_map` (custom code) and a non-empty `requirements` list/dict, prints a heads-up. `from_pretrained` will otherwise fail with an `ImportError` deep in the loader stack when a listed dep is missing. """ import diffusers try: config = diffusers.ModularPipelineBlocks.load_config(args.model, token=args.token, revision=args.revision) except Exception: return # no modular_config.json or unreachable — nothing to warn about if not isinstance(config, dict): return if not config.get("auto_map"): return requirements = config.get("requirements") if not requirements: return # `requirements` may be a dict {name: version} or (older repos) a list of [name, version] pairs. if isinstance(requirements, dict): pairs = list(requirements.items()) elif isinstance(requirements, list): pairs = [(item[0], item[1]) for item in requirements if isinstance(item, (list, tuple)) and len(item) >= 2] else: pairs = [] if not pairs: return formatted = ", ".join(f"{name}=={version}" for name, version in pairs) logger.warning( f"{args.model!r} ships custom block code with external dependencies: {formatted}. " "You will need to install these in order to determine the pipeline schema." ) def _parse_docstring_args(docstring: str | None) -> dict[str, str]: """Extract per-argument descriptions from a Google-style `Args:` block. Returns a `{name: description}` mapping. Best-effort — unrecognised formats just yield an empty dict rather than raising. """ if not docstring: return {} lines = docstring.expandtabs().splitlines() start = None section_indent = 0 for i, line in enumerate(lines): if line.strip() in ("Args:", "Arguments:", "Parameters:"): start = i + 1 section_indent = len(line) - len(line.lstrip()) break if start is None: return {} descriptions: dict[str, str] = {} current_name: str | None = None current_lines: list[str] = [] arg_indent: int | None = None name_pattern = re.compile(r"^(\w+)\s*(?:\([^)]*\))?\s*:?\s*(.*)$") def _flush() -> None: if current_name and current_lines: descriptions[current_name] = " ".join(s.strip() for s in current_lines).strip() for line in lines[start:]: if not line.strip(): continue indent = len(line) - len(line.lstrip()) # A new top-level section ends the Args block. if indent <= section_indent and line.strip().endswith(":"): break if arg_indent is None: arg_indent = indent if indent == arg_indent: _flush() current_lines = [] match = name_pattern.match(line.strip()) if match: current_name = match.group(1) tail = match.group(2).strip() if tail: current_lines.append(tail) else: current_name = None elif current_name is not None and indent > arg_indent: current_lines.append(line.strip()) _flush() return descriptions class SchemaCommand(BaseDiffusersCLICommand): task = "schema" @staticmethod def register_subcommand(subparsers: _SubParsersAction) -> None: from argparse import RawDescriptionHelpFormatter epilog = ( "Examples\n" " $ diffusers-cli schema -m stabilityai/stable-diffusion-xl-base-1.0\n" " $ diffusers-cli schema -m black-forest-labs/FLUX.1-dev --verbose\n" " $ diffusers-cli --format json schema -m stabilityai/stable-diffusion-xl-base-1.0\n" "\n" "Learn more\n" " Use `diffusers-cli --help` for more information about a command.\n" " Read the documentation at https://huggingface.co/docs/diffusers\n" ) parser: ArgumentParser = subparsers.add_parser( "schema", help="Print the input schema for a diffusers pipeline repo. No weights downloaded.", usage="\n diffusers-cli schema [options]", epilog=epilog, formatter_class=RawDescriptionHelpFormatter, ) parser._optionals.title = "Options" parser.add_argument( "--model", "-m", required=True, help="Model id on the Hugging Face Hub or local path.", ) parser.add_argument( "--revision", default=None, help="Model revision (branch, tag, or commit SHA).", ) parser.add_argument( "--token", default=None, help="Hugging Face token for gated/private models.", ) parser.add_argument( "--trust-remote-code", action="store_true", help="Allow custom code from the Hub (required for modular pipelines that ship block code).", ) parser.add_argument( "--verbose", "-v", action="store_true", help=( "Also include per-argument descriptions from the pipeline's __call__ docstring. " "Modular pipelines always include block-declared descriptions; --verbose populates " "the equivalent field for standard pipelines by parsing the Google-style Args: block." ), ) parser.set_defaults(func=SchemaCommand) def __init__(self, args: Namespace): self.args = args def run(self) -> None: _schema(self.args)