From 38642d6b33821d0fe4c178cf0689b5ba3c05a6e4 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Mon, 31 Aug 2026 20:44:11 +0200 Subject: [PATCH 1/4] Towards #20: Ignore missing input ids in LPJmLInputType --- pycoupler/coupler.py | 6 +++--- pycoupler/data.py | 46 ++++++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pycoupler/coupler.py b/pycoupler/coupler.py index c968036..35a26fc 100644 --- a/pycoupler/coupler.py +++ b/pycoupler/coupler.py @@ -1043,7 +1043,7 @@ def _init_input(self): self._input_ids = { input_sockets[inp]["id"]: inp for inp in input_sockets - if input_sockets[inp]["id"] in LPJmLInputType.ids + if input_sockets[inp]["id"] in LPJmLInputType.ids_to_names } # send number of bands for each output data stream @@ -1172,7 +1172,7 @@ def _get_config_input_sockets(self): sockets = self._config.get_input_sockets() socket_ids = [sock_id["id"] for sock_id in sockets.values()] # filter input names - input_ids = [inp for inp in LPJmLInputType.ids] + input_ids = [inp for inp in LPJmLInputType.ids_to_names] # check if input is defined in LPJmLInputType (band size required) valid_inputs = { @@ -1184,7 +1184,7 @@ def _get_config_input_sockets(self): self.close() raise ValueError( f"Configurated sockets {sockets.keys()} not defined in" - + f" {LPJmLInputType.names}!" + + f" {LPJmLInputType.ids_to_names.values()}!" ) return valid_inputs diff --git a/pycoupler/data.py b/pycoupler/data.py index bb68f1e..7786c5d 100644 --- a/pycoupler/data.py +++ b/pycoupler/data.py @@ -8,6 +8,7 @@ from scipy.spatial import KDTree from xarray.core.variable import Variable import xarray.core.utils as utils +import warnings from pycoupler.utils import read_json @@ -32,6 +33,7 @@ class LPJmLInputType: """ __input_types__ = None # This will hold the configuration data + ids_to_names = {} def __init__(self, id=None, name=None): """Initialize the instance with an id (index).""" @@ -41,32 +43,38 @@ def __init__(self, id=None, name=None): ) if id is not None: - # Find the corresponding input id from the provided id - self.__dict__.update( - next( - value.update({"name": key}) or value - for key, value in LPJmLInputType.__input_types__.items() - if value["id"] == id - ) - ) + name = LPJmLInputType.ids_to_names.get(id, None) + input_type = LPJmLInputType.__input_types__.get(name, None) + if input_type: + self.name = name + self.id = input_type["id"] + self.filename = input_type.get("name", None) + self.fmt = input_type.get("fmt", None) + else: + raise ValueError(f"Provided id '{id}' has no registered input type.") elif name is not None: - # Find the corresponding input name from the provided name - self.__dict__.update( - next( - value.update({"name": key}) or value - for key, value in LPJmLInputType.__input_types__.items() - if key == name - ) - ) + input_type = LPJmLInputType.__input_types__.get(name, None) + if input_type: + self.name = name + self.id = input_type["id"] + self.filename = input_type.get("name", None) + self.fmt = input_type.get("fmt", None) + else: + raise ValueError(f"Provided name '{name}' is not a registered input type.") else: raise ValueError("Either 'id' or 'name' must be provided.") @classmethod def load_config(cls, config): """Load input types from the provided config.""" - cls.__input_types__ = config.input.to_dict() - cls.names = list(cls.__input_types__.keys()) - cls.ids = [value["id"] for value in cls.__input_types__.values()] + cls.__input_types__ = {} + cls.ids_to_names = {} + for name, input_type in config.input.to_dict().items(): + if "id" in input_type: + cls.ids_to_names[input_type["id"]] = name + cls.__input_types__[name] = input_type + else: + warnings.warn(f"Input type '{name}' is missing the required id field. It will be ignored.") @property def nband(self): From 5b2c1274d0922d051ebb1ff90a07b21355b4e938 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Tue, 1 Sep 2026 14:42:12 +0200 Subject: [PATCH 2/4] Fixes #20: Set ids for inputs in coupled configs --- pycoupler/config.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pycoupler/config.py b/pycoupler/config.py index d005366..da1be6e 100644 --- a/pycoupler/config.py +++ b/pycoupler/config.py @@ -4,6 +4,7 @@ import sys import subprocess import json +import warnings from subprocess import run from ruamel.yaml import YAML @@ -330,6 +331,34 @@ def set_coupled( self.sim_path = create_subdirs(sim_path, self.sim_name) output_path = f"{sim_path}/output/{self.sim_name}" + # Set ids for all inputs in case they are missing + available_ids = set() + min_id = len(self.input.to_dict()) + 10 + + def find_new_id(): + nonlocal min_id + new_id = min_id + 1 + while new_id in available_ids: + new_id += 1 + min_id = new_id + return new_id + + for key, inp in self.input.to_dict().items(): + if "id" not in inp: + # Missing id + warnings.warn(f"Input '{key}' is missing an id. Setting a new one for now.") + id = find_new_id() + setattr(getattr(self.input, key), "id", id) + elif inp["id"] in available_ids: + # Duplicate id + id = find_new_id() + warnings.warn(f"Inputs contain duplicate ids. Violating input: '{key}' (id: '{inp["id"]}')") + setattr(getattr(self.input, key), "id", id) + else: + id = inp["id"] + + available_ids |= {id} + # set time range for coupled run self._set_timerange( start_year=start_year, end_year=end_year, write_start_year=start_year From be18843453d4c5b231e850b06c4cf43c5a8db64d Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 3 Sep 2026 11:14:33 +0200 Subject: [PATCH 3/4] Test _ensure_input_ids() --- pycoupler/config.py | 66 +++++++++++++++++------------ tests/conftest.py | 21 +++++++-- tests/data/config_coupled_test.json | 4 +- tests/data/lpjml_config.json | 4 +- tests/test_config.py | 58 ++++++++++++++++++++----- tests/test_run.py | 12 +++--- 6 files changed, 113 insertions(+), 52 deletions(-) diff --git a/pycoupler/config.py b/pycoupler/config.py index da1be6e..0d19ec1 100644 --- a/pycoupler/config.py +++ b/pycoupler/config.py @@ -331,33 +331,9 @@ def set_coupled( self.sim_path = create_subdirs(sim_path, self.sim_name) output_path = f"{sim_path}/output/{self.sim_name}" - # Set ids for all inputs in case they are missing - available_ids = set() - min_id = len(self.input.to_dict()) + 10 - - def find_new_id(): - nonlocal min_id - new_id = min_id + 1 - while new_id in available_ids: - new_id += 1 - min_id = new_id - return new_id - - for key, inp in self.input.to_dict().items(): - if "id" not in inp: - # Missing id - warnings.warn(f"Input '{key}' is missing an id. Setting a new one for now.") - id = find_new_id() - setattr(getattr(self.input, key), "id", id) - elif inp["id"] in available_ids: - # Duplicate id - id = find_new_id() - warnings.warn(f"Inputs contain duplicate ids. Violating input: '{key}' (id: '{inp["id"]}')") - setattr(getattr(self.input, key), "id", id) - else: - id = inp["id"] - - available_ids |= {id} + # The coupling depends on ids in the inputs.cjson, + # therefore, they need to be set, if they are missing + self._ensure_input_ids() # set time range for coupled run self._set_timerange( @@ -582,6 +558,42 @@ def _set_coupling( self.start_coupling = start_year else: self.start_coupling = self.firstyear + + def _ensure_input_ids(self) -> None: + """ + Ensure that all inputs in the config have a unique id. + Warns on missing or duplicate ids, but sets new ones. + """ + available_ids = set() + input_dict = self.input.to_dict() + # As a guess of the next free id, use the number of inputs + 1 + next_free_id = len(input_dict) + 1 + + def find_new_id(): + nonlocal next_free_id + new_id = next_free_id + while new_id in available_ids: + new_id += 1 + next_free_id = new_id + 1 + return new_id + + for key, inp in input_dict.items(): + if "id" not in inp: + # Missing id + warnings.warn(f"Input '{key}' is missing an id. Setting a new one for now.") + id = find_new_id() + setattr(getattr(self.input, key), "id", id) + elif inp["id"] in available_ids: + # Duplicate id + id = find_new_id() + warnings.warn(f"Inputs contain duplicate ids. Violating input: '{key}' (id: '{inp["id"]}')") + setattr(getattr(self.input, key), "id", id) + else: + id = inp["id"] + if id >= next_free_id: + next_free_id = id + 1 + + available_ids |= {id} def _set_input_sockets(self, inputs=[]): """Set sockets for inputs and outputs (via corresponding ids)""" diff --git a/tests/conftest.py b/tests/conftest.py index 4be4e40..54991fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,11 +12,11 @@ def test_path(): @pytest.fixture -def lpjml_coupler(config_coupled): +def lpjml_coupler(config_coupled_json): os.environ["TEST_LINE_COUNTER"] = "0" # Using yield enables safe teardown of the fixture # (see https://docs.pytest.org/en/stable/how-to/fixtures.html#safe-teardowns) - yield LPJmLCoupler(config_file=config_coupled) + yield LPJmLCoupler(config_file=config_coupled_json) # Reset test line env variable os.environ["TEST_LINE_COUNTER"] = "0" @@ -57,7 +57,7 @@ def outputpath_helper(output_dict, path): @pytest.fixture() -def config_coupled(sim_path, model_path, test_path, sim_inputs, output_path): +def config_coupled_json(sim_path, model_path, test_path, sim_inputs, output_path): new_config = sim_path / "config_coupled.json" with open(f"{test_path}/data/config_coupled_test.json") as conf: conf_d = json.load(conf) @@ -68,7 +68,20 @@ def config_coupled(sim_path, model_path, test_path, sim_inputs, output_path): ] with new_config.open("w") as f: json.dump(conf_d, f) - return str(new_config) + return str(new_config) + + +@pytest.fixture() +def lpjml_config_json(sim_path, model_path, test_path, sim_inputs, output_path): + new_config = sim_path / "lpjml_config.json" + with open(f"{test_path}/data/lpjml_config.json") as conf: + conf_d = json.load(conf) + conf_d["output"] = [ + outputpath_helper(out, str(output_path)) for out in conf_d["output"] + ] + with new_config.open("w") as f: + json.dump(conf_d, f) + return str(new_config) def pytest_configure(config): diff --git a/tests/data/config_coupled_test.json b/tests/data/config_coupled_test.json index dea59ff..b0a5694 100644 --- a/tests/data/config_coupled_test.json +++ b/tests/data/config_coupled_test.json @@ -15387,7 +15387,7 @@ "name": "input_VERSION2/lwnet_erainterim_1901-2011.clm" }, "lwdown": { - "id": 43, + "id": 45, "fmt": "clm", "name": "DUMMYLOCATION" }, @@ -15407,7 +15407,7 @@ "name": "input_VERSION2/mwindspeed_1860-2100_67420.clm" }, "tamp": { - "id": 3, + "id": 46, "fmt": "clm", "name": "CRUDATA_TS3_23/cru_ts3.23.1901.2014.dtr.dat.clm" }, diff --git a/tests/data/lpjml_config.json b/tests/data/lpjml_config.json index f705f94..4992cde 100644 --- a/tests/data/lpjml_config.json +++ b/tests/data/lpjml_config.json @@ -15387,7 +15387,7 @@ "name": "input_VERSION2/lwnet_erainterim_1901-2011.clm" }, "lwdown": { - "id": 43, + "id": 45, "fmt": "clm", "name": "DUMMYLOCATION" }, @@ -15407,7 +15407,7 @@ "name": "input_VERSION2/mwindspeed_1860-2100_67420.clm" }, "tamp": { - "id": 3, + "id": 46, "fmt": "clm", "name": "CRUDATA_TS3_23/cru_ts3.23.1901.2014.dtr.dat.clm" }, diff --git a/tests/test_config.py b/tests/test_config.py index 9b204f9..0a3cc19 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,7 +1,10 @@ """Test the LPJmLConfig class.""" from pycoupler.config import read_config, read_yaml, CoupledConfig, parse_config +import json +import pytest +from tests.conftest import output_path, sim_inputs def test_set_spinup_config(test_path): """Test the set_config method of the LPJmLCoupler class.""" @@ -69,11 +72,11 @@ def test_set_historic_config(test_path): assert config_historic.double_harvest is False -def test_set_coupled_config(test_path): +def test_set_coupled_config(lpjml_config_json, config_coupled_json, model_path, sim_path, output_path): """Test the set_config method of the LPJmLCoupler class.""" # create config for coupled run config_coupled = read_config( - model_path=f"{test_path}/data", file_name="lpjml_config.json" + model_path=model_path, file_name=lpjml_config_json ) config_coupled.startgrid = 27410 @@ -81,7 +84,7 @@ def test_set_coupled_config(test_path): # set coupled run configuration config_coupled.set_coupled( - sim_path=f"{test_path}/data", + sim_path=sim_path, sim_name="coupled_test", dependency="historic_run", start_year=2001, @@ -128,10 +131,10 @@ def test_set_coupled_config(test_path): # create config for coupled run check_config_coupled = read_config( - model_path=f"{test_path}/data", file_name="config_coupled_test.json" + model_path=model_path, file_name=config_coupled_json ) # update with actual output path (test directory) - check_config_coupled._set_outputpath(f"{test_path}/data/output/coupled_test") + check_config_coupled._set_outputpath(output_path) # align both config objects check_config_coupled.restart_filename = config_coupled.restart_filename @@ -145,11 +148,12 @@ def test_set_coupled_config(test_path): assert ( repr(config_coupled) - == f"\nSettings: lpjml v5.8\n (general)\n * sim_name coupled_test\n * firstyear 2001\n * lastyear 2050\n * startgrid 27410\n * endgrid 27411\n * landuse yes\n (changed)\n * model_path {test_path}/data\n * sim_path {test_path}/data\n * outputyear 2022\n * output_metafile True\n * grid_type float\n * write_restart False\n * nspinup 0\n * float_grid True\n * restart_filename {test_path}/data/restart/restart_historic_run.lpj\n * outputyear 2022\n * radiation cloudiness\n * fix_co2 True\n * fix_co2_year 2018\n * fix_climate True\n * fix_climate_cycle 11\n * fix_climate_year 2013\n * river_routing False\n * tillage_type read\n * residue_treatment fixed_residue_remove\n * double_harvest False\n * intercrop True\nCoupled model: copan:CORE\n * start_coupling 2023\n * input (coupled) ['with_tillage']\n * output (coupled) ['grid', 'pft_harvestc', 'cftfrac', 'soilc_agr_layer', 'hdate', 'country', 'region']\n" # noqa + == f"\nSettings: lpjml v5.8\n (general)\n * sim_name coupled_test\n * firstyear 2001\n * lastyear 2050\n * startgrid 27410\n * endgrid 27411\n * landuse yes\n (changed)\n * model_path {str(model_path)}\n * sim_path {str(sim_path)}\n * outputyear 2022\n * output_metafile True\n * grid_type float\n * write_restart False\n * nspinup 0\n * float_grid True\n * restart_filename {str(sim_path)}/restart/restart_historic_run.lpj\n * outputyear 2022\n * radiation cloudiness\n * fix_co2 True\n * fix_co2_year 2018\n * fix_climate True\n * fix_climate_cycle 11\n * fix_climate_year 2013\n * river_routing False\n * tillage_type read\n * residue_treatment fixed_residue_remove\n * double_harvest False\n * intercrop True\nCoupled model: copan:CORE\n * start_coupling 2023\n * input (coupled) ['with_tillage']\n * output (coupled) ['grid', 'pft_harvestc', 'cftfrac', 'soilc_agr_layer', 'hdate', 'country', 'region']\n" # noqa ) # noqa + assert config_coupled_dict["input"] == check_config_coupled_dict["input"] assert config_coupled_dict == check_config_coupled_dict - config_coupled.sim_path = f"{test_path}/data" + config_coupled.sim_path = sim_path assert config_coupled.convert_cdf_to_raw() == "tested" assert { @@ -162,7 +166,6 @@ def test_set_coupled_config(test_path): "region", # noqa }.issubset(set(config_coupled.get_output())) - def test_read_yaml(test_path): coupled_config = read_yaml(f"{test_path}/data/config.yaml", CoupledConfig) @@ -191,12 +194,45 @@ def test_read_config(test_path): assert coupled_config.__class__.__name__ == "LpjmlConfig" -def test_parse_config(test_path): - coupled_config = parse_config(f"{test_path}/data/lpjml_config.json") +def test_parse_config(lpjml_config_json): + coupled_config = parse_config(lpjml_config_json) assert coupled_config["model_path"] == "LPJmL_internal" assert coupled_config["coupled_model"] is None coupled_config = parse_config( - f"{test_path}/data/lpjml_config.json", config_class=CoupledConfig + lpjml_config_json, config_class=CoupledConfig ) assert coupled_config.__class__.__name__ == "CoupledConfig" + + +@pytest.fixture(params=[ + {"fmt": "clm", "name": "test/test.clm"}, + {"id": 1, "fmt": "clm", "name": "test/test.clm"}, + pytest.param({"id": 2, "fmt": "clm", "name": "test/test.clm"}, marks=pytest.mark.xfail), +], ids=["no_id", "duplicate_id", "no_errors"]) +def lpjml_config_wrong_ids(request, lpjml_config_json): + with open(lpjml_config_json, "r+") as conf: + conf_d = json.load(fp=conf) + conf_d["input"] = { + "test": request.param, + "test2": {"id": 1, "fmt": "clm", "name": "input/test2.clm"} + } + conf.seek(0) + json.dump(conf_d, conf) + conf.truncate() + return str(lpjml_config_json) + +def test_wrong_ids(lpjml_config_wrong_ids, sim_path): + config_coupled = read_config( + lpjml_config_wrong_ids + ) + + with pytest.warns(UserWarning): + config_coupled._ensure_input_ids() + + inputs = config_coupled.input.to_dict() + ids = [] + for inp in inputs.values(): + assert "id" in inp, "Not every entry has an ID" + ids.append(inp["id"]) + assert len(ids) == len(set(ids)), "IDs are not unique" \ No newline at end of file diff --git a/tests/test_run.py b/tests/test_run.py index 53365ad..e506d91 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -45,11 +45,11 @@ def submit( self, mock_venv, sim_path, - config_coupled, + config_coupled_json, request, ): return submit_lpjml( - config_coupled, + config_coupled_json, group=self.group, sclass=self.sclass, ntasks=self.ntasks, @@ -79,7 +79,7 @@ def test_lpjsubmit_error_cases(self, mock_lpjsubmit): # The test does nothing, we expect the fail in the fixtures pass - def test_command(self, sim_path, config_coupled, fp, submit): + def test_command(self, sim_path, config_coupled_json, fp, submit): run_script_path = sim_path / "output/coupled_test/copan_lpjml.sh" assert ( fp.call_count( @@ -98,7 +98,7 @@ def test_command(self, sim_path, config_coupled, fp, submit): "-couple", str(run_script_path), str(self.ntasks), - config_coupled, + config_coupled_json, ] ) == 1 @@ -113,7 +113,7 @@ def test_command(self, sim_path, config_coupled, fp, submit): ], indirect=True, ) - def test_run_script(self, sim_path, config_coupled, mock_venv, request, submit): + def test_run_script(self, sim_path, config_coupled_json, mock_venv, request, submit): run_script_path = sim_path / "output/coupled_test/copan_lpjml.sh" assert run_script_path.is_file(), "run script should have been created" assert ( @@ -125,7 +125,7 @@ def test_run_script(self, sim_path, config_coupled, mock_venv, request, submit): == f"""#!/bin/bash # Define the path to the config file -config_file="{config_coupled}" +config_file="{config_coupled_json}" # Call the Python script with the config file as an argument {f"{mock_venv}/bin/python" if mock_venv else "python3"} {self.couple_script} \ From bd8f488c867588516745698b4ddc066ca35d6526 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Thu, 3 Sep 2026 11:50:38 +0200 Subject: [PATCH 4/4] Fix code style and stop line length linting --- pycoupler/__init__.py | 1 - pycoupler/config.py | 20 +++++++++++--------- pycoupler/data.py | 8 ++++++-- tests/test_config.py | 40 +++++++++++++++++++++------------------- tests/test_couple.py | 5 +---- tests/test_run.py | 9 ++++----- 6 files changed, 43 insertions(+), 40 deletions(-) diff --git a/pycoupler/__init__.py b/pycoupler/__init__.py index df59fcb..37185fb 100644 --- a/pycoupler/__init__.py +++ b/pycoupler/__init__.py @@ -32,7 +32,6 @@ detect_io_type, ) - __all__ = [ "LpjmlConfig", "CoupledConfig", diff --git a/pycoupler/config.py b/pycoupler/config.py index 0d19ec1..cec9530 100644 --- a/pycoupler/config.py +++ b/pycoupler/config.py @@ -558,7 +558,7 @@ def _set_coupling( self.start_coupling = start_year else: self.start_coupling = self.firstyear - + def _ensure_input_ids(self) -> None: """ Ensure that all inputs in the config have a unique id. @@ -568,7 +568,7 @@ def _ensure_input_ids(self) -> None: input_dict = self.input.to_dict() # As a guess of the next free id, use the number of inputs + 1 next_free_id = len(input_dict) + 1 - + def find_new_id(): nonlocal next_free_id new_id = next_free_id @@ -580,13 +580,17 @@ def find_new_id(): for key, inp in input_dict.items(): if "id" not in inp: # Missing id - warnings.warn(f"Input '{key}' is missing an id. Setting a new one for now.") + warnings.warn( + f"Input '{key}' is missing an id. Setting a new one for now." + ) id = find_new_id() setattr(getattr(self.input, key), "id", id) elif inp["id"] in available_ids: # Duplicate id id = find_new_id() - warnings.warn(f"Inputs contain duplicate ids. Violating input: '{key}' (id: '{inp["id"]}')") + warnings.warn( + f"Inputs contain duplicate ids. Violating input: '{key}' (id: '{inp["id"]}')" + ) setattr(getattr(self.input, key), "id", id) else: id = inp["id"] @@ -1176,12 +1180,10 @@ def __repr__(self, sub_repr=1, order=1): for key, value in self.__dict__.items(): if isinstance(value, SubConfig): - summary += ( - f"""{' ' * sub_repr}* {key}: {value.__repr__( + summary += f"""{' ' * sub_repr}* {key}: { + value.__repr__( sub_repr + 1, order + 1 - )}""".strip() - + spacing - ) + )}""".strip() + spacing else: summary += ( f"{' ' * sub_repr}* {key:<20} {value}".strip() + spacing diff --git a/pycoupler/data.py b/pycoupler/data.py index 7786c5d..f7aad26 100644 --- a/pycoupler/data.py +++ b/pycoupler/data.py @@ -60,7 +60,9 @@ def __init__(self, id=None, name=None): self.filename = input_type.get("name", None) self.fmt = input_type.get("fmt", None) else: - raise ValueError(f"Provided name '{name}' is not a registered input type.") + raise ValueError( + f"Provided name '{name}' is not a registered input type." + ) else: raise ValueError("Either 'id' or 'name' must be provided.") @@ -74,7 +76,9 @@ def load_config(cls, config): cls.ids_to_names[input_type["id"]] = name cls.__input_types__[name] = input_type else: - warnings.warn(f"Input type '{name}' is missing the required id field. It will be ignored.") + warnings.warn( + f"Input type '{name}' is missing the required id field. It will be ignored." + ) @property def nband(self): diff --git a/tests/test_config.py b/tests/test_config.py index 0a3cc19..631d1cd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,7 +4,6 @@ import json import pytest -from tests.conftest import output_path, sim_inputs def test_set_spinup_config(test_path): """Test the set_config method of the LPJmLCoupler class.""" @@ -72,12 +71,12 @@ def test_set_historic_config(test_path): assert config_historic.double_harvest is False -def test_set_coupled_config(lpjml_config_json, config_coupled_json, model_path, sim_path, output_path): +def test_set_coupled_config( + lpjml_config_json, config_coupled_json, model_path, sim_path, output_path +): """Test the set_config method of the LPJmLCoupler class.""" # create config for coupled run - config_coupled = read_config( - model_path=model_path, file_name=lpjml_config_json - ) + config_coupled = read_config(model_path=model_path, file_name=lpjml_config_json) config_coupled.startgrid = 27410 config_coupled.endgrid = 27411 @@ -166,6 +165,7 @@ def test_set_coupled_config(lpjml_config_json, config_coupled_json, model_path, "region", # noqa }.issubset(set(config_coupled.get_output())) + def test_read_yaml(test_path): coupled_config = read_yaml(f"{test_path}/data/config.yaml", CoupledConfig) @@ -199,40 +199,42 @@ def test_parse_config(lpjml_config_json): assert coupled_config["model_path"] == "LPJmL_internal" assert coupled_config["coupled_model"] is None - coupled_config = parse_config( - lpjml_config_json, config_class=CoupledConfig - ) + coupled_config = parse_config(lpjml_config_json, config_class=CoupledConfig) assert coupled_config.__class__.__name__ == "CoupledConfig" -@pytest.fixture(params=[ - {"fmt": "clm", "name": "test/test.clm"}, - {"id": 1, "fmt": "clm", "name": "test/test.clm"}, - pytest.param({"id": 2, "fmt": "clm", "name": "test/test.clm"}, marks=pytest.mark.xfail), -], ids=["no_id", "duplicate_id", "no_errors"]) +@pytest.fixture( + params=[ + {"fmt": "clm", "name": "test/test.clm"}, + {"id": 1, "fmt": "clm", "name": "test/test.clm"}, + pytest.param( + {"id": 2, "fmt": "clm", "name": "test/test.clm"}, marks=pytest.mark.xfail + ), + ], + ids=["no_id", "duplicate_id", "no_errors"], +) def lpjml_config_wrong_ids(request, lpjml_config_json): with open(lpjml_config_json, "r+") as conf: conf_d = json.load(fp=conf) conf_d["input"] = { "test": request.param, - "test2": {"id": 1, "fmt": "clm", "name": "input/test2.clm"} + "test2": {"id": 1, "fmt": "clm", "name": "input/test2.clm"}, } conf.seek(0) json.dump(conf_d, conf) conf.truncate() return str(lpjml_config_json) + def test_wrong_ids(lpjml_config_wrong_ids, sim_path): - config_coupled = read_config( - lpjml_config_wrong_ids - ) + config_coupled = read_config(lpjml_config_wrong_ids) with pytest.warns(UserWarning): config_coupled._ensure_input_ids() - + inputs = config_coupled.input.to_dict() ids = [] for inp in inputs.values(): assert "id" in inp, "Not every entry has an ID" ids.append(inp["id"]) - assert len(ids) == len(set(ids)), "IDs are not unique" \ No newline at end of file + assert len(ids) == len(set(ids)), "IDs are not unique" diff --git a/tests/test_couple.py b/tests/test_couple.py index 51e2f5c..b2fb602 100644 --- a/tests/test_couple.py +++ b/tests/test_couple.py @@ -43,9 +43,7 @@ def test_lpjml_coupler(model_path, sim_path, lpjml_coupler): assert lpjml_coupler.sim_years == [] assert lpjml_coupler.coupled_years == [] assert [year for year in lpjml_coupler.get_coupled_years()] == [] - assert ( - repr(lpjml_coupler) - == f""" + assert repr(lpjml_coupler) == f""" Simulation: (version: 3, localhost:) * sim_year 2050 * ncell 2 @@ -85,7 +83,6 @@ def test_lpjml_coupler(model_path, sim_path, lpjml_coupler): * input (coupled) ['with_tillage'] * output (coupled) ['grid', 'pft_harvestc', 'cftfrac', 'soilc_agr_layer', 'hdate', 'country', 'region'] """ # noqa - ) def test_lpjml_coupler_codes_name(lpjml_coupler): diff --git a/tests/test_run.py b/tests/test_run.py index e506d91..4faaa95 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -113,16 +113,16 @@ def test_command(self, sim_path, config_coupled_json, fp, submit): ], indirect=True, ) - def test_run_script(self, sim_path, config_coupled_json, mock_venv, request, submit): + def test_run_script( + self, sim_path, config_coupled_json, mock_venv, request, submit + ): run_script_path = sim_path / "output/coupled_test/copan_lpjml.sh" assert run_script_path.is_file(), "run script should have been created" assert ( run_script_path.stat().st_mode & 0o0100 ), "run script should be executable" with run_script_path.open("r") as f: - assert ( - f.read() - == f"""#!/bin/bash + assert f.read() == f"""#!/bin/bash # Define the path to the config file config_file="{config_coupled_json}" @@ -131,4 +131,3 @@ def test_run_script(self, sim_path, config_coupled_json, mock_venv, request, sub {f"{mock_venv}/bin/python" if mock_venv else "python3"} {self.couple_script} \ $config_file """ - )