Skip to content

Commit

Permalink
Add a !datetime tag. (#597)
Browse files Browse the repository at this point in the history
Adds a !datetime tag and makes all tags lowercase.

Fixes user-identified gap identified in Discussion #595.

---------

Co-authored-by: Paul Madden <[email protected]>
  • Loading branch information
christinaholtNOAA and maddenp-noaa authored Sep 3, 2024
1 parent 506f50a commit dc9846d
Show file tree
Hide file tree
Showing 12 changed files with 68 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"noaa": ("https://www.noaa.gov/%s", "%s"),
"pylint": ("https://pylint.readthedocs.io/en/stable/%s", "%s"),
"pytest": ("https://docs.pytest.org/en/7.4.x/%s", "%s"),
"python": ("https://docs.python.org/3/library/%s", "%s"),
"rocoto": ("https://christopherwharrop.github.io/rocoto/%s", "%s"),
"rst": ("https://www.sphinx-doc.org/en/master/usage/restructuredtext/%s", "%s"),
"rtd": ("https://readthedocs.org/projects/uwtools/%s", "%s"),
Expand Down
20 changes: 19 additions & 1 deletion docs/sections/user_guide/yaml/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ Or explicit:
Additionally, UW defines the following tags to support use cases not covered by standard tags:

``!datetime``
^^^^^^^^^^^^^

Converts the tagged node to a Python ``datetime`` object. For example, given ``input.yaml``:

.. code-block:: yaml
date1: 2024-09-01
date2: !datetime "{{ date1 }}"
.. code-block:: text
% uw config realize -i ../input.yaml --output-format yaml
date1: 2024-09-01
date2: 2024-09-01 00:00:00
The value provided to the tag must be in :python:`ISO 8601 format<datetime.html#datetime.datetime.fromisoformat>` to be interpreted correctly by the ``!datetime`` tag.

``!float``
^^^^^^^^^^

Expand Down Expand Up @@ -62,7 +80,7 @@ Parse the tagged file and include its tags. For example, given ``input.yaml``:

.. code-block:: yaml
values: !INCLUDE [./supplemental.yaml]
values: !include [./supplemental.yaml]
and ``supplemental.yaml``:

Expand Down
3 changes: 2 additions & 1 deletion src/uwtools/config/jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
from datetime import datetime
from functools import cached_property
from pathlib import Path
from typing import Optional, Union
Expand All @@ -14,7 +15,7 @@
from uwtools.logging import INDENT, MSGWIDTH, log
from uwtools.utils.file import get_file_format, readable, writable

_ConfigVal = Union[bool, dict, float, int, list, str, UWYAMLConvert, UWYAMLRemove]
_ConfigVal = Union[bool, datetime, dict, float, int, list, str, UWYAMLConvert, UWYAMLRemove]


class J2Template:
Expand Down
13 changes: 8 additions & 5 deletions src/uwtools/config/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

import math
from collections import OrderedDict
from datetime import datetime
from importlib import import_module
from typing import Type, Union
from typing import Callable, Type, Union

import yaml

from uwtools.exceptions import UWConfigError
from uwtools.logging import log
from uwtools.strings import FORMAT

INCLUDE_TAG = "!INCLUDE"
INCLUDE_TAG = "!include"


# Public functions
Expand Down Expand Up @@ -107,15 +108,17 @@ class UWYAMLConvert(UWYAMLTag):
method. See the pyyaml documentation for details.
"""

TAGS = ("!float", "!int")
TAGS = ("!datetime", "!float", "!int")

def convert(self) -> Union[float, int]:
def convert(self) -> Union[datetime, float, int]:
"""
Return the original YAML value converted to the specified type.
Will raise an exception if the value cannot be represented as the specified type.
"""
converters: dict[str, Union[type[float], type[int]]] = dict(zip(self.TAGS, [float, int]))
converters: dict[str, Union[Callable[[str], datetime], type[float], type[int]]] = dict(
zip(self.TAGS, [datetime.fromisoformat, float, int])
)
return converters[self.tag](self.value)


Expand Down
10 changes: 8 additions & 2 deletions src/uwtools/tests/config/formats/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import datetime as dt
import logging
import os
from datetime import datetime
from unittest.mock import patch

import yaml
Expand Down Expand Up @@ -97,7 +98,7 @@ def test__parse_include(config):
config.data.update(
{
"config": {
"salad_include": f"!INCLUDE [{include_path}]",
"salad_include": f"!include [{include_path}]",
"meat": "beef",
"dressing": "poppyseed",
}
Expand Down Expand Up @@ -155,23 +156,28 @@ def test_dereference(tmp_path):
e:
- !int '42'
- !float '3.14'
- !datetime '{{ D }}'
f:
f1: !int '42'
f2: !float '3.14'
D: 2024-10-10 00:19:00
N: "22"
""".strip()
path = tmp_path / "config.yaml"
with open(path, "w", encoding="utf-8") as f:
print(yaml, file=f)
config = YAMLConfig(path)
with patch.dict(os.environ, {"N": "999"}, clear=True):
config.dereference()
print(config["e"])
assert config == {
"a": 44,
"b": {"c": 33},
"d": "{{ X }}",
"e": [42, 3.14],
"e": [42, 3.14, datetime.fromisoformat("2024-10-10 00:19:00")],
"f": {"f1": 42, "f2": 3.14},
"D": datetime.fromisoformat("2024-10-10 00:19:00"),
"N": "22",
}

Expand Down
12 changes: 10 additions & 2 deletions src/uwtools/tests/config/test_jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import logging
import os
from datetime import datetime
from io import StringIO
from textwrap import dedent
from types import SimpleNamespace as ns
Expand Down Expand Up @@ -280,7 +281,7 @@ def test_unrendered(s, status):
assert jinja2.unrendered(s) is status


@mark.parametrize("tag", ["!float", "!int"])
@mark.parametrize("tag", ["!datetime", "!float", "!int"])
def test__deref_convert_no(caplog, tag):
log.setLevel(logging.DEBUG)
loader = yaml.SafeLoader(os.devnull)
Expand All @@ -290,7 +291,14 @@ def test__deref_convert_no(caplog, tag):
assert regex_logged(caplog, "Conversion failed")


@mark.parametrize("converted,tag,value", [(3.14, "!float", "3.14"), (42, "!int", "42")])
@mark.parametrize(
"converted,tag,value",
[
(datetime(2024, 9, 9, 0, 0), "!datetime", "2024-09-09 00:00:00"),
(3.14, "!float", "3.14"),
(42, "!int", "42"),
],
)
def test__deref_convert_ok(caplog, converted, tag, value):
log.setLevel(logging.DEBUG)
loader = yaml.SafeLoader(os.devnull)
Expand Down
13 changes: 13 additions & 0 deletions src/uwtools/tests/config/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import logging
from collections import OrderedDict
from datetime import datetime

import yaml
from pytest import fixture, mark, raises
Expand Down Expand Up @@ -87,6 +88,18 @@ def loader(self):
# demonstrate that those nodes' convert() methods return representations in type type specified
# by the tag.

def test_datetime_no(self, loader):
ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!datetime", value="foo"))
with raises(ValueError):
ts.convert()

def test_datetime_ok(self, loader):
ts = support.UWYAMLConvert(
loader, yaml.ScalarNode(tag="!datetime", value="2024-08-09 12:22:42")
)
assert ts.convert() == datetime(2024, 8, 9, 12, 22, 42)
self.comp(ts, "!datetime '2024-08-09 12:22:42'")

def test_float_no(self, loader):
ts = support.UWYAMLConvert(loader, yaml.ScalarNode(tag="!float", value="foo"))
with raises(ValueError):
Expand Down
2 changes: 1 addition & 1 deletion src/uwtools/tests/fixtures/include_files.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[config]
salad_include = !INCLUDE [./fruit_config.ini]
salad_include = !include [./fruit_config.ini]
meat = beef
dressing = poppyseed
2 changes: 1 addition & 1 deletion src/uwtools/tests/fixtures/include_files.nml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
&config
salad_include = '!INCLUDE [./fruit_config.nml]'
salad_include = '!include [./fruit_config.nml]'
meat = beef
dressing = poppyseed
/
2 changes: 1 addition & 1 deletion src/uwtools/tests/fixtures/include_files.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
salad_include="!INCLUDE [./fruit_config.sh]"
salad_include="!include [./fruit_config.sh]"
meat=beef
dressing=poppyseed
6 changes: 3 additions & 3 deletions src/uwtools/tests/fixtures/include_files.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
salad: !INCLUDE [./fruit_config.yaml]
two_files: !INCLUDE [./fruit_config.yaml, ./fruit_config_similar.yaml]
reverse_files: !INCLUDE [./fruit_config_similar.yaml, ./fruit_config.yaml]
salad: !include [./fruit_config.yaml]
two_files: !include [./fruit_config.yaml, ./fruit_config_similar.yaml]
reverse_files: !include [./fruit_config_similar.yaml, ./fruit_config.yaml]
2 changes: 1 addition & 1 deletion src/uwtools/tests/fixtures/include_files_with_sect.nml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
&config
salad_include = '!INCLUDE [./fruit_config_mult_sect.nml]'
salad_include = '!include [./fruit_config_mult_sect.nml]'
meat = beef
dressing = poppyseed
/

0 comments on commit dc9846d

Please sign in to comment.