Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ Some values may include Jinja2 expressions, processed at run-time with [`jinja2.

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

wxvx

Expand All @@ -299,6 +299,8 @@ Required arguments:
Optional arguments:
-d, --debug
Log all messages
-f, --fail
Exit with error status if task is incomplete
-h, --help
Show help and exit
-k, --check
Expand All @@ -313,6 +315,10 @@ Optional arguments:
Show version and exit
```

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.

In some (e.g. retrospective) cases, however, it may 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.

### Example

Consider a `config.yaml`
Expand Down
2 changes: 1 addition & 1 deletion recipe/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@
"zarr ==3.1.*"
]
},
"version": "0.6.0"
"version": "0.7.0"
}
3 changes: 1 addition & 2 deletions src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

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

Expand Down
12 changes: 10 additions & 2 deletions src/wxvx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ def main() -> None:
c = validated_config(yc)
if args.check:
sys.exit(0)
logging.info("Preparing task graph for %s", args.task)
logging.info("Preparing task graph for '%s'", args.task)
task = getattr(workflow, args.task)
if args.threads > 1:
logging.info("Using %s threads", args.threads)
initialize_pool(args.threads)
initialize_session(args.threads)
task(c, threads=args.threads)
node = task(c, threads=args.threads)
if args.fail and not node.ready:
fail(f"Task '{args.task}' is incomplete")
Comment on lines +39 to +41
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main logic for the new feature.

except WXVXError as e:
for line in traceback.format_exc().strip().split("\n"):
logging.debug(line)
Expand Down Expand Up @@ -85,6 +87,12 @@ def _parse_args(argv: list[str]) -> Namespace:
action="store_true",
help="Log all messages",
)
optional.add_argument(
"-f",
"--fail",
action="store_true",
help="Exit with error status if task is incomplete",
)
Comment on lines +90 to +95
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the new feature to the CLI.

optional.add_argument(
"-h",
"--help",
Expand Down
2 changes: 1 addition & 1 deletion src/wxvx/resources/info.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"buildnum": "0",
"version": "0.6.0"
"version": "0.7.0"
}
35 changes: 27 additions & 8 deletions src/wxvx/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
Tests for wxvx.cli.
"""

import logging
import re
from argparse import ArgumentTypeError, Namespace
from pathlib import Path
from unittest.mock import patch

import yaml
from iotaa import Asset, external
from pytest import mark, raises

from wxvx import cli
from wxvx import cli, workflow
from wxvx.strings import S
from wxvx.types import Config
from wxvx.util import WXVXError, pkgname, resource_path
Expand All @@ -25,8 +25,7 @@
@mark.parametrize("threads", [1, 2])
def test_cli_main(config_data, logged, switch_c, switch_n, switch_t, threads, tmp_path):
cfgfile = tmp_path / "config.yaml"
with cfgfile.open("w") as f:
yaml.safe_dump(config_data, f)
cfgfile.write_text(yaml.safe_dump(config_data))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a few places, I simplified code slightly by using Path.read_text() or Path.write_text().

argv = [pkgname, switch_c, str(cfgfile), switch_n, str(threads), switch_t, S.plots]
with (
patch.object(cli, "_parse_args", wraps=cli._parse_args) as _parse_args,
Expand Down Expand Up @@ -85,9 +84,30 @@ def test_cli_main__exception(logged):
assert e.value.code == 1


@mark.parametrize("switch", ["-f", "--fail", None])
def test_cli_main__fail(config_data, switch, tmp_path):
@external
def bad(_):
yield "bad"
yield Asset(None, lambda: False)

cfgfile = tmp_path / "config.yaml"
cfgfile.write_text(yaml.safe_dump(config_data))
argv = [pkgname, "-c", str(cfgfile), "-t", "bad", switch]
with (
patch.object(cli.sys, "argv", list(filter(None, argv))),
patch.object(workflow, "bad", create=True, new=bad),
):
if switch:
with raises(SystemExit) as e:
cli.main()
assert e.value.code == 1
else:
cli.main()
Comment on lines +87 to +106
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test for the new feature.

I also verified manually with a real config and vx use case.



@mark.parametrize("switch", ["-l", "--list"])
def test_cli_main__task_list(caplog, switch, tidy):
caplog.set_level(logging.INFO)
with (
patch.object(
cli.sys, "argv", [pkgname, "-c", str(resource_path("config-grid.yaml")), switch]
Expand Down Expand Up @@ -126,14 +146,13 @@ def test_cli_main__show_config(capsys, check, fs, switch):
assert isinstance(yaml.safe_load(capsys.readouterr().out), dict)


def test_cli_main__task_missing(caplog):
caplog.set_level(logging.INFO)
def test_cli_main__task_missing(logged):
argv = [pkgname, "-c", str(resource_path("config-grid.yaml")), "-t", "foo"]
with patch.object(cli.sys, "argv", argv), patch.object(cli, "use_uwtools_logger"):
with raises(SystemExit) as e:
cli.main()
assert e.value.code == 1
assert "No such task: foo" in caplog.messages
assert logged("No such task: foo")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simplified a few tests by using the logged fixture instead of interacting with caplog directly.



def test_cli__arg_type_int_greater_than_zero__pass():
Expand Down
3 changes: 1 addition & 2 deletions src/wxvx/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,7 @@ def validator(fs: FakeFilesystem, *args: Any) -> Callable:
"""
schema_path = resource_path("config.jsonschema")
fs.add_real_file(schema_path)
with schema_path.open() as f:
schema = json.load(f)
schema = json.loads(schema_path.read_text())
defs = schema.get("$defs", {})
for arg in args:
schema = schema[arg]
Expand Down
6 changes: 3 additions & 3 deletions src/wxvx/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,17 @@ def test_util_expand_stop_precedes_start(utc):
assert str(e.value) == "Stop time 2024-12-19 06:00:00 precedes start time 2024-12-19 12:00:00"


def test_util_fail(caplog):
def test_util_fail(caplog, logged):
caplog.set_level(logging.INFO)
with raises(SystemExit) as e:
util.fail()
assert not caplog.messages
with raises(SystemExit) as e:
util.fail("foo")
assert "foo" in caplog.messages
assert logged("foo")
with raises(SystemExit) as e:
util.fail("foo %s", "bar")
assert "foo bar" in caplog.messages
assert logged("foo bar")
assert e.value.code == 1


Expand Down
3 changes: 1 addition & 2 deletions src/wxvx/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ def render(template: str, tc: TimeCoords, context: dict | None = None) -> str:


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


def resource_path(relpath: str | Path) -> Path:
Expand Down