diff --git a/recipe/meta.json b/recipe/meta.json index 4818b42d5..4cf069980 100644 --- a/recipe/meta.json +++ b/recipe/meta.json @@ -8,7 +8,7 @@ "coverage =7.4.*", "docformatter =1.7.*", "f90nml =1.4.*", - "iotaa =0.8.*", + "iotaa =0.7.*", "isort =5.13.*", "jinja2 =3.1.*", "jq =1.7.*", @@ -24,7 +24,7 @@ ], "run": [ "f90nml =1.4.*", - "iotaa =0.8.*", + "iotaa =0.7.*", "jinja2 =3.1.*", "jsonschema =4.21.*", "lxml =5.1.*", diff --git a/recipe/meta.yaml b/recipe/meta.yaml index a1e530a2a..cdf4234e0 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -14,7 +14,7 @@ requirements: - pip run: - f90nml 1.4.* - - iotaa 0.8.* + - iotaa 0.7.* - jinja2 3.1.* - jsonschema 4.21.* - lxml 5.1.* diff --git a/src/uwtools/drivers/driver.py b/src/uwtools/drivers/driver.py index 48098c44e..3dde13ce0 100644 --- a/src/uwtools/drivers/driver.py +++ b/src/uwtools/drivers/driver.py @@ -20,7 +20,6 @@ from uwtools.logging import log from uwtools.scheduler import JobScheduler from uwtools.utils.processing import execute -from uwtools.utils.tasks import file class Driver(ABC): @@ -85,10 +84,7 @@ def _run_via_batch_submission(self): yield self._taskname("run via batch submission") path = Path("%s.submit" % self._runscript_path) yield asset(path, path.is_file) - yield [ - self.provisioned_run_directory(), - file(Path(self._driver_config["execution"]["executable"])), - ] + yield self.provisioned_run_directory() self._scheduler.submit_job(runscript=self._runscript_path, submit_file=path) @task @@ -99,10 +95,7 @@ def _run_via_local_execution(self): yield self._taskname("run via local execution") path = self._rundir / f"done.{self._driver_name}" yield asset(path, path.is_file) - yield [ - self.provisioned_run_directory(), - file(Path(self._driver_config["execution"]["executable"])), - ] + yield self.provisioned_run_directory() cmd = "{x} >{x}.out 2>&1".format(x=self._runscript_path) execute(cmd=cmd, cwd=self._rundir, log_output=True) diff --git a/src/uwtools/tests/utils/test_tasks.py b/src/uwtools/tests/utils/test_tasks.py index f9c552a78..84fab86b6 100644 --- a/src/uwtools/tests/utils/test_tasks.py +++ b/src/uwtools/tests/utils/test_tasks.py @@ -1,42 +1,67 @@ # pylint: disable=missing-function-docstring import os +import stat +from unittest.mock import patch from uwtools.utils import tasks +# Helpers + + +def ready(taskval): + return taskval.ready() + + +# Tests + + +def test_tasks_executable(tmp_path): + p = tmp_path / "program" + # Ensure that only our temp directory is on the path: + with patch.dict(os.environ, {"PATH": str(tmp_path)}, clear=True): + # Program does not exist: + assert not ready(tasks.executable(program=p)) + # Program exists but is not executable: + p.touch() + assert not ready(tasks.executable(program=p)) + # Program exists and is executable: + os.chmod(p, os.stat(p).st_mode | stat.S_IEXEC) # set executable bits + assert ready(tasks.executable(program=p)) + def test_tasks_existing_missing(tmp_path): path = tmp_path / "x" - assert not tasks.existing(path=path).ready() # type: ignore # pylint: disable=no-member + assert not ready(tasks.existing(path=path)) def test_tasks_existing_present_directory(tmp_path): path = tmp_path / "directory" path.mkdir() - assert tasks.existing(path=path).ready() # type: ignore # pylint: disable=no-member + assert ready(tasks.existing(path=path)) def test_tasks_existing_present_file(tmp_path): path = tmp_path / "file" path.touch() - assert tasks.existing(path=path).ready() # type: ignore # pylint: disable=no-member + assert ready(tasks.existing(path=path)) def test_tasks_existing_present_symlink(tmp_path): path = tmp_path / "symlink" path.symlink_to(os.devnull) - assert tasks.existing(path=path).ready() # type: ignore # pylint: disable=no-member + assert ready(tasks.existing(path=path)) def test_tasks_file_missing(tmp_path): path = tmp_path / "file" - assert not tasks.file(path=path).ready() # type: ignore # pylint: disable=no-member + assert not ready(tasks.file(path=path)) def test_tasks_file_present(tmp_path): path = tmp_path / "file" path.touch() - assert tasks.file(path=path).ready() # type: ignore # pylint: disable=no-member + assert ready(tasks.file(path=path)) def test_tasks_filecopy(tmp_path): diff --git a/src/uwtools/utils/tasks.py b/src/uwtools/utils/tasks.py index 9f716933d..07fbb586f 100644 --- a/src/uwtools/utils/tasks.py +++ b/src/uwtools/utils/tasks.py @@ -4,11 +4,23 @@ import os from pathlib import Path -from shutil import copy +from shutil import copy, which +from typing import Union from iotaa import asset, external, task +@external +def executable(program: Union[Path, str]): + """ + An executable program located on the current path. + + :param program: Name of or path to the program. + """ + yield "Executable program %s" % program + yield asset(program, lambda: bool(which(program))) + + @external def existing(path: Path): """