diff --git a/src/qtoolkit/io/base.py b/src/qtoolkit/io/base.py index b935949..833d8ac 100644 --- a/src/qtoolkit/io/base.py +++ b/src/qtoolkit/io/base.py @@ -1,6 +1,7 @@ from __future__ import annotations import abc +import difflib import shlex from dataclasses import fields from pathlib import Path @@ -82,9 +83,27 @@ def generate_header(self, options: dict | QResources | None) -> str: # check that all the options are present in the template keys = set(options.keys()) - extra = keys.difference(template.get_identifiers()) + all_identifiers = template.get_identifiers() + extra = keys.difference(all_identifiers) if extra: - msg = f"The following keys are not present in the template: {', '.join(sorted(extra))}" + close_matches = {} + for extra_val in extra: + m = difflib.get_close_matches( + extra_val, all_identifiers, n=3, cutoff=0.65 + ) + if m: + close_matches[extra_val] = m + msg = ( + f"The following keys are not present in the template: {', '.join(sorted(extra))}. " + f"Check the template in {type(self).__module__}.{type(self).__qualname__}.header_template." + ) + if close_matches: + msg += " Possible replacements:" + for extra_val in sorted(close_matches): + replacements = " or ".join( + f"'{m}'" for m in close_matches[extra_val] + ) + msg += f" {replacements} instead of '{extra_val}'." raise ValueError(msg) unclean_header = template.safe_substitute(options) diff --git a/tests/io/test_base.py b/tests/io/test_base.py index 09aa6a5..bc05ad5 100644 --- a/tests/io/test_base.py +++ b/tests/io/test_base.py @@ -144,9 +144,12 @@ def test_generate_header(self, scheduler): #SPECCMD --nodes=4""" ) + # check that the error message contains the expected error, but should not match + # the possible replacements, as they are too different with pytest.raises( ValueError, - match=r"The following keys are not present in the template: tata, titi", + match=r"The following keys are not present in the template: tata, titi. Check " + r"the template in .*MyScheduler.header_template(?!.*instead of 'titi')", ): res = QResources( nodes=4, @@ -155,6 +158,19 @@ def test_generate_header(self, scheduler): ) scheduler.generate_header(res) + with pytest.raises( + ValueError, + match=r"The following keys are not present in the template: option32, processes-per-node. " + r"Check the template in .*MyScheduler.header_template.*'option3' or 'option2' or 'option1' " + r"instead of 'option32'. 'processes_per_node' or 'processes' instead of 'processes-per-node'", + ): + res = QResources( + nodes=4, + processes_per_node=16, + scheduler_kwargs={"option32": "xxx", "processes-per-node": "yyy"}, + ) + scheduler.generate_header(res) + def test_generate_ids_list(self, scheduler): ids_list = scheduler.generate_ids_list( [QJob(job_id=4), QJob(job_id="job_id_abc1"), 215, "job12345"]