Skip to content

Commit e34952a

Browse files
authored
Add option to exit with error status if workflow is incomplete (#108)
1 parent 5cca902 commit e34952a

9 files changed

Lines changed: 52 additions & 22 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ Some values may include Jinja2 expressions, processed at run-time with [`jinja2.
286286

287287
```
288288
$ wxvx --help
289-
usage: wxvx [-c FILE] [-t TASK] [-d] [-h] [-k] [-l] [-n N] [-s] [-v]
289+
usage: wxvx [-c FILE] [-t TASK] [-d] [-f] [-h] [-k] [-l] [-n N] [-s] [-v]
290290

291291
wxvx
292292

@@ -299,6 +299,8 @@ Required arguments:
299299
Optional arguments:
300300
-d, --debug
301301
Log all messages
302+
-f, --fail
303+
Exit with error status if task is incomplete
302304
-h, --help
303305
Show help and exit
304306
-k, --check
@@ -313,6 +315,10 @@ Optional arguments:
313315
Show version and exit
314316
```
315317
318+
In many cases, for example when analyses or observations are not yet available to verify leading-edge forecast leadtimes, `wxvx` will not be able to complete the entire verification workflow in a single invocation, but to make partial progress each time it is iterated, to eventual completion. For this reason, `wxvx` defaults to exiting with `0` (success) status even if the requested task is incomplete.
319+
320+
In some (e.g. retrospective) cases, however, it may be expected that the entire workflow will complete in a single invocation, and that failure to do so is an error. In these cases, the `-f` / `--fail` switch may be used to tell `wxvx` to exit with `1` (error) status if the requested task is incomplete.
321+
316322
### Example
317323
318324
Consider a `config.yaml`

recipe/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@
4848
"zarr ==3.1.*"
4949
]
5050
},
51-
"version": "0.6.0"
51+
"version": "0.7.0"
5252
}

src/setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313

1414
recipe = os.environ.get("RECIPE_DIR", "../recipe")
1515
metasrc = Path(recipe, "meta.json")
16-
with metasrc.open() as f:
17-
meta = json.load(f)
16+
meta = json.loads(metasrc.read_text())
1817
name_conda = meta["name"]
1918
name_py = name_conda.replace("-", "_")
2019

src/wxvx/cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ def main() -> None:
3030
c = validated_config(yc)
3131
if args.check:
3232
sys.exit(0)
33-
logging.info("Preparing task graph for %s", args.task)
33+
logging.info("Preparing task graph for '%s'", args.task)
3434
task = getattr(workflow, args.task)
3535
if args.threads > 1:
3636
logging.info("Using %s threads", args.threads)
3737
initialize_pool(args.threads)
3838
initialize_session(args.threads)
39-
task(c, threads=args.threads)
39+
node = task(c, threads=args.threads)
40+
if args.fail and not node.ready:
41+
fail(f"Task '{args.task}' is incomplete")
4042
except WXVXError as e:
4143
for line in traceback.format_exc().strip().split("\n"):
4244
logging.debug(line)
@@ -85,6 +87,12 @@ def _parse_args(argv: list[str]) -> Namespace:
8587
action="store_true",
8688
help="Log all messages",
8789
)
90+
optional.add_argument(
91+
"-f",
92+
"--fail",
93+
action="store_true",
94+
help="Exit with error status if task is incomplete",
95+
)
8896
optional.add_argument(
8997
"-h",
9098
"--help",

src/wxvx/resources/info.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"buildnum": "0",
3-
"version": "0.6.0"
3+
"version": "0.7.0"
44
}

src/wxvx/tests/test_cli.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
Tests for wxvx.cli.
33
"""
44

5-
import logging
65
import re
76
from argparse import ArgumentTypeError, Namespace
87
from pathlib import Path
98
from unittest.mock import patch
109

1110
import yaml
11+
from iotaa import Asset, external
1212
from pytest import mark, raises
1313

14-
from wxvx import cli
14+
from wxvx import cli, workflow
1515
from wxvx.strings import S
1616
from wxvx.types import Config
1717
from wxvx.util import WXVXError, pkgname, resource_path
@@ -25,8 +25,7 @@
2525
@mark.parametrize("threads", [1, 2])
2626
def test_cli_main(config_data, logged, switch_c, switch_n, switch_t, threads, tmp_path):
2727
cfgfile = tmp_path / "config.yaml"
28-
with cfgfile.open("w") as f:
29-
yaml.safe_dump(config_data, f)
28+
cfgfile.write_text(yaml.safe_dump(config_data))
3029
argv = [pkgname, switch_c, str(cfgfile), switch_n, str(threads), switch_t, S.plots]
3130
with (
3231
patch.object(cli, "_parse_args", wraps=cli._parse_args) as _parse_args,
@@ -85,9 +84,30 @@ def test_cli_main__exception(logged):
8584
assert e.value.code == 1
8685

8786

87+
@mark.parametrize("switch", ["-f", "--fail", None])
88+
def test_cli_main__fail(config_data, switch, tmp_path):
89+
@external
90+
def bad(_):
91+
yield "bad"
92+
yield Asset(None, lambda: False)
93+
94+
cfgfile = tmp_path / "config.yaml"
95+
cfgfile.write_text(yaml.safe_dump(config_data))
96+
argv = [pkgname, "-c", str(cfgfile), "-t", "bad", switch]
97+
with (
98+
patch.object(cli.sys, "argv", list(filter(None, argv))),
99+
patch.object(workflow, "bad", create=True, new=bad),
100+
):
101+
if switch:
102+
with raises(SystemExit) as e:
103+
cli.main()
104+
assert e.value.code == 1
105+
else:
106+
cli.main()
107+
108+
88109
@mark.parametrize("switch", ["-l", "--list"])
89110
def test_cli_main__task_list(caplog, switch, tidy):
90-
caplog.set_level(logging.INFO)
91111
with (
92112
patch.object(
93113
cli.sys, "argv", [pkgname, "-c", str(resource_path("config-grid.yaml")), switch]
@@ -126,14 +146,13 @@ def test_cli_main__show_config(capsys, check, fs, switch):
126146
assert isinstance(yaml.safe_load(capsys.readouterr().out), dict)
127147

128148

129-
def test_cli_main__task_missing(caplog):
130-
caplog.set_level(logging.INFO)
149+
def test_cli_main__task_missing(logged):
131150
argv = [pkgname, "-c", str(resource_path("config-grid.yaml")), "-t", "foo"]
132151
with patch.object(cli.sys, "argv", argv), patch.object(cli, "use_uwtools_logger"):
133152
with raises(SystemExit) as e:
134153
cli.main()
135154
assert e.value.code == 1
136-
assert "No such task: foo" in caplog.messages
155+
assert logged("No such task: foo")
137156

138157

139158
def test_cli__arg_type_int_greater_than_zero__pass():

src/wxvx/tests/test_schema.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,7 @@ def validator(fs: FakeFilesystem, *args: Any) -> Callable:
372372
"""
373373
schema_path = resource_path("config.jsonschema")
374374
fs.add_real_file(schema_path)
375-
with schema_path.open() as f:
376-
schema = json.load(f)
375+
schema = json.loads(schema_path.read_text())
377376
defs = schema.get("$defs", {})
378377
for arg in args:
379378
schema = schema[arg]

src/wxvx/tests/test_util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,17 @@ def test_util_expand_stop_precedes_start(utc):
117117
assert str(e.value) == "Stop time 2024-12-19 06:00:00 precedes start time 2024-12-19 12:00:00"
118118

119119

120-
def test_util_fail(caplog):
120+
def test_util_fail(caplog, logged):
121121
caplog.set_level(logging.INFO)
122122
with raises(SystemExit) as e:
123123
util.fail()
124124
assert not caplog.messages
125125
with raises(SystemExit) as e:
126126
util.fail("foo")
127-
assert "foo" in caplog.messages
127+
assert logged("foo")
128128
with raises(SystemExit) as e:
129129
util.fail("foo %s", "bar")
130-
assert "foo bar" in caplog.messages
130+
assert logged("foo bar")
131131
assert e.value.code == 1
132132

133133

src/wxvx/util.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,7 @@ def render(template: str, tc: TimeCoords, context: dict | None = None) -> str:
178178

179179

180180
def resource(relpath: str | Path) -> str:
181-
with resource_path(relpath).open("r") as f:
182-
return f.read()
181+
return resource_path(relpath).read_text()
183182

184183

185184
def resource_path(relpath: str | Path) -> Path:

0 commit comments

Comments
 (0)