Skip to content

Commit 7344f41

Browse files
authored
Merge pull request #250 from Climate-REF/fix-esmvaltool-paths
2 parents e845872 + 507223f commit 7344f41

12 files changed

Lines changed: 135 additions & 49 deletions

File tree

.github/workflows/ci-integration.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ jobs:
4040
key: ilamb
4141
- name: Run tests
4242
run: |
43+
make virtual-environment
4344
make fetch-test-data
44-
uv run ref datasets fetch-data ilamb
45+
uv run ref datasets fetch-data --registry ilamb
4546
make test
4647
# Upload the scratch and results directories as artifacts
4748
- name: Upload scratch artifacts

changelog/250.fix.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed how the path to ESMValTool outputs was determined,
2+
and added support for outputs in nested directories.

docs/how-to-guides/running-metrics-locally.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
data_catalog={
9595
SourceDatasetType.CMIP6: data_catalog,
9696
},
97-
metric=provider.get("global-mean-timeseries"),
97+
metric=metric,
9898
provider=provider,
9999
)
100100

@@ -144,6 +144,7 @@
144144
# This will not perform and validation/verification of the output results.
145145

146146
# %%
147+
definition.output_directory.mkdir(parents=True, exist_ok=True)
147148
direct_result = metric.run(definition=definition)
148149
assert direct_result.successful
149150

