Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/ufs-community/uwtools into …
Browse files Browse the repository at this point in the history
…fix_filtered_topo
  • Loading branch information
christinaholtNOAA committed Sep 16, 2024
2 parents 7291eff + afce2f2 commit 8bb56cb
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 56 deletions.
24 changes: 21 additions & 3 deletions docs/sections/user_guide/yaml/rocoto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,30 @@ In the example, the resulting log would appear in the XML file as:

.. code-block:: xml
<log>
<cyclestr>/some/path/to/&FOO;</cyclestr>
</log>
<log><cyclestr>/some/path/to/&FOO;</cyclestr></log>
The ``attrs:`` block is optional within the ``cyclestr:`` block and can be used to specify the cycle offset.

Wherever a ``cyclestr:`` block is accepted, a YAML sequence mixing text and ``cyclestr:`` blocks may also be provided. For example,

.. code-block:: yaml
log:
- cyclestr:
value: "%Y%m%d%H"
- -through-
- cyclestr:
attrs:
offset: "06:00:00"
value: "%Y%m%d%H"
- .log
would be rendered as

.. code-block:: xml
<log><cyclestr>%Y%m%d%H</cyclestr>-through-<cyclestr offset="06:00:00">%Y%m%d%H</cyclestr>.log</log>
Tasks Section
-------------

