From 69a9974419277e8be18e282dda47081fd71a1616 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 00:38:45 +0000 Subject: [PATCH 01/15] WIP --- src/uwtools/api/config.py | 7 ++----- src/uwtools/config/formats/base.py | 5 ++++- src/uwtools/config/validator.py | 32 +++++++++++++++--------------- src/uwtools/rocoto.py | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 5df802939..6c2003253 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -15,6 +15,7 @@ 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 ConfigT 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 @@ -161,11 +162,7 @@ def realize_to_dict( # pylint: disable=unused-argument return realize(**{**locals(), "output_file": Path(os.devnull), "output_format": _FORMAT.yaml}) -def validate( - schema_file: Union[Path, str], - config: Optional[Union[dict, YAMLConfig, Path, str]] = None, - stdin_ok: bool = False, -) -> bool: +def validate(schema_file: Union[Path, str], config: ConfigT = None, stdin_ok: bool = False) -> bool: """ Check whether the specified config conforms to the specified JSON Schema spec. diff --git a/src/uwtools/config/formats/base.py b/src/uwtools/config/formats/base.py index 37b736226..b7dc49100 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 @@ -217,7 +219,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. """ @@ -235,6 +237,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..c3df7ee05 100644 --- a/src/uwtools/config/validator.py +++ b/src/uwtools/config/validator.py @@ -19,6 +19,9 @@ # Public functions +JSONValueT = Union[bool, dict, float, int, list, str, None] +ConfigT = Optional[Union[JSONValueT, Path, YAMLConfig]] + def bundle(schema: dict, keys: Optional[list] = None) -> dict: """ @@ -57,7 +60,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,9 +80,7 @@ def validate(schema: dict, desc: str, config: dict) -> bool: return not bool(errors) -def validate_internal( - schema_name: str, desc: str, config: Optional[Union[dict, YAMLConfig, Path]] = None -) -> None: +def validate_internal(schema_name: str, desc: str, config: ConfigT = None) -> None: """ Validate a config against a uwtools-internal schema. @@ -92,9 +93,7 @@ def validate_internal( validate_external(config=config, schema_file=internal_schema_file(schema_name), desc=desc) -def validate_external( - schema_file: Path, desc: str, config: Optional[Union[dict, YAMLConfig, Path]] = None -) -> None: +def validate_external(schema_file: Path, desc: str, config: ConfigT = None) -> None: """ Validate a YAML config against the JSON Schema in the given schema file. @@ -107,24 +106,25 @@ def validate_external( 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=_prep_config(config)): raise UWConfigError("YAML validation errors") # Private functions -def _prep_config(config: Union[dict, YAMLConfig, Optional[Path]]) -> YAMLConfig: +def _prep_config(config: ConfigT) -> JSONValueT: """ - Ensure a dereferenced YAMLConfig object for various input types. + Ensure a dereferenced JSON-compatible value for various input types. :param config: The config to validate. - :return: A dereferenced YAMLConfig object based on the input config. + :return: A JSON-compatible value, dereferenced if necessary. """ - cfgobj = config if isinstance(config, YAMLConfig) else YAMLConfig(config) - cfgobj.dereference() - return cfgobj + if isinstance(config, (dict, Path)): + config = YAMLConfig(config) + if isinstance(config, YAMLConfig): + return config.dereference().data + return config @cache @@ -143,7 +143,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/rocoto.py b/src/uwtools/rocoto.py index 180d66aaf..7cc709135 100644 --- a/src/uwtools/rocoto.py +++ b/src/uwtools/rocoto.py @@ -350,7 +350,7 @@ 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: """ Validate the given YAML config. From b83fd1301dc176a364f7281017d9b2a1205379eb Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 00:40:32 +0000 Subject: [PATCH 02/15] Tests @ 99% --- src/uwtools/tests/config/test_validator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uwtools/tests/config/test_validator.py b/src/uwtools/tests/config/test_validator.py index 7eb035749..a85bdb670 100644 --- a/src/uwtools/tests/config/test_validator.py +++ b/src/uwtools/tests/config/test_validator.py @@ -217,13 +217,13 @@ def test_validate_external(assets, config, schema): def test_prep_config_cfgobj(prep_config_dict): cfgobj = validator._prep_config(config=YAMLConfig(config=prep_config_dict)) - assert isinstance(cfgobj, YAMLConfig) + assert isinstance(cfgobj, dict) 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 isinstance(cfgobj, dict) assert cfgobj == {"roses": "red", "color": "red"} @@ -232,7 +232,7 @@ def test__prep_config_file(prep_config_dict, tmp_path): 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 isinstance(cfgobj, dict) assert cfgobj == {"roses": "red", "color": "red"} From ddc3e64decc58b4b48d701de1bfc79c8f1c568b2 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 15:40:20 +0000 Subject: [PATCH 03/15] Update unit test for dereference() return value --- src/uwtools/tests/config/formats/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uwtools/tests/config/formats/test_base.py b/src/uwtools/tests/config/formats/test_base.py index c3557d3cc..a6ec4fdd7 100644 --- a/src/uwtools/tests/config/formats/test_base.py +++ b/src/uwtools/tests/config/formats/test_base.py @@ -231,8 +231,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}, From d505a3b1b6cf1d7320596745e2ee6a35a153e778 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 15:58:26 +0000 Subject: [PATCH 04/15] Unit tests @ 100% --- src/uwtools/tests/config/test_validator.py | 39 ++++++++++++---------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/uwtools/tests/config/test_validator.py b/src/uwtools/tests/config/test_validator.py index a85bdb670..651cba5e0 100644 --- a/src/uwtools/tests/config/test_validator.py +++ b/src/uwtools/tests/config/test_validator.py @@ -10,7 +10,7 @@ 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,16 @@ 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): +def test_config_validator_validate(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 +167,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 +175,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,7 +194,7 @@ 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): +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"}) @@ -203,31 +203,31 @@ def test_validate_internal_no(caplog, schema_file): 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"}) -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) validate.assert_called_once_with(schema=schema, desc="test", config=config) -def test_prep_config_cfgobj(prep_config_dict): +def test_config_validator_prep_config_cfgobj(prep_config_dict): cfgobj = validator._prep_config(config=YAMLConfig(config=prep_config_dict)) assert isinstance(cfgobj, dict) assert cfgobj == {"roses": "red", "color": "red"} -def test__prep_config_dict(prep_config_dict): +def test_config_validator__prep_config_dict(prep_config_dict): cfgobj = validator._prep_config(config=prep_config_dict) assert isinstance(cfgobj, dict) assert cfgobj == {"roses": "red", "color": "red"} -def test__prep_config_file(prep_config_dict, tmp_path): +def test_config_validator__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) @@ -236,7 +236,12 @@ def test__prep_config_file(prep_config_dict, tmp_path): assert cfgobj == {"roses": "red", "color": "red"} -def test__registry(tmp_path): +@mark.parametrize("val", [True, 3.14, 42, [1, 2, 3], "foo", None]) +def test_config_validator__prep_config_other(val): + assert validator._prep_config(config=val) == val + + +def test_config_validator__registry(tmp_path): validator._registry.cache_clear() d = {"foo": "bar"} path = tmp_path / "foo-bar.jsonschema" @@ -248,15 +253,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) From bdbde9ab8079018b799b3f883c2b7cf4219d36c9 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 21:02:32 +0000 Subject: [PATCH 05/15] Tests @ 99% --- src/uwtools/api/config.py | 20 ++++-- src/uwtools/cli.py | 2 +- src/uwtools/config/formats/base.py | 5 +- src/uwtools/config/validator.py | 77 +++++++++++++++------- src/uwtools/drivers/driver.py | 8 ++- src/uwtools/fs.py | 10 ++- src/uwtools/rocoto.py | 11 +++- src/uwtools/tests/api/test_config.py | 15 +++-- src/uwtools/tests/config/test_validator.py | 37 ++--------- src/uwtools/tests/drivers/test_driver.py | 10 +-- src/uwtools/tests/test_cli.py | 3 +- 11 files changed, 116 insertions(+), 82 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 6c2003253..1e15ae92d 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -15,7 +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 ConfigT +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 @@ -162,7 +163,12 @@ def realize_to_dict( # pylint: disable=unused-argument return realize(**{**locals(), "output_file": Path(os.devnull), "output_format": _FORMAT.yaml}) -def validate(schema_file: Union[Path, str], config: ConfigT = None, stdin_ok: bool = False) -> bool: +def validate( + schema_file: Union[Path, str], + 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. @@ -170,15 +176,21 @@ def validate(schema_file: Union[Path, str], config: ConfigT = None, stdin_ok: bo ``dict`` or a YAMLConfig instance may also be provided for validation. :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 config_* arguments are not set appropriately. :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 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 b7dc49100..e5b1926b0 100644 --- a/src/uwtools/config/formats/base.py +++ b/src/uwtools/config/formats/base.py @@ -26,7 +26,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. """ @@ -34,6 +34,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 = None + self.update(config.data) else: self._config_file = str2path(config) if config else None self.data = self._load(self._config_file) diff --git a/src/uwtools/config/validator.py b/src/uwtools/config/validator.py index c3df7ee05..e1aff264d 100644 --- a/src/uwtools/config/validator.py +++ b/src/uwtools/config/validator.py @@ -19,8 +19,9 @@ # Public functions -JSONValueT = Union[bool, dict, float, int, list, str, None] -ConfigT = Optional[Union[JSONValueT, Path, YAMLConfig]] +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: @@ -80,53 +81,79 @@ def validate(schema: dict, desc: str, config: JSONValueT) -> bool: return not bool(errors) -def validate_internal(schema_name: str, desc: str, config: ConfigT = None) -> None: +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_data: Optional[ConfigDataT] = None, + config_path: Optional[ConfigPathT] = None, +) -> None: """ Validate a config against a uwtools-internal schema. :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 config_* arguments are not set appropriately. """ + 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( + config_data=config_data, + config_path=config_path, + schema_file=internal_schema_file(schema_name), + desc=desc, + ) -def validate_external(schema_file: Path, desc: str, config: ConfigT = None) -> None: +def validate_external( + 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. :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 config_* arguments are not set appropriately. + """ + 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) - if not validate(schema=schema, desc=desc, config=_prep_config(config)): + if not validate(schema=schema, desc=desc, config=config): raise UWConfigError("YAML validation errors") # Private functions -def _prep_config(config: ConfigT) -> JSONValueT: - """ - Ensure a dereferenced JSON-compatible value for various input types. - - :param config: The config to validate. - :return: A JSON-compatible value, dereferenced if necessary. - """ - if isinstance(config, (dict, Path)): - config = YAMLConfig(config) - if isinstance(config, YAMLConfig): - return config.dereference().data - return config - - @cache def _registry() -> Registry: """ 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/rocoto.py b/src/uwtools/rocoto.py index 7cc709135..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: Optional[Union[dict, Path, YAMLConfig]]) -> 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/test_validator.py b/src/uwtools/tests/config/test_validator.py index 651cba5e0..2e1d6fb9d 100644 --- a/src/uwtools/tests/config/test_validator.py +++ b/src/uwtools/tests/config/test_validator.py @@ -9,8 +9,7 @@ from typing import Any from unittest.mock import Mock, patch -import yaml -from pytest import fixture, mark, raises +from pytest import fixture, raises from uwtools.config import validator from uwtools.config.formats.yaml import YAMLConfig @@ -197,7 +196,9 @@ def test_config_validator_validate_fail_top_level(caplog): 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" @@ -205,42 +206,16 @@ def test_config_validator_validate_internal_no(caplog, 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_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_config_validator_prep_config_cfgobj(prep_config_dict): - cfgobj = validator._prep_config(config=YAMLConfig(config=prep_config_dict)) - assert isinstance(cfgobj, dict) - assert cfgobj == {"roses": "red", "color": "red"} - - -def test_config_validator__prep_config_dict(prep_config_dict): - cfgobj = validator._prep_config(config=prep_config_dict) - assert isinstance(cfgobj, dict) - assert cfgobj == {"roses": "red", "color": "red"} - - -def test_config_validator__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, dict) - assert cfgobj == {"roses": "red", "color": "red"} - - -@mark.parametrize("val", [True, 3.14, 42, [1, 2, 3], "foo", None]) -def test_config_validator__prep_config_other(val): - assert validator._prep_config(config=val) == val - - def test_config_validator__registry(tmp_path): validator._registry.cache_clear() d = {"foo": "bar"} 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") From 97ffcb28a7214a11fa35674e0439d1387fd26af8 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 21:14:37 +0000 Subject: [PATCH 06/15] Work on tests --- src/uwtools/tests/config/test_validator.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/uwtools/tests/config/test_validator.py b/src/uwtools/tests/config/test_validator.py index 2e1d6fb9d..1c7f26004 100644 --- a/src/uwtools/tests/config/test_validator.py +++ b/src/uwtools/tests/config/test_validator.py @@ -4,12 +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 -from pytest import fixture, raises +from pytest import fixture, mark, raises from uwtools.config import validator from uwtools.config.formats.yaml import YAMLConfig @@ -193,6 +194,19 @@ def test_config_validator_validate_fail_top_level(caplog): assert all(line in caplog.messages for line in dedent(expected).strip().split("\n")) +@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: From 1ff074bfc22301a218e0a1988e8d0d9fd3ccd88c Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 21:18:07 +0000 Subject: [PATCH 07/15] Tests @ 100% --- src/uwtools/tests/config/formats/test_base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/uwtools/tests/config/formats/test_base.py b/src/uwtools/tests/config/formats/test_base.py index a5b43fe83..e16ffefc0 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 isinstance(config, ConcreteConfig) + 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() From 55a20479e70b7d0a048211150388a3119c5ba79e Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 21:19:37 +0000 Subject: [PATCH 08/15] Update test --- src/uwtools/config/formats/base.py | 2 +- src/uwtools/tests/config/formats/test_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uwtools/config/formats/base.py b/src/uwtools/config/formats/base.py index 521c64582..5f91a28bf 100644 --- a/src/uwtools/config/formats/base.py +++ b/src/uwtools/config/formats/base.py @@ -34,7 +34,7 @@ def __init__(self, config: Optional[Union[dict, str, Config, Path]] = None) -> N self._config_file = None self.update(config) elif isinstance(config, Config): - self._config_file = None + self._config_file = config._config_file self.update(config.data) else: self._config_file = str2path(config) if config else None diff --git a/src/uwtools/tests/config/formats/test_base.py b/src/uwtools/tests/config/formats/test_base.py index e16ffefc0..f71311ab2 100644 --- a/src/uwtools/tests/config/formats/test_base.py +++ b/src/uwtools/tests/config/formats/test_base.py @@ -195,7 +195,7 @@ def test_compare_config_ini(caplog, salad_base): def test_config_from_config(config): - assert isinstance(config, ConcreteConfig) + assert config.config_file.name == "config.yaml" assert ConcreteConfig(config).data == config.data From c9e56a96b7bafbf92838e1b8d3b617c53effaf02 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 21:42:00 +0000 Subject: [PATCH 09/15] Add tests --- src/uwtools/tests/config/test_validator.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/uwtools/tests/config/test_validator.py b/src/uwtools/tests/config/test_validator.py index 1c7f26004..aec53d62c 100644 --- a/src/uwtools/tests/config/test_validator.py +++ b/src/uwtools/tests/config/test_validator.py @@ -155,7 +155,21 @@ def test_config_validator_internal_schema_file(): assert validator.internal_schema_file("baz") == Path("/foo/bar/baz.jsonschema") -def test_config_validator_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) From 63097f1574e97a44befaa1de03774e8b70d55f72 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Tue, 7 Jan 2025 21:43:56 +0000 Subject: [PATCH 10/15] Set version to 2.6.0 --- recipe/meta.json | 2 +- src/uwtools/resources/info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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" } From 6bb82bc935bb86b1f8bb36c5e5baa292a2c2a52e Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 8 Jan 2025 00:41:14 +0000 Subject: [PATCH 11/15] Work on docs --- docs/conf.py | 6 ++++-- docs/sections/user_guide/api/config.rst | 2 +- src/uwtools/api/config.py | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8e0e67f94..eb4bed22b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,8 +20,10 @@ html_logo = os.path.join("static", "ufs.png") 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/src/uwtools/api/config.py b/src/uwtools/api/config.py index 1e15ae92d..9d9098816 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 @@ -113,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, @@ -145,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, @@ -173,7 +173,7 @@ def validate( 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. + ``dict`` or a ``YAMLConfig`` instance may also be provided for validation. :param schema_file: The JSON Schema file to use for validation. :param config_data: A config to validate. @@ -261,6 +261,7 @@ def validate( ).strip() __all__ = [ + "Config", "FieldTableConfig", "INIConfig", "NMLConfig", From cb42359870af5f337b2ebca78536b2456df668b5 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 8 Jan 2025 00:56:21 +0000 Subject: [PATCH 12/15] Work on docs --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index eb4bed22b..ca0351aa2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,8 +20,8 @@ html_logo = os.path.join("static", "ufs.png") html_static_path = ["static"] html_theme = "sphinx_rtd_theme" +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} nitpick_ignore = [ - ("py:class", "Path"), ("py:class", "f90nml.Namelist"), ] numfig = True From 7acd96986c9ac89ce7a0782123487d3d2f6391f6 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 8 Jan 2025 01:09:14 +0000 Subject: [PATCH 13/15] Work on docs --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index ca0351aa2..c1ec605c0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,7 @@ html_theme = "sphinx_rtd_theme" intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} nitpick_ignore = [ + ("py:class", "Path"), ("py:class", "f90nml.Namelist"), ] numfig = True From deb723c95d2295f962329074e232526397bd3221 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 8 Jan 2025 01:42:05 +0000 Subject: [PATCH 14/15] Updates --- src/uwtools/api/config.py | 6 +++--- src/uwtools/config/validator.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/uwtools/api/config.py b/src/uwtools/api/config.py index 9d9098816..bf327ce2a 100644 --- a/src/uwtools/api/config.py +++ b/src/uwtools/api/config.py @@ -172,14 +172,14 @@ def validate( """ 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_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 config_* arguments are not set appropriately. + :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) diff --git a/src/uwtools/config/validator.py b/src/uwtools/config/validator.py index e1aff264d..0e8910ded 100644 --- a/src/uwtools/config/validator.py +++ b/src/uwtools/config/validator.py @@ -104,19 +104,22 @@ def validate_internal( """ 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_data: A config to validate. :param config_path: A path to a file containing a config to validate. - :raises: TypeError if config_* arguments are not set appropriately. + :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_data=config_data, - config_path=config_path, schema_file=internal_schema_file(schema_name), desc=desc, + config_data=config_data, + config_path=config_path, ) @@ -129,11 +132,14 @@ def validate_external( """ 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_data: A config to validate. :param config_path: A path to a file containing a config to validate. - :raises: TypeError if config_* arguments are not set appropriately. + :raises: TypeError if both config_* arguments specified. """ validate_check_config(config_data, config_path) config: JSONValueT From 585039dbfffdda9c7d7a210f9af31284a931711f Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Wed, 8 Jan 2025 01:58:24 +0000 Subject: [PATCH 15/15] Update notebooks --- notebooks/config.ipynb | 26 ++++++++++++++------------ notebooks/template.ipynb | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) 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",