packages/ref-core/src/cmip_ref_core/logging.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,14 @@ def redirect_logs(definition: MetricExecutionDefinition, log_level: str) -> Gene
112112
The logger will also be reset to this level after leaving the context manager.
113113
114114
"""
115+
app_logger_configured = hasattr(logger, "default_handler_id")
116+
115117
# Remove existing default log handler
116-
remove_log_handler()
118+
# This swallows the logs from the app logger
119+
# If the app logger hasn't been configured yet, we don't need to remove it,
120+
# as logs will also be written to the console as loguru adds a stderr handler by default
121+
if app_logger_configured:
122+
remove_log_handler()
117123

118124
# Add a new log handler for the execution log
119125
output_file = definition.output_directory / EXECUTION_LOG_FILENAME
@@ -131,7 +137,10 @@ def redirect_logs(definition: MetricExecutionDefinition, log_level: str) -> Gene
131137

132138
# Reset the logger to the default
133139
logger.remove(file_handler_id)
134-
add_log_handler(**logger.default_handler_kwargs) # type: ignore[attr-defined]
140+
141+
# We only re-add the app handler if it was configured before
142+
if app_logger_configured:
143+
add_log_handler(**logger.default_handler_kwargs) # type: ignore[attr-defined]
135144

136145

137146
__all__ = ["add_log_handler", "capture_logging", "logger", "redirect_logs"]

packages/ref-core/tests/unit/test_logging.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@ def test_redirect_logs(definition, caplog):
5252
assert text_inner in output_file.read_text()
5353

5454

55+
def test_redirect_logs_without_handler(definition):
56+
definition, output_file = definition
57+
58+
assert not hasattr(logger, "default_handler_id")
59+
with redirect_logs(definition, "INFO"):
60+
logger.info("inner")
61+
assert not hasattr(logger, "default_handler_id")
62+
63+
assert "inner" in output_file.read_text()
64+
65+
66+
def test_redirect_logs_stdlog_captured(definition):
67+
definition, output_file = definition
68+
69+
with redirect_logs(definition, "INFO"):
70+
import logging
71+
72+
stdlog = logging.getLogger("stdlog")
73+
stdlog.info("stdlog inner")
74+
stdlog.debug("stdlog debug")
75+
76+
assert "stdlog inner" in output_file.read_text()
77+
assert "stdlog debug" not in output_file.read_text()
78+
79+
5580
def test_redirect_logs_exception(definition, caplog):
5681
log_level = "DEBUG"
5782

packages/ref-metrics-esmvaltool/src/cmip_ref_metrics_esmvaltool/metrics/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def build_metric_result(
142142
metadata = yaml.load(metadata_file.read_text(encoding="utf-8"))
143143
for filename in metadata:
144144
caption = metadata[filename].get("caption", "")
145-
relative_path = Path(filename).relative_to(result_dir.absolute())
145+
relative_path = definition.as_relative_path(filename)
146146
if relative_path.suffix in plot_suffixes:
147147
key = OutputCV.PLOTS.value
148148
else:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pytest
2+
from cmip_ref_metrics_esmvaltool import provider
3+
4+
from cmip_ref.models import MetricExecutionResult as MetricExecutionResultModel
5+
from cmip_ref.solver import solve_metric_executions
6+
from cmip_ref.testing import validate_result
7+
from cmip_ref_core.metrics import Metric
8+
9+
metrics = [pytest.param(metric, id=metric.slug) for metric in provider.metrics()]
10+
11+
12+
@pytest.mark.slow
13+
@pytest.mark.parametrize("metric", metrics)
14+
def test_metrics(metric: Metric, data_catalog, tmp_path, config, mocker):
15+
mocker.patch.object(MetricExecutionResultModel, "metric_execution_group")
16+
# Ensure the conda prefix is set
17+
provider.configure(config)
18+
19+
# Get the first match from the data catalog
20+
execution = next(
21+
solve_metric_executions(
22+
data_catalog=data_catalog,
23+
metric=metric,
24+
provider=provider,
25+
)
26+
)
27+
28+
# Run the metric
29+
definition = execution.build_metric_execution_info(output_root=config.paths.scratch)
30+
definition.output_directory.mkdir(parents=True, exist_ok=True)
31+
result = metric.run(definition)
32+
33+
# Check the result
34+
validate_result(config, result)

packages/ref-metrics-pmp/tests/integration/test_variability_modes.py

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@
22
from cmip_ref_metrics_pmp import provider
33
from cmip_ref_metrics_pmp.variability_modes import ExtratropicalModesOfVariability
44

5-
from cmip_ref.database import Database
6-
from cmip_ref.executor import handle_execution_result
75
from cmip_ref.models import MetricExecutionResult as MetricExecutionResultModel
86
from cmip_ref.solver import solve_metric_executions
9-
from cmip_ref_core.metrics import MetricExecutionResult
10-
from cmip_ref_core.pycmec.metric import CMECMetric
11-
from cmip_ref_core.pycmec.output import CMECOutput
7+
from cmip_ref.testing import validate_result
128

139
variability_metrics = [
1410
pytest.param(metric, id=metric.slug)
@@ -17,31 +13,6 @@
1713
]
1814

1915

20-
def validate_result(config, result: MetricExecutionResult):
21-
database = Database.from_config(config)
22-
metric_execution_result = MetricExecutionResultModel(
23-
metric_execution_group_id=1,
24-
dataset_hash=result.definition.metric_dataset.hash,
25-
output_fragment=str(result.definition.output_fragment()),
26-
)
27-
database.session.add(metric_execution_result)
28-
database.session.flush()
29-
30-
assert result.successful
31-
32-
# Validate bundles
33-
CMECMetric.load_from_json(result.to_output_path(result.metric_bundle_filename))
34-
CMECOutput.load_from_json(result.to_output_path(result.output_bundle_filename))
35-
36-
# Create a fake log file
37-
result.to_output_path("out.log").touch()
38-
39-
# This checks if the bundles are valid
40-
handle_execution_result(
41-
config, database=database, metric_execution_result=metric_execution_result, result=result
42-
)
43-
44-
4516
@pytest.mark.slow
4617
@pytest.mark.parametrize("metric", variability_metrics)
4718
def test_variability_modes(metric: ExtratropicalModesOfVariability, data_catalog, tmp_path, config, mocker):

packages/ref/src/cmip_ref/executor/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ def _copy_file_to_results(
104104
if not (input_directory / filename).exists():
105105
raise FileNotFoundError(f"Could not find {filename} in {input_directory}")
106106

107-
if not output_directory.exists():
108-
output_directory.mkdir(parents=True)
107+
output_filename = output_directory / filename
108+
output_filename.parent.mkdir(parents=True, exist_ok=True)
109109

110-
shutil.copy(input_directory / filename, output_directory / filename)
110+
shutil.copy(input_directory / filename, output_filename)
111111

112112

113113
def handle_execution_result(

packages/ref/src/cmip_ref/solver.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ def build_metric_execution_info(self, output_root: pathlib.Path) -> MetricExecut
9797
"""
9898
Build the metric execution info for the current metric execution
9999
"""
100+
# Ensure that the output root is always an absolute path
101+
output_root = output_root.resolve()
102+
100103
# This is the desired path relative to the output directory
101104
fragment = pathlib.Path() / self.provider.slug / self.metric.slug / self.metric_dataset.hash
102105

0 commit comments

Comments
 (0)