From 2119d54c22afc353560cd6156fe6de38af8aedf5 Mon Sep 17 00:00:00 2001 From: Paul Madden <136389411+maddenp-noaa@users.noreply.github.com> Date: Tue, 9 Jul 2024 08:28:07 -0600 Subject: [PATCH] Allow API users to obtain copies of driver configs (#521) --- .github/CODEOWNERS | 7 - docs/conf.py | 1 + src/uwtools/drivers/chgres_cube.py | 36 +-- src/uwtools/drivers/driver.py | 259 +++++++++++++++++++--- src/uwtools/drivers/esg_grid.py | 22 +- src/uwtools/drivers/filter_topo.py | 22 +- src/uwtools/drivers/fv3.py | 36 +-- src/uwtools/drivers/global_equiv_resol.py | 22 +- src/uwtools/drivers/ioda.py | 2 + src/uwtools/drivers/jedi_base.py | 36 +-- src/uwtools/drivers/make_hgrid.py | 24 +- src/uwtools/drivers/make_solo_mosaic.py | 24 +- src/uwtools/drivers/mpas_base.py | 36 +-- src/uwtools/drivers/orog_gsl.py | 22 +- src/uwtools/drivers/schism.py | 28 +-- src/uwtools/drivers/sfc_climo_gen.py | 22 +- src/uwtools/drivers/shave.py | 24 +- src/uwtools/drivers/ungrib.py | 37 +--- src/uwtools/drivers/upp.py | 44 +--- src/uwtools/drivers/ww3.py | 28 +-- src/uwtools/tests/drivers/test_driver.py | 200 +++++++++-------- src/uwtools/tests/drivers/test_schism.py | 6 +- src/uwtools/tests/drivers/test_support.py | 6 +- src/uwtools/tests/drivers/test_ww3.py | 6 +- src/uwtools/tests/utils/test_api.py | 25 +-- 25 files changed, 394 insertions(+), 581 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c04a87b5..1121ad809 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,10 +1,3 @@ # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -# Code owners: - * @NaureenBharwaniNOAA @christinaholtNOAA @elcarpenterNOAA @fgabelmannjr @maddenp-noaa @weirae - -# Documentation owners: - -.readthedocs.yaml @christinaholtNOAA @jprestop @maddenp-noaa -docs/* @christinaholtNOAA @jprestop @maddenp-noaa diff --git a/docs/conf.py b/docs/conf.py index 9919fa750..4d027ba06 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,7 @@ with open("../recipe/meta.json", "r", encoding="utf-8") as f: _metadata = json.loads(f.read()) +autoclass_content = "both" autodoc_mock_imports = ["f90nml", "iotaa", "jsonschema", "lxml", "referencing"] copyright = str(dt.datetime.now().year) extensions = ["sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx"] diff --git a/src/uwtools/drivers/chgres_cube.py b/src/uwtools/drivers/chgres_cube.py index 5f51d1687..f26307f7c 100644 --- a/src/uwtools/drivers/chgres_cube.py +++ b/src/uwtools/drivers/chgres_cube.py @@ -2,45 +2,21 @@ A driver for chgres_cube. """ -from datetime import datetime from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverCycleBased from uwtools.strings import STR from uwtools.utils.tasks import file -class ChgresCube(Driver): +class ChgresCube(DriverCycleBased): """ A driver for chgres_cube. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @task @@ -107,11 +83,3 @@ def _driver_name(self) -> str: Returns the name of this driver. """ return STR.chgrescube - - def _taskname(self, suffix: str) -> str: - """ - Returns a common tag for graph-task log messages. - - :param suffix: Log-string suffix. - """ - return self._taskname_with_cycle(self._cycle, suffix) diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py index 95a2f4137..cdf5bf00d 100644 --- a/src/uwtools/drivers/driver.py +++ b/src/uwtools/drivers/driver.py @@ -7,6 +7,7 @@ import re import stat from abc import ABC, abstractmethod +from copy import deepcopy from datetime import datetime, timedelta from functools import partial from pathlib import Path @@ -18,7 +19,7 @@ from uwtools.config.formats.base import Config from uwtools.config.formats.yaml import YAMLConfig from uwtools.config.validator import get_schema_file, validate, validate_internal -from uwtools.exceptions import UWConfigError, UWError +from uwtools.exceptions import UWConfigError from uwtools.logging import log from uwtools.scheduler import JobScheduler from uwtools.utils.processing import execute @@ -31,40 +32,62 @@ class Assets(ABC): def __init__( self, - config: Optional[Union[dict, Path]], - dry_run: bool = False, - batch: bool = False, cycle: Optional[datetime] = None, leadtime: Optional[timedelta] = None, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, key_path: Optional[list[str]] = None, ) -> None: """ A component driver. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? :param cycle: The cycle. :param leadtime: The leadtime. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? :param key_path: Keys leading through the config to the driver's configuration block. """ - dryrun(enable=dry_run) self._config = YAMLConfig(config=config) - self._batch = batch - has_leadtime = leadtime is not None - if has_leadtime and not cycle: - raise UWError("When leadtime is specified, cycle is required") self._config.dereference( context={ **({"cycle": cycle} if cycle else {}), - **({"leadtime": leadtime} if has_leadtime else {}), + **({"leadtime": leadtime} if leadtime is not None else {}), **self._config.data, } ) - key_path = key_path or [] - for key in key_path: + for key in key_path or []: self._config = self._config[key] self._validate() + dryrun(enable=dry_run) + + def __repr__(self) -> str: + cycle = self._cycle.strftime("%Y-%m-%dT%H:%M") if hasattr(self, "_cycle") else None + leadtime = None + if hasattr(self, "_leadtime"): + 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["rundir"]]) + ) + + def __str__(self) -> str: + return self._driver_name + + @property + def config(self) -> dict: + """ + Return a copy of the driver config. + """ + return deepcopy(self._driver_config) + + @property + def config_full(self) -> dict: + """ + Return a copy of the full input config. + """ + full_config: dict = self._config.data + return deepcopy(full_config) # Workflow tasks @@ -174,36 +197,95 @@ def _taskname(self, suffix: str) -> str: :param suffix: Log-string suffix. """ - return "%s %s" % (self._driver_name, suffix) + cycle = getattr(self, "_cycle", None) + leadtime = getattr(self, "_leadtime", None) + timestr = ( + (cycle + leadtime).strftime("%Y%m%d %H:%M:%S") + if cycle and leadtime is not None + else cycle.strftime("%Y%m%d %HZ") if cycle else None + ) + return " ".join(filter(None, [timestr, self._driver_name, suffix])) - def _taskname_with_cycle(self, cycle: datetime, suffix: str) -> str: + def _validate(self) -> None: """ - Returns a common tag for graph-task log messages. + Perform all necessary schema validation. + """ + schema_name = self._driver_name.replace("_", "-") + validate_internal(schema_name=schema_name, config=self._config) - :param suffix: Log-string suffix. + +class AssetsCycleBased(Assets): + """ + An abstract class to provision assets for cycle-based components. + """ + + def __init__( + self, + cycle: datetime, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + ): """ - return "%s %s %s" % (cycle.strftime("%Y%m%d %HZ"), self._driver_name, suffix) + The driver. - def _taskname_with_cycle_and_leadtime( - self, cycle: datetime, leadtime: timedelta, suffix: str - ) -> str: + :param cycle: The cycle. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. """ - Returns a common tag for graph-task log messages. + super().__init__(cycle=cycle, config=config, dry_run=dry_run, key_path=key_path) + self._cycle = cycle - :param suffix: Log-string suffix. + +class AssetsCycleAndLeadtimeBased(Assets): + """ + An abstract class to provision assets for cycle-and-leadtime-based components. + """ + + def __init__( + self, + cycle: datetime, + leadtime: timedelta, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + ): + """ + The driver. + + :param cycle: The cycle. + :param leadtime: The leadtime. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. """ - return "%s %s %s" % ( - (cycle + leadtime).strftime("%Y%m%d %H:%M:%S"), - self._driver_name, - suffix, + super().__init__( + cycle=cycle, leadtime=leadtime, config=config, dry_run=dry_run, key_path=key_path ) + self._cycle = cycle + self._leadtime = leadtime - def _validate(self) -> None: + +class AssetsTimeInvariant(Assets): + """ + An abstract class to provision assets for time-invariant components. + """ + + def __init__( + self, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + ): """ - Perform all necessary schema validation. + The driver. + + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. """ - schema_name = self._driver_name.replace("_", "-") - validate_internal(schema_name=schema_name, config=self._config) + super().__init__(config=config, dry_run=dry_run, key_path=key_path) class Driver(Assets): @@ -211,6 +293,30 @@ class Driver(Assets): An abstract class for standalone component drivers. """ + def __init__( + self, + cycle: Optional[datetime] = None, + leadtime: Optional[timedelta] = None, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + batch: bool = False, + ): + """ + The driver. + + :param cycle: The cycle. + :param leadtime: The leadtime. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. + :param batch: Run component via the batch system? + """ + super().__init__( + cycle=cycle, leadtime=leadtime, config=config, dry_run=dry_run, key_path=key_path + ) + self._batch = batch + # Workflow tasks @tasks @@ -371,4 +477,91 @@ def _write_runscript(self, path: Path, envvars: Optional[dict[str, str]] = None) os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) +class DriverCycleBased(Driver): + """ + An abstract class for standalone cycle-based component drivers. + """ + + def __init__( + self, + cycle: datetime, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + batch: bool = False, + ): + """ + The driver. + + :param cycle: The cycle. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. + :param batch: Run component via the batch system? + """ + super().__init__( + cycle=cycle, config=config, dry_run=dry_run, key_path=key_path, batch=batch + ) + self._cycle = cycle + + +class DriverCycleAndLeadtimeBased(Driver): + """ + An abstract class for standalone cycle-and-leadtime-based component drivers. + """ + + def __init__( + self, + cycle: datetime, + leadtime: timedelta, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + batch: bool = False, + ): + """ + The driver. + + :param cycle: The cycle. + :param leadtime: The leadtime. + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. + :param batch: Run component via the batch system? + """ + super().__init__( + cycle=cycle, + leadtime=leadtime, + config=config, + dry_run=dry_run, + key_path=key_path, + batch=batch, + ) + self._cycle = cycle + self._leadtime = leadtime + + +class DriverTimeInvariant(Driver): + """ + An abstract class for standalone time-invariant component drivers. + """ + + def __init__( + self, + config: Optional[Union[dict, Path]] = None, + dry_run: bool = False, + key_path: Optional[list[str]] = None, + batch: bool = False, + ): + """ + The driver. + + :param config: Path to config file (read stdin if missing or None). + :param dry_run: Run in dry-run mode? + :param key_path: Keys leading through the config to the driver's configuration block. + :param batch: Run component via the batch system? + """ + super().__init__(config=config, dry_run=dry_run, key_path=key_path, batch=batch) + + DriverT = Union[type[Assets], type[Driver]] diff --git a/src/uwtools/drivers/esg_grid.py b/src/uwtools/drivers/esg_grid.py index d459605d7..e7eca1670 100644 --- a/src/uwtools/drivers/esg_grid.py +++ b/src/uwtools/drivers/esg_grid.py @@ -3,38 +3,20 @@ """ from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR from uwtools.utils.tasks import file -class ESGGrid(Driver): +class ESGGrid(DriverTimeInvariant): """ A driver for esg_grid. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @task diff --git a/src/uwtools/drivers/filter_topo.py b/src/uwtools/drivers/filter_topo.py index 87f3f4842..0cbb720a2 100644 --- a/src/uwtools/drivers/filter_topo.py +++ b/src/uwtools/drivers/filter_topo.py @@ -3,38 +3,20 @@ """ from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR from uwtools.utils.tasks import symlink -class FilterTopo(Driver): +class FilterTopo(DriverTimeInvariant): """ A driver for filter_topo. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @task diff --git a/src/uwtools/drivers/fv3.py b/src/uwtools/drivers/fv3.py index e325b189d..816c95fab 100644 --- a/src/uwtools/drivers/fv3.py +++ b/src/uwtools/drivers/fv3.py @@ -2,48 +2,24 @@ A driver for the FV3 model. """ -from datetime import datetime from pathlib import Path from shutil import copy -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig from uwtools.config.formats.yaml import YAMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverCycleBased from uwtools.logging import log from uwtools.strings import STR from uwtools.utils.tasks import file, filecopy, symlink -class FV3(Driver): +class FV3(DriverCycleBased): """ A driver for the FV3 model. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @tasks @@ -207,11 +183,3 @@ def _driver_name(self) -> str: Returns the name of this driver. """ return STR.fv3 - - def _taskname(self, suffix: str) -> str: - """ - Returns a common tag for graph-task log messages. - - :param suffix: Log-string suffix. - """ - return self._taskname_with_cycle(self._cycle, suffix) diff --git a/src/uwtools/drivers/global_equiv_resol.py b/src/uwtools/drivers/global_equiv_resol.py index 368f9015f..c946febf9 100644 --- a/src/uwtools/drivers/global_equiv_resol.py +++ b/src/uwtools/drivers/global_equiv_resol.py @@ -3,36 +3,18 @@ """ from pathlib import Path -from typing import Optional from iotaa import asset, external, tasks -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR -class GlobalEquivResol(Driver): +class GlobalEquivResol(DriverTimeInvariant): """ A driver for global_equiv_resol. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @external diff --git a/src/uwtools/drivers/ioda.py b/src/uwtools/drivers/ioda.py index 03d4d313e..cb120bf08 100644 --- a/src/uwtools/drivers/ioda.py +++ b/src/uwtools/drivers/ioda.py @@ -13,6 +13,8 @@ class IODA(JEDIBase): A driver for the IODA component. """ + # Workflow tasks + @tasks def provisioned_rundir(self): """ diff --git a/src/uwtools/drivers/jedi_base.py b/src/uwtools/drivers/jedi_base.py index d05a2e124..3c965bab6 100644 --- a/src/uwtools/drivers/jedi_base.py +++ b/src/uwtools/drivers/jedi_base.py @@ -3,44 +3,20 @@ """ from abc import abstractmethod -from datetime import datetime from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.yaml import YAMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverCycleBased from uwtools.utils.tasks import file, filecopy, symlink -class JEDIBase(Driver): +class JEDIBase(DriverCycleBased): """ A base class for the JEDI-like drivers. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The forecast cycle. - :param config: Path to config file. - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @task @@ -97,11 +73,3 @@ def _config_fn(self) -> str: """ Returns the name of the config file used in execution. """ - - def _taskname(self, suffix: str) -> str: - """ - Returns a common tag for graph-task log messages. - - :param suffix: Log-string suffix. - """ - return self._taskname_with_cycle(self._cycle, suffix) diff --git a/src/uwtools/drivers/make_hgrid.py b/src/uwtools/drivers/make_hgrid.py index 04209fbcf..1d3ead8cd 100644 --- a/src/uwtools/drivers/make_hgrid.py +++ b/src/uwtools/drivers/make_hgrid.py @@ -2,37 +2,17 @@ A driver for make_hgrid. """ -from pathlib import Path -from typing import Optional - from iotaa import tasks -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR -class MakeHgrid(Driver): +class MakeHgrid(DriverTimeInvariant): """ A driver for make_hgrid. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @tasks diff --git a/src/uwtools/drivers/make_solo_mosaic.py b/src/uwtools/drivers/make_solo_mosaic.py index 299ee40ac..34bc83587 100644 --- a/src/uwtools/drivers/make_solo_mosaic.py +++ b/src/uwtools/drivers/make_solo_mosaic.py @@ -2,37 +2,17 @@ A driver for make_solo_mosaic. """ -from pathlib import Path -from typing import Optional - from iotaa import tasks -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR -class MakeSoloMosaic(Driver): +class MakeSoloMosaic(DriverTimeInvariant): """ A driver for make_solo_mosaic. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch) - # Workflow tasks @tasks diff --git a/src/uwtools/drivers/mpas_base.py b/src/uwtools/drivers/mpas_base.py index 7f86370f3..851b29a08 100644 --- a/src/uwtools/drivers/mpas_base.py +++ b/src/uwtools/drivers/mpas_base.py @@ -3,45 +3,21 @@ """ from abc import abstractmethod -from datetime import datetime from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from lxml import etree from lxml.etree import Element, SubElement -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverCycleBased from uwtools.utils.tasks import filecopy, symlink -class MPASBase(Driver): +class MPASBase(DriverCycleBased): """ A base class for MPAS drivers. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config_file: Path to config file (read stdin if missing or None). - :param cycle: The cycle. - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, cycle=cycle, dry_run=dry_run, batch=batch, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @tasks @@ -140,11 +116,3 @@ def _streams_fn(self) -> str: """ The streams filename. """ - - def _taskname(self, suffix: str) -> str: - """ - Returns a common tag for graph-task log messages. - - :param suffix: Log-string suffix. - """ - return self._taskname_with_cycle(self._cycle, suffix) diff --git a/src/uwtools/drivers/orog_gsl.py b/src/uwtools/drivers/orog_gsl.py index 2183f2b2d..9af84a59c 100644 --- a/src/uwtools/drivers/orog_gsl.py +++ b/src/uwtools/drivers/orog_gsl.py @@ -3,37 +3,19 @@ """ from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR from uwtools.utils.tasks import symlink -class OrogGSL(Driver): +class OrogGSL(DriverTimeInvariant): """ A driver for orog_gsl. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @task diff --git a/src/uwtools/drivers/schism.py b/src/uwtools/drivers/schism.py index 655d57b1b..bc6806e09 100644 --- a/src/uwtools/drivers/schism.py +++ b/src/uwtools/drivers/schism.py @@ -2,45 +2,21 @@ An assets driver for SCHISM. """ -from datetime import datetime from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.api.template import render -from uwtools.drivers.driver import Assets +from uwtools.drivers.driver import AssetsCycleBased from uwtools.strings import STR from uwtools.utils.tasks import file -class SCHISM(Assets): +class SCHISM(AssetsCycleBased): """ An assets driver for SCHISM. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @task diff --git a/src/uwtools/drivers/sfc_climo_gen.py b/src/uwtools/drivers/sfc_climo_gen.py index f34a883b0..3cf6bca38 100644 --- a/src/uwtools/drivers/sfc_climo_gen.py +++ b/src/uwtools/drivers/sfc_climo_gen.py @@ -3,38 +3,20 @@ """ from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR from uwtools.utils.tasks import file -class SfcClimoGen(Driver): +class SfcClimoGen(DriverTimeInvariant): """ A driver for sfc_climo_gen. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @task diff --git a/src/uwtools/drivers/shave.py b/src/uwtools/drivers/shave.py index 242ad57a9..af41306f3 100644 --- a/src/uwtools/drivers/shave.py +++ b/src/uwtools/drivers/shave.py @@ -2,37 +2,17 @@ A driver for shave. """ -from pathlib import Path -from typing import Optional - from iotaa import tasks -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant from uwtools.strings import STR -class Shave(Driver): +class Shave(DriverTimeInvariant): """ A driver for shave. """ - def __init__( - self, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__(config=config, dry_run=dry_run, batch=batch, key_path=key_path) - # Workflow tasks @tasks diff --git a/src/uwtools/drivers/ungrib.py b/src/uwtools/drivers/ungrib.py index ce719b786..1b79871a0 100644 --- a/src/uwtools/drivers/ungrib.py +++ b/src/uwtools/drivers/ungrib.py @@ -2,45 +2,22 @@ A driver for the ungrib component. """ -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverCycleBased from uwtools.strings import STR from uwtools.utils.tasks import file -class Ungrib(Driver): +class Ungrib(DriverCycleBased): """ A driver for ungrib. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @tasks @@ -148,14 +125,6 @@ def _gribfile(self, infile: Path, link: Path): link.parent.mkdir(parents=True, exist_ok=True) link.symlink_to(infile) - def _taskname(self, suffix: str) -> str: - """ - Returns a common tag for graph-task log messages. - - :param suffix: Log-string suffix. - """ - return self._taskname_with_cycle(self._cycle, suffix) - def _ext(n): """ diff --git a/src/uwtools/drivers/upp.py b/src/uwtools/drivers/upp.py index b1ea0d81c..866d34ca4 100644 --- a/src/uwtools/drivers/upp.py +++ b/src/uwtools/drivers/upp.py @@ -2,53 +2,21 @@ A driver for UPP. """ -from datetime import datetime, timedelta from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.config.formats.nml import NMLConfig -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverCycleAndLeadtimeBased from uwtools.strings import STR from uwtools.utils.tasks import file, filecopy, symlink -class UPP(Driver): +class UPP(DriverCycleAndLeadtimeBased): """ A driver for UPP. """ - def __init__( - self, - cycle: datetime, - leadtime: timedelta, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The cycle. - :param leadtime: The leadtime. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, - dry_run=dry_run, - batch=batch, - cycle=cycle, - key_path=key_path, - leadtime=leadtime, - ) - self._cycle = cycle - self._leadtime = leadtime - # Workflow tasks @tasks @@ -133,11 +101,3 @@ def _runcmd(self) -> str: "%s < %s" % (execution["executable"], self._namelist_path.name), ] return " ".join(filter(None, components)) - - def _taskname(self, suffix: str) -> str: - """ - Returns a common tag for graph-task log messages. - - :param suffix: Log-string suffix. - """ - return self._taskname_with_cycle_and_leadtime(self._cycle, self._leadtime, suffix) diff --git a/src/uwtools/drivers/ww3.py b/src/uwtools/drivers/ww3.py index 4414d5685..864ec2caf 100644 --- a/src/uwtools/drivers/ww3.py +++ b/src/uwtools/drivers/ww3.py @@ -2,45 +2,21 @@ An assets driver for ww3. """ -from datetime import datetime from pathlib import Path -from typing import Optional from iotaa import asset, task, tasks from uwtools.api.template import render -from uwtools.drivers.driver import Assets +from uwtools.drivers.driver import AssetsCycleBased from uwtools.strings import STR from uwtools.utils.tasks import file -class WaveWatchIII(Assets): +class WaveWatchIII(AssetsCycleBased): """ A library driver for ww3. """ - def __init__( - self, - cycle: datetime, - config: Optional[Path] = None, - dry_run: bool = False, - batch: bool = False, - key_path: Optional[list[str]] = None, - ): - """ - The driver. - - :param cycle: The cycle. - :param config: Path to config file (read stdin if missing or None). - :param dry_run: Run in dry-run mode? - :param batch: Run component via the batch system? - :param key_path: Keys leading through the config to the driver's configuration block. - """ - super().__init__( - config=config, dry_run=dry_run, batch=batch, cycle=cycle, key_path=key_path - ) - self._cycle = cycle - # Workflow tasks @task diff --git a/src/uwtools/tests/drivers/test_driver.py b/src/uwtools/tests/drivers/test_driver.py index 84b1d2a86..fdd0c2b51 100644 --- a/src/uwtools/tests/drivers/test_driver.py +++ b/src/uwtools/tests/drivers/test_driver.py @@ -1,4 +1,7 @@ -# pylint: disable=missing-function-docstring,protected-access,redefined-outer-name +# pylint: disable=missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=protected-access +# pylint: disable=redefined-outer-name """ Tests for uwtools.drivers.driver module. """ @@ -15,7 +18,7 @@ from uwtools.config.formats.yaml import YAMLConfig from uwtools.drivers import driver -from uwtools.exceptions import UWConfigError, UWError +from uwtools.exceptions import UWConfigError from uwtools.logging import log from uwtools.scheduler import Slurm from uwtools.tests.support import regex_logged @@ -23,10 +26,14 @@ # Helpers -class ConcreteAssets(driver.Assets): - """ - Driver subclass for testing purposes. - """ +class Common: + + __test__ = False + + @task + def atask(self): + yield "atask" + yield asset("atask", lambda: True) def provisioned_rundir(self): pass @@ -35,40 +42,32 @@ def provisioned_rundir(self): def _driver_name(self) -> str: return "concrete" - def _taskname(self, suffix: str) -> str: - return "concrete" - def _validate(self) -> None: pass - @task - def atask(self): - yield "atask" - yield asset("atask", lambda: True) +class ConcreteAssetsCycleBased(Common, driver.AssetsCycleBased): + pass -class ConcreteDriver(driver.Driver): - """ - Driver subclass for testing purposes. - """ - def provisioned_rundir(self): - pass +class ConcreteAssetsCycleAndLeadtimeBased(Common, driver.AssetsCycleAndLeadtimeBased): + pass - @property - def _driver_name(self) -> str: - return "concrete" - def _taskname(self, suffix: str) -> str: - return "concrete" +class ConcreteAssetsTimeInvariant(Common, driver.AssetsTimeInvariant): + pass - def _validate(self) -> None: - pass - @task - def atask(self): - yield "atask" - yield asset("atask", lambda: True) +class ConcreteDriverCycleBased(Common, driver.DriverCycleBased): + pass + + +class ConcreteDriverCycleAndLeadtimeBased(Common, driver.DriverCycleAndLeadtimeBased): + pass + + +class ConcreteDriverTimeInvariant(Common, driver.DriverTimeInvariant): + pass def write(path, s): @@ -108,46 +107,64 @@ def config(tmp_path): @fixture -def assetobj(config): - return ConcreteAssets( - config=config, - dry_run=False, - batch=True, - cycle=dt.datetime(2024, 3, 22, 18), - leadtime=dt.timedelta(hours=24), - ) +def assetsobj(config): + return ConcreteAssetsTimeInvariant(config=config, dry_run=False) @fixture def driverobj(config): - return ConcreteDriver( - config=config, - dry_run=False, - batch=True, - cycle=dt.datetime(2024, 3, 22, 18), - leadtime=dt.timedelta(hours=24), - ) + return ConcreteDriverTimeInvariant(config=config, dry_run=False, batch=True) # Assets Tests -def test_Assets(assetobj): - assert Path(assetobj._driver_config["base_file"]).name == "base.yaml" - assert assetobj._batch is True +def test_Assets(assetsobj): + assert Path(assetsobj._driver_config["base_file"]).name == "base.yaml" + +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"] + assert repr(obj) == expected -@mark.parametrize("hours", [0, 24, 168]) -def test_Assets_cycle_leadtime_error(config, hours): - with raises(UWError) as e: - ConcreteAssets(config=config, leadtime=dt.timedelta(hours=hours)) - assert "When leadtime is specified, cycle is required" in str(e) + +def test_Assets_repr_cycle_and_leadtime_based(config): + obj = ConcreteAssetsCycleAndLeadtimeBased( + 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"] + assert repr(obj) == expected + + +def test_Assets_repr_time_invariant(config): + obj = ConcreteAssetsTimeInvariant(config=config) + expected = "concrete in %s" % obj._driver_config["rundir"] + assert repr(obj) == expected + + +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 + # But they are separate objects: + assert not assetsobj.config is assetsobj._driver_config + + +def test_Assets_config_full(assetsobj): + # The user-accessible object is equivalent to the internal driver config: + assert assetsobj.config_full == assetsobj._config + # But they are separate objects: + assert not assetsobj.config_full is assetsobj._config @mark.parametrize("val", (True, False)) def test_Assets_dry_run(config, val): with patch.object(driver, "dryrun") as dryrun: - ConcreteAssets(config=config, dry_run=val) + ConcreteAssetsTimeInvariant(config=config, dry_run=val) dryrun.assert_called_once_with(enable=val) @@ -157,20 +174,15 @@ def test_Assets_dry_run(config, val): def test_key_path(config, tmp_path): config_file = tmp_path / "config.yaml" config_file.write_text(yaml.dump({"foo": {"bar": config}})) - assetobj = ConcreteAssets( - config=config_file, - dry_run=False, - batch=True, - cycle=dt.datetime(2024, 3, 22, 18), - key_path=["foo", "bar"], - leadtime=dt.timedelta(hours=24), + assetsobj = ConcreteAssetsTimeInvariant( + config=config_file, dry_run=False, key_path=["foo", "bar"] ) - assert config == assetobj._config + assert config == assetsobj._config -def test_Assets_validate(assetobj, caplog): +def test_Assets_validate(assetsobj, caplog): log.setLevel(logging.INFO) - assetobj.validate() + assetsobj.validate() assert regex_logged(caplog, "State: Ready") @@ -187,29 +199,31 @@ def test_Assets_validate(assetobj, caplog): ], ) def test_Assets__create_user_updated_config_base_file( - assetobj, base_file, expected, tmp_path, update_values + assetsobj, base_file, expected, tmp_path, update_values ): path = tmp_path / "updated.yaml" - dc = assetobj._driver_config + dc = assetsobj._driver_config if not base_file: del dc["base_file"] if not update_values: del dc["update_values"] - ConcreteAssets._create_user_updated_config(config_class=YAMLConfig, config_values=dc, path=path) + ConcreteAssetsTimeInvariant._create_user_updated_config( + config_class=YAMLConfig, config_values=dc, path=path + ) with open(path, "r", encoding="utf-8") as f: updated = yaml.safe_load(f) assert updated == expected -def test_Assets__driver_config_fail(assetobj): - del assetobj._config["concrete"] +def test_Assets__driver_config_fail(assetsobj): + del assetsobj._config["concrete"] with raises(UWConfigError) as e: - assert assetobj._driver_config + assert assetsobj._driver_config assert str(e.value) == "Required 'concrete' block missing in config" -def test_Assets__driver_config_pass(assetobj): - assert set(assetobj._driver_config.keys()) == { +def test_Assets__driver_config_pass(assetsobj): + assert set(assetsobj._driver_config.keys()) == { "base_file", "execution", "rundir", @@ -217,17 +231,17 @@ def test_Assets__driver_config_pass(assetobj): } -def test_Assets__rundir(assetobj): - assert assetobj._rundir == Path(assetobj._driver_config["rundir"]) +def test_Assets__rundir(assetsobj): + assert assetsobj._rundir == Path(assetsobj._driver_config["rundir"]) -def test_Assets__validate(assetobj): - with patch.object(assetobj, "_validate", driver.Assets._validate): +def test_Assets__validate(assetsobj): + with patch.object(assetsobj, "_validate", driver.Assets._validate): with patch.object(driver, "validate_internal") as validate_internal: - assetobj._validate(assetobj) + assetsobj._validate(assetsobj) assert validate_internal.call_args_list[0].kwargs == { "schema_name": "concrete", - "config": assetobj._config, + "config": assetsobj._config, } @@ -273,7 +287,9 @@ def test_Driver__run_via_batch_submission(driverobj): executable = Path(driverobj._driver_config["execution"]["executable"]) executable.touch() with patch.object(driverobj, "provisioned_rundir") as prd: - with patch.object(ConcreteDriver, "_scheduler", new_callable=PropertyMock) as scheduler: + with patch.object( + ConcreteDriverTimeInvariant, "_scheduler", new_callable=PropertyMock + ) as scheduler: driverobj._run_via_batch_submission() scheduler().submit_job.assert_called_once_with( runscript=runscript, submit_file=Path(f"{runscript}.submit") @@ -316,7 +332,9 @@ def test_Driver__create_user_updated_config_base_file( del dc["base_file"] if not update_values: del dc["update_values"] - ConcreteDriver._create_user_updated_config(config_class=YAMLConfig, config_values=dc, path=path) + ConcreteDriverTimeInvariant._create_user_updated_config( + config_class=YAMLConfig, config_values=dc, path=path + ) with open(path, "r", encoding="utf-8") as f: updated = yaml.safe_load(f) assert updated == expected @@ -344,7 +362,9 @@ def test_Driver__namelist_schema_custom(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(ConcreteDriver, "_driver_config", new_callable=PropertyMock) as dc: + with patch.object( + ConcreteDriverTimeInvariant, "_driver_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 ( @@ -363,14 +383,18 @@ 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(ConcreteDriver, "_driver_config", new_callable=PropertyMock) as dc: + with patch.object( + ConcreteDriverTimeInvariant, "_driver_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(ConcreteDriver, "_driver_config", new_callable=PropertyMock) as dc: + with patch.object( + ConcreteDriverTimeInvariant, "_driver_config", new_callable=PropertyMock + ) as dc: dc.return_value = {"namelist": {"validate": False}} assert driverobj._namelist_schema() == {"type": "object"} @@ -455,17 +479,17 @@ def test_Driver__scheduler(driverobj): JobScheduler.get_scheduler.assert_called_with(driverobj._resources) -def test_Driver__validate(assetobj): - with patch.object(assetobj, "_validate", driver.Driver._validate): +def test_Driver__validate(assetsobj): + with patch.object(assetsobj, "_validate", driver.Driver._validate): with patch.object(driver, "validate_internal") as validate_internal: - assetobj._validate(assetobj) + assetsobj._validate(assetsobj) assert validate_internal.call_args_list[0].kwargs == { "schema_name": "concrete", - "config": assetobj._config, + "config": assetsobj._config, } assert validate_internal.call_args_list[1].kwargs == { "schema_name": "platform", - "config": assetobj._config, + "config": assetsobj._config, } diff --git a/src/uwtools/tests/drivers/test_schism.py b/src/uwtools/tests/drivers/test_schism.py index 7a775073c..c9f76cb3e 100644 --- a/src/uwtools/tests/drivers/test_schism.py +++ b/src/uwtools/tests/drivers/test_schism.py @@ -9,7 +9,7 @@ import yaml from pytest import fixture, mark -from uwtools.drivers.driver import Assets +from uwtools.drivers.driver import AssetsCycleBased from uwtools.drivers.schism import SCHISM # Fixtures @@ -37,7 +37,7 @@ def cycle(): @fixture def driverobj(config, cycle): - return SCHISM(config=config, cycle=cycle, batch=True) + return SCHISM(config=config, cycle=cycle) # Tests @@ -52,7 +52,7 @@ def driverobj(config, cycle): ], ) def test_SCHISM(method): - assert getattr(SCHISM, method) is getattr(Assets, method) + assert getattr(SCHISM, method) is getattr(AssetsCycleBased, method) def test_SCHISM_namelist_file(driverobj): diff --git a/src/uwtools/tests/drivers/test_support.py b/src/uwtools/tests/drivers/test_support.py index dae1af27a..6d4a4a62c 100644 --- a/src/uwtools/tests/drivers/test_support.py +++ b/src/uwtools/tests/drivers/test_support.py @@ -5,7 +5,7 @@ from iotaa import asset, external, task, tasks from uwtools.drivers import support -from uwtools.drivers.driver import Driver +from uwtools.drivers.driver import DriverTimeInvariant def test_graph(): @@ -17,9 +17,7 @@ def ready(): ready() assert support.graph().startswith("digraph") - -def test_tasks(): - class SomeDriver(Driver): + class SomeDriver(DriverTimeInvariant): def provisioned_rundir(self): pass diff --git a/src/uwtools/tests/drivers/test_ww3.py b/src/uwtools/tests/drivers/test_ww3.py index c59a4b9ef..bebcc69cd 100644 --- a/src/uwtools/tests/drivers/test_ww3.py +++ b/src/uwtools/tests/drivers/test_ww3.py @@ -9,7 +9,7 @@ import yaml from pytest import fixture, mark -from uwtools.drivers.driver import Assets +from uwtools.drivers.driver import AssetsCycleBased from uwtools.drivers.ww3 import WaveWatchIII # Fixtures @@ -37,7 +37,7 @@ def cycle(): @fixture def driverobj(config, cycle): - return WaveWatchIII(config=config, cycle=cycle, batch=True) + return WaveWatchIII(config=config, cycle=cycle) # Tests @@ -52,7 +52,7 @@ def driverobj(config, cycle): ], ) def test_WaveWatchIII(method): - assert getattr(WaveWatchIII, method) is getattr(Assets, method) + assert getattr(WaveWatchIII, method) is getattr(AssetsCycleBased, method) def test_WaveWatchIII_namelist_file(driverobj): diff --git a/src/uwtools/tests/utils/test_api.py b/src/uwtools/tests/utils/test_api.py index a06c58b8a..d3e6b1ee5 100644 --- a/src/uwtools/tests/utils/test_api.py +++ b/src/uwtools/tests/utils/test_api.py @@ -1,14 +1,15 @@ # pylint: disable=missing-function-docstring,protected-access,redefined-outer-name import datetime as dt +import sys from pathlib import Path from unittest.mock import patch from pytest import fixture, mark, raises from uwtools.exceptions import UWError -from uwtools.tests.drivers import test_driver -from uwtools.tests.drivers.test_driver import ConcreteDriver +from uwtools.tests.drivers.test_driver import ConcreteDriverCycleAndLeadtimeBased as TestDriverWCL +from uwtools.tests.drivers.test_driver import ConcreteDriverTimeInvariant as TestDriver from uwtools.utils import api @@ -48,7 +49,7 @@ def test_ensure_data_source_str_to_path(): def test_make_execute(execute_kwargs): - func = api.make_execute(driver_class=ConcreteDriver, with_cycle=False) + func = api.make_execute(driver_class=TestDriver, with_cycle=False) assert func.__name__ == "execute" assert func.__doc__ is not None assert ":param cycle:" not in func.__doc__ @@ -57,13 +58,13 @@ def test_make_execute(execute_kwargs): with patch.object(api, "_execute", return_value=True) as _execute: assert func(**execute_kwargs) is True _execute.assert_called_once_with( - driver_class=ConcreteDriver, cycle=None, leadtime=None, **execute_kwargs + driver_class=TestDriver, cycle=None, leadtime=None, **execute_kwargs ) def test_make_execute_cycle(execute_kwargs): execute_kwargs["cycle"] = dt.datetime.now() - func = api.make_execute(driver_class=ConcreteDriver, with_cycle=True) + func = api.make_execute(driver_class=TestDriver, with_cycle=True) assert func.__name__ == "execute" assert func.__doc__ is not None assert ":param cycle:" in func.__doc__ @@ -71,15 +72,13 @@ def test_make_execute_cycle(execute_kwargs): assert ":param task:" in func.__doc__ with patch.object(api, "_execute", return_value=True) as _execute: assert func(**execute_kwargs) is True - _execute.assert_called_once_with( - driver_class=ConcreteDriver, leadtime=None, **execute_kwargs - ) + _execute.assert_called_once_with(driver_class=TestDriver, leadtime=None, **execute_kwargs) def test_make_execute_cycle_leadtime(execute_kwargs): execute_kwargs["cycle"] = dt.datetime.now() execute_kwargs["leadtime"] = dt.timedelta(hours=24) - func = api.make_execute(driver_class=ConcreteDriver, with_cycle=True, with_leadtime=True) + func = api.make_execute(driver_class=TestDriver, with_cycle=True, with_leadtime=True) assert func.__name__ == "execute" assert func.__doc__ is not None assert ":param cycle:" in func.__doc__ @@ -88,18 +87,18 @@ def test_make_execute_cycle_leadtime(execute_kwargs): assert ":param task:" in func.__doc__ with patch.object(api, "_execute", return_value=True) as _execute: assert func(**execute_kwargs) is True - _execute.assert_called_once_with(driver_class=ConcreteDriver, **execute_kwargs) + _execute.assert_called_once_with(driver_class=TestDriver, **execute_kwargs) def test_make_execute_leadtime_no_cycle_error(execute_kwargs): execute_kwargs["leadtime"] = dt.timedelta(hours=24) with raises(UWError) as e: - api.make_execute(driver_class=ConcreteDriver, with_leadtime=True) + api.make_execute(driver_class=TestDriver, with_leadtime=True) assert "When leadtime is specified, cycle is required" in str(e) def test_make_tasks(): - func = api.make_tasks(driver_class=ConcreteDriver) + func = api.make_tasks(driver_class=TestDriver) assert func.__name__ == "tasks" taskmap = func() assert list(taskmap.keys()) == ["atask", "run", "runscript", "validate"] @@ -120,7 +119,7 @@ def test_str2path_convert(): @mark.parametrize("hours", [0, 24, 168]) def test__execute(execute_kwargs, hours, tmp_path): graph_file = tmp_path / "g.dot" - with patch.object(test_driver, "ConcreteDriver", wraps=test_driver.ConcreteDriver) as cd: + with patch.object(sys.modules[__name__], "TestDriverWCL", wraps=TestDriverWCL) as cd: kwargs = { **execute_kwargs, "driver_class": cd,