Skip to content

Commit

Permalink
UW-580 config realize update (ufs-community#493)
Browse files Browse the repository at this point in the history
  • Loading branch information
maddenp-noaa authored May 22, 2024
1 parent 73f4f4e commit 8d3fdae
Show file tree
Hide file tree
Showing 16 changed files with 733 additions and 688 deletions.
357 changes: 173 additions & 184 deletions docs/sections/user_guide/cli/tools/config.rst

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/sections/user_guide/yaml/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ Removes the tagged YAML key/value pair. For example, given ``input.yaml``:
e: 2.718
pi: 3.141
and ``supplemental.yaml``:
and ``update.yaml``:

.. code-block:: yaml
e: !remove
.. code-block:: text
% uw config realize --input-file input.yaml supplemental.yaml --output-format yaml
% uw config realize --input-file input.yaml --update-file update.yaml --output-format yaml
pi: 3.141
58 changes: 31 additions & 27 deletions src/uwtools/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get_fieldtable_config(
:param config: FieldTable file to load (``None`` or unspecified => read ``stdin``), or initial
``dict``
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: An initialized ``FieldTableConfig`` object
"""
return _FieldTableConfig(config=_ensure_data_source(config, stdin_ok))
Expand All @@ -61,7 +61,7 @@ def get_ini_config(
Get an ``INIConfig`` object.
:param config: INI file to load (``None`` or unspecified => read ``stdin``), or initial ``dict``
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: An initialized ``INIConfig`` object
"""
return _INIConfig(config=_ensure_data_source(config, stdin_ok))
Expand All @@ -76,7 +76,7 @@ def get_nml_config(
:param config: Fortran namelist file to load (``None`` or unspecified => read ``stdin``), or
initial ``dict``
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: An initialized ``NMLConfig`` object
"""
return _NMLConfig(config=_ensure_data_source(config, stdin_ok))
Expand All @@ -91,7 +91,7 @@ def get_sh_config(
:param config: File of shell 'key=value' pairs to load (``None`` or unspecified => read
``stdin``), or initial ``dict``
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: An initialized ``SHConfig`` object
"""
return _SHConfig(config=_ensure_data_source(config, stdin_ok))
Expand All @@ -106,7 +106,7 @@ def get_yaml_config(
:param config: YAML file to load (``None`` or unspecified => read ``stdin``), or initial
``dict``
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: An initialized ``YAMLConfig`` object
"""
return _YAMLConfig(config=_ensure_data_source(config, stdin_ok))
Expand All @@ -115,10 +115,11 @@ def get_yaml_config(
def realize(
input_config: Optional[Union[dict, _Config, Path, str]] = None,
input_format: Optional[str] = None,
output_block: Optional[List[Union[str, int]]] = None,
update_config: Optional[Union[dict, _Config, Path, str]] = None,
update_format: Optional[str] = None,
output_file: Optional[Union[Path, str]] = None,
output_format: Optional[str] = None,
supplemental_configs: Optional[List[Union[dict, _Config, Path, str]]] = None,
output_block: Optional[List[Union[str, int]]] = None,
values_needed: bool = False,
total: bool = False,
dry_run: bool = False,
Expand All @@ -130,14 +131,14 @@ def realize(
input_config = (
_YAMLConfig(config=input_config) if isinstance(input_config, dict) else input_config
)
scs = [_str2path(x) for x in supplemental_configs] if supplemental_configs else None
_realize(
input_config=_ensure_data_source(input_config, stdin_ok),
input_format=input_format,
output_block=output_block,
update_config=_ensure_data_source(update_config, stdin_ok),
update_format=update_format,
output_file=_str2path(output_file),
output_format=output_format,
supplemental_configs=scs,
output_block=output_block,
values_needed=values_needed,
total=total,
dry_run=dry_run,
Expand All @@ -147,13 +148,14 @@ def realize(
def realize_to_dict( # pylint: disable=unused-argument
input_config: Optional[Union[dict, _Config, Path, str]] = None,
input_format: Optional[str] = None,
supplemental_configs: Optional[List[Union[dict, _Config, Path, str]]] = None,
update_config: Optional[Union[dict, _Config, Path, str]] = None,
update_format: Optional[str] = None,
values_needed: bool = False,
dry_run: bool = False,
stdin_ok: bool = False,
) -> dict:
"""
Realize a config to a ``dict``, based on an input config and optional supplemental configs.
Realize a config to a ``dict``, based on a base input config and an optional update config.
See ``realize()`` for details on arguments, etc.
"""
Expand All @@ -173,7 +175,7 @@ def validate(
:param schema_file: The JSON Schema file to use for validation
:param config: The config to validate
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: ``True`` if the YAML file conforms to the schema, ``False`` otherwise
"""
return _validate_yaml(
Expand Down Expand Up @@ -203,20 +205,21 @@ def validate(


realize.__doc__ = """
Realize a config based on an input config and optional supplemental configs.
Realize a config based on a base input config and an optional update config.
If no input is specified, ``stdin`` is read. A ``dict`` or ``Config`` object may also be provided as
input. If no output is specified, ``stdout`` is written to. When an input or output filename is
specified, its format will be deduced from its extension, if possible. This can be overridden by
specifying the format explicitly, and it is required to do so for reads from ``stdin`` or writes to
``stdout``, as no attempt is made to deduce the format of streamed data.
The input config may be specified as a filesystem path, a ``dict``, or a ``Config`` object. When it
is not, it will be read from ``stdin``.
If optional supplemental configs (which may likewise be file paths or ``Config`` / ``dict`` objects)
are provided, they will be merged, in the order specified, onto the input config. The format of all
input configs must match.
If an update config is specified, it is merged onto the input config, augmenting or overriding base
values. It may be specified as a filesystem path, a ``dict``, or a ``Config`` object. When it is
not, it will be read from ``stdin``.
If the input-config format is YAML, any supported output format may be specified. For all other
input formats, the output format must match the input.
At most one of the input config or the update config may be left unspecified, in which case the
other will be read from ``stdin``. If neither filename or format is specified for the update config, no
update will be performed.
The output destination may be specified as a filesystem path. When it is not, it will be written to
``stdout``.
If ``values_needed`` is ``True``, a report of values needed to realize the config is logged. In
``dry_run`` mode, output is written to ``stderr``.
Expand All @@ -228,14 +231,15 @@ def validate(
:param input_config: Input config file (``None`` or unspecified => read ``stdin``)
:param input_format: Format of the input config (optional if file's extension is recognized)
:param output_block: Path through keys to the desired output block
:param update_config: Update config file (``None`` or unspecified => read ``stdin``)
:param update_format: Format of the update config (optional if file's extension is recognized)
:param output_file: Output config file (``None`` or unspecified => write to ``stdout``)
:param output_format: Format of the output config (optional if file's extension is recognized)
:param supplemental_configs: Configs to merge, in order, onto the input
:param output_block: Path through 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
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:raises: UWConfigRealizeError if ``total`` is ``True`` and any Jinja2 variable/expression was not rendered
""".format(
extensions=", ".join(_FORMAT.extensions())
Expand Down
4 changes: 2 additions & 2 deletions src/uwtools/api/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def copy(
:param config: YAML-file path, or ``dict`` (read ``stdin`` if missing or ``None``).
:param keys: YAML keys leading to file dst/src block
:param dry_run: Do not copy files
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: ``True`` if no exception is raised
"""
_FileCopier(
Expand All @@ -50,7 +50,7 @@ def link(
:param config: YAML-file path, or ``dict`` (read ``stdin`` if missing or ``None``).
:param keys: YAML keys leading to file dst/src block
:param dry_run: Do not link files
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: ``True`` if no exception is raised
"""
_FileLinker(
Expand Down
4 changes: 2 additions & 2 deletions src/uwtools/api/rocoto.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def realize(
``YAMLConfig`` object
:param output_file: Path to write rendered XML file (``None`` or unspecified => write to
``stdout``)
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: ``True``
"""
_realize(config=_ensure_data_source(config, stdin_ok), output_file=_str2path(output_file))
Expand All @@ -43,7 +43,7 @@ def validate(
Validate purported Rocoto XML file against its schema.
:param xml_file: Path to XML file (``None`` or unspecified => read ``stdin``)
:param stdin_ok: OK to read from stdin?
: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))
4 changes: 2 additions & 2 deletions src/uwtools/api/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def render(
:param searchpath: Paths to search for extra templates
:param values_needed: Just report variables needed to render the template?
:param dry_run: Run in dry-run mode?
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: The rendered template string
:raises: UWTemplateRenderError if template could not be rendered
"""
Expand Down Expand Up @@ -100,7 +100,7 @@ def translate(
read ``stdin``)
:param output_file: Path to the file to write the converted template to
:param dry_run: Run in dry-run mode?
:param stdin_ok: OK to read from stdin?
:param stdin_ok: OK to read from ``stdin``?
:return: ``True``
"""
_convert_atparse_to_jinja2(
Expand Down
47 changes: 34 additions & 13 deletions src/uwtools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,19 @@ def _add_subparser_config_realize(subparsers: Subparsers) -> ActionChecks:
optional = _basic_setup(parser)
_add_arg_input_file(optional)
_add_arg_input_format(optional, choices=FORMATS)
_add_arg_update_file(optional)
_add_arg_update_format(optional, choices=FORMATS)
_add_arg_output_file(optional)
_add_arg_output_format(optional, choices=FORMATS)
_add_arg_output_block(optional)
_add_arg_values_needed(optional)
_add_arg_total(optional)
_add_arg_dry_run(optional)
checks = _add_args_verbosity(optional)
_add_arg_supplemental_files(optional)
return checks + [
partial(_check_file_vs_format, STR.infile, STR.infmt),
partial(_check_file_vs_format, STR.outfile, STR.outfmt),
_check_update,
]


Expand Down Expand Up @@ -211,10 +213,11 @@ def _dispatch_config_realize(args: Args) -> bool:
uwtools.api.config.realize(
input_config=args[STR.infile],
input_format=args[STR.infmt],
output_block=args[STR.outblock],
update_config=args[STR.updatefile],
update_format=args[STR.updatefmt],
output_file=args[STR.outfile],
output_format=args[STR.outfmt],
supplemental_configs=args[STR.suppfiles],
output_block=args[STR.outblock],
values_needed=args[STR.valsneeded],
total=args[STR.total],
dry_run=args[STR.dryrun],
Expand Down Expand Up @@ -716,16 +719,6 @@ def _add_arg_search_path(group: Group) -> None:
)


def _add_arg_supplemental_files(group: Group) -> None:
group.add_argument(
STR.suppfiles,
help="Additional files to supplement primary input",
metavar="PATH",
nargs="*",
type=Path,
)


def _add_arg_target_dir(group: Group, required: bool) -> None:
group.add_argument(
_switch(STR.targetdir),
Expand All @@ -744,6 +737,27 @@ def _add_arg_total(group: Group) -> None:
)


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)",
metavar="PATH",
required=required,
type=Path,
)


def _add_arg_update_format(group: Group, choices: List[str], required: bool = False) -> None:
group.add_argument(
_switch(STR.updatefmt),
choices=choices,
help="Update format",
required=required,
type=str,
)


def _add_arg_values_file(group: Group, required: bool = False) -> None:
group.add_argument(
_switch(STR.valsfile),
Expand Down Expand Up @@ -928,6 +942,13 @@ def _check_template_render_vals_args(args: Args) -> Args:
return args


def _check_update(args: Args) -> Args:
if args.get(STR.updatefile) is not None:
if args.get(STR.updatefmt) is None:
args[STR.updatefmt] = get_file_format(args[STR.updatefile])
return args


def _check_verbosity(args: Args) -> Args:
if args.get(STR.quiet) and args.get(STR.verbose):
_abort("%s may not be used with %s" % (_switch(STR.quiet), _switch(STR.verbose)))
Expand Down
2 changes: 1 addition & 1 deletion src/uwtools/config/formats/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self, config: Optional[Union[dict, Path]] = None) -> None:

def __repr__(self) -> str:
"""
Returns the string representation of a Config object.
Returns the YAML string representation of a Config object.
"""
s = StringIO()
yaml.dump(self.data, s)
Expand Down
1 change: 0 additions & 1 deletion src/uwtools/config/formats/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def dump(self, path: Optional[Path] = None) -> None:
:param path: Path to dump config to.
"""
config_check_depths_dump(config_obj=self, target_format=FORMAT.ini)

self.dump_dict(self.data, path)

@staticmethod
Expand Down
Loading

0 comments on commit 8d3fdae

Please sign in to comment.