Skip to content

Commit

Permalink
UW-510 Add uw template render --partial switch (ufs-community#418)
Browse files Browse the repository at this point in the history
  • Loading branch information
maddenp-noaa authored Feb 29, 2024
1 parent 12c0ebc commit 6c0fea9
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 22 deletions.
15 changes: 12 additions & 3 deletions docs/sections/user_guide/cli/tools/mode_template.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ The ``uw`` mode for handling :jinja2:`Jinja2 templates<templates>`.
$ uw template render --help
usage: uw template render [-h] [--input-file PATH] [--output-file PATH] [--values-file PATH]
[--values-format {ini,nml,sh,yaml}] [--values-needed] [--dry-run]
[--quiet] [--verbose]
[--values-format {ini,nml,sh,yaml}] [--values-needed] [--partial]
[--dry-run] [--debug] [--quiet] [--verbose]
[KEY=VALUE ...]
Render a template
Expand All @@ -49,6 +49,8 @@ The ``uw`` mode for handling :jinja2:`Jinja2 templates<templates>`.
Values format
--values-needed
Print report of values needed to render template
--partial
Permit partial template rendering
--dry-run
Only log info, making no changes
--debug
Expand Down Expand Up @@ -142,7 +144,14 @@ and a YAML file called ``values.yaml`` with the following contents:
[2023-12-18T19:30:05] ERROR Required value(s) not provided:
[2023-12-18T19:30:05] ERROR recipient
But values may be supplemented by ``key=value`` command-line arguments. For example:
But the ``--partial`` switch may be used to render as much as possible while passing expressions containing missing values through unchanged:

.. code-block:: text
$ uw template render --input-file template --values-file values.yaml --partial
Hello, {{ recipient }}!
Values may also be supplemented by ``key=value`` command-line arguments. For example:

.. code-block:: text
Expand Down
3 changes: 3 additions & 0 deletions src/uwtools/api/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def render(
output_file: Optional[Path] = None,
overrides: Optional[Dict[str, str]] = None,
values_needed: bool = False,
partial: bool = False,
dry_run: bool = False,
) -> bool:
"""
Expand All @@ -34,6 +35,7 @@ def render(
to ``stdout``)
:param overrides: Supplemental override values
:param values_needed: Just report variables needed to render the template?
:param partial: Permit unrendered expressions in output?
:param dry_run: Run in dry-run mode?
:return: ``True`` if Jinja2 template was successfully rendered, ``False`` otherwise
"""
Expand All @@ -44,6 +46,7 @@ def render(
output_file=output_file,
overrides=overrides,
values_needed=values_needed,
partial=partial,
dry_run=dry_run,
)

Expand Down
13 changes: 12 additions & 1 deletion src/uwtools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from uwtools.logging import log, setup_logging
from uwtools.utils.file import FORMAT, get_file_format

FORMATS = list(FORMAT.formats().keys())
FORMATS = FORMAT.extensions()
TITLE_REQ_ARG = "Required arguments"

Args = Dict[str, Any]
Expand Down Expand Up @@ -430,6 +430,7 @@ def _add_subparser_template_render(subparsers: Subparsers) -> ActionChecks:
_add_arg_values_file(optional)
_add_arg_values_format(optional, choices=FORMATS)
_add_arg_values_needed(optional)
_add_arg_partial(optional)
_add_arg_dry_run(optional)
checks = _add_args_verbosity(optional)
_add_arg_key_eq_val_pairs(optional)
Expand Down Expand Up @@ -463,6 +464,7 @@ def _dispatch_template_render(args: Args) -> bool:
output_file=args[STR.outfile],
overrides=_dict_from_key_eq_val_strings(args[STR.keyvalpairs]),
values_needed=args[STR.valsneeded],
partial=args[STR.partial],
dry_run=args[STR.dryrun],
)

Expand Down Expand Up @@ -613,6 +615,14 @@ def _add_arg_output_format(group: Group, choices: List[str], required: bool = Fa
)


def _add_arg_partial(group: Group) -> None:
group.add_argument(
_switch(STR.partial),
action="store_true",
help="Permit partial template rendering",
)


