* Added container property to wildcard definitions.

This commit is contained in:
Antonio Cordero Balcazar
2026-05-05 16:29:21 +02:00
parent fcf577ea50
commit b03a522ed4
5 changed files with 110 additions and 43 deletions
+16 -3
View File
@@ -134,23 +134,36 @@ The best format is a yaml file with a dictionary of wildcards inside. An editor
In a choice, the content after a `#` is ignored.
If the first choice follows the format of wildcard parameters (*including the final `$$`*), it will be used as default parameters for that wildcard (see examples in the tests folder).
If the first choice follows the format of wildcard parameters (*including the final `$$`*), it will be used as default parameters for that wildcard (see examples in the tests folder). Unless the only property used is the wildcard description.
The choices of the wildcard follow the same format as in the choices construct, or the object format of *Dynamic Prompts* (only in structured files). If using the object format for a choice you can use a new `if` property for the condition, and the `labels` property (an array of strings) and `command` property (a boolean) in addition to the standard `weight` and `text`/`content`.
The choices of the wildcard follow the same format as in the choices construct, or the object format of *Dynamic Prompts* (only in structured files).
If using the object format for a choice you can use the following in addition to the standard `weight` and `text`/`content`:
* `if`: the condition (a string)
* `labels`: list of labels (an array of strings)
* `command`: indicates the content is a command (a boolean)
```yaml
{ command: false, labels: ["some_label"], weight: 2, if: "_is_pony", content: "the text" } # "text" property can be used instead of "content"
```
Wildcard parameters in a json/yaml file can also be in object format, and support two additional properties, prefix and suffix:
Wildcard parameters in a json/yaml file can also be in object format, and support some additional properties, that are not included in the string format:
* `prefix`: content to prefix the list of choices
* `suffix`: content to suffix the list of choices
* `container`: includes the prefix, choices array variable, and suffix
```yaml
{ sampler: "~", repeating: false, optional: false, from: 2, to: 3, description: "test wildcard", container: "prefix-${_choices[&'/']}-suffix" }
{ sampler: "~", repeating: false, optional: false, count: 2, description: "test wildcard", prefix: "prefix-", suffix: "-suffix", separator: "/" }
{ sampler: "~", repeating: false, optional: false, from: 2, to: 3, description: "test wildcard", prefix: "prefix-", suffix: "-suffix", separator: "/" }
```
The prefix and suffix are added to the result along with the selected choices and separators. They can contain other constructs, but the separator can't.
The container is a new option that replaces prefix/suffix/separator, and makes use of the recent support for array variables. Its value would be the concatenation of any prefix and/or suffix with the echoing (with the chosen separator) of a temporary `_choices[]` variable that holds the chosen values. This property is preferred over prefix/suffix/separator unless you only need the separator.
It is recommended to use the object format for the wildcard parameters and for choices with complex options.
If your first choice is interpreted as parameters, and you don't need parameters, you can avoid the problem by adding an empty parameters object `{}` as first choice.
+78 -40
View File
@@ -1231,6 +1231,11 @@ class TreeProcessor(lark.visitors.Interpreter):
f"Invalid use of starred value for '{escape_single_quotes(variable_name)}'! Starred values can only be assigned or added to unindexed array variables."
)
newvalue = ""
elif not isinstance(newvalue, list):
self.warn_or_stop(
f"Invalid starred value for '{escape_single_quotes(variable_name)}'! Starred values should be a list."
)
newvalue = ""
if is_array:
if variable_specifier is not None:
# Accessing an existing index, we need to update the array
@@ -1644,7 +1649,7 @@ class TreeProcessor(lark.visitors.Interpreter):
choice_values: list[dict],
filter_specifier: Optional[list[list[str]]] = None,
wildcard_key: str = None,
) -> tuple[str, list[str], str, str]:
) -> tuple[lark.Tree, list[str]]:
"""
Select choices based on the options.
@@ -1655,7 +1660,7 @@ class TreeProcessor(lark.visitors.Interpreter):
wildcard_key (str): The wildcard key if it is a wildcard.
Returns:
tuple: A tuple containing the prefix, selected choices, separator and suffix
tuple[lark.Tree,list[str]]: The resulting container and list of chosen choices.
"""
seen_wildcards_len = len(self.__seen_wildcards)
if options is None:
@@ -1772,13 +1777,6 @@ class TreeProcessor(lark.visitors.Interpreter):
if self.state.options.keep_choices_order:
selected_choices = sorted(selected_choices, key=lambda x: x["choice_index"])
selected_choices_text = []
prefix: str = (
self.__visit(options.get("prefix", None), False, True)
if options.get("prefix", None) is not None
else ""
)
if prefix != "" and re.match(r"\w", prefix[-1]):
prefix += " "
for i, c in enumerate(selected_choices):
t1 = time.monotonic_ns()
choice_content_obj = c.get("content", c.get("text", None))
@@ -1793,26 +1791,55 @@ class TreeProcessor(lark.visitors.Interpreter):
+ textwrap.indent(re.sub(r"\n$", "", choice_content), " "),
)
selected_choices_text.append(choice_content)
suffix: str = (
self.__visit(options.get("suffix", None), False, True)
if options.get("suffix", None) is not None
else ""
)
if suffix != "" and re.match(r"\w", suffix[0]):
suffix = " " + suffix
# remove comments
results = [re.sub(r"\s*#[^\n]*(?:\n|$)", "", r, flags=re.DOTALL) for r in selected_choices_text]
else:
prefix = ""
suffix = ""
results = []
container = options.get("container", None)
if container is None:
separator = options.get("separator", self.state.options.choice_separator)
container = lark.Tree(
lark.Token("RULE", "content"),
[
lark.Tree(
lark.Token("RULE", "variableuse"),
[
lark.Tree(
lark.Token("RULE", "vardescriptor_get"),
[
lark.Token("__identifier", "_choices"),
lark.Token("__openbracket", "["),
lark.Tree(
lark.Token("RULE", "separator_descriptor"),
[
lark.Token("__separatorflag", "&"),
lark.Token("STRING", "'" + separator + "'"),
],
),
lark.Token("__closebracket", "]"),
],
),
None,
],
),
],
)
self.log(
logging.DEBUG,
"Unseen wildcards: "
+ ", ".join([f"'{escape_single_quotes(x)}'" for x in self.__seen_wildcards[seen_wildcards_len:]]),
)
self.__seen_wildcards = self.__seen_wildcards[:seen_wildcards_len]
return (prefix, results, separator, suffix)
return container, results
def __apply_container(self, container: lark.Tree, choices: list[str]) -> str:
# we save the choices variable in case there are nested choices
old_choices = self.state.variables.get_system("_choices[]", None)
self.state.variables.set_system("_choices[]", choices)
joined_results = self.__visit(container, False, True)
# we restore the old choices variable
self.state.variables.set_system("_choices[]", old_choices)
return joined_results
def __convert_choices_options(self, options: Optional[lark.Tree], is_wcdef: bool = False) -> dict:
"""
@@ -1967,26 +1994,38 @@ class TreeProcessor(lark.visitors.Interpreter):
if isinstance(wildcard.unprocessed_choices[0], dict):
if self.state.wildcards_obj.is_dict_wcdef_options(wildcard.unprocessed_choices[0]):
options = wildcard.unprocessed_choices[0]
prefix = options.get("prefix", None)
if prefix is not None and isinstance(prefix, str):
container = options.get("container", None)
container_kind = "specified"
if container is None or not isinstance(container, str):
container_kind = "assembled"
has_separator = "separator" in options
has_prefix = "prefix" in options
has_suffix = "suffix" in options
if has_separator or has_prefix or has_suffix:
separator = options.get("separator", self.state.options.choice_separator)
container = "${_choices[&'" + separator + "']}"
prefix = options.get("prefix", None)
if prefix is not None and isinstance(prefix, str):
if prefix != "" and re.match(r"\w", prefix[-1]):
prefix += " "
container = prefix + container
suffix = options.get("suffix", None)
if suffix is not None and isinstance(suffix, str):
if suffix != "" and re.match(r"\w", suffix[0]):
suffix = " " + suffix
container += suffix
options.pop("separator", None)
options.pop("prefix", None)
options.pop("suffix", None)
options.pop("container", None)
if container is not None:
try:
options["prefix"] = parse_prompt(
self.state, "choicevalue", prefix, self.state.parsers["choicevalue"], True
options["container"] = parse_prompt(
self.state, "choicevalue", container, self.state.parsers["choicevalue"], True
)
except lark.exceptions.UnexpectedInput as e:
self.warn_or_stop(
f"Error parsing choice prefix '{escape_single_quotes(prefix)}' in wildcard '{escape_single_quotes(wildcard.key)}'! : {e.__class__.__name__}",
e,
)
suffix = options.get("suffix", None)
if suffix is not None and isinstance(suffix, str):
try:
options["suffix"] = parse_prompt(
self.state, "choicevalue", suffix, self.state.parsers["choicevalue"], True
)
except lark.exceptions.UnexpectedInput as e:
self.warn_or_stop(
f"Error parsing choice suffix '{escape_single_quotes(suffix)}' in wildcard '{escape_single_quotes(wildcard.key)}'! : {e.__class__.__name__}",
f"Error parsing choice {container_kind} container '{escape_single_quotes(container)}' in wildcard '{escape_single_quotes(wildcard.key)}'! : {e.__class__.__name__}",
e,
)
n = 1
@@ -2110,11 +2149,11 @@ class TreeProcessor(lark.visitors.Interpreter):
logging.DEBUG, f"Options for wildcard '{escape_single_quotes(wildcard.key)}' are ignored!"
)
choice_values_all += choice_values
prefix, chosen_choices, separator, suffix = self.__get_choices_select(
container, chosen_choices = self.__get_choices_select(
applied_options, choice_values_all, filter_specifier, wildcard_key
)
if chosen_choices:
self.__result += prefix + separator.join(chosen_choices) + suffix
self.__result += self.__apply_container(container, chosen_choices)
if wildcard_key in self.__wildcard_filters:
del self.__wildcard_filters[wildcard_key]
if variablename is not None:
@@ -2181,9 +2220,8 @@ class TreeProcessor(lark.visitors.Interpreter):
ch = self.__get_original_node_content(tree, "?{...}")
if self.state.options.process_wildcards:
self.log(logging.DEBUG, "Processing choices:")
prefix, chosen_choices, separator, suffix = self.__get_choices_select(options, choice_values)
if chosen_choices:
self.__result += prefix + separator.join(chosen_choices) + suffix
container, chosen_choices = self.__get_choices_select(options, choice_values)
self.__result += self.__apply_container(container, chosen_choices)
elif self.state.options.if_wildcards != IFWILDCARDS_CHOICES.remove:
self.__detectedWildcards.append((ch, self.__is_negative))
self.__result += ch
+1
View File
@@ -261,6 +261,7 @@ class PPPWildcards:
"to",
"prefix",
"suffix",
"container",
"description",
"separator",
]
+6
View File
@@ -389,6 +389,12 @@ class TestWildcards(TestPromptPostProcessorBase):
OutputTuple("the choices are: [(prefix2-choice2-suffix:1.5)]", ""),
)
def test_wc_wildcardContainer_yaml(self): # yaml wildcard with object formatted choices and options and container
self.process(
InputTuple("the choices are: [__yaml/wildcardContainer__]", ""),
OutputTuple("the choices are: [(prefix1-choice2/choice3-suffix:1.5)]", ""),
)
def test_wc_wildcardAt_yaml(self): # yaml wildcard with attention in choices
self.process(
InputTuple("the choices are: [__yaml/wildcardAt__]", ""),
+9
View File
@@ -64,6 +64,15 @@ yaml:
- { text: choice1 }
- { text: choice2 }
- { text: choice3 }
wildcardContainer:
- {
count: 2,
container: "(prefix{1|2}-${_choices[&'/']}-suffix:1.5)",
}
- { weight: 3, text: choice1 }
- { weight: 2, text: choice2 }
- { weight: 1, text: choice3 }
more_nested:
even_more_nested: # this would be __yaml/more_nested/even_more_nested__