From 22a770f8fe1a534496ad2d0d8d5e0d494f2a4839 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 14 Feb 2024 17:30:09 +0000 Subject: [PATCH 1/6] Add CLI and API support --- src/uwtools/api/fv3.py | 18 +++++++++++++++--- src/uwtools/cli.py | 12 ++++++++++++ src/uwtools/tests/test_cli.py | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/uwtools/api/fv3.py b/src/uwtools/api/fv3.py index 65150b483..e6bff87e4 100644 --- a/src/uwtools/api/fv3.py +++ b/src/uwtools/api/fv3.py @@ -1,8 +1,8 @@ import datetime as dt from pathlib import Path -from typing import Dict +from typing import Dict, Optional -import iotaa +import iotaa as _iotaa from uwtools.drivers.fv3 import FV3 @@ -12,6 +12,7 @@ def execute( config_file: Path, cycle: dt.datetime, batch: bool = False, + graph_file: Optional[Path] = None, dry_run: bool = False, ) -> bool: """ @@ -24,18 +25,29 @@ def execute( :param config_file: Path to UW YAML config file :param cycle: The cycle to run :param batch: Submit run to the batch system + :param graph_file: Write Graphviz DOT output here :param dry_run: Do not run forecast, just report what would have been done :return: True if task completes without raising an exception """ obj = FV3(config_file=config_file, cycle=cycle, batch=batch, dry_run=dry_run) getattr(obj, task)() + if graph_file: + with open(graph_file, "w", encoding="utf-8") as f: + print(graph(), file=f) return True +def graph() -> str: + """ + Returns Graphviz DOT code for the most recently executed task. + """ + return _iotaa.graph() + + def tasks() -> Dict[str, str]: """ Returns a mapping from task names to their one-line descriptions. """ return { - task: getattr(FV3, task).__doc__.strip().split("\n")[0] for task in iotaa.tasknames(FV3) + task: getattr(FV3, task).__doc__.strip().split("\n")[0] for task in _iotaa.tasknames(FV3) } diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 6f8c4dd0a..256ef7fb1 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -231,6 +231,7 @@ def _add_subparser_fv3_task(subparsers: Subparsers, task: str, helpmsg: str) -> optional = _basic_setup(parser) _add_arg_batch(optional) _add_arg_dry_run(optional) + _add_arg_graph_file(optional) checks = _add_args_verbosity(optional) return checks @@ -246,6 +247,7 @@ def _dispatch_fv3(args: Args) -> bool: config_file=args[STR.cfgfile], cycle=args[STR.cycle], batch=args[STR.batch], + graph_file=args[STR.graphfile], dry_run=args[STR.dryrun], ) @@ -495,6 +497,15 @@ def _add_arg_file_path(group: Group, switch: str, helpmsg: str, required: bool = ) +def _add_arg_graph_file(group: Group) -> None: + group.add_argument( + _switch(STR.graphfile), + help="Path to Graphviz DOT output", + metavar="PATH", + type=str, + ) + + def _add_arg_input_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.infile), @@ -776,6 +787,7 @@ class STR: file2fmt: str = "file_2_format" file2path: str = "file_2_path" fv3: str = "fv3" + graphfile: str = "graph_file" help: str = "help" infile: str = "input_file" infmt: str = "input_format" diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py index 2ae1f8bc3..f99c08be5 100644 --- a/src/uwtools/tests/test_cli.py +++ b/src/uwtools/tests/test_cli.py @@ -268,6 +268,7 @@ def test__dispatch_fv3(): "batch": True, "config_file": "config.yaml", "cycle": dt.datetime.now(), + "graph_file": None, "dry_run": False, } with patch.object(uwtools.api.fv3, "execute") as execute: From 29bf85ebd497f5639d1f3d8b057d8943ff2320bd Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 14 Feb 2024 17:47:49 +0000 Subject: [PATCH 2/6] Unit tests @ 100% --- src/uwtools/tests/api/test_fv3.py | 46 +++++++++++++++++++------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/uwtools/tests/api/test_fv3.py b/src/uwtools/tests/api/test_fv3.py index d89c80919..f33b7ec6a 100644 --- a/src/uwtools/tests/api/test_fv3.py +++ b/src/uwtools/tests/api/test_fv3.py @@ -1,42 +1,52 @@ -# pylint: disable=missing-function-docstring,protected-access +# pylint: disable=missing-function-docstring import datetime as dt from unittest.mock import patch -from iotaa import external, task, tasks +from iotaa import asset, external, task, tasks from uwtools.api import fv3 -@external -def t1(): - "@external t1" - - -@task -def t2(): - "@task t2" - - -@tasks -def t3(): - "@tasks t3" - - -def test_execute(): +def test_execute(tmp_path): + dot = tmp_path / "graph.dot" args: dict = { "config_file": "config.yaml", "cycle": dt.datetime.utcnow(), "batch": False, + "graph_file": dot, "dry_run": True, } with patch.object(fv3, "FV3") as FV3: assert fv3.execute(**args, task="foo") is True + del args["graph_file"] FV3.assert_called_once_with(**args) FV3().foo.assert_called_once_with() +def test_graph(): + @external + def ready(): + yield "ready" + yield asset("ready", lambda: True) + + ready() + assert fv3.graph().startswith("digraph") + + def test_tasks(): + @external + def t1(): + "@external t1" + + @task + def t2(): + "@task t2" + + @tasks + def t3(): + "@tasks t3" + with patch.object(fv3, "FV3") as FV3: FV3.t1 = t1 FV3.t2 = t2 From 272b159dd42ee0c6b7ac65ea058e5e85a8f2b453 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Fri, 23 Feb 2024 00:01:14 +0000 Subject: [PATCH 3/6] Add dot-output to sfc_climo_gen --- src/uwtools/api/fv3.py | 4 ++-- src/uwtools/api/sfc_climo_gen.py | 18 +++++++++++++++--- src/uwtools/cli.py | 4 +++- src/uwtools/tests/api/test_fv3.py | 2 +- src/uwtools/tests/api/test_sfc_climo_gen.py | 17 +++++++++++++++-- src/uwtools/tests/test_cli.py | 3 ++- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/uwtools/api/fv3.py b/src/uwtools/api/fv3.py index 42bc9ac7b..d271ba75e 100644 --- a/src/uwtools/api/fv3.py +++ b/src/uwtools/api/fv3.py @@ -15,8 +15,8 @@ def execute( config_file: Path, cycle: dt.datetime, batch: bool = False, - graph_file: Optional[Path] = None, dry_run: bool = False, + graph_file: Optional[Path] = None, ) -> bool: """ Execute an FV3 task. @@ -28,8 +28,8 @@ def execute( :param config_file: Path to YAML config file :param cycle: The cycle to run :param batch: Submit run to the batch system - :param graph_file: Write Graphviz DOT output here :param dry_run: Do not run forecast, just report what would have been done + :param graph_file: Write Graphviz DOT output here :return: True if task completes without raising an exception """ obj = FV3(config_file=config_file, cycle=cycle, batch=batch, dry_run=dry_run) diff --git a/src/uwtools/api/sfc_climo_gen.py b/src/uwtools/api/sfc_climo_gen.py index 3dcce7c41..60e4c7422 100644 --- a/src/uwtools/api/sfc_climo_gen.py +++ b/src/uwtools/api/sfc_climo_gen.py @@ -2,9 +2,9 @@ API access to the uwtools sfc_climo_gen driver. """ from pathlib import Path -from typing import Dict +from typing import Dict, Optional -import iotaa +import iotaa as _iotaa from uwtools.drivers.sfc_climo_gen import SfcClimoGen @@ -14,6 +14,7 @@ def execute( config_file: Path, batch: bool = False, dry_run: bool = False, + graph_file: Optional[Path] = None, ) -> bool: """ Execute an sfc_climo_gen task. @@ -25,18 +26,29 @@ def execute( :param config_file: Path to YAML config file :param batch: Submit run to the batch system :param dry_run: Do not run forecast, just report what would have been done + :param graph_file: Write Graphviz DOT output here :return: True if task completes without raising an exception """ obj = SfcClimoGen(config_file=config_file, batch=batch, dry_run=dry_run) getattr(obj, task)() + if graph_file: + with open(graph_file, "w", encoding="utf-8") as f: + print(graph(), file=f) return True +def graph() -> str: + """ + Returns Graphviz DOT code for the most recently executed task. + """ + return _iotaa.graph() + + def tasks() -> Dict[str, str]: """ Returns a mapping from task names to their one-line descriptions. """ return { task: getattr(SfcClimoGen, task).__doc__.strip().split("\n")[0] - for task in iotaa.tasknames(SfcClimoGen) + for task in _iotaa.tasknames(SfcClimoGen) } diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 8289e02fe..e9e173f62 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -249,8 +249,8 @@ def _dispatch_fv3(args: Args) -> bool: config_file=args[STR.cfgfile], cycle=args[STR.cycle], batch=args[STR.batch], - graph_file=args[STR.graphfile], dry_run=args[STR.dryrun], + graph_file=args[STR.graphfile], ) @@ -365,6 +365,7 @@ def _add_subparser_sfc_climo_gen_task( optional = _basic_setup(parser) _add_arg_batch(optional) _add_arg_dry_run(optional) + _add_arg_graph_file(optional) checks = _add_args_verbosity(optional) return checks @@ -380,6 +381,7 @@ def _dispatch_sfc_climo_gen(args: Args) -> bool: config_file=args[STR.cfgfile], batch=args[STR.batch], dry_run=args[STR.dryrun], + graph_file=args[STR.graphfile], ) diff --git a/src/uwtools/tests/api/test_fv3.py b/src/uwtools/tests/api/test_fv3.py index f33b7ec6a..eda0092f3 100644 --- a/src/uwtools/tests/api/test_fv3.py +++ b/src/uwtools/tests/api/test_fv3.py @@ -14,8 +14,8 @@ def test_execute(tmp_path): "config_file": "config.yaml", "cycle": dt.datetime.utcnow(), "batch": False, - "graph_file": dot, "dry_run": True, + "graph_file": dot, } with patch.object(fv3, "FV3") as FV3: assert fv3.execute(**args, task="foo") is True diff --git a/src/uwtools/tests/api/test_sfc_climo_gen.py b/src/uwtools/tests/api/test_sfc_climo_gen.py index 7cbc5342c..8f1e223eb 100644 --- a/src/uwtools/tests/api/test_sfc_climo_gen.py +++ b/src/uwtools/tests/api/test_sfc_climo_gen.py @@ -2,23 +2,36 @@ from unittest.mock import patch -from iotaa import external, task, tasks +from iotaa import asset, external, task, tasks from uwtools.api import sfc_climo_gen -def test_execute(): +def test_execute(tmp_path): + dot = tmp_path / "graph.dot" args: dict = { "config_file": "config.yaml", "batch": False, "dry_run": True, + "graph_file": dot, } with patch.object(sfc_climo_gen, "SfcClimoGen") as SfcClimoGen: assert sfc_climo_gen.execute(**args, task="foo") is True + del args["graph_file"] SfcClimoGen.assert_called_once_with(**args) SfcClimoGen().foo.assert_called_once_with() +def test_graph(): + @external + def ready(): + yield "ready" + yield asset("ready", lambda: True) + + ready() + assert sfc_climo_gen.graph().startswith("digraph") + + def test_tasks(): @external def t1(): diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py index 55f5e5b63..01f521886 100644 --- a/src/uwtools/tests/test_cli.py +++ b/src/uwtools/tests/test_cli.py @@ -295,8 +295,8 @@ def test__dispatch_fv3(): "batch": True, "config_file": "config.yaml", "cycle": dt.datetime.now(), - "graph_file": None, "dry_run": False, + "graph_file": None, } with patch.object(uwtools.api.fv3, "execute") as execute: cli._dispatch_fv3({**args, "action": "foo"}) @@ -357,6 +357,7 @@ def test__dispatch_sfc_climo_gen(): "batch": True, "config_file": "config.yaml", "dry_run": False, + "graph_file": None, } with patch.object(uwtools.api.sfc_climo_gen, "execute") as execute: cli._dispatch_sfc_climo_gen({**args, "action": "foo"}) From 9912e59a0b398ab2a6323c598f1d5b9df16cb0e1 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 27 Feb 2024 17:47:07 +0000 Subject: [PATCH 4/6] Use iotaa 0.7.3 --- recipe/meta.json | 4 ++-- recipe/meta.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/recipe/meta.json b/recipe/meta.json index 1bf44d204..bdf4eb33f 100644 --- a/recipe/meta.json +++ b/recipe/meta.json @@ -8,7 +8,7 @@ "coverage =7.3.*", "docformatter =1.7.*", "f90nml =1.4.*", - "iotaa =0.7.2.*", + "iotaa =0.7.3.*", "isort =5.13.*", "jinja2 =3.1.*", "jq =1.7.*", @@ -24,7 +24,7 @@ ], "run": [ "f90nml =1.4.*", - "iotaa =0.7.2.*", + "iotaa =0.7.3.*", "jinja2 =3.1.*", "jsonschema =4.20.*", "lxml =4.9.*", diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 55bd38d69..4ceaf4734 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -12,7 +12,7 @@ requirements: - pip run: - f90nml 1.4.* - - iotaa 0.7.2.* + - iotaa 0.7.3.* - jinja2 3.1.* - jsonschema 4.20.* - lxml 4.9.* From 621bb7e19cf73eb91317009b943625cfff9a01d7 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 27 Feb 2024 19:49:30 +0000 Subject: [PATCH 5/6] Mark graph switch experimental --- src/uwtools/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index e9e173f62..47352af52 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -556,7 +556,7 @@ def _add_arg_file_path(group: Group, switch: str, helpmsg: str, required: bool = def _add_arg_graph_file(group: Group) -> None: group.add_argument( _switch(STR.graphfile), - help="Path to Graphviz DOT output", + help="Path to Graphviz DOT output [experimental]", metavar="PATH", type=str, ) From 56ee6fa3d18bca8d057edd7e6f6588b6db7034ae Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 28 Feb 2024 15:56:01 +0000 Subject: [PATCH 6/6] Parse graph-file arg as Path --- src/uwtools/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 47352af52..0fbb78584 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -558,7 +558,7 @@ def _add_arg_graph_file(group: Group) -> None: _switch(STR.graphfile), help="Path to Graphviz DOT output [experimental]", metavar="PATH", - type=str, + type=Path, )