cmds.projects.lib.templates: merge_values(): Don't split strings

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>
This commit is contained in:
Jan Lindemann 2026-09-08 21:41:09 +02:00
commit 955fb8101b
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
6 changed files with 103 additions and 5 deletions

View file

@ -0,0 +1,4 @@
TOPDIR = ../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/dirs.mk

View file

@ -0,0 +1,4 @@
TOPDIR = ../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/dirs.mk

View file

@ -0,0 +1,4 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/dirs.mk

View file

@ -0,0 +1,8 @@
TOPDIR = ../../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,73 @@
from jw.pkg.cmds.projects.lib.templates import (
is_list_dict,
is_str_dict,
is_tuple_list,
merge_values,
render_values_to_list_dict,
)
# -- Type guards --
assert is_str_dict({'a': 'x', 'b': 'y'})
assert not is_str_dict({'a': ['x']})
assert not is_str_dict({'a': 1})
assert not is_str_dict([('a', 'x')])
assert is_list_dict({'a': ['x'], 'b': ['y']})
# A string value is a legitimate list-dict value, too
assert is_list_dict({'a': ['x'], 'b': 'y'})
assert not is_list_dict({'a': 1})
assert not is_list_dict({'a': ['x', 1]})
assert not is_list_dict([('a', 'x')])
assert is_tuple_list([('a', 'x'), ('b', 'y')])
assert not is_tuple_list([('a', 'x', 'y')])
assert not is_tuple_list([('a', 1)])
assert not is_tuple_list(['a'])
# -- render_values_to_list_dict --
# A list of lists passes through unchanged
assert render_values_to_list_dict({'a': ['x', 'y']}) == {'a': ['x', 'y']}
# String values are normalized to single-element lists, not split up
assert render_values_to_list_dict({'a': 'xyz'}) == {'a': ['xyz']}
assert render_values_to_list_dict({'a': ['x'], 'b': 'y'}) == {'a': ['x'], 'b': ['y']}
# Tuple lists are folded into a dict of lists
assert render_values_to_list_dict([('a', 'x'), ('a', 'y'), ('b', 'z')]) == {
'a': ['x', 'y'],
'b': ['z'],
}
# Unsupported layouts raise
try:
render_values_to_list_dict({'a': 1})
assert False, 'Should have raised'
except Exception:
pass
# -- merge_values --
# String values are merged as whole strings, not character by character
assert merge_values({
'prefix': '/usr', 'name': 'jw-pkg'
}) == {
'prefix': ['/usr'],
'name': ['jw-pkg'],
}
# Values for the same key are appended in order
assert merge_values({'a': ['x']}, {'a': ['y']}) == {'a': ['x', 'y']}
assert merge_values({'a': 'x'}, {'a': 'y'}) == {'a': ['x', 'y']}
# All three layouts can be mixed
assert merge_values(
{'a': ['x']},
{'a': 'y'},
[('a', 'z'), ('b', 'w')],
) == {
'a': ['x', 'y', 'z'], 'b': ['w']
}
print('All templates merge tests passed')