cmds.projects.CmdCreateFile: Add --field-separator option

List values in a rendered template are joined with a fixed separator of
",\n" (comma plus newline), and there is no way to change that. Templates
sometimes want a different separator, e.g. a plain newline or a single-line
comma-separated list.

Add a --field-separator option that controls how list values are joined
in the rendered output. %n expands to a newline, and the default remains
",\n" for both formats. render_tmpl() applies the separator to the rendered
template, while render_pyright() rejects any other value: the built-in
pyrightconfig.json template is a fixed JSON document, and no alternative
separator is supported for it.

Update the help output test expectation accordingly.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-09-12 19:33:54 +02:00
commit 1349665c39
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
4 changed files with 76 additions and 8 deletions

View file

@ -55,32 +55,56 @@ class CmdCreateFile(Cmd): # export
p = 'render_' p = 'render_'
return [name.removeprefix(p) for name in dir(self) if name.startswith(p)] return [name.removeprefix(p) for name in dir(self) if name.startswith(p)]
def __format_separator(self, separator: str | None, default: str) -> str:
if separator is None:
return default
format_chars = {
'%n': '\n',
}
for src, dst in format_chars.items():
separator = separator.replace(src, dst)
return separator
def __render( def __render(
self, self,
template_name: str, template_name: str,
values: list[RenderValues], values: list[RenderValues],
li_quote: bool = False, li_quote: bool = False,
li_delimiter: str = '\n', li_separator: str = '\n',
) -> str: ) -> str:
return tmpl_render( return tmpl_render(
template_name, template_name,
values, values,
li_quote = li_quote, li_quote = li_quote,
li_delimiter = li_delimiter, li_delimiter = li_separator,
search_path = self.app.args.search_path.split(':'), search_path = self.app.args.search_path.split(':'),
) )
def render_tmpl(self, module: str, extra_fields: RenderValues) -> str: def render_tmpl(
self,
module: str,
extra_fields: RenderValues,
separator: str | None,
) -> str:
template_name = self.app.args.template_name template_name = self.app.args.template_name
if template_name is None: if template_name is None:
raise Exception('Can\'t render template without name') raise Exception('Can\'t render template without name')
return self.__render( return self.__render(
template_name, [extra_fields], template_name,
[extra_fields],
li_quote = self.app.args.quote, li_quote = self.app.args.quote,
li_delimiter = ',\n' li_separator = self.__format_separator(separator, ',\n'),
) )
def render_pyright(self, module: str, extra_fields: RenderValues) -> str: def render_pyright(
self,
module: str,
extra_fields: RenderValues,
separator: str | None,
) -> str:
separator = self.__format_separator(separator, ',\n')
if separator != ',\n':
raise Exception(f'Unsupported separator for pyright config: "{separator}"')
extra_paths = [] extra_paths = []
for m in self.__jw_required(include_self = True): for m in self.__jw_required(include_self = True):
path = self.app.find_dir(m, search_subdirs = ['src/python', 'tools/python']) path = self.app.find_dir(m, search_subdirs = ['src/python', 'tools/python'])
@ -94,7 +118,7 @@ class CmdCreateFile(Cmd): # export
return self.__render( return self.__render(
'pyrightconfig.json', [values, extra_fields], 'pyrightconfig.json', [values, extra_fields],
li_quote = True, li_quote = True,
li_delimiter = ',\n' li_separator = ',\n'
) )
def __init__(self, parent: Parent) -> None: def __init__(self, parent: Parent) -> None:
@ -142,6 +166,13 @@ class CmdCreateFile(Cmd): # export
'rendering output' 'rendering output'
) )
) )
parser.add_argument(
'--field-separator',
help = (
'Field separator. %%n expands to newline. Default value depends on '
'--format'
)
)
parser.add_argument('module', help = 'The module to generate the file for') parser.add_argument('module', help = 'The module to generate the file for')
@override @override
@ -166,4 +197,4 @@ class CmdCreateFile(Cmd): # export
raise Exception(f'Unsupported output format {args.format}') raise Exception(f'Unsupported output format {args.format}')
# -- Render # -- Render
sys.stdout.write(method(args.module, fields)) sys.stdout.write(method(args.module, fields, args.field_separator))

View file

@ -337,6 +337,7 @@ usage: jw-pkg.py projects create-file [-h] --format {pyright,tmpl}
[--template-name TEMPLATE_NAME] [--template-name TEMPLATE_NAME]
[--quote] [-f KEY=VALUE] [--quote] [-f KEY=VALUE]
[--field-keys FIELD_KEYS] [--field-keys FIELD_KEYS]
[--field-separator FIELD_SEPARATOR]
module module
Generate a file from project metadata Generate a file from project metadata
@ -365,6 +366,9 @@ options:
not specified here is an error, and not passing one not specified here is an error, and not passing one
makes the respective field go away from the rendering makes the respective field go away from the rendering
output (default: None) output (default: None)
--field-separator FIELD_SEPARATOR
Field separator. %n expands to newline. Default value
depends on --format (default: None)
============= Running: jw-pkg.py -t ../../../.. --log-level info projects create-pkg-config --help ============= Running: jw-pkg.py -t ../../../.. --log-level info projects create-pkg-config --help
usage: jw-pkg.py projects create-pkg-config [-h] [-F PROJECT_DESCR_FILE] usage: jw-pkg.py projects create-pkg-config [-h] [-F PROJECT_DESCR_FILE]
[-d DESCRIPTION] [-n NAME] [-d DESCRIPTION] [-n NAME]

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,25 @@
from jw.pkg.cmds.projects.CmdCreateFile import CmdCreateFile
# __format_separator() is a private method that does not use self, so an
# uninitialised instance is a sufficient receiver for the bound method
inst = CmdCreateFile.__new__(CmdCreateFile)
fmt = getattr(inst, '_CmdCreateFile__format_separator')
# -- __format_separator --
# A None separator falls back to the format's default
assert fmt(None, ',\n') == ',\n'
# %n expands to a newline, wherever it appears in the separator
assert fmt('%n', ',\n') == '\n'
assert fmt(',%n', '\n') == ',\n'
assert fmt('a%n b', '') == 'a\n b'
# Several %n expand independently
assert fmt('%n%n', '') == '\n\n'
# Anything that is not %n passes through untouched
assert fmt('%', '') == '%'
assert fmt('%x', '') == '%x'
print('All CmdCreateFile tests passed')