def _add_arg_quiet(group: Group) -> None:
group.add_argument(
_switch(STR.quiet),
Expand Down Expand Up @@ -854,6 +864,7 @@ class STR:
model: str = "model"
outfile: str = "output_file"
outfmt: str = "output_format"
partial: str = "partial"
quiet: str = "quiet"
realize: str = "realize"
render: str = "render"
Expand Down
30 changes: 15 additions & 15 deletions src/uwtools/config/jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from jinja2 import (
BaseLoader,
DebugUndefined,
Environment,
FileSystemLoader,
StrictUndefined,
Expand Down Expand Up @@ -149,6 +150,7 @@ def render(
output_file: Optional[Path] = None,
overrides: Optional[Dict[str, str]] = None,
values_needed: bool = False,
partial: bool = False,
dry_run: bool = False,
) -> bool:
"""
Expand All @@ -160,12 +162,10 @@ def render(
:param output_file: Path to write rendered Jinja2 template to (None => write to stdout).
:param overrides: Supplemental override values.
:param values_needed: Just report variables needed to render the template?
:param partial: Permit unrendered expressions in output?
:param dry_run: Run in dry-run mode?
:return: Jinja2 template was successfully rendered.
:return: True if Jinja2 template was successfully rendered, False otherwise.
"""

# Render template.

_report(locals())
if not isinstance(values, dict):
values = _set_up_values_obj(
Expand All @@ -182,20 +182,20 @@ def render(
if values_needed:
return _values_needed(undeclared_variables)

# Check for missing values required to render the template. If found, report them and raise an
# exception.
# If partial rendering has been requested, do a best-effort render. Otherwise, report any
# missing values and return an error to the caller.

missing = [var for var in undeclared_variables if var not in values.keys()]
if missing:
return _log_missing_values(missing)
if partial:
rendered = Environment(undefined=DebugUndefined).from_string(template_str).render(values)
else:
missing = [var for var in undeclared_variables if var not in values.keys()]
if missing:
return _log_missing_values(missing)
rendered = template.render()

# In dry-run mode, log the rendered template. Otherwise, write the rendered template.
# Log (dry-run mode) or write the rendered template.

return (
_dry_run_template(template.render())
if dry_run
else _write_template(output_file, template.render())
)
return _dry_run_template(rendered) if dry_run else _write_template(output_file, rendered)


# Private functions
Expand Down
1 change: 1 addition & 0 deletions src/uwtools/tests/api/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def test_render():
"values": "valsfile",
"values_format": "format",
"overrides": {"key": "val"},
"partial": True,
"values_needed": True,
"dry_run": True,
}
Expand Down
17 changes: 16 additions & 1 deletion src/uwtools/tests/config/test_jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import logging
import os
from io import StringIO
from types import SimpleNamespace as ns
from unittest.mock import patch

Expand Down Expand Up @@ -215,9 +216,23 @@ def test_render_dry_run(caplog, template, values_file):
assert logged(caplog, "roses are red, violets are blue")


@pytest.mark.parametrize("partial", [False, True])
def test_render_partial(caplog, capsys, partial):
log.setLevel(logging.INFO)
content = StringIO(initial_value="{{ greeting }} {{ recipient }}")
with patch.object(jinja2, "readable") as readable:
readable.return_value.__enter__.return_value = content
assert jinja2.render(values={"greeting": "Hello"}, partial=partial) is partial
if partial:
assert "Hello {{ recipient }}" in capsys.readouterr().out
else:
assert logged(caplog, "Required value(s) not provided:")
assert logged(caplog, "recipient")


def test_render_values_missing(caplog, template, values_file):
# Read in the config, remove the "roses" key, then re-write it.
log.setLevel(logging.INFO)
# Read in the config, remove the "roses" key, then re-write it.
with open(values_file, "r", encoding="utf-8") as f:
cfgobj = yaml.safe_load(f.read())
del cfgobj["roses_color"]
Expand Down
8 changes: 6 additions & 2 deletions src/uwtools/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ def test__dispatch_template_render_no_optional():
STR.valsfmt: None,
STR.keyvalpairs: [],
STR.valsneeded: False,
STR.partial: False,
STR.dryrun: False,
}
with patch.object(uwtools.api.template, "render") as render:
Expand All @@ -395,6 +396,7 @@ def test__dispatch_template_render_no_optional():
values_format=None,
overrides={},
values_needed=False,
partial=False,
dry_run=False,
)

Expand All @@ -407,7 +409,8 @@ def test__dispatch_template_render_yaml():
STR.valsfmt: 4,
STR.keyvalpairs: ["foo=88", "bar=99"],
STR.valsneeded: 6,
STR.dryrun: 7,
STR.partial: 7,
STR.dryrun: 8,
}
with patch.object(uwtools.api.template, "render") as render:
cli._dispatch_template_render(args)
Expand All @@ -418,7 +421,8 @@ def test__dispatch_template_render_yaml():
values_format=4,
overrides={"foo": "88", "bar": "99"},
values_needed=6,
dry_run=7,
partial=7,
dry_run=8,
)


Expand Down

0 comments on commit 6c0fea9

Please sign in to comment.