Skip to content

Commit d96c577

Browse files
Allow CLI use without ecFlow installed (#965)
* first pass * dev version bump and test * remove .dev from version * tidy * remove extra line * add installation note * Apply suggestions from code review Co-authored-by: Paul Madden <136389411+maddenp-cu@users.noreply.github.com> * WIP * remove lazy imports * typo * update import --------- Co-authored-by: Paul Madden <136389411+maddenp-cu@users.noreply.github.com>
1 parent 7ee50d9 commit d96c577

5 files changed

Lines changed: 71 additions & 18 deletions

File tree

docs/sections/user_guide/installation.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,7 @@ If you are using an app that has its own environment requirements that do not wo
9797
9898
cd src/
9999
pip install .
100+
101+
.. note::
102+
103+
Since ecFlow is not currently published to PyPI, it cannot be installed as a pip dependency. Thus, the ``ecflow`` mode is available only when ecFlow is separately available.

src/uwtools/cli.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import uwtools.api
2323
import uwtools.api.config
2424
import uwtools.api.driver
25-
import uwtools.api.ecflow
2625
import uwtools.api.execute
2726
import uwtools.api.fs
2827
import uwtools.api.rocoto
@@ -42,6 +41,13 @@
4241
if TYPE_CHECKING:
4342
from iotaa import Node
4443

44+
_ECFLOW_AVAILABLE = True
45+
try:
46+
import uwtools.api.ecflow
47+
except ImportError:
48+
_ECFLOW_AVAILABLE = False
49+
50+
4551
DRIVERS = [
4652
STR.cdeps,
4753
STR.chgres_cube,
@@ -111,8 +117,9 @@ def main() -> None:
111117
STR.fs: _dispatch_fs,
112118
STR.rocoto: _dispatch_rocoto,
113119
STR.template: _dispatch_template,
114-
STR.ecflow: _dispatch_ecflow,
115120
}
121+
if _ECFLOW_AVAILABLE:
122+
tools[STR.ecflow] = _dispatch_ecflow
116123
drivers: dict[str, Callable[..., bool]] = {
117124
x: partial(_dispatch_to_driver, x) for x in DRIVERS
118125
}
@@ -1565,12 +1572,13 @@ def _parse_args(raw_args: list[str]) -> tuple[Args, Checks]:
15651572
subparsers = _add_subparsers(parser, STR.mode, STR.mode.upper())
15661573
tools = {
15671574
STR.config: partial(_add_subparser_config, subparsers),
1568-
STR.ecflow: partial(_add_subparser_ecflow, subparsers),
15691575
STR.execute: partial(_add_subparser_execute, subparsers),
15701576
STR.fs: partial(_add_subparser_fs, subparsers),
15711577
STR.rocoto: partial(_add_subparser_rocoto, subparsers),
15721578
STR.template: partial(_add_subparser_template, subparsers),
15731579
}
1580+
if _ECFLOW_AVAILABLE:
1581+
tools[STR.ecflow] = partial(_add_subparser_ecflow, subparsers)
15741582
no_components: list[str] = []
15751583
assets = {
15761584
component: partial(_add_subparser_for_driver, component, subparsers)

src/uwtools/ecflow.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,29 @@
1616
from time import sleep
1717
from typing import TYPE_CHECKING, cast
1818

19-
from ecflow import ( # type: ignore[import-untyped]
20-
Client,
21-
Defs,
22-
DState,
23-
Family,
24-
Late,
25-
Node,
26-
RepeatDate,
27-
RepeatDateTime,
28-
RepeatDay,
29-
RepeatEnumerated,
30-
RepeatInteger,
31-
Suite,
32-
Task,
33-
)
19+
try:
20+
from ecflow import ( # type: ignore[import-untyped]
21+
Client,
22+
Defs,
23+
DState,
24+
Family,
25+
Late,
26+
Node,
27+
RepeatDate,
28+
RepeatDateTime,
29+
RepeatDay,
30+
RepeatEnumerated,
31+
RepeatInteger,
32+
Suite,
33+
Task,
34+
)
35+
except ImportError as e:
36+
msg = (
37+
"The ecFlow Python library could not be imported. To use ecFlow functionality, ensure "
38+
"that the ecFlow executables are on PATH and the ecFlow Python libraries are on "
39+
"PYTHONPATH. "
40+
)
41+
raise ImportError(msg) from e
3442

3543
from uwtools.config.formats.yaml import YAMLConfig
3644
from uwtools.config.validator import validate_internal

src/uwtools/tests/test_cli.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from argparse import ArgumentParser as Parser
44
from argparse import _SubParsersAction
55
from datetime import timedelta
6+
from importlib import reload
67
from pathlib import Path
78
from textwrap import dedent
89
from unittest.mock import Mock, patch
@@ -928,6 +929,29 @@ def test_cli__dispatch_to_driver_show_schema(capsys):
928929
assert capsys.readouterr().out == dedent(expected).lstrip()
929930

930931

932+
def test_cli__ecflow_importable():
933+
reload(cli)
934+
assert cli._ECFLOW_AVAILABLE is True
935+
936+
937+
def test_cli__ecflow_importable_fail():
938+
with patch.dict(sys.modules, {"uwtools.api.ecflow": None}):
939+
reload(cli)
940+
assert cli._ECFLOW_AVAILABLE is False
941+
reload(cli)
942+
943+
944+
def test_cli_main_ecflow_unavailable():
945+
with (
946+
patch.object(cli, "_ECFLOW_AVAILABLE", False),
947+
patch.object(cli, "_dispatch_template", return_value=True),
948+
patch.object(sys, "argv", ["uw", "template", "render"]),
949+
raises(SystemExit) as e,
950+
):
951+
cli.main()
952+
assert e.value.code == 0
953+
954+
931955
@mark.parametrize("quiet", [False, True])
932956
@mark.parametrize("verbose", [False, True])
933957
def test_cli_main_fail_checks(capsys, quiet, verbose):

src/uwtools/tests/test_ecflow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import socket
88
import sys
99
from copy import deepcopy
10+
from importlib import reload
1011
from io import StringIO
1112
from pathlib import Path
1213
from textwrap import dedent
@@ -1208,3 +1209,11 @@ def fake_run(cmd, **_kwargs):
12081209
assert (ssl_dir / "server.crt").is_file()
12091210
assert (ssl_dir / "dh2048.pem").is_file()
12101211
assert logged("SSL certificate files written to %s" % ssl_dir)
1212+
1213+
1214+
def test_ecflow_import_error():
1215+
with (
1216+
patch.dict(sys.modules, {"ecflow": None}),
1217+
raises(ImportError, match="ecFlow Python library could not be imported"),
1218+
):
1219+
reload(ecflow)

0 commit comments

Comments
 (0)