Skip to content

Commit 62263ea

Browse files
swarnaleemsol1105
andauthored
Fix :latest checker version resolution for compliance-checker >= 6.0.0 (#24)
* Normalize checker:latest to unversioned checker and rely on compliance-checker to select latest checker version * Various bugfixes * Update pyproject.toml --------- Co-authored-by: sol1105 <10836031+sol1105@users.noreply.github.com>
1 parent 115bc76 commit 62263ea

4 files changed

Lines changed: 82 additions & 27 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ $ esgqa [-h] [-P <parallel_processes>] [-o <OUTPUT_DIR>] [-t <TEST>] [-O OPTION]
8888
- `-h, --help`: show this help message and exit
8989
- `-P, --parallel_processes`: Specify the maximum number of parallel processes. Default: 0 (= number of cores).
9090
- `-o, --output_dir OUTPUT_DIR`: Directory to store QA results. Needs to be non-existing or empty or from previous QA run. If not specified, will store results in `./cc-qa-check-results/YYYYMMDD-HHmm_<hash>`.
91-
- `-t, --test TEST`: The test to run (eg. `'wcrp_cmip6:latest'`, `'wcrp_cordex_cmip6:latest'` or `'cf:<version>'`, can be specified multiple times, eg.: `'-t wcrp_cmip6:latest -t cf:1.7'`) - default: running latest CF checks `'cf:latest'`. If the version is omitted, `latest` will be used.
91+
- `-t, --test TEST`: The test to run (eg. `'wcrp_cmip6:latest'`, `'wcrp_cordex_cmip6:latest'` or `'cf:<version>'`, can be specified multiple times, eg.: `'-t wcrp_cmip6:latest -t cf:1.7'`) - default: running latest CF checks. If the version is omitted, `latest` will be used (`'cf'` and `'cf:latest'` are equivalent).
9292
- `-O, --option OPTION`: Additional options to be passed to the checkers. Format: `'<checker>:<option_name>[:<option_value>]'`. Multiple invocations possible.
9393
- `-i, --info INFO`: Information used to tag the QA results, eg. the simulation id to identify the checked run. Suggested is the original experiment-id you gave the run.
9494
- `-r, --resume`: Specify to continue a previous QC run. Requires the `<output_dir>` argument to be set.

esgf_qa/run_qa.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -140,23 +140,57 @@ def get_checker_release_versions(checkers, checker_options={}):
140140
check_suite = CheckSuite(options=checker_options)
141141
check_suite.load_all_available_checkers()
142142
for checker in checkers:
143-
if checker.split(":")[0] not in checker_release_versions:
144-
if checker.split(":")[0] in checker_dict:
145-
checker_release_versions[checker.split(":")[0]] = (
146-
check_suite.checkers.get(
147-
checker, "unknown version"
148-
)._cc_spec_version
149-
)
150-
elif checker.split(":")[0] in checker_dict_ext:
151-
checker_release_versions[checker.split(":")[0]] = version
143+
checker_name = checker.split(":")[0]
144+
if checker_name not in checker_release_versions:
145+
if checker_name in checker_dict_ext and checker_name not in checker_dict:
146+
# Internal esgf-qa checker (cons, cont, comp) - use esgf-qa version
147+
checker_release_versions[checker_name] = version
152148
else:
153-
checker_release_versions[checker.split(":")[0]] = (
154-
check_suite.checkers.get(
155-
checker, "unknown version"
156-
)._cc_spec_version
149+
# compliance-checker plugin: look up _cc_spec_version.
150+
# CC >= 6.0.0 removed :latest, so fall back to the highest
151+
# explicitly versioned key when the requested key is missing.
152+
checker_obj = check_suite.checkers.get(checker)
153+
if checker_obj is None:
154+
prefix = checker_name + ":"
155+
candidates = [
156+
k for k in check_suite.checkers if k.startswith(prefix)
157+
]
158+
if candidates:
159+
resolved_key = max(
160+
candidates,
161+
key=lambda k: pversion.parse(k.split(":")[1]),
162+
)
163+
checker_obj = check_suite.checkers.get(resolved_key)
164+
checker_release_versions[checker_name] = (
165+
checker_obj._cc_spec_version
166+
if checker_obj is not None
167+
else "unknown"
157168
)
158169

159170

171+
def normalize_checker_specs(checkers_versions):
172+
"""
173+
Normalize checker specifications for compliance-checker.
174+
175+
Parameters
176+
----------
177+
checkers_versions : dict
178+
Mapping of checker name to requested version string.
179+
180+
Returns
181+
-------
182+
list
183+
Sorted checker specs where explicit versions are kept as
184+
'<checker>:<version>' and 'latest' maps to unversioned '<checker>'.
185+
"""
186+
return sorted(
187+
[
188+
checker if checker_version == "latest" else f"{checker}:{checker_version}"
189+
for checker, checker_version in checkers_versions.items()
190+
]
191+
)
192+
193+
160194
def run_compliance_checker(file_path, checkers, checker_options={}):
161195
"""
162196
Run the compliance checker on a file with the specified checkers and options.
@@ -194,7 +228,7 @@ def run_compliance_checker(file_path, checkers, checker_options={}):
194228
if include_checks:
195229
results = {}
196230
for checker in checkers:
197-
if include_checks and "cc6:latest" in checker or "mip:latest" in checker:
231+
if include_checks and checker.split(":")[0] in ("cc6", "mip"):
198232
results.update(
199233
check_suite.run_all(ds, [checker], include_checks, skip_checks=[])
200234
)
@@ -274,7 +308,10 @@ def process_file(
274308
and os.path.isfile(result_file)
275309
and (
276310
os.path.isfile(consistency_file)
277-
or not any(cn.startswith("cc6") or cn.startswith("mip") for cn in checkers)
311+
or not any(
312+
cn.split(":", 1)[0] in checker_supporting_consistency_checks
313+
for cn in checkers
314+
)
278315
)
279316
):
280317
with open(result_file) as file:
@@ -737,9 +774,11 @@ def main():
737774
"ERROR: Cannot run both 'cc6' and 'mip' checkers at the same time."
738775
)
739776

740-
# Combine checkers and versions
741-
# (checker_options are hardcoded)
742-
checkers = sorted([f"{c}:{v}" for c, v in checkers_versions.items()])
777+
# Normalize checker specifications for compliance-checker:
778+
# - explicit versions are forwarded as '<checker>:<version>'
779+
# - omitted versions and ':latest' are both forwarded as '<checker>'
780+
# so compliance-checker selects the highest installed version.
781+
checkers = normalize_checker_specs(checkers_versions)
743782

744783
# Does parent_dir exist?
745784
if parent_dir is None:
@@ -760,13 +799,17 @@ def main():
760799
with open(os.path.join(result_dir, ".resume_info"), "w") as f:
761800
json.dump(resume_info, f, sort_keys=True, indent=4)
762801

763-
# If only cf checker is selected, run cc6 time checks only
802+
# If none of the selected checkers support consistency checks,
803+
# add mip time checks so consistency output can be generated.
764804
if (
765-
not any(cn.startswith("cc6") or cn.startswith("mip") for cn in checkers)
805+
not any(
806+
cn.split(":", 1)[0] in checker_supporting_consistency_checks
807+
for cn in checkers
808+
)
766809
and include_consistency_checks
767810
):
768811
time_checks_only = True
769-
checkers.append("mip:latest")
812+
checkers.append("mip")
770813
checkers.sort()
771814
else:
772815
time_checks_only = False
@@ -1026,7 +1069,7 @@ def main():
10261069
)
10271070
del result
10281071

1029-
# Skip continuity and consistency checks if no cc6/mip checks were run
1072+
# Skip continuity and consistency checks if no appropriate checkers were run
10301073
# (and thus no consistency output file was created)
10311074
if any(
10321075
ch.split(":", 1)[0] in checker_supporting_consistency_checks for ch in checkers
@@ -1120,7 +1163,7 @@ def main():
11201163
print()
11211164
print("#" * 50)
11221165
print(
1123-
f"# QA Part {'3' if 'cc6:latest' in checkers or 'mip:latest' in checkers else '2'} - Summarizing and clustering the results"
1166+
f"# QA Part {'3' if any(cn.split(':')[0] in checker_supporting_consistency_checks for cn in checkers) else '2'} - Summarizing and clustering the results"
11241167
)
11251168
print("#" * 50)
11261169
print()

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ classifiers = [
2929
dependencies = [
3030
"cftime",
3131
"cf_xarray",
32-
"compliance-checker>=5.3.0",
32+
"compliance-checker>=6.0.0",
3333
"dask",
3434
"netCDF4",
3535
"packaging",
3636
"pandas",
3737
"textual",
3838
"xarray",
39-
"cc-plugin-cc6>=0.4.0",
40-
"cc-plugin-wcrp"
39+
"cc-plugin-cc6>=0.4.3",
40+
"cc-plugin-wcrp>=2.1.0"
4141
]
4242
dynamic = [
4343
"version"

tests/test_run_qa.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
get_checker_release_versions,
1414
get_default_result_dir,
1515
get_dsid,
16+
normalize_checker_specs,
1617
parse_options,
1718
track_checked_datasets,
1819
)
@@ -179,3 +180,14 @@ def test_parse_options():
179180
},
180181
)
181182
assert _verify_options_dict(opt_dict) is True
183+
184+
185+
def test_latest_and_omitted_versions_are_equivalent_in_internal_specs():
186+
checkers_versions = {"cf": "latest", "cc6": "latest", "wcrp_cmip6": "1.7"}
187+
checkers = normalize_checker_specs(checkers_versions)
188+
189+
assert "cf" in checkers
190+
assert "cc6" in checkers
191+
assert "wcrp_cmip6:1.7" in checkers
192+
assert "cf:latest" not in checkers
193+
assert "cc6:latest" not in checkers

0 commit comments

Comments
 (0)