cmds.projects.CmdCreateFile: Add --field-* options and integration tests #100
9 changed files with 300 additions and 8 deletions
|
|
@ -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:
|
||||||
|
|
@ -132,11 +156,45 @@ class CmdCreateFile(Cmd): # export
|
||||||
metavar = 'KEY=VALUE',
|
metavar = 'KEY=VALUE',
|
||||||
help = 'Additional fields to insert into the output file',
|
help = 'Additional fields to insert into the output file',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--field-keys',
|
||||||
|
help = (
|
||||||
|
'Possible keys for the --field option, comma-separated. Defaults to '
|
||||||
|
'all passed via the --field option. If this field is specified, '
|
||||||
|
'passing a key in --field that\'s not specified here is an error, '
|
||||||
|
'and not passing one makes the respective field go away from the '
|
||||||
|
'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
|
||||||
async def _run(self, args: Namespace) -> None:
|
async def _run(self, args: Namespace) -> None:
|
||||||
|
|
||||||
|
# -- Honour --field-keys option
|
||||||
|
fields = args.field
|
||||||
|
if args.field_keys:
|
||||||
|
passed_keys = set([field[0] for field in fields])
|
||||||
|
field_keys = set(args.field_keys.split(','))
|
||||||
|
if passed_keys - field_keys:
|
||||||
|
raise Exception(
|
||||||
|
'The following keys in --field are not in --field-keys: ' +
|
||||||
|
', '.join(sorted(passed_keys - field_keys))
|
||||||
|
)
|
||||||
|
for key in field_keys - passed_keys:
|
||||||
|
fields.append((key, ''))
|
||||||
|
|
||||||
|
# -- Find method for --format
|
||||||
method = getattr(self, 'render_' + args.format, None)
|
method = getattr(self, 'render_' + args.format, None)
|
||||||
if method is None: # choices already restricts this; keeps the linter happy
|
if method is None: # choices already restricts this; keeps the linter happy
|
||||||
raise Exception(f'Unsupported output format {args.format}')
|
raise Exception(f'Unsupported output format {args.format}')
|
||||||
sys.stdout.write(method(args.module, args.field))
|
|
||||||
|
# -- Render
|
||||||
|
sys.stdout.write(method(args.module, fields, args.field_separator))
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,8 @@ usage: jw-pkg.py projects create-file [-h] --format {pyright,tmpl}
|
||||||
[--search-path SEARCH_PATH]
|
[--search-path SEARCH_PATH]
|
||||||
[--template-name TEMPLATE_NAME]
|
[--template-name TEMPLATE_NAME]
|
||||||
[--quote] [-f KEY=VALUE]
|
[--quote] [-f KEY=VALUE]
|
||||||
|
[--field-keys FIELD_KEYS]
|
||||||
|
[--field-separator FIELD_SEPARATOR]
|
||||||
module
|
module
|
||||||
|
|
||||||
Generate a file from project metadata
|
Generate a file from project metadata
|
||||||
|
|
@ -357,6 +359,16 @@ options:
|
||||||
-f, --field KEY=VALUE
|
-f, --field KEY=VALUE
|
||||||
Additional fields to insert into the output file
|
Additional fields to insert into the output file
|
||||||
(default: [])
|
(default: [])
|
||||||
|
--field-keys FIELD_KEYS
|
||||||
|
Possible keys for the --field option, comma-separated.
|
||||||
|
Defaults to all passed via the --field option. If this
|
||||||
|
field is specified, passing a key in --field that's
|
||||||
|
not specified here is an error, and not passing one
|
||||||
|
makes the respective field go away from the rendering
|
||||||
|
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]
|
||||||
|
|
|
||||||
23
test/integration/jw-pkg/projects/create-file/Makefile
Normal file
23
test/integration/jw-pkg/projects/create-file/Makefile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
TOPDIR = ../../../../..
|
||||||
|
|
||||||
|
OUTPUT = test-out.txt
|
||||||
|
REFERENCE = test-expected.txt
|
||||||
|
|
||||||
|
# Relative topdir keeps the rendered project paths relocatable; dropping the
|
||||||
|
# log position keeps the error output free of volatile line numbers.
|
||||||
|
TEST_CMD_ARGS = --topdir-format relative --log-flags prio,stderr
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(TOPDIR)/make/test-jw-pkg.mk
|
||||||
|
|
||||||
|
all:
|
||||||
|
|
||||||
|
$(OUTPUT): Makefile test.sh templates/basic.tmpl templates/list.tmpl
|
||||||
|
bash ./test.sh $(TEST_CMD_LINE) > $(OUTPUT).tmp 2>&1
|
||||||
|
diff $(REFERENCE) $(OUTPUT).tmp
|
||||||
|
mv $(OUTPUT).tmp $(OUTPUT)
|
||||||
|
|
||||||
|
test: $(OUTPUT)
|
||||||
|
clean: test.integration.in-tree.clean
|
||||||
|
test.integration.in-tree.clean:
|
||||||
|
rm -f $(OUTPUT) $(OUTPUT).tmp
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
name={name}
|
||||||
|
path={path}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
start={items}end
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name basic.tmpl --search-path templates --field name=foo jw-pkg
|
||||||
|
name=foo
|
||||||
|
path={path}
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name list.tmpl --search-path templates --field items=a --field items=b jw-pkg
|
||||||
|
start=a,
|
||||||
|
start=bend
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name list.tmpl --search-path templates --field items=a --field items=b --field-separator | jw-pkg
|
||||||
|
start=a|bend
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name list.tmpl --search-path templates --field items=a --field items=b --field-separator %n jw-pkg
|
||||||
|
start=a
|
||||||
|
start=bend
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name list.tmpl --search-path templates --field items=a --field items=b --quote --field-separator | jw-pkg
|
||||||
|
start="a"|"b"end
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name basic.tmpl --search-path templates --field-keys name,path --field name=foo jw-pkg
|
||||||
|
name=foo
|
||||||
|
path=
|
||||||
|
============= Running: jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format pyright --field base=./conf/project/pyrightconfig-base.json --field include=src/python jw-pkg
|
||||||
|
{
|
||||||
|
"extends": "./conf/project/pyrightconfig-base.json",
|
||||||
|
"include": [
|
||||||
|
"src/python"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/__pycache__",
|
||||||
|
"**/.pytest_cache",
|
||||||
|
"**/.mypy_cache",
|
||||||
|
"**/.ruff_cache",
|
||||||
|
"**/.venv",
|
||||||
|
"**/build",
|
||||||
|
"**/dist"
|
||||||
|
],
|
||||||
|
"extraPaths": [
|
||||||
|
"src/python"
|
||||||
|
],
|
||||||
|
"typeCheckingMode": "basic",
|
||||||
|
"pythonPlatform": "Linux"
|
||||||
|
}
|
||||||
|
============= Running (expect failure): jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl jw-pkg
|
||||||
|
<E> Failed: Can't render template without name
|
||||||
|
============= Exit status: 1
|
||||||
|
============= Running (expect failure): jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name nosuch.tmpl --search-path templates jw-pkg
|
||||||
|
<E> Failed: Failed to find template "nosuch.tmpl"
|
||||||
|
============= Exit status: 1
|
||||||
|
============= Running (expect failure): jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name basic.tmpl --search-path templates --field-keys name --field path=x jw-pkg
|
||||||
|
<E> Failed: The following keys in --field are not in --field-keys: path
|
||||||
|
============= Exit status: 1
|
||||||
|
============= Running (expect failure): jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format pyright --field base=x --field include=y --field-separator | jw-pkg
|
||||||
|
<E> Failed: Unsupported separator for pyright config: "|"
|
||||||
|
============= Exit status: 1
|
||||||
|
============= Running (expect failure): jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format bogus jw-pkg
|
||||||
|
jw-pkg.py projects create-file: error: argument --format: invalid choice: 'bogus' (choose from 'pyright', 'tmpl')
|
||||||
|
============= Exit status: 2
|
||||||
|
============= Running (expect failure): jw-pkg.py -t ../../../../.. --log-level info --topdir-format relative --log-flags prio,stderr projects create-file --format tmpl --template-name basic.tmpl --search-path templates --field noval jw-pkg
|
||||||
|
jw-pkg.py projects create-file: error: argument -f/--field: Expected KEY=VALUE
|
||||||
|
============= Exit status: 2
|
||||||
108
test/integration/jw-pkg/projects/create-file/test.sh
Normal file
108
test/integration/jw-pkg/projects/create-file/test.sh
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# shellcheck disable=SC2048,SC2086
|
||||||
|
# Unquoted $* is intentional — the jw-pkg command line
|
||||||
|
|
||||||
|
export LC_ALL="C"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
jw_pkg_py="$*"
|
||||||
|
|
||||||
|
# Print a header and run a command whose output is part of the expected
|
||||||
|
# result. Under `set -e` a non-zero exit aborts the test.
|
||||||
|
run()
|
||||||
|
{
|
||||||
|
local log_cmd
|
||||||
|
# shellcheck disable=SC2001
|
||||||
|
log_cmd=$(echo "$*" | sed 's|.*python3[0-9.]*\s\+\(\.\.\/\)*scripts/||')
|
||||||
|
printf '============= Running: %s\n' "$log_cmd"
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a command that must exit non-zero and report the given message. Prints
|
||||||
|
# the command, the resulting error message, and the observed exit status.
|
||||||
|
# Fails the test if the command unexpectedly succeeds or the message is
|
||||||
|
# missing. Only the last output line is shown: for the app-level errors it is
|
||||||
|
# the error itself, and for argparse rejections it is the error line, which
|
||||||
|
# keeps the expected file free of the volatile usage text.
|
||||||
|
expect_fail()
|
||||||
|
{
|
||||||
|
local expected out rc log_cmd
|
||||||
|
expected=$1
|
||||||
|
shift
|
||||||
|
# shellcheck disable=SC2001
|
||||||
|
log_cmd=$(echo "$*" | sed 's|.*python3[0-9.]*\s\+\(\.\.\/\)*scripts/||')
|
||||||
|
printf '============= Running (expect failure): %s\n' "$log_cmd"
|
||||||
|
set +e
|
||||||
|
out=$("$@" 2>&1)
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
printf '%s\n' "$(tail -n 1 <<<"$out")"
|
||||||
|
printf '============= Exit status: %s\n' "$rc"
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
echo "ERROR: expected failure, but the command succeeded" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! grep -qF -- "$expected" <<<"$out"; then
|
||||||
|
echo "ERROR: expected message not found: $expected" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_test()
|
||||||
|
{
|
||||||
|
# -- tmpl: substitute a field; a marker without a value stays literal
|
||||||
|
run $jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name basic.tmpl --search-path templates \
|
||||||
|
--field name=foo jw-pkg
|
||||||
|
|
||||||
|
# -- tmpl: multi-value field, default separator (comma, newline)
|
||||||
|
run $jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name list.tmpl --search-path templates \
|
||||||
|
--field items=a --field items=b jw-pkg
|
||||||
|
|
||||||
|
# -- tmpl: multi-value field, custom separator
|
||||||
|
run $jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name list.tmpl --search-path templates \
|
||||||
|
--field items=a --field items=b --field-separator '|' jw-pkg
|
||||||
|
|
||||||
|
# -- tmpl: multi-value field, "%n" separator
|
||||||
|
run $jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name list.tmpl --search-path templates \
|
||||||
|
--field items=a --field items=b --field-separator '%n' jw-pkg
|
||||||
|
|
||||||
|
# -- tmpl: multi-value field, quoted values
|
||||||
|
run $jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name list.tmpl --search-path templates \
|
||||||
|
--field items=a --field items=b --quote --field-separator '|' jw-pkg
|
||||||
|
|
||||||
|
# -- tmpl: --field-keys adds keys that were not passed as empty fields
|
||||||
|
run $jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name basic.tmpl --search-path templates \
|
||||||
|
--field-keys name,path --field name=foo jw-pkg
|
||||||
|
|
||||||
|
# -- pyright: built-in template, extra paths from project metadata
|
||||||
|
run $jw_pkg_py projects create-file --format pyright \
|
||||||
|
--field base=./conf/project/pyrightconfig-base.json \
|
||||||
|
--field include=src/python jw-pkg
|
||||||
|
|
||||||
|
# -- errors
|
||||||
|
expect_fail "Can't render template without name" \
|
||||||
|
$jw_pkg_py projects create-file --format tmpl jw-pkg
|
||||||
|
expect_fail 'Failed to find template "nosuch.tmpl"' \
|
||||||
|
$jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name nosuch.tmpl --search-path templates jw-pkg
|
||||||
|
expect_fail "not in --field-keys: path" \
|
||||||
|
$jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name basic.tmpl --search-path templates \
|
||||||
|
--field-keys name --field path=x jw-pkg
|
||||||
|
expect_fail 'Unsupported separator for pyright config: "|"' \
|
||||||
|
$jw_pkg_py projects create-file --format pyright \
|
||||||
|
--field base=x --field include=y --field-separator '|' jw-pkg
|
||||||
|
expect_fail "invalid choice: 'bogus'" \
|
||||||
|
$jw_pkg_py projects create-file --format bogus jw-pkg
|
||||||
|
expect_fail "Expected KEY=VALUE" \
|
||||||
|
$jw_pkg_py projects create-file --format tmpl \
|
||||||
|
--template-name basic.tmpl --search-path templates --field noval jw-pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_test
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
TOPDIR = ../../../../../../../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-run.mk
|
||||||
|
|
||||||
|
all:
|
||||||
|
|
||||||
|
test: run
|
||||||
25
test/unit/python/jw/pkg/cmds/projects/CmdCreateFile/test.py
Normal file
25
test/unit/python/jw/pkg/cmds/projects/CmdCreateFile/test.py
Normal 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')
|
||||||
Loading…
Reference in a new issue