Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a !datetime tag. #597

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
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 ISO 8601 format to be interpreted correctly by the ``!datetime`` tag.
christinaholtNOAA marked this conversation as resolved.
Show resolved Hide resolved

``!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
/
Loading