From 217fe92f0fd3cb307a50a725afdf74f2a4e8aa8a Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 11:54:25 -0400 Subject: [PATCH 01/20] added export_variables function --- src/uwtools/config.py | 15 +++++++++++++ src/uwtools/tests/test_config.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 8e7d73db6..552f11578 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 @@ -752,3 +753,17 @@ def create_config_obj( raise ValueError(err_msg) # Dump to file: dump_method(path=outfile, cfg=config_obj) + + +def export_variables(config_dict: dict, section_path: list, log: Logger) -> None: + """ + Allows users to export sections of the config file as environment variables. + """ + for key in section_path: + config_dict = config_dict[key] + for key, value in config_dict.items(): + if type(value) not in (bool, str, int, float): + log.error(f"Non-scalar variable {key} was provided") + raise UWConfigError("Section values provided must be scalar values") + for key, value in config_dict.items(): + print(f"{key}={value}") diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 35969c631..c84979187 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -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,39 @@ 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_export_variables_yaml(capsys): + config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) + section = ["sgs_tke", "profile_type"] + config.export_variables(config_obj.data, section, log=Logger()) + actual = capsys.readouterr().out + expected = """name=fixed +surface_value=0.0\n""" + assert actual == expected + + +def test_export_variables_yaml_for_nonscalar(): + config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) + section = ["o3mr"] + with raises(UWConfigError): + config.export_variables(config_obj.data, section, log=Logger()) + + +def test_export_variables_ini(capsys): + config_obj = config.INIConfig(fixture_path("simple3.ini")) + section = ["dessert"] + config.export_variables(config_obj.data, section, log=Logger()) + actual = capsys.readouterr().out + expected = """type=pie +flavor={{flavor}} +side=False +servings=0\n""" + assert actual == expected + + +def test_export_variables_ini_missing_section(): + config_obj = config.INIConfig(fixture_path("simple3.ini")) + section = ["sandwich"] + with raises(KeyError): + config.export_variables(config_obj.data, section, log=Logger()) From 62dad2ea4077e5c74c871b2bcef59dda7588822b Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 15:26:00 -0400 Subject: [PATCH 02/20] updated section_path type --- src/uwtools/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 552f11578..9e04edafb 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -755,7 +755,7 @@ def create_config_obj( dump_method(path=outfile, cfg=config_obj) -def export_variables(config_dict: dict, section_path: list, log: Logger) -> None: +def export_variables(config_dict: dict, section_path: List[str], log: Logger) -> None: """ Allows users to export sections of the config file as environment variables. """ From ecc91789ba7c0e06e37cfe0cb0c304594dcc4c2f Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 15:27:12 -0400 Subject: [PATCH 03/20] config_dict -> config --- src/uwtools/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 9e04edafb..66eaffef5 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -755,15 +755,15 @@ def create_config_obj( dump_method(path=outfile, cfg=config_obj) -def export_variables(config_dict: dict, section_path: List[str], log: Logger) -> None: +def export_variables(config: dict, section_path: List[str], log: Logger) -> None: """ Allows users to export sections of the config file as environment variables. """ for key in section_path: - config_dict = config_dict[key] - for key, value in config_dict.items(): + config = config[key] + for key, value in config.items(): if type(value) not in (bool, str, int, float): log.error(f"Non-scalar variable {key} was provided") raise UWConfigError("Section values provided must be scalar values") - for key, value in config_dict.items(): + for key, value in config.items(): print(f"{key}={value}") From 8981da1b5da6e852f37bae3b2b1875c8448dc831 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 15:29:56 -0400 Subject: [PATCH 04/20] updated doc string --- src/uwtools/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 66eaffef5..afb5992be 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -757,7 +757,8 @@ def create_config_obj( def export_variables(config: dict, section_path: List[str], log: Logger) -> None: """ - Allows users to export sections of the config file as environment variables. + Descends into the config via the given section keys, then prints the contents of the located + subtree as key=value pairs, one per line. """ for key in section_path: config = config[key] From d27f4e21b99feb9a28138a6d2c849e7a6a74d0db Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 15:32:50 -0400 Subject: [PATCH 05/20] ordered type values --- src/uwtools/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index afb5992be..d06257071 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -763,8 +763,8 @@ def export_variables(config: dict, section_path: List[str], log: Logger) -> None for key in section_path: config = config[key] for key, value in config.items(): - if type(value) not in (bool, str, int, float): - log.error(f"Non-scalar variable {key} was provided") + if type(value) not in (bool, float, int, str): + log.error(f"Non-scalar value {key} was provided") raise UWConfigError("Section values provided must be scalar values") for key, value in config.items(): print(f"{key}={value}") From c88749b800bd726ef49edfab36d0078e45c66aeb Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 15:37:35 -0400 Subject: [PATCH 06/20] updated function name --- src/uwtools/config.py | 2 +- src/uwtools/tests/test_config.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index d06257071..554adb509 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -755,7 +755,7 @@ def create_config_obj( dump_method(path=outfile, cfg=config_obj) -def export_variables(config: dict, section_path: List[str], log: Logger) -> None: +def print_config_section(config: dict, section_path: List[str], log: Logger) -> None: """ Descends into the config via the given section keys, then prints the contents of the located subtree as key=value pairs, one per line. diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index c84979187..9434a728c 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -813,27 +813,27 @@ def test_YAMLConfig__load_unexpected_error(tmp_path): assert msg in str(e.value) -def test_export_variables_yaml(capsys): +def test_print_config_section_yaml(capsys): config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) section = ["sgs_tke", "profile_type"] - config.export_variables(config_obj.data, section, log=Logger()) + config.print_config_section(config_obj.data, section, log=Logger()) actual = capsys.readouterr().out expected = """name=fixed surface_value=0.0\n""" assert actual == expected -def test_export_variables_yaml_for_nonscalar(): +def test_print_config_section_yaml_for_nonscalar(): config_obj = config.YAMLConfig(fixture_path("FV3_GFS_v16.yaml")) section = ["o3mr"] with raises(UWConfigError): - config.export_variables(config_obj.data, section, log=Logger()) + config.print_config_section(config_obj.data, section, log=Logger()) -def test_export_variables_ini(capsys): +def test_print_config_section_ini(capsys): config_obj = config.INIConfig(fixture_path("simple3.ini")) section = ["dessert"] - config.export_variables(config_obj.data, section, log=Logger()) + config.print_config_section(config_obj.data, section, log=Logger()) actual = capsys.readouterr().out expected = """type=pie flavor={{flavor}} @@ -842,8 +842,8 @@ def test_export_variables_ini(capsys): assert actual == expected -def test_export_variables_ini_missing_section(): +def test_print_config_section_ini_missing_section(): config_obj = config.INIConfig(fixture_path("simple3.ini")) section = ["sandwich"] with raises(KeyError): - config.export_variables(config_obj.data, section, log=Logger()) + config.print_config_section(config_obj.data, section, log=Logger()) From 567718fcec82cc852484a7a843c94962d1ec725c Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Mon, 14 Aug 2023 16:02:59 -0400 Subject: [PATCH 07/20] added dict type checking --- src/uwtools/config.py | 6 +++++- src/uwtools/tests/test_config.py | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 554adb509..6755f60cd 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -761,7 +761,11 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> subtree as key=value pairs, one per line. """ for key in section_path: - config = config[key] + if isinstance(config[key], dict): + config = config[key] + else: + log.error(f"{key} type must be a dictionary") + raise UWConfigError("Key type must be a dictionary") for key, value in config.items(): if type(value) not in (bool, float, int, str): log.error(f"Non-scalar value {key} was provided") diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 9434a728c..1bcff700e 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -830,6 +830,13 @@ def test_print_config_section_yaml_for_nonscalar(): config.print_config_section(config_obj.data, section, log=Logger()) +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): + config.print_config_section(config_obj.data, section, log=Logger()) + + def test_print_config_section_ini(capsys): config_obj = config.INIConfig(fixture_path("simple3.ini")) section = ["dessert"] From 08fa4580d473b653c156daebe2c2e4501a8e57bd Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Tue, 15 Aug 2023 15:27:48 -0400 Subject: [PATCH 08/20] added list handling --- src/uwtools/config.py | 37 ++++++++++++----- src/uwtools/tests/test_config.py | 69 ++++++++++++++++++++++---------- 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 6755f60cd..2b23913ea 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -760,15 +760,32 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> Descends into the config via the given section keys, then prints the contents of the located subtree as key=value pairs, one per line. """ + keys = [] for key in section_path: - if isinstance(config[key], dict): - config = config[key] + keys.append(key) + current_path = " -> ".join(keys) + try: + subconfig = config[key] + except KeyError as exc: + msg = f"Bad config path: {current_path}" + log.error(msg) + raise UWConfigError(msg) from exc + if type(subconfig) in (dict, list): + config = subconfig else: - log.error(f"{key} type must be a dictionary") - raise UWConfigError("Key type must be a dictionary") - for key, value in config.items(): - if type(value) not in (bool, float, int, str): - log.error(f"Non-scalar value {key} was provided") - raise UWConfigError("Section values provided must be scalar values") - for key, value in config.items(): - print(f"{key}={value}") + msg = f"Value at {current_path} must be a dictionary or list" + log.error(msg) + raise UWConfigError(msg) + if isinstance(config, dict): + for key, value in config.items(): + if type(value) not in (bool, float, int, str): + log.error(f"Non-scalar value {key} was provided") + raise UWConfigError("Section values provided must be scalar values") + for key, value in config.items(): + print(f"{key}={value}") + else: + for value in config: + if type(value) not in (bool, float, int, str): + log.error(f"Non-scalar value {keys[-1]} was provided") + raise UWConfigError("Section values provided must be scalar values") + print(f"{keys[-1]}={config}") diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 1bcff700e..f1178709a 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -813,44 +813,71 @@ def test_YAMLConfig__load_unexpected_error(tmp_path): 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 = """ +type=pie +flavor={{flavor}} +side=False +servings=0 +""".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\n""" + 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): - config.print_config_section(config_obj.data, section, log=Logger()) - - -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): + with raises(UWConfigError) as e: config.print_config_section(config_obj.data, section, log=Logger()) + assert "must be scalar" in str(e.value) -def test_print_config_section_ini(capsys): - config_obj = config.INIConfig(fixture_path("simple3.ini")) - section = ["dessert"] +def test_print_config_section_yaml_list(capsys): + config_obj = config.YAMLConfig(fixture_path("srw_example.yaml")) + section = ["FV3GFS", "nomads", "file_names", "grib2", "anl"] config.print_config_section(config_obj.data, section, log=Logger()) actual = capsys.readouterr().out - expected = """type=pie -flavor={{flavor}} -side=False -servings=0\n""" + expected = """ +anl=['gfs.t{{ hh }}z.atmanl.nemsio', 'gfs.t{{ hh }}z.sfcanl.nemsio'] +""".lstrip() assert actual == expected -def test_print_config_section_ini_missing_section(): - config_obj = config.INIConfig(fixture_path("simple3.ini")) - section = ["sandwich"] - with raises(KeyError): +def test_print_config_section_yaml_list_nonscalar(): + config_obj = config.YAMLConfig(fixture_path("result4.yaml")) + section = ["models"] + with raises(UWConfigError) as e: + config.print_config_section(config_obj.data, section, log=Logger()) + assert "must be scalar" 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) From e5e88009a4b2299dce37641a8e47c22ad85d6e71 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Wed, 16 Aug 2023 15:29:03 -0400 Subject: [PATCH 09/20] added helper functions --- src/uwtools/config.py | 41 +++++++++++++++++++------------- src/uwtools/tests/test_config.py | 19 +++++++++++++-- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 2b23913ea..1802ec58d 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -761,31 +761,40 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> subtree as key=value pairs, one per line. """ keys = [] - for key in section_path: - keys.append(key) + for section in section_path: + keys.append(section) current_path = " -> ".join(keys) try: - subconfig = config[key] - except KeyError as exc: - msg = f"Bad config path: {current_path}" - log.error(msg) - raise UWConfigError(msg) from exc + subconfig = config[section] + except KeyError: + raise_uwconfigrerror(f"Bad config path: {current_path}", log) if type(subconfig) in (dict, list): config = subconfig else: - msg = f"Value at {current_path} must be a dictionary or list" - log.error(msg) - raise UWConfigError(msg) + raise_uwconfigrerror(f"Value at {current_path} must be a dictionary or list", log) if isinstance(config, dict): for key, value in config.items(): - if type(value) not in (bool, float, int, str): - log.error(f"Non-scalar value {key} was provided") - raise UWConfigError("Section values provided must be scalar values") + if nonscalar_type_check(value): + raise_uwconfigrerror(f"Non-scalar value {key} was provided", log) for key, value in config.items(): print(f"{key}={value}") else: for value in config: - if type(value) not in (bool, float, int, str): - log.error(f"Non-scalar value {keys[-1]} was provided") - raise UWConfigError("Section values provided must be scalar values") + if nonscalar_type_check(value): + raise_uwconfigrerror(f"Non-scalar value {keys[-1]} was provided", log) print(f"{keys[-1]}={config}") + + +def raise_uwconfigrerror(msg: str, log: Logger) -> None: + """ + Will log an error message and raise a UWConfigError with the provided message and string. + """ + log.error(msg) + raise UWConfigError(msg) + + +def nonscalar_type_check(value) -> bool: + """ + Returns true if user-provided value is non-scalar and false otherwise. + """ + return type(value) not in (bool, float, int, str) diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index f1178709a..0dee8220b 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -853,7 +853,7 @@ def test_print_config_section_yaml_for_nonscalar(): section = ["o3mr"] with raises(UWConfigError) as e: config.print_config_section(config_obj.data, section, log=Logger()) - assert "must be scalar" in str(e.value) + assert "Non-scalar value" in str(e.value) def test_print_config_section_yaml_list(capsys): @@ -872,7 +872,7 @@ def test_print_config_section_yaml_list_nonscalar(): section = ["models"] with raises(UWConfigError) as e: config.print_config_section(config_obj.data, section, log=Logger()) - assert "must be scalar" in str(e.value) + assert "Non-scalar value" in str(e.value) def test_print_config_section_yaml_not_dict(): @@ -881,3 +881,18 @@ def test_print_config_section_yaml_not_dict(): 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_raise_uwconfigrerror(): + with raises(UWConfigError) as e: + config.raise_uwconfigrerror("Must be scalar value", log=Logger()) + assert "Must be scalar value" in str(e.value) + + +def test_nonscalar_type_check(): + assert config.nonscalar_type_check(True) is False + assert config.nonscalar_type_check(3.14) is False + assert config.nonscalar_type_check(88) is False + assert config.nonscalar_type_check("string") is False + assert config.nonscalar_type_check({"key": "value"}) is True + assert config.nonscalar_type_check(["string", "sandwich", "test"]) is True From 335ee73691841c26c32ac5bec32cfca578aece74 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Wed, 16 Aug 2023 16:27:29 -0400 Subject: [PATCH 10/20] adding Paul's reorg --- src/uwtools/config.py | 32 +++++++++++++------------------- src/uwtools/tests/test_config.py | 13 ++----------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 1802ec58d..42247002a 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -767,34 +767,28 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> try: subconfig = config[section] except KeyError: - raise_uwconfigrerror(f"Bad config path: {current_path}", log) + log_and_error(f"Bad config path: {current_path}", log) if type(subconfig) in (dict, list): config = subconfig else: - raise_uwconfigrerror(f"Value at {current_path} must be a dictionary or list", log) + log_and_error(f"Value at {current_path} must be a dictionary or list", log) + output_lines = [] + for item in config: + value = config[item] if isinstance(config, dict) else item + if type(value) not in (bool, float, int, str): + log_and_error(f"Non-scalar value {value} found at {current_path}", log) + else: + if isinstance(config, dict): + output_lines.append(f"{item}={value}") if isinstance(config, dict): - for key, value in config.items(): - if nonscalar_type_check(value): - raise_uwconfigrerror(f"Non-scalar value {key} was provided", log) - for key, value in config.items(): - print(f"{key}={value}") + print("\n".join(output_lines)) else: - for value in config: - if nonscalar_type_check(value): - raise_uwconfigrerror(f"Non-scalar value {keys[-1]} was provided", log) print(f"{keys[-1]}={config}") -def raise_uwconfigrerror(msg: str, log: Logger) -> None: +def log_and_error(msg: str, log: Logger) -> None: """ - Will log an error message and raise a UWConfigError with the provided message and string. + Will log a user-provided error message and raise a UWConfigError with the same message. """ log.error(msg) raise UWConfigError(msg) - - -def nonscalar_type_check(value) -> bool: - """ - Returns true if user-provided value is non-scalar and false otherwise. - """ - return type(value) not in (bool, float, int, str) diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 0dee8220b..816681d41 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -883,16 +883,7 @@ def test_print_config_section_yaml_not_dict(): assert "must be a dictionary" in str(e.value) -def test_raise_uwconfigrerror(): +def test_log_and_error(): with raises(UWConfigError) as e: - config.raise_uwconfigrerror("Must be scalar value", log=Logger()) + config.log_and_error("Must be scalar value", log=Logger()) assert "Must be scalar value" in str(e.value) - - -def test_nonscalar_type_check(): - assert config.nonscalar_type_check(True) is False - assert config.nonscalar_type_check(3.14) is False - assert config.nonscalar_type_check(88) is False - assert config.nonscalar_type_check("string") is False - assert config.nonscalar_type_check({"key": "value"}) is True - assert config.nonscalar_type_check(["string", "sandwich", "test"]) is True From 6adfcf641a5252785b2123884246a3093036d34b Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Wed, 16 Aug 2023 17:02:07 -0400 Subject: [PATCH 11/20] re-order functions --- src/uwtools/config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 42247002a..295fe77b6 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -755,6 +755,14 @@ def create_config_obj( dump_method(path=outfile, cfg=config_obj) +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) + + def print_config_section(config: dict, section_path: List[str], log: Logger) -> None: """ Descends into the config via the given section keys, then prints the contents of the located @@ -784,11 +792,3 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> print("\n".join(output_lines)) else: print(f"{keys[-1]}={config}") - - -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) From 077a4efcb2b05f4293eb5732694c62400845aea3 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Wed, 16 Aug 2023 17:17:20 -0400 Subject: [PATCH 12/20] decluttering --- src/uwtools/config.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 295fe77b6..6633d2b82 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -776,18 +776,16 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> subconfig = config[section] except KeyError: log_and_error(f"Bad config path: {current_path}", log) - if type(subconfig) in (dict, list): - config = subconfig - else: + if not type(subconfig) in (dict, list): log_and_error(f"Value at {current_path} must be a dictionary or list", log) + config = subconfig output_lines = [] for item in config: value = config[item] if isinstance(config, dict) else item if type(value) not in (bool, float, int, str): log_and_error(f"Non-scalar value {value} found at {current_path}", log) - else: - if isinstance(config, dict): - output_lines.append(f"{item}={value}") + if isinstance(config, dict): + output_lines.append(f"{item}={value}") if isinstance(config, dict): print("\n".join(output_lines)) else: From e475a7fc4e04a47597e1aba5547455bad5d50bd0 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Thu, 17 Aug 2023 10:34:12 -0400 Subject: [PATCH 13/20] convert to sorted set --- src/uwtools/config.py | 9 +++------ src/uwtools/tests/test_config.py | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 6633d2b82..0cc9c8582 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -779,14 +779,11 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> if not type(subconfig) in (dict, list): log_and_error(f"Value at {current_path} must be a dictionary or list", log) config = subconfig - output_lines = [] + output_lines = set() for item in config: value = config[item] if isinstance(config, dict) else item if type(value) not in (bool, float, int, str): log_and_error(f"Non-scalar value {value} found at {current_path}", log) if isinstance(config, dict): - output_lines.append(f"{item}={value}") - if isinstance(config, dict): - print("\n".join(output_lines)) - else: - print(f"{keys[-1]}={config}") + output_lines.add(f"{item}={value}") + print("\n".join(sorted(output_lines)) if isinstance(config, dict) else f"{keys[-1]}={config}") diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 816681d41..426777313 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -819,10 +819,10 @@ def test_print_config_section_ini(capsys): config.print_config_section(config_obj.data, section, log=Logger()) actual = capsys.readouterr().out expected = """ -type=pie flavor={{flavor}} -side=False servings=0 +side=False +type=pie """.lstrip() assert actual == expected @@ -842,7 +842,7 @@ def test_print_config_section_yaml(capsys): config.print_config_section(config_obj.data, section, log=Logger()) actual = capsys.readouterr().out expected = """ - name=fixed +name=fixed surface_value=0.0 """.lstrip() assert actual == expected From bf679813b83752d993fec558b1711738efe695d6 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Thu, 17 Aug 2023 12:39:09 -0400 Subject: [PATCH 14/20] output_lines as a list --- src/uwtools/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 0cc9c8582..6ab54d364 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -779,11 +779,11 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> if not type(subconfig) in (dict, list): log_and_error(f"Value at {current_path} must be a dictionary or list", log) config = subconfig - output_lines = set() + output_lines = [] for item in config: value = config[item] if isinstance(config, dict) else item if type(value) not in (bool, float, int, str): log_and_error(f"Non-scalar value {value} found at {current_path}", log) if isinstance(config, dict): - output_lines.add(f"{item}={value}") + output_lines.append(f"{item}={value}") print("\n".join(sorted(output_lines)) if isinstance(config, dict) else f"{keys[-1]}={config}") From ec998981ca8ea93bb093d209fada6e23963a35f2 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Fri, 18 Aug 2023 11:10:29 -0400 Subject: [PATCH 15/20] removed list functionality --- src/uwtools/config.py | 16 +++++++--------- src/uwtools/tests/test_config.py | 15 ++++++--------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 6ab54d364..de9d13a10 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -776,14 +776,12 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> subconfig = config[section] except KeyError: log_and_error(f"Bad config path: {current_path}", log) - if not type(subconfig) in (dict, list): - log_and_error(f"Value at {current_path} must be a dictionary or list", log) + if not isinstance(subconfig, dict): + log_and_error(f"Value at {current_path} must be a dictionary", log) config = subconfig output_lines = [] - for item in config: - value = config[item] if isinstance(config, dict) else item - if type(value) not in (bool, float, int, str): - log_and_error(f"Non-scalar value {value} found at {current_path}", log) - if isinstance(config, dict): - output_lines.append(f"{item}={value}") - print("\n".join(sorted(output_lines)) if isinstance(config, dict) else f"{keys[-1]}={config}") + for key, value in config.items(): + if type(value) not in (bool, float, int, list, str): + log_and_error(f"Non-dictionary 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 426777313..907e0390a 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -853,18 +853,15 @@ def test_print_config_section_yaml_for_nonscalar(): 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) + assert "Non-dictionary value" in str(e.value) -def test_print_config_section_yaml_list(capsys): +def test_print_config_section_yaml_list(): config_obj = config.YAMLConfig(fixture_path("srw_example.yaml")) section = ["FV3GFS", "nomads", "file_names", "grib2", "anl"] - config.print_config_section(config_obj.data, section, log=Logger()) - actual = capsys.readouterr().out - expected = """ -anl=['gfs.t{{ hh }}z.atmanl.nemsio', 'gfs.t{{ hh }}z.sfcanl.nemsio'] -""".lstrip() - assert actual == expected + 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_list_nonscalar(): @@ -872,7 +869,7 @@ def test_print_config_section_yaml_list_nonscalar(): section = ["models"] with raises(UWConfigError) as e: config.print_config_section(config_obj.data, section, log=Logger()) - assert "Non-scalar value" in str(e.value) + assert "must be a dictionary" in str(e.value) def test_print_config_section_yaml_not_dict(): From f13c7af5535813fa830cd62d5644e9e30e9f8d67 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Fri, 18 Aug 2023 11:12:28 -0400 Subject: [PATCH 16/20] removed superfluous test --- src/uwtools/tests/test_config.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index 907e0390a..b8a796b9c 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -864,14 +864,6 @@ def test_print_config_section_yaml_list(): assert "must be a dictionary" in str(e.value) -def test_print_config_section_yaml_list_nonscalar(): - config_obj = config.YAMLConfig(fixture_path("result4.yaml")) - section = ["models"] - 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"] From 72708e16a8b059bf7f4c23106a9ee4325b90e55b Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Fri, 18 Aug 2023 11:20:22 -0400 Subject: [PATCH 17/20] fixed list handling error --- src/uwtools/config.py | 4 ++-- src/uwtools/tests/test_config.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index de9d13a10..8c611b88a 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -781,7 +781,7 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> config = subconfig output_lines = [] for key, value in config.items(): - if type(value) not in (bool, float, int, list, str): - log_and_error(f"Non-dictionary value {value} found at {current_path}", log) + 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 b8a796b9c..d9602e8f3 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -853,7 +853,7 @@ def test_print_config_section_yaml_for_nonscalar(): section = ["o3mr"] with raises(UWConfigError) as e: config.print_config_section(config_obj.data, section, log=Logger()) - assert "Non-dictionary value" in str(e.value) + assert "Non-scalar value" in str(e.value) def test_print_config_section_yaml_list(): From aff67c10bcf13a0cb62391a6f6e032fb7c782f5c Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Fri, 18 Aug 2023 11:53:18 -0400 Subject: [PATCH 18/20] reorg --- src/uwtools/config.py | 28 +++++++++++++++++----------- src/uwtools/tests/test_config.py | 4 ++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index 8c611b88a..b9e5bceea 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -654,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, @@ -755,14 +769,6 @@ def create_config_obj( dump_method(path=outfile, cfg=config_obj) -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) - - def print_config_section(config: dict, section_path: List[str], log: Logger) -> None: """ Descends into the config via the given section keys, then prints the contents of the located @@ -775,13 +781,13 @@ def print_config_section(config: dict, section_path: List[str], log: Logger) -> try: subconfig = config[section] except KeyError: - log_and_error(f"Bad config path: {current_path}", log) + _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) + _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) + _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 d9602e8f3..c0d396bde 100644 --- a/src/uwtools/tests/test_config.py +++ b/src/uwtools/tests/test_config.py @@ -872,7 +872,7 @@ def test_print_config_section_yaml_not_dict(): assert "must be a dictionary" in str(e.value) -def test_log_and_error(): +def test__log_and_error(): with raises(UWConfigError) as e: - config.log_and_error("Must be scalar value", log=Logger()) + config._log_and_error("Must be scalar value", log=Logger()) assert "Must be scalar value" in str(e.value) From e07a97de8b8d6cf12e6531c0b337520899dcdd57 Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Fri, 18 Aug 2023 12:04:58 -0400 Subject: [PATCH 19/20] disable protected access --- src/uwtools/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uwtools/tests/test_config.py b/src/uwtools/tests/test_config.py index c0d396bde..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. """ From 49a4f0605c840354e221e9d04498a284786943fe Mon Sep 17 00:00:00 2001 From: elcarpenterNOAA Date: Fri, 18 Aug 2023 12:37:37 -0400 Subject: [PATCH 20/20] name change --- src/uwtools/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uwtools/config.py b/src/uwtools/config.py index b9e5bceea..a6e94362e 100644 --- a/src/uwtools/config.py +++ b/src/uwtools/config.py @@ -769,13 +769,13 @@ def create_config_obj( dump_method(path=outfile, cfg=config_obj) -def print_config_section(config: dict, section_path: List[str], log: Logger) -> None: +def print_config_section(config: dict, key_path: List[str], log: Logger) -> None: """ - Descends into the config via the given section keys, then prints the contents of the located - subtree as key=value pairs, one per line. + 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 section_path: + for section in key_path: keys.append(section) current_path = " -> ".join(keys) try: