Skip to content
9 changes: 9 additions & 0 deletions docs/usage/yaml_dev/model_yaml.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
The model yaml defines reusable variables and paths to compile, post-processing, analysis, and cmor yamls. Required fields in the model yaml include: ``fre_properties``, ``build``, and ``experiments``.

* `fre_cli_version`: Version of fre-cli this yaml is compatible with

- value type: string
- when specified, fre-cli will verify that the installed version matches this value
- if the versions do not match, a clear error message will be displayed
- if not specified, a warning will be logged recommending its addition

* `fre_properties`: Reusable variables

- list of variables
Expand All @@ -14,6 +21,8 @@ The model.yaml can follow the structure below:

.. code-block::

fre_cli_version: "2025.04"

fre_properties:
- &variable1 "value1" (string)
- &variable2 "value2" (string)
Expand Down
6 changes: 5 additions & 1 deletion fre/yamltools/combine_yamls_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from fre.yamltools.info_parsers import compile_info_parser as cip
from fre.yamltools.info_parsers import pp_info_parser as ppip
from fre.yamltools.info_parsers import analysis_info_parser as aip
from fre.yamltools.helpers import output_yaml
from fre.yamltools.helpers import output_yaml, check_fre_version

from . import *

Expand Down Expand Up @@ -161,6 +161,10 @@ def consolidate_yamls(yamlfile:str, experiment:str, platform:str,
..note:: The output file name should include a .yaml extension to indicate
it is a YAML configuration file
"""
# Check fre_cli_version compatibility before any YAML combining
fre_logger.info('checking fre_cli_version compatibility...')
check_fre_version(yamlfile)

if use == "compile":
fre_logger.info('initializing a compile yaml instance...')
compilecombined = cip.InitCompileYaml(yamlfile, platform, target)
Expand Down
43 changes: 42 additions & 1 deletion fre/yamltools/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from . import *


import fre

fre_logger = logging.getLogger(__name__)

def yaml_load(yamlfile):
Expand All @@ -33,6 +35,45 @@ def yaml_load(yamlfile):
return y


def check_fre_version(yamlfile: str) -> None:
"""
Check that the fre_cli_version specified in the model yaml matches the
installed version of fre-cli. If fre_cli_version is not specified, a
warning is logged. If fre_cli_version does not match, a ValueError is raised.

:param yamlfile: Path to model YAML configuration file
:type yamlfile: str
:raises ValueError: if the fre_cli_version in the YAML does not match the installed fre-cli version
"""
try:
loaded_yaml = yaml_load(yamlfile)
except Exception as exc:
fre_logger.warning(
"Could not load %s for fre_cli_version check: %s",
yamlfile, exc
)
return

yaml_fre_version = loaded_yaml.get("fre_cli_version")
installed_version = fre.version

if yaml_fre_version is None:
fre_logger.info(
"fre_cli_version not specified in %s. "
"It is recommended to add 'fre_cli_version' to your model yaml "
"to ensure compatibility with the correct version of fre-cli.",
yamlfile
)
return

if str(yaml_fre_version) != str(installed_version):
raise ValueError(
f"The fre_cli_version specified in the model yaml ({yaml_fre_version}) "
f"does not match the installed version of fre-cli ({installed_version}). "
f"Please update your model yaml or install the correct version of fre-cli."
)


def output_yaml(cleaned_yaml, output):
"""
Write out the combined yaml dictionary info
Expand Down Expand Up @@ -131,7 +172,7 @@ def clean_yaml(yml_dict):
"""
# Clean the yaml
# If keys exists, delete:
keys_clean=["fre_properties", "experiments"]
keys_clean=["fre_properties", "fre_cli_version", "experiments"]
for kc in keys_clean:
if kc in yml_dict.keys():
del yml_dict[kc]
Expand Down
47 changes: 46 additions & 1 deletion fre/yamltools/tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import pytest
import yaml

from fre.yamltools.helpers import yaml_load
import fre
from fre.yamltools.helpers import yaml_load, check_fre_version


@pytest.fixture
Expand All @@ -24,3 +25,47 @@ def test_yaml_load_reads_yaml_file_correctly(temp_path):
def test_yaml_load_raises_file_not_found():
with pytest.raises(FileNotFoundError):
yaml_load("this_file_should_not_exist.yml")

## fre_cli_version checks
@pytest.fixture
def yaml_with_matching_version(tmp_path):
"""Create a YAML file with the correct fre_cli_version."""
data = {'fre_cli_version': fre.version, 'fre_properties': []}
path = tmp_path / "matching_version.yaml"
with open(path, 'w') as f:
yaml.dump(data, f)
return str(path)

@pytest.fixture
def yaml_with_wrong_version(tmp_path):
"""Create a YAML file with an incorrect fre_cli_version."""
data = {'fre_cli_version': '0000.00', 'fre_properties': []}
path = tmp_path / "wrong_version.yaml"
with open(path, 'w') as f:
yaml.dump(data, f)
return str(path)

@pytest.fixture
def yaml_without_version(tmp_path):
"""Create a YAML file without fre_cli_version."""
data = {'fre_properties': []}
path = tmp_path / "no_version.yaml"
with open(path, 'w') as f:
yaml.dump(data, f)
return str(path)

def test_check_fre_version_matching(yaml_with_matching_version):
"""check_fre_version should pass when fre_cli_version matches installed version."""
check_fre_version(yaml_with_matching_version)

def test_check_fre_version_mismatch(yaml_with_wrong_version):
"""check_fre_version should raise ValueError when fre_cli_version does not match."""
with pytest.raises(ValueError, match="does not match the installed version"):
check_fre_version(yaml_with_wrong_version)

def test_check_fre_version_missing(yaml_without_version, caplog):
"""check_fre_version should log info but not error when fre_cli_version is missing."""
import logging
with caplog.at_level(logging.INFO):
check_fre_version(yaml_without_version)
assert "fre_cli_version not specified" in caplog.text
Loading