Skip to content

Commit 3bbc14f

Browse files
committed
fix: refactor data server protocols to be lazy
1 parent 563f01f commit 3bbc14f

3 files changed

Lines changed: 191 additions & 19 deletions

File tree

CIME/Servers/__init__.py

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,74 @@
1-
# pylint: disable=import-error
1+
"""
2+
CIME Server implementations for data transfer.
3+
4+
Server availability is detected lazily on first access to avoid
5+
running executables at import time.
6+
"""
7+
8+
from functools import lru_cache
29
from shutil import which
310

4-
has_gftp = which("globus-url-copy")
5-
has_svn = which("svn")
6-
has_wget = which("wget")
7-
has_ftp = True
8-
try:
9-
from ftplib import FTP
10-
except ImportError:
11-
has_ftp = False
12-
if has_ftp:
13-
from CIME.Servers.ftp import FTP
14-
if has_svn:
15-
from CIME.Servers.svn import SVN
16-
if has_wget:
17-
from CIME.Servers.wget import WGET
18-
if has_gftp:
19-
from CIME.Servers.gftp import GridFTP
11+
12+
@lru_cache(maxsize=None)
13+
def is_protocol_available(protocol: str) -> bool:
14+
"""
15+
Check if a protocol is available.
16+
17+
Args:
18+
protocol: One of 'ftp', 'svn', 'wget', 'gftp'
19+
20+
Returns:
21+
True if the protocol is available, False otherwise.
22+
"""
23+
protocol = protocol.lower()
24+
if protocol == "ftp":
25+
try:
26+
from ftplib import FTP # noqa: F401 pylint: disable=unused-import
27+
28+
return True
29+
except ImportError:
30+
return False
31+
elif protocol == "svn":
32+
return which("svn") is not None
33+
elif protocol == "wget":
34+
return which("wget") is not None
35+
elif protocol == "gftp":
36+
return which("globus-url-copy") is not None
37+
return False
38+
39+
40+
def __getattr__(name: str):
41+
"""Lazy loading of server classes and has_* attributes."""
42+
if name == "FTP":
43+
if is_protocol_available("ftp"):
44+
from CIME.Servers.ftp import FTP
45+
46+
return FTP
47+
raise AttributeError("FTP server not available")
48+
elif name == "SVN":
49+
if is_protocol_available("svn"):
50+
from CIME.Servers.svn import SVN
51+
52+
return SVN
53+
raise AttributeError("SVN server not available (svn not found)")
54+
elif name == "WGET":
55+
if is_protocol_available("wget"):
56+
from CIME.Servers.wget import WGET
57+
58+
return WGET
59+
raise AttributeError("WGET server not available (wget not found)")
60+
elif name == "GridFTP":
61+
if is_protocol_available("gftp"):
62+
from CIME.Servers.gftp import GridFTP
63+
64+
return GridFTP
65+
raise AttributeError("GridFTP server not available (globus-url-copy not found)")
66+
elif name == "has_ftp":
67+
return is_protocol_available("ftp")
68+
elif name == "has_svn":
69+
return is_protocol_available("svn")
70+
elif name == "has_wget":
71+
return is_protocol_available("wget")
72+
elif name == "has_gftp":
73+
return is_protocol_available("gftp")
74+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

