diff --git a/src/uwtools/api/driver.py b/src/uwtools/api/driver.py index 0e99c920b..7e1713983 100644 --- a/src/uwtools/api/driver.py +++ b/src/uwtools/api/driver.py @@ -2,7 +2,6 @@ API access to the ``uwtools`` driver base classes. """ -import sys from datetime import datetime, timedelta from importlib import import_module from importlib.util import module_from_spec, spec_from_file_location @@ -11,6 +10,16 @@ from types import ModuleType from typing import Optional, Type, Union +from uwtools.drivers.driver import ( # pylint: disable=unused-import + Assets, + AssetsCycleBased, + AssetsCycleLeadtimeBased, + AssetsTimeInvariant, + Driver, + DriverCycleBased, + DriverCycleLeadtimeBased, + DriverTimeInvariant, +) from uwtools.drivers.support import graph from uwtools.drivers.support import tasks as _tasks from uwtools.logging import log @@ -91,25 +100,6 @@ def tasks(module: str, classname: str) -> dict[str, str]: return _tasks(class_) -_CLASSNAMES = [ - "Assets", - "AssetsCycleBased", - "AssetsCycleLeadtimeBased", - "AssetsTimeInvariant", - "Driver", - "DriverCycleBased", - "DriverCycleLeadtimeBased", - "DriverTimeInvariant", -] - - -def _add_classes(): - m = import_module("uwtools.drivers.driver") - for classname in _CLASSNAMES: - setattr(sys.modules[__name__], classname, getattr(m, classname)) - __all__.append(classname) - - def _get_driver_class(module: Union[Path, str], classname: str) -> Optional[Type]: """ Returns the driver class. @@ -161,5 +151,19 @@ def _get_driver_module_implicit(module: str) -> Optional[ModuleType]: return None -__all__: list[str] = [graph.__name__] -_add_classes() +__all__ = [ + getattr(x, "__name__") + for x in ( + Assets, + AssetsCycleBased, + AssetsCycleLeadtimeBased, + AssetsTimeInvariant, + Driver, + DriverCycleBased, + DriverCycleLeadtimeBased, + DriverTimeInvariant, + execute, + graph, + tasks, + ) +] diff --git a/src/uwtools/config/tools.py b/src/uwtools/config/tools.py index 26d62bc14..dfda5b7cb 100644 --- a/src/uwtools/config/tools.py +++ b/src/uwtools/config/tools.py @@ -110,6 +110,29 @@ def realize_config( return input_obj.data +def walk_key_path(config: dict, key_path: list[str]) -> tuple[dict, str]: + """ + Navigate to the sub-config at the end of the path of given keys. + + :param config: A config. + :param key_path: Path of keys to subsection of config file. + :return: The sub-config and a string representation of the key path. + """ + keys = [] + pathstr = "" + for key in key_path: + keys.append(key) + pathstr = " -> ".join(keys) + try: + subconfig = config[key] + except KeyError as e: + raise log_and_error(f"Bad config path: {pathstr}") from e + if not isinstance(subconfig, dict): + raise log_and_error(f"Value at {pathstr} must be a dictionary") + config = subconfig + return config, pathstr + + # Private functions @@ -138,25 +161,16 @@ def _ensure_format( def _print_config_section(config: dict, key_path: list[str]) -> None: """ - Descends into the config via the given keys, then prints the contents of the located subtree as - key=value pairs, one per line. + Prints the contents of the located subtree as key=value pairs, one per line. + + :param config: A config. + :param key_path: Path of keys to subsection of config file. """ - keys = [] - current_path = "" - for section in key_path: - keys.append(section) - current_path = " -> ".join(keys) - try: - subconfig = config[section] - except KeyError as e: - raise log_and_error(f"Bad config path: {current_path}") from e - if not isinstance(subconfig, dict): - raise log_and_error(f"Value at {current_path} must be a dictionary") - config = subconfig + config, pathstr = walk_key_path(config, key_path) output_lines = [] for key, value in config.items(): if type(value) not in (bool, float, int, str): - raise log_and_error(f"Non-scalar value {value} found at {current_path}") + raise log_and_error(f"Non-scalar value {value} found at {pathstr}") output_lines.append(f"{key}={value}") print("\n".join(sorted(output_lines))) diff --git a/src/uwtools/drivers/cdeps.py b/src/uwtools/drivers/cdeps.py index fc76a347a..b93778eeb 100644 --- a/src/uwtools/drivers/cdeps.py +++ b/src/uwtools/drivers/cdeps.py @@ -42,7 +42,6 @@ def atm_nml(self): path = self._rundir / fn yield asset(path, path.is_file) yield None - path.parent.mkdir(parents=True, exist_ok=True) self._model_namelist_file("atm_in", path) @task @@ -54,7 +53,7 @@ def atm_stream(self): yield self._taskname(f"stream file {fn}") path = self._rundir / fn yield asset(path, path.is_file) - template_file = self._driver_config["atm_streams"]["template_file"] + template_file = self.config["atm_streams"]["template_file"] yield file(path=Path(template_file)) self._model_stream_file("atm_streams", path, template_file) @@ -79,7 +78,6 @@ def ocn_nml(self): path = self._rundir / fn yield asset(path, path.is_file) yield None - path.parent.mkdir(parents=True, exist_ok=True) self._model_namelist_file("ocn_in", path) @task @@ -91,7 +89,7 @@ def ocn_stream(self): yield self._taskname(f"stream file {fn}") path = self._rundir / fn yield asset(path, path.is_file) - template_file = self._driver_config["ocn_streams"]["template_file"] + template_file = self.config["ocn_streams"]["template_file"] yield file(path=Path(template_file)) self._model_stream_file("ocn_streams", path, template_file) @@ -112,7 +110,7 @@ def _model_namelist_file(self, group: str, path: Path) -> None: :param path: Path to write namelist to. """ self._create_user_updated_config( - config_class=NMLConfig, config_values=self._driver_config[group], path=path + config_class=NMLConfig, config_values=self.config[group], path=path ) def _model_stream_file(self, group: str, path: Path, template_file: str) -> None: @@ -126,7 +124,7 @@ def _model_stream_file(self, group: str, path: Path, template_file: str) -> None _render( input_file=Path(template_file), output_file=path, - values_src=self._driver_config[group], + values_src=self.config[group], ) diff --git a/src/uwtools/drivers/chgres_cube.py b/src/uwtools/drivers/chgres_cube.py index eacc8998b..b4ef57c6b 100644 --- a/src/uwtools/drivers/chgres_cube.py +++ b/src/uwtools/drivers/chgres_cube.py @@ -30,7 +30,7 @@ def namelist_file(self): path = self._rundir / fn yield asset(path, path.is_file) input_files = [] - namelist = self._driver_config[STR.namelist] + namelist = self.config[STR.namelist] if base_file := namelist.get(STR.basefile): input_files.append(base_file) if update_values := namelist.get(STR.updatevalues): @@ -84,7 +84,7 @@ def runscript(self): yield None envvars = { "KMP_AFFINITY": "scatter", - "OMP_NUM_THREADS": self._driver_config.get(STR.execution, {}).get(STR.threads, 1), + "OMP_NUM_THREADS": self.config.get(STR.execution, {}).get(STR.threads, 1), "OMP_STACKSIZE": "1024m", } self._write_runscript(path=path, envvars=envvars) diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py index c3a549eac..dbaac93aa 100644 --- a/src/uwtools/drivers/driver.py +++ b/src/uwtools/drivers/driver.py @@ -18,11 +18,13 @@ from uwtools.config.formats.base import Config from uwtools.config.formats.yaml import YAMLConfig +from uwtools.config.tools import walk_key_path from uwtools.config.validator import get_schema_file, validate, validate_external, validate_internal from uwtools.exceptions import UWConfigError from uwtools.logging import log from uwtools.scheduler import JobScheduler from uwtools.strings import STR +from uwtools.utils.file import writable from uwtools.utils.processing import execute # NB: Class docstrings are programmatically defined. @@ -43,19 +45,25 @@ def __init__( schema_file: Optional[Path] = None, controller: Optional[str] = None, ) -> None: - self._config_full = config if isinstance(config, YAMLConfig) else YAMLConfig(config=config) - self._config_full.dereference( + config = config if isinstance(config, YAMLConfig) else YAMLConfig(config=config) + config.dereference( context={ **({STR.cycle: cycle} if cycle else {}), **({STR.leadtime: leadtime} if leadtime is not None else {}), - **self._config_full.data, + **config.data, } ) - self._config = self._config_full - for key in key_path or []: - self._config = self._config[key] + # The _config_full attribute points to the config block that includes the driver-specific + # block and any surrounding context, including e.g. platform: for some drivers. + self._config_full, _ = walk_key_path(config.data, key_path or []) + # The _config attribute points to the driver-specific config block. It is supplemented with + # config data from the controller block, if specified. + try: + self._config = self._config_full[self._driver_name] + except KeyError as e: + raise UWConfigError("Required '%s' block missing in config" % self._driver_name) from e if controller: - self._config[self._driver_name][STR.rundir] = self._config_full[controller][STR.rundir] + self._config[STR.rundir] = self._config_full[controller][STR.rundir] self._validate(schema_file) dryrun(enable=dry_run) @@ -66,9 +74,7 @@ def __repr__(self) -> str: h, r = divmod(self._leadtime.total_seconds(), 3600) m, s = divmod(r, 60) leadtime = "%02d:%02d:%02d" % (h, m, s) - return " ".join( - filter(None, [str(self), cycle, leadtime, "in", self._driver_config[STR.rundir]]) - ) + return " ".join(filter(None, [str(self), cycle, leadtime, "in", self.config[STR.rundir]])) def __str__(self) -> str: return self._driver_name @@ -76,17 +82,16 @@ def __str__(self) -> str: @property def config(self) -> dict: """ - Return a copy of the driver config. + A copy of the driver-specific config block. """ - return deepcopy(self._driver_config) + return deepcopy(self._config) @property def config_full(self) -> dict: """ - Return a copy of the full input config. + A copy of the driver-specific config block's parent block. """ - full_config: dict = self._config.data - return deepcopy(full_config) + return deepcopy(self._config_full) # Workflow tasks @@ -123,24 +128,11 @@ def _create_user_updated_config( config = user_values dump = partial(config_class.dump_dict, config, path) if validate(schema=schema or {"type": "object"}, config=config): - path.parent.mkdir(parents=True, exist_ok=True) dump() log.debug(f"Wrote config to {path}") else: log.debug(f"Failed to validate {path}") - @property - def _driver_config(self) -> dict[str, Any]: - """ - Returns the config block specific to this driver. - """ - name = self._driver_name - try: - driver_config: dict[str, Any] = self._config[name] - return driver_config - except KeyError as e: - raise UWConfigError("Required '%s' block missing in config" % name) from e - @property @abstractmethod def _driver_name(self) -> str: @@ -158,7 +150,7 @@ def _namelist_schema( :param schema_keys: Keys leading to the namelist-validating (sub)schema. """ schema: dict = {"type": "object"} - nmlcfg = self._driver_config + nmlcfg = self.config for config_key in config_keys or [STR.namelist]: nmlcfg = nmlcfg[config_key] if nmlcfg.get(STR.validate, True): @@ -181,7 +173,7 @@ def _rundir(self) -> Path: """ The path to the component's run directory. """ - return Path(self._driver_config[STR.rundir]) + return Path(self.config[STR.rundir]) def _taskname(self, suffix: str) -> str: """ @@ -206,9 +198,11 @@ def _validate(self, schema_file: Optional[Path] = None) -> None: :raises: UWConfigError if config fails validation. """ if schema_file: - validate_external(schema_file=schema_file, config=self._config) + validate_external(schema_file=schema_file, config=self.config_full) else: - validate_internal(schema_name=self._driver_name.replace("_", "-"), config=self._config) + validate_internal( + schema_name=self._driver_name.replace("_", "-"), config=self.config_full + ) class AssetsCycleBased(Assets): @@ -235,6 +229,13 @@ def __init__( ) self._cycle = cycle + @property + def cycle(self): + """ + The cycle. + """ + return self._cycle + class AssetsCycleLeadtimeBased(Assets): """ @@ -263,6 +264,20 @@ def __init__( self._cycle = cycle self._leadtime = leadtime + @property + def cycle(self): + """ + The cycle. + """ + return self._cycle + + @property + def leadtime(self): + """ + The leadtime. + """ + return self._leadtime + class AssetsTimeInvariant(Assets): """ @@ -313,9 +328,7 @@ def __init__( ) self._batch = batch if controller: - self._config[self._driver_name][STR.execution] = self._config_full[controller][ - STR.execution - ] + self._config[STR.execution] = self._config_full[controller][STR.execution] # Workflow tasks @@ -376,17 +389,17 @@ def _run_resources(self) -> dict[str, Any]: Returns platform configuration data. """ try: - platform = self._config[STR.platform] + platform = self.config_full[STR.platform] except KeyError as e: raise UWConfigError("Required 'platform' block missing in config") from e - threads = self._driver_config.get(STR.execution, {}).get(STR.threads) + threads = self.config.get(STR.execution, {}).get(STR.threads) return { STR.account: platform[STR.account], STR.rundir: self._rundir, STR.scheduler: platform[STR.scheduler], STR.stdout: "%s.out" % self._runscript_path.name, # config may override **({STR.threads: threads} if threads else {}), - **self._driver_config.get(STR.execution, {}).get(STR.batchargs, {}), + **self.config.get(STR.execution, {}).get(STR.batchargs, {}), } @property @@ -394,7 +407,7 @@ def _runcmd(self) -> str: """ Returns the full command-line component invocation. """ - execution = self._driver_config.get(STR.execution, {}) + execution = self.config.get(STR.execution, {}) mpiargs = execution.get(STR.mpiargs, []) components = [ execution.get(STR.mpicmd), # MPI run program @@ -463,24 +476,28 @@ def _scheduler(self) -> JobScheduler: def _validate(self, schema_file: Optional[Path] = None) -> None: """ Perform all necessary schema validation. + + :param schema_file: The JSON Schema file to use for validation. + :raises: UWConfigError if config fails validation. """ if schema_file: - validate_external(schema_file=schema_file, config=self._config) + validate_external(schema_file=schema_file, config=self.config_full) else: - validate_internal(schema_name=self._driver_name.replace("_", "-"), config=self._config) - validate_internal(schema_name=STR.platform, config=self._config) + validate_internal( + schema_name=self._driver_name.replace("_", "-"), config=self.config_full + ) + validate_internal(schema_name=STR.platform, config=self.config_full) def _write_runscript(self, path: Path, envvars: Optional[dict[str, str]] = None) -> None: """ Write the runscript. """ - path.parent.mkdir(parents=True, exist_ok=True) envvars = envvars or {} - threads = self._driver_config.get(STR.execution, {}).get(STR.threads) + threads = self.config.get(STR.execution, {}).get(STR.threads) if threads and "OMP_NUM_THREADS" not in envvars: raise UWConfigError("Config specified threads but driver does not set OMP_NUM_THREADS") rs = self._runscript( - envcmds=self._driver_config.get(STR.execution, {}).get(STR.envcmds, []), + envcmds=self.config.get(STR.execution, {}).get(STR.envcmds, []), envvars=envvars, execution=[ "time %s" % self._runcmd, @@ -488,7 +505,7 @@ def _write_runscript(self, path: Path, envvars: Optional[dict[str, str]] = None) ], scheduler=self._scheduler if self._batch else None, ) - with open(path, "w", encoding="utf-8") as f: + with writable(path) as f: print(rs, file=f) os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) @@ -519,6 +536,13 @@ def __init__( ) self._cycle = cycle + @property + def cycle(self): + """ + The cycle. + """ + return self._cycle + class DriverCycleLeadtimeBased(Driver): """ @@ -549,6 +573,20 @@ def __init__( self._cycle = cycle self._leadtime = leadtime + @property + def cycle(self): + """ + The cycle. + """ + return self._cycle + + @property + def leadtime(self): + """ + The leadtime. + """ + return self._leadtime + class DriverTimeInvariant(Driver): """ diff --git a/src/uwtools/drivers/esg_grid.py b/src/uwtools/drivers/esg_grid.py index e3f86b037..ba446eaa0 100644 --- a/src/uwtools/drivers/esg_grid.py +++ b/src/uwtools/drivers/esg_grid.py @@ -29,11 +29,11 @@ def namelist_file(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - base_file = self._driver_config[STR.namelist].get(STR.basefile) + base_file = self.config[STR.namelist].get(STR.basefile) yield file(Path(base_file)) if base_file else None self._create_user_updated_config( config_class=NMLConfig, - config_values=self._driver_config[STR.namelist], + config_values=self.config[STR.namelist], path=path, schema=self._namelist_schema(schema_keys=["$defs", "namelist_content"]), ) diff --git a/src/uwtools/drivers/filter_topo.py b/src/uwtools/drivers/filter_topo.py index 18a12b7c8..4bb58d0ac 100644 --- a/src/uwtools/drivers/filter_topo.py +++ b/src/uwtools/drivers/filter_topo.py @@ -25,8 +25,8 @@ def input_grid_file(self): """ The input grid file. """ - src = Path(self._driver_config["config"]["input_grid_file"]) - dst = Path(self._driver_config[STR.rundir]) / src.name + src = Path(self.config["config"]["input_grid_file"]) + dst = Path(self.config[STR.rundir], src.name) yield self._taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) @@ -43,7 +43,7 @@ def namelist_file(self): yield None self._create_user_updated_config( config_class=NMLConfig, - config_values=self._driver_config[STR.namelist], + config_values=self.config[STR.namelist], path=path, schema=self._namelist_schema(), ) diff --git a/src/uwtools/drivers/fv3.py b/src/uwtools/drivers/fv3.py index d7ab65c7e..dde218ddf 100644 --- a/src/uwtools/drivers/fv3.py +++ b/src/uwtools/drivers/fv3.py @@ -29,12 +29,12 @@ def boundary_files(self): Lateral boundary-condition files. """ yield self._taskname("lateral boundary-condition files") - lbcs = self._driver_config["lateral_boundary_conditions"] + lbcs = self.config["lateral_boundary_conditions"] offset = abs(lbcs["offset"]) - endhour = self._driver_config["length"] + offset + 1 + endhour = self.config["length"] + offset + 1 interval = lbcs["interval_hours"] symlinks = {} - for n in [7] if self._driver_config["domain"] == "global" else range(1, 7): + for n in [7] if self.config["domain"] == "global" else range(1, 7): for boundary_hour in range(offset, endhour, interval): target = Path(lbcs["path"].format(tile=n, forecast_hour=boundary_hour)) linkname = ( @@ -53,7 +53,7 @@ def diag_table(self): path = self._rundir / fn yield asset(path, path.is_file) yield None - if src := self._driver_config.get(fn): + if src := self.config.get(fn): path.parent.mkdir(parents=True, exist_ok=True) copy(src=src, dst=path) else: @@ -68,7 +68,7 @@ def field_table(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - yield filecopy(src=Path(self._driver_config["field_table"][STR.basefile]), dst=path) + yield filecopy(src=Path(self.config["field_table"][STR.basefile]), dst=path) @tasks def files_copied(self): @@ -78,7 +78,7 @@ def files_copied(self): yield self._taskname("files copied") yield [ filecopy(src=Path(src), dst=self._rundir / dst) - for dst, src in self._driver_config.get("files_to_copy", {}).items() + for dst, src in self.config.get("files_to_copy", {}).items() ] @tasks @@ -89,7 +89,7 @@ def files_linked(self): yield self._taskname("files linked") yield [ symlink(target=Path(target), linkname=self._rundir / linkname) - for linkname, target in self._driver_config.get("files_to_link", {}).items() + for linkname, target in self.config.get("files_to_link", {}).items() ] @task @@ -101,11 +101,11 @@ def model_configure(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - base_file = self._driver_config["model_configure"].get(STR.basefile) + base_file = self.config["model_configure"].get(STR.basefile) yield file(Path(base_file)) if base_file else None self._create_user_updated_config( config_class=YAMLConfig, - config_values=self._driver_config["model_configure"], + config_values=self.config["model_configure"], path=path, ) @@ -118,11 +118,11 @@ def namelist_file(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - base_file = self._driver_config[STR.namelist].get(STR.basefile) + base_file = self.config[STR.namelist].get(STR.basefile) yield file(Path(base_file)) if base_file else None self._create_user_updated_config( config_class=NMLConfig, - config_values=self._driver_config[STR.namelist], + config_values=self.config[STR.namelist], path=path, schema=self._namelist_schema(), ) @@ -143,7 +143,7 @@ def provisioned_rundir(self): self.restart_directory(), self.runscript(), ] - if self._driver_config["domain"] == "regional": + if self.config["domain"] == "regional": required.append(self.boundary_files()) yield required @@ -171,7 +171,7 @@ def runscript(self): "ESMF_RUNTIME_COMPLIANCECHECK": "OFF:depth=4", "KMP_AFFINITY": "scatter", "MPI_TYPE_DEPTH": 20, - "OMP_NUM_THREADS": self._driver_config.get(STR.execution, {}).get(STR.threads, 1), + "OMP_NUM_THREADS": self.config.get(STR.execution, {}).get(STR.threads, 1), "OMP_STACKSIZE": "512m", } self._write_runscript(path=path, envvars=envvars) diff --git a/src/uwtools/drivers/global_equiv_resol.py b/src/uwtools/drivers/global_equiv_resol.py index 59eff0149..5f99051a1 100644 --- a/src/uwtools/drivers/global_equiv_resol.py +++ b/src/uwtools/drivers/global_equiv_resol.py @@ -23,7 +23,7 @@ def input_file(self): """ Ensure the specified input grid file exists. """ - path = Path(self._driver_config["input_grid_file"]) + path = Path(self.config["input_grid_file"]) yield self._taskname(path.name) yield asset(path, path.is_file) @@ -52,8 +52,8 @@ def _runcmd(self): """ Returns the full command-line component invocation. """ - executable = self._driver_config[STR.execution][STR.executable] - input_file_path = self._driver_config["input_grid_file"] + executable = self.config[STR.execution][STR.executable] + input_file_path = self.config["input_grid_file"] return f"{executable} {input_file_path}" diff --git a/src/uwtools/drivers/ioda.py b/src/uwtools/drivers/ioda.py index 2be609535..f169c15df 100644 --- a/src/uwtools/drivers/ioda.py +++ b/src/uwtools/drivers/ioda.py @@ -50,7 +50,7 @@ def _runcmd(self) -> str: """ Returns the full command-line component invocation. """ - executable = self._driver_config[STR.execution][STR.executable] + executable = self.config[STR.execution][STR.executable] jedi_config = str(self._rundir / self._config_fn) return " ".join([executable, jedi_config]) diff --git a/src/uwtools/drivers/jedi.py b/src/uwtools/drivers/jedi.py index 4b7e57210..caf1bb9e6 100644 --- a/src/uwtools/drivers/jedi.py +++ b/src/uwtools/drivers/jedi.py @@ -43,11 +43,11 @@ def validate_only(self): yield taskname a = asset(None, lambda: False) yield a - executable = file(Path(self._driver_config[STR.execution][STR.executable])) + executable = file(Path(self.config[STR.execution][STR.executable])) config = self.configuration_file() yield [executable, config] cmd = "time %s --validate-only %s 2>&1" % (refs(executable), refs(config)) - if envcmds := self._driver_config[STR.execution].get(STR.envcmds): + if envcmds := self.config[STR.execution].get(STR.envcmds): cmd = " && ".join([*envcmds, cmd]) result = run(taskname, cmd) if result.success: @@ -75,7 +75,7 @@ def _runcmd(self) -> str: """ Returns the full command-line component invocation. """ - execution = self._driver_config[STR.execution] + execution = self.config[STR.execution] jedi_config = self._rundir / self._config_fn mpiargs = execution.get(STR.mpiargs, []) components = [ diff --git a/src/uwtools/drivers/jedi_base.py b/src/uwtools/drivers/jedi_base.py index 11d029177..c4fdfa4f2 100644 --- a/src/uwtools/drivers/jedi_base.py +++ b/src/uwtools/drivers/jedi_base.py @@ -29,11 +29,11 @@ def configuration_file(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - base_file = self._driver_config["configuration_file"].get(STR.basefile) + base_file = self.config["configuration_file"].get(STR.basefile) yield file(Path(base_file)) if base_file else None self._create_user_updated_config( config_class=YAMLConfig, - config_values=self._driver_config["configuration_file"], + config_values=self.config["configuration_file"], path=path, ) @@ -45,7 +45,7 @@ def files_copied(self): yield self._taskname("files copied") yield [ filecopy(src=Path(src), dst=self._rundir / dst) - for dst, src in self._driver_config.get("files_to_copy", {}).items() + for dst, src in self.config.get("files_to_copy", {}).items() ] @tasks @@ -56,7 +56,7 @@ def files_linked(self): yield self._taskname("files linked") yield [ symlink(target=Path(target), linkname=self._rundir / linkname) - for linkname, target in self._driver_config.get("files_to_link", {}).items() + for linkname, target in self.config.get("files_to_link", {}).items() ] @tasks diff --git a/src/uwtools/drivers/make_hgrid.py b/src/uwtools/drivers/make_hgrid.py index 821e39e40..b82dd4457 100644 --- a/src/uwtools/drivers/make_hgrid.py +++ b/src/uwtools/drivers/make_hgrid.py @@ -38,8 +38,8 @@ def _runcmd(self): """ Returns the full command-line component invocation. """ - executable = self._driver_config[STR.execution][STR.executable] - config = self._driver_config["config"] + executable = self.config[STR.execution][STR.executable] + config = self.config["config"] flags = [] for k, v in config.items(): if isinstance(v, bool): diff --git a/src/uwtools/drivers/make_solo_mosaic.py b/src/uwtools/drivers/make_solo_mosaic.py index 57abb6a3d..d3d6bdea7 100644 --- a/src/uwtools/drivers/make_solo_mosaic.py +++ b/src/uwtools/drivers/make_solo_mosaic.py @@ -38,8 +38,8 @@ def _runcmd(self): """ Returns the full command-line component invocation. """ - executable = self._driver_config[STR.execution][STR.executable] - flags = " ".join(f"--{k} {v}" for k, v in self._driver_config["config"].items()) + executable = self.config[STR.execution][STR.executable] + flags = " ".join(f"--{k} {v}" for k, v in self.config["config"].items()) return f"{executable} {flags}" def _taskname(self, suffix: str) -> str: diff --git a/src/uwtools/drivers/mpas.py b/src/uwtools/drivers/mpas.py index e99a5b1ce..506290093 100644 --- a/src/uwtools/drivers/mpas.py +++ b/src/uwtools/drivers/mpas.py @@ -27,15 +27,15 @@ def boundary_files(self): Boundary files. """ yield self._taskname("boundary files") - lbcs = self._driver_config["lateral_boundary_conditions"] - endhour = self._driver_config["length"] + lbcs = self.config["lateral_boundary_conditions"] + endhour = self.config["length"] interval = lbcs["interval_hours"] symlinks = {} for boundary_hour in range(0, endhour + 1, interval): file_date = self._cycle + timedelta(hours=boundary_hour) fn = f"lbc.{file_date.strftime('%Y-%m-%d_%H.%M.%S')}.nc" linkname = self._rundir / fn - symlinks[linkname] = Path(lbcs["path"]) / fn + symlinks[linkname] = Path(lbcs["path"], fn) yield [symlink(target=t, linkname=l) for l, t in symlinks.items()] @task @@ -46,11 +46,11 @@ def namelist_file(self): path = self._rundir / "namelist.atmosphere" yield self._taskname(str(path)) yield asset(path, path.is_file) - base_file = self._driver_config[STR.namelist].get(STR.basefile) + base_file = self.config[STR.namelist].get(STR.basefile) yield file(Path(base_file)) if base_file else None - duration = timedelta(hours=self._driver_config["length"]) + duration = timedelta(hours=self.config["length"]) str_duration = str(duration).replace(" days, ", "_") - namelist = self._driver_config[STR.namelist] + namelist = self.config[STR.namelist] update_values = namelist.get(STR.updatevalues, {}) update_values.setdefault("nhyd_model", {}).update( { diff --git a/src/uwtools/drivers/mpas_base.py b/src/uwtools/drivers/mpas_base.py index 851b29a08..41676b3af 100644 --- a/src/uwtools/drivers/mpas_base.py +++ b/src/uwtools/drivers/mpas_base.py @@ -35,7 +35,7 @@ def files_copied(self): yield self._taskname("files copied") yield [ filecopy(src=Path(src), dst=self._rundir / dst) - for dst, src in self._driver_config.get("files_to_copy", {}).items() + for dst, src in self.config.get("files_to_copy", {}).items() ] @tasks @@ -46,7 +46,7 @@ def files_linked(self): yield self._taskname("files linked") yield [ symlink(target=Path(target), linkname=self._rundir / linkname) - for linkname, target in self._driver_config.get("files_to_link", {}).items() + for linkname, target in self.config.get("files_to_link", {}).items() ] @task @@ -82,7 +82,7 @@ def streams_file(self): yield asset(path, path.is_file) yield None streams = Element("streams") - for k, v in self._driver_config["streams"].items(): + for k, v in self.config["streams"].items(): stream = SubElement(streams, "stream" if v["mutable"] else "immutable_stream") stream.set("name", k) for attr in ["type", "filename_template"]: diff --git a/src/uwtools/drivers/mpas_init.py b/src/uwtools/drivers/mpas_init.py index f7b7b7cc7..eeb4b7d29 100644 --- a/src/uwtools/drivers/mpas_init.py +++ b/src/uwtools/drivers/mpas_init.py @@ -27,7 +27,7 @@ def boundary_files(self): Boundary files. """ yield self._taskname("boundary files") - lbcs = self._driver_config["boundary_conditions"] + lbcs = self.config["boundary_conditions"] endhour = lbcs["length"] interval = lbcs["interval_hours"] symlinks = {} @@ -35,7 +35,7 @@ def boundary_files(self): for boundary_hour in range(0, endhour + 1, interval): file_date = self._cycle + timedelta(hours=boundary_hour) fn = f"FILE:{file_date.strftime('%Y-%m-%d_%H')}" - target = Path(boundary_filepath) / fn + target = Path(boundary_filepath, fn) linkname = self._rundir / fn symlinks[target] = linkname yield [symlink(target=t, linkname=l) for t, l in symlinks.items()] @@ -49,12 +49,10 @@ def namelist_file(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - base_file = self._driver_config[STR.namelist].get(STR.basefile) + base_file = self.config[STR.namelist].get(STR.basefile) yield file(Path(base_file)) if base_file else None - stop_time = self._cycle + timedelta( - hours=self._driver_config["boundary_conditions"]["length"] - ) - namelist = self._driver_config[STR.namelist] + stop_time = self._cycle + timedelta(hours=self.config["boundary_conditions"]["length"]) + namelist = self.config[STR.namelist] update_values = namelist.get(STR.updatevalues, {}) update_values.setdefault("nhyd_model", {}).update( { diff --git a/src/uwtools/drivers/orog_gsl.py b/src/uwtools/drivers/orog_gsl.py index 19b03feba..c25806a5f 100644 --- a/src/uwtools/drivers/orog_gsl.py +++ b/src/uwtools/drivers/orog_gsl.py @@ -25,10 +25,10 @@ def input_grid_file(self): The input grid file. """ fn = "C%s_grid.tile%s.halo%s.nc" % tuple( - self._driver_config["config"][k] for k in ["resolution", "tile", "halo"] + self.config["config"][k] for k in ["resolution", "tile", "halo"] ) - src = Path(self._driver_config["config"]["input_grid_file"]) - dst = Path(self._driver_config[STR.rundir]) / fn + src = Path(self.config["config"]["input_grid_file"]) + dst = Path(self.config[STR.rundir], fn) yield self._taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) @@ -52,8 +52,8 @@ def topo_data_2p5m(self): Global topographic data on 2.5-minute lat-lon grid. """ fn = "geo_em.d01.lat-lon.2.5m.HGT_M.nc" - src = Path(self._driver_config["config"]["topo_data_2p5m"]) - dst = Path(self._driver_config[STR.rundir]) / fn + src = Path(self.config["config"]["topo_data_2p5m"]) + dst = Path(self.config[STR.rundir], fn) yield self._taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) @@ -64,8 +64,8 @@ def topo_data_30s(self): Global topographic data on 30-second lat-lon grid. """ fn = "HGT.Beljaars_filtered.lat-lon.30s_res.nc" - src = Path(self._driver_config["config"]["topo_data_30s"]) - dst = Path(self._driver_config[STR.rundir]) / fn + src = Path(self.config["config"]["topo_data_30s"]) + dst = Path(self.config[STR.rundir], fn) yield self._taskname("Input grid") yield asset(dst, dst.is_file) yield symlink(target=src, linkname=dst) @@ -84,8 +84,8 @@ def _runcmd(self): """ Returns the full command-line component invocation. """ - inputs = [str(self._driver_config["config"][k]) for k in ("tile", "resolution", "halo")] - executable = self._driver_config[STR.execution][STR.executable] + inputs = [str(self.config["config"][k]) for k in ("tile", "resolution", "halo")] + executable = self.config[STR.execution][STR.executable] return "echo '%s' | %s" % ("\n".join(inputs), executable) diff --git a/src/uwtools/drivers/schism.py b/src/uwtools/drivers/schism.py index 9877c9239..6dab0523a 100644 --- a/src/uwtools/drivers/schism.py +++ b/src/uwtools/drivers/schism.py @@ -29,12 +29,12 @@ def namelist_file(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - template_file = Path(self._driver_config[STR.namelist]["template_file"]) + template_file = Path(self.config[STR.namelist]["template_file"]) yield file(path=template_file) render( input_file=template_file, output_file=path, - overrides=self._driver_config[STR.namelist]["template_values"], + overrides=self.config[STR.namelist]["template_values"], ) @tasks diff --git a/src/uwtools/drivers/sfc_climo_gen.py b/src/uwtools/drivers/sfc_climo_gen.py index 0a34928c8..ab2ea24ea 100644 --- a/src/uwtools/drivers/sfc_climo_gen.py +++ b/src/uwtools/drivers/sfc_climo_gen.py @@ -29,14 +29,14 @@ def namelist_file(self): yield self._taskname(f"namelist file {fn}") path = self._rundir / fn yield asset(path, path.is_file) - vals = self._driver_config[STR.namelist][STR.updatevalues]["config"] + vals = self.config[STR.namelist][STR.updatevalues]["config"] input_paths = [Path(v) for k, v in vals.items() if k.startswith("input_")] input_paths += [Path(vals["mosaic_file_mdl"])] - input_paths += [Path(vals["orog_dir_mdl"]) / fn for fn in vals["orog_files_mdl"]] + input_paths += [Path(vals["orog_dir_mdl"], fn) for fn in vals["orog_files_mdl"]] yield [file(input_path) for input_path in input_paths] self._create_user_updated_config( config_class=NMLConfig, - config_values=self._driver_config[STR.namelist], + config_values=self.config[STR.namelist], path=path, schema=self._namelist_schema(), ) diff --git a/src/uwtools/drivers/shave.py b/src/uwtools/drivers/shave.py index 39ffb50be..3437d8b98 100644 --- a/src/uwtools/drivers/shave.py +++ b/src/uwtools/drivers/shave.py @@ -38,8 +38,8 @@ def _runcmd(self): """ Returns the full command-line component invocation. """ - executable = self._driver_config[STR.execution][STR.executable] - config = self._driver_config["config"] + executable = self.config[STR.execution][STR.executable] + config = self.config["config"] input_file = config["input_grid_file"] output_file = input_file.replace(".nc", "_NH0.nc") flags = [config[key] for key in ["nx", "ny", "nh4", "input_grid_file"]] diff --git a/src/uwtools/drivers/ungrib.py b/src/uwtools/drivers/ungrib.py index ca2cbbcdb..938c34ac7 100644 --- a/src/uwtools/drivers/ungrib.py +++ b/src/uwtools/drivers/ungrib.py @@ -27,7 +27,7 @@ def gribfiles(self): Symlinks to all the GRIB files. """ yield self._taskname("GRIB files") - gfs_files = self._driver_config["gfs_files"] + gfs_files = self.config["gfs_files"] offset = abs(gfs_files["offset"]) endhour = gfs_files["forecast_length"] + offset interval = gfs_files["interval_hours"] @@ -47,7 +47,7 @@ def namelist_file(self): The namelist file. """ # Do not use offset here. It's relative to the MPAS fcst to run. - gfs_files = self._driver_config["gfs_files"] + gfs_files = self.config["gfs_files"] endhour = gfs_files["forecast_length"] end_date = self._cycle + timedelta(hours=endhour) interval = int(gfs_files["interval_hours"]) * 3600 # hour to sec @@ -70,7 +70,6 @@ def namelist_file(self): yield self._taskname(str(path)) yield asset(path, path.is_file) yield None - path.parent.mkdir(parents=True, exist_ok=True) self._create_user_updated_config( config_class=NMLConfig, config_values=d, @@ -98,10 +97,10 @@ def vtable(self): path = self._rundir / "Vtable" yield self._taskname(str(path)) yield asset(path, path.is_symlink) - infile = Path(self._driver_config["vtable"]) + infile = Path(self.config["vtable"]) yield file(path=infile) path.parent.mkdir(parents=True, exist_ok=True) - path.symlink_to(Path(self._driver_config["vtable"])) + path.symlink_to(Path(self.config["vtable"])) # Private helper methods diff --git a/src/uwtools/drivers/upp.py b/src/uwtools/drivers/upp.py index 313fb372b..1df762d0b 100644 --- a/src/uwtools/drivers/upp.py +++ b/src/uwtools/drivers/upp.py @@ -28,7 +28,7 @@ def files_copied(self): yield self._taskname("files copied") yield [ filecopy(src=Path(src), dst=self._rundir / dst) - for dst, src in self._driver_config.get("files_to_copy", {}).items() + for dst, src in self.config.get("files_to_copy", {}).items() ] @tasks @@ -39,7 +39,7 @@ def files_linked(self): yield self._taskname("files linked") yield [ symlink(target=Path(target), linkname=self._rundir / linkname) - for linkname, target in self._driver_config.get("files_to_link", {}).items() + for linkname, target in self.config.get("files_to_link", {}).items() ] @task @@ -50,12 +50,11 @@ def namelist_file(self): path = self._namelist_path yield self._taskname(str(path)) yield asset(path, path.is_file) - base_file = self._driver_config[STR.namelist].get(STR.basefile) + base_file = self.config[STR.namelist].get(STR.basefile) yield file(Path(base_file)) if base_file else None - path.parent.mkdir(parents=True, exist_ok=True) self._create_user_updated_config( config_class=NMLConfig, - config_values=self._driver_config[STR.namelist], + config_values=self.config[STR.namelist], path=path, schema=self._namelist_schema(), ) @@ -94,7 +93,7 @@ def _runcmd(self) -> str: """ Returns the full command-line component invocation. """ - execution = self._driver_config.get(STR.execution, {}) + execution = self.config.get(STR.execution, {}) mpiargs = execution.get(STR.mpiargs, []) components = [ execution.get(STR.mpicmd), diff --git a/src/uwtools/drivers/ww3.py b/src/uwtools/drivers/ww3.py index 5b7c7cc7e..c81768d2d 100644 --- a/src/uwtools/drivers/ww3.py +++ b/src/uwtools/drivers/ww3.py @@ -29,12 +29,12 @@ def namelist_file(self): yield self._taskname(fn) path = self._rundir / fn yield asset(path, path.is_file) - template_file = Path(self._driver_config[STR.namelist]["template_file"]) + template_file = Path(self.config[STR.namelist]["template_file"]) yield file(template_file) render( input_file=template_file, output_file=path, - overrides=self._driver_config[STR.namelist]["template_values"], + overrides=self.config[STR.namelist]["template_values"], ) @tasks diff --git a/src/uwtools/py.typed b/src/uwtools/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/uwtools/tests/api/test_driver.py b/src/uwtools/tests/api/test_driver.py index f00e13a4c..27927c9df 100644 --- a/src/uwtools/tests/api/test_driver.py +++ b/src/uwtools/tests/api/test_driver.py @@ -44,7 +44,19 @@ def kwargs(args): # Tests -@mark.parametrize("classname", driver_api._CLASSNAMES) +@mark.parametrize( + "classname", + [ + "Assets", + "AssetsCycleBased", + "AssetsCycleLeadtimeBased", + "AssetsTimeInvariant", + "Driver", + "DriverCycleBased", + "DriverCycleLeadtimeBased", + "DriverTimeInvariant", + ], +) def test_driver(classname): assert getattr(driver_api, classname) is getattr(driver_lib, classname) diff --git a/src/uwtools/tests/config/test_tools.py b/src/uwtools/tests/config/test_tools.py index fa9c901e3..2964dbe30 100644 --- a/src/uwtools/tests/config/test_tools.py +++ b/src/uwtools/tests/config/test_tools.py @@ -520,6 +520,23 @@ def test_realize_config_values_needed_yaml(caplog): assert actual.strip() == dedent(expected).strip() +def test_walk_key_path_fail_bad_key_path(): + with raises(UWError) as e: + tools.walk_key_path({"a": {"b": {"c": "cherry"}}}, ["a", "x"]) + assert str(e.value) == "Bad config path: a -> x" + + +def test_walk_key_path_fail_bad_leaf_value(): + with raises(UWError) as e: + tools.walk_key_path({"a": {"b": {"c": "cherry"}}}, ["a", "b", "c"]) + assert str(e.value) == "Value at a -> b -> c must be a dictionary" + + +def test_walk_key_path_pass(): + expected = ({"c": "cherry"}, "a -> b") + assert tools.walk_key_path({"a": {"b": {"c": "cherry"}}}, ["a", "b"]) == expected + + def test__ensure_format_bad_no_path_no_format(): with raises(UWError) as e: tools._ensure_format(desc="foo") diff --git a/src/uwtools/tests/drivers/test_cdeps.py b/src/uwtools/tests/drivers/test_cdeps.py index 081f292ba..c74efe9e8 100644 --- a/src/uwtools/tests/drivers/test_cdeps.py +++ b/src/uwtools/tests/drivers/test_cdeps.py @@ -48,7 +48,7 @@ def test_CDEPS_nml(caplog, driverobj, group): log.setLevel(logging.DEBUG) dst = driverobj._rundir / f"d{group}_in" assert not dst.is_file() - del driverobj._driver_config[f"{group}_in"]["base_file"] + del driverobj._config[f"{group}_in"]["base_file"] task = getattr(driverobj, f"{group}_nml") path = Path(refs(task())) assert dst.is_file() @@ -87,7 +87,7 @@ def test_CDEPS_streams(driverobj, group): template_file = driverobj._rundir.parent / "template.jinja2" with open(template_file, "w", encoding="utf-8") as f: print(dedent(template).strip(), file=f) - driverobj._driver_config[f"{group}_streams"]["template_file"] = template_file + driverobj._config[f"{group}_streams"]["template_file"] = template_file task = getattr(driverobj, f"{group}_stream") path = Path(refs(task())) assert dst.is_file() @@ -121,7 +121,7 @@ def test_CDEPS__model_namelist_file(driverobj): with patch.object(driverobj, "_create_user_updated_config") as cuuc: driverobj._model_namelist_file(group=group, path=path) cuuc.assert_called_once_with( - config_class=NMLConfig, config_values=driverobj._driver_config[group], path=path + config_class=NMLConfig, config_values=driverobj.config[group], path=path ) @@ -134,5 +134,5 @@ def test_CDEPS__model_stream_file(driverobj): render.assert_called_once_with( input_file=template_file, output_file=path, - values_src=driverobj._driver_config[group], + values_src=driverobj.config[group], ) diff --git a/src/uwtools/tests/drivers/test_chgres_cube.py b/src/uwtools/tests/drivers/test_chgres_cube.py index c63552199..ba651a1ad 100644 --- a/src/uwtools/tests/drivers/test_chgres_cube.py +++ b/src/uwtools/tests/drivers/test_chgres_cube.py @@ -86,7 +86,6 @@ def driverobj(config, cycle): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -116,7 +115,7 @@ def test_ChgresCube_namelist_file(caplog, driverobj): def test_ChgresCube_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["config"]["convert_atm"] = "string" + driverobj._config["namelist"]["update_values"]["config"]["convert_atm"] = "string" path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert logged(caplog, f"Failed to validate {path}") @@ -125,8 +124,8 @@ def test_ChgresCube_namelist_file_fails_validation(caplog, driverobj): def test_ChgresCube_namelist_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = str(Path(driverobj._driver_config["rundir"]) / "missing.nml") - driverobj._driver_config["namelist"]["base_file"] = base_file + base_file = str(Path(driverobj.config["rundir"], "missing.nml")) + driverobj._config["namelist"]["base_file"] = base_file path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert regex_logged(caplog, "missing.nml: State: Not Ready (external asset)") diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py index b7a8d21eb..c4624727f 100644 --- a/src/uwtools/tests/drivers/test_driver.py +++ b/src/uwtools/tests/drivers/test_driver.py @@ -141,56 +141,68 @@ def driverobj(config): def test_Assets(assetsobj): - assert Path(assetsobj._driver_config["base_file"]).name == "base.yaml" + assert Path(assetsobj.config["base_file"]).name == "base.yaml" -def test_Assets_controller(config, controller_schema): - config["controller"] = {"rundir": "/controller/run/dir"} - del config["concrete"]["rundir"] - with patch.object(ConcreteAssetsTimeInvariant, "_validate", driver.Assets._validate): - with raises(UWConfigError): - ConcreteAssetsTimeInvariant(config=config, schema_file=controller_schema) - assert ConcreteAssetsTimeInvariant( - config=config, schema_file=controller_schema, controller="controller" - ) +def test_Assets_fail_bad_config(config): + with raises(UWConfigError) as e: + ConcreteAssetsTimeInvariant(config=config["concrete"]) + assert str(e.value) == "Required 'concrete' block missing in config" -def test_Assets_repr_cycle_based(config): +def test_Assets___repr___cycle_based(config): obj = ConcreteAssetsCycleBased(config=config, cycle=dt.datetime(2024, 7, 2, 12)) - expected = "concrete 2024-07-02T12:00 in %s" % obj._driver_config["rundir"] + expected = "concrete 2024-07-02T12:00 in %s" % obj.config["rundir"] assert repr(obj) == expected -def test_Assets_repr_cycle_and_leadtime_based(config): +def test_Assets___repr___cycle_and_leadtime_based(config): obj = ConcreteAssetsCycleLeadtimeBased( config=config, cycle=dt.datetime(2024, 7, 2, 12), leadtime=dt.timedelta(hours=6) ) - expected = "concrete 2024-07-02T12:00 06:00:00 in %s" % obj._driver_config["rundir"] + expected = "concrete 2024-07-02T12:00 06:00:00 in %s" % obj.config["rundir"] assert repr(obj) == expected -def test_Assets_repr_time_invariant(config): +def test_Assets___repr___time_invariant(config): obj = ConcreteAssetsTimeInvariant(config=config) - expected = "concrete in %s" % obj._driver_config["rundir"] + expected = "concrete in %s" % obj.config["rundir"] assert repr(obj) == expected -def test_Assets_str(assetsobj): +def test_Assets___str__(assetsobj): assert str(assetsobj) == "concrete" def test_Assets_config(assetsobj): # The user-accessible object is equivalent to the internal driver config: - assert assetsobj.config == assetsobj._driver_config + assert assetsobj.config == assetsobj._config # But they are separate objects: - assert not assetsobj.config is assetsobj._driver_config + assert not assetsobj.config is assetsobj._config def test_Assets_config_full(assetsobj): # The user-accessible object is equivalent to the internal driver config: - assert assetsobj.config_full == assetsobj._config + assert assetsobj.config_full == assetsobj._config_full # But they are separate objects: - assert not assetsobj.config_full is assetsobj._config + assert not assetsobj.config_full is assetsobj._config_full + + +def test_Assets_controller(config, controller_schema): + config["controller"] = {"rundir": "/controller/run/dir"} + del config["concrete"]["rundir"] + with patch.object(ConcreteAssetsTimeInvariant, "_validate", driver.Assets._validate): + with raises(UWConfigError): + ConcreteAssetsTimeInvariant(config=config, schema_file=controller_schema) + assert ConcreteAssetsTimeInvariant( + config=config, schema_file=controller_schema, controller="controller" + ) + + +def test_Assets_cycle(config): + cycle = dt.datetime(2024, 7, 2, 12) + obj = ConcreteAssetsCycleBased(config=config, cycle=cycle) + assert obj.cycle == cycle @mark.parametrize("val", (True, False)) @@ -200,16 +212,21 @@ def test_Assets_dry_run(config, val): dryrun.assert_called_once_with(enable=val) -# Tests for workflow methods - - -def test_key_path(config, tmp_path): +def test_Assets_key_path(config, tmp_path): config_file = tmp_path / "config.yaml" config_file.write_text(yaml.dump({"foo": {"bar": config}})) assetsobj = ConcreteAssetsTimeInvariant( config=config_file, dry_run=False, key_path=["foo", "bar"] ) - assert config == assetsobj._config + assert config == assetsobj.config_full + + +def test_Assets_leadtime(config): + cycle = dt.datetime(2024, 7, 2, 12) + leadtime = dt.timedelta(hours=6) + obj = ConcreteAssetsCycleLeadtimeBased(config=config, cycle=cycle, leadtime=leadtime) + assert obj.cycle == cycle + assert obj.leadtime == leadtime def test_Assets_validate(assetsobj, caplog): @@ -218,9 +235,6 @@ def test_Assets_validate(assetsobj, caplog): assert regex_logged(caplog, "State: Ready") -# Tests for private helper methods - - @mark.parametrize( "base_file,update_values,expected", [ @@ -234,7 +248,7 @@ def test_Assets__create_user_updated_config_base_file( assetsobj, base_file, expected, tmp_path, update_values ): path = tmp_path / "updated.yaml" - dc = assetsobj._driver_config + dc = assetsobj.config if not base_file: del dc["base_file"] if not update_values: @@ -247,24 +261,8 @@ def test_Assets__create_user_updated_config_base_file( assert updated == expected -def test_Assets__driver_config_fail(assetsobj): - del assetsobj._config["concrete"] - with raises(UWConfigError) as e: - assert assetsobj._driver_config - assert str(e.value) == "Required 'concrete' block missing in config" - - -def test_Assets__driver_config_pass(assetsobj): - assert set(assetsobj._driver_config.keys()) == { - "base_file", - "execution", - "rundir", - "update_values", - } - - def test_Assets__rundir(assetsobj): - assert assetsobj._rundir == Path(assetsobj._driver_config["rundir"]) + assert assetsobj._rundir == Path(assetsobj.config["rundir"]) def test_Assets__validate_internal(assetsobj): @@ -273,7 +271,7 @@ def test_Assets__validate_internal(assetsobj): assetsobj._validate(assetsobj) assert validate_internal.call_args_list[0].kwargs == { "schema_name": "concrete", - "config": assetsobj._config, + "config": assetsobj.config_full, } @@ -284,7 +282,7 @@ def test_Assets__validate_external(config): assetsobj = ConcreteAssetsTimeInvariant(schema_file=schema_file, config=config) assert validate_external.call_args_list[0].kwargs == { "schema_file": schema_file, - "config": assetsobj._config, + "config": assetsobj.config_full, } @@ -292,10 +290,16 @@ def test_Assets__validate_external(config): def test_Driver(driverobj): - assert Path(driverobj._driver_config["base_file"]).name == "base.yaml" + assert Path(driverobj.config["base_file"]).name == "base.yaml" assert driverobj._batch is True +def test_Driver_cycle(config): + cycle = dt.datetime(2024, 7, 2, 12) + obj = ConcreteDriverCycleBased(config=config, cycle=cycle) + assert obj.cycle == cycle + + def test_Driver_controller(config, controller_schema): config["controller"] = { "execution": {"executable": "/path/to/coupled.exe"}, @@ -311,13 +315,18 @@ def test_Driver_controller(config, controller_schema): ) -# Tests for workflow methods +def test_Driver_leadtime(config): + cycle = dt.datetime(2024, 7, 2, 12) + leadtime = dt.timedelta(hours=6) + obj = ConcreteDriverCycleLeadtimeBased(config=config, cycle=cycle, leadtime=leadtime) + assert obj.cycle == cycle + assert obj.leadtime == leadtime @mark.parametrize("batch", [True, False]) def test_Driver_run(batch, driverobj): driverobj._batch = batch - executable = Path(driverobj._driver_config["execution"]["executable"]) + executable = Path(driverobj.config["execution"]["executable"]) executable.touch() with patch.object(driverobj, "_run_via_batch_submission") as rvbs: with patch.object(driverobj, "_run_via_local_execution") as rvle: @@ -342,7 +351,7 @@ def test_Driver_runscript(arg, driverobj, type_): def test_Driver__run_via_batch_submission(driverobj): runscript = driverobj._runscript_path - executable = Path(driverobj._driver_config["execution"]["executable"]) + executable = Path(driverobj.config["execution"]["executable"]) executable.touch() with patch.object(driverobj, "provisioned_rundir") as prd: with patch.object( @@ -356,7 +365,7 @@ def test_Driver__run_via_batch_submission(driverobj): def test_Driver__run_via_local_execution(driverobj): - executable = Path(driverobj._driver_config["execution"]["executable"]) + executable = Path(driverobj.config["execution"]["executable"]) executable.touch() with patch.object(driverobj, "provisioned_rundir") as prd: with patch.object(driver, "execute") as execute: @@ -369,9 +378,6 @@ def test_Driver__run_via_local_execution(driverobj): prd.assert_called_once_with() -# Tests for private helper methods - - @mark.parametrize( "base_file,update_values,expected", [ @@ -385,44 +391,25 @@ def test_Driver__create_user_updated_config_base_file( base_file, driverobj, expected, tmp_path, update_values ): path = tmp_path / "updated.yaml" - dc = driverobj._driver_config if not base_file: - del dc["base_file"] + del driverobj._config["base_file"] if not update_values: - del dc["update_values"] + del driverobj._config["update_values"] ConcreteDriverTimeInvariant._create_user_updated_config( - config_class=YAMLConfig, config_values=dc, path=path + config_class=YAMLConfig, config_values=driverobj.config, path=path ) with open(path, "r", encoding="utf-8") as f: updated = yaml.safe_load(f) assert updated == expected -def test_Driver__driver_config_fail(driverobj): - del driverobj._config["concrete"] - with raises(UWConfigError) as e: - assert driverobj._driver_config - assert str(e.value) == "Required 'concrete' block missing in config" - - -def test_Driver__driver_config_pass(driverobj): - assert set(driverobj._driver_config.keys()) == { - "base_file", - "execution", - "rundir", - "update_values", - } - - def test_Driver__namelist_schema_custom(driverobj, tmp_path): nmlschema = {"properties": {"n": {"type": "integer"}}, "type": "object"} schema = {"foo": {"bar": nmlschema}} schema_path = tmp_path / "test.jsonschema" with open(schema_path, "w", encoding="utf-8") as f: json.dump(schema, f) - with patch.object( - ConcreteDriverTimeInvariant, "_driver_config", new_callable=PropertyMock - ) as dc: + with patch.object(ConcreteDriverTimeInvariant, "config", new_callable=PropertyMock) as dc: dc.return_value = {"baz": {"qux": {"validate": True}}} with patch.object(driver, "get_schema_file", return_value=schema_path): assert ( @@ -441,24 +428,20 @@ def test_Driver__namelist_schema_default(driverobj, tmp_path): schema_path = tmp_path / "test.jsonschema" with open(schema_path, "w", encoding="utf-8") as f: json.dump(schema, f) - with patch.object( - ConcreteDriverTimeInvariant, "_driver_config", new_callable=PropertyMock - ) as dc: + with patch.object(ConcreteDriverTimeInvariant, "config", new_callable=PropertyMock) as dc: dc.return_value = {"namelist": {"validate": True}} with patch.object(driver, "get_schema_file", return_value=schema_path): assert driverobj._namelist_schema() == nmlschema def test_Driver__namelist_schema_default_disable(driverobj): - with patch.object( - ConcreteDriverTimeInvariant, "_driver_config", new_callable=PropertyMock - ) as dc: + with patch.object(ConcreteDriverTimeInvariant, "config", new_callable=PropertyMock) as dc: dc.return_value = {"namelist": {"validate": False}} assert driverobj._namelist_schema() == {"type": "object"} def test_Driver__run_resources_fail(driverobj): - del driverobj._config["platform"] + del driverobj._config_full["platform"] with raises(UWConfigError) as e: assert driverobj._run_resources assert str(e.value) == "Required 'platform' block missing in config" @@ -468,9 +451,7 @@ def test_Driver__run_resources_pass(driverobj): account = "me" scheduler = "slurm" walltime = "00:05:00" - driverobj._driver_config["execution"].update( - {"batchargs": {"threads": 4, "walltime": walltime}} - ) + driverobj._config["execution"].update({"batchargs": {"threads": 4, "walltime": walltime}}) assert driverobj._run_resources == { "account": account, "rundir": driverobj._rundir, @@ -482,7 +463,7 @@ def test_Driver__run_resources_pass(driverobj): def test_Driver__runcmd(driverobj): - executable = driverobj._driver_config["execution"]["executable"] + executable = driverobj.config["execution"]["executable"] assert driverobj._runcmd == f"foo bar baz {executable}" @@ -529,7 +510,7 @@ def test_Driver__runscript_done_file(driverobj): def test_Driver__runscript_path(driverobj): - rundir = Path(driverobj._driver_config["rundir"]) + rundir = Path(driverobj.config["rundir"]) assert driverobj._runscript_path == rundir / "runscript.concrete" @@ -546,11 +527,11 @@ def test_Driver__validate_internal(assetsobj): assetsobj._validate(assetsobj) assert validate_internal.call_args_list[0].kwargs == { "schema_name": "concrete", - "config": assetsobj._config, + "config": assetsobj.config_full, } assert validate_internal.call_args_list[1].kwargs == { "schema_name": "platform", - "config": assetsobj._config, + "config": assetsobj.config_full, } @@ -561,14 +542,14 @@ def test_Driver__validate_external(config): assetsobj = ConcreteAssetsTimeInvariant(schema_file=schema_file, config=config) assert validate_external.call_args_list[0].kwargs == { "schema_file": schema_file, - "config": assetsobj._config, + "config": assetsobj.config_full, } def test_Driver__write_runscript(driverobj): - rundir = driverobj._driver_config["rundir"] - path = Path(rundir) / "runscript" - executable = driverobj._driver_config["execution"]["executable"] + rundir = driverobj.config["rundir"] + path = Path(rundir, "runscript") + executable = driverobj.config["execution"]["executable"] driverobj._write_runscript(path=path, envvars={"FOO": "bar", "BAZ": "qux"}) expected = f""" #!/bin/bash @@ -592,8 +573,8 @@ def test_Driver__write_runscript(driverobj): def test_Driver__write_runscript_threads_fail(driverobj): - path = Path(driverobj._driver_config["rundir"]) / "runscript" - driverobj._driver_config["execution"]["threads"] = 4 + path = Path(driverobj.config["rundir"], "runscript") + driverobj._config["execution"]["threads"] = 4 with raises(UWConfigError) as e: driverobj._write_runscript(path=path) assert str(e.value) == "Config specified threads but driver does not set OMP_NUM_THREADS" diff --git a/src/uwtools/tests/drivers/test_esg_grid.py b/src/uwtools/tests/drivers/test_esg_grid.py index 916ec2d2c..c7a0e0e14 100644 --- a/src/uwtools/tests/drivers/test_esg_grid.py +++ b/src/uwtools/tests/drivers/test_esg_grid.py @@ -65,7 +65,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -97,7 +96,7 @@ def test_ESGGrid_namelist_file(caplog, driverobj): def test_ESGGrid_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["regional_grid_nml"]["delx"] = "string" + driverobj._config["namelist"]["update_values"]["regional_grid_nml"]["delx"] = "string" path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert logged(caplog, f"Failed to validate {path}") @@ -106,8 +105,8 @@ def test_ESGGrid_namelist_file_fails_validation(caplog, driverobj): def test_ESGGrid_namelist_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = str(Path(driverobj._driver_config["rundir"]) / "missing.nml") - driverobj._driver_config["namelist"]["base_file"] = base_file + base_file = str(Path(driverobj.config["rundir"], "missing.nml")) + driverobj._config["namelist"]["base_file"] = base_file path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert regex_logged(caplog, "missing.nml: State: Not Ready (external asset)") diff --git a/src/uwtools/tests/drivers/test_filter_topo.py b/src/uwtools/tests/drivers/test_filter_topo.py index 6d1ae60ab..f37fea3ec 100644 --- a/src/uwtools/tests/drivers/test_filter_topo.py +++ b/src/uwtools/tests/drivers/test_filter_topo.py @@ -60,7 +60,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -81,7 +80,7 @@ def test_FilterTopo(method): def test_FilterTopo_input_grid_file(driverobj): - path = Path(driverobj._driver_config["rundir"]) / "C403_grid.tile7.halo4.nc" + path = Path(driverobj.config["rundir"], "C403_grid.tile7.halo4.nc") assert not path.is_file() driverobj.input_grid_file() assert path.is_symlink() @@ -90,7 +89,7 @@ def test_FilterTopo_input_grid_file(driverobj): def test_FilterTopo_namelist_file(driverobj): path = refs(driverobj.namelist_file()) actual = from_od(f90nml.read(path).todict()) - expected = driverobj._driver_config["namelist"]["update_values"] + expected = driverobj.config["namelist"]["update_values"] assert actual == expected diff --git a/src/uwtools/tests/drivers/test_fv3.py b/src/uwtools/tests/drivers/test_fv3.py index eb53b5875..dbe5a6502 100644 --- a/src/uwtools/tests/drivers/test_fv3.py +++ b/src/uwtools/tests/drivers/test_fv3.py @@ -84,7 +84,6 @@ def true(): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -115,7 +114,7 @@ def test_FV3_boundary_files(driverobj): def test_FV3_diag_table(driverobj): src = driverobj._rundir / "diag_table.in" src.touch() - driverobj._driver_config["diag_table"] = src + driverobj._config["diag_table"] = src dst = driverobj._rundir / "diag_table" assert not dst.is_file() driverobj.diag_table() @@ -132,7 +131,7 @@ def test_FV3_field_table(driverobj): src.touch() dst = driverobj._rundir / "field_table" assert not dst.is_file() - driverobj._driver_config["field_table"] = {"base_file": str(src)} + driverobj._config["field_table"] = {"base_file": str(src)} driverobj.field_table() assert dst.is_file() @@ -168,7 +167,7 @@ def test_FV3_model_configure(base_file_exists, caplog, driverobj): yaml.dump({}, f) dst = driverobj._rundir / "model_configure" assert not dst.is_file() - driverobj._driver_config["model_configure"] = {"base_file": src} + driverobj._config["model_configure"] = {"base_file": src} driverobj.model_configure() if base_file_exists: assert dst.is_file() @@ -184,7 +183,7 @@ def test_FV3_namelist_file(caplog, driverobj): yaml.dump({}, f) dst = driverobj._rundir / "input.nml" assert not dst.is_file() - driverobj._driver_config["namelist_file"] = {"base_file": src} + driverobj._config["namelist_file"] = {"base_file": src} path = Path(refs(driverobj.namelist_file())) assert logged(caplog, f"Wrote config to {path}") assert dst.is_file() @@ -192,7 +191,7 @@ def test_FV3_namelist_file(caplog, driverobj): def test_FV3_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["namsfc"]["foo"] = None + driverobj._config["namelist"]["update_values"]["namsfc"]["foo"] = None path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert logged(caplog, f"Failed to validate {path}") @@ -201,8 +200,8 @@ def test_FV3_namelist_file_fails_validation(caplog, driverobj): def test_FV3_namelist_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = str(Path(driverobj._driver_config["rundir"]) / "missing.nml") - driverobj._driver_config["namelist"]["base_file"] = base_file + base_file = str(Path(driverobj.config["rundir"], "missing.nml")) + driverobj._config["namelist"]["base_file"] = base_file path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert regex_logged(caplog, "missing.nml: State: Not Ready (external asset)") @@ -210,7 +209,7 @@ def test_FV3_namelist_file_missing_base_file(caplog, driverobj): @mark.parametrize("domain", ("global", "regional")) def test_FV3_provisioned_rundir(domain, driverobj): - driverobj._driver_config["domain"] = domain + driverobj._config["domain"] = domain with patch.multiple( driverobj, boundary_files=D, diff --git a/src/uwtools/tests/drivers/test_global_equiv_resol.py b/src/uwtools/tests/drivers/test_global_equiv_resol.py index 58c07ecc8..45de29fee 100644 --- a/src/uwtools/tests/drivers/test_global_equiv_resol.py +++ b/src/uwtools/tests/drivers/test_global_equiv_resol.py @@ -46,7 +46,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -66,7 +65,7 @@ def test_GlobalEquivResol(method): def test_GlobalEquivResol_input_file(driverobj): - path = Path(driverobj._driver_config["input_grid_file"]) + path = Path(driverobj.config["input_grid_file"]) assert not driverobj.input_file().ready() path.parent.mkdir() path.touch() @@ -86,7 +85,7 @@ def test_GlobalEquivResol_provisioned_rundir(driverobj): def test_GlobalEquivResol__runcmd(driverobj): cmd = driverobj._runcmd - input_file_path = driverobj._driver_config["input_grid_file"] + input_file_path = driverobj.config["input_grid_file"] assert cmd == f"/path/to/global_equiv_resol.exe {input_file_path}" diff --git a/src/uwtools/tests/drivers/test_ioda.py b/src/uwtools/tests/drivers/test_ioda.py index 4cd71f5cb..0b6765e61 100644 --- a/src/uwtools/tests/drivers/test_ioda.py +++ b/src/uwtools/tests/drivers/test_ioda.py @@ -70,7 +70,6 @@ def driverobj(config, cycle): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", diff --git a/src/uwtools/tests/drivers/test_jedi.py b/src/uwtools/tests/drivers/test_jedi.py index ebeb8214e..24e79e52f 100644 --- a/src/uwtools/tests/drivers/test_jedi.py +++ b/src/uwtools/tests/drivers/test_jedi.py @@ -80,7 +80,6 @@ def driverobj(config, cycle): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -100,10 +99,10 @@ def test_JEDI(method): def test_JEDI_configuration_file(driverobj): basecfg = {"foo": "bar"} - base_file = Path(driverobj._driver_config["configuration_file"]["base_file"]) + base_file = Path(driverobj.config["configuration_file"]["base_file"]) with open(base_file, "w", encoding="utf-8") as f: yaml.dump(basecfg, f) - cfgfile = Path(driverobj._driver_config["rundir"]) / "jedi.yaml" + cfgfile = Path(driverobj.config["rundir"], "jedi.yaml") assert not cfgfile.is_file() driverobj.configuration_file() assert cfgfile.is_file() @@ -113,9 +112,9 @@ def test_JEDI_configuration_file(driverobj): def test_JEDI_configuration_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = Path(driverobj._driver_config["rundir"]) / "missing" - driverobj._driver_config["configuration_file"]["base_file"] = base_file - cfgfile = Path(driverobj._driver_config["rundir"]) / "jedi.yaml" + base_file = Path(driverobj.config["rundir"], "missing") + driverobj._config["configuration_file"]["base_file"] = base_file + cfgfile = Path(driverobj.config["rundir"], "jedi.yaml") assert not cfgfile.is_file() driverobj.configuration_file() assert not cfgfile.is_file() @@ -124,7 +123,7 @@ def test_JEDI_configuration_file_missing_base_file(caplog, driverobj): def test_JEDI_files_copied(driverobj): with patch.object(jedi_base, "filecopy") as filecopy: - driverobj._driver_config["rundir"] = "/path/to/run" + driverobj._config["rundir"] = "/path/to/run" driverobj.files_copied() assert filecopy.call_count == 2 assert ( @@ -138,7 +137,7 @@ def test_JEDI_files_copied(driverobj): def test_JEDI_files_linked(driverobj): with patch.object(jedi_base, "symlink") as symlink: - driverobj._driver_config["rundir"] = "/path/to/run" + driverobj._config["rundir"] = "/path/to/run" driverobj.files_linked() assert symlink.call_count == 2 assert ( @@ -178,12 +177,12 @@ def file(path: Path): result = Mock(output="", success=True) run.return_value = result driverobj.validate_only() - cfgfile = Path(driverobj._driver_config["rundir"]) / "jedi.yaml" + cfgfile = Path(driverobj.config["rundir"], "jedi.yaml") cmds = [ "module load some-module", "module load jedi-module", "time %s --validate-only %s 2>&1" - % (driverobj._driver_config["execution"]["executable"], cfgfile), + % (driverobj.config["execution"]["executable"], cfgfile), ] run.assert_called_once_with("20240201 18Z jedi validate_only", " && ".join(cmds)) assert regex_logged(caplog, "Config is valid") @@ -198,7 +197,7 @@ def test_JEDI__driver_name(driverobj): def test_JEDI__runcmd(driverobj): - executable = driverobj._driver_config["execution"]["executable"] + executable = driverobj.config["execution"]["executable"] config = driverobj._rundir / driverobj._config_fn assert ( driverobj._runcmd == f"srun --export=ALL --ntasks $SLURM_CPUS_ON_NODE {executable} {config}" diff --git a/src/uwtools/tests/drivers/test_make_hgrid.py b/src/uwtools/tests/drivers/test_make_hgrid.py index 14fb89a00..7fcaabce4 100644 --- a/src/uwtools/tests/drivers/test_make_hgrid.py +++ b/src/uwtools/tests/drivers/test_make_hgrid.py @@ -51,7 +51,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", diff --git a/src/uwtools/tests/drivers/test_make_solo_mosaic.py b/src/uwtools/tests/drivers/test_make_solo_mosaic.py index 4a70f2ed8..4c22c4dee 100644 --- a/src/uwtools/tests/drivers/test_make_solo_mosaic.py +++ b/src/uwtools/tests/drivers/test_make_solo_mosaic.py @@ -47,7 +47,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -76,7 +75,7 @@ def test_MakeSoloMosiac__driver_name(driverobj): def test_MakeSoloMosaic__runcmd(driverobj): - dir_path = driverobj._config["make_solo_mosaic"]["config"]["dir"] + dir_path = driverobj.config["config"]["dir"] cmd = driverobj._runcmd assert cmd == f"/path/to/make_solo_mosaic.exe --dir {dir_path} --num_tiles 1" diff --git a/src/uwtools/tests/drivers/test_mpas.py b/src/uwtools/tests/drivers/test_mpas.py index 192c6a1ae..2a0b58f4a 100644 --- a/src/uwtools/tests/drivers/test_mpas.py +++ b/src/uwtools/tests/drivers/test_mpas.py @@ -26,7 +26,7 @@ def streams_file(config, driverobj, drivername): array_elements = {"file", "stream", "var", "var_array", "var_struct"} array_elements_tested = set() driverobj.streams_file() - path = Path(driverobj._driver_config["rundir"]) / driverobj._streams_fn + path = Path(driverobj.config["rundir"], driverobj._streams_fn) with open(path, "r", encoding="utf-8") as f: xml = etree.parse(f).getroot() assert xml.tag == "streams" @@ -120,7 +120,6 @@ def driverobj(config, cycle): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -148,7 +147,7 @@ def test_MPAS_boundary_files(driverobj, cycle): for n in ns ] assert not any(link.is_file() for link in links) - infile_path = Path(driverobj._driver_config["lateral_boundary_conditions"]["path"]) + infile_path = Path(driverobj.config["lateral_boundary_conditions"]["path"]) infile_path.mkdir() for n in ns: path = infile_path / f"lbc.{(cycle+dt.timedelta(hours=n)).strftime('%Y-%m-%d_%H.%M.%S')}.nc" @@ -206,7 +205,7 @@ def test_MPAS_namelist_file_long_duration(caplog, config, cycle): def test_MPAS_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["nhyd_model"]["foo"] = None + driverobj._config["namelist"]["update_values"]["nhyd_model"]["foo"] = None path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert logged(caplog, f"Failed to validate {path}") @@ -215,8 +214,8 @@ def test_MPAS_namelist_file_fails_validation(caplog, driverobj): def test_MPAS_namelist_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = str(Path(driverobj._driver_config["rundir"]) / "missing.nml") - driverobj._driver_config["namelist"]["base_file"] = base_file + base_file = str(Path(driverobj.config["rundir"], "missing.nml")) + driverobj._config["namelist"]["base_file"] = base_file path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert regex_logged(caplog, "missing.nml: State: Not Ready (external asset)") diff --git a/src/uwtools/tests/drivers/test_mpas_init.py b/src/uwtools/tests/drivers/test_mpas_init.py index 1bad75832..a1d07dcb3 100644 --- a/src/uwtools/tests/drivers/test_mpas_init.py +++ b/src/uwtools/tests/drivers/test_mpas_init.py @@ -102,7 +102,6 @@ def driverobj(config, cycle): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -130,7 +129,7 @@ def test_MPASInit_boundary_files(cycle, driverobj): for n in ns ] assert not any(link.is_file() for link in links) - input_path = Path(driverobj._driver_config["boundary_conditions"]["path"]) + input_path = Path(driverobj.config["boundary_conditions"]["path"]) input_path.mkdir() for n in ns: (input_path / f"FILE:{(cycle+dt.timedelta(hours=n)).strftime('%Y-%m-%d_%H')}").touch() @@ -181,7 +180,7 @@ def test_MPASInit_namelist_file(caplog, driverobj): def test_MPASInit_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["nhyd_model"]["foo"] = None + driverobj._config["namelist"]["update_values"]["nhyd_model"]["foo"] = None path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert logged(caplog, f"Failed to validate {path}") @@ -190,8 +189,8 @@ def test_MPASInit_namelist_file_fails_validation(caplog, driverobj): def test_MPASInit_namelist_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = str(Path(driverobj._driver_config["rundir"]) / "missing.nml") - driverobj._driver_config["namelist"]["base_file"] = base_file + base_file = str(Path(driverobj.config["rundir"], "missing.nml")) + driverobj._config["namelist"]["base_file"] = base_file path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert regex_logged(caplog, "missing.nml: State: Not Ready (external asset)") diff --git a/src/uwtools/tests/drivers/test_orog_gsl.py b/src/uwtools/tests/drivers/test_orog_gsl.py index 05a48158c..1abf5a9f9 100644 --- a/src/uwtools/tests/drivers/test_orog_gsl.py +++ b/src/uwtools/tests/drivers/test_orog_gsl.py @@ -54,7 +54,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -74,7 +73,7 @@ def test_OrogGSL(method): def test_OrogGSL_input_grid_file(driverobj): - path = Path(driverobj._driver_config["rundir"]) / "C403_grid.tile7.halo4.nc" + path = Path(driverobj.config["rundir"], "C403_grid.tile7.halo4.nc") assert not path.is_file() driverobj.input_grid_file() assert path.is_symlink() @@ -90,14 +89,14 @@ def test_OrogGSL_provisioned_rundir(driverobj): def test_OrogGSL_topo_data_2p5m(driverobj): - path = Path(driverobj._driver_config["rundir"]) / "geo_em.d01.lat-lon.2.5m.HGT_M.nc" + path = Path(driverobj.config["rundir"], "geo_em.d01.lat-lon.2.5m.HGT_M.nc") assert not path.is_file() driverobj.topo_data_2p5m() assert path.is_symlink() def test_OrogGSL_topo_data_3os(driverobj): - path = Path(driverobj._driver_config["rundir"]) / "HGT.Beljaars_filtered.lat-lon.30s_res.nc" + path = Path(driverobj.config["rundir"], "HGT.Beljaars_filtered.lat-lon.30s_res.nc") assert not path.is_file() driverobj.topo_data_30s() assert path.is_symlink() @@ -108,8 +107,8 @@ def test_OrogGSL__driver_name(driverobj): def test_OrogGSL__runcmd(driverobj): - inputs = [str(driverobj._driver_config["config"][k]) for k in ("tile", "resolution", "halo")] + inputs = [str(driverobj.config["config"][k]) for k in ("tile", "resolution", "halo")] assert driverobj._runcmd == "echo '%s' | %s" % ( "\n".join(inputs), - driverobj._driver_config["execution"]["executable"], + driverobj.config["execution"]["executable"], ) diff --git a/src/uwtools/tests/drivers/test_schism.py b/src/uwtools/tests/drivers/test_schism.py index 5c0635e14..5dc5cd366 100644 --- a/src/uwtools/tests/drivers/test_schism.py +++ b/src/uwtools/tests/drivers/test_schism.py @@ -45,18 +45,14 @@ def driverobj(config, cycle): @mark.parametrize( "method", - [ - "_driver_config", - "_taskname", - "_validate", - ], + ["_taskname", "_validate"], ) def test_SCHISM(method): assert getattr(SCHISM, method) is getattr(AssetsCycleBased, method) def test_SCHISM_namelist_file(driverobj): - src = driverobj._driver_config["namelist"]["template_file"] + src = driverobj.config["namelist"]["template_file"] with open(src, "w", encoding="utf-8") as f: yaml.dump({}, f) dst = driverobj._rundir / "param.nml" diff --git a/src/uwtools/tests/drivers/test_sfc_climo_gen.py b/src/uwtools/tests/drivers/test_sfc_climo_gen.py index b0445c1d5..292a772e0 100644 --- a/src/uwtools/tests/drivers/test_sfc_climo_gen.py +++ b/src/uwtools/tests/drivers/test_sfc_climo_gen.py @@ -87,7 +87,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -120,7 +119,7 @@ def test_SfcClimoGen_namelist_file(caplog, driverobj): def test_SfcClimoGen_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["config"]["halo"] = "string" + driverobj._config["namelist"]["update_values"]["config"]["halo"] = "string" with patch.object(sfc_climo_gen, "file", new=ready): path = Path(refs(driverobj.namelist_file())) assert not path.exists() diff --git a/src/uwtools/tests/drivers/test_shave.py b/src/uwtools/tests/drivers/test_shave.py index cb3f8a86e..287ee6066 100644 --- a/src/uwtools/tests/drivers/test_shave.py +++ b/src/uwtools/tests/drivers/test_shave.py @@ -52,7 +52,6 @@ def driverobj(config): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -87,9 +86,9 @@ def test_Shave__driver_name(driverobj): def test_Shave__runcmd(driverobj): cmd = driverobj._runcmd - nx = driverobj._driver_config["config"]["nx"] - ny = driverobj._driver_config["config"]["ny"] - nh4 = driverobj._driver_config["config"]["nh4"] - input_file_path = driverobj._driver_config["config"]["input_grid_file"] + nx = driverobj.config["config"]["nx"] + ny = driverobj.config["config"]["ny"] + nh4 = driverobj.config["config"]["nh4"] + input_file_path = driverobj._config["config"]["input_grid_file"] output_file_path = input_file_path.replace(".nc", "_NH0.nc") assert cmd == f"/path/to/shave {nx} {ny} {nh4} {input_file_path} {output_file_path}" diff --git a/src/uwtools/tests/drivers/test_support.py b/src/uwtools/tests/drivers/test_support.py index 421abaae0..9ed14ae11 100644 --- a/src/uwtools/tests/drivers/test_support.py +++ b/src/uwtools/tests/drivers/test_support.py @@ -56,10 +56,6 @@ def t2(self): def t3(self): "@tasks t3" - @property - def _driver_config(self): - pass - @property def _driver_name(self): pass diff --git a/src/uwtools/tests/drivers/test_ungrib.py b/src/uwtools/tests/drivers/test_ungrib.py index 0d02d54ab..7d3f9499a 100644 --- a/src/uwtools/tests/drivers/test_ungrib.py +++ b/src/uwtools/tests/drivers/test_ungrib.py @@ -59,7 +59,6 @@ def driverobj(config, cycle): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -119,7 +118,7 @@ def test_Ungrib_provisioned_rundir(driverobj): def test_Ungrib_vtable(driverobj): src = driverobj._rundir / "Vtable.GFS.in" src.touch() - driverobj._driver_config["vtable"] = src + driverobj._config["vtable"] = src dst = driverobj._rundir / "Vtable" assert not dst.is_symlink() driverobj.vtable() diff --git a/src/uwtools/tests/drivers/test_upp.py b/src/uwtools/tests/drivers/test_upp.py index 0548d4dfa..9132141fd 100644 --- a/src/uwtools/tests/drivers/test_upp.py +++ b/src/uwtools/tests/drivers/test_upp.py @@ -80,7 +80,6 @@ def leadtime(): @mark.parametrize( "method", [ - "_driver_config", "_run_resources", "_run_via_batch_submission", "_run_via_local_execution", @@ -99,29 +98,29 @@ def test_UPP(method): def test_UPP_files_copied(driverobj): - for _, src in driverobj._driver_config["files_to_copy"].items(): + for _, src in driverobj.config["files_to_copy"].items(): Path(src).touch() - for dst, _ in driverobj._driver_config["files_to_copy"].items(): - assert not Path(driverobj._rundir / dst).is_file() + for dst, _ in driverobj.config["files_to_copy"].items(): + assert not (driverobj._rundir / dst).is_file() driverobj.files_copied() - for dst, _ in driverobj._driver_config["files_to_copy"].items(): - assert Path(driverobj._rundir / dst).is_file() + for dst, _ in driverobj.config["files_to_copy"].items(): + assert (driverobj._rundir / dst).is_file() def test_UPP_files_linked(driverobj): - for _, src in driverobj._driver_config["files_to_link"].items(): + for _, src in driverobj.config["files_to_link"].items(): Path(src).touch() - for dst, _ in driverobj._driver_config["files_to_link"].items(): - assert not Path(driverobj._rundir / dst).is_file() + for dst, _ in driverobj.config["files_to_link"].items(): + assert not (driverobj._rundir / dst).is_file() driverobj.files_linked() - for dst, _ in driverobj._driver_config["files_to_link"].items(): - assert Path(driverobj._rundir / dst).is_symlink() + for dst, _ in driverobj.config["files_to_link"].items(): + assert (driverobj._rundir / dst).is_symlink() def test_UPP_namelist_file(caplog, driverobj): log.setLevel(logging.DEBUG) datestr = "2024-05-05_12:00:00" - with open(driverobj._driver_config["namelist"]["base_file"], "w", encoding="utf-8") as f: + with open(driverobj.config["namelist"]["base_file"], "w", encoding="utf-8") as f: print("&model_inputs datestr='%s' / &nampgb kpv=88 /" % datestr, file=f) dst = driverobj._rundir / "itag" assert not dst.is_file() @@ -138,8 +137,8 @@ def test_UPP_namelist_file(caplog, driverobj): def test_UPP_namelist_file_fails_validation(caplog, driverobj): log.setLevel(logging.DEBUG) - driverobj._driver_config["namelist"]["update_values"]["nampgb"]["kpo"] = "string" - del driverobj._driver_config["namelist"]["base_file"] + driverobj._config["namelist"]["update_values"]["nampgb"]["kpo"] = "string" + del driverobj._config["namelist"]["base_file"] path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert logged(caplog, f"Failed to validate {path}") @@ -148,8 +147,8 @@ def test_UPP_namelist_file_fails_validation(caplog, driverobj): def test_UPP_namelist_file_missing_base_file(caplog, driverobj): log.setLevel(logging.DEBUG) - base_file = str(Path(driverobj._driver_config["rundir"]) / "missing.nml") - driverobj._driver_config["namelist"]["base_file"] = base_file + base_file = str(Path(driverobj.config["rundir"], "missing.nml")) + driverobj._config["namelist"]["base_file"] = base_file path = Path(refs(driverobj.namelist_file())) assert not path.exists() assert regex_logged(caplog, "missing.nml: State: Not Ready (external asset)") @@ -177,7 +176,7 @@ def test_UPP__namelist_path(driverobj): def test_UPP__runcmd(driverobj): - assert driverobj._runcmd == "%s < itag" % driverobj._driver_config["execution"]["executable"] + assert driverobj._runcmd == "%s < itag" % driverobj.config["execution"]["executable"] def test_UPP__taskname(driverobj): diff --git a/src/uwtools/tests/drivers/test_ww3.py b/src/uwtools/tests/drivers/test_ww3.py index 78a6937f0..9e8dd15aa 100644 --- a/src/uwtools/tests/drivers/test_ww3.py +++ b/src/uwtools/tests/drivers/test_ww3.py @@ -45,18 +45,14 @@ def driverobj(config, cycle): @mark.parametrize( "method", - [ - "_driver_config", - "_taskname", - "_validate", - ], + ["_taskname", "_validate"], ) def test_WaveWatchIII(method): assert getattr(WaveWatchIII, method) is getattr(AssetsCycleBased, method) def test_WaveWatchIII_namelist_file(driverobj): - src = driverobj._driver_config["namelist"]["template_file"] + src = driverobj.config["namelist"]["template_file"] with open(src, "w", encoding="utf-8") as f: yaml.dump({}, f) dst = driverobj._rundir / "ww3_shel.nml" diff --git a/src/uwtools/tests/utils/test_api.py b/src/uwtools/tests/utils/test_api.py index debb95a5b..374cd8cf4 100644 --- a/src/uwtools/tests/utils/test_api.py +++ b/src/uwtools/tests/utils/test_api.py @@ -95,7 +95,7 @@ def test__execute(execute_kwargs, hours, tmp_path): kwargs = { **execute_kwargs, "driver_class": TestDriverCL, - "config": {"some": "config"}, + "config": {"concrete": {"some": "config"}}, "cycle": dt.datetime.now(), "leadtime": dt.timedelta(hours=hours), "graph_file": graph_file,