Skip to content

Commit

Permalink
Fixes related to run_fcst integration (ufs-community#657)
Browse files Browse the repository at this point in the history
A small collection of changes that were needed or useful during the SRW run_fcst integration. They include:

* A fix to the lateral boundary condition logic
* The addition of a !bool tag for YAML
* Treating the fv3 diag_table as a template instead of a file path.
  • Loading branch information
christinaholtNOAA authored Nov 21, 2024
1 parent c52b4de commit b00a124
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 26 deletions.
17 changes: 17 additions & 0 deletions docs/sections/user_guide/yaml/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ Or explicit:
Additionally, UW defines the following tags to support use cases not covered by standard tags:

``!bool``
^^^^^^^^^

Converts the tagged node to a Python ``boolean`` object. For example, given ``input.yaml``:

.. code-block:: yaml
flag1: True
flag2: !bool "{{ flag1 }}"
.. code-block:: text
% uw config realize -i ../input.yaml --output-format yaml
flag1: True
flag2: True
``!datetime``
^^^^^^^^^^^^^

Expand Down
11 changes: 8 additions & 3 deletions src/uwtools/config/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,21 @@ class UWYAMLConvert(UWYAMLTag):
method. See the pyyaml documentation for details.
"""

TAGS = ("!datetime", "!float", "!int")
TAGS = ("!bool", "!datetime", "!float", "!int")

def convert(self) -> Union[datetime, float, int]:
"""
Return the original YAML value converted to the specified type.
Will raise an exception if the value cannot be represented as the specified type.
"""
converters: dict[str, Union[Callable[[str], datetime], type[float], type[int]]] = dict(
zip(self.TAGS, [datetime.fromisoformat, float, int])
converters: dict[
str, Union[Callable[[str], bool], Callable[[str], datetime], type[float], type[int]]
] = dict(
zip(
self.TAGS,
[lambda x: {"True": True, "False": False}[x], datetime.fromisoformat, float, int],
)
)
return converters[self.tag](self.value)

Expand Down
21 changes: 12 additions & 9 deletions src/uwtools/drivers/fv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
"""

from pathlib import Path
from shutil import copy

from iotaa import asset, task, tasks

from uwtools.api.template import render
from uwtools.config.formats.nml import NMLConfig
from uwtools.config.formats.yaml import YAMLConfig
from uwtools.drivers.driver import DriverCycleBased
from uwtools.drivers.support import set_driver_docstring
from uwtools.logging import log
from uwtools.strings import STR
from uwtools.utils.tasks import file, filecopy, symlink

Expand All @@ -34,7 +33,7 @@ def boundary_files(self):
endhour = self.config["length"] + offset + 1
interval = lbcs["interval_hours"]
symlinks = {}
for n in [7] if self.config["domain"] == "global" else range(1, 7):
for n in [7] if self.config["domain"] == "regional" else range(1, 7):
for boundary_hour in range(offset, endhour, interval):
target = Path(lbcs["path"].format(tile=n, forecast_hour=boundary_hour))
linkname = (
Expand All @@ -52,12 +51,16 @@ def diag_table(self):
yield self.taskname(fn)
path = self.rundir / fn
yield asset(path, path.is_file)
yield None
if src := self.config.get(fn):
path.parent.mkdir(parents=True, exist_ok=True)
copy(src=src, dst=path)
else:
log.warning("No '%s' defined in config", fn)
template_file = Path(self.config[fn]["template_file"])
yield file(template_file)
render(
input_file=template_file,
output_file=path,
overrides={
**self.config[fn].get("template_values", {}),
"cycle": self.cycle,
},
)

@task
def field_table(self):
Expand Down
16 changes: 15 additions & 1 deletion src/uwtools/resources/jsonschema/fv3.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,20 @@
],
"properties": {
"diag_table": {
"type": "string"
"additionalProperties": false,
"properties": {
"template_file": {
"type": "string"
},
"template_values": {
"minProperties": 1,
"type": "object"
}
},
"required": [
"template_file"
],
"type": "object"
},
"domain": {
"enum": [
Expand Down Expand Up @@ -142,6 +155,7 @@
}
},
"required": [
"diag_table",
"domain",
"execution",
"field_table",
Expand Down
8 changes: 6 additions & 2 deletions src/uwtools/tests/config/formats/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,12 @@ def test_dereference(tmp_path):
- !int '42'
- !float '3.14'
- !datetime '{{ D }}'
- !bool "False"
f:
f1: !int '42'
f2: !float '3.14'
f3: True
g: !bool '{{ f.f3 }}'
D: 2024-10-10 00:19:00
N: "22"
Expand All @@ -229,8 +232,9 @@ def test_dereference(tmp_path):
"a": 44,
"b": {"c": 33},
"d": "{{ X }}",
"e": [42, 3.14, datetime.fromisoformat("2024-10-10 00:19:00")],
"f": {"f1": 42, "f2": 3.14},
"e": [42, 3.14, datetime.fromisoformat("2024-10-10 00:19:00"), False],
"f": {"f1": 42, "f2": 3.14, "f3": True},
"g": True,
"D": datetime.fromisoformat("2024-10-10 00:19:00"),
"N": "22",
}
Expand Down
3 changes: 2 additions & 1 deletion src/uwtools/tests/config/test_jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def test_unrendered(s, status):
assert jinja2.unrendered(s) is status


@mark.parametrize("tag", ["!datetime", "!float", "!int"])
@mark.parametrize("tag", ["!bool", "!datetime", "!float", "!int"])
def test__deref_convert_no(caplog, tag):
log.setLevel(logging.DEBUG)
loader = yaml.SafeLoader(os.devnull)
Expand All @@ -294,6 +294,7 @@ def test__deref_convert_no(caplog, tag):
@mark.parametrize(
"converted,tag,value",
[
(True, "!bool", "True"),
(datetime(2024, 9, 9, 0, 0), "!datetime", "2024-09-09 00:00:00"),
(3.14, "!float", "3.14"),
(42, "!int", "42"),
Expand Down
10 changes: 10 additions & 0 deletions src/uwtools/tests/config/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ def loader(self):
# demonstrate that those nodes' convert() methods return representations in type type specified
# by the tag.

def test_bool_bad(self, loader):
ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!bool", value="foo"))
with raises(KeyError):
ts.convert()

@mark.parametrize("value, expected", [("False", False), ("True", True)])
def test_bool_values(self, expected, loader, value):
ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!bool", value=value))
assert ts.convert() == expected

def test_datetime_no(self, loader):
ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!datetime", value="foo"))
with raises(ValueError):
Expand Down
12 changes: 5 additions & 7 deletions src/uwtools/tests/drivers/test_fv3.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
def config(tmp_path):
return {
"fv3": {
"domain": "global",
"diag_table": {
"template_file": "/path/to/tmpl",
},
"domain": "regional",
"execution": {
"batchargs": {
"walltime": "00:02:00",
Expand Down Expand Up @@ -116,18 +119,13 @@ def test_FV3_boundary_files(driverobj):
def test_FV3_diag_table(driverobj):
src = driverobj.rundir / "diag_table.in"
src.touch()
driverobj._config["diag_table"] = src
driverobj._config["diag_table"] = {"template_file": src}
dst = driverobj.rundir / "diag_table"
assert not dst.is_file()
driverobj.diag_table()
assert dst.is_file()


def test_FV3_diag_table_warn(caplog, driverobj):
driverobj.diag_table()
assert logged(caplog, "No 'diag_table' defined in config")


def test_FV3_driver_name(driverobj):
assert driverobj.driver_name() == FV3.driver_name() == "fv3"

Expand Down
6 changes: 3 additions & 3 deletions src/uwtools/tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@ def test_schema_filter_topo():

def test_schema_fv3():
config = {
"diag_table": {"template_file": "/path"},
"domain": "regional",
"execution": {"executable": "fv3"},
"field_table": {"base_file": "/path"},
Expand All @@ -887,7 +888,6 @@ def test_schema_fv3():
assert not errors(
{
**config,
"diag_table": "/path",
"files_to_copy": {"fn": "/path"},
"files_to_link": {"fn": "/path"},
"model_configure": {"base_file": "/path"},
Expand All @@ -903,9 +903,9 @@ def test_schema_fv3():
def test_schema_fv3_diag_table(fv3_prop):
errors = fv3_prop("diag_table")
# String value is ok:
assert not errors("/path/to/file")
assert not errors({"template_file": "/path/to/file", "template_values": {"foo": "bar"}})
# Anything else is not:
assert "42 is not of type 'string'\n" in errors(42)
assert "42 is not of type 'object'\n" in errors(42)


def test_schema_fv3_domain(fv3_prop):
Expand Down

0 comments on commit b00a124

Please sign in to comment.