Skip to content

Commit 6fd40c1

Browse files
fix(cli): lightning-sdk installation process with uv (#640)
* fix(cli): Improve lightning-sdk installation process with uv support * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add test for failure case in _ensure_lightning_installed function * fix(cli): Enhance lightning-sdk installation process to support multiple installers * updated tests * re run --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 274018e commit 6fd40c1

File tree

2 files changed

+111
-9
lines changed

2 files changed

+111
-9
lines changed

src/litserve/cli.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
1+
import importlib.util
2+
import shutil
13
import subprocess
24
import sys
35

46
from litserve.utils import is_package_installed
57

68

79
def _ensure_lightning_installed():
8-
if not is_package_installed("lightning_sdk"):
9-
print("Lightning CLI not found. Installing...")
10-
subprocess.check_call([sys.executable, "-m", "pip", "install", "-U", "lightning-sdk"])
10+
"""Ensure lightning-sdk is installed, attempting auto-installation if needed."""
11+
if is_package_installed("lightning_sdk"):
12+
return
13+
14+
print("Lightning CLI not found. Installing lightning-sdk...")
15+
16+
# Build list of available installers (pip first as it respects the active environment)
17+
installers = []
18+
if importlib.util.find_spec("pip"):
19+
installers.append([sys.executable, "-m", "pip"])
20+
if shutil.which("uv"):
21+
installers.append(["uv", "pip"])
22+
23+
for installer in installers:
24+
try:
25+
subprocess.run([*installer, "install", "-U", "lightning-sdk"], check=True)
26+
return
27+
except (subprocess.CalledProcessError, FileNotFoundError):
28+
continue
29+
30+
sys.exit("Failed to install lightning-sdk. Run: pip install lightning-sdk")
1131

1232

1333
def main():

tests/unit/test_cli.py

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import subprocess
23
import sys
34
from unittest.mock import MagicMock, patch
45

@@ -34,19 +35,98 @@ def test_dockerize_command(monkeypatch, capsys):
3435

3536

3637
@patch("litserve.cli.is_package_installed")
37-
@patch("subprocess.check_call")
38-
def test_ensure_lightning_installed(mock_check_call, mock_is_package_installed):
38+
@patch("litserve.cli.importlib.util.find_spec")
39+
@patch("litserve.cli.shutil.which")
40+
@patch("subprocess.run")
41+
def test_ensure_lightning_installed_with_pip(mock_run, mock_which, mock_find_spec, mock_is_package_installed):
3942
mock_is_package_installed.return_value = False
43+
mock_find_spec.return_value = True # pip available
44+
mock_which.return_value = None # uv not available
4045
_ensure_lightning_installed()
41-
mock_check_call.assert_called_once_with([sys.executable, "-m", "pip", "install", "-U", "lightning-sdk"])
46+
mock_run.assert_called_once_with([sys.executable, "-m", "pip", "install", "-U", "lightning-sdk"], check=True)
47+
48+
49+
@patch("litserve.cli.is_package_installed")
50+
@patch("litserve.cli.importlib.util.find_spec")
51+
@patch("litserve.cli.shutil.which")
52+
@patch("subprocess.run")
53+
def test_ensure_lightning_installed_pip_preferred(mock_run, mock_which, mock_find_spec, mock_is_package_installed):
54+
"""When both pip and uv are available, pip should be used first."""
55+
mock_is_package_installed.return_value = False
56+
mock_find_spec.return_value = True # pip available
57+
mock_which.return_value = "/usr/bin/uv" # uv also available
58+
_ensure_lightning_installed()
59+
mock_run.assert_called_once_with([sys.executable, "-m", "pip", "install", "-U", "lightning-sdk"], check=True)
60+
61+
62+
@patch("litserve.cli.is_package_installed")
63+
@patch("litserve.cli.importlib.util.find_spec")
64+
@patch("litserve.cli.shutil.which")
65+
@patch("subprocess.run")
66+
def test_ensure_lightning_installed_with_uv(mock_run, mock_which, mock_find_spec, mock_is_package_installed):
67+
mock_is_package_installed.return_value = False
68+
mock_find_spec.return_value = None # pip not available
69+
mock_which.return_value = "/usr/bin/uv" # uv available
70+
_ensure_lightning_installed()
71+
mock_run.assert_called_once_with(["uv", "pip", "install", "-U", "lightning-sdk"], check=True)
72+
73+
74+
@patch("litserve.cli.is_package_installed")
75+
@patch("litserve.cli.importlib.util.find_spec")
76+
@patch("litserve.cli.shutil.which")
77+
@patch("subprocess.run")
78+
def test_ensure_lightning_installed_fallback_to_uv(mock_run, mock_which, mock_find_spec, mock_is_package_installed):
79+
"""When pip fails, should fall back to uv."""
80+
mock_is_package_installed.return_value = False
81+
mock_find_spec.return_value = True # pip available
82+
mock_which.return_value = "/usr/bin/uv" # uv also available
83+
mock_run.side_effect = [subprocess.CalledProcessError(1, "pip"), None] # pip fails, uv succeeds
84+
_ensure_lightning_installed()
85+
assert mock_run.call_count == 2
86+
mock_run.assert_called_with(["uv", "pip", "install", "-U", "lightning-sdk"], check=True)
87+
88+
89+
@patch("litserve.cli.is_package_installed")
90+
@patch("litserve.cli.importlib.util.find_spec")
91+
@patch("litserve.cli.shutil.which")
92+
@patch("subprocess.run")
93+
def test_ensure_lightning_installed_failure(mock_run, mock_which, mock_find_spec, mock_is_package_installed):
94+
"""When all available installers fail, should exit with error."""
95+
mock_is_package_installed.return_value = False
96+
mock_find_spec.return_value = True # pip available
97+
mock_which.return_value = "/usr/bin/uv" # uv also available
98+
mock_run.side_effect = subprocess.CalledProcessError(1, "install") # both fail
99+
100+
with pytest.raises(SystemExit, match="Failed to install lightning-sdk"):
101+
_ensure_lightning_installed()
102+
assert mock_run.call_count == 2 # tried both pip and uv
103+
104+
105+
@patch("litserve.cli.is_package_installed")
106+
@patch("litserve.cli.importlib.util.find_spec")
107+
@patch("litserve.cli.shutil.which")
108+
@patch("subprocess.run")
109+
def test_ensure_lightning_installed_no_installer_available(
110+
mock_run, mock_which, mock_find_spec, mock_is_package_installed
111+
):
112+
"""When neither pip nor uv is available, should exit with error."""
113+
mock_is_package_installed.return_value = False
114+
mock_find_spec.return_value = None # pip not available
115+
mock_which.return_value = None # uv not available
116+
117+
with pytest.raises(SystemExit, match="Failed to install lightning-sdk"):
118+
_ensure_lightning_installed()
119+
mock_run.assert_not_called() # no installer was tried
42120

43121

44122
# TODO: Remove this once we have a fix for Python 3.10
45123
@pytest.mark.skipif(sys.version_info[:2] in [(3, 10)], reason="Test fails on Python 3.10")
46124
@patch("litserve.cli.is_package_installed")
47-
@patch("subprocess.check_call")
125+
@patch("litserve.cli.importlib.util.find_spec")
126+
@patch("litserve.cli.shutil.which")
127+
@patch("subprocess.run")
48128
@patch("builtins.__import__")
49-
def test_cli_main_lightning_not_installed(mock_import, mock_check_call, mock_is_package_installed):
129+
def test_cli_main_lightning_not_installed(mock_import, mock_run, mock_which, mock_find_spec, mock_is_package_installed):
50130
# Create a mock for the lightning_sdk module and its components
51131
mock_lightning_sdk = MagicMock()
52132
mock_lightning_sdk.cli.entrypoint.main_cli = MagicMock()
@@ -58,6 +138,8 @@ def side_effect(name, *args, **kwargs):
58138
return __import__(name, *args, **kwargs)
59139

60140
mock_import.side_effect = side_effect
141+
mock_find_spec.return_value = True # pip available
142+
mock_which.return_value = None # uv not available
61143

62144
# Test when lightning_sdk is not installed but gets installed dynamically
63145
mock_is_package_installed.side_effect = [False, True] # First call returns False, second call returns True
@@ -66,7 +148,7 @@ def side_effect(name, *args, **kwargs):
66148
with patch.object(sys, "argv", test_args):
67149
cli_main()
68150

69-
mock_check_call.assert_called_once_with([sys.executable, "-m", "pip", "install", "-U", "lightning-sdk"])
151+
mock_run.assert_called_once_with([sys.executable, "-m", "pip", "install", "-U", "lightning-sdk"], check=True)
70152

71153

72154
@pytest.mark.skipif(sys.version_info[:2] in [(3, 10)], reason="Test fails on Python 3.10")

0 commit comments

Comments
 (0)