Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

69 lines
3.2 KiB
Python

from __future__ import annotations
from collections.abc import Iterator
from dynamicprompts.commands import (
Command,
SequenceCommand,
VariantCommand,
WildcardCommand,
WrapCommand,
)
from dynamicprompts.parser.parse import parse
from dynamicprompts.wildcards import WildcardManager
from pyparsing import ParseException
def _iter_wildcard_names(command: Command, in_variant: bool = False) -> Iterator[str]:
"""Recursively yield the statically-known wildcard names that appear as (part of) a variant value.
Only wildcards reachable from a `VariantCommand` value are yielded: those are the references that
hang the combinatorial generator (see `find_missing_wildcards`). The same wildcard used as plain
literal text (e.g. `a __nope__ b`) generates fine, so it is intentionally not reported.
"""
if isinstance(command, WildcardCommand):
# The wildcard name may itself be a dynamic Command (e.g. `__${var}__`). Only plain string
# names can be validated ahead of time, so the dynamic case is intentionally skipped.
if in_variant and isinstance(command.wildcard, str):
yield command.wildcard
elif isinstance(command, SequenceCommand):
for token in command.tokens:
yield from _iter_wildcard_names(token, in_variant)
elif isinstance(command, VariantCommand):
# Everything below a variant value is a variant-nested reference, even across sequences.
for value in command.values:
yield from _iter_wildcard_names(value, in_variant=True)
elif isinstance(command, WrapCommand):
yield from _iter_wildcard_names(command.wrapper, in_variant)
yield from _iter_wildcard_names(command.inner, in_variant)
# LiteralCommand and variable commands reference no wildcards we can resolve statically.
def find_missing_wildcards(prompt: str, wildcard_manager: WildcardManager | None = None) -> list[str]:
"""Return the unique unknown wildcard names in `prompt` that hang the combinatorial generator.
Referencing an unknown wildcard *as a variant value* (e.g. `{__nope__|x}`) makes dynamicprompts'
combinatorial generator loop forever: its not-found fallback (`get_wildcard_not_found_fallback`)
yields the wrapped wildcard infinitely, and the combinatorial variant logic dedupes those
duplicates away without ever advancing. Detecting these names up front lets callers report a clear
error instead of hanging. Only the combinatorial generator is affected, and only for wildcards
nested in a variant — a bare `a __nope__ b` generates fine and is not reported.
Without a configured `wildcard_manager`, an empty one is used so that every referenced wildcard is
treated as missing (wildcards are not resolved against any files here).
"""
if wildcard_manager is None:
wildcard_manager = WildcardManager()
try:
tree = parse(prompt)
except ParseException:
# Malformed prompts are surfaced separately by the generators; nothing to validate here.
return []
missing: list[str] = []
for name in _iter_wildcard_names(tree):
if name not in missing and not wildcard_manager.get_values(name):
missing.append(name)
return missing