From 2e279c8f156f66323e13b466197bc3ce5ee9a30b Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 15:11:45 +0000 Subject: [PATCH 01/46] Add CLI option --- src/uwtools/cli.py | 16 ++++++++++++++++ src/uwtools/strings.py | 1 + src/uwtools/tests/test_cli.py | 7 ++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 40745795c..9c9322701 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -118,6 +118,7 @@ def _add_subparser_config(subparsers: Subparsers) -> ModeChecks: subparsers = _add_subparsers(parser, STR.action, STR.action.upper()) return { STR.compare: _add_subparser_config_compare(subparsers), + STR.compose: _add_subparser_config_compose(subparsers), STR.realize: _add_subparser_config_realize(subparsers), STR.validate: _add_subparser_config_validate(subparsers), } @@ -153,6 +154,21 @@ def _add_subparser_config_compare(subparsers: Subparsers) -> ActionChecks: ] +def _add_subparser_config_compose(subparsers: Subparsers) -> ActionChecks: + """ + Add subparser for mode: config compose. + + :param subparsers: Parent parser's subparsers, to add this subparser to. + """ + parser = _add_subparser(subparsers, STR.compose, "Compose configs") + optional = _basic_setup(parser) + _add_arg_input_format(optional, choices=FORMATS) + _add_arg_output_format(optional, choices=FORMATS) + checks = _add_args_verbosity(optional) + parser.add_argument("file", metavar="FILE", nargs="+", type=Path) + return checks + + def _add_subparser_config_realize(subparsers: Subparsers) -> ActionChecks: """ Add subparser for mode: config realize. diff --git a/src/uwtools/strings.py b/src/uwtools/strings.py index 8bbbb3432..28b999123 100644 --- a/src/uwtools/strings.py +++ b/src/uwtools/strings.py @@ -70,6 +70,7 @@ class STR: chgrescube: str = "chgres_cube" classname: str = "classname" compare: str = "compare" + compose: str = "compose" config: str = "config" copy: str = "copy" cycle: str = "cycle" diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py index e310f2d71..6cdaceb0d 100644 --- a/src/uwtools/tests/test_cli.py +++ b/src/uwtools/tests/test_cli.py @@ -80,7 +80,12 @@ def test_cli__abort(capsys): def test_cli__add_subparser_config(subparsers): cli._add_subparser_config(subparsers) - assert actions(subparsers.choices[STR.config]) == [STR.compare, STR.realize, STR.validate] + assert actions(subparsers.choices[STR.config]) == [ + STR.compare, + STR.compose, + STR.realize, + STR.validate, + ] def test_cli__add_subparser_config_compare(subparsers): From 1b9560f5113885f1013b7391ca97ede337723950 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 15:16:31 +0000 Subject: [PATCH 02/46] Add comment --- src/uwtools/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 9c9322701..4ee458e69 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -1364,6 +1364,8 @@ def _parse_args(raw_args: list[str]) -> tuple[Args, Checks]: **drivers_with_cycle_and_leadtime, } checks = {k: modes[k]() for k in sorted(modes.keys())} + # Return a dict version of the Namespace object returned by parse_args(), supporting lookups + # like args[STR.foo], which would otherwise have to be the even noisier getattr(args, STR.foo). return vars(parser.parse_args(raw_args)), checks From c99c8554034b040ac9f154f13274d5efc4e46360 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 15:27:49 +0000 Subject: [PATCH 03/46] CLI updates --- src/uwtools/cli.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 4ee458e69..793520a79 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -164,6 +164,7 @@ def _add_subparser_config_compose(subparsers: Subparsers) -> ActionChecks: optional = _basic_setup(parser) _add_arg_input_format(optional, choices=FORMATS) _add_arg_output_format(optional, choices=FORMATS) + _add_arg_output_file(optional) checks = _add_args_verbosity(optional) parser.add_argument("file", metavar="FILE", nargs="+", type=Path) return checks @@ -743,7 +744,7 @@ def _add_arg_classname(group: Group) -> None: def _add_arg_config_file(group: Group, required: bool = False) -> None: - msg = "Path to UW YAML config file" + ("" if required else " (default: read from stdin)") + msg = "Path to UW YAML config file" + ("" if required else " (default: stdin)") group.add_argument( _switch(STR.cfgfile), "-c", @@ -824,7 +825,7 @@ def _add_arg_input_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.infile), "-i", - help="Path to input file (defaults to stdin)", + help="Path to input file (default: stdin)", metavar="PATH", required=required, type=Path, @@ -835,7 +836,8 @@ def _add_arg_input_format(group: Group, choices: list[str], required: bool = Fal group.add_argument( _switch(STR.infmt), choices=choices, - help="Input format", + default=FORMAT.yaml, + help=f"Input format (default: {FORMAT.yaml})", required=required, type=str, ) @@ -882,7 +884,7 @@ def _add_arg_output_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.outfile), "-o", - help="Path to output file (defaults to stdout)", + help="Path to output file (default: stdout)", metavar="PATH", required=required, type=Path, @@ -893,7 +895,8 @@ def _add_arg_output_format(group: Group, choices: list[str], required: bool = Fa group.add_argument( _switch(STR.outfmt), choices=choices, - help="Output format", + default=FORMAT.yaml, + help=f"Output format (default: {FORMAT.yaml})", required=required, type=str, ) @@ -997,7 +1000,7 @@ def _add_arg_update_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.updatefile), "-u", - help="Path to update file (defaults to stdin)", + help="Path to update file (default: stdin)", metavar="PATH", required=required, type=Path, From c78945a9a812fe713aa5a60b42378f08a654605f Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 15:30:39 +0000 Subject: [PATCH 04/46] WIP --- src/uwtools/api/config.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index a821143ca..5957b6320 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -39,12 +39,7 @@ def compare( """ NB: This docstring is dynamically replaced: See compare.__doc__ definition below. """ - return _compare( - path1=Path(path1), - path2=Path(path2), - format1=format1, - format2=format2, - ) + return _compare(path1=Path(path1), path2=Path(path2), format1=format1, format2=format2) def get_fieldtable_config( From e7ce6462eddbcbdf8b063fb8f1c422065ce84e80 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 15:57:50 +0000 Subject: [PATCH 05/46] WIP --- src/uwtools/api/config.py | 14 ++++++++++---- src/uwtools/cli.py | 17 ++++++++++++++++- src/uwtools/strings.py | 1 + src/uwtools/tests/test_cli.py | 22 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 5957b6320..396a7f019 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -31,10 +31,7 @@ def compare( - path1: Path | str, - path2: Path | str, - format1: str | None = None, - format2: str | None = None, + path1: Path | str, path2: Path | str, format1: str | None = None, format2: str | None = None ) -> bool: """ NB: This docstring is dynamically replaced: See compare.__doc__ definition below. @@ -42,6 +39,15 @@ def compare( return _compare(path1=Path(path1), path2=Path(path2), format1=format1, format2=format2) +def compose( + configs: list[str | Path], # noqa: ARG001 + output_file: Path | str | None = None, # noqa: ARG001 + input_format: str | None = None, # noqa: ARG001 + output_format: str | None = None, # noqa: ARG001 +) -> bool: + return True + + def get_fieldtable_config( config: dict | Path | str | None = None, stdin_ok: bool = False ) -> FieldTableConfig: diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 793520a79..bc82aadf6 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -166,7 +166,7 @@ def _add_subparser_config_compose(subparsers: Subparsers) -> ActionChecks: _add_arg_output_format(optional, choices=FORMATS) _add_arg_output_file(optional) checks = _add_args_verbosity(optional) - parser.add_argument("file", metavar="FILE", nargs="+", type=Path) + parser.add_argument("configs", metavar="CONFIG", nargs="+", type=Path) return checks @@ -218,6 +218,7 @@ def _dispatch_config(args: Args) -> bool: """ actions = { STR.compare: _dispatch_config_compare, + STR.compose: _dispatch_config_compose, STR.realize: _dispatch_config_realize, STR.validate: _dispatch_config_validate, } @@ -238,6 +239,20 @@ def _dispatch_config_compare(args: Args) -> bool: ) +def _dispatch_config_compose(args: Args) -> bool: + """ + Define dispatch logic for config compose action. + + :param args: Parsed command-line args. + """ + return uwtools.api.config.compose( + configs=args[STR.configs], + output_file=args[STR.outfile], + input_format=args[STR.infmt], + output_format=args[STR.outfmt], + ) + + def _dispatch_config_realize(args: Args) -> bool: """ Define dispatch logic for config realize action. diff --git a/src/uwtools/strings.py b/src/uwtools/strings.py index 28b999123..f97de674b 100644 --- a/src/uwtools/strings.py +++ b/src/uwtools/strings.py @@ -72,6 +72,7 @@ class STR: compare: str = "compare" compose: str = "compose" config: str = "config" + configs: str = "configs" copy: str = "copy" cycle: str = "cycle" database: str = "database" diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py index 6cdaceb0d..96078a909 100644 --- a/src/uwtools/tests/test_cli.py +++ b/src/uwtools/tests/test_cli.py @@ -93,6 +93,11 @@ def test_cli__add_subparser_config_compare(subparsers): assert subparsers.choices[STR.compare] +def test_cli__add_subparser_config_compose(subparsers): + cli._add_subparser_config_compose(subparsers) + assert subparsers.choices[STR.compose] + + def test_cli__add_subparser_config_realize(subparsers): cli._add_subparser_config_realize(subparsers) assert subparsers.choices[STR.realize] @@ -300,6 +305,7 @@ def test_cli__dict_from_key_eq_val_strings(): "params", [ (STR.compare, "_dispatch_config_compare"), + (STR.compose, "_dispatch_config_compose"), (STR.realize, "_dispatch_config_realize"), (STR.validate, "_dispatch_config_validate"), ], @@ -324,6 +330,22 @@ def test_cli__dispatch_config_compare(): ) +def test_cli__dispatch_config_compose(): + configs = ["/path/to/a", "/path/to/b"] + outfile = "/path/to/output" + args = { + STR.configs: configs, + STR.outfile: outfile, + STR.infmt: FORMAT.yaml, + STR.outfmt: FORMAT.yaml, + } + with patch.object(cli.uwtools.api.config, "compose") as compose: + cli._dispatch_config_compose(args) + compose.assert_called_once_with( + configs=configs, output_file=outfile, input_format=FORMAT.yaml, output_format=FORMAT.yaml + ) + + def test_cli__dispatch_config_realize(args_config_realize): with patch.object(cli.uwtools.api.config, "realize") as realize: cli._dispatch_config_realize(args_config_realize) From a9569c1d611676bfe94a74dadd225620ea54e9de Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 16:07:34 +0000 Subject: [PATCH 06/46] Plumbing --- src/uwtools/api/config.py | 18 ++++++++++++------ src/uwtools/config/tools.py | 20 +++++++++++++------- src/uwtools/tests/config/test_tools.py | 24 ++++++++++++------------ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 396a7f019..53ad53d52 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -14,7 +14,8 @@ from uwtools.config.formats.nml import NMLConfig from uwtools.config.formats.sh import SHConfig from uwtools.config.formats.yaml import YAMLConfig -from uwtools.config.tools import compare_configs as _compare +from uwtools.config.tools import compare as _compare +from uwtools.config.tools import compose as _compose from uwtools.config.tools import realize_config as _realize from uwtools.config.validator import ConfigDataT, ConfigPathT from uwtools.config.validator import validate_check_config as _validate_check_config @@ -40,12 +41,17 @@ def compare( def compose( - configs: list[str | Path], # noqa: ARG001 - output_file: Path | str | None = None, # noqa: ARG001 - input_format: str | None = None, # noqa: ARG001 - output_format: str | None = None, # noqa: ARG001 + configs: list[str | Path], + output_file: Path | str | None = None, + input_format: str | None = None, + output_format: str | None = None, ) -> bool: - return True + return _compose( + configs=list(map(Path, configs)), + output_file=Path(output_file) if output_file else None, + input_format=input_format, + output_format=output_format, + ) def get_fieldtable_config( diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 790073d4f..cc129542a 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -20,14 +20,11 @@ # Public functions -def compare_configs( - path1: Path, - path2: Path, - format1: str | None = None, - format2: str | None = None, +def compare( + path1: Path, path2: Path, format1: str | None = None, format2: str | None = None ) -> bool: """ - NB: This docstring is dynamically replaced: See compare_configs.__doc__ definition below. + NB: This docstring is dynamically replaced: See compare.__doc__ definition below. """ format1 = _ensure_format("1st config file", format1, path1) format2 = _ensure_format("2nd config file", format2, path2) @@ -41,6 +38,15 @@ def compare_configs( return cfg_1.compare_config(cfg_2.as_dict()) +def compose( + configs: list[Path], # noqa: ARG001 + output_file: Path | None = None, # noqa: ARG001 + input_format: str | None = None, # noqa: ARG001 + output_format: str | None = None, # noqa: ARG001 +) -> bool: + return True + + def config_check_depths_dump(config_obj: Config | dict, target_format: str) -> None: """ Check that the depth does not exceed the target format's max. @@ -312,7 +318,7 @@ def _validate_format(other_fmt_desc: str, other_fmt: str, input_fmt: str) -> Non # work if the docstrings are inlined in the functions. They must remain separate statements to avoid # hardcoding values into them. -compare_configs.__doc__ = """ +compare.__doc__ = """ Compare two config files. Recognized file extensions are: {extensions} diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 9c48bfa3d..a978abb66 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -29,7 +29,7 @@ @fixture -def compare_configs_assets(tmp_path): +def compare_assets(tmp_path): d = {"foo": {"bar": 42}, "baz": {"qux": 43}} a = tmp_path / "a" b = tmp_path / "b" @@ -105,18 +105,18 @@ def help_realize_config_simple(infn, infmt, tmpdir): # Tests -def test_compare_configs__good(compare_configs_assets, logged): - _, a, b = compare_configs_assets - assert tools.compare_configs(path1=a, format1=FORMAT.yaml, path2=b, format2=FORMAT.yaml) +def test_compare__good(compare_assets, logged): + _, a, b = compare_assets + assert tools.compare(path1=a, format1=FORMAT.yaml, path2=b, format2=FORMAT.yaml) assert logged(".*", regex=True) -def test_compare_configs__changed_value(compare_configs_assets, logged): - d, a, b = compare_configs_assets +def test_compare__changed_value(compare_assets, logged): + d, a, b = compare_assets d["baz"]["qux"] = 11 with writable(b) as f: yaml.dump(d, f) - assert not tools.compare_configs(path1=a, format1=FORMAT.yaml, path2=b, format2=FORMAT.yaml) + assert not tools.compare(path1=a, format1=FORMAT.yaml, path2=b, format2=FORMAT.yaml) expected = """ - %s + %s @@ -138,13 +138,13 @@ def test_compare_configs__changed_value(compare_configs_assets, logged): assert logged(line) -def test_compare_configs__missing_key(compare_configs_assets, logged): - d, a, b = compare_configs_assets +def test_compare__missing_key(compare_assets, logged): + d, a, b = compare_assets del d["baz"] with writable(b) as f: yaml.dump(d, f) # Note that a and b are swapped: - assert not tools.compare_configs(path1=b, format1=FORMAT.yaml, path2=a, format2=FORMAT.yaml) + assert not tools.compare(path1=b, format1=FORMAT.yaml, path2=a, format2=FORMAT.yaml) expected = """ - %s + %s @@ -163,8 +163,8 @@ def test_compare_configs__missing_key(compare_configs_assets, logged): assert logged(line) -def test_compare_configs__bad_format(logged): - assert not tools.compare_configs( +def test_compare__bad_format(logged): + assert not tools.compare( path1=Path("/not/used"), format1="jpg", path2=Path("/not/used"), From ef8229e541a2c5b6ee1b3df7324fedb65f05d2fd Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Mon, 15 Sep 2025 21:16:08 +0000 Subject: [PATCH 07/46] Update docs --- .../sections/user_guide/cli/drivers/cdeps/run-help.out | 2 +- .../user_guide/cli/drivers/chgres_cube/run-help.out | 2 +- .../user_guide/cli/drivers/esg_grid/run-help.out | 2 +- .../user_guide/cli/drivers/filter_topo/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/fv3/run-help.out | 2 +- .../cli/drivers/global_equiv_resol/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/gsi/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/ioda/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/jedi/run-help.out | 2 +- .../user_guide/cli/drivers/make_hgrid/run-help.out | 2 +- .../cli/drivers/make_solo_mosaic/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/mpas/run-help.out | 2 +- .../user_guide/cli/drivers/mpas_init/run-help.out | 2 +- .../user_guide/cli/drivers/mpassit/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/orog/run-help.out | 2 +- .../user_guide/cli/drivers/orog_gsl/run-help.out | 2 +- .../user_guide/cli/drivers/sfc_climo_gen/run-help.out | 2 +- .../sections/user_guide/cli/drivers/shave/run-help.out | 2 +- .../user_guide/cli/drivers/ungrib/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/upp/run-help.out | 2 +- .../user_guide/cli/drivers/upp_assets/run-help.out | 2 +- docs/sections/user_guide/cli/tools/config/help.out | 2 ++ .../user_guide/cli/tools/config/realize-help.out | 10 +++++----- .../user_guide/cli/tools/config/validate-help.out | 2 +- docs/sections/user_guide/cli/tools/execute/help.out | 2 +- docs/sections/user_guide/cli/tools/fs/copy-help.out | 2 +- .../sections/user_guide/cli/tools/fs/hardlink-help.out | 2 +- docs/sections/user_guide/cli/tools/fs/link-help.out | 2 +- .../sections/user_guide/cli/tools/fs/makedirs-help.out | 2 +- .../user_guide/cli/tools/rocoto/realize-help.out | 4 ++-- .../user_guide/cli/tools/rocoto/validate-help.out | 2 +- .../user_guide/cli/tools/template/render-help.out | 4 ++-- .../user_guide/cli/tools/template/translate-help.out | 4 ++-- 33 files changed, 41 insertions(+), 39 deletions(-) diff --git a/docs/sections/user_guide/cli/drivers/cdeps/run-help.out b/docs/sections/user_guide/cli/drivers/cdeps/run-help.out index 2d51479b9..ae1be8fbf 100644 --- a/docs/sections/user_guide/cli/drivers/cdeps/run-help.out +++ b/docs/sections/user_guide/cli/drivers/cdeps/run-help.out @@ -14,7 +14,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --dry-run Only log info, making no changes --graph-file PATH diff --git a/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out b/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out index 826f759d1..e28413a43 100644 --- a/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out +++ b/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out @@ -17,7 +17,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out b/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out index cafb5c91b..3caa80221 100644 --- a/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out +++ b/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out b/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out index 76136a649..0b3974c69 100644 --- a/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out +++ b/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/fv3/run-help.out b/docs/sections/user_guide/cli/drivers/fv3/run-help.out index a493b4ed3..f1df35c50 100644 --- a/docs/sections/user_guide/cli/drivers/fv3/run-help.out +++ b/docs/sections/user_guide/cli/drivers/fv3/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out b/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out index 5739e5514..bf9866367 100644 --- a/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out +++ b/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/gsi/run-help.out b/docs/sections/user_guide/cli/drivers/gsi/run-help.out index f9b8d9251..72ab2bbef 100644 --- a/docs/sections/user_guide/cli/drivers/gsi/run-help.out +++ b/docs/sections/user_guide/cli/drivers/gsi/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/ioda/run-help.out b/docs/sections/user_guide/cli/drivers/ioda/run-help.out index 26d2b9a02..141cc5457 100644 --- a/docs/sections/user_guide/cli/drivers/ioda/run-help.out +++ b/docs/sections/user_guide/cli/drivers/ioda/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/jedi/run-help.out b/docs/sections/user_guide/cli/drivers/jedi/run-help.out index 235d8cba1..fae91e795 100644 --- a/docs/sections/user_guide/cli/drivers/jedi/run-help.out +++ b/docs/sections/user_guide/cli/drivers/jedi/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out b/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out index 3d655f66b..961190f23 100644 --- a/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out +++ b/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out b/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out index fd44fad43..8a4a70236 100644 --- a/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out +++ b/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/mpas/run-help.out b/docs/sections/user_guide/cli/drivers/mpas/run-help.out index aa2b084dd..b45f0057c 100644 --- a/docs/sections/user_guide/cli/drivers/mpas/run-help.out +++ b/docs/sections/user_guide/cli/drivers/mpas/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out b/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out index 88b5bc00a..412463b81 100644 --- a/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out +++ b/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/mpassit/run-help.out b/docs/sections/user_guide/cli/drivers/mpassit/run-help.out index 04f96e320..55711c3a0 100644 --- a/docs/sections/user_guide/cli/drivers/mpassit/run-help.out +++ b/docs/sections/user_guide/cli/drivers/mpassit/run-help.out @@ -17,7 +17,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/orog/run-help.out b/docs/sections/user_guide/cli/drivers/orog/run-help.out index 877be3ab0..bd1752f36 100644 --- a/docs/sections/user_guide/cli/drivers/orog/run-help.out +++ b/docs/sections/user_guide/cli/drivers/orog/run-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out index 447e9b3dd..708764f11 100644 --- a/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out b/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out index 75de057c6..8a4b49954 100644 --- a/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out +++ b/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/shave/run-help.out b/docs/sections/user_guide/cli/drivers/shave/run-help.out index 3a6ec4ef4..186165d02 100644 --- a/docs/sections/user_guide/cli/drivers/shave/run-help.out +++ b/docs/sections/user_guide/cli/drivers/shave/run-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/ungrib/run-help.out b/docs/sections/user_guide/cli/drivers/ungrib/run-help.out index ef63e0bdb..4a13cde91 100644 --- a/docs/sections/user_guide/cli/drivers/ungrib/run-help.out +++ b/docs/sections/user_guide/cli/drivers/ungrib/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/upp/run-help.out b/docs/sections/user_guide/cli/drivers/upp/run-help.out index 3bfcc318a..7496de737 100644 --- a/docs/sections/user_guide/cli/drivers/upp/run-help.out +++ b/docs/sections/user_guide/cli/drivers/upp/run-help.out @@ -17,7 +17,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out b/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out index fa0f992ad..f7e078706 100644 --- a/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out +++ b/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out @@ -19,7 +19,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --dry-run Only log info, making no changes --graph-file PATH diff --git a/docs/sections/user_guide/cli/tools/config/help.out b/docs/sections/user_guide/cli/tools/config/help.out index b3e075f75..ca4bd537d 100644 --- a/docs/sections/user_guide/cli/tools/config/help.out +++ b/docs/sections/user_guide/cli/tools/config/help.out @@ -12,6 +12,8 @@ Positional arguments: ACTION compare Compare configs + compose + Compose configs realize Realize config validate diff --git a/docs/sections/user_guide/cli/tools/config/realize-help.out b/docs/sections/user_guide/cli/tools/config/realize-help.out index acc724049..1d8b3ec49 100644 --- a/docs/sections/user_guide/cli/tools/config/realize-help.out +++ b/docs/sections/user_guide/cli/tools/config/realize-help.out @@ -15,17 +15,17 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (defaults to stdin) + Path to input file (default: stdin) --input-format {ini,nml,sh,yaml} - Input format + Input format (default: yaml) --update-file PATH, -u PATH - Path to update file (defaults to stdin) + Path to update file (default: stdin) --update-format {ini,nml,sh,yaml} Update format --output-file PATH, -o PATH - Path to output file (defaults to stdout) + Path to output file (default: stdout) --output-format {ini,nml,sh,yaml} - Output format + Output format (default: yaml) --key-path KEY[.KEY...] Dot-separated path of keys to the block to be output --values-needed diff --git a/docs/sections/user_guide/cli/tools/config/validate-help.out b/docs/sections/user_guide/cli/tools/config/validate-help.out index bc69e8087..f2d08cee2 100644 --- a/docs/sections/user_guide/cli/tools/config/validate-help.out +++ b/docs/sections/user_guide/cli/tools/config/validate-help.out @@ -13,7 +13,7 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (defaults to stdin) + Path to input file (default: stdin) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/execute/help.out b/docs/sections/user_guide/cli/tools/execute/help.out index cc99c1284..e6956922a 100644 --- a/docs/sections/user_guide/cli/tools/execute/help.out +++ b/docs/sections/user_guide/cli/tools/execute/help.out @@ -20,7 +20,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --schema-file PATH Path to schema file to use for validation --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/copy-help.out b/docs/sections/user_guide/cli/tools/fs/copy-help.out index 023636c60..5efb4be63 100644 --- a/docs/sections/user_guide/cli/tools/fs/copy-help.out +++ b/docs/sections/user_guide/cli/tools/fs/copy-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/hardlink-help.out b/docs/sections/user_guide/cli/tools/fs/hardlink-help.out index 0f24e93fd..1a66c80e2 100644 --- a/docs/sections/user_guide/cli/tools/fs/hardlink-help.out +++ b/docs/sections/user_guide/cli/tools/fs/hardlink-help.out @@ -12,7 +12,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/link-help.out b/docs/sections/user_guide/cli/tools/fs/link-help.out index 1d39b9337..b371b872f 100644 --- a/docs/sections/user_guide/cli/tools/fs/link-help.out +++ b/docs/sections/user_guide/cli/tools/fs/link-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/makedirs-help.out b/docs/sections/user_guide/cli/tools/fs/makedirs-help.out index abfe4e36c..ab062acf5 100644 --- a/docs/sections/user_guide/cli/tools/fs/makedirs-help.out +++ b/docs/sections/user_guide/cli/tools/fs/makedirs-help.out @@ -12,7 +12,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/rocoto/realize-help.out b/docs/sections/user_guide/cli/tools/rocoto/realize-help.out index 25a08e3af..93db5424e 100644 --- a/docs/sections/user_guide/cli/tools/rocoto/realize-help.out +++ b/docs/sections/user_guide/cli/tools/rocoto/realize-help.out @@ -9,9 +9,9 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: read from stdin) + Path to UW YAML config file (default: stdin) --output-file PATH, -o PATH - Path to output file (defaults to stdout) + Path to output file (default: stdout) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/rocoto/validate-help.out b/docs/sections/user_guide/cli/tools/rocoto/validate-help.out index 23c28085e..e86ebd4d4 100644 --- a/docs/sections/user_guide/cli/tools/rocoto/validate-help.out +++ b/docs/sections/user_guide/cli/tools/rocoto/validate-help.out @@ -9,7 +9,7 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (defaults to stdin) + Path to input file (default: stdin) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/template/render-help.out b/docs/sections/user_guide/cli/tools/template/render-help.out index c48e430bd..7caa7aa2a 100644 --- a/docs/sections/user_guide/cli/tools/template/render-help.out +++ b/docs/sections/user_guide/cli/tools/template/render-help.out @@ -13,9 +13,9 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (defaults to stdin) + Path to input file (default: stdin) --output-file PATH, -o PATH - Path to output file (defaults to stdout) + Path to output file (default: stdout) --values-file PATH Path to file providing override or interpolation values --values-format {ini,nml,sh,yaml} diff --git a/docs/sections/user_guide/cli/tools/template/translate-help.out b/docs/sections/user_guide/cli/tools/template/translate-help.out index d1ca93c39..228874c9f 100644 --- a/docs/sections/user_guide/cli/tools/template/translate-help.out +++ b/docs/sections/user_guide/cli/tools/template/translate-help.out @@ -10,9 +10,9 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (defaults to stdin) + Path to input file (default: stdin) --output-file PATH, -o PATH - Path to output file (defaults to stdout) + Path to output file (default: stdout) --dry-run Only log info, making no changes --quiet, -q From 80eb2eb0eee8960b70b5a8b2eb77d705dfb8e176 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 12:09:15 +0000 Subject: [PATCH 08/46] Update docs --- docs/sections/user_guide/cli/drivers/cdeps/run-help.out | 2 +- .../user_guide/cli/drivers/chgres_cube/run-help.out | 2 +- .../sections/user_guide/cli/drivers/esg_grid/run-help.out | 2 +- .../user_guide/cli/drivers/filter_topo/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/fv3/run-help.out | 2 +- .../cli/drivers/global_equiv_resol/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/gsi/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/ioda/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/jedi/run-help.out | 2 +- .../user_guide/cli/drivers/make_hgrid/run-help.out | 2 +- .../user_guide/cli/drivers/make_solo_mosaic/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/mpas/run-help.out | 2 +- .../user_guide/cli/drivers/mpas_init/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/mpassit/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/orog/run-help.out | 2 +- .../sections/user_guide/cli/drivers/orog_gsl/run-help.out | 2 +- .../user_guide/cli/drivers/sfc_climo_gen/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/shave/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/ungrib/run-help.out | 2 +- docs/sections/user_guide/cli/drivers/upp/run-help.out | 2 +- .../user_guide/cli/drivers/upp_assets/run-help.out | 2 +- .../sections/user_guide/cli/tools/config/realize-help.out | 6 +++--- .../user_guide/cli/tools/config/validate-help.out | 2 +- docs/sections/user_guide/cli/tools/execute/help.out | 2 +- docs/sections/user_guide/cli/tools/fs/copy-help.out | 2 +- docs/sections/user_guide/cli/tools/fs/hardlink-help.out | 2 +- docs/sections/user_guide/cli/tools/fs/link-help.out | 2 +- docs/sections/user_guide/cli/tools/fs/makedirs-help.out | 2 +- .../sections/user_guide/cli/tools/rocoto/realize-help.out | 4 ++-- .../user_guide/cli/tools/rocoto/validate-help.out | 2 +- .../user_guide/cli/tools/template/render-help.out | 4 ++-- .../user_guide/cli/tools/template/translate-help.out | 4 ++-- src/uwtools/cli.py | 8 ++++---- 33 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/sections/user_guide/cli/drivers/cdeps/run-help.out b/docs/sections/user_guide/cli/drivers/cdeps/run-help.out index ae1be8fbf..2d51479b9 100644 --- a/docs/sections/user_guide/cli/drivers/cdeps/run-help.out +++ b/docs/sections/user_guide/cli/drivers/cdeps/run-help.out @@ -14,7 +14,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --dry-run Only log info, making no changes --graph-file PATH diff --git a/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out b/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out index e28413a43..826f759d1 100644 --- a/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out +++ b/docs/sections/user_guide/cli/drivers/chgres_cube/run-help.out @@ -17,7 +17,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out b/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out index 3caa80221..cafb5c91b 100644 --- a/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out +++ b/docs/sections/user_guide/cli/drivers/esg_grid/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out b/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out index 0b3974c69..76136a649 100644 --- a/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out +++ b/docs/sections/user_guide/cli/drivers/filter_topo/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/fv3/run-help.out b/docs/sections/user_guide/cli/drivers/fv3/run-help.out index f1df35c50..a493b4ed3 100644 --- a/docs/sections/user_guide/cli/drivers/fv3/run-help.out +++ b/docs/sections/user_guide/cli/drivers/fv3/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out b/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out index bf9866367..5739e5514 100644 --- a/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out +++ b/docs/sections/user_guide/cli/drivers/global_equiv_resol/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/gsi/run-help.out b/docs/sections/user_guide/cli/drivers/gsi/run-help.out index 72ab2bbef..f9b8d9251 100644 --- a/docs/sections/user_guide/cli/drivers/gsi/run-help.out +++ b/docs/sections/user_guide/cli/drivers/gsi/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/ioda/run-help.out b/docs/sections/user_guide/cli/drivers/ioda/run-help.out index 141cc5457..26d2b9a02 100644 --- a/docs/sections/user_guide/cli/drivers/ioda/run-help.out +++ b/docs/sections/user_guide/cli/drivers/ioda/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/jedi/run-help.out b/docs/sections/user_guide/cli/drivers/jedi/run-help.out index fae91e795..235d8cba1 100644 --- a/docs/sections/user_guide/cli/drivers/jedi/run-help.out +++ b/docs/sections/user_guide/cli/drivers/jedi/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out b/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out index 961190f23..3d655f66b 100644 --- a/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out +++ b/docs/sections/user_guide/cli/drivers/make_hgrid/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out b/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out index 8a4a70236..fd44fad43 100644 --- a/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out +++ b/docs/sections/user_guide/cli/drivers/make_solo_mosaic/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/mpas/run-help.out b/docs/sections/user_guide/cli/drivers/mpas/run-help.out index b45f0057c..aa2b084dd 100644 --- a/docs/sections/user_guide/cli/drivers/mpas/run-help.out +++ b/docs/sections/user_guide/cli/drivers/mpas/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out b/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out index 412463b81..88b5bc00a 100644 --- a/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out +++ b/docs/sections/user_guide/cli/drivers/mpas_init/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/mpassit/run-help.out b/docs/sections/user_guide/cli/drivers/mpassit/run-help.out index 55711c3a0..04f96e320 100644 --- a/docs/sections/user_guide/cli/drivers/mpassit/run-help.out +++ b/docs/sections/user_guide/cli/drivers/mpassit/run-help.out @@ -17,7 +17,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/orog/run-help.out b/docs/sections/user_guide/cli/drivers/orog/run-help.out index bd1752f36..877be3ab0 100644 --- a/docs/sections/user_guide/cli/drivers/orog/run-help.out +++ b/docs/sections/user_guide/cli/drivers/orog/run-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out index 708764f11..447e9b3dd 100644 --- a/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out +++ b/docs/sections/user_guide/cli/drivers/orog_gsl/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out b/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out index 8a4b49954..75de057c6 100644 --- a/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out +++ b/docs/sections/user_guide/cli/drivers/sfc_climo_gen/run-help.out @@ -11,7 +11,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/shave/run-help.out b/docs/sections/user_guide/cli/drivers/shave/run-help.out index 186165d02..3a6ec4ef4 100644 --- a/docs/sections/user_guide/cli/drivers/shave/run-help.out +++ b/docs/sections/user_guide/cli/drivers/shave/run-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/ungrib/run-help.out b/docs/sections/user_guide/cli/drivers/ungrib/run-help.out index 4a13cde91..ef63e0bdb 100644 --- a/docs/sections/user_guide/cli/drivers/ungrib/run-help.out +++ b/docs/sections/user_guide/cli/drivers/ungrib/run-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/upp/run-help.out b/docs/sections/user_guide/cli/drivers/upp/run-help.out index 7496de737..3bfcc318a 100644 --- a/docs/sections/user_guide/cli/drivers/upp/run-help.out +++ b/docs/sections/user_guide/cli/drivers/upp/run-help.out @@ -17,7 +17,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --batch Submit job to batch scheduler --dry-run diff --git a/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out b/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out index f7e078706..fa0f992ad 100644 --- a/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out +++ b/docs/sections/user_guide/cli/drivers/upp_assets/run-help.out @@ -19,7 +19,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --dry-run Only log info, making no changes --graph-file PATH diff --git a/docs/sections/user_guide/cli/tools/config/realize-help.out b/docs/sections/user_guide/cli/tools/config/realize-help.out index 1d8b3ec49..9c3237c8a 100644 --- a/docs/sections/user_guide/cli/tools/config/realize-help.out +++ b/docs/sections/user_guide/cli/tools/config/realize-help.out @@ -15,15 +15,15 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (default: stdin) + Path to input file (default: read from stdin) --input-format {ini,nml,sh,yaml} Input format (default: yaml) --update-file PATH, -u PATH - Path to update file (default: stdin) + Path to update file (default: read from stdin) --update-format {ini,nml,sh,yaml} Update format --output-file PATH, -o PATH - Path to output file (default: stdout) + Path to output file (default: write to stdout) --output-format {ini,nml,sh,yaml} Output format (default: yaml) --key-path KEY[.KEY...] diff --git a/docs/sections/user_guide/cli/tools/config/validate-help.out b/docs/sections/user_guide/cli/tools/config/validate-help.out index f2d08cee2..a260fb606 100644 --- a/docs/sections/user_guide/cli/tools/config/validate-help.out +++ b/docs/sections/user_guide/cli/tools/config/validate-help.out @@ -13,7 +13,7 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (default: stdin) + Path to input file (default: read from stdin) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/execute/help.out b/docs/sections/user_guide/cli/tools/execute/help.out index e6956922a..cc99c1284 100644 --- a/docs/sections/user_guide/cli/tools/execute/help.out +++ b/docs/sections/user_guide/cli/tools/execute/help.out @@ -20,7 +20,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --schema-file PATH Path to schema file to use for validation --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/copy-help.out b/docs/sections/user_guide/cli/tools/fs/copy-help.out index 5efb4be63..023636c60 100644 --- a/docs/sections/user_guide/cli/tools/fs/copy-help.out +++ b/docs/sections/user_guide/cli/tools/fs/copy-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/hardlink-help.out b/docs/sections/user_guide/cli/tools/fs/hardlink-help.out index 1a66c80e2..0f24e93fd 100644 --- a/docs/sections/user_guide/cli/tools/fs/hardlink-help.out +++ b/docs/sections/user_guide/cli/tools/fs/hardlink-help.out @@ -12,7 +12,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/link-help.out b/docs/sections/user_guide/cli/tools/fs/link-help.out index b371b872f..1d39b9337 100644 --- a/docs/sections/user_guide/cli/tools/fs/link-help.out +++ b/docs/sections/user_guide/cli/tools/fs/link-help.out @@ -10,7 +10,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/fs/makedirs-help.out b/docs/sections/user_guide/cli/tools/fs/makedirs-help.out index ab062acf5..abfe4e36c 100644 --- a/docs/sections/user_guide/cli/tools/fs/makedirs-help.out +++ b/docs/sections/user_guide/cli/tools/fs/makedirs-help.out @@ -12,7 +12,7 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --target-dir PATH Root directory for relative destination paths --cycle CYCLE diff --git a/docs/sections/user_guide/cli/tools/rocoto/realize-help.out b/docs/sections/user_guide/cli/tools/rocoto/realize-help.out index 93db5424e..614c1957d 100644 --- a/docs/sections/user_guide/cli/tools/rocoto/realize-help.out +++ b/docs/sections/user_guide/cli/tools/rocoto/realize-help.out @@ -9,9 +9,9 @@ Optional arguments: --version Show version info and exit --config-file PATH, -c PATH - Path to UW YAML config file (default: stdin) + Path to UW YAML config file (default: read from stdin) --output-file PATH, -o PATH - Path to output file (default: stdout) + Path to output file (default: write to stdout) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/rocoto/validate-help.out b/docs/sections/user_guide/cli/tools/rocoto/validate-help.out index e86ebd4d4..4816b797e 100644 --- a/docs/sections/user_guide/cli/tools/rocoto/validate-help.out +++ b/docs/sections/user_guide/cli/tools/rocoto/validate-help.out @@ -9,7 +9,7 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (default: stdin) + Path to input file (default: read from stdin) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/template/render-help.out b/docs/sections/user_guide/cli/tools/template/render-help.out index 7caa7aa2a..3aeef68fc 100644 --- a/docs/sections/user_guide/cli/tools/template/render-help.out +++ b/docs/sections/user_guide/cli/tools/template/render-help.out @@ -13,9 +13,9 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (default: stdin) + Path to input file (default: read from stdin) --output-file PATH, -o PATH - Path to output file (default: stdout) + Path to output file (default: write to stdout) --values-file PATH Path to file providing override or interpolation values --values-format {ini,nml,sh,yaml} diff --git a/docs/sections/user_guide/cli/tools/template/translate-help.out b/docs/sections/user_guide/cli/tools/template/translate-help.out index 228874c9f..9107720c0 100644 --- a/docs/sections/user_guide/cli/tools/template/translate-help.out +++ b/docs/sections/user_guide/cli/tools/template/translate-help.out @@ -10,9 +10,9 @@ Optional arguments: --version Show version info and exit --input-file PATH, -i PATH - Path to input file (default: stdin) + Path to input file (default: read from stdin) --output-file PATH, -o PATH - Path to output file (default: stdout) + Path to output file (default: write to stdout) --dry-run Only log info, making no changes --quiet, -q diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index bc82aadf6..7535c3579 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -759,7 +759,7 @@ def _add_arg_classname(group: Group) -> None: def _add_arg_config_file(group: Group, required: bool = False) -> None: - msg = "Path to UW YAML config file" + ("" if required else " (default: stdin)") + msg = "Path to UW YAML config file" + ("" if required else " (default: read from stdin)") group.add_argument( _switch(STR.cfgfile), "-c", @@ -840,7 +840,7 @@ def _add_arg_input_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.infile), "-i", - help="Path to input file (default: stdin)", + help="Path to input file (default: read from stdin)", metavar="PATH", required=required, type=Path, @@ -899,7 +899,7 @@ def _add_arg_output_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.outfile), "-o", - help="Path to output file (default: stdout)", + help="Path to output file (default: write to stdout)", metavar="PATH", required=required, type=Path, @@ -1015,7 +1015,7 @@ def _add_arg_update_file(group: Group, required: bool = False) -> None: group.add_argument( _switch(STR.updatefile), "-u", - help="Path to update file (default: stdin)", + help="Path to update file (default: read from stdin)", metavar="PATH", required=required, type=Path, From 18c5b80045bcab84596e38db6ea70bff66c8ae04 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 12:24:23 +0000 Subject: [PATCH 09/46] Add test --- src/uwtools/api/config.py | 4 +- src/uwtools/tests/api/test_config.py | 73 ++++++++++++++++++---------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 53ad53d52..c1612ef82 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -49,8 +49,8 @@ def compose( return _compose( configs=list(map(Path, configs)), output_file=Path(output_file) if output_file else None, - input_format=input_format, - output_format=output_format, + input_format=input_format or _FORMAT.yaml, + output_format=output_format or _FORMAT.yaml, ) diff --git a/src/uwtools/tests/api/test_config.py b/src/uwtools/tests/api/test_config.py index 29a5db9b6..de4988cb7 100644 --- a/src/uwtools/tests/api/test_config.py +++ b/src/uwtools/tests/api/test_config.py @@ -11,7 +11,7 @@ from uwtools.utils.file import FORMAT -def test_compare(): +def test_api_config_compare(): kwargs: dict = { "path1": "path1", "format1": "fmt1", @@ -29,6 +29,27 @@ def test_compare(): ) +@mark.parametrize("output_file", [None, "/path/to/out.yaml"]) +@mark.parametrize("input_format", [None, FORMAT.yaml, FORMAT.nml]) +@mark.parametrize("output_format", [None, FORMAT.yaml, FORMAT.nml]) +def test_api_config_compose(output_file, input_format, output_format): + pathstrs = ["/path/to/c1.yaml", "/path/to/c2.yaml"] + kwargs: dict = { + "configs": pathstrs, + "output_file": output_file, + "input_format": input_format, + "output_format": output_format, + } + with patch.object(config, "_compose") as _compose: + config.compose(**kwargs) + _compose.assert_called_once_with( + configs=list(map(Path, pathstrs)), + output_file=None if output_file is None else Path(output_file), + input_format=input_format or FORMAT.yaml, + output_format=output_format or FORMAT.yaml, + ) + + @mark.parametrize( ("classname", "f"), [ @@ -39,14 +60,14 @@ def test_compare(): ("YAMLConfig", config.get_yaml_config), ], ) -def test_get_config(classname, f): +def test_api_config__get_config(classname, f): kwargs: dict = dict(config={}) with patch.object(config, classname) as constructor: f(**kwargs) constructor.assert_called_once_with(**kwargs) -def test_realize(): +def test_api_config_realize(): kwargs: dict = { "input_config": "path1", "input_format": "fmt1", @@ -71,32 +92,13 @@ def test_realize(): ) -def test_realize_to_dict(): - kwargs: dict = { - "input_config": "path1", - "input_format": "fmt1", - "update_config": None, - "update_format": None, - "key_path": None, - "values_needed": True, - "total": False, - "dry_run": False, - "stdin_ok": False, - } - with patch.object(config, "realize") as realize: - config.realize_to_dict(**kwargs) - realize.assert_called_once_with( - **{**kwargs, "output_file": Path(os.devnull), "output_format": FORMAT.yaml} - ) - - -def test_realize_update_config_from_stdin(): +def test_api_config_realize__update_config_from_stdin(): with raises(UWError) as e: config.realize(input_config={}, output_file="output.yaml", update_format="yaml") assert str(e.value) == "Set stdin_ok=True to permit read from stdin" -def test_realize_update_config_none(): +def test_api_config_realize__update_config_none(): input_config = {"n": 42} output_file = Path("output.yaml") with patch.object(config, "_realize") as _realize: @@ -115,8 +117,27 @@ def test_realize_update_config_none(): ) +def test_api_config_realize_to_dict(): + kwargs: dict = { + "input_config": "path1", + "input_format": "fmt1", + "update_config": None, + "update_format": None, + "key_path": None, + "values_needed": True, + "total": False, + "dry_run": False, + "stdin_ok": False, + } + with patch.object(config, "realize") as realize: + config.realize_to_dict(**kwargs) + realize.assert_called_once_with( + **{**kwargs, "output_file": Path(os.devnull), "output_format": FORMAT.yaml} + ) + + @mark.parametrize("cfg", [{"foo": "bar"}, YAMLConfig(config={"foo": "bar"})]) -def test_validate_config_data(cfg): +def test_api_config_validate__config_data(cfg): kwargs: dict = {"schema_file": "schema-file", "config_data": cfg} with patch.object(config, "_validate_external") as _validate_external: assert config.validate(**kwargs) is True @@ -131,7 +152,7 @@ def test_validate_config_data(cfg): @mark.parametrize("cast", [str, Path]) -def test_validate_config_path(cast, tmp_path): +def test_api_config__validate_config_path(cast, tmp_path): cfg = tmp_path / "config.yaml" cfg.write_text(yaml.dump({})) kwargs: dict = {"schema_file": "schema-file", "config_path": cast(cfg)} From 594de1a46b8267503ff66d863adf4281df742500 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 12:36:47 +0000 Subject: [PATCH 10/46] WIP --- src/uwtools/config/tools.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index cc129542a..c67d69c5e 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -39,11 +39,14 @@ def compare( def compose( - configs: list[Path], # noqa: ARG001 - output_file: Path | None = None, # noqa: ARG001 - input_format: str | None = None, # noqa: ARG001 - output_format: str | None = None, # noqa: ARG001 + configs: list[Path], output_file: Path | None, input_format: str, output_format: str ) -> bool: + config, configs = configs[0], configs[1:] + print(output_file, input_format, output_format, config) # PM# REMOVE + # input_class = cast(Config, format_to_config(input_format)) + # output_class = cast(Config, format_to_config(output_format)) + # input_class = Config, format_to_config(input_format) + # output_class = Config, format_to_config(output_format) return True From e9a4f65b005e1ac64b8d995bb2fcb14028fc3c91 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 12:47:58 +0000 Subject: [PATCH 11/46] WIP --- src/uwtools/config/tools.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index c67d69c5e..acc792fec 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -41,12 +41,13 @@ def compare( def compose( configs: list[Path], output_file: Path | None, input_format: str, output_format: str ) -> bool: - config, configs = configs[0], configs[1:] - print(output_file, input_format, output_format, config) # PM# REMOVE - # input_class = cast(Config, format_to_config(input_format)) - # output_class = cast(Config, format_to_config(output_format)) - # input_class = Config, format_to_config(input_format) - # output_class = Config, format_to_config(output_format) + input_class = format_to_config(input_format) + config, rest = input_class(configs[0]), list(map(input_class, configs[1:])) + for c in rest: + config.update_from(c) + output_class = format_to_config(output_format) + output_config = output_class(config) + output_config.dump(output_file) return True From 207b96841434a03998ab04f0d3acad2c57a36cd1 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 13:05:37 +0000 Subject: [PATCH 12/46] realize_config -> realize --- src/uwtools/api/config.py | 2 +- src/uwtools/config/tools.py | 22 +-- src/uwtools/tests/config/test_tools.py | 230 ++++++++++++------------- 3 files changed, 124 insertions(+), 130 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index c1612ef82..6867d6d04 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -16,7 +16,7 @@ from uwtools.config.formats.yaml import YAMLConfig from uwtools.config.tools import compare as _compare from uwtools.config.tools import compose as _compose -from uwtools.config.tools import realize_config as _realize +from uwtools.config.tools import realize as _realize from uwtools.config.validator import ConfigDataT, ConfigPathT from uwtools.config.validator import validate_check_config as _validate_check_config from uwtools.config.validator import validate_external as _validate_external diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index acc792fec..8518e40c3 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -88,7 +88,7 @@ def config_check_depths_update(config_obj: Config | dict, target_format: str) -> _validate_depth(config_obj, target_format, "update", bad_depth) -def realize_config( +def realize( input_config: Config | Path | dict | None = None, input_format: str | None = None, update_config: Config | Path | dict | None = None, @@ -101,12 +101,12 @@ def realize_config( dry_run: bool = False, ) -> dict: """ - NB: This docstring is dynamically replaced: See realize_config.__doc__ definition below. + NB: This docstring is dynamically replaced: See realize.__doc__ definition below. """ - input_obj = _realize_config_input_setup(input_config, input_format) - input_obj = _realize_config_update(input_obj, update_config, update_format) + input_obj = _realize_input_setup(input_config, input_format) + input_obj = _realize_update(input_obj, update_config, update_format) input_obj.dereference() - output_data, output_format = _realize_config_output_setup( + output_data, output_format = _realize_output_setup( input_obj, output_file, output_format, key_path ) if dry_run: @@ -114,7 +114,7 @@ def realize_config( log.info(line) return {} if values_needed: - _realize_config_values_needed(input_obj) + _realize_values_needed(input_obj) return {} if total and unrendered(str(input_obj)): msg = "Config could not be totally realized" @@ -172,7 +172,7 @@ def _ensure_format( return get_config_format(config, desc) -def _realize_config_input_setup( +def _realize_input_setup( input_config: Config | Path | dict | None = None, input_format: str | None = None ) -> Config: """ @@ -191,7 +191,7 @@ def _realize_config_input_setup( return config_obj -def _realize_config_output_setup( +def _realize_output_setup( input_obj: Config, output_file: Path | None = None, output_format: str | None = None, @@ -218,7 +218,7 @@ def _realize_config_output_setup( return output_data, output_format -def _realize_config_update( +def _realize_update( input_obj: Config, update_config: Config | Path | dict | None = None, update_format: str | None = None, @@ -251,7 +251,7 @@ def _realize_config_update( return input_obj -def _realize_config_values_needed(input_obj: Config) -> None: +def _realize_values_needed(input_obj: Config) -> None: """ Print a report characterizing input values as complete, empty, or template placeholders. @@ -335,7 +335,7 @@ def _validate_format(other_fmt_desc: str, other_fmt: str, input_fmt: str) -> Non """.format(extensions=", ".join(FORMAT.extensions())).strip() -realize_config.__doc__ = """ +realize.__doc__ = """ Realize an output config based on an input config and optional values-providing configs. Recognized file extensions are: {extensions} diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index a978abb66..31cec8139 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -41,12 +41,12 @@ def compare_assets(tmp_path): @fixture -def realize_config_testobj(realize_config_yaml_input): - return YAMLConfig(config=realize_config_yaml_input) +def realize_testobj(realize_yaml_input): + return YAMLConfig(config=realize_yaml_input) @fixture -def realize_config_yaml_input(tmp_path): +def realize_yaml_input(tmp_path): path = tmp_path / "a.yaml" d = {1: {2: {3: 42}}} # depth 3 with writable(path) as f: @@ -57,20 +57,20 @@ def realize_config_yaml_input(tmp_path): # Helpers -def help_realize_config_double_tag(config, expected, tmp_path): +def help_realize_double_tag(config, expected, tmp_path): path_in = tmp_path / "in.yaml" path_out = tmp_path / "out.yaml" path_in.write_text(dedent(config).strip()) - tools.realize_config(input_config=path_in, output_file=path_out) + tools.realize(input_config=path_in, output_file=path_out) assert path_out.read_text().strip() == dedent(expected).strip() -def help_realize_config_fmt2fmt(input_file, input_format, update_file, update_format, tmpdir): +def help_realize_fmt2fmt(input_file, input_format, update_file, update_format, tmpdir): input_file = fixture_path(input_file) update_file = fixture_path(update_file) ext = Path(input_file).suffix output_file = tmpdir / f"output_file{ext}" - tools.realize_config( + tools.realize( input_config=input_file, input_format=input_format, update_config=update_file, @@ -86,11 +86,11 @@ def help_realize_config_fmt2fmt(input_file, input_format, update_file, update_fo assert compare_files(reference, output_file) -def help_realize_config_simple(infn, infmt, tmpdir): +def help_realize_simple(infn, infmt, tmpdir): infile = fixture_path(infn) ext = Path(infile).suffix outfile = tmpdir / f"outfile{ext}" - tools.realize_config( + tools.realize( input_config=infile, input_format=infmt, output_file=outfile, @@ -174,21 +174,17 @@ def test_compare__bad_format(logged): assert logged(msg) -def test_config_check_depths_realize__fail(realize_config_testobj): - depthin = depth(realize_config_testobj.data) +def test_config_check_depths_realize__fail(realize_testobj): + depthin = depth(realize_testobj.data) with raises(UWConfigError) as e: - tools.config_check_depths_realize( - config_obj=realize_config_testobj, target_format=FORMAT.ini - ) + tools.config_check_depths_realize(config_obj=realize_testobj, target_format=FORMAT.ini) assert f"Cannot realize depth-{depthin} config to type-'ini' config" in str(e.value) -def test_config_check_depths_update__fail(realize_config_testobj): - depthin = depth(realize_config_testobj.data) +def test_config_check_depths_update__fail(realize_testobj): + depthin = depth(realize_testobj.data) with raises(UWConfigError) as e: - tools.config_check_depths_update( - config_obj=realize_config_testobj, target_format=FORMAT.ini - ) + tools.config_check_depths_update(config_obj=realize_testobj, target_format=FORMAT.ini) assert f"Cannot update depth-{depthin} config to type-'ini' config" in str(e.value) @@ -211,13 +207,13 @@ def test_config_tools_format_to_config__fail(): tools.format_to_config("no-such-config-type") -def test_realize_config__conversion_cfg_to_yaml(tmp_path): +def test_realize__conversion_cfg_to_yaml(tmp_path): """ Test that a .cfg file can be used to create a YAML object. """ infile = fixture_path("srw_example_yaml.cfg") outfile = tmp_path / "test_ouput.yaml" - tools.realize_config( + tools.realize( input_config=infile, input_format=FORMAT.yaml, output_file=outfile, @@ -231,25 +227,25 @@ def test_realize_config__conversion_cfg_to_yaml(tmp_path): assert outfile.read_text()[-1] == "\n" -def test_realize_config__depth_mismatch_to_ini(realize_config_yaml_input): +def test_realize__depth_mismatch_to_ini(realize_yaml_input): with raises(UWConfigError): - tools.realize_config( - input_config=realize_config_yaml_input, + tools.realize( + input_config=realize_yaml_input, input_format=FORMAT.yaml, output_format=FORMAT.ini, ) -def test_realize_config__depth_mismatch_to_sh(realize_config_yaml_input): +def test_realize__depth_mismatch_to_sh(realize_yaml_input): with raises(UWConfigError): - tools.realize_config( - input_config=realize_config_yaml_input, + tools.realize( + input_config=realize_yaml_input, input_format=FORMAT.yaml, output_format=FORMAT.sh, ) -def test_realize_config__double_tag_flat(tmp_path): +def test_realize__double_tag_flat(tmp_path): config = """ a: 1 b: 2 @@ -262,10 +258,10 @@ def test_realize_config__double_tag_flat(tmp_path): foo: 3 bar: 3 """ - help_realize_config_double_tag(config, expected, tmp_path) + help_realize_double_tag(config, expected, tmp_path) -def test_realize_config__double_tag_nest(tmp_path): +def test_realize__double_tag_nest(tmp_path): config = """ a: 1.0 b: 2.0 @@ -280,10 +276,10 @@ def test_realize_config__double_tag_nest(tmp_path): foo: 3.0 bar: 3.0 """ - help_realize_config_double_tag(config, expected, tmp_path) + help_realize_double_tag(config, expected, tmp_path) -def test_realize_config__double_tag_nest_forward_reference(tmp_path): +def test_realize__double_tag_nest_forward_reference(tmp_path): config = """ a: true b: false @@ -298,17 +294,17 @@ def test_realize_config__double_tag_nest_forward_reference(tmp_path): qux: foo: true """ - help_realize_config_double_tag(config, expected, tmp_path) + help_realize_double_tag(config, expected, tmp_path) -def test_realize_config__dry_run(logged): +def test_realize__dry_run(logged): """ Test that providing a YAML base file with a dry-run flag will print an YAML config file. """ infile = fixture_path("fruit_config.yaml") yaml_config = YAMLConfig(infile) yaml_config.dereference() - tools.realize_config( + tools.realize( input_config=infile, input_format=FORMAT.yaml, output_format=FORMAT.yaml, @@ -317,13 +313,13 @@ def test_realize_config__dry_run(logged): assert logged(str(yaml_config), multiline=True) -def test_realize_config__field_table(tmp_path): +def test_realize__field_table(tmp_path): """ Test reading a YAML config object and generating a field file table. """ infile = fixture_path("FV3_GFS_v16.yaml") outfile = tmp_path / "field_table_from_yaml.FV3_GFS" - tools.realize_config( + tools.realize( input_config=infile, input_format=FORMAT.yaml, output_file=outfile, @@ -338,51 +334,51 @@ def test_realize_config__field_table(tmp_path): assert line1 in line2 -def test_realize_config__fmt2fmt_nml2nml(tmp_path): +def test_realize__fmt2fmt_nml2nml(tmp_path): """ Test that providing a namelist base input file and a config file will create and update namelist config file. """ - help_realize_config_fmt2fmt("simple.nml", FORMAT.nml, "simple2.nml", FORMAT.nml, tmp_path) + help_realize_fmt2fmt("simple.nml", FORMAT.nml, "simple2.nml", FORMAT.nml, tmp_path) -def test_realize_config__fmt2fmt_ini2ini(tmp_path): +def test_realize__fmt2fmt_ini2ini(tmp_path): """ Test that providing an INI base input file and an INI config file will create and update INI config file. """ - help_realize_config_fmt2fmt("simple.ini", FORMAT.ini, "simple2.ini", FORMAT.ini, tmp_path) + help_realize_fmt2fmt("simple.ini", FORMAT.ini, "simple2.ini", FORMAT.ini, tmp_path) -def test_realize_config__fmt2fmt_yaml2yaml(tmp_path): +def test_realize__fmt2fmt_yaml2yaml(tmp_path): """ Test that providing a YAML base input file and a YAML config file will create and update YAML config file. """ - help_realize_config_fmt2fmt( + help_realize_fmt2fmt( "fruit_config.yaml", FORMAT.yaml, "fruit_config_similar.yaml", FORMAT.yaml, tmp_path ) -def test_realize_config__incompatible_file_type(): +def test_realize__incompatible_file_type(): """ Test that providing an incompatible file type for input base file will return print statement. """ with raises(UWError): - tools.realize_config( + tools.realize( input_config=fixture_path("model_configure.sample"), input_format="sample", output_format=FORMAT.yaml, ) -def test_realize_config__output_file_format(tmp_path): +def test_realize__output_file_format(tmp_path): """ Test that output_format overrides bad output_file extension. """ infile = fixture_path("simple.nml") outfile = tmp_path / "test_ouput.cfg" - tools.realize_config( + tools.realize( input_config=infile, output_file=outfile, output_format=FORMAT.nml, @@ -390,7 +386,7 @@ def test_realize_config__output_file_format(tmp_path): assert compare_files(outfile, infile) -def test_realize_config__remove_nml_to_nml(tmp_path): +def test_realize__remove_nml_to_nml(tmp_path): input_config = NMLConfig({"constants": {"pi": 3.141, "e": 2.718}}) s = """ constants: @@ -400,7 +396,7 @@ def test_realize_config__remove_nml_to_nml(tmp_path): update_config.write_text(dedent(s).strip()) output_file = tmp_path / "config.nml" assert not output_file.is_file() - tools.realize_config( + tools.realize( input_config=input_config, update_config=update_config, output_file=output_file, @@ -408,7 +404,7 @@ def test_realize_config__remove_nml_to_nml(tmp_path): assert f90nml.read(output_file) == {"constants": {"pi": 3.141}} -def test_realize_config__remove_yaml_to_yaml_scalar(tmp_path): +def test_realize__remove_yaml_to_yaml_scalar(tmp_path): input_config = YAMLConfig({"a": {"b": {"c": 11, "d": 22, "e": 33}}}) s = """ a: @@ -417,14 +413,14 @@ def test_realize_config__remove_yaml_to_yaml_scalar(tmp_path): """ update_config = tmp_path / "update.yaml" update_config.write_text(dedent(s).strip()) - assert tools.realize_config( + assert tools.realize( input_config=input_config, update_config=update_config, output_format=FORMAT.yaml, ) == {"a": {"b": {"c": 11, "e": 33}}} -def test_realize_config__remove_yaml_to_yaml_subtree(tmp_path): +def test_realize__remove_yaml_to_yaml_subtree(tmp_path): input_config = YAMLConfig(yaml.safe_load("a: {b: {c: 11, d: 22, e: 33}}")) s = """ a: @@ -432,16 +428,16 @@ def test_realize_config__remove_yaml_to_yaml_subtree(tmp_path): """ update_config = tmp_path / "update.yaml" update_config.write_text(dedent(s).strip()) - assert tools.realize_config( + assert tools.realize( input_config=input_config, update_config=update_config, output_format=FORMAT.yaml, ) == {"a": {}} -def test_realize_config__scalar_value(capsys): +def test_realize__scalar_value(capsys): stdinproxy.cache_clear() - tools.realize_config( + tools.realize( input_config=YAMLConfig(config={"foo": {"bar": "baz"}}), output_format="yaml", key_path=["foo", "bar"], @@ -449,42 +445,42 @@ def test_realize_config__scalar_value(capsys): assert capsys.readouterr().out.strip() == "baz" -def test_realize_config__simple_ini(tmp_path): +def test_realize__simple_ini(tmp_path): """ Test that providing an INI file with necessary settings will create an INI config file. """ - help_realize_config_simple("simple.ini", FORMAT.ini, tmp_path) + help_realize_simple("simple.ini", FORMAT.ini, tmp_path) -def test_realize_config__simple_namelist(tmp_path): +def test_realize__simple_namelist(tmp_path): """ Test that providing a namelist file with necessary settings will create a namelist config file. """ - help_realize_config_simple("simple.nml", FORMAT.nml, tmp_path) + help_realize_simple("simple.nml", FORMAT.nml, tmp_path) -def test_realize_config__simple_sh(tmp_path): +def test_realize__simple_sh(tmp_path): """ Test that providing an sh file with necessary settings will create an sh config file. """ - help_realize_config_simple("simple.sh", FORMAT.sh, tmp_path) + help_realize_simple("simple.sh", FORMAT.sh, tmp_path) -def test_realize_config__simple_yaml(tmp_path): +def test_realize__simple_yaml(tmp_path): """ Test that providing a YAML base file with necessary settings will create a YAML config file. """ - help_realize_config_simple("simple2.yaml", FORMAT.yaml, tmp_path) + help_realize_simple("simple2.yaml", FORMAT.yaml, tmp_path) -def test_realize_config__single_dereference(capsys, tmp_path): +def test_realize__single_dereference(capsys, tmp_path): input_config = tmp_path / "a.yaml" update_config = tmp_path / "b.yaml" with writable(input_config) as f: yaml.dump({"1": "a", "2": "{{ deref }}", "3": "{{ temporalis }}"}, f) with writable(update_config) as f: yaml.dump({"2": "b", "temporalis": "c", "deref": "d"}, f) - tools.realize_config( + tools.realize( input_config=input_config, update_config=update_config, output_format=FORMAT.yaml, @@ -500,22 +496,22 @@ def test_realize_config__single_dereference(capsys, tmp_path): assert actual == dedent(expected).strip() -def test_realize_config__total_fail(): +def test_realize__total_fail(): with raises(UWConfigError) as e: - tools.realize_config( + tools.realize( input_config=YAMLConfig({"foo": "{{ bar }}"}), output_format=FORMAT.yaml, total=True ) assert str(e.value) == "Config could not be totally realized" -def test_realize_config__update_bad_format_defaults_to_yaml(capsys, tmp_path): +def test_realize__update_bad_format_defaults_to_yaml(capsys, tmp_path): input_config = tmp_path / "a.yaml" update_config = tmp_path / "b.clj" with writable(input_config) as f: yaml.dump({"1": "a", "2": "{{ deref }}", "3": "{{ temporalis }}", "deref": "b"}, f) with writable(update_config) as f: yaml.dump({"2": "b", "temporalis": "c"}, f) - tools.realize_config( + tools.realize( input_config=input_config, update_config=update_config, output_format=FORMAT.yaml, @@ -530,11 +526,11 @@ def test_realize_config__update_bad_format_defaults_to_yaml(capsys, tmp_path): assert capsys.readouterr().out.strip() == dedent(expected).strip() -def test_realize_config__update_none(capsys, tmp_path): +def test_realize__update_none(capsys, tmp_path): path = tmp_path / "a.yaml" with writable(path) as f: yaml.dump({"1": "a", "2": "{{ deref }}", "3": "{{ temporalis }}", "deref": "b"}, f) - tools.realize_config( + tools.realize( input_config=path, input_format=FORMAT.yaml, output_format=FORMAT.yaml, @@ -549,12 +545,12 @@ def test_realize_config__update_none(capsys, tmp_path): assert actual == dedent(expected).strip() -def test_realize_config__values_needed_ini(logged): +def test_realize__values_needed_ini(logged): """ Test that the values_needed flag logs keys completed and keys containing unrendered Jinja2 variables/expressions. """ - tools.realize_config( + tools.realize( input_config=fixture_path("simple3.ini"), input_format=FORMAT.ini, output_format=FORMAT.ini, @@ -581,12 +577,12 @@ def test_realize_config__values_needed_ini(logged): assert logged(dedent(expected), multiline=True) -def test_realize_config__values_needed_yaml(logged): +def test_realize__values_needed_yaml(logged): """ Test that the values_needed flag logs keys completed and keys containing unrendered Jinja2 variables/expressions. """ - tools.realize_config( + tools.realize( input_config=fixture_path("srw_example.yaml"), input_format=FORMAT.yaml, output_format=FORMAT.yaml, @@ -662,25 +658,25 @@ def test__ensure_format__explicitly_specified_with_path(): ) -def test__realize_config_input_setup__ini_cfgobj(): +def test__realize_input_setup__ini_cfgobj(): data = {"section": {"foo": "bar"}} cfgobj = INIConfig(config=data) - input_obj = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test__realize_config_input_setup__ini_file(tmp_path): +def test__realize_input_setup__ini_file(tmp_path): data = """ [section] foo = bar """ path = tmp_path / "config.ini" path.write_text(dedent(data).strip()) - input_obj = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_input_setup(input_config=path) assert input_obj.data == {"section": {"foo": "bar"}} -def test__realize_config_input_setup__ini_stdin(logged): +def test__realize_input_setup__ini_stdin(logged): data = """ [section] foo = bar @@ -691,19 +687,19 @@ def test__realize_config_input_setup__ini_stdin(logged): print(dedent(data).strip(), file=sio) sio.seek(0) with patch.object(sys, "stdin", new=sio): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.ini) + input_obj = tools._realize_input_setup(input_format=FORMAT.ini) assert input_obj.data == {"section": {"foo": "bar", "baz": "42"}} # note: 42 is str, not int assert logged("Reading input from stdin") -def test__realize_config_input_setup__nml_cfgobj(): +def test__realize_input_setup__nml_cfgobj(): data = {"nl": {"pi": 3.14}} cfgobj = NMLConfig(config=data) - input_obj = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test__realize_config_input_setup__nml_file(tmp_path): +def test__realize_input_setup__nml_file(tmp_path): data = """ &nl pi = 3.14 @@ -711,11 +707,11 @@ def test__realize_config_input_setup__nml_file(tmp_path): """ path = tmp_path / "config.nml" path.write_text(dedent(data).strip()) - input_obj = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_input_setup(input_config=path) assert input_obj["nl"]["pi"] == 3.14 -def test__realize_config_input_setup__nml_stdin(logged): +def test__realize_input_setup__nml_stdin(logged): data = """ &nl pi = 3.14 @@ -726,29 +722,29 @@ def test__realize_config_input_setup__nml_stdin(logged): print(dedent(data).strip(), file=sio) sio.seek(0) with patch.object(sys, "stdin", new=sio): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.nml) + input_obj = tools._realize_input_setup(input_format=FORMAT.nml) assert input_obj["nl"]["pi"] == 3.14 assert logged("Reading input from stdin") -def test__realize_config_input_setup__sh_cfgobj(): +def test__realize_input_setup__sh_cfgobj(): data = {"foo": "bar"} cfgobj = SHConfig(config=data) - input_obj = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test__realize_config_input_setup__sh_file(tmp_path): +def test__realize_input_setup__sh_file(tmp_path): data = """ foo=bar """ path = tmp_path / "config.sh" path.write_text(dedent(data).strip()) - input_obj = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_input_setup(input_config=path) assert input_obj.data == {"foo": "bar"} -def test__realize_config_input_setup__sh_stdin(logged): +def test__realize_input_setup__sh_stdin(logged): data = """ foo=bar """ @@ -757,29 +753,29 @@ def test__realize_config_input_setup__sh_stdin(logged): print(dedent(data).strip(), file=sio) sio.seek(0) with patch.object(sys, "stdin", new=sio): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.sh) + input_obj = tools._realize_input_setup(input_format=FORMAT.sh) assert input_obj.data == {"foo": "bar"} assert logged("Reading input from stdin") -def test__realize_config_input_setup__yaml_cfgobj(): +def test__realize_input_setup__yaml_cfgobj(): data = {"foo": "bar"} cfgobj = YAMLConfig(config=data) - input_obj = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test__realize_config_input_setup__yaml_file(tmp_path): +def test__realize_input_setup__yaml_file(tmp_path): data = """ foo: bar """ path = tmp_path / "config.yaml" path.write_text(dedent(data).strip()) - input_obj = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_input_setup(input_config=path) assert input_obj.data == {"foo": "bar"} -def test__realize_config_input_setup__yaml_stdin(logged): +def test__realize_input_setup__yaml_stdin(logged): data = """ foo: bar """ @@ -788,70 +784,68 @@ def test__realize_config_input_setup__yaml_stdin(logged): print(dedent(data).strip(), file=sio) sio.seek(0) with patch.object(sys, "stdin", new=sio): - input_obj = tools._realize_config_input_setup(input_format=FORMAT.yaml) + input_obj = tools._realize_input_setup(input_format=FORMAT.yaml) assert input_obj.data == {"foo": "bar"} assert logged("Reading input from stdin") -def test__realize_config_output_setup(logged, tmp_path): +def test__realize_output_setup(logged, tmp_path): input_obj = YAMLConfig({"a": {"b": {"foo": "bar"}}}) output_file = tmp_path / "output.yaml" - assert tools._realize_config_output_setup( + assert tools._realize_output_setup( input_obj=input_obj, output_file=output_file, key_path=["a", "b"] ) == ({"foo": "bar"}, FORMAT.yaml) assert logged(f"Writing output to {output_file}") -def test__realize_config_update__cfgobj(realize_config_testobj): - assert realize_config_testobj[1][2][3] == 42 +def test__realize_update__cfgobj(realize_testobj): + assert realize_testobj[1][2][3] == 42 update_config = YAMLConfig(config={1: {2: {3: 43}}}) - o = tools._realize_config_update(input_obj=realize_config_testobj, update_config=update_config) + o = tools._realize_update(input_obj=realize_testobj, update_config=update_config) assert o[1][2][3] == 43 -def test__realize_config_update__stdin(logged, realize_config_testobj): +def test__realize_update__stdin(logged, realize_testobj): stdinproxy.cache_clear() - assert realize_config_testobj[1][2][3] == 42 + assert realize_testobj[1][2][3] == 42 with StringIO() as sio: print("{1: {2: {3: 43}}}", file=sio) sio.seek(0) with patch.object(sys, "stdin", new=sio): - o = tools._realize_config_update( - input_obj=realize_config_testobj, update_format=FORMAT.yaml - ) + o = tools._realize_update(input_obj=realize_testobj, update_format=FORMAT.yaml) assert o[1][2][3] == 43 assert logged("Reading update from stdin") -def test__realize_config_update__noop(realize_config_testobj): - assert tools._realize_config_update(input_obj=realize_config_testobj) == realize_config_testobj +def test__realize_update__noop(realize_testobj): + assert tools._realize_update(input_obj=realize_testobj) == realize_testobj -def test__realize_config_update__file(realize_config_testobj, tmp_path): - assert realize_config_testobj[1][2][3] == 42 +def test__realize_update__file(realize_testobj, tmp_path): + assert realize_testobj[1][2][3] == 42 values = {1: {2: {3: 43}}} update_config = tmp_path / "config.yaml" update_config.write_text(yaml.dump(values)) - o = tools._realize_config_update(input_obj=realize_config_testobj, update_config=update_config) + o = tools._realize_update(input_obj=realize_testobj, update_config=update_config) assert o[1][2][3] == 43 -def test__realize_config_values_needed(logged, tmp_path): +def test__realize_values_needed(logged, tmp_path): path = tmp_path / "a.yaml" with writable(path) as f: yaml.dump({1: "complete", 2: "{{ jinja2 }}", 3: ""}, f) c = YAMLConfig(config=path) - tools._realize_config_values_needed(input_obj=c) + tools._realize_values_needed(input_obj=c) assert logged("Keys that are complete:\n 1", multiline=True) assert logged("Keys with unrendered Jinja2 variables/expressions:\n 2", multiline=True) -def test__realize_config_values_needed__negative_results(logged, tmp_path): +def test__realize_values_needed__negative_results(logged, tmp_path): path = tmp_path / "a.yaml" with writable(path) as f: yaml.dump({}, f) c = YAMLConfig(config=path) - tools._realize_config_values_needed(input_obj=c) + tools._realize_values_needed(input_obj=c) assert logged("No keys are complete.") assert logged("No keys have unrendered Jinja2 variables/expressions.") From 2f2091572999760bc403394b8655852500e41e6b Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 13:20:03 +0000 Subject: [PATCH 13/46] Work on dynamic docstring --- src/uwtools/api/config.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 6867d6d04..72d3ebac1 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -46,6 +46,9 @@ def compose( input_format: str | None = None, output_format: str | None = None, ) -> bool: + """ + NB: This docstring is dynamically replaced: See compose.__doc__ definition below. + """ return _compose( configs=list(map(Path, configs)), output_file=Path(output_file) if output_file else None, @@ -224,7 +227,24 @@ def validate( :param format1: Format of 1st config file (optional if file's extension is recognized). :param format2: Format of 2nd config file (optional if file's extension is recognized). :return: ``False`` if config files had differences, otherwise ``True``. -""".format(extensions=", ".join(_FORMAT.extensions())).strip() +""".format(extensions=", ".join(["``{x}``" for x in _FORMAT.extensions()])).strip() + +compose.__doc__ = """ +Compose config files. + +Specify explicit input or output formats to override default treatment baseed on file extension. +Recognized file extensions are: {extensions}. + +:param configs: Paths to configs to compose. +:param output_file: Output config destination (default: write to ``stdout``). +:param input_format: Format of configs to compose (choices: {choices}, default: ``{default}``) +:param output_format: Format of output config (choices: {choices}, default: ``{default}``) +:return: ``True`` if no errors were encountered. +""".format( + default=_FORMAT.yaml, + extensions=", ".join(["``{x}``" for x in _FORMAT.extensions()]), + choices=", ".join([f"``{x}``" for x in (_FORMAT.ini, _FORMAT.nml, _FORMAT.sh, _FORMAT.yaml)]), +).strip() realize.__doc__ = """ @@ -265,7 +285,7 @@ def validate( :param stdin_ok: OK to read from ``stdin``? :return: The ``dict`` representation of the realized config. :raises: ``UWConfigRealizeError`` if ``total`` is ``True`` and any Jinja2 syntax was not rendered. -""".format(extensions=", ".join(_FORMAT.extensions())).strip() # noqa: E501 +""".format(extensions=", ".join(["``{x}``" for x in _FORMAT.extensions()])).strip() # noqa: E501 __all__ = [ "Config", @@ -275,6 +295,7 @@ def validate( "SHConfig", "YAMLConfig", "compare", + "compose", "get_fieldtable_config", "get_ini_config", "get_nml_config", From 21130e842cdd00a9d0984c4db71bce3e29661853 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 13:27:18 +0000 Subject: [PATCH 14/46] Work on dynamic docstring --- src/uwtools/api/config.py | 1 - src/uwtools/config/tools.py | 32 +++++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 72d3ebac1..b99f00b59 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -246,7 +246,6 @@ def validate( choices=", ".join([f"``{x}``" for x in (_FORMAT.ini, _FORMAT.nml, _FORMAT.sh, _FORMAT.yaml)]), ).strip() - realize.__doc__ = """ Realize a config based on a base input config and an optional update config. diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 8518e40c3..e8e58c85e 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -41,6 +41,9 @@ def compare( def compose( configs: list[Path], output_file: Path | None, input_format: str, output_format: str ) -> bool: + """ + NB: This docstring is dynamically replaced: See compose.__doc__ definition below. + """ input_class = format_to_config(input_format) config, rest = input_class(configs[0]), list(map(input_class, configs[1:])) for c in rest: @@ -178,7 +181,7 @@ def _realize_input_setup( """ Set up config-realize input. - :param input_config: Input config source (None => read stdin). + :param input_config: Input config source (None => read from stdin). :param input_format: Format of the input config. :return: The input Config object. """ @@ -227,7 +230,7 @@ def _realize_update( Set up config-realize update. :param input_obj: The input Config object. - :param update_config: Input config source (None => read stdin). + :param update_config: Input config source (None => read from stdin). :param update_format: Format of the update config. :return: The updated but unrealized Config object. """ @@ -331,25 +334,40 @@ def _validate_format(other_fmt_desc: str, other_fmt: str, input_fmt: str) -> Non :param path2: Path to 2nd config file :param format1: Format of 1st config file (optional if file's extension is recognized) :param format2: Format of 2nd config file (optional if file's extension is recognized) -:return: ``False`` if config files had differences, otherwise ``True`` +:return: False if config files had differences, otherwise True """.format(extensions=", ".join(FORMAT.extensions())).strip() +compose.__doc__ = """ +Compose config files. + +Recognized file extensions are: {extensions} + +:param configs: Paths to configs to compose. +:param output_file: Output config destination (default: write to stdout). +:param input_format: Format of configs to compose (choices: {choices}, default: {default}) +:param output_format: Format of output config (choices: {choices}, default: {default}) +:return: True if no errors were encountered. +""".format( + default=FORMAT.yaml, + extensions=", ".join(FORMAT.extensions()), + choices=", ".join([FORMAT.ini, FORMAT.nml, FORMAT.sh, FORMAT.yaml]), +).strip() realize.__doc__ = """ Realize an output config based on an input config and optional values-providing configs. Recognized file extensions are: {extensions} -:param input_config: Input config source (None => read ``stdin``). +:param input_config: Input config source (None => read from stdin). :param input_format: Input config format. -:param update_config: Input config source (None => read ``stdin``). +:param update_config: Input config source (None => read from stdin). :param update_format: Update config format. -:param output_file: Output config destination (None => write to ``stdout``). +:param output_file: Output config destination (None => write to stdout). :param output_format: Output config format. :param key_path: Path of keys to the desired output block. :param values_needed: Report complete, missing, and template values. :param total: Require rendering of all Jinja2 variables/expressions. :param dry_run: Log output instead of writing to output. -:raises: UWConfigRealizeError if ``total`` is ``True`` and config cannot be totally realized. +:raises: UWConfigRealizeError if total is True and config cannot be totally realized. :return: The realized config (or an empty-dict for no-op modes). """.format(extensions=", ".join(FORMAT.extensions())).strip() From d88f271ef030a423946d71587fcf7fb314f687fa Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 13:34:44 +0000 Subject: [PATCH 15/46] WIP --- src/uwtools/config/tools.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index e8e58c85e..88ecd5ebf 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -45,11 +45,16 @@ def compose( NB: This docstring is dynamically replaced: See compose.__doc__ definition below. """ input_class = format_to_config(input_format) - config, rest = input_class(configs[0]), list(map(input_class, configs[1:])) - for c in rest: + log.debug("Reading %s as base config to compose onto", configs[0]) + config = input_class(configs[0]) + for path in configs[1:]: + log.debug("Composing %s onto base", path) + c = input_class(path) + config_check_depths_update(c, input_format) config.update_from(c) output_class = format_to_config(output_format) output_config = output_class(config) + config_check_depths_dump(config, output_format) output_config.dump(output_file) return True From 1f296e224ebe9d49765b629fa46eda60c232ee3e Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 13:45:01 +0000 Subject: [PATCH 16/46] WIP --- src/uwtools/config/tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 88ecd5ebf..51517ebee 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -44,6 +44,7 @@ def compose( """ NB: This docstring is dynamically replaced: See compose.__doc__ definition below. """ + _validate_format("output format", output_format, input_format) input_class = format_to_config(input_format) log.debug("Reading %s as base config to compose onto", configs[0]) config = input_class(configs[0]) From 8126013476033455800e29fd3930fe4562fdf938 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 13:48:10 +0000 Subject: [PATCH 17/46] WIP --- src/uwtools/config/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 51517ebee..a1b5347b3 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -62,7 +62,7 @@ def compose( def config_check_depths_dump(config_obj: Config | dict, target_format: str) -> None: """ - Check that the depth does not exceed the target format's max. + Ensure that the depth does not exceed the target format's max. :param config_obj: The reference config dictionary. :param target_format: The target format. @@ -75,7 +75,7 @@ def config_check_depths_dump(config_obj: Config | dict, target_format: str) -> N def config_check_depths_realize(config_obj: Config | dict, target_format: str) -> None: """ - Check that the depth does not exceed the target format's max. + Ensure that the depth is that required by the target format. :param config_obj: The reference config object. :param target_format: The target format. @@ -87,7 +87,7 @@ def config_check_depths_realize(config_obj: Config | dict, target_format: str) - def config_check_depths_update(config_obj: Config | dict, target_format: str) -> None: """ - Check that the depth does not exceed the target format's max. + Ensure that the depth does not exceed the target format's max. :param config_obj: The reference config object. :param target_format: The target format. From f6d6a6b24d03e6ed47096b0652938d0ecaae6fee Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 16:17:41 +0000 Subject: [PATCH 18/46] Post-merge fixup --- src/uwtools/config/tools.py | 4 +- src/uwtools/tests/config/test_tools.py | 80 +++++++++++++------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index c1fe701ec..2a6d2e57c 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -51,11 +51,11 @@ def compose( for path in configs[1:]: log.debug("Composing %s onto base", path) c = input_class(path) - config_check_depths_update(c, input_format) + # config_check_depths_update(c, input_format) config.update_from(c) output_class = format_to_config(output_format) output_config = output_class(config) - config_check_depths_dump(config, output_format) + # config_check_depths_dump(config, output_format) output_config.dump(output_file) return True diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index e7e4470b7..f15164860 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -192,7 +192,7 @@ def test_config_tools_config_tools_format_to_config__fail(): tools.format_to_config("no-such-config-type") -def test_config_tools_realize_config__conversion_cfg_to_yaml(tmp_path): +def test_config_tools_realize__conversion_cfg_to_yaml(tmp_path): """ Test that a .cfg file can be used to create a YAML object. """ @@ -212,7 +212,7 @@ def test_config_tools_realize_config__conversion_cfg_to_yaml(tmp_path): assert outfile.read_text()[-1] == "\n" -def test_config_tools_realize_config__depth_mismatch_to_ini(realize_config_yaml_input): +def test_config_tools_realize__depth_mismatch_to_ini(realize_yaml_input): with raises(UWConfigError): tools.realize( input_config=realize_yaml_input, @@ -221,7 +221,7 @@ def test_config_tools_realize_config__depth_mismatch_to_ini(realize_config_yaml_ ) -def test_config_tools_realize_config__depth_mismatch_to_sh(realize_config_yaml_input): +def test_config_tools_realize__depth_mismatch_to_sh(realize_yaml_input): with raises(UWConfigError): tools.realize( input_config=realize_yaml_input, @@ -230,7 +230,7 @@ def test_config_tools_realize_config__depth_mismatch_to_sh(realize_config_yaml_i ) -def test_config_tools_realize_config__double_tag_flat(tmp_path): +def test_config_tools_realize__double_tag_flat(tmp_path): config = """ a: 1 b: 2 @@ -246,7 +246,7 @@ def test_config_tools_realize_config__double_tag_flat(tmp_path): help_realize_double_tag(config, expected, tmp_path) -def test_config_tools_realize_config__double_tag_nest(tmp_path): +def test_config_tools_realize__double_tag_nest(tmp_path): config = """ a: 1.0 b: 2.0 @@ -264,7 +264,7 @@ def test_config_tools_realize_config__double_tag_nest(tmp_path): help_realize_double_tag(config, expected, tmp_path) -def test_config_tools_realize_config__double_tag_nest_forward_reference(tmp_path): +def test_config_tools_realize__double_tag_nest_forward_reference(tmp_path): config = """ a: true b: false @@ -282,7 +282,7 @@ def test_config_tools_realize_config__double_tag_nest_forward_reference(tmp_path help_realize_double_tag(config, expected, tmp_path) -def test_config_tools_realize_config__dry_run(logged): +def test_config_tools_realize__dry_run(logged): """ Test that providing a YAML base file with a dry-run flag will print an YAML config file. """ @@ -298,7 +298,7 @@ def test_config_tools_realize_config__dry_run(logged): assert logged(str(yaml_config), multiline=True) -def test_config_tools_realize_config__field_table(tmp_path): +def test_config_tools_realize__field_table(tmp_path): """ Test reading a YAML config object and generating a field file table. """ @@ -319,7 +319,7 @@ def test_config_tools_realize_config__field_table(tmp_path): assert line1 in line2 -def test_config_tools_realize_config__fmt2fmt_nml2nml(tmp_path): +def test_config_tools_realize__fmt2fmt_nml2nml(tmp_path): """ Test that providing a namelist base input file and a config file will create and update namelist config file. @@ -327,7 +327,7 @@ def test_config_tools_realize_config__fmt2fmt_nml2nml(tmp_path): help_realize_fmt2fmt("simple.nml", FORMAT.nml, "simple2.nml", FORMAT.nml, tmp_path) -def test_config_tools_realize_config__fmt2fmt_ini2ini(tmp_path): +def test_config_tools_realize__fmt2fmt_ini2ini(tmp_path): """ Test that providing an INI base input file and an INI config file will create and update INI config file. @@ -335,7 +335,7 @@ def test_config_tools_realize_config__fmt2fmt_ini2ini(tmp_path): help_realize_fmt2fmt("simple.ini", FORMAT.ini, "simple2.ini", FORMAT.ini, tmp_path) -def test_config_tools_realize_config__fmt2fmt_yaml2yaml(tmp_path): +def test_config_tools_realize__fmt2fmt_yaml2yaml(tmp_path): """ Test that providing a YAML base input file and a YAML config file will create and update YAML config file. @@ -345,7 +345,7 @@ def test_config_tools_realize_config__fmt2fmt_yaml2yaml(tmp_path): ) -def test_config_tools_realize_config__incompatible_file_type(): +def test_config_tools_realize__incompatible_file_type(): """ Test that providing an incompatible file type for input base file will return print statement. """ @@ -357,7 +357,7 @@ def test_config_tools_realize_config__incompatible_file_type(): ) -def test_config_tools_realize_config__output_file_format(tmp_path): +def test_config_tools_realize__output_file_format(tmp_path): """ Test that output_format overrides bad output_file extension. """ @@ -371,7 +371,7 @@ def test_config_tools_realize_config__output_file_format(tmp_path): assert compare_files(outfile, infile) -def test_config_tools_realize_config__remove_nml_to_nml(tmp_path): +def test_config_tools_realize__remove_nml_to_nml(tmp_path): input_config = NMLConfig({"constants": {"pi": 3.141, "e": 2.718}}) s = """ constants: @@ -389,7 +389,7 @@ def test_config_tools_realize_config__remove_nml_to_nml(tmp_path): assert f90nml.read(output_file) == {"constants": {"pi": 3.141}} -def test_config_tools_realize_config__remove_yaml_to_yaml_scalar(tmp_path): +def test_config_tools_realize__remove_yaml_to_yaml_scalar(tmp_path): input_config = YAMLConfig({"a": {"b": {"c": 11, "d": 22, "e": 33}}}) s = """ a: @@ -405,7 +405,7 @@ def test_config_tools_realize_config__remove_yaml_to_yaml_scalar(tmp_path): ) == {"a": {"b": {"c": 11, "e": 33}}} -def test_config_tools_realize_config__remove_yaml_to_yaml_subtree(tmp_path): +def test_config_tools_realize__remove_yaml_to_yaml_subtree(tmp_path): input_config = YAMLConfig(yaml.safe_load("a: {b: {c: 11, d: 22, e: 33}}")) s = """ a: @@ -420,7 +420,7 @@ def test_config_tools_realize_config__remove_yaml_to_yaml_subtree(tmp_path): ) == {"a": {}} -def test_config_tools_realize_config__scalar_value(capsys): +def test_config_tools_realize__scalar_value(capsys): stdinproxy.cache_clear() tools.realize( input_config=YAMLConfig(config={"foo": {"bar": "baz"}}), @@ -430,35 +430,35 @@ def test_config_tools_realize_config__scalar_value(capsys): assert capsys.readouterr().out.strip() == "baz" -def test_config_tools_realize_config__simple_ini(tmp_path): +def test_config_tools_realize__simple_ini(tmp_path): """ Test that providing an INI file with necessary settings will create an INI config file. """ help_realize_simple("simple.ini", FORMAT.ini, tmp_path) -def test_config_tools_realize_config__simple_namelist(tmp_path): +def test_config_tools_realize__simple_namelist(tmp_path): """ Test that providing a namelist file with necessary settings will create a namelist config file. """ help_realize_simple("simple.nml", FORMAT.nml, tmp_path) -def test_config_tools_realize_config__simple_sh(tmp_path): +def test_config_tools_realize__simple_sh(tmp_path): """ Test that providing an sh file with necessary settings will create an sh config file. """ help_realize_simple("simple.sh", FORMAT.sh, tmp_path) -def test_config_tools_realize_config__simple_yaml(tmp_path): +def test_config_tools_realize__simple_yaml(tmp_path): """ Test that providing a YAML base file with necessary settings will create a YAML config file. """ help_realize_simple("simple2.yaml", FORMAT.yaml, tmp_path) -def test_config_tools_realize_config__single_dereference(capsys, tmp_path): +def test_config_tools_realize__single_dereference(capsys, tmp_path): input_config = tmp_path / "a.yaml" update_config = tmp_path / "b.yaml" with writable(input_config) as f: @@ -481,7 +481,7 @@ def test_config_tools_realize_config__single_dereference(capsys, tmp_path): assert actual == dedent(expected).strip() -def test_config_tools_realize_config__total_fail(): +def test_config_tools_realize__total_fail(): with raises(UWConfigError) as e: tools.realize( input_config=YAMLConfig({"foo": "{{ bar }}"}), output_format=FORMAT.yaml, total=True @@ -489,7 +489,7 @@ def test_config_tools_realize_config__total_fail(): assert str(e.value) == "Config could not be totally realized" -def test_config_tools_realize_config__update_bad_format_defaults_to_yaml(capsys, tmp_path): +def test_config_tools_realize__update_bad_format_defaults_to_yaml(capsys, tmp_path): input_config = tmp_path / "a.yaml" update_config = tmp_path / "b.clj" with writable(input_config) as f: @@ -511,7 +511,7 @@ def test_config_tools_realize_config__update_bad_format_defaults_to_yaml(capsys, assert capsys.readouterr().out.strip() == dedent(expected).strip() -def test_config_tools_realize_config__update_none(capsys, tmp_path): +def test_config_tools_realize__update_none(capsys, tmp_path): path = tmp_path / "a.yaml" with writable(path) as f: yaml.dump({"1": "a", "2": "{{ deref }}", "3": "{{ temporalis }}", "deref": "b"}, f) @@ -530,7 +530,7 @@ def test_config_tools_realize_config__update_none(capsys, tmp_path): assert actual == dedent(expected).strip() -def test_config_tools_realize_config__values_needed_ini(logged): +def test_config_tools_realize__values_needed_ini(logged): """ Test that the values_needed flag logs keys completed and keys containing unrendered Jinja2 variables/expressions. @@ -562,7 +562,7 @@ def test_config_tools_realize_config__values_needed_ini(logged): assert logged(dedent(expected), multiline=True) -def test_config_tools_realize_config__values_needed_yaml(logged): +def test_config_tools_realize__values_needed_yaml(logged): """ Test that the values_needed flag logs keys completed and keys containing unrendered Jinja2 variables/expressions. @@ -673,14 +673,14 @@ def test_config_tools__ensure_format__explicitly_specified_with_path(): ) -def test_config_tools__realize_config_input_setup__ini_cfgobj(): +def test_config_tools__realize_input_setup__ini_cfgobj(): data = {"section": {"foo": "bar"}} cfgobj = INIConfig(config=data) input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test_config_tools__realize_config_input_setup__ini_file(tmp_path): +def test_config_tools__realize_input_setup__ini_file(tmp_path): data = """ [section] foo = bar @@ -691,7 +691,7 @@ def test_config_tools__realize_config_input_setup__ini_file(tmp_path): assert input_obj.data == {"section": {"foo": "bar"}} -def test_config_tools__realize_config_input_setup__ini_stdin(logged): +def test_config_tools__realize_input_setup__ini_stdin(logged): data = """ [section] foo = bar @@ -707,14 +707,14 @@ def test_config_tools__realize_config_input_setup__ini_stdin(logged): assert logged("Reading input from stdin") -def test_config_tools__realize_config_input_setup__nml_cfgobj(): +def test_config_tools__realize_input_setup__nml_cfgobj(): data = {"nl": {"pi": 3.14}} cfgobj = NMLConfig(config=data) input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test_config_tools__realize_config_input_setup__nml_file(tmp_path): +def test_config_tools__realize_input_setup__nml_file(tmp_path): data = """ &nl pi = 3.14 @@ -726,7 +726,7 @@ def test_config_tools__realize_config_input_setup__nml_file(tmp_path): assert input_obj["nl"]["pi"] == 3.14 -def test_config_tools__realize_config_input_setup__nml_stdin(logged): +def test_config_tools__realize_input_setup__nml_stdin(logged): data = """ &nl pi = 3.14 @@ -742,14 +742,14 @@ def test_config_tools__realize_config_input_setup__nml_stdin(logged): assert logged("Reading input from stdin") -def test_config_tools__realize_config_input_setup__sh_cfgobj(): +def test_config_tools__realize_input_setup__sh_cfgobj(): data = {"foo": "bar"} cfgobj = SHConfig(config=data) input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test_config_tools__realize_config_input_setup__sh_file(tmp_path): +def test_config_tools__realize_input_setup__sh_file(tmp_path): data = """ foo=bar """ @@ -759,7 +759,7 @@ def test_config_tools__realize_config_input_setup__sh_file(tmp_path): assert input_obj.data == {"foo": "bar"} -def test_config_tools__realize_config_input_setup__sh_stdin(logged): +def test_config_tools__realize_input_setup__sh_stdin(logged): data = """ foo=bar """ @@ -773,14 +773,14 @@ def test_config_tools__realize_config_input_setup__sh_stdin(logged): assert logged("Reading input from stdin") -def test_config_tools__realize_config_input_setup__yaml_cfgobj(): +def test_config_tools__realize_input_setup__yaml_cfgobj(): data = {"foo": "bar"} cfgobj = YAMLConfig(config=data) input_obj = tools._realize_input_setup(input_config=cfgobj) assert input_obj.data == data -def test_config_tools__realize_config_input_setup__yaml_file(tmp_path): +def test_config_tools__realize_input_setup__yaml_file(tmp_path): data = """ foo: bar """ @@ -790,7 +790,7 @@ def test_config_tools__realize_config_input_setup__yaml_file(tmp_path): assert input_obj.data == {"foo": "bar"} -def test_config_tools__realize_config_input_setup__yaml_stdin(logged): +def test_config_tools__realize_input_setup__yaml_stdin(logged): data = """ foo: bar """ @@ -804,7 +804,7 @@ def test_config_tools__realize_config_input_setup__yaml_stdin(logged): assert logged("Reading input from stdin") -def test_config_tools__realize_config_output_setup(logged, tmp_path): +def test_config_tools__realize_output_setup(logged, tmp_path): input_obj = YAMLConfig({"a": {"b": {"foo": "bar"}}}) output_file = tmp_path / "output.yaml" assert tools._realize_output_setup( From 467ef0c35333e2116a6171b0cdd0a1ed74d63ff7 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 17:12:33 +0000 Subject: [PATCH 19/46] WIP --- src/uwtools/api/config.py | 4 ++-- src/uwtools/cli.py | 2 -- src/uwtools/config/tools.py | 21 ++++++++++++--------- src/uwtools/tests/api/test_config.py | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index b99f00b59..a3025742f 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -52,8 +52,8 @@ def compose( return _compose( configs=list(map(Path, configs)), output_file=Path(output_file) if output_file else None, - input_format=input_format or _FORMAT.yaml, - output_format=output_format or _FORMAT.yaml, + input_format=input_format, + output_format=output_format, ) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 7535c3579..23a1bcfb1 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -851,7 +851,6 @@ def _add_arg_input_format(group: Group, choices: list[str], required: bool = Fal group.add_argument( _switch(STR.infmt), choices=choices, - default=FORMAT.yaml, help=f"Input format (default: {FORMAT.yaml})", required=required, type=str, @@ -910,7 +909,6 @@ def _add_arg_output_format(group: Group, choices: list[str], required: bool = Fa group.add_argument( _switch(STR.outfmt), choices=choices, - default=FORMAT.yaml, help=f"Output format (default: {FORMAT.yaml})", required=required, type=str, diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 2a6d2e57c..296cb76d4 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -39,23 +39,26 @@ def compare( def compose( - configs: list[Path], output_file: Path | None, input_format: str, output_format: str + configs: list[Path], + output_file: Path | None, + input_format: str | None, + output_format: str | None, ) -> bool: """ NB: This docstring is dynamically replaced: See compose.__doc__ definition below. """ - _validate_format("output format", output_format, input_format) + # _validate_format("output format", output_format, input_format) + basepath = configs[0] + input_format = input_format or get_config_format(basepath) input_class = format_to_config(input_format) - log.debug("Reading %s as base config to compose onto", configs[0]) - config = input_class(configs[0]) + log.debug("Reading %s as base %s config to compose onto", basepath, input_format) + config = input_class(basepath) for path in configs[1:]: - log.debug("Composing %s onto base", path) - c = input_class(path) - # config_check_depths_update(c, input_format) - config.update_from(c) + log.debug("Composing %s config from %s", input_format, path) + config.update_from(input_class(path)) + output_format = output_format or get_config_format(output_file) output_class = format_to_config(output_format) output_config = output_class(config) - # config_check_depths_dump(config, output_format) output_config.dump(output_file) return True diff --git a/src/uwtools/tests/api/test_config.py b/src/uwtools/tests/api/test_config.py index de4988cb7..bf8afcd5b 100644 --- a/src/uwtools/tests/api/test_config.py +++ b/src/uwtools/tests/api/test_config.py @@ -45,8 +45,8 @@ def test_api_config_compose(output_file, input_format, output_format): _compose.assert_called_once_with( configs=list(map(Path, pathstrs)), output_file=None if output_file is None else Path(output_file), - input_format=input_format or FORMAT.yaml, - output_format=output_format or FORMAT.yaml, + input_format=input_format, + output_format=output_format, ) From 3c6ee1e07053e56bd2cd5aacbd904a44d8bc9ee2 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 17:21:38 +0000 Subject: [PATCH 20/46] WIP --- src/uwtools/config/tools.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 296cb76d4..887b24643 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -47,16 +47,15 @@ def compose( """ NB: This docstring is dynamically replaced: See compose.__doc__ definition below. """ - # _validate_format("output format", output_format, input_format) basepath = configs[0] - input_format = input_format or get_config_format(basepath) + input_format = input_format or get_config_format(basepath, "input") input_class = format_to_config(input_format) - log.debug("Reading %s as base %s config to compose onto", basepath, input_format) + log.debug("Reading %s as base '%s' config", basepath, input_format) config = input_class(basepath) for path in configs[1:]: - log.debug("Composing %s config from %s", input_format, path) + log.debug("Composing '%s' config from %s", input_format, path) config.update_from(input_class(path)) - output_format = output_format or get_config_format(output_file) + output_format = output_format or get_config_format(output_file, "output") output_class = format_to_config(output_format) output_config = output_class(config) output_config.dump(output_file) From 0799f14f4d400266529199cef947e6d5987674b7 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 20:18:33 +0000 Subject: [PATCH 21/46] Work on tests --- src/uwtools/config/tools.py | 6 +++--- src/uwtools/tests/config/test_tools.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 887b24643..b9aa3f9e9 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -40,9 +40,9 @@ def compare( def compose( configs: list[Path], - output_file: Path | None, - input_format: str | None, - output_format: str | None, + output_file: Path | None = None, + input_format: str | None = None, + output_format: str | None = None, ) -> bool: """ NB: This docstring is dynamically replaced: See compose.__doc__ definition below. diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index f15164860..922b94a71 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -39,6 +39,14 @@ def compare_assets(tmp_path): return d, a, b +@fixture +def compose_assets(): + d1 = {"one": 1} + d2 = {"one": {"two": 2}} + d3 = {"one": {"two": {"three": 3}}} + return d1, d2, d3 + + @fixture def realize_testobj(realize_yaml_input): return YAMLConfig(config=realize_yaml_input) @@ -173,6 +181,21 @@ def test_config_tools_compare__bad_format(logged): assert logged(msg) +@mark.parametrize("suffix", ["", ".yaml", ".foo"]) +def test_config_tools_compose__single_default_stdout(compose_assets, logged, suffix, tmp_path): + path = (tmp_path / "config").with_suffix(suffix) + for d in compose_assets: + path.unlink(missing_ok=True) + assert not path.exists() + path.write_text(yaml.dump(d)) + kwargs: dict = {"configs": [path]} + if suffix and suffix != ".yaml": + kwargs["input_format"] = FORMAT.yaml + tools.compose(**kwargs) + assert logged(f"Reading {path} as base 'yaml' config") + assert YAMLConfig(path) == d + + @mark.parametrize( ("cfgtype", "fmt"), [ From be0234acb496463dce80721a8b8801dced015898 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 21:39:36 +0000 Subject: [PATCH 22/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 71 +++++++++++++++++++++----- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 922b94a71..886ebe6d9 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -41,10 +41,13 @@ def compare_assets(tmp_path): @fixture def compose_assets(): - d1 = {"one": 1} - d2 = {"one": {"two": 2}} - d3 = {"one": {"two": {"three": 3}}} - return d1, d2, d3 + d1 = {"one": 1, "foo": "bar"} + d2 = {"one": {"two": 2, "baz": "qux"}, "foo": "bar"} + d3 = {"one": {"two": {"three": 3, "asdf": "qwer"}, "baz": "qux"}, "foo": "bar"} + u1 = {"one": "won", "two": "too"} + u2 = {"one": {"two": "too"}, "won": 1} + u3 = {"one": {"two": {"three": ["drei", "tres"], "also": 3}}} + return d1, d2, d3, u1, u2, u3 @fixture @@ -181,19 +184,61 @@ def test_config_tools_compare__bad_format(logged): assert logged(msg) +@mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) -def test_config_tools_compose__single_default_stdout(compose_assets, logged, suffix, tmp_path): - path = (tmp_path / "config").with_suffix(suffix) - for d in compose_assets: - path.unlink(missing_ok=True) - assert not path.exists() - path.write_text(yaml.dump(d)) - kwargs: dict = {"configs": [path]} +def test_config_tools_compose__single_yaml(compose_assets, logged, suffix, tmp_path, tofile): + d1, d2, d3, _, _, _ = compose_assets + dpath = (tmp_path / "d").with_suffix(suffix) + for d in (d1, d2, d3): + dpath.unlink(missing_ok=True) + assert not dpath.exists() + dpath.write_text(yaml.dump(d)) + kwargs: dict = {"configs": [dpath]} if suffix and suffix != ".yaml": kwargs["input_format"] = FORMAT.yaml + if tofile: + outpath = (tmp_path / "out").with_suffix(suffix) + kwargs["output_file"] = outpath tools.compose(**kwargs) - assert logged(f"Reading {path} as base 'yaml' config") - assert YAMLConfig(path) == d + assert logged(f"Reading {dpath} as base 'yaml' config") + if tofile: + assert YAMLConfig(outpath) == d + outpath.unlink() + + +@mark.parametrize("tofile", [False, True]) +@mark.parametrize("suffix", ["", ".yaml", ".foo"]) +def test_config_tools_compose__double_yaml(compose_assets, logged, suffix, tmp_path, tofile): + d1, d2, d3, u1, u2, u3 = compose_assets + dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] + for d, u in [(d1, u1), (d2, u2), (d3, u3)]: + for path in [dpath, upath]: + path.unlink(missing_ok=True) + assert not path.exists() + dpath.write_text(yaml.dump(d)) + upath.write_text(yaml.dump(u)) + kwargs: dict = {"configs": [dpath, upath]} + if suffix and suffix != ".yaml": + kwargs["input_format"] = FORMAT.yaml + if tofile: + outpath = (tmp_path / "out").with_suffix(suffix) + kwargs["output_file"] = outpath + tools.compose(**kwargs) + assert logged(f"Reading {dpath} as base 'yaml' config") + if tofile: + expected = { + str(u1): {"one": "won", "two": "too", "foo": "bar"}, + str(u2): {"one": {"two": "too", "baz": "qux"}, "foo": "bar", "won": 1}, + str(u3): { + "one": { + "two": {"three": ["drei", "tres"], "asdf": "qwer", "also": 3}, + "baz": "qux", + }, + "foo": "bar", + }, + } + assert YAMLConfig(outpath) == expected[str(u)] + outpath.unlink() @mark.parametrize( From 787f70ee709f9fc0cdad130e776bd7f4bc856016 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 22:15:30 +0000 Subject: [PATCH 23/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 35 ++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 886ebe6d9..a72b9423c 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -40,7 +40,7 @@ def compare_assets(tmp_path): @fixture -def compose_assets(): +def compose_assets_yaml(): d1 = {"one": 1, "foo": "bar"} d2 = {"one": {"two": 2, "baz": "qux"}, "foo": "bar"} d3 = {"one": {"two": {"three": 3, "asdf": "qwer"}, "baz": "qux"}, "foo": "bar"} @@ -186,8 +186,8 @@ def test_config_tools_compare__bad_format(logged): @mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) -def test_config_tools_compose__single_yaml(compose_assets, logged, suffix, tmp_path, tofile): - d1, d2, d3, _, _, _ = compose_assets +def test_config_tools_compose__yaml_single(compose_assets_yaml, logged, suffix, tmp_path, tofile): + d1, d2, d3, _, _, _ = compose_assets_yaml dpath = (tmp_path / "d").with_suffix(suffix) for d in (d1, d2, d3): dpath.unlink(missing_ok=True) @@ -199,7 +199,7 @@ def test_config_tools_compose__single_yaml(compose_assets, logged, suffix, tmp_p if tofile: outpath = (tmp_path / "out").with_suffix(suffix) kwargs["output_file"] = outpath - tools.compose(**kwargs) + assert tools.compose(**kwargs) is True assert logged(f"Reading {dpath} as base 'yaml' config") if tofile: assert YAMLConfig(outpath) == d @@ -208,8 +208,8 @@ def test_config_tools_compose__single_yaml(compose_assets, logged, suffix, tmp_p @mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) -def test_config_tools_compose__double_yaml(compose_assets, logged, suffix, tmp_path, tofile): - d1, d2, d3, u1, u2, u3 = compose_assets +def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, tmp_path, tofile): + d1, d2, d3, u1, u2, u3 = compose_assets_yaml dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] for d, u in [(d1, u1), (d2, u2), (d3, u3)]: for path in [dpath, upath]: @@ -223,8 +223,9 @@ def test_config_tools_compose__double_yaml(compose_assets, logged, suffix, tmp_p if tofile: outpath = (tmp_path / "out").with_suffix(suffix) kwargs["output_file"] = outpath - tools.compose(**kwargs) + assert tools.compose(**kwargs) is True assert logged(f"Reading {dpath} as base 'yaml' config") + assert logged(f"Composing 'yaml' config from {upath}") if tofile: expected = { str(u1): {"one": "won", "two": "too", "foo": "bar"}, @@ -241,6 +242,26 @@ def test_config_tools_compose__double_yaml(compose_assets, logged, suffix, tmp_p outpath.unlink() +def test_config_tools_compose__nml_double(logged, tmp_path): + d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} + u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} + dpath, upath = [tmp_path / x for x in ("d.nml", "u.nml")] + NMLConfig(d).dump(dpath) + NMLConfig(u).dump(upath) + outpath = tmp_path / "out.nml" + kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} + assert tools.compose(**kwargs) is True + assert logged(f"Reading {dpath} as base 'nml' config") + assert logged(f"Composing 'nml' config from {upath}") + assert NMLConfig(outpath) == NMLConfig( + { + "constants": {"pi": 3.142, "e": 2.718}, + "trees": {"leaf": "elm", "needle": "fir"}, + "colors": {"red": "crimson", "green": "clover"}, + } + ) + + @mark.parametrize( ("cfgtype", "fmt"), [ From 33a830d3ded4a29dca76861b4878892b6f7116de Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 22:20:57 +0000 Subject: [PATCH 24/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index a72b9423c..3b0243857 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -50,6 +50,13 @@ def compose_assets_yaml(): return d1, d2, d3, u1, u2, u3 +@fixture +def compose_assets_depth_2(): + d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} + u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} + return d, u + + @fixture def realize_testobj(realize_yaml_input): return YAMLConfig(config=realize_yaml_input) @@ -242,9 +249,27 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, outpath.unlink() -def test_config_tools_compose__nml_double(logged, tmp_path): - d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} - u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} +# def test_config_tools_compose__ini_double(compose_assets_ini, logged, tmp_path): +# d, u = compose_assets_ini +# dpath, upath = [tmp_path / x for x in ("d.ini", "u.ini")] +# INIConfig(d).dump(dpath) +# INIConfig(u).dump(upath) +# outpath = tmp_path / "out.ini" +# kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} +# assert tools.compose(**kwargs) is True +# assert logged(f"Reading {dpath} as base 'ini' config") +# assert logged(f"Composing 'ini' config from {upath}") +# assert INIConfig(outpath) == INIConfig( +# { +# "constants": {"pi": 3.142, "e": 2.718}, +# "trees": {"leaf": "elm", "needle": "fir"}, +# "colors": {"red": "crimson", "green": "clover"}, +# } +# ) + + +def test_config_tools_compose__nml_double(compose_assets_depth_2, logged, tmp_path): + d, u = compose_assets_depth_2 dpath, upath = [tmp_path / x for x in ("d.nml", "u.nml")] NMLConfig(d).dump(dpath) NMLConfig(u).dump(upath) From ca5108083390eaa08fcc46a195048c1f9a8002fc Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 22:43:13 +0000 Subject: [PATCH 25/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 52 ++++++++++---------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 3b0243857..4599efa28 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -249,42 +249,28 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, outpath.unlink() -# def test_config_tools_compose__ini_double(compose_assets_ini, logged, tmp_path): -# d, u = compose_assets_ini -# dpath, upath = [tmp_path / x for x in ("d.ini", "u.ini")] -# INIConfig(d).dump(dpath) -# INIConfig(u).dump(upath) -# outpath = tmp_path / "out.ini" -# kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} -# assert tools.compose(**kwargs) is True -# assert logged(f"Reading {dpath} as base 'ini' config") -# assert logged(f"Composing 'ini' config from {upath}") -# assert INIConfig(outpath) == INIConfig( -# { -# "constants": {"pi": 3.142, "e": 2.718}, -# "trees": {"leaf": "elm", "needle": "fir"}, -# "colors": {"red": "crimson", "green": "clover"}, -# } -# ) - - -def test_config_tools_compose__nml_double(compose_assets_depth_2, logged, tmp_path): +@mark.parametrize(("configclass", "suffix"), [(INIConfig, ".ini"), (NMLConfig, ".nml")]) +def test_config_tools_compose__ini_nml_double( + compose_assets_depth_2, configclass, logged, suffix, tmp_path +): d, u = compose_assets_depth_2 - dpath, upath = [tmp_path / x for x in ("d.nml", "u.nml")] - NMLConfig(d).dump(dpath) - NMLConfig(u).dump(upath) - outpath = tmp_path / "out.nml" + dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] + configclass(d).dump(dpath) + configclass(u).dump(upath) + outpath = (tmp_path / "out").with_suffix(suffix) kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} assert tools.compose(**kwargs) is True - assert logged(f"Reading {dpath} as base 'nml' config") - assert logged(f"Composing 'nml' config from {upath}") - assert NMLConfig(outpath) == NMLConfig( - { - "constants": {"pi": 3.142, "e": 2.718}, - "trees": {"leaf": "elm", "needle": "fir"}, - "colors": {"red": "crimson", "green": "clover"}, - } - ) + fmt = suffix.lstrip(".") + assert logged(f"Reading {dpath} as base '{fmt}' config") + assert logged(f"Composing '{fmt}' config from {upath}") + expected = { + "constants": {"pi": 3.142, "e": 2.718}, + "trees": {"leaf": "elm", "needle": "fir"}, + "colors": {"red": "crimson", "green": "clover"}, + } + if fmt == "ini": + expected["constants"] = {"pi": "3.142", "e": "2.718"} + assert configclass(outpath) == configclass(expected) @mark.parametrize( From f0a657c993f3ead670be692daac9c9db9530817c Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 22:45:14 +0000 Subject: [PATCH 26/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 4599efa28..261a9dd2e 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -249,18 +249,18 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, outpath.unlink() -@mark.parametrize(("configclass", "suffix"), [(INIConfig, ".ini"), (NMLConfig, ".nml")]) +@mark.parametrize(("configclass", "fmt"), [(INIConfig, FORMAT.ini), (NMLConfig, FORMAT.nml)]) def test_config_tools_compose__ini_nml_double( - compose_assets_depth_2, configclass, logged, suffix, tmp_path + compose_assets_depth_2, configclass, fmt, logged, tmp_path ): d, u = compose_assets_depth_2 + suffix = f".{fmt}" dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] configclass(d).dump(dpath) configclass(u).dump(upath) outpath = (tmp_path / "out").with_suffix(suffix) kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} assert tools.compose(**kwargs) is True - fmt = suffix.lstrip(".") assert logged(f"Reading {dpath} as base '{fmt}' config") assert logged(f"Composing '{fmt}' config from {upath}") expected = { @@ -268,7 +268,7 @@ def test_config_tools_compose__ini_nml_double( "trees": {"leaf": "elm", "needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}, } - if fmt == "ini": + if fmt == FORMAT.ini: expected["constants"] = {"pi": "3.142", "e": "2.718"} assert configclass(outpath) == configclass(expected) From fae9c710e0a25768721dc52fcf6cd5fc457d94cb Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 22:46:14 +0000 Subject: [PATCH 27/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 261a9dd2e..f3e90282a 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -50,13 +50,6 @@ def compose_assets_yaml(): return d1, d2, d3, u1, u2, u3 -@fixture -def compose_assets_depth_2(): - d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} - u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} - return d, u - - @fixture def realize_testobj(realize_yaml_input): return YAMLConfig(config=realize_yaml_input) @@ -250,10 +243,9 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, @mark.parametrize(("configclass", "fmt"), [(INIConfig, FORMAT.ini), (NMLConfig, FORMAT.nml)]) -def test_config_tools_compose__ini_nml_double( - compose_assets_depth_2, configclass, fmt, logged, tmp_path -): - d, u = compose_assets_depth_2 +def test_config_tools_compose__ini_nml_double(configclass, fmt, logged, tmp_path): + d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} + u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} suffix = f".{fmt}" dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] configclass(d).dump(dpath) From ec246f0e8f03f3b1b803dd4127543573bde37a1e Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 22:55:54 +0000 Subject: [PATCH 28/46] Work on tests --- src/uwtools/tests/config/test_tools.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index f3e90282a..9e1972d1a 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -265,6 +265,20 @@ def test_config_tools_compose__ini_nml_double(configclass, fmt, logged, tmp_path assert configclass(outpath) == configclass(expected) +def test_config_tools_compose__sh_double(logged, tmp_path): + d = {"foo": 1, "bar": 2} + u = {"foo": 3, "baz": 4} + dpath, upath = [tmp_path / x for x in ("d.sh", "u.sh")] + SHConfig(d).dump(dpath) + SHConfig(u).dump(upath) + outpath = tmp_path / "out.sh" + kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} + assert tools.compose(**kwargs) is True + assert logged(f"Reading {dpath} as base 'sh' config") + assert logged(f"Composing 'sh' config from {upath}") + assert SHConfig(outpath) == SHConfig({"foo": "3", "bar": "2", "baz": "4"}) + + @mark.parametrize( ("cfgtype", "fmt"), [ From c26f53b80823d5789ac39cebf622c561a9f173d0 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 23:05:22 +0000 Subject: [PATCH 29/46] Work on docs --- docs/index.rst | 7 ++++++ docs/sections/user_guide/cli/tools/config.rst | 16 ++++++++++++ .../cli/tools/config/compose-help.cmd | 1 + .../cli/tools/config/compose-help.out | 25 +++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 docs/sections/user_guide/cli/tools/config/compose-help.cmd create mode 100644 docs/sections/user_guide/cli/tools/config/compose-help.out diff --git a/docs/index.rst b/docs/index.rst index 4c23ab5e9..a5d79bbe4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,6 +37,13 @@ When the Linux diff tool just doesn't work for comparing unordered namelists wit | :any:`CLI documentation with examples` +Compose Action +"""""""""""""" + +To compose configs is to start with a base config and to update its values, structurally, with the context of one or more other configs of the same type (YAML, Fortran namelist, etc.). Composition supportings building up complex experiment configurations by, for example, combining default values appropriate to the overall application with values needed on a particular machine, and then with specific user requrements, each stored in its own config file. Such a hierarchical approach to configuration management provides flexibility and avoids repetition of common values across files. + +| :any:`CLI documentation with examples` + Realize Action """""""""""""" diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index a1ad8256f..55ed65a14 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -87,6 +87,22 @@ The examples that follow use identical namelist files ``a.nml`` and ``b.nml`` wi .. literalinclude:: config/compare-format-mismatch.out :language: text +.. _cli_config_compose_examples: + +``compose`` +----------- + +The ``compose`` action builds up a final config by repeatedly updating a base config with the contents of other cofigs of the same format. + +.. literalinclude:: config/compose-help.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: config/compose-help.out + :language: text + +Examples +^^^^^^^^ + .. _cli_config_realize_examples: ``realize`` diff --git a/docs/sections/user_guide/cli/tools/config/compose-help.cmd b/docs/sections/user_guide/cli/tools/config/compose-help.cmd new file mode 100644 index 000000000..742b3cff0 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-help.cmd @@ -0,0 +1 @@ +uw config compose --help diff --git a/docs/sections/user_guide/cli/tools/config/compose-help.out b/docs/sections/user_guide/cli/tools/config/compose-help.out new file mode 100644 index 000000000..52c3eec3a --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-help.out @@ -0,0 +1,25 @@ +usage: uw config compose [-h] [--version] [--input-format {ini,nml,sh,yaml}] + [--output-format {ini,nml,sh,yaml}] + [--output-file PATH] [--quiet] [--verbose] + CONFIG [CONFIG ...] + +Compose configs + +positional arguments: + CONFIG + +Optional arguments: + -h, --help + Show help and exit + --version + Show version info and exit + --input-format {ini,nml,sh,yaml} + Input format (default: yaml) + --output-format {ini,nml,sh,yaml} + Output format (default: yaml) + --output-file PATH, -o PATH + Path to output file (default: write to stdout) + --quiet, -q + Print no logging messages + --verbose, -v + Print all logging messages From b0bf44b4d75aeb2a709f871039dbcabb56174a3b Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 23:19:30 +0000 Subject: [PATCH 30/46] Fix docstrings --- src/uwtools/api/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index a3025742f..031ef3f4a 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -227,7 +227,7 @@ def validate( :param format1: Format of 1st config file (optional if file's extension is recognized). :param format2: Format of 2nd config file (optional if file's extension is recognized). :return: ``False`` if config files had differences, otherwise ``True``. -""".format(extensions=", ".join(["``{x}``" for x in _FORMAT.extensions()])).strip() +""".format(extensions=", ".join([f"``{x}``" for x in _FORMAT.extensions()])).strip() compose.__doc__ = """ Compose config files. @@ -242,7 +242,7 @@ def validate( :return: ``True`` if no errors were encountered. """.format( default=_FORMAT.yaml, - extensions=", ".join(["``{x}``" for x in _FORMAT.extensions()]), + extensions=", ".join([f"``{x}``" for x in _FORMAT.extensions()]), choices=", ".join([f"``{x}``" for x in (_FORMAT.ini, _FORMAT.nml, _FORMAT.sh, _FORMAT.yaml)]), ).strip() @@ -284,7 +284,7 @@ def validate( :param stdin_ok: OK to read from ``stdin``? :return: The ``dict`` representation of the realized config. :raises: ``UWConfigRealizeError`` if ``total`` is ``True`` and any Jinja2 syntax was not rendered. -""".format(extensions=", ".join(["``{x}``" for x in _FORMAT.extensions()])).strip() # noqa: E501 +""".format(extensions=", ".join([f"``{x}``" for x in _FORMAT.extensions()])).strip() # noqa: E501 __all__ = [ "Config", From b9e4b99a4c77017d4080592ff4464e12410bcd2e Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 23:56:36 +0000 Subject: [PATCH 31/46] Work on docs --- docs/sections/user_guide/cli/tools/config.rst | 26 +++++++++++++++++++ .../user_guide/cli/tools/config/compose-1.cmd | 2 ++ .../user_guide/cli/tools/config/compose-1.out | 5 ++++ .../cli/tools/config/compose-base.yaml | 3 +++ .../cli/tools/config/compose-update-1.yaml | 2 ++ .../cli/tools/config/compose-update-2.yaml | 1 + docs/static/custom.css | 10 +++++++ 7 files changed, 49 insertions(+) create mode 100644 docs/sections/user_guide/cli/tools/config/compose-1.cmd create mode 100644 docs/sections/user_guide/cli/tools/config/compose-1.out create mode 100644 docs/sections/user_guide/cli/tools/config/compose-base.yaml create mode 100644 docs/sections/user_guide/cli/tools/config/compose-update-1.yaml create mode 100644 docs/sections/user_guide/cli/tools/config/compose-update-2.yaml diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index 55ed65a14..5189b3176 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -103,6 +103,32 @@ The ``compose`` action builds up a final config by repeatedly updating a base co Examples ^^^^^^^^ +* Consider three YAML configs: + + .. literalinclude:: config/compose-base.yaml + :caption: ``compose-base.yaml`` + :language: yaml + .. literalinclude:: config/compose-update-1.yaml + :caption: ``compose-update-1.yaml`` + :language: yaml + .. literalinclude:: config/compose-update-2.yaml + :caption: ``compose-update-2.yaml`` + :language: yaml + +Compose the three together, writing to ``stdout``: + +.. literalinclude:: config/compose-1.cmd + :language: text + :emphasize-lines: 1 +.. literalinclude:: config/compose-1.out + :language: yaml + +Values provided by update configs override or augment values provided in the base config, while unaffected values survive to the final config. Priority of values increases from left to right. + +The ``--output-file`` / ``-o`` option can be used to direct the output to a file. + +The ``--input-config`` and ``--output-config`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. The the formats are neither explicitly provided or deduced, ``yaml`` is implied. + .. _cli_config_realize_examples: ``realize`` diff --git a/docs/sections/user_guide/cli/tools/config/compose-1.cmd b/docs/sections/user_guide/cli/tools/config/compose-1.cmd new file mode 100644 index 000000000..aa04384b2 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-1.cmd @@ -0,0 +1,2 @@ +uw config compose compose-base.yaml compose-update-1.yaml compose-update-2.yaml + diff --git a/docs/sections/user_guide/cli/tools/config/compose-1.out b/docs/sections/user_guide/cli/tools/config/compose-1.out new file mode 100644 index 000000000..acb77993a --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-1.out @@ -0,0 +1,5 @@ +constants: + pi: 3.142 + e: 2.718 +color: blue +flower: rose diff --git a/docs/sections/user_guide/cli/tools/config/compose-base.yaml b/docs/sections/user_guide/cli/tools/config/compose-base.yaml new file mode 100644 index 000000000..a0462fd24 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-base.yaml @@ -0,0 +1,3 @@ +constants: + pi: 3.142 +color: blue diff --git a/docs/sections/user_guide/cli/tools/config/compose-update-1.yaml b/docs/sections/user_guide/cli/tools/config/compose-update-1.yaml new file mode 100644 index 000000000..47f1ad217 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-update-1.yaml @@ -0,0 +1,2 @@ +constants: + e: 2.718 diff --git a/docs/sections/user_guide/cli/tools/config/compose-update-2.yaml b/docs/sections/user_guide/cli/tools/config/compose-update-2.yaml new file mode 100644 index 000000000..c2b9d2bb0 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-update-2.yaml @@ -0,0 +1 @@ +flower: rose diff --git a/docs/static/custom.css b/docs/static/custom.css index cb22e007e..ff5a2d585 100644 --- a/docs/static/custom.css +++ b/docs/static/custom.css @@ -21,3 +21,13 @@ html.writer-html5 .rst-content table.docutils td>p { .wy-table-responsive table td, .wy-table-responsive table th { white-space: inherit; } + +/* Left align captions. */ + +.rst-content .caption-number { + display: none; +} + +.rst-content .code-block-caption { + text-align: left; +} From fd8b84c48584c89e32994103b8ad3e4b8559d403 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 16 Sep 2025 23:59:30 +0000 Subject: [PATCH 32/46] Work on docs --- docs/sections/user_guide/cli/tools/config.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index 5189b3176..813748440 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -106,13 +106,13 @@ Examples * Consider three YAML configs: .. literalinclude:: config/compose-base.yaml - :caption: ``compose-base.yaml`` + :caption: compose-base.yaml :language: yaml .. literalinclude:: config/compose-update-1.yaml - :caption: ``compose-update-1.yaml`` + :caption: compose-update-1.yaml :language: yaml .. literalinclude:: config/compose-update-2.yaml - :caption: ``compose-update-2.yaml`` + :caption: compose-update-2.yaml :language: yaml Compose the three together, writing to ``stdout``: @@ -129,6 +129,8 @@ The ``--output-file`` / ``-o`` option can be used to direct the output to a file The ``--input-config`` and ``--output-config`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. The the formats are neither explicitly provided or deduced, ``yaml`` is implied. +Configs of ``ini``, ``nml``, and ``sh`` format can be similarly composed. + .. _cli_config_realize_examples: ``realize`` From 2db169519e10e7253e96d934640f84ef13a5dfeb Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 01:08:53 +0000 Subject: [PATCH 33/46] Work on docs --- docs/sections/user_guide/cli/tools/config.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index 813748440..1936cd1e4 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -125,11 +125,11 @@ Compose the three together, writing to ``stdout``: Values provided by update configs override or augment values provided in the base config, while unaffected values survive to the final config. Priority of values increases from left to right. -The ``--output-file`` / ``-o`` option can be used to direct the output to a file. +Additionally: -The ``--input-config`` and ``--output-config`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. The the formats are neither explicitly provided or deduced, ``yaml`` is implied. - -Configs of ``ini``, ``nml``, and ``sh`` format can be similarly composed. + * Configs in the ``ini``, ``nml``, and ``sh`` formats can be similarly composed. + * The ``--input-config`` and ``--output-config`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. The the formats are neither explicitly provided or deduced, ``yaml`` is assumed. + * The ``--output-file`` / ``-o`` option can be added to direct the output to a file. .. _cli_config_realize_examples: From 9f93fbce36317cc0d46f22774fc1daeb84679fd1 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 01:15:50 +0000 Subject: [PATCH 34/46] Update docs --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index a5d79bbe4..9d7306b7e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,7 +40,7 @@ When the Linux diff tool just doesn't work for comparing unordered namelists wit Compose Action """""""""""""" -To compose configs is to start with a base config and to update its values, structurally, with the context of one or more other configs of the same type (YAML, Fortran namelist, etc.). Composition supportings building up complex experiment configurations by, for example, combining default values appropriate to the overall application with values needed on a particular machine, and then with specific user requrements, each stored in its own config file. Such a hierarchical approach to configuration management provides flexibility and avoids repetition of common values across files. +To compose configs is to start with a base config and to update its values, structurally, from the contents of one or more other configs of the same type (YAML, Fortran namelist, etc.). Composition supports building up complex experiment configurations by, for example, combining default values appropriate to the overall application with values needed on a particular machine, and then with specific user requrements, each stored in their own config files. Such a hierarchical approach to configuration management provides flexibility and avoids repetition of common values across files. | :any:`CLI documentation with examples` From 1eca86bb6feb4a9acc5885a94d3e492fef5431e3 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 01:16:44 +0000 Subject: [PATCH 35/46] Fix typo --- docs/sections/user_guide/cli/tools/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index 1936cd1e4..a5bcad671 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -92,7 +92,7 @@ The examples that follow use identical namelist files ``a.nml`` and ``b.nml`` wi ``compose`` ----------- -The ``compose`` action builds up a final config by repeatedly updating a base config with the contents of other cofigs of the same format. +The ``compose`` action builds up a final config by repeatedly updating a base config with the contents of other configs of the same format. .. literalinclude:: config/compose-help.cmd :language: text From 91dd2357b9d48856e1ba73e43798e3947d72bd06 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 01:20:19 +0000 Subject: [PATCH 36/46] Update docs --- docs/sections/user_guide/cli/tools/config.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index a5bcad671..32e37c8b2 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -127,9 +127,9 @@ Values provided by update configs override or augment values provided in the bas Additionally: - * Configs in the ``ini``, ``nml``, and ``sh`` formats can be similarly composed. - * The ``--input-config`` and ``--output-config`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. The the formats are neither explicitly provided or deduced, ``yaml`` is assumed. - * The ``--output-file`` / ``-o`` option can be added to direct the output to a file. + * Sets of configs in the ``ini``, ``nml``, and ``sh`` formats can be similarly composed. + * The ``--input-format`` and ``--output-format`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. When the formats are neither explicitly specified or deduced, ``yaml`` is assumed. + * The ``--output-file`` / ``-o`` option can be added to write the final config to a file instead of to ``stdout``. .. _cli_config_realize_examples: From 68caa5f4f07e55b9df047be236308f8ec54252f7 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 03:25:09 +0000 Subject: [PATCH 37/46] Use more regular keys/vals in unit test --- src/uwtools/tests/config/test_tools.py | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 9e1972d1a..49f8b6f58 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -41,13 +41,13 @@ def compare_assets(tmp_path): @fixture def compose_assets_yaml(): - d1 = {"one": 1, "foo": "bar"} - d2 = {"one": {"two": 2, "baz": "qux"}, "foo": "bar"} - d3 = {"one": {"two": {"three": 3, "asdf": "qwer"}, "baz": "qux"}, "foo": "bar"} - u1 = {"one": "won", "two": "too"} - u2 = {"one": {"two": "too"}, "won": 1} - u3 = {"one": {"two": {"three": ["drei", "tres"], "also": 3}}} - return d1, d2, d3, u1, u2, u3 + d1 = {"k10": "v10", "k11": "v11"} + u1 = {"k10": "u10", "k12": "v12"} + d2 = {"k10": {"k21": "v21", "k22": "v22"}, "k11": "v11"} + u2 = {"k10": {"k21": "u21"}, "k12": "v12"} + d3 = {"k10": {"k21": {"k31": "v31", "k32": "v32"}, "k22": "v22"}, "k11": "v11"} + u3 = {"k10": {"k21": {"k31": ["v310", "v311"], "k33": "v33"}}} + return d1, u1, d2, u2, d3, u3 @fixture @@ -187,7 +187,7 @@ def test_config_tools_compare__bad_format(logged): @mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) def test_config_tools_compose__yaml_single(compose_assets_yaml, logged, suffix, tmp_path, tofile): - d1, d2, d3, _, _, _ = compose_assets_yaml + d1, _, d2, _, d3, _ = compose_assets_yaml dpath = (tmp_path / "d").with_suffix(suffix) for d in (d1, d2, d3): dpath.unlink(missing_ok=True) @@ -209,7 +209,7 @@ def test_config_tools_compose__yaml_single(compose_assets_yaml, logged, suffix, @mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, tmp_path, tofile): - d1, d2, d3, u1, u2, u3 = compose_assets_yaml + d1, u1, d2, u2, d3, u3 = compose_assets_yaml dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] for d, u in [(d1, u1), (d2, u2), (d3, u3)]: for path in [dpath, upath]: @@ -228,14 +228,14 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, assert logged(f"Composing 'yaml' config from {upath}") if tofile: expected = { - str(u1): {"one": "won", "two": "too", "foo": "bar"}, - str(u2): {"one": {"two": "too", "baz": "qux"}, "foo": "bar", "won": 1}, + str(u1): {"k10": "u10", "k11": "v11", "k12": "v12"}, + str(u2): {"k10": {"k21": "u21", "k22": "v22"}, "k11": "v11", "k12": "v12"}, str(u3): { - "one": { - "two": {"three": ["drei", "tres"], "asdf": "qwer", "also": 3}, - "baz": "qux", + "k10": { + "k21": {"k31": ["v310", "v311"], "k32": "v32", "k33": "v33"}, + "k22": "v22", }, - "foo": "bar", + "k11": "v11", }, } assert YAMLConfig(outpath) == expected[str(u)] From 7ceefc9ebac88ff9d56c2a851deef2965bb162b2 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 04:20:59 +0000 Subject: [PATCH 38/46] Implement --realize --- docs/sections/user_guide/cli/tools/config.rst | 47 ++++++--- .../{compose-1.cmd => compose-basic.cmd} | 0 .../{compose-1.out => compose-basic.out} | 0 .../cli/tools/config/compose-help.out | 13 ++- .../cli/tools/config/compose-realize-no.cmd | 1 + .../cli/tools/config/compose-realize-no.out | 3 + .../cli/tools/config/compose-realize-yes.cmd | 1 + .../cli/tools/config/compose-realize-yes.out | 3 + .../cli/tools/config/compose-template.yaml | 1 + .../cli/tools/config/compose-values.yaml | 2 + src/uwtools/api/config.py | 3 + src/uwtools/cli.py | 12 ++- src/uwtools/config/tools.py | 3 + src/uwtools/tests/api/test_config.py | 2 + src/uwtools/tests/config/test_tools.py | 97 +++++++++++-------- src/uwtools/tests/test_cli.py | 7 +- 16 files changed, 138 insertions(+), 57 deletions(-) rename docs/sections/user_guide/cli/tools/config/{compose-1.cmd => compose-basic.cmd} (100%) rename docs/sections/user_guide/cli/tools/config/{compose-1.out => compose-basic.out} (100%) create mode 100644 docs/sections/user_guide/cli/tools/config/compose-realize-no.cmd create mode 100644 docs/sections/user_guide/cli/tools/config/compose-realize-no.out create mode 100644 docs/sections/user_guide/cli/tools/config/compose-realize-yes.cmd create mode 100644 docs/sections/user_guide/cli/tools/config/compose-realize-yes.out create mode 100644 docs/sections/user_guide/cli/tools/config/compose-template.yaml create mode 100644 docs/sections/user_guide/cli/tools/config/compose-values.yaml diff --git a/docs/sections/user_guide/cli/tools/config.rst b/docs/sections/user_guide/cli/tools/config.rst index 32e37c8b2..c936adfa0 100644 --- a/docs/sections/user_guide/cli/tools/config.rst +++ b/docs/sections/user_guide/cli/tools/config.rst @@ -115,21 +115,46 @@ Examples :caption: compose-update-2.yaml :language: yaml -Compose the three together, writing to ``stdout``: + Compose the three together, writing to ``stdout``: -.. literalinclude:: config/compose-1.cmd - :language: text - :emphasize-lines: 1 -.. literalinclude:: config/compose-1.out - :language: yaml + .. literalinclude:: config/compose-basic.cmd + :language: text + :emphasize-lines: 1 + .. literalinclude:: config/compose-basic.out + :language: yaml + + Values provided by update configs override or augment values provided in the base config, while unaffected values survive to the final config. Priority of values increases from left to right. + + Additionally: + + * Sets of configs in the ``ini``, ``nml``, and ``sh`` formats can be similarly composed. + * The ``--input-format`` and ``--output-format`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. When the formats are neither explicitly specified or deduced, ``yaml`` is assumed. + * The ``--output-file`` / ``-o`` option can be added to write the final config to a file instead of to ``stdout``. + +* The optional ``--realize`` switch can be used to render as many Jinja2 template expressions as possible in the final config, using the config itself as a source of values. For example: -Values provided by update configs override or augment values provided in the base config, while unaffected values survive to the final config. Priority of values increases from left to right. + .. literalinclude:: config/compose-template.yaml + :caption: compose-template.yaml + :language: yaml + .. literalinclude:: config/compose-values.yaml + :caption: compose-values.yaml + :language: yaml + + Without the ``--realize`` switch: + + .. literalinclude:: config/compose-realize-no.cmd + :language: text + :emphasize-lines: 1 + .. literalinclude:: config/compose-realize-no.out + :language: yaml -Additionally: + And with ``--realize``: - * Sets of configs in the ``ini``, ``nml``, and ``sh`` formats can be similarly composed. - * The ``--input-format`` and ``--output-format`` options can be used to specify the format of the input and output configs, respectively, for cases when ``uwtools`` cannot deduce the format of configs from their filename extensions. When the formats are neither explicitly specified or deduced, ``yaml`` is assumed. - * The ``--output-file`` / ``-o`` option can be added to write the final config to a file instead of to ``stdout``. + .. literalinclude:: config/compose-realize-yes.cmd + :language: text + :emphasize-lines: 1 + .. literalinclude:: config/compose-realize-yes.out + :language: yaml .. _cli_config_realize_examples: diff --git a/docs/sections/user_guide/cli/tools/config/compose-1.cmd b/docs/sections/user_guide/cli/tools/config/compose-basic.cmd similarity index 100% rename from docs/sections/user_guide/cli/tools/config/compose-1.cmd rename to docs/sections/user_guide/cli/tools/config/compose-basic.cmd diff --git a/docs/sections/user_guide/cli/tools/config/compose-1.out b/docs/sections/user_guide/cli/tools/config/compose-basic.out similarity index 100% rename from docs/sections/user_guide/cli/tools/config/compose-1.out rename to docs/sections/user_guide/cli/tools/config/compose-basic.out diff --git a/docs/sections/user_guide/cli/tools/config/compose-help.out b/docs/sections/user_guide/cli/tools/config/compose-help.out index 52c3eec3a..cf6e7308f 100644 --- a/docs/sections/user_guide/cli/tools/config/compose-help.out +++ b/docs/sections/user_guide/cli/tools/config/compose-help.out @@ -1,6 +1,7 @@ -usage: uw config compose [-h] [--version] [--input-format {ini,nml,sh,yaml}] - [--output-format {ini,nml,sh,yaml}] - [--output-file PATH] [--quiet] [--verbose] +usage: uw config compose [-h] [--version] [--realize] [--output-file PATH] + [--input-format {ini,nml,sh,yaml}] + [--output-format {ini,nml,sh,yaml}] [--quiet] + [--verbose] CONFIG [CONFIG ...] Compose configs @@ -13,12 +14,14 @@ Optional arguments: Show help and exit --version Show version info and exit + --realize + Try to render template expressions + --output-file PATH, -o PATH + Path to output file (default: write to stdout) --input-format {ini,nml,sh,yaml} Input format (default: yaml) --output-format {ini,nml,sh,yaml} Output format (default: yaml) - --output-file PATH, -o PATH - Path to output file (default: write to stdout) --quiet, -q Print no logging messages --verbose, -v diff --git a/docs/sections/user_guide/cli/tools/config/compose-realize-no.cmd b/docs/sections/user_guide/cli/tools/config/compose-realize-no.cmd new file mode 100644 index 000000000..36d282c35 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-realize-no.cmd @@ -0,0 +1 @@ +uw config compose compose-template.yaml compose-values.yaml diff --git a/docs/sections/user_guide/cli/tools/config/compose-realize-no.out b/docs/sections/user_guide/cli/tools/config/compose-realize-no.out new file mode 100644 index 000000000..58733b6c2 --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-realize-no.out @@ -0,0 +1,3 @@ +radius: !float '{{ 2.0 * pi * r }}' +pi: 3.142 +r: 1.0 diff --git a/docs/sections/user_guide/cli/tools/config/compose-realize-yes.cmd b/docs/sections/user_guide/cli/tools/config/compose-realize-yes.cmd new file mode 100644 index 000000000..58b22369a --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-realize-yes.cmd @@ -0,0 +1 @@ +uw config compose compose-template.yaml compose-values.yaml --realize diff --git a/docs/sections/user_guide/cli/tools/config/compose-realize-yes.out b/docs/sections/user_guide/cli/tools/config/compose-realize-yes.out new file mode 100644 index 000000000..4024e4f8f --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-realize-yes.out @@ -0,0 +1,3 @@ +radius: 6.284 +pi: 3.142 +r: 1.0 diff --git a/docs/sections/user_guide/cli/tools/config/compose-template.yaml b/docs/sections/user_guide/cli/tools/config/compose-template.yaml new file mode 100644 index 000000000..1cafba0ea --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-template.yaml @@ -0,0 +1 @@ +radius: !float '{{ 2.0 * pi * r }}' diff --git a/docs/sections/user_guide/cli/tools/config/compose-values.yaml b/docs/sections/user_guide/cli/tools/config/compose-values.yaml new file mode 100644 index 000000000..71871573e --- /dev/null +++ b/docs/sections/user_guide/cli/tools/config/compose-values.yaml @@ -0,0 +1,2 @@ +pi: 3.142 +r: 1.0 diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 031ef3f4a..427867149 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -42,6 +42,7 @@ def compare( def compose( configs: list[str | Path], + realize: bool = False, output_file: Path | str | None = None, input_format: str | None = None, output_format: str | None = None, @@ -51,6 +52,7 @@ def compose( """ return _compose( configs=list(map(Path, configs)), + realize=realize, output_file=Path(output_file) if output_file else None, input_format=input_format, output_format=output_format, @@ -236,6 +238,7 @@ def validate( Recognized file extensions are: {extensions}. :param configs: Paths to configs to compose. +:param realize: Try to render template expressions. :param output_file: Output config destination (default: write to ``stdout``). :param input_format: Format of configs to compose (choices: {choices}, default: ``{default}``) :param output_format: Format of output config (choices: {choices}, default: ``{default}``) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 23a1bcfb1..6558042b0 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -162,9 +162,10 @@ def _add_subparser_config_compose(subparsers: Subparsers) -> ActionChecks: """ parser = _add_subparser(subparsers, STR.compose, "Compose configs") optional = _basic_setup(parser) + _add_arg_realize(optional) + _add_arg_output_file(optional) _add_arg_input_format(optional, choices=FORMATS) _add_arg_output_format(optional, choices=FORMATS) - _add_arg_output_file(optional) checks = _add_args_verbosity(optional) parser.add_argument("configs", metavar="CONFIG", nargs="+", type=Path) return checks @@ -248,6 +249,7 @@ def _dispatch_config_compose(args: Args) -> bool: return uwtools.api.config.compose( configs=args[STR.configs], output_file=args[STR.outfile], + realize=args[STR.realize], input_format=args[STR.infmt], output_format=args[STR.outfmt], ) @@ -937,6 +939,14 @@ def _add_arg_rate(group: Group) -> None: ) +def _add_arg_realize(group: Group) -> None: + group.add_argument( + _switch(STR.realize), + action="store_true", + help="Try to render template expressions", + ) + + def _add_arg_report(group: Group) -> None: group.add_argument( _switch(STR.report), diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index b9aa3f9e9..e736978db 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -40,6 +40,7 @@ def compare( def compose( configs: list[Path], + realize: bool, output_file: Path | None = None, input_format: str | None = None, output_format: str | None = None, @@ -58,6 +59,8 @@ def compose( output_format = output_format or get_config_format(output_file, "output") output_class = format_to_config(output_format) output_config = output_class(config) + if realize: + output_config.dereference() output_config.dump(output_file) return True diff --git a/src/uwtools/tests/api/test_config.py b/src/uwtools/tests/api/test_config.py index bf8afcd5b..db0377a05 100644 --- a/src/uwtools/tests/api/test_config.py +++ b/src/uwtools/tests/api/test_config.py @@ -36,6 +36,7 @@ def test_api_config_compose(output_file, input_format, output_format): pathstrs = ["/path/to/c1.yaml", "/path/to/c2.yaml"] kwargs: dict = { "configs": pathstrs, + "realize": False, "output_file": output_file, "input_format": input_format, "output_format": output_format, @@ -44,6 +45,7 @@ def test_api_config_compose(output_file, input_format, output_format): config.compose(**kwargs) _compose.assert_called_once_with( configs=list(map(Path, pathstrs)), + realize=False, output_file=None if output_file is None else Path(output_file), input_format=input_format, output_format=output_format, diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 49f8b6f58..de607c38e 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -184,16 +184,53 @@ def test_config_tools_compare__bad_format(logged): assert logged(msg) +@mark.parametrize(("configclass", "fmt"), [(INIConfig, FORMAT.ini), (NMLConfig, FORMAT.nml)]) +def test_config_tools_compose__fmt_ini_nml_2x(configclass, fmt, logged, tmp_path): + d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} + u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} + suffix = f".{fmt}" + dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] + configclass(d).dump(dpath) + configclass(u).dump(upath) + outpath = (tmp_path / "out").with_suffix(suffix) + kwargs: dict = {"configs": [dpath, upath], "realize": False, "output_file": outpath} + assert tools.compose(**kwargs) is True + assert logged(f"Reading {dpath} as base '{fmt}' config") + assert logged(f"Composing '{fmt}' config from {upath}") + expected = { + "constants": {"pi": 3.142, "e": 2.718}, + "trees": {"leaf": "elm", "needle": "fir"}, + "colors": {"red": "crimson", "green": "clover"}, + } + if fmt == FORMAT.ini: + expected["constants"] = {"pi": "3.142", "e": "2.718"} + assert configclass(outpath) == configclass(expected) + + +def test_config_tools_compose__fmt_sh_2x(logged, tmp_path): + d = {"foo": 1, "bar": 2} + u = {"foo": 3, "baz": 4} + dpath, upath = [tmp_path / x for x in ("d.sh", "u.sh")] + SHConfig(d).dump(dpath) + SHConfig(u).dump(upath) + outpath = tmp_path / "out.sh" + kwargs: dict = {"configs": [dpath, upath], "realize": False, "output_file": outpath} + assert tools.compose(**kwargs) is True + assert logged(f"Reading {dpath} as base 'sh' config") + assert logged(f"Composing 'sh' config from {upath}") + assert SHConfig(outpath) == SHConfig({"foo": "3", "bar": "2", "baz": "4"}) + + @mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) -def test_config_tools_compose__yaml_single(compose_assets_yaml, logged, suffix, tmp_path, tofile): +def test_config_tools_compose__fmt_yaml_1x(compose_assets_yaml, logged, suffix, tmp_path, tofile): d1, _, d2, _, d3, _ = compose_assets_yaml dpath = (tmp_path / "d").with_suffix(suffix) for d in (d1, d2, d3): dpath.unlink(missing_ok=True) assert not dpath.exists() dpath.write_text(yaml.dump(d)) - kwargs: dict = {"configs": [dpath]} + kwargs: dict = {"configs": [dpath], "realize": False} if suffix and suffix != ".yaml": kwargs["input_format"] = FORMAT.yaml if tofile: @@ -208,7 +245,7 @@ def test_config_tools_compose__yaml_single(compose_assets_yaml, logged, suffix, @mark.parametrize("tofile", [False, True]) @mark.parametrize("suffix", ["", ".yaml", ".foo"]) -def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, tmp_path, tofile): +def test_config_tools_compose__fmt_yaml_2x(compose_assets_yaml, logged, suffix, tmp_path, tofile): d1, u1, d2, u2, d3, u3 = compose_assets_yaml dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] for d, u in [(d1, u1), (d2, u2), (d3, u3)]: @@ -217,7 +254,7 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, assert not path.exists() dpath.write_text(yaml.dump(d)) upath.write_text(yaml.dump(u)) - kwargs: dict = {"configs": [dpath, upath]} + kwargs: dict = {"configs": [dpath, upath], "realize": False} if suffix and suffix != ".yaml": kwargs["input_format"] = FORMAT.yaml if tofile: @@ -242,41 +279,23 @@ def test_config_tools_compose__yaml_double(compose_assets_yaml, logged, suffix, outpath.unlink() -@mark.parametrize(("configclass", "fmt"), [(INIConfig, FORMAT.ini), (NMLConfig, FORMAT.nml)]) -def test_config_tools_compose__ini_nml_double(configclass, fmt, logged, tmp_path): - d = {"constants": {"pi": 3.142, "e": 2.718}, "trees": {"leaf": "elm", "needle": "spruce"}} - u = {"trees": {"needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}} - suffix = f".{fmt}" - dpath, upath = [(tmp_path / x).with_suffix(suffix) for x in ("d", "u")] - configclass(d).dump(dpath) - configclass(u).dump(upath) - outpath = (tmp_path / "out").with_suffix(suffix) - kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} - assert tools.compose(**kwargs) is True - assert logged(f"Reading {dpath} as base '{fmt}' config") - assert logged(f"Composing '{fmt}' config from {upath}") - expected = { - "constants": {"pi": 3.142, "e": 2.718}, - "trees": {"leaf": "elm", "needle": "fir"}, - "colors": {"red": "crimson", "green": "clover"}, - } - if fmt == FORMAT.ini: - expected["constants"] = {"pi": "3.142", "e": "2.718"} - assert configclass(outpath) == configclass(expected) - - -def test_config_tools_compose__sh_double(logged, tmp_path): - d = {"foo": 1, "bar": 2} - u = {"foo": 3, "baz": 4} - dpath, upath = [tmp_path / x for x in ("d.sh", "u.sh")] - SHConfig(d).dump(dpath) - SHConfig(u).dump(upath) - outpath = tmp_path / "out.sh" - kwargs: dict = {"configs": [dpath, upath], "output_file": outpath} - assert tools.compose(**kwargs) is True - assert logged(f"Reading {dpath} as base 'sh' config") - assert logged(f"Composing 'sh' config from {upath}") - assert SHConfig(outpath) == SHConfig({"foo": "3", "bar": "2", "baz": "4"}) +@mark.parametrize("realize", [False, True]) +def test_config_tools_compose__realize(realize, tmp_path): + dyaml = """ + radius: !float '{{ 2.0 * pi * r }}' + """ + dpath = tmp_path / "d.yaml" + dpath.write_text(dedent(dyaml)) + uyaml = """ + pi: 3.142 + r: 1.0 + """ + upath = tmp_path / "u.yaml" + upath.write_text(dedent(uyaml)) + outpath = tmp_path / "out.yaml" + assert tools.compose(configs=[dpath, upath], realize=realize, output_file=outpath) is True + radius = YAMLConfig(outpath)["radius"] + assert (radius == 6.284) if realize else (radius.tagged_string == "!float '{{ 2.0 * pi * r }}'") @mark.parametrize( diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py index 96078a909..8b11a8ff8 100644 --- a/src/uwtools/tests/test_cli.py +++ b/src/uwtools/tests/test_cli.py @@ -335,6 +335,7 @@ def test_cli__dispatch_config_compose(): outfile = "/path/to/output" args = { STR.configs: configs, + STR.realize: True, STR.outfile: outfile, STR.infmt: FORMAT.yaml, STR.outfmt: FORMAT.yaml, @@ -342,7 +343,11 @@ def test_cli__dispatch_config_compose(): with patch.object(cli.uwtools.api.config, "compose") as compose: cli._dispatch_config_compose(args) compose.assert_called_once_with( - configs=configs, output_file=outfile, input_format=FORMAT.yaml, output_format=FORMAT.yaml + configs=configs, + realize=True, + output_file=outfile, + input_format=FORMAT.yaml, + output_format=FORMAT.yaml, ) From 003c63e9d0fc6bf358df68b62776d7b35701121c Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 04:23:39 +0000 Subject: [PATCH 39/46] Update custom.css --- docs/static/custom.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/static/custom.css b/docs/static/custom.css index ff5a2d585..b29e00523 100644 --- a/docs/static/custom.css +++ b/docs/static/custom.css @@ -22,12 +22,14 @@ html.writer-html5 .rst-content table.docutils td>p { white-space: inherit; } -/* Left align captions. */ +/* Hide "Listing ..." text in captions */ .rst-content .caption-number { display: none; } +/* Left align captions. */ + .rst-content .code-block-caption { text-align: left; } From 7b714bc9ca45ed0d98dfabb14add21825f8f988f Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 04:31:03 +0000 Subject: [PATCH 40/46] Update docstring --- src/uwtools/config/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index e736978db..ada854688 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -309,8 +309,8 @@ def _validate_format(other_fmt_desc: str, other_fmt: str, input_fmt: str) -> Non :param configs: Paths to configs to compose. :param output_file: Output config destination (default: write to stdout). -:param input_format: Format of configs to compose (choices: {choices}, default: {default}) -:param output_format: Format of output config (choices: {choices}, default: {default}) +:param input_format: Format of configs to compose (choices: {choices}, default: {default}). +:param output_format: Format of output config (choices: {choices}, default: {default}). :return: True if no errors were encountered. """.format( default=FORMAT.yaml, From db7882369352668ccc706a1030f3b7cbf6ae3cfc Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 04:33:49 +0000 Subject: [PATCH 41/46] Update 'render' description --- src/uwtools/api/config.py | 2 +- src/uwtools/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 427867149..03f49be2c 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -238,7 +238,7 @@ def validate( Recognized file extensions are: {extensions}. :param configs: Paths to configs to compose. -:param realize: Try to render template expressions. +:param realize: Render template expressions where possible. :param output_file: Output config destination (default: write to ``stdout``). :param input_format: Format of configs to compose (choices: {choices}, default: ``{default}``) :param output_format: Format of output config (choices: {choices}, default: ``{default}``) diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py index 6558042b0..6b91ddc47 100644 --- a/src/uwtools/cli.py +++ b/src/uwtools/cli.py @@ -943,7 +943,7 @@ def _add_arg_realize(group: Group) -> None: group.add_argument( _switch(STR.realize), action="store_true", - help="Try to render template expressions", + help="Render template expressions where possible", ) From cdc7eb8c0b98b7746f060a7dd1076a3e2483b86d Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 04:34:46 +0000 Subject: [PATCH 42/46] Update docs --- docs/sections/user_guide/cli/tools/config/compose-help.out | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sections/user_guide/cli/tools/config/compose-help.out b/docs/sections/user_guide/cli/tools/config/compose-help.out index cf6e7308f..59d8173d5 100644 --- a/docs/sections/user_guide/cli/tools/config/compose-help.out +++ b/docs/sections/user_guide/cli/tools/config/compose-help.out @@ -15,7 +15,7 @@ Optional arguments: --version Show version info and exit --realize - Try to render template expressions + Render template expressions where possible --output-file PATH, -o PATH Path to output file (default: write to stdout) --input-format {ini,nml,sh,yaml} From beaf3f9a5b2a715c5224ae9febae8cf15f9dda2b Mon Sep 17 00:00:00 2001 From: Paul Madden <136389411+maddenp-cu@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:42:04 -0600 Subject: [PATCH 43/46] Update docs/index.rst Co-authored-by: Emily Carpenter <137525341+elcarpenterNOAA@users.noreply.github.com> --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 9d7306b7e..81d8c859e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,7 +40,7 @@ When the Linux diff tool just doesn't work for comparing unordered namelists wit Compose Action """""""""""""" -To compose configs is to start with a base config and to update its values, structurally, from the contents of one or more other configs of the same type (YAML, Fortran namelist, etc.). Composition supports building up complex experiment configurations by, for example, combining default values appropriate to the overall application with values needed on a particular machine, and then with specific user requrements, each stored in their own config files. Such a hierarchical approach to configuration management provides flexibility and avoids repetition of common values across files. +To compose configs is to start with a base config and to update its values, structurally, from the contents of one or more other configs of the same type (YAML, Fortran namelist, etc.). Composition supports building up complex experiment configurations by, for example, combining default values appropriate to the overall application with values needed on a particular machine, and then with specific user requirements, each stored in their own config files. Such a hierarchical approach to configuration management provides flexibility and avoids repetition of common values across files. | :any:`CLI documentation with examples` From 06df16314eb53340802863a7366c8334d4ea3bb2 Mon Sep 17 00:00:00 2001 From: Paul Madden <136389411+maddenp-cu@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:42:15 -0600 Subject: [PATCH 44/46] Update src/uwtools/api/config.py Co-authored-by: Emily Carpenter <137525341+elcarpenterNOAA@users.noreply.github.com> --- src/uwtools/api/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 03f49be2c..ff058972c 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -234,7 +234,7 @@ def validate( compose.__doc__ = """ Compose config files. -Specify explicit input or output formats to override default treatment baseed on file extension. +Specify explicit input or output formats to override default treatment based on file extension. Recognized file extensions are: {extensions}. :param configs: Paths to configs to compose. From 8613b26404148607322b14d4ba09db4821ab50dc Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 20:28:36 +0000 Subject: [PATCH 45/46] Add value to override in compose-base.yaml --- docs/sections/user_guide/cli/tools/config/compose-base.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sections/user_guide/cli/tools/config/compose-base.yaml b/docs/sections/user_guide/cli/tools/config/compose-base.yaml index a0462fd24..0e050e998 100644 --- a/docs/sections/user_guide/cli/tools/config/compose-base.yaml +++ b/docs/sections/user_guide/cli/tools/config/compose-base.yaml @@ -1,3 +1,4 @@ constants: pi: 3.142 color: blue +flower: violet From c5ef0a9ccfe860c3a31fda79d3863dc87d20d0f5 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 17 Sep 2025 20:32:09 +0000 Subject: [PATCH 46/46] Update test --- src/uwtools/tests/config/test_tools.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index de607c38e..e26fd8a1e 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -197,13 +197,15 @@ def test_config_tools_compose__fmt_ini_nml_2x(configclass, fmt, logged, tmp_path assert tools.compose(**kwargs) is True assert logged(f"Reading {dpath} as base '{fmt}' config") assert logged(f"Composing '{fmt}' config from {upath}") + constants = {"pi": 3.142, "e": 2.718} expected = { - "constants": {"pi": 3.142, "e": 2.718}, + "constants": constants, "trees": {"leaf": "elm", "needle": "fir"}, "colors": {"red": "crimson", "green": "clover"}, } if fmt == FORMAT.ini: - expected["constants"] = {"pi": "3.142", "e": "2.718"} + # All values in INI configs are strings: + expected["constants"] = {k: str(v) for k, v in constants.items()} assert configclass(outpath) == configclass(expected)