Expand Down
37 changes: 24 additions & 13 deletions src/uwtools/drivers/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
) -> None:
config_input = config if isinstance(config, YAMLConfig) else YAMLConfig(config=config)
config_input.dereference(
Expand All @@ -65,8 +65,7 @@ def __init__(
self._config: dict = self._config_intermediate[self.driver_name()]
except KeyError as e:
raise UWConfigError("Required '%s' block missing in config" % self.driver_name()) from e
if controller:
self._config[STR.rundir] = self._config_intermediate[controller][STR.rundir]
self._delegate(controller, STR.rundir)
self.schema_file = schema_file
self._validate()
dryrun(enable=dry_run)
Expand Down Expand Up @@ -167,6 +166,19 @@ def _create_user_updated_config(
else:
log.debug(f"Failed to validate {path}")

def _delegate(self, controller: Optional[list[str]], config_key: str) -> None:
"""
Selectively delegate config to controller.
:param controller: Key(s) leading to block in config controlling run-time values.
:param config_key: Name of config item to delegate to controller.
"""
if controller:
val = self._config_intermediate[controller[0]]
for key in controller[1:]:
val = val[key]
self._config[config_key] = val[config_key]

# Public helper methods

@classmethod
Expand Down Expand Up @@ -241,7 +253,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
Expand Down Expand Up @@ -274,7 +286,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
Expand Down Expand Up @@ -314,7 +326,7 @@ def __init__(
dry_run: bool = False,
key_path: Optional[list[str]] = None,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
config=config,
Expand All @@ -339,7 +351,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
Expand All @@ -351,8 +363,7 @@ def __init__(
controller=controller,
)
self._batch = batch
if controller:
self._config[STR.execution] = self.config_full[controller][STR.execution]
self._delegate(controller, STR.execution)

# Workflow tasks

Expand Down Expand Up @@ -541,7 +552,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
Expand Down Expand Up @@ -576,7 +587,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
cycle=cycle,
Expand Down Expand Up @@ -618,7 +629,7 @@ def __init__(
key_path: Optional[list[str]] = None,
batch: bool = False,
schema_file: Optional[Path] = None,
controller: Optional[str] = None,
controller: Optional[list[str]] = None,
):
super().__init__(
config=config,
Expand Down Expand Up @@ -650,7 +661,7 @@ def _add_docstring(class_: type, omit: Optional[list[str]] = None) -> None:
:param key_path: Keys leading through the config to the driver's configuration block.
:param batch: Run component via the batch system?
:param schema_file: Path to schema file to use to validate an external driver.
:param controller: Name of block in config controlling run-time values.
:param controller: Key(s) leading to block in config controlling run-time values.
"""
setattr(
class_,
Expand Down
56 changes: 36 additions & 20 deletions src/uwtools/resources/jsonschema/rocoto.jsonschema
Original file line number Diff line number Diff line change
@@ -1,44 +1,60 @@
{
"$defs": {
"compoundTimeString": {
"anyOf": [
"oneOf": [
{
"type": "integer"
"$ref": "#/$defs/compoundTimeStringElement"
},
{
"type": "string"
"items": {
"$ref": "#/$defs/compoundTimeStringElement"
},
"type": "array"
}
]
},
"compoundTimeStringElement": {
"oneOf": [
{
"$ref": "#/$defs/cycleString"
},
{
"type": "integer"
},
{
"type": "string"
}
]
},
"cycleString": {
"additionalProperties": false,
"properties": {
"cyclestr": {
"additionalProperties": false,
"properties": {
"cyclestr": {
"attrs": {
"additionalProperties": false,
"properties": {
"attrs": {
"additionalProperties": false,
"properties": {
"offset": {
"$ref": "#/$defs/time"
}
},
"type": "object"
},
"value": {
"type": "string"
"offset": {
"$ref": "#/$defs/time"
}
},
"required": [
"value"
],
"type": "object"
},
"value": {
"type": "string"
}
},
"required": [
"cyclestr"
"value"
],
"type": "object"
}
]
},
"required": [
"cyclestr"
],
"type": "object"
},
"dependency": {
"additionalProperties": false,
Expand Down
17 changes: 7 additions & 10 deletions src/uwtools/rocoto.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Any, Optional, Union

from lxml import etree
from lxml.builder import E # type: ignore
from lxml.etree import Element, SubElement, _Element

from uwtools.config.formats.yaml import YAMLConfig
Expand Down Expand Up @@ -113,16 +114,12 @@ def _add_compound_time_string(self, e: _Element, config: Any, tag: str) -> _Elem
:param tag: Name of child element to add.
:return: The child element.
"""
e = SubElement(e, tag)
if isinstance(config, dict):
self._set_attrs(e, config)
if subconfig := config.get(STR.cyclestr, {}):
cyclestr = SubElement(e, STR.cyclestr)
cyclestr.text = subconfig[STR.value]
self._set_attrs(cyclestr, subconfig)
else:
e.text = str(config)
return e
config = config if isinstance(config, list) else [config]
cyclestr = lambda x: E.cyclestr(x["cyclestr"]["value"], **x["cyclestr"].get("attrs", {}))
items = [cyclestr(x) if isinstance(x, dict) else str(x) for x in [tag, *config]]
child: _Element = E(*items) # pylint: disable=not-callable
e.append(child)
return child

def _add_metatask(self, e: _Element, config: dict, name_attr: str) -> None:
"""
Expand Down
11 changes: 9 additions & 2 deletions src/uwtools/tests/drivers/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def test_Assets_controller(config, controller_schema):
with raises(UWConfigError):
ConcreteAssetsTimeInvariant(config=config, schema_file=controller_schema)
assert ConcreteAssetsTimeInvariant(
config=config, schema_file=controller_schema, controller="controller"
config=config, schema_file=controller_schema, controller=["controller"]
)


Expand Down Expand Up @@ -285,6 +285,13 @@ def test_Assets__create_user_updated_config_base_file(
assert updated == expected


def test_Assets__delegate(driverobj):
assert "roses" not in driverobj.config
driverobj._config_intermediate["plants"] = {"flowers": {"roses": "red"}}
driverobj._delegate(["plants", "flowers"], "roses")
assert driverobj.config["roses"] == "red"


def test_Assets__rundir(assetsobj):
assert assetsobj.rundir == Path(assetsobj.config["rundir"])

Expand Down Expand Up @@ -342,7 +349,7 @@ def test_Driver_controller(config, controller_schema):
with raises(UWConfigError):
ConcreteDriverTimeInvariant(config=config, schema_file=controller_schema)
assert ConcreteDriverTimeInvariant(
config=config, schema_file=controller_schema, controller="controller"
config=config, schema_file=controller_schema, controller=["controller"]
)


Expand Down
37 changes: 29 additions & 8 deletions src/uwtools/tests/test_rocoto.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest.mock import DEFAULT as D
from unittest.mock import PropertyMock, patch

from lxml import etree
from pytest import fixture, mark, raises

from uwtools import rocoto
Expand Down Expand Up @@ -110,22 +111,42 @@ def test_instantiate_from_cfgobj(self, assets):
cfgfile, _ = assets
assert rocoto._RocotoXML(config=YAMLConfig(cfgfile))._root.tag == "workflow"

def test__add_compound_time_string_basic(self, instance, root):
config = "bar"
@mark.parametrize("config", ["bar", 42])
def test__add_compound_time_string_basic(self, config, instance, root):
instance._add_compound_time_string(e=root, config=config, tag="foo")
child = root[0]
assert child.tag == "foo"
assert child.text == "bar"
assert child.text == str(config)

def test__add_compound_time_string_cyclestr(self, instance, root):
config = {"attrs": {"bar": "42"}, "cyclestr": {"attrs": {"baz": "43"}, "value": "qux"}}
config = {"cyclestr": {"attrs": {"baz": "42"}, "value": "qux"}}
instance._add_compound_time_string(e=root, config=config, tag="foo")
child = root[0]
assert child.get("bar") == "42"
cyclestr = child[0]
assert cyclestr.get("baz") == "43"
cyclestr = root[0][0]
assert cyclestr.get("baz") == "42"
assert cyclestr.text == "qux"

def test__add_compound_time_string_list(self, instance, root):
config = [
"cycle-",
{"cyclestr": {"value": "%s"}},
"-valid-",
{"cyclestr": {"value": "%s", "attrs": {"offset": "00:06:00"}}},
".log",
]
xml = "<a>{}</a>".format(
"".join(
[
"cycle-",
"<cyclestr>%s</cyclestr>",
"-valid-",
'<cyclestr offset="00:06:00">%s</cyclestr>',
".log",
]
)
)
instance._add_compound_time_string(e=root, config=config, tag="a")
assert etree.tostring(root[0]).decode("utf-8") == xml

def test__add_metatask(self, instance, root):
config = {
"metatask_foo": "1",
Expand Down

0 comments on commit 8bb56cb

Please sign in to comment.