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
69 lines
3.2 KiB
Python
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
|