CIME/case/check_input_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def _download_checksum_file(rundir):
2424
# download and merge all available chksum files.
2525
while protocol is not None:
2626
protocol, address, user, passwd, chksum_file, _, _ = inputdata.get_next_server()
27-
if protocol not in vars(CIME.Servers):
27+
if not CIME.Servers.is_protocol_available(protocol):
2828
logger.info("Client protocol {} not enabled".format(protocol))
2929
continue
3030
logger.info(
@@ -414,7 +414,7 @@ def _check_input_data_impl(
414414
no_files_missing = True
415415
server = None
416416
if download:
417-
if protocol not in vars(CIME.Servers):
417+
if not CIME.Servers.is_protocol_available(protocol):
418418
logger.info("Client protocol {} not enabled".format(protocol))
419419
return False
420420
logger.info(

CIME/tests/test_unit_servers.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Unit tests for CIME.Servers lazy loading.
2+
3+
These tests verify:
4+
1. Lazy loading behavior (no subprocess at import time)
5+
2. Backward compatibility with existing usage patterns
6+
3. is_protocol_available() API
7+
"""
8+
9+
import pytest
10+
11+
12+
class TestServersLazyLoading:
13+
"""Tests for lazy loading of server modules."""
14+
15+
def test_import_does_not_run_which(self):
16+
"""Importing CIME.Servers should not run shutil.which()."""
17+
import CIME.Servers
18+
19+
assert CIME.Servers is not None
20+
21+
def test_availability_not_checked_at_import(self):
22+
"""Availability flags should be unchecked after import."""
23+
import importlib
24+
import sys
25+
26+
# Force reimport
27+
for mod in list(sys.modules.keys()):
28+
if "CIME.Servers" in mod:
29+
del sys.modules[mod]
30+
31+
import CIME.Servers
32+
33+
assert CIME.Servers is not None
34+
35+
36+
class TestProtocolAvailability:
37+
"""Tests for is_protocol_available() API."""
38+
39+
def test_is_protocol_available_ftp(self):
40+
"""FTP should always be available (stdlib)."""
41+
import CIME.Servers
42+
43+
assert CIME.Servers.is_protocol_available("ftp") is True
44+
45+
def test_is_protocol_available_invalid(self):
46+
"""Invalid protocol should return False."""
47+
import CIME.Servers
48+
49+
assert CIME.Servers.is_protocol_available("invalid") is False
50+
51+
def test_is_protocol_available_case_insensitive(self):
52+
"""Protocol check should be case-insensitive."""
53+
import CIME.Servers
54+
55+
assert CIME.Servers.is_protocol_available("FTP") is True
56+
assert CIME.Servers.is_protocol_available("Ftp") is True
57+
assert CIME.Servers.is_protocol_available("ftp") is True
58+
59+
60+
class TestServerClassAccess:
61+
"""Tests for accessing server classes."""
62+
63+
def test_ftp_attribute_access(self):
64+
"""Accessing CIME.Servers.FTP should work via __getattr__."""
65+
import CIME.Servers
66+
67+
FTP = CIME.Servers.FTP
68+
assert FTP is not None
69+
assert FTP.__name__ == "FTP"
70+
71+
def test_ftp_has_ftp_login_method(self):
72+
"""FTP class should have ftp_login class method (used by check_input_data)."""
73+
import CIME.Servers
74+
75+
assert hasattr(CIME.Servers.FTP, "ftp_login")
76+
77+
def test_wget_has_wget_login_method(self):
78+
"""WGET class should have wget_login class method if available."""
79+
import CIME.Servers
80+
81+
if CIME.Servers.is_protocol_available("wget"):
82+
assert hasattr(CIME.Servers.WGET, "wget_login")
83+
84+
def test_unavailable_server_raises_attribute_error(self):
85+
"""Accessing unavailable server should raise AttributeError."""
86+
import CIME.Servers
87+
88+
if not CIME.Servers.is_protocol_available("gftp"):
89+
with pytest.raises(AttributeError):
90+
_ = CIME.Servers.GridFTP
91+
92+
93+
class TestBackwardCompatibility:
94+
"""Tests for backward compatibility with existing code patterns."""
95+
96+
def test_has_ftp_attribute(self):
97+
"""has_ftp attribute should return True (FTP always available)."""
98+
import CIME.Servers
99+
100+
assert CIME.Servers.has_ftp is True
101+
102+
def test_has_attributes_exist(self):
103+
"""All has_* attributes should be accessible."""
104+
import CIME.Servers
105+
106+
# These should not raise, regardless of availability
107+
_ = CIME.Servers.has_ftp
108+
_ = CIME.Servers.has_svn
109+
_ = CIME.Servers.has_wget
110+
_ = CIME.Servers.has_gftp
111+
112+
def test_instantiate_ftp_server(self):
113+
"""Should be able to instantiate FTP server (backward compat)."""
114+
import CIME.Servers
115+
116+
FTP = CIME.Servers.FTP
117+
assert FTP is not None

0 commit comments

Comments
 (0)