diff --git a/src/python/jw/pkg/cmds/projects/lib/templates.py b/src/python/jw/pkg/cmds/projects/lib/templates.py index 3df12aa5..bc9bf23c 100644 --- a/src/python/jw/pkg/cmds/projects/lib/templates.py +++ b/src/python/jw/pkg/cmds/projects/lib/templates.py @@ -7,7 +7,7 @@ ListDict: TypeAlias = dict[str, list[str]] StrDict: TypeAlias = dict[str, str] RenderValues: TypeAlias = ListDict | StrDict | TupleList -def is_str_dict(values: RenderValues) -> TypeGuard[StrDict]: +def is_str_dict(values: object) -> TypeGuard[StrDict]: if not isinstance(values, dict): return False for key, val in values.items(): @@ -17,7 +17,7 @@ def is_str_dict(values: RenderValues) -> TypeGuard[StrDict]: return False return True -def is_list_dict(values: RenderValues) -> TypeGuard[ListDict]: +def is_list_dict(values: object) -> TypeGuard[ListDict]: if not isinstance(values, dict): return False for key, val in values.items(): @@ -32,7 +32,7 @@ def is_list_dict(values: RenderValues) -> TypeGuard[ListDict]: return False return True -def is_tuple_list(values: RenderValues) -> TypeGuard[TupleList]: +def is_tuple_list(values: object) -> TypeGuard[TupleList]: if not isinstance(values, list): return False for item in values: @@ -46,7 +46,7 @@ def is_tuple_list(values: RenderValues) -> TypeGuard[TupleList]: return False return True -def render_values_to_list_dict(values: RenderValues) -> ListDict: +def render_values_to_list_dict(values: object) -> ListDict: def __tuple_list_to_dict(src: TupleList) -> ListDict: ret: ListDict = {} @@ -56,7 +56,12 @@ def render_values_to_list_dict(values: RenderValues) -> ListDict: return ret if is_list_dict(values): - return 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') diff --git a/test/unit/python/jw/pkg/cmds/Makefile b/test/unit/python/jw/pkg/cmds/Makefile new file mode 100644 index 00000000..f817ac8c --- /dev/null +++ b/test/unit/python/jw/pkg/cmds/Makefile @@ -0,0 +1,4 @@ +TOPDIR = ../../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/dirs.mk diff --git a/test/unit/python/jw/pkg/cmds/projects/Makefile b/test/unit/python/jw/pkg/cmds/projects/Makefile new file mode 100644 index 00000000..c87020cb --- /dev/null +++ b/test/unit/python/jw/pkg/cmds/projects/Makefile @@ -0,0 +1,4 @@ +TOPDIR = ../../../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/dirs.mk diff --git a/test/unit/python/jw/pkg/cmds/projects/lib/Makefile b/test/unit/python/jw/pkg/cmds/projects/lib/Makefile new file mode 100644 index 00000000..3b96da8e --- /dev/null +++ b/test/unit/python/jw/pkg/cmds/projects/lib/Makefile @@ -0,0 +1,4 @@ +TOPDIR = ../../../../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/dirs.mk diff --git a/test/unit/python/jw/pkg/cmds/projects/lib/templates/Makefile b/test/unit/python/jw/pkg/cmds/projects/lib/templates/Makefile new file mode 100644 index 00000000..6a0e35b2 --- /dev/null +++ b/test/unit/python/jw/pkg/cmds/projects/lib/templates/Makefile @@ -0,0 +1,8 @@ +TOPDIR = ../../../../../../../../.. + +include $(TOPDIR)/make/proj.mk +include $(JWBDIR)/make/py-run.mk + +all: + +test: run diff --git a/test/unit/python/jw/pkg/cmds/projects/lib/templates/test.py b/test/unit/python/jw/pkg/cmds/projects/lib/templates/test.py new file mode 100644 index 00000000..46400ddd --- /dev/null +++ b/test/unit/python/jw/pkg/cmds/projects/lib/templates/test.py @@ -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')