diff --git a/docs/usage/yaml_dev/model_yaml.rst b/docs/usage/yaml_dev/model_yaml.rst index 636519dc9..ea461be7a 100644 --- a/docs/usage/yaml_dev/model_yaml.rst +++ b/docs/usage/yaml_dev/model_yaml.rst @@ -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 @@ -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) diff --git a/fre/yamltools/combine_yamls_script.py b/fre/yamltools/combine_yamls_script.py index 3b4af30a8..2cac51ff9 100755 --- a/fre/yamltools/combine_yamls_script.py +++ b/fre/yamltools/combine_yamls_script.py @@ -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 * @@ -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) diff --git a/fre/yamltools/helpers.py b/fre/yamltools/helpers.py index 858458ec2..dd7f748ac 100644 --- a/fre/yamltools/helpers.py +++ b/fre/yamltools/helpers.py @@ -15,6 +15,8 @@ from . import * +import fre + fre_logger = logging.getLogger(__name__) def yaml_load(yamlfile): @@ -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 @@ -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] diff --git a/fre/yamltools/tests/test_helpers.py b/fre/yamltools/tests/test_helpers.py index bd7a50bb2..5a8eb1810 100644 --- a/fre/yamltools/tests/test_helpers.py +++ b/fre/yamltools/tests/test_helpers.py @@ -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 @@ -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