diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index b8e91faed..7201cfb61 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -50,7 +50,7 @@ def get_fieldtable_config( :param stdin_ok: OK to read from ``stdin``? :return: An initialized ``FieldTableConfig`` object """ - return _FieldTableConfig(config=_ensure_data_source(config, stdin_ok)) + return _FieldTableConfig(config=_ensure_data_source(_str2path(config), stdin_ok)) def get_ini_config( @@ -64,7 +64,7 @@ def get_ini_config( :param stdin_ok: OK to read from ``stdin``? :return: An initialized ``INIConfig`` object """ - return _INIConfig(config=_ensure_data_source(config, stdin_ok)) + return _INIConfig(config=_ensure_data_source(_str2path(config), stdin_ok)) def get_nml_config( @@ -79,7 +79,7 @@ def get_nml_config( :param stdin_ok: OK to read from ``stdin``? :return: An initialized ``NMLConfig`` object """ - return _NMLConfig(config=_ensure_data_source(config, stdin_ok)) + return _NMLConfig(config=_ensure_data_source(_str2path(config), stdin_ok)) def get_sh_config( @@ -94,7 +94,7 @@ def get_sh_config( :param stdin_ok: OK to read from ``stdin``? :return: An initialized ``SHConfig`` object """ - return _SHConfig(config=_ensure_data_source(config, stdin_ok)) + return _SHConfig(config=_ensure_data_source(_str2path(config), stdin_ok)) def get_yaml_config( @@ -109,13 +109,13 @@ def get_yaml_config( :param stdin_ok: OK to read from ``stdin``? :return: An initialized ``YAMLConfig`` object """ - return _YAMLConfig(config=_ensure_data_source(config, stdin_ok)) + return _YAMLConfig(config=_ensure_data_source(_str2path(config), stdin_ok)) def realize( - input_config: Optional[Union[dict, _Config, Path, str]] = None, + input_config: Optional[Union[_Config, Path, dict, str]] = None, input_format: Optional[str] = None, - update_config: Optional[Union[dict, _Config, Path, str]] = None, + update_config: Optional[Union[_Config, Path, dict, str]] = None, update_format: Optional[str] = None, output_file: Optional[Union[Path, str]] = None, output_format: Optional[str] = None, @@ -124,17 +124,14 @@ def realize( total: bool = False, dry_run: bool = False, stdin_ok: bool = False, -) -> None: +) -> dict: """ NB: This docstring is dynamically replaced: See realize.__doc__ definition below. """ - input_config = ( - _YAMLConfig(config=input_config) if isinstance(input_config, dict) else input_config - ) - _realize( - input_config=_ensure_data_source(input_config, stdin_ok), + return _realize( + input_config=_ensure_data_source(_str2path(input_config), stdin_ok), input_format=input_format, - update_config=_ensure_data_source(update_config, stdin_ok), + update_config=_ensure_data_source(_str2path(update_config), stdin_ok), update_format=update_format, output_file=_str2path(output_file), output_format=output_format, @@ -150,7 +147,9 @@ def realize_to_dict( # pylint: disable=unused-argument input_format: Optional[str] = None, update_config: Optional[Union[dict, _Config, Path, str]] = None, update_format: Optional[str] = None, + key_path: Optional[list[Union[str, int]]] = None, values_needed: bool = False, + total: bool = False, dry_run: bool = False, stdin_ok: bool = False, ) -> dict: @@ -159,7 +158,7 @@ def realize_to_dict( # pylint: disable=unused-argument See ``realize()`` for details on arguments, etc. """ - return _realize(**{**locals(), "output_file": Path(os.devnull), "output_format": None}) + return realize(**{**locals(), "output_file": Path(os.devnull), "output_format": _FORMAT.yaml}) def validate( @@ -179,7 +178,7 @@ def validate( :return: ``True`` if the YAML file conforms to the schema, ``False`` otherwise """ return _validate_yaml( - schema_file=_ensure_data_source(schema_file, stdin_ok), config=_str2path(config) + schema_file=_str2path(schema_file), config=_ensure_data_source(_str2path(config), stdin_ok) ) @@ -240,6 +239,7 @@ def validate( :param total: Require rendering of all Jinja2 variables/expressions :param dry_run: Log output instead of writing to output :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 variable/expression was not rendered """.format( extensions=", ".join(_FORMAT.extensions()) diff --git a/src/uwtools/api/file.py b/src/uwtools/api/file.py index 48a8065f5..e6cfebb11 100644 --- a/src/uwtools/api/file.py +++ b/src/uwtools/api/file.py @@ -9,11 +9,12 @@ from uwtools.file import FileCopier as _FileCopier from uwtools.file import FileLinker as _FileLinker from uwtools.utils.api import ensure_data_source as _ensure_data_source +from uwtools.utils.api import str2path as _str2path def copy( target_dir: Union[Path, str], - config: Optional[Union[dict, Path, str]] = None, + config: Optional[Union[Path, dict, str]] = None, cycle: Optional[dt.datetime] = None, leadtime: Optional[dt.timedelta] = None, keys: Optional[list[str]] = None, @@ -34,7 +35,7 @@ def copy( """ _FileCopier( target_dir=Path(target_dir), - config=_ensure_data_source(config, stdin_ok), + config=_ensure_data_source(_str2path(config), stdin_ok), cycle=cycle, leadtime=leadtime, keys=keys, @@ -45,7 +46,7 @@ def copy( def link( target_dir: Union[Path, str], - config: Optional[Union[dict, Path, str]] = None, + config: Optional[Union[Path, dict, str]] = None, cycle: Optional[dt.datetime] = None, leadtime: Optional[dt.timedelta] = None, keys: Optional[list[str]] = None, @@ -66,7 +67,7 @@ def link( """ _FileLinker( target_dir=Path(target_dir), - config=_ensure_data_source(config, stdin_ok), + config=_ensure_data_source(_str2path(config), stdin_ok), cycle=cycle, leadtime=leadtime, keys=keys, diff --git a/src/uwtools/api/rocoto.py b/src/uwtools/api/rocoto.py index 2c25f873c..e8db86207 100644 --- a/src/uwtools/api/rocoto.py +++ b/src/uwtools/api/rocoto.py @@ -31,7 +31,9 @@ def realize( :param stdin_ok: OK to read from ``stdin``? :return: ``True`` """ - _realize(config=_ensure_data_source(config, stdin_ok), output_file=_str2path(output_file)) + _realize( + config=_ensure_data_source(_str2path(config), stdin_ok), output_file=_str2path(output_file) + ) return True @@ -46,4 +48,4 @@ def validate( :param stdin_ok: OK to read from ``stdin``? :return: ``True`` if the XML conforms to the schema, ``False`` otherwise """ - return _validate(xml_file=_ensure_data_source(xml_file, stdin_ok)) + return _validate(xml_file=_ensure_data_source(_str2path(xml_file), stdin_ok)) diff --git a/src/uwtools/api/template.py b/src/uwtools/api/template.py index 7f589ce08..ea12fe5e6 100644 --- a/src/uwtools/api/template.py +++ b/src/uwtools/api/template.py @@ -52,7 +52,7 @@ def render( result = _render( values_src=_str2path(values_src), values_format=values_format, - input_file=_ensure_data_source(input_file, stdin_ok), + input_file=_ensure_data_source(_str2path(input_file), stdin_ok), output_file=_str2path(output_file), overrides=overrides, env=env, @@ -104,7 +104,7 @@ def translate( :return: ``True`` """ _convert_atparse_to_jinja2( - input_file=_ensure_data_source(input_file, stdin_ok), + input_file=_ensure_data_source(_str2path(input_file), stdin_ok), output_file=_str2path(output_file), dry_run=dry_run, ) diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index ab86472a7..26d62bc14 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -76,9 +76,9 @@ def config_check_depths_update(config_obj: Union[Config, dict], target_format: s def realize_config( - input_config: Union[Config, Optional[Path]] = None, + input_config: Optional[Union[Config, Path, dict]] = None, input_format: Optional[str] = None, - update_config: Union[Config, Optional[Path]] = None, + update_config: Optional[Union[Config, Path, dict]] = None, update_format: Optional[str] = None, output_file: Optional[Path] = None, output_format: Optional[str] = None, @@ -90,7 +90,7 @@ def realize_config( """ NB: This docstring is dynamically replaced: See realize_config.__doc__ definition below. """ - input_obj, input_format = _realize_config_input_setup(input_config, input_format) + input_obj = _realize_config_input_setup(input_config, input_format) input_obj = _realize_config_update(input_obj, update_config, update_format) input_obj.dereference() output_data, output_format = _realize_config_output_setup( @@ -114,7 +114,7 @@ def realize_config( def _ensure_format( - desc: str, fmt: Optional[str] = None, config: Union[Config, Optional[Path]] = None + desc: str, fmt: Optional[str] = None, config: Optional[Union[Config, Path, dict]] = None ) -> str: """ Return the given format, or the appropriate format as deduced from the config. @@ -127,11 +127,12 @@ def _ensure_format( """ if isinstance(config, Config): return config.get_format() + if isinstance(config, Path): + return fmt or get_file_format(config) + if isinstance(config, dict): + return fmt or FORMAT.yaml if fmt is None: - if config is not None: - fmt = get_file_format(config) - else: - raise UWError(f"Either {desc} file format or name must be specified") + raise UWError(f"Either {desc} path or format name must be specified") return fmt @@ -161,24 +162,22 @@ def _print_config_section(config: dict, key_path: list[str]) -> None: def _realize_config_input_setup( - input_config: Union[Config, Optional[Path]] = None, input_format: Optional[str] = None -) -> tuple[Config, str]: + input_config: Optional[Union[Config, Path, dict]] = None, input_format: Optional[str] = None +) -> Config: """ Set up config-realize input. :param input_config: Input config source (None => read stdin). :param input_format: Format of the input config. - :return: The input Config object and its format name. + :return: The input Config object. """ + if isinstance(input_config, Config): + return input_config input_format = _ensure_format("input", input_format, input_config) if not input_config: log.debug("Reading input from stdin") - input_obj: Config = ( - input_config - if isinstance(input_config, Config) - else format_to_config(input_format)(config=input_config) - ) - return input_obj, input_format + config_obj: Config = format_to_config(input_format)(config=input_config) + return config_obj def _realize_config_output_setup( @@ -209,7 +208,7 @@ def _realize_config_output_setup( def _realize_config_update( input_obj: Config, - update_config: Union[Config, Optional[Path]] = None, + update_config: Optional[Union[Config, Path, dict]] = None, update_format: Optional[str] = None, ) -> Config: """ diff --git a/src/uwtools/tests/api/test_config.py b/src/uwtools/tests/api/test_config.py index 8336f6c6b..e58744684 100644 --- a/src/uwtools/tests/api/test_config.py +++ b/src/uwtools/tests/api/test_config.py @@ -9,6 +9,7 @@ from uwtools.api import config from uwtools.config.formats.yaml import YAMLConfig +from uwtools.utils.file import FORMAT def test_compare(): @@ -77,14 +78,16 @@ def test_realize_to_dict(): "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: + with patch.object(config, "realize") as realize: config.realize_to_dict(**kwargs) - _realize.assert_called_once_with( - **dict({**kwargs, **{"output_file": Path(os.devnull), "output_format": None}}) + realize.assert_called_once_with( + **dict({**kwargs, **{"output_file": Path(os.devnull), "output_format": FORMAT.yaml}}) ) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index 2ea860181..94a3f0873 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -520,18 +520,27 @@ def test_realize_config_values_needed_yaml(caplog): assert actual.strip() == dedent(expected).strip() -def test__ensure_format_bad(): +def test__ensure_format_bad_no_path_no_format(): with raises(UWError) as e: tools._ensure_format(desc="foo") - assert str(e.value) == "Either foo file format or name must be specified" + assert str(e.value) == "Either foo path or format name must be specified" def test__ensure_format_config_obj(): - assert tools._ensure_format(desc="foo", config=YAMLConfig(config={})) == FORMAT.yaml + config = NMLConfig({"nl": {"n": 88}}) + assert tools._ensure_format(desc="foo", config=config) == FORMAT.nml + + +def test__ensure_format_dict_explicit(): + assert tools._ensure_format(desc="foo", fmt=FORMAT.yaml, config={}) == FORMAT.yaml + + +def test__ensure_format_dict_implicit(): + assert tools._ensure_format(desc="foo", config={}) == FORMAT.yaml def test__ensure_format_deduced(): - assert tools._ensure_format(desc="foo", config=Path("/tmp/config.yaml")) == FORMAT.yaml + assert tools._ensure_format(desc="foo", config=Path("/tmp/config.nml")) == FORMAT.nml def test__ensure_format_explicitly_specified_no_path(): @@ -607,9 +616,8 @@ def test__print_config_section_yaml_not_dict(): def test__realize_config_input_setup_ini_cfgobj(): data = {"section": {"foo": "bar"}} cfgobj = INIConfig(config=data) - input_obj, input_format = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_config_input_setup(input_config=cfgobj) assert input_obj.data == data - assert input_format == FORMAT.ini def test__realize_config_input_setup_ini_file(tmp_path): @@ -620,9 +628,8 @@ def test__realize_config_input_setup_ini_file(tmp_path): path = tmp_path / "config.ini" with open(path, "w", encoding="utf-8") as f: print(dedent(data).strip(), file=f) - input_obj, input_format = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_config_input_setup(input_config=path) assert input_obj.data == {"section": {"foo": "bar"}} - assert input_format == FORMAT.ini def test__realize_config_input_setup_ini_stdin(caplog): @@ -637,18 +644,16 @@ def test__realize_config_input_setup_ini_stdin(caplog): print(dedent(data).strip(), file=s) s.seek(0) with patch.object(sys, "stdin", new=s): - input_obj, input_format = tools._realize_config_input_setup(input_format=FORMAT.ini) + input_obj = tools._realize_config_input_setup(input_format=FORMAT.ini) assert input_obj.data == {"section": {"foo": "bar", "baz": "88"}} # note: 88 is str, not int - assert input_format == FORMAT.ini assert logged(caplog, "Reading input from stdin") def test__realize_config_input_setup_nml_cfgobj(): data = {"nl": {"pi": 3.14}} cfgobj = NMLConfig(config=data) - input_obj, input_format = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_config_input_setup(input_config=cfgobj) assert input_obj.data == data - assert input_format == FORMAT.nml def test__realize_config_input_setup_nml_file(tmp_path): @@ -660,9 +665,8 @@ def test__realize_config_input_setup_nml_file(tmp_path): path = tmp_path / "config.nml" with open(path, "w", encoding="utf-8") as f: print(dedent(data).strip(), file=f) - input_obj, input_format = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_config_input_setup(input_config=path) assert input_obj["nl"]["pi"] == 3.14 - assert input_format == FORMAT.nml def test__realize_config_input_setup_nml_stdin(caplog): @@ -677,18 +681,16 @@ def test__realize_config_input_setup_nml_stdin(caplog): print(dedent(data).strip(), file=s) s.seek(0) with patch.object(sys, "stdin", new=s): - input_obj, input_format = tools._realize_config_input_setup(input_format=FORMAT.nml) + input_obj = tools._realize_config_input_setup(input_format=FORMAT.nml) assert input_obj["nl"]["pi"] == 3.14 - assert input_format == FORMAT.nml assert logged(caplog, "Reading input from stdin") def test__realize_config_input_setup_sh_cfgobj(): data = {"foo": "bar"} cfgobj = SHConfig(config=data) - input_obj, input_format = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_config_input_setup(input_config=cfgobj) assert input_obj.data == data - assert input_format == FORMAT.sh def test__realize_config_input_setup_sh_file(tmp_path): @@ -698,9 +700,8 @@ def test__realize_config_input_setup_sh_file(tmp_path): path = tmp_path / "config.sh" with open(path, "w", encoding="utf-8") as f: print(dedent(data).strip(), file=f) - input_obj, input_format = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_config_input_setup(input_config=path) assert input_obj.data == {"foo": "bar"} - assert input_format == FORMAT.sh def test__realize_config_input_setup_sh_stdin(caplog): @@ -713,18 +714,16 @@ def test__realize_config_input_setup_sh_stdin(caplog): print(dedent(data).strip(), file=s) s.seek(0) with patch.object(sys, "stdin", new=s): - input_obj, input_format = tools._realize_config_input_setup(input_format=FORMAT.sh) + input_obj = tools._realize_config_input_setup(input_format=FORMAT.sh) assert input_obj.data == {"foo": "bar"} - assert input_format == FORMAT.sh assert logged(caplog, "Reading input from stdin") def test__realize_config_input_setup_yaml_cfgobj(): data = {"foo": "bar"} cfgobj = YAMLConfig(config=data) - input_obj, input_format = tools._realize_config_input_setup(input_config=cfgobj) + input_obj = tools._realize_config_input_setup(input_config=cfgobj) assert input_obj.data == data - assert input_format == FORMAT.yaml def test__realize_config_input_setup_yaml_file(tmp_path): @@ -734,9 +733,8 @@ def test__realize_config_input_setup_yaml_file(tmp_path): path = tmp_path / "config.yaml" with open(path, "w", encoding="utf-8") as f: print(dedent(data).strip(), file=f) - input_obj, input_format = tools._realize_config_input_setup(input_config=path) + input_obj = tools._realize_config_input_setup(input_config=path) assert input_obj.data == {"foo": "bar"} - assert input_format == FORMAT.yaml def test__realize_config_input_setup_yaml_stdin(caplog): @@ -749,9 +747,8 @@ def test__realize_config_input_setup_yaml_stdin(caplog): print(dedent(data).strip(), file=s) s.seek(0) with patch.object(sys, "stdin", new=s): - input_obj, input_format = tools._realize_config_input_setup(input_format=FORMAT.yaml) + input_obj = tools._realize_config_input_setup(input_format=FORMAT.yaml) assert input_obj.data == {"foo": "bar"} - assert input_format == FORMAT.yaml assert logged(caplog, "Reading input from stdin") diff --git a/src/uwtools/tests/utils/test_api.py b/src/uwtools/tests/utils/test_api.py index d3e6b1ee5..89ba2ac67 100644 --- a/src/uwtools/tests/utils/test_api.py +++ b/src/uwtools/tests/utils/test_api.py @@ -41,13 +41,6 @@ def test_ensure_data_source_stdin_ok(): assert api.ensure_data_source(data_source=None, stdin_ok=True) is None -def test_ensure_data_source_str_to_path(): - val = "/some/path" - result = api.ensure_data_source(data_source=val, stdin_ok=False) - assert isinstance(result, Path) - assert result == Path(val) - - def test_make_execute(execute_kwargs): func = api.make_execute(driver_class=TestDriver, with_cycle=False) assert func.__name__ == "execute" diff --git a/src/uwtools/utils/api.py b/src/uwtools/utils/api.py index b18348b51..180a7d791 100644 --- a/src/uwtools/utils/api.py +++ b/src/uwtools/utils/api.py @@ -5,31 +5,30 @@ import datetime as dt import re from pathlib import Path -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Optional, TypeVar, Union -from uwtools.config.formats.base import Config from uwtools.drivers.driver import DriverT from uwtools.drivers.support import graph from uwtools.drivers.support import tasks as _tasks from uwtools.exceptions import UWError +T = TypeVar("T") + # Public -def ensure_data_source( - data_source: Optional[Union[dict, Config, Path, str]], stdin_ok: bool -) -> Any: +def ensure_data_source(data_source: T, stdin_ok: bool) -> T: """ - If stdin read is disabled, ensure that a data source was provided. Convert str -> Path. + If stdin read is disabled, ensure that a data source was provided. :param data_source: Data source as provided to API. :param stdin_ok: OK to read from stdin? - :return: Data source, with a str converted to Path. + :return: Data source. :raises: UWError if no data source was provided and stdin read is disabled. """ if data_source is None and not stdin_ok: raise UWError("Set stdin_ok=True to permit read from stdin") - return str2path(data_source) + return data_source def make_execute( @@ -192,7 +191,7 @@ def _execute( :return: ``True`` if task completes without raising an exception. """ kwargs = dict( - config=ensure_data_source(config, stdin_ok), + config=ensure_data_source(str2path(config), stdin_ok), batch=batch, dry_run=dry_run, key_path=key_path,