Skip to content

Commit

Permalink
* Improved wildcard definition support.
Browse files Browse the repository at this point in the history
  • Loading branch information
acorderob committed Oct 19, 2024
1 parent a18e5e6 commit e9092b8
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 40 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ __parameters$$wildcard'filter'(var=value)__

The parameters, the filter, and the setting of a variable are optional. The parameters follow the same format as for the choices.

The wildcard identifier can contain globbing formatting, to read multiple wildcards and merge their choices. Note that if there are no parameters specified, the globbing will use the ones from the first wildcard that matches and have parameters (sorted by keys), so if you don't want that you might want to specify them.
The wildcard identifier can contain globbing formatting, to read multiple wildcards and merge their choices. Note that if there are no parameters specified, the globbing will use the ones from the first wildcard that matches and have parameters (sorted by keys), so if you don't want that you might want to specify them. Also note that, unlike with Dynamic Prompts, the wildcard name has to be specified with its full path (unless you use globbing).

The filter can be used to filter specific choices from the wildcard. The filtering works before applying the choice conditions (if any). The surrounding quotes can be single or double. The filter is a comma separated list of an integer (positional choice index) or choice label. You can also compound them with "+". That is, the comma separated items act as an OR and the "+" inside them as an AND. Using labels can simplify the definitions of complex wildcards where you want to have direct access to specific choices on occasion (you don't need to create wildcards for each individual choice). There are some additional formats when using filters. You can specify "^wildcard" as a filter to use the filter of a previous wildcard in the chain. You can start the filter (regular or inherited) with "#" and it will not be applied to the current wildcard choices, but the filter will remain in memory to use by other descendant wildcards. You use "#" and "^" when you want to pass a filter to inner wildcards (see the test files).

Expand All @@ -153,8 +153,10 @@ __path/wildcard(var=value)__ # select 1 choice using the specified variabl

A wildcard definition can be:

* A txt file. The wildcard name will be the relative path of the file, without the extension. Each line will be a choice. Lines starting with "#" or empty are ignored.
* An array or string inside a json or yaml file. The wildcard name includes the relative folder path of the file (without the name or extension) but also the path of the object inside the file.
* A txt file. The wildcard name will be the relative path of the file, without the extension. Each line will be a choice. Lines starting with "#" or empty are ignored. Doesn't support nesting.
* An array or scalar value inside a json or yaml file. The wildcard name includes the relative folder path of the file, without the extension, but also the path of the value inside the file (if there is one). If the file contains a dictionary, the filename part is not used for the wildcard name. Supports nesting by having dictionaries inside dictionaries.

The best format is a yaml file with a dictionary of wildcards inside. An editor supporting yaml syntax is recommended.

In a choice, the content after a "#" is ignored.

Expand Down
100 changes: 63 additions & 37 deletions ppp_wildcards.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import json
from typing import Optional
import logging
import yaml

from ppp_logging import DEBUG_LEVEL
Expand All @@ -22,7 +23,7 @@ class PPPWildcards:
DEFAULT_WILDCARDS_FOLDER = "wildcards"

def __init__(self, logger):
self.logger = logger
self.logger: logging.Logger = logger
self.debug_level = DEBUG_LEVEL.none
self.wildcards_folders = []
self.wildcards: dict[str, PPPWildcard] = {}
Expand Down Expand Up @@ -126,62 +127,87 @@ def __get_wildcards_in_file(self, base, full_path: str):
if last_modified_cached is not None and last_modified == self.wildcard_files[full_path]:
return
filename = os.path.basename(full_path)
name, extension = os.path.splitext(filename)
_, extension = os.path.splitext(filename)
if extension not in (".txt", ".json", ".yaml", ".yml"):
return
self.__remove_wildcards_from_file(full_path, False)
if last_modified_cached is not None and self.debug_level != DEBUG_LEVEL.none:
self.logger.debug(f"Updating wildcards from file: {full_path}")
relfolders = os.path.relpath(os.path.dirname(full_path), base)
if relfolders == ".":
relfolders = ""
elif relfolders != "":
relfolders += "/"
if extension == ".txt":
self.__get_wildcards_in_text_file(full_path, relfolders, name)
self.__get_wildcards_in_text_file(full_path, base)
elif extension in (".json", ".yaml", ".yml"):
self.__get_wildcards_in_structured_file(full_path, relfolders, extension)
self.__get_wildcards_in_structured_file(full_path, base)
self.wildcard_files[full_path] = last_modified

def __get_wildcards_in_structured_file(self, full_path, relfolders, extension):
def __get_choices(self, obj: object) -> list[dict]:
choices = None
if obj is not None:
if isinstance(obj, (str, dict)):
choices = [obj]
elif isinstance(obj, (int, float, bool)):
choices = [str(obj)]
elif isinstance(obj, list) and len(obj) > 0:
choices = []
for c in obj:
if isinstance(c, (str, dict)):
choices.append(c)
elif isinstance(c, (int, float, bool)):
choices.append(str(c))
return choices

def __get_wildcards_in_structured_file(self, full_path, base):
external_key: str = os.path.relpath(os.path.splitext(full_path)[0], base)
external_key_parts = external_key.split(os.sep)
_, extension = os.path.splitext(full_path)
with open(full_path, "r", encoding="utf-8") as file:
if extension == ".json":
content = json.loads(file.read())
else:
content = yaml.safe_load(file)
keys = self.__get_keys_in_dict(content)
for key in keys:
fullkey = f"{relfolders}{key}"
if self.wildcards.get(fullkey, None) is not None:
self.logger.warning(
f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!"
)
else:
obj = self.__get_nested(content, key)
choices = []
if obj is not None:
if isinstance(obj, (str, dict)):
choices = [obj]
elif isinstance(obj, (int, float, bool)):
choices = [str(obj)]
elif isinstance(obj, list) and len(obj) > 0:
choices = []
for c in obj:
if isinstance(c, (str, dict)):
choices.append(c)
else:
obj = None
if obj is None:
self.logger.warning(f"Invalid wildcard '{fullkey}' in file '{full_path}'!")
if isinstance(content, dict):
external_key_parts = external_key_parts[:-1]
keys = self.__get_keys_in_dict(content)
for key in keys:
fullkey = "/".join(external_key_parts + [key])
if self.wildcards.get(fullkey, None) is not None:
self.logger.warning(
f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!"
)
else:
self.wildcards[fullkey] = PPPWildcard(full_path, fullkey, choices)
obj = self.__get_nested(content, key)
choices = self.__get_choices(obj)
if choices is None:
self.logger.warning(f"Invalid wildcard '{fullkey}' in file '{full_path}'!")
else:
self.wildcards[fullkey] = PPPWildcard(full_path, fullkey, choices)
return
if isinstance(content, str):
content = [content]
elif isinstance(content, (int, float, bool)):
content = [str(content)]
if not isinstance(content, list):
self.logger.warning(f"Invalid wildcard in file '{full_path}'!")
return
fullkey = "/".join(external_key_parts)
if self.wildcards.get(fullkey, None) is not None:
self.logger.warning(
f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!"
)
else:
choices = self.__get_choices(content)
if choices is None:
self.logger.warning(f"Invalid wildcard '{fullkey}' in file '{full_path}'!")
else:
self.wildcards[fullkey] = PPPWildcard(full_path, fullkey, choices)

def __get_wildcards_in_text_file(self, full_path, relfolders, name):
def __get_wildcards_in_text_file(self, full_path, base):
external_key: str = os.path.relpath(os.path.splitext(full_path)[0], base)
external_key_parts = external_key.split(os.sep)
with open(full_path, "r", encoding="utf-8") as file:
text_content = map(lambda x: x.strip("\n\r"), file.readlines())
text_content = list(filter(lambda x: x.strip() != "" and not x.strip().startswith("#"), text_content))
text_content = [x.split("#")[0].rstrip() if len(x.split("#")) > 1 else x for x in text_content]
fullkey = f"{relfolders}{name}"
fullkey = "/".join(external_key_parts)
if self.wildcards.get(fullkey, None) is not None:
self.logger.warning(
f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!"
Expand Down
5 changes: 5 additions & 0 deletions tests/wildcards/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ yaml:
- { 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__
- one
- two
3 changes: 3 additions & 0 deletions tests/wildcards/testwc/test2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- one
- 2
- three
1 change: 1 addition & 0 deletions tests/wildcards/testwc/test3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
one choice

0 comments on commit e9092b8

Please sign in to comment.