is_list_dict() accepts a string value as a list-dict value, and render_values_to_list_dict() passes such values through unchanged. merge_values() then merges them with list extension, and [] += 'xyz' appends the individual characters, so every string value ends up as a list of its characters. Normalize string values to single-element lists in render_values_to_list_dict(), so that merge_values() appends whole values. jw-pkg projects create-pkg-config is hit by this, because all of its values are strings: the generated file contains one line per character. The code path was just never exercised lately. Add unit tests for the value layout guards, the normalization, and the merging of the three supported layouts. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann <jan@janware.com>
178 lines
5 KiB
Python
178 lines
5 KiB
Python
import textwrap
|
|
|
|
from typing import Any, Iterable, TypeAlias, TypeGuard
|
|
|
|
TupleList: TypeAlias = Iterable[tuple[str, str]]
|
|
ListDict: TypeAlias = dict[str, list[str]]
|
|
StrDict: TypeAlias = dict[str, str]
|
|
RenderValues: TypeAlias = ListDict | StrDict | TupleList
|
|
|
|
def is_str_dict(values: object) -> TypeGuard[StrDict]:
|
|
if not isinstance(values, dict):
|
|
return False
|
|
for key, val in values.items():
|
|
if not isinstance(key, str):
|
|
return False
|
|
if not isinstance(val, str):
|
|
return False
|
|
return True
|
|
|
|
def is_list_dict(values: object) -> TypeGuard[ListDict]:
|
|
if not isinstance(values, dict):
|
|
return False
|
|
for key, val in values.items():
|
|
if not isinstance(key, str):
|
|
return False
|
|
if isinstance(val, str):
|
|
continue
|
|
if not isinstance(val, list):
|
|
return False
|
|
for entry in val:
|
|
if not isinstance(entry, str):
|
|
return False
|
|
return True
|
|
|
|
def is_tuple_list(values: object) -> TypeGuard[TupleList]:
|
|
if not isinstance(values, list):
|
|
return False
|
|
for item in values:
|
|
if not isinstance(item, tuple):
|
|
return False
|
|
if not len(item) == 2:
|
|
return False
|
|
if not isinstance(item[0], str):
|
|
return False
|
|
if not isinstance(item[1], str):
|
|
return False
|
|
return True
|
|
|
|
def render_values_to_list_dict(values: object) -> ListDict:
|
|
|
|
def __tuple_list_to_dict(src: TupleList) -> ListDict:
|
|
ret: ListDict = {}
|
|
for key, val in src:
|
|
entry = ret.setdefault(key, [])
|
|
entry.append(val)
|
|
return ret
|
|
|
|
if is_list_dict(values):
|
|
ret: ListDict = {}
|
|
for key, val in values.items():
|
|
# -- A string value is treated as a list with a single element;
|
|
# merging it unconverted would split it into characters
|
|
ret[key] = val if isinstance(val, list) else [val]
|
|
return ret
|
|
if is_tuple_list(values):
|
|
return __tuple_list_to_dict(values)
|
|
raise Exception('Unsupported template value layout')
|
|
|
|
def merge_values(*values: RenderValues) -> ListDict:
|
|
ret: ListDict = {}
|
|
for rhs in values:
|
|
rhs_dict = render_values_to_list_dict(rhs)
|
|
for key, val in rhs_dict.items():
|
|
entry = ret.setdefault(key, [])
|
|
entry += val
|
|
return ret
|
|
|
|
def format_list_dict(
|
|
template: str, values: ListDict | dict[str, str], li_quote: bool, li_delimiter: str
|
|
) -> str:
|
|
|
|
def __format_value(val: Any) -> str:
|
|
if not li_quote:
|
|
return str(val)
|
|
return f'"{val}"'
|
|
|
|
fmt_dict: dict[str, str] = {}
|
|
for key, value in values.items():
|
|
if isinstance(value, (list, tuple)):
|
|
fmt_dict[key] = li_delimiter.join(map(__format_value, value))
|
|
elif isinstance(value, str):
|
|
fmt_dict[key] = value
|
|
|
|
ret: list[str] = []
|
|
parts = template.splitlines(keepends = True)
|
|
|
|
for line in parts:
|
|
for key, value in fmt_dict.items():
|
|
marker = '{' + key + '}'
|
|
if marker in line:
|
|
indent = line[:line.index(marker)]
|
|
value = str(value).replace('\n', '\n' + indent)
|
|
line = line.replace(marker, value)
|
|
ret.append(line)
|
|
|
|
return ''.join(ret)
|
|
|
|
def format_lines(
|
|
template: str, values: list[RenderValues], li_quote: bool, li_delimiter: str
|
|
) -> str:
|
|
return format_list_dict(template, merge_values(*values), li_quote, li_delimiter)
|
|
|
|
_templates = {
|
|
'pkg-config':
|
|
"""\
|
|
prefix = {prefix}
|
|
exec_prefix = {{prefix}}
|
|
includedir = {{prefix}}/include
|
|
libdir = {{exec_prefix}}/lib
|
|
|
|
Name: {name}
|
|
Description: {description}
|
|
Version: {version}
|
|
""",
|
|
'pyrightconfig.json':
|
|
"""\
|
|
{
|
|
"extends": {base},
|
|
"include": [
|
|
{include}
|
|
],
|
|
"exclude": [
|
|
"**/__pycache__",
|
|
"**/.pytest_cache",
|
|
"**/.mypy_cache",
|
|
"**/.ruff_cache",
|
|
"**/.venv",
|
|
"**/build",
|
|
"**/dist"
|
|
],
|
|
"extraPaths": [
|
|
{extra_paths}
|
|
],
|
|
"typeCheckingMode": "basic",
|
|
"pythonPlatform": "Linux"
|
|
}
|
|
""",
|
|
}
|
|
|
|
def tmpl_render(
|
|
template_name: str,
|
|
values: list[RenderValues],
|
|
li_quote: bool = False,
|
|
li_delimiter: str = '\n',
|
|
search_path: list[str] | None = None,
|
|
) -> str:
|
|
|
|
def __format(template: str) -> str:
|
|
return format_lines(
|
|
template,
|
|
values,
|
|
li_quote = li_quote,
|
|
li_delimiter = li_delimiter,
|
|
)
|
|
|
|
for d in search_path if search_path else []:
|
|
path = d + '/' + template_name
|
|
try:
|
|
with open(path, 'r') as f:
|
|
template = f.read()
|
|
return __format(template)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
raw = _templates.get(template_name, None)
|
|
if raw is None:
|
|
raise Exception(f'Failed to find template "{template_name}"')
|
|
return __format(textwrap.dedent(raw))
|