diff --git a/docs/conf.py b/docs/conf.py
index 8e0e67f94..c1ec605c0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,7 +21,10 @@
html_static_path = ["static"]
html_theme = "sphinx_rtd_theme"
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
-nitpick_ignore_regex = [("py:class", r"^uwtools\..*"), ("py:class", "f90nml.Namelist")]
+nitpick_ignore = [
+ ("py:class", "Path"),
+ ("py:class", "f90nml.Namelist"),
+]
numfig = True
numfig_format = {"figure": "Figure %s"}
project = "Unified Workflow Tools"
diff --git a/docs/sections/user_guide/api/config.rst b/docs/sections/user_guide/api/config.rst
index 7b9811bb3..da9415bac 100644
--- a/docs/sections/user_guide/api/config.rst
+++ b/docs/sections/user_guide/api/config.rst
@@ -5,5 +5,5 @@
:target: https://mybinder.org/v2/gh/ufs-community/uwtools/main?labpath=notebooks%2Fconfig.ipynb
.. automodule:: uwtools.api.config
- :inherited-members: UserDict
:members:
+ :show-inheritance:
diff --git a/notebooks/config.ipynb b/notebooks/config.ipynb
index e49a44ae1..1cbd80e7b 100644
--- a/notebooks/config.ipynb
+++ b/notebooks/config.ipynb
@@ -1228,15 +1228,17 @@
"text": [
"Help on function validate in module uwtools.api.config:\n",
"\n",
- "validate(schema_file: Union[pathlib.Path, str], config: Union[dict, str, uwtools.config.formats.yaml.YAMLConfig, pathlib.Path, NoneType] = None, stdin_ok: bool = False) -> bool\n",
+ "validate(schema_file: Union[pathlib.Path, str], config_data: Union[bool, dict, float, int, list, str, uwtools.config.formats.yaml.YAMLConfig, NoneType] = None, config_path: Union[str, pathlib.Path, NoneType] = None, stdin_ok: bool = False) -> bool\n",
" Check whether the specified config conforms to the specified JSON Schema spec.\n",
"\n",
- " If no config is specified, ``stdin`` is read and will be parsed as YAML and then validated. A\n",
- " ``dict`` or a YAMLConfig instance may also be provided for validation.\n",
+ " Specify at most one of config_data or config_path. If no config is specified, ``stdin`` is read\n",
+ " and will be parsed as YAML and then validated.\n",
"\n",
" :param schema_file: The JSON Schema file to use for validation.\n",
- " :param config: The config to validate.\n",
+ " :param config_data: A config to validate.\n",
+ " :param config_path: A path to a file containing a config to validate.\n",
" :param stdin_ok: OK to read from ``stdin``?\n",
+ " :raises: TypeError if both config_* arguments specified.\n",
" :return: ``True`` if the YAML file conforms to the schema, ``False`` otherwise.\n",
"\n"
]
@@ -1322,7 +1324,7 @@
"id": "8c61a2d2-473c-45c6-9c6c-6c07fc5bf940",
"metadata": {},
"source": [
- "The schema file and config from above are passed to the respective `schema_file` and `config` parameters. Config file paths should be passed as a string or Path object. Files should be of YAML format, or parseable as YAML. Alternatively, a `YAMLConfig` object or a Python `dict` can be provided. `validate()` returns `True` if the config conforms to the JSON schema, and `False` otherwise. With a logger initialized, details about any validation errors are reported.\n",
+ "The schema file and config from above are passed to the respective `schema_file` and `config_path` parameters. Config file paths should be passed as a string or Path object. Files should be of YAML format, or parseable as YAML. Alternatively, a `YAMLConfig` object or a Python `dict` can be provided. `validate()` returns `True` if the config conforms to the JSON schema, and `False` otherwise. With a logger initialized, details about any validation errors are reported.\n",
""
]
},
@@ -1353,7 +1355,7 @@
"source": [
"config.validate(\n",
" schema_file='fixtures/config/validate.jsonschema',\n",
- " config='fixtures/config/get-config.yaml'\n",
+ " config_path='fixtures/config/get-config.yaml'\n",
")"
]
},
@@ -1362,7 +1364,7 @@
"id": "8d151205-4a95-4b46-aa1d-30ec30d96e88",
"metadata": {},
"source": [
- "The `config` argument also accepts a dictionary. In the next example, validation errors exist, and the logger reports the number of errors found along with their locations and details.\n",
+ "A mutually-exclusive alternative to the `config_path` argument, the `config_data` argument commonly accepts a `dict` object, but can also validate configs based on `bool`, `float`, `int`, `list`, or `str` values. In the next example, validation errors exist, and the logger reports the number of errors found along with their locations and details.\n",
""
]
},
@@ -1395,7 +1397,7 @@
"source": [
"config.validate(\n",
" schema_file='fixtures/config/validate.jsonschema',\n",
- " config={'greeting':'Hello', 'recipient':47}\n",
+ " config_data={'greeting':'Hello', 'recipient':47}\n",
")"
]
},
@@ -1470,10 +1472,10 @@
" | ----------------------------------------------------------------------\n",
" | Methods inherited from uwtools.config.formats.base.Config:\n",
" |\n",
- " | __repr__(self) -> str\n",
+ " | __repr__(self) -> 'str'\n",
" | Return the string representation of a Config object.\n",
" |\n",
- " | compare_config(self, dict1: dict, dict2: Optional[dict] = None, header: Optional[bool] = True) -> bool\n",
+ " | compare_config(self, dict1: 'dict', dict2: 'Optional[dict]' = None, header: 'Optional[bool]' = True) -> 'bool'\n",
" | Compare two config dictionaries.\n",
" |\n",
" | Assumes a section/key/value structure.\n",
@@ -1482,10 +1484,10 @@
" | :param dict2: The second dictionary (default: this config).\n",
" | :return: True if the configs are identical, False otherwise.\n",
" |\n",
- " | dereference(self, context: Optional[dict] = None) -> None\n",
+ " | dereference(self, context: 'Optional[dict]' = None) -> 'Config'\n",
" | Render as much Jinja2 syntax as possible.\n",
" |\n",
- " | update_from(self, src: Union[dict, collections.UserDict]) -> None\n",
+ " | update_from(self, src: 'Union[dict, UserDict]') -> 'None'\n",
" | Update a config.\n",
" |\n",
" | :param src: The dictionary with new data to use.\n",
diff --git a/notebooks/template.ipynb b/notebooks/template.ipynb
index 26890bc7e..2d30db496 100644
--- a/notebooks/template.ipynb
+++ b/notebooks/template.ipynb
@@ -52,7 +52,7 @@
"text": [
"Help on function render in module uwtools.api.template:\n",
"\n",
- "render(values_src: Union[dict, str, pathlib.Path, NoneType] = None, values_format: Optional[str] = None, input_file: Union[str, pathlib.Path, NoneType] = None, output_file: Union[str, pathlib.Path, NoneType] = None, overrides: Optional[dict[str, str]] = None, env: bool = False, searchpath: Optional[list[str]] = None, values_needed: bool = False, dry_run: bool = False, stdin_ok: bool = False) -> str\n",
+ "render(values_src: Union[dict, pathlib.Path, str, NoneType] = None, values_format: Optional[str] = None, input_file: Union[str, pathlib.Path, NoneType] = None, output_file: Union[str, pathlib.Path, NoneType] = None, overrides: Optional[dict[str, str]] = None, env: bool = False, searchpath: Optional[list[str]] = None, values_needed: bool = False, dry_run: bool = False, stdin_ok: bool = False) -> str\n",
" Render a Jinja2 template to a file, based on specified values.\n",
"\n",
" Primary values used to render the template are taken from the specified file. The format of the\n",
@@ -314,7 +314,7 @@
"text": [
"Help on function render_to_str in module uwtools.api.template:\n",
"\n",
- "render_to_str(values_src: Union[dict, str, pathlib.Path, NoneType] = None, values_format: Optional[str] = None, input_file: Union[str, pathlib.Path, NoneType] = None, overrides: Optional[dict[str, str]] = None, env: bool = False, searchpath: Optional[list[str]] = None, values_needed: bool = False, dry_run: bool = False) -> str\n",
+ "render_to_str(values_src: Union[dict, pathlib.Path, str, NoneType] = None, values_format: Optional[str] = None, input_file: Union[str, pathlib.Path, NoneType] = None, overrides: Optional[dict[str, str]] = None, env: bool = False, searchpath: Optional[list[str]] = None, values_needed: bool = False, dry_run: bool = False) -> str\n",
" Render a Jinja2 template to a string, based on specified values.\n",
"\n",
" See ``render()`` for details on arguments, etc.\n",
diff --git a/recipe/meta.json b/recipe/meta.json
index b56cc1342..c6c597cb0 100644
--- a/recipe/meta.json
+++ b/recipe/meta.json
@@ -36,5 +36,5 @@
"requests =2.32.*"
]
},
- "version": "2.5.0"
+ "version": "2.6.0"
}
diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py
index 5df802939..bf327ce2a 100644
--- a/src/uwtools/api/config.py
+++ b/src/uwtools/api/config.py
@@ -6,7 +6,7 @@
from pathlib import Path
from typing import Optional, Union
-from uwtools.config.formats.base import Config as _Config
+from uwtools.config.formats.base import Config
from uwtools.config.formats.fieldtable import FieldTableConfig
from uwtools.config.formats.ini import INIConfig
from uwtools.config.formats.nml import NMLConfig
@@ -15,6 +15,8 @@
from uwtools.config.support import YAMLKey
from uwtools.config.tools import compare_configs as _compare
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
from uwtools.config.validator import validate_external as _validate_external
from uwtools.exceptions import UWConfigError
from uwtools.utils.api import ensure_data_source as _ensure_data_source
@@ -111,9 +113,9 @@ def get_yaml_config(
def realize(
- input_config: Optional[Union[_Config, Path, dict, str]] = None,
+ input_config: Optional[Union[Config, Path, dict, str]] = None,
input_format: Optional[str] = None,
- update_config: Optional[Union[_Config, Path, dict, 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,
@@ -143,9 +145,9 @@ def realize(
def realize_to_dict( # pylint: disable=unused-argument
- input_config: Optional[Union[dict, _Config, Path, str]] = None,
+ input_config: Optional[Union[dict, Config, Path, str]] = None,
input_format: Optional[str] = None,
- update_config: Optional[Union[dict, _Config, Path, str]] = None,
+ update_config: Optional[Union[dict, Config, Path, str]] = None,
update_format: Optional[str] = None,
key_path: Optional[list[YAMLKey]] = None,
values_needed: bool = False,
@@ -163,25 +165,32 @@ def realize_to_dict( # pylint: disable=unused-argument
def validate(
schema_file: Union[Path, str],
- config: Optional[Union[dict, YAMLConfig, Path, str]] = None,
+ config_data: Optional[ConfigDataT] = None,
+ config_path: Optional[ConfigPathT] = None,
stdin_ok: bool = False,
) -> bool:
"""
Check whether the specified config conforms to the specified JSON Schema spec.
- If no config is specified, ``stdin`` is read and will be parsed as YAML and then validated. A
- ``dict`` or a YAMLConfig instance may also be provided for validation.
+ Specify at most one of config_data or config_path. If no config is specified, ``stdin`` is read
+ and will be parsed as YAML and then validated.
:param schema_file: The JSON Schema file to use for validation.
- :param config: The config to validate.
+ :param config_data: A config to validate.
+ :param config_path: A path to a file containing a config to validate.
:param stdin_ok: OK to read from ``stdin``?
+ :raises: TypeError if both config_* arguments specified.
:return: ``True`` if the YAML file conforms to the schema, ``False`` otherwise.
"""
+ _validate_check_config(config_data, config_path)
+ if config_data is None:
+ config_path = _ensure_data_source(_str2path(config_path), stdin_ok)
try:
_validate_external(
schema_file=_str2path(schema_file),
desc="config",
- config=_ensure_data_source(_str2path(config), stdin_ok),
+ config_data=config_data,
+ config_path=config_path,
)
except UWConfigError:
return False
@@ -252,6 +261,7 @@ def validate(
).strip()
__all__ = [
+ "Config",
"FieldTableConfig",
"INIConfig",
"NMLConfig",
diff --git a/src/uwtools/cli.py b/src/uwtools/cli.py
index 84c162f1c..cec57a408 100644
--- a/src/uwtools/cli.py
+++ b/src/uwtools/cli.py
@@ -252,7 +252,7 @@ def _dispatch_config_validate(args: Args) -> bool:
"""
return uwtools.api.config.validate(
schema_file=args[STR.schemafile],
- config=args[STR.infile],
+ config_path=args[STR.infile],
stdin_ok=True,
)
diff --git a/src/uwtools/config/formats/base.py b/src/uwtools/config/formats/base.py
index ff3f18671..5f91a28bf 100644
--- a/src/uwtools/config/formats/base.py
+++ b/src/uwtools/config/formats/base.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import difflib
import os
import re
@@ -23,7 +25,7 @@ class Config(ABC, UserDict):
several configuration-file formats.
"""
- def __init__(self, config: Optional[Union[dict, str, Path]] = None) -> None:
+ def __init__(self, config: Optional[Union[dict, str, Config, Path]] = None) -> None:
"""
:param config: Config file to load (None => read from stdin), or initial dict.
"""
@@ -31,6 +33,9 @@ def __init__(self, config: Optional[Union[dict, str, Path]] = None) -> None:
if isinstance(config, dict):
self._config_file = None
self.update(config)
+ elif isinstance(config, Config):
+ self._config_file = config._config_file
+ self.update(config.data)
else:
self._config_file = str2path(config) if config else None
self.data = self._load(self._config_file)
@@ -216,7 +221,7 @@ def config_file(self) -> Optional[Path]:
"""
return self._config_file
- def dereference(self, context: Optional[dict] = None) -> None:
+ def dereference(self, context: Optional[dict] = None) -> Config:
"""
Render as much Jinja2 syntax as possible.
"""
@@ -234,6 +239,7 @@ def logstate(state: str) -> None:
break
self.data = new
logstate("final")
+ return self
@abstractmethod
def dump(self, path: Optional[Path]) -> None:
diff --git a/src/uwtools/config/validator.py b/src/uwtools/config/validator.py
index 201197aae..0e8910ded 100644
--- a/src/uwtools/config/validator.py
+++ b/src/uwtools/config/validator.py
@@ -19,6 +19,10 @@
# Public functions
+JSONValueT = Union[bool, dict, float, int, list, str]
+ConfigDataT = Union[JSONValueT, YAMLConfig]
+ConfigPathT = Union[str, Path]
+
def bundle(schema: dict, keys: Optional[list] = None) -> dict:
"""
@@ -57,7 +61,7 @@ def internal_schema_file(schema_name: str) -> Path:
return resource_path("jsonschema") / f"{schema_name}.jsonschema"
-def validate(schema: dict, desc: str, config: dict) -> bool:
+def validate(schema: dict, desc: str, config: JSONValueT) -> bool:
"""
Report any errors arising from validation of the given config against the given JSON Schema.
@@ -77,56 +81,85 @@ def validate(schema: dict, desc: str, config: dict) -> bool:
return not bool(errors)
+def validate_check_config(
+ config_data: Optional[ConfigDataT] = None, config_path: Optional[ConfigPathT] = None
+) -> None:
+ """
+ Enforce mutual exclusivity of config_* arguments.
+
+ :param config_data: A config to validate.
+ :param config_path: A path to a file containing a config to validate.
+ :raises: TypeError if both config_* arguments specified.
+ """
+ if config_data is not None and config_path is not None:
+ raise TypeError("Specify at most one of config_data, config_path")
+
+
def validate_internal(
- schema_name: str, desc: str, config: Optional[Union[dict, YAMLConfig, Path]] = None
+ schema_name: str,
+ desc: str,
+ config_data: Optional[ConfigDataT] = None,
+ config_path: Optional[ConfigPathT] = None,
) -> None:
"""
Validate a config against a uwtools-internal schema.
+ Specify at most one of config_data or config_path. If no config is specified, ``stdin`` is read
+ and will be parsed as YAML and then validated.
+
:param schema_name: Name of uwtools schema to validate the config against.
:param desc: A description of the config being validated, for logging.
- :param config: The config to validate.
- :raises: UWConfigError if config fails validation.
+ :param config_data: A config to validate.
+ :param config_path: A path to a file containing a config to validate.
+ :raises: TypeError if both config_* arguments specified.
"""
+ validate_check_config(config_data, config_path)
log.info("Validating config against internal schema: %s", schema_name)
- validate_external(config=config, schema_file=internal_schema_file(schema_name), desc=desc)
+ validate_external(
+ schema_file=internal_schema_file(schema_name),
+ desc=desc,
+ config_data=config_data,
+ config_path=config_path,
+ )
def validate_external(
- schema_file: Path, desc: str, config: Optional[Union[dict, YAMLConfig, Path]] = None
+ schema_file: Path,
+ desc: str,
+ config_data: Optional[ConfigDataT] = None,
+ config_path: Optional[ConfigPathT] = None,
) -> None:
"""
Validate a YAML config against the JSON Schema in the given schema file.
+ Specify at most one of config_data or config_path. If no config is specified, ``stdin`` is read
+ and will be parsed as YAML and then validated.
+
:param schema_file: The JSON Schema file to use for validation.
:param desc: A description of the config being validated, for logging.
- :param config: The config to validate.
- :raises: UWConfigError if config fails validation.
- """
+ :param config_data: A config to validate.
+ :param config_path: A path to a file containing a config to validate.
+ :raises: TypeError if both config_* arguments specified.
+ """
+ validate_check_config(config_data, config_path)
+ config: JSONValueT
+ if config_data is None:
+ config = YAMLConfig(config_path).dereference().data
+ elif isinstance(config_data, YAMLConfig):
+ config = config_data.data
+ else:
+ config = config_data
if not str(schema_file).startswith(str(resource_path())):
log.debug("Using schema file: %s", schema_file)
with open(schema_file, "r", encoding="utf-8") as f:
schema = json.load(f)
- cfgobj = _prep_config(config)
- if not validate(schema=schema, desc=desc, config=cfgobj.data):
+ if not validate(schema=schema, desc=desc, config=config):
raise UWConfigError("YAML validation errors")
# Private functions
-def _prep_config(config: Union[dict, YAMLConfig, Optional[Path]]) -> YAMLConfig:
- """
- Ensure a dereferenced YAMLConfig object for various input types.
-
- :param config: The config to validate.
- :return: A dereferenced YAMLConfig object based on the input config.
- """
- cfgobj = config if isinstance(config, YAMLConfig) else YAMLConfig(config)
- cfgobj.dereference()
- return cfgobj
-
-
@cache
def _registry() -> Registry:
"""
@@ -143,7 +176,7 @@ def retrieve(uri: str) -> Resource:
return Registry(retrieve=retrieve) # type: ignore
-def _validation_errors(config: Union[dict, list], schema: dict) -> list[ValidationError]:
+def _validation_errors(config: JSONValueT, schema: dict) -> list[ValidationError]:
"""
Identify schema-validation errors.
diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py
index 28bc8badd..35fd3c7d3 100644
--- a/src/uwtools/drivers/driver.py
+++ b/src/uwtools/drivers/driver.py
@@ -46,7 +46,7 @@ def __init__(
self,
cycle: Optional[datetime] = None,
leadtime: Optional[timedelta] = None,
- config: Optional[Union[dict, str, YAMLConfig, Path]] = None,
+ config: Optional[Union[dict, str, Path, YAMLConfig]] = None,
dry_run: bool = False,
key_path: Optional[list[YAMLKey]] = None,
schema_file: Optional[Path] = None,
@@ -233,7 +233,7 @@ def _validate(self) -> None:
:raises: UWConfigError if config fails validation.
"""
kwargs: dict = {
- "config": self._config_intermediate,
+ "config_data": self._config_intermediate,
"desc": "%s config" % self.driver_name(),
}
if self.schema_file:
@@ -536,7 +536,9 @@ def _validate(self) -> None:
"""
Assets._validate(self)
validate_internal(
- schema_name=STR.platform, desc="platform config", config=self._config_intermediate
+ schema_name=STR.platform,
+ desc="platform config",
+ config_data=self._config_intermediate,
)
def _write_runscript(self, path: Path, envvars: Optional[dict[str, str]] = None) -> None:
diff --git a/src/uwtools/fs.py b/src/uwtools/fs.py
index f7d214a49..41809117a 100644
--- a/src/uwtools/fs.py
+++ b/src/uwtools/fs.py
@@ -116,7 +116,15 @@ def _validate(self) -> None:
:raises: UWConfigError if config fails validation.
"""
- validate_internal(schema_name=self._schema, desc="fs config", config=self._config)
+ config_data, config_path = (
+ (self._config, None) if isinstance(self._config, dict) else (None, self._config)
+ )
+ validate_internal(
+ schema_name=self._schema,
+ desc="fs config",
+ config_data=config_data,
+ config_path=config_path,
+ )
class FileStager(Stager):
diff --git a/src/uwtools/resources/info.json b/src/uwtools/resources/info.json
index bc21a6c20..ffa9be4b9 100644
--- a/src/uwtools/resources/info.json
+++ b/src/uwtools/resources/info.json
@@ -1,4 +1,4 @@
{
"buildnum": "0",
- "version": "2.5.0"
+ "version": "2.6.0"
}
diff --git a/src/uwtools/rocoto.py b/src/uwtools/rocoto.py
index 180d66aaf..e98c94589 100644
--- a/src/uwtools/rocoto.py
+++ b/src/uwtools/rocoto.py
@@ -350,15 +350,20 @@ def _add_workflow_tasks(self, e: _Element, config: dict) -> None:
tag, name = self._tag_name(key)
{STR.metatask: self._add_metatask, STR.task: self._add_task}[tag](e, subconfig, name)
- def _config_validate(self, config: Union[dict, YAMLConfig, Optional[Path]]) -> None:
+ def _config_validate(self, config: Optional[Union[dict, Path, YAMLConfig]] = None) -> None:
"""
Validate the given YAML config.
:param config: YAMLConfig object or path to YAML file (None => read stdin).
:raises: UWConfigError if config fails validation.
"""
- schema_file = resource_path("jsonschema/rocoto.jsonschema")
- validate_yaml(schema_file=schema_file, desc="Rocoto config", config=config)
+ config_data, config_path = (None, config) if isinstance(config, Path) else (config, None)
+ validate_yaml(
+ schema_file=resource_path("jsonschema/rocoto.jsonschema"),
+ desc="Rocoto config",
+ config_data=config_data,
+ config_path=config_path,
+ )
@property
def _doctype(self) -> Optional[str]:
diff --git a/src/uwtools/tests/api/test_config.py b/src/uwtools/tests/api/test_config.py
index 71796c814..101024dda 100644
--- a/src/uwtools/tests/api/test_config.py
+++ b/src/uwtools/tests/api/test_config.py
@@ -117,9 +117,9 @@ def test_realize_update_config_none():
)
-@mark.parametrize("cfg", [{"foo": "bar"}, YAMLConfig(config={})])
-def test_validate(cfg):
- kwargs: dict = {"schema_file": "schema-file", "config": cfg}
+@mark.parametrize("cfg", [{"foo": "bar"}, YAMLConfig(config={"foo": "bar"})])
+def test_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
_validate_external.side_effect = UWConfigError()
@@ -127,18 +127,19 @@ def test_validate(cfg):
_validate_external.assert_called_with(
schema_file=Path(kwargs["schema_file"]),
desc="config",
- config=kwargs["config"],
+ config_data=kwargs["config_data"],
+ config_path=None,
)
@mark.parametrize("cast", (str, Path))
-def test_validate_config_file(cast, tmp_path):
+def test_validate_config_path(cast, tmp_path):
cfg = tmp_path / "config.yaml"
with open(cfg, "w", encoding="utf-8") as f:
yaml.dump({}, f)
- kwargs: dict = {"schema_file": "schema-file", "config": cast(cfg)}
+ kwargs: dict = {"schema_file": "schema-file", "config_path": cast(cfg)}
with patch.object(config, "_validate_external", return_value=True) as _validate_external:
assert config.validate(**kwargs)
_validate_external.assert_called_once_with(
- schema_file=Path(kwargs["schema_file"]), desc="config", config=cfg
+ schema_file=Path(kwargs["schema_file"]), desc="config", config_data=None, config_path=cfg
)
diff --git a/src/uwtools/tests/config/formats/test_base.py b/src/uwtools/tests/config/formats/test_base.py
index 4bb3baaad..f71311ab2 100644
--- a/src/uwtools/tests/config/formats/test_base.py
+++ b/src/uwtools/tests/config/formats/test_base.py
@@ -194,7 +194,12 @@ def test_compare_config_ini(caplog, salad_base):
assert not logged(caplog, line)
-def test_config_file(config):
+def test_config_from_config(config):
+ assert config.config_file.name == "config.yaml"
+ assert ConcreteConfig(config).data == config.data
+
+
+def test_config_from_file(config):
assert config.config_file.name == "config.yaml"
assert config.config_file.is_file()
@@ -240,8 +245,8 @@ def test_dereference(tmp_path):
print(yaml, file=f)
config = YAMLConfig(path)
with patch.dict(os.environ, {"N": "999"}, clear=True):
- config.dereference()
- print(config["e"])
+ retval = config.dereference()
+ assert retval is config
assert config == {
"a": 44,
"b": {"c": 33},
diff --git a/src/uwtools/tests/config/test_validator.py b/src/uwtools/tests/config/test_validator.py
index 7eb035749..aec53d62c 100644
--- a/src/uwtools/tests/config/test_validator.py
+++ b/src/uwtools/tests/config/test_validator.py
@@ -4,13 +4,13 @@
"""
import json
import logging
+from functools import partial
from pathlib import Path
from textwrap import dedent
from typing import Any
from unittest.mock import Mock, patch
-import yaml
-from pytest import fixture, raises
+from pytest import fixture, mark, raises
from uwtools.config import validator
from uwtools.config.formats.yaml import YAMLConfig
@@ -125,7 +125,7 @@ def write_as_json(data: dict[str, Any], path: Path) -> Path:
# Test functions
-def test_bundle(caplog):
+def test_config_validator_bundle(caplog):
log.setLevel(logging.DEBUG)
schema = {"fruit": {"$ref": "urn:uwtools:a"}, "flowers": None}
with patch.object(validator, "_registry") as _registry:
@@ -150,16 +150,30 @@ def test_bundle(caplog):
assert logged(caplog, msg)
-def test_internal_schema_file():
+def test_config_validator_internal_schema_file():
with patch.object(validator, "resource_path", return_value=Path("/foo/bar")):
assert validator.internal_schema_file("baz") == Path("/foo/bar/baz.jsonschema")
-def test_validate(config, schema):
+@mark.parametrize(
+ "schema,config",
+ [
+ ({"type": "boolean"}, True), # bool
+ ({"type": "number"}, 3.14), # float
+ ({"type": "integer"}, 42), # int
+ ({"type": "array"}, [1, 2, 3]), # list
+ ({"type": "string"}, "foo"), # str
+ ],
+)
+def test_config_validator_validate_alt_types(schema, config):
+ assert validator.validate(schema=schema, desc="test", config=config) is True
+
+
+def test_config_validator_validate_dict(config, schema):
assert validator.validate(schema=schema, desc="test", config=config)
-def test_validate_fail_bad_enum_val(caplog, config, schema):
+def test_config_validator_validate_fail_bad_enum_val(caplog, config, schema):
log.setLevel(logging.INFO)
config["color"] = "yellow" # invalid enum value
assert not validator.validate(schema=schema, desc="test", config=config)
@@ -167,7 +181,7 @@ def test_validate_fail_bad_enum_val(caplog, config, schema):
assert any(x for x in caplog.records if "'yellow' is not one of" in x.message)
-def test_validate_fail_bad_number_val(caplog, config, schema):
+def test_config_validator_validate_fail_bad_number_val(caplog, config, schema):
log.setLevel(logging.INFO)
config["number"] = "string" # invalid number value
assert not validator.validate(schema=schema, desc="test", config=config)
@@ -175,7 +189,7 @@ def test_validate_fail_bad_number_val(caplog, config, schema):
assert any(x for x in caplog.records if "'string' is not of type 'number'" in x.message)
-def test_validate_fail_top_level(caplog):
+def test_config_validator_validate_fail_top_level(caplog):
schema = {
"additionalProperties": False,
"properties": {"n": {"type": "integer"}},
@@ -194,49 +208,43 @@ def test_validate_fail_top_level(caplog):
assert all(line in caplog.messages for line in dedent(expected).strip().split("\n"))
-def test_validate_internal_no(caplog, schema_file):
+@mark.parametrize(
+ "config_data,config_path", [(True, None), (None, True), (None, None), (True, True)]
+)
+def test_config_validator_validate_check_config(config_data, config_path):
+ f = partial(validator.validate_check_config, config_data, config_path)
+ if config_data is None or config_path is None:
+ assert f() is None
+ else:
+ with raises(TypeError) as e:
+ f()
+ assert str(e.value) == "Specify at most one of config_data, config_path"
+
+
+def test_config_validator_validate_internal_no(caplog, schema_file):
with patch.object(validator, "resource_path", return_value=schema_file.parent):
with raises(UWConfigError) as e:
- validator.validate_internal(schema_name="a", desc="test", config={"color": "orange"})
+ validator.validate_internal(
+ schema_name="a", desc="test", config_data={"color": "orange"}
+ )
assert logged(caplog, "Error at color:")
assert logged(caplog, " 'orange' is not one of ['blue', 'red']")
assert str(e.value) == "YAML validation errors"
-def test_validate_internal_ok(schema_file):
+def test_config_validator_validate_internal_ok(schema_file):
with patch.object(validator, "resource_path", return_value=schema_file.parent):
- validator.validate_internal(schema_name="a", desc="test", config={"color": "blue"})
+ validator.validate_internal(schema_name="a", desc="test", config_data={"color": "blue"})
-def test_validate_external(assets, config, schema):
+def test_config_validator_validate_external(assets, config, schema):
schema_file, _, cfgobj = assets
with patch.object(validator, "validate") as validate:
- validator.validate_external(schema_file=schema_file, desc="test", config=cfgobj)
+ validator.validate_external(schema_file=schema_file, desc="test", config_data=cfgobj)
validate.assert_called_once_with(schema=schema, desc="test", config=config)
-def test_prep_config_cfgobj(prep_config_dict):
- cfgobj = validator._prep_config(config=YAMLConfig(config=prep_config_dict))
- assert isinstance(cfgobj, YAMLConfig)
- assert cfgobj == {"roses": "red", "color": "red"}
-
-
-def test__prep_config_dict(prep_config_dict):
- cfgobj = validator._prep_config(config=prep_config_dict)
- assert isinstance(cfgobj, YAMLConfig)
- assert cfgobj == {"roses": "red", "color": "red"}
-
-
-def test__prep_config_file(prep_config_dict, tmp_path):
- path = tmp_path / "config.yaml"
- with open(path, "w", encoding="utf-8") as f:
- yaml.dump(prep_config_dict, f)
- cfgobj = validator._prep_config(config=path)
- assert isinstance(cfgobj, YAMLConfig)
- assert cfgobj == {"roses": "red", "color": "red"}
-
-
-def test__registry(tmp_path):
+def test_config_validator__registry(tmp_path):
validator._registry.cache_clear()
d = {"foo": "bar"}
path = tmp_path / "foo-bar.jsonschema"
@@ -248,15 +256,15 @@ def test__registry(tmp_path):
resource_path.assert_called_once_with("jsonschema/foo-bar.jsonschema")
-def test__validation_errors_bad_enum_value(config, schema):
+def test_config_validator__validation_errors_bad_enum_value(config, schema):
config["color"] = "yellow"
assert len(validator._validation_errors(config, schema)) == 1
-def test__validation_errors_bad_number_value(config, schema):
+def test_config_validator__validation_errors_bad_number_value(config, schema):
config["number"] = "string"
assert len(validator._validation_errors(config, schema)) == 1
-def test__validation_errors_pass(config, schema):
+def test_config_validator__validation_errors_pass(config, schema):
assert not validator._validation_errors(config, schema)
diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py
index 9f33c2961..1553d699d 100644
--- a/src/uwtools/tests/drivers/test_driver.py
+++ b/src/uwtools/tests/drivers/test_driver.py
@@ -312,7 +312,7 @@ def test_Assets__validate_internal(assetsobj):
assert validate_internal.call_args_list[0].kwargs == {
"schema_name": "concrete",
"desc": "concrete config",
- "config": assetsobj.config_full,
+ "config_data": assetsobj.config_full,
}
@@ -324,7 +324,7 @@ def test_Assets__validate_external(config):
assert validate_external.call_args_list[0].kwargs == {
"schema_file": schema_file,
"desc": "concrete config",
- "config": assetsobj.config_full,
+ "config_data": assetsobj.config_full,
}
@@ -607,7 +607,7 @@ def test_Driver__validate_external(config):
assert validate_external.call_args_list[0].kwargs == {
"schema_file": schema_file,
"desc": "concrete config",
- "config": assetsobj.config_full,
+ "config_data": assetsobj.config_full,
}
@@ -618,12 +618,12 @@ def test_Driver__validate_internal(assetsobj):
assert validate_internal.call_args_list[0].kwargs == {
"schema_name": "concrete",
"desc": "concrete config",
- "config": assetsobj.config_full,
+ "config_data": assetsobj.config_full,
}
assert validate_internal.call_args_list[1].kwargs == {
"schema_name": "platform",
"desc": "platform config",
- "config": assetsobj.config_full,
+ "config_data": assetsobj.config_full,
}
diff --git a/src/uwtools/tests/test_cli.py b/src/uwtools/tests/test_cli.py
index 6a5248ff5..6fa40ac9a 100644
--- a/src/uwtools/tests/test_cli.py
+++ b/src/uwtools/tests/test_cli.py
@@ -371,7 +371,8 @@ def test__dispatch_config_validate_config_obj():
cli._dispatch_config_validate(_dispatch_config_validate_args)
_validate_external_args = {
STR.schemafile: _dispatch_config_validate_args[STR.schemafile],
- STR.config: _dispatch_config_validate_args[STR.infile],
+ "config_data": None,
+ "config_path": _dispatch_config_validate_args[STR.infile],
}
_validate_external.assert_called_once_with(**_validate_external_args, desc="config")