diff --git a/recipe/meta.json b/recipe/meta.json index 8a55437b5..767c182c6 100644 --- a/recipe/meta.json +++ b/recipe/meta.json @@ -36,5 +36,5 @@ "requests >=2.32,<2.33" ] }, - "version": "2.12.1" + "version": "2.12.2" } diff --git a/src/uwtools/config/support.py b/src/uwtools/config/support.py index 8947d13d5..9ca6b02f2 100644 --- a/src/uwtools/config/support.py +++ b/src/uwtools/config/support.py @@ -51,6 +51,16 @@ def add_yaml_representers() -> None: """ Add representers to the YAML dumper for custom types. """ + + def timedelta2str(dumper: yaml.dumper.Dumper, data: timedelta) -> yaml.ScalarNode: + seconds = int(data.total_seconds()) + hours = seconds // 3600 + seconds %= 3600 + minutes = seconds // 60 + seconds %= 60 + s = f"{hours}:{minutes:02d}:{seconds:02d}" + return dumper.represent_scalar("!timedelta", s) + yaml.add_representer(Namelist, _represent_namelist) yaml.add_representer(OrderedDict, _represent_ordereddict) yaml.add_representer( @@ -59,10 +69,7 @@ def add_yaml_representers() -> None: "tag:yaml.org,2002:timestamp", to_iso8601(data) ), ) - yaml.add_representer( - timedelta, - lambda dumper, data: dumper.represent_scalar("!timedelta", str(data)), - ) + yaml.add_representer(timedelta, timedelta2str) for tag_class in [UWYAMLConvert, UWYAMLGlob, UWYAMLRemove]: yaml.add_representer(tag_class, tag_class.represent) diff --git a/src/uwtools/resources/info.json b/src/uwtools/resources/info.json index d231de7c8..d82b3afce 100644 --- a/src/uwtools/resources/info.json +++ b/src/uwtools/resources/info.json @@ -1,4 +1,4 @@ { "buildnum": "0", - "version": "2.12.1" + "version": "2.12.2" } diff --git a/src/uwtools/tests/config/test_support.py b/src/uwtools/tests/config/test_support.py index 657db569d..5d903caf1 100644 --- a/src/uwtools/tests/config/test_support.py +++ b/src/uwtools/tests/config/test_support.py @@ -4,6 +4,7 @@ from collections import OrderedDict from datetime import datetime, timedelta +from textwrap import dedent import f90nml # type: ignore[import-untyped] import yaml @@ -54,6 +55,22 @@ def test_config_support_add_yaml_representers__represent_ordereddict(): assert yaml.dump(ordereddict_values, default_flow_style=True).strip() == expected +def test_config_support_add_yaml_representers__represent_timedelta(tmp_path): + support.add_yaml_representers() + s = """ + start: !datetime 2025-01-01T00 + stop: !datetime 2025-01-03T03 + raw: !timedelta '{{ ((stop - start) * 0.8).total_seconds() / 3600 }}' + """ + path = tmp_path / "config.yaml" + path.write_text(dedent(s)) + config = YAMLConfig(config=path) + config.dereference() + assert config["raw"] == timedelta(days=1, seconds=60480) + config.dump(path) + assert path.read_text().strip().split("\n")[-1] == "raw: !timedelta '40:48:00'" + + @mark.parametrize( ("d", "n"), [({1: 42}, 1), ({1: {2: 42}}, 2), ({1: {2: {3: 42}}}, 3), ({1: {}}, 2)] ) @@ -182,6 +199,17 @@ def test_UWYAMLConvert_list_ok(self, loader): assert ts.converted == [1, 2, 3] self.comp(ts, "!list '[1,2,3,]'") + def test_UWYAMLConvert__timedelta_no(self, loader): + ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!timedelta", value="null")) + with raises(ValueError, match="could not convert string to float"): + assert ts.converted + + @mark.parametrize("value", ["49", "49:00", "49:00:00"]) + def test_UWYAMLConvert__timedelta_ok(self, loader, value): + ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!timedelta", value=value)) + assert ts.converted == timedelta(hours=49) + self.comp(ts, f"!timedelta '{value}'") + def test_UWYAMLConvert_tagged_string(self, loader): ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!list", value="{{ foo }}")) assert ts.tagged_string == "!list '{{ foo }}'"