diff --git a/docs/sections/user_guide/cli/drivers/upp/run-help.out b/docs/sections/user_guide/cli/drivers/upp/run-help.out index 2e0c9206b..d727fd2ad 100644 --- a/docs/sections/user_guide/cli/drivers/upp/run-help.out +++ b/docs/sections/user_guide/cli/drivers/upp/run-help.out @@ -9,7 +9,7 @@ Required arguments: --cycle CYCLE The cycle in ISO8601 format (e.g. 2024-05-23T18) --leadtime LEADTIME - The leadtime as HH[:MM[:SS]] + The leadtime as hours[:minutes[:seconds]] Optional arguments: -h, --help diff --git a/docs/sections/user_guide/cli/tools/file/copy-help.out b/docs/sections/user_guide/cli/tools/file/copy-help.out index a38f16b9f..8afa28e7f 100644 --- a/docs/sections/user_guide/cli/tools/file/copy-help.out +++ b/docs/sections/user_guide/cli/tools/file/copy-help.out @@ -19,7 +19,7 @@ Optional arguments: --cycle CYCLE The cycle in ISO8601 format (e.g. 2024-05-29T12) --leadtime LEADTIME - The leadtime as HH[:MM[:SS]] + The leadtime as hours[:minutes[:seconds]] --dry-run Only log info, making no changes --quiet, -q diff --git a/docs/sections/user_guide/cli/tools/file/link-help.out b/docs/sections/user_guide/cli/tools/file/link-help.out index 45a774402..633ce6d8c 100644 --- a/docs/sections/user_guide/cli/tools/file/link-help.out +++ b/docs/sections/user_guide/cli/tools/file/link-help.out @@ -19,7 +19,7 @@ Optional arguments: --cycle CYCLE The cycle in ISO8601 format (e.g. 2024-05-29T12) --leadtime LEADTIME - The leadtime as HH[:MM[:SS]] + The leadtime as hours[:minutes[:seconds]] --dry-run Only log info, making no changes --quiet, -q diff --git a/recipe/meta.json b/recipe/meta.json index 09cf57924..05052a59a 100644 --- a/recipe/meta.json +++ b/recipe/meta.json @@ -32,5 +32,5 @@ "pyyaml =6.0.*" ] }, - "version": "2.3.1" + "version": "2.3.2" } diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 0fa176f1b..82146cb18 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -4,6 +4,7 @@ import datetime as dt import json +import re import sys from argparse import ArgumentParser as Parser from argparse import HelpFormatter @@ -27,7 +28,7 @@ from uwtools.utils.file import get_file_format, resource_path FORMATS = FORMAT.extensions() -LEADTIME_DESC = "HH[:MM[:SS]]" +LEADTIME_DESC = "hours[:minutes[:seconds]]" TITLE_REQ_ARG = "Required arguments" Args = Dict[str, Any] @@ -995,7 +996,7 @@ def _dispatch_to_driver(name: str, args: Args) -> bool: } if cycle := args.get(STR.cycle): kwargs[STR.cycle] = cycle - if leadtime := args.get(STR.leadtime): + if (leadtime := args.get(STR.leadtime)) is not None: kwargs[STR.leadtime] = leadtime return execute(**kwargs) @@ -1077,13 +1078,9 @@ def _timedelta_from_str(tds: str) -> dt.timedelta: :param tds: The timedelta string to parse. """ - fmts = ("%H:%M:%S", "%H:%M", "%H") - for fmt in fmts: - try: - t = dt.datetime.strptime(tds, fmt) - return dt.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) - except ValueError: - pass + if matches := re.match(r"(\d+)(:(\d+))?(:(\d+))?", tds): + h, m, s = [int(matches.groups()[n] or 0) for n in (0, 2, 4)] + return dt.timedelta(hours=h, minutes=m, seconds=s) _abort(f"Specify leadtime as {LEADTIME_DESC}") diff --git a/src/uwtools/drivers/chgres_cube.py b/src/uwtools/drivers/chgres_cube.py index e429025d2..a37b1639e 100644 --- a/src/uwtools/drivers/chgres_cube.py +++ b/src/uwtools/drivers/chgres_cube.py @@ -59,6 +59,7 @@ def namelist_file(self): ] + [ Path(config_files["data_dir_input_grid"]) / config_files[k] for k in ("atm_files_input_grid", "grib2_file_input_grid", "sfc_files_input_grid") + if k in config_files ] yield [file(input_path) for input_path in input_paths] self._create_user_updated_config( diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py index 5de6330a4..5a61f5a4a 100644 --- a/src/uwtools/drivers/driver.py +++ b/src/uwtools/drivers/driver.py @@ -51,12 +51,13 @@ def __init__( dryrun(enable=dry_run) self._config = YAMLConfig(config=config) self._batch = batch - if leadtime and not cycle: + has_leadtime = leadtime is not None + if has_leadtime and not cycle: raise UWError("When leadtime is specified, cycle is required") self._config.dereference( context={ **({"cycle": cycle} if cycle else {}), - **({"leadtime": leadtime} if leadtime else {}), + **({"leadtime": leadtime} if has_leadtime else {}), **self._config.data, } ) diff --git a/src/uwtools/resources/info.json b/src/uwtools/resources/info.json index 948e92c4d..c5a6a6831 100644 --- a/src/uwtools/resources/info.json +++ b/src/uwtools/resources/info.json @@ -1,4 +1,4 @@ { - "version": "2.3.1", + "version": "2.3.2", "buildnum": "0" } diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py index eff8efa59..d09e12967 100644 --- a/src/uwtools/tests/drivers/test_driver.py +++ b/src/uwtools/tests/drivers/test_driver.py @@ -129,7 +129,7 @@ def driverobj(config): ) -# Asset Tests +# Assets Tests def test_Assets(assetobj): @@ -137,14 +137,15 @@ def test_Assets(assetobj): assert assetobj._batch is True -def test_Asset_cycle_leadtime_error(config): +@pytest.mark.parametrize("hours", [0, 24, 168]) +def test_Assets_cycle_leadtime_error(config, hours): with raises(UWError) as e: - ConcreteAssets(config=config, leadtime=dt.timedelta(hours=24)) + ConcreteAssets(config=config, leadtime=dt.timedelta(hours=hours)) assert "When leadtime is specified, cycle is required" in str(e) @pytest.mark.parametrize("val", (True, False)) -def test_Asset_dry_run(config, val): +def test_Assets_dry_run(config, val): with patch.object(driver, "dryrun") as dryrun: ConcreteAssets(config=config, dry_run=val) dryrun.assert_called_once_with(enable=val) @@ -165,7 +166,7 @@ def test_key_path(config): assert config == assetobj._config -def test_Asset_validate(caplog, assetobj): +def test_Assets_validate(assetobj, caplog): log.setLevel(logging.INFO) assetobj.validate() assert regex_logged(caplog, "State: Ready") @@ -183,8 +184,8 @@ def test_Asset_validate(caplog, assetobj): (True, True, {"a": 33, "b": 22}), ], ) -def test_Asset__create_user_updated_config_base_file( - base_file, assetobj, expected, tmp_path, update_values +def test_Assets__create_user_updated_config_base_file( + assetobj, base_file, expected, tmp_path, update_values ): path = tmp_path / "updated.yaml" dc = assetobj._driver_config @@ -198,14 +199,14 @@ def test_Asset__create_user_updated_config_base_file( assert updated == expected -def test_Asset__driver_config_fail(assetobj): +def test_Assets__driver_config_fail(assetobj): del assetobj._config["concrete"] with raises(UWConfigError) as e: assert assetobj._driver_config assert str(e.value) == "Required 'concrete' block missing in config" -def test_Asset__driver_config_pass(assetobj): +def test_Assets__driver_config_pass(assetobj): assert set(assetobj._driver_config.keys()) == { "base_file", "execution", @@ -214,11 +215,11 @@ def test_Asset__driver_config_pass(assetobj): } -def test_Asset__rundir(assetobj): - assert assetobj._rundir == Path("/path/to/2024032218/run") +def test_Assets__rundir(assetobj): + assert assetobj._rundir == Path(assetobj._driver_config["run_dir"]) -def test_Asset__validate(assetobj): +def test_Assets__validate(assetobj): with patch.object(assetobj, "_validate", driver.Assets._validate): with patch.object(driver, "validate_internal") as validate_internal: assetobj._validate(assetobj) diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py index 9dac1f6a9..abb0b4e8c 100644 --- a/src/uwtools/tests/test_cli.py +++ b/src/uwtools/tests/test_cli.py @@ -545,10 +545,11 @@ def test__dispatch_template_translate_no_optional(): ) -def test__dispatch_to_driver(): +@pytest.mark.parametrize("hours", [0, 24, 168]) +def test__dispatch_to_driver(hours): name = "adriver" cycle = dt.datetime.now() - leadtime = dt.timedelta(hours=24) + leadtime = dt.timedelta(hours=hours) args: dict = { "action": "foo", "batch": True, @@ -646,9 +647,10 @@ def test__switch(): def test__timedelta_from_str(capsys): - assert cli._timedelta_from_str("11:12:13") == dt.timedelta(hours=11, minutes=12, seconds=13) - assert cli._timedelta_from_str("11:12") == dt.timedelta(hours=11, minutes=12) - assert cli._timedelta_from_str("11") == dt.timedelta(hours=11) + assert cli._timedelta_from_str("111:222:333").total_seconds() == 111 * 3600 + 222 * 60 + 333 + assert cli._timedelta_from_str("111:222").total_seconds() == 111 * 3600 + 222 * 60 + assert cli._timedelta_from_str("111").total_seconds() == 111 * 3600 + assert cli._timedelta_from_str("01:15:07").total_seconds() == 1 * 3600 + 15 * 60 + 7 with raises(SystemExit): cli._timedelta_from_str("foo") assert f"Specify leadtime as {cli.LEADTIME_DESC}" in capsys.readouterr().err diff --git a/src/uwtools/tests/utils/test_api.py b/src/uwtools/tests/utils/test_api.py index cab2f799b..ba19afd92 100644 --- a/src/uwtools/tests/utils/test_api.py +++ b/src/uwtools/tests/utils/test_api.py @@ -5,10 +5,10 @@ from unittest.mock import patch import pytest -import yaml from pytest import fixture, raises from uwtools.exceptions import UWError +from uwtools.tests.drivers import test_driver from uwtools.tests.drivers.test_driver import ConcreteDriver from uwtools.utils import api @@ -118,20 +118,19 @@ def test_str2path_convert(): assert result == Path(val) -def test__execute(execute_kwargs, tmp_path): - config = tmp_path / "config.yaml" - with open(config, "w", encoding="utf-8") as f: - yaml.dump({"some": "config"}, f) +@pytest.mark.parametrize("hours", [0, 24, 168]) +def test__execute(execute_kwargs, hours, tmp_path): graph_file = tmp_path / "g.dot" - kwargs = { - **execute_kwargs, - "driver_class": ConcreteDriver, - "config": config, - "cycle": dt.datetime.now(), - "leadtime": dt.timedelta(hours=24), - "graph_file": graph_file, - } - assert not graph_file.is_file() - with patch.object(ConcreteDriver, "_validate"): + with patch.object(test_driver, "ConcreteDriver", wraps=test_driver.ConcreteDriver) as cd: + kwargs = { + **execute_kwargs, + "driver_class": cd, + "config": {"some": "config"}, + "cycle": dt.datetime.now(), + "leadtime": dt.timedelta(hours=hours), + "graph_file": graph_file, + } + assert not graph_file.is_file() assert api._execute(**kwargs) is True + assert cd.call_args.kwargs["leadtime"] == dt.timedelta(hours=hours) assert graph_file.is_file() diff --git a/src/uwtools/utils/api.py b/src/uwtools/utils/api.py index 6e36fb205..6f93898c1 100644 --- a/src/uwtools/utils/api.py +++ b/src/uwtools/utils/api.py @@ -199,7 +199,7 @@ def _execute( ) if cycle: kwargs["cycle"] = cycle - if leadtime: + if leadtime is not None: kwargs["leadtime"] = leadtime obj = driver_class(**kwargs) getattr(obj, task)()