diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 8e7d73db6..a6e94362e 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -21,6 +21,7 @@ import yaml from uwtools import exceptions, logger +from uwtools.exceptions import UWConfigError from uwtools.j2template import J2Template from uwtools.logger import Logger from uwtools.utils import cli_helpers @@ -653,6 +654,20 @@ def dump_file_from_dict(path: str, cfg: dict, opts: Optional[ns] = None) -> None file_name.write("\n".join(lines)) +# Private functions + + +def _log_and_error(msg: str, log: Logger) -> None: + """ + Will log a user-provided error message and raise a UWConfigError with the same message. + """ + log.error(msg) + raise UWConfigError(msg) + + +# Public functions + + def create_config_obj( input_base_file: str, compare: bool = False, @@ -752,3 +767,27 @@ def create_config_obj( raise ValueError(err_msg) # Dump to file: dump_method(path=outfile, cfg=config_obj) + + +def print_config_section(config: dict, key_path: List[str], log: Logger) -> None: + """ + Descends into the config via the given keys, then prints the contents of the located subtree as + key=value pairs, one per line. + """ + keys = [] + for section in key_path: + keys.append(section) + current_path = " -> ".join(keys) + try: + subconfig = config[section] + except KeyError: + _log_and_error(f"Bad config path: {current_path}", log) + if not isinstance(subconfig, dict): + _log_and_error(f"Value at {current_path} must be a dictionary", log) + config = subconfig + output_lines = [] + for key, value in config.items(): + if type(value) not in (bool, float, int, str): + _log_and_error(f"Non-scalar value {value} found at {current_path}", log) + output_lines.append(f"{key}={value}") + print("\n".join(sorted(output_lines))) diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 35969c631..a69677039 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -1,4 +1,4 @@ -# pylint: disable=duplicate-code,missing-function-docstring,redefined-outer-name +# pylint: disable=duplicate-code,missing-function-docstring,protected-access,redefined-outer-name """ Tests for uwtools.config module. """ @@ -22,6 +22,7 @@ from uwtools import config, exceptions from uwtools.exceptions import UWConfigError +from uwtools.logger import Logger from uwtools.tests.support import compare_files, fixture_path, line_in_lines, msg_in_caplog from uwtools.utils import cli_helpers @@ -810,3 +811,68 @@ def test_YAMLConfig__load_unexpected_error(tmp_path): with raises(UWConfigError) as e: config.YAMLConfig(config_path=cfgfile) assert msg in str(e.value) + + +def test_print_config_section_ini(capsys): + config_obj = config.INIConfig(fixture_path("simple3.ini")) + section = ["dessert"] + config.print_config_section(config_obj.data, section, log=Logger()) + actual = capsys.readouterr().out + expected = """ +flavor={{flavor}} +servings=0 +side=False +type=pie +""".lstrip() + assert actual == expected + + +def test_print_config_section_ini_missing_section(): + config_obj = config.INIConfig(fixture_path("simple3.ini")) + section = ["sandwich"] + msg = "Bad config path: sandwich" + with raises(UWConfigError) as e: + config.print_config_section(config_obj.data, section, log=Logger()) + assert msg in str(e.value) + + +def test_print_config_section_yaml(capsys): + config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) + section = ["sgs_tke", "profile_type"] + config.print_config_section(config_obj.data, section, log=Logger()) + actual = capsys.readouterr().out + expected = """ +name=fixed +surface_value=0.0 +""".lstrip() + assert actual == expected + + +def test_print_config_section_yaml_for_nonscalar(): + config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) + section = ["o3mr"] + with raises(UWConfigError) as e: + config.print_config_section(config_obj.data, section, log=Logger()) + assert "Non-scalar value" in str(e.value) + + +def test_print_config_section_yaml_list(): + config_obj = config.YAMLConfig(fixture_path("srw_example.yaml")) + section = ["FV3GFS", "nomads", "file_names", "grib2", "anl"] + with raises(UWConfigError) as e: + config.print_config_section(config_obj.data, section, log=Logger()) + assert "must be a dictionary" in str(e.value) + + +def test_print_config_section_yaml_not_dict(): + config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) + section = ["sgs_tke", "units"] + with raises(UWConfigError) as e: + config.print_config_section(config_obj.data, section, log=Logger()) + assert "must be a dictionary" in str(e.value) + + +def test__log_and_error(): + with raises(UWConfigError) as e: + config._log_and_error("Must be scalar value", log=Logger()) + assert "Must be scalar value" in str(e.value)