1
1
import json
2
- import os
3
2
import shlex
4
3
import sys
5
4
import textwrap
5
+ from abc import ABC , abstractmethod
6
6
from enum import Enum
7
7
from pathlib import Path
8
8
from typing import Any , Dict , Iterable , List , Optional , Sequence , Tuple , TypedDict , cast
@@ -52,38 +52,71 @@ class PipInspectReport(TypedDict, total=False):
52
52
environment : Dict [str , str ]
53
53
54
54
55
- class Installer (str , Enum ):
55
+ class InstallerFlavor (str , Enum ):
56
56
pip = "pip"
57
57
uvpip = "uvpip"
58
58
59
59
60
- def _pip_install_cmd_and_env (python : str ) -> Tuple [List [str ], Dict [str , str ]]:
61
- return [* get_pip_command (python ), "install" ], {}
60
+ class Installer (ABC ):
61
+ @abstractmethod
62
+ def install_cmd (self , python : str ) -> List [str ]: ...
62
63
64
+ @abstractmethod
65
+ def uninstall_cmd (self , python : str ) -> List [str ]: ...
63
66
64
- def _uv_pip_install_cmd_and_env ( python : str ) -> Tuple [ List [ str ], Dict [ str , str ]]:
65
- return [ sys . executable , "-m" , "uv" , "pip" , "install" , "--python" , python ], {}
67
+ @ abstractmethod
68
+ def freeze_cmd ( self , python : str ) -> List [ str ]: ...
66
69
70
+ @abstractmethod
71
+ def has_metadata_cache (self ) -> bool :
72
+ """Whether the installer caches metadata preparation results."""
73
+ ...
67
74
68
- def _install_cmd_and_env (
69
- installer : Installer , python : str
70
- ) -> Tuple [List [str ], Dict [str , str ]]:
71
- if installer == Installer .pip :
72
- return _pip_install_cmd_and_env (python )
73
- elif installer == Installer .uvpip :
74
- if get_python_version_info (python ) < (3 , 7 ):
75
- log_error ("The 'uv' installer requires Python 3.7 or later." )
76
- raise typer .Exit (1 )
77
- return _uv_pip_install_cmd_and_env (python )
78
- raise NotImplementedError (f"Installer { installer } is not implemented." )
75
+ @classmethod
76
+ def create (cls , flavor : InstallerFlavor , python : str ) -> "Installer" :
77
+ if flavor == InstallerFlavor .pip :
78
+ return PipInstaller ()
79
+ elif flavor == InstallerFlavor .uvpip :
80
+ if get_python_version_info (python ) < (3 , 7 ):
81
+ log_error ("The 'uv' installer requires Python 3.7 or later." )
82
+ raise typer .Exit (1 )
83
+ return UvpipInstaller ()
84
+
85
+
86
+ class PipInstaller (Installer ):
87
+ def install_cmd (self , python : str ) -> List [str ]:
88
+ return [* get_pip_command (python ), "install" ]
89
+
90
+ def uninstall_cmd (self , python : str ) -> List [str ]:
91
+ return [* get_pip_command (python ), "uninstall" , "--yes" ]
92
+
93
+ def freeze_cmd (self , python : str ) -> List [str ]:
94
+ return [* get_pip_command (python ), "freeze" , "--all" ]
95
+
96
+ def has_metadata_cache (self ) -> bool :
97
+ return False
98
+
99
+
100
+ class UvpipInstaller (Installer ):
101
+ def install_cmd (self , python : str ) -> List [str ]:
102
+ return [sys .executable , "-m" , "uv" , "pip" , "install" , "--python" , python ]
103
+
104
+ def uninstall_cmd (self , python : str ) -> List [str ]:
105
+ return [sys .executable , "-m" , "uv" , "pip" , "uninstall" , "--python" , python ]
106
+
107
+ def freeze_cmd (self , python : str ) -> List [str ]:
108
+ return [sys .executable , "-m" , "uv" , "pip" , "freeze" , "--python" , python ]
109
+
110
+ def has_metadata_cache (self ) -> bool :
111
+ return True
79
112
80
113
81
114
def pip_upgrade_project (
115
+ installer : Installer ,
82
116
python : str ,
83
117
constraints_filename : Path ,
84
118
project_root : Path ,
85
119
extras : Optional [Sequence [NormalizedName ]] = None ,
86
- installer : Installer = Installer .pip ,
87
120
installer_options : Optional [List [str ]] = None ,
88
121
) -> None :
89
122
"""Upgrade a project.
@@ -138,7 +171,9 @@ def pip_upgrade_project(
138
171
# 2. get installed frozen dependencies of project
139
172
installed_reqs = {
140
173
get_req_name (req_line ): normalize_req_line (req_line )
141
- for req_line in pip_freeze_dependencies (python , project_root , extras )[0 ]
174
+ for req_line in pip_freeze_dependencies (
175
+ installer , python , project_root , extras
176
+ )[0 ]
142
177
}
143
178
assert all (installed_reqs .keys ()) # XXX user error instead?
144
179
# 3. uninstall dependencies that do not match constraints
@@ -152,11 +187,11 @@ def pip_upgrade_project(
152
187
if to_uninstall :
153
188
to_uninstall_str = "," .join (to_uninstall )
154
189
log_info (f"Uninstalling dependencies to update: { to_uninstall_str } " )
155
- pip_uninstall (python , to_uninstall )
190
+ pip_uninstall (installer , python , to_uninstall )
156
191
# 4. install project with constraints
157
192
project_name = get_project_name (python , project_root )
158
193
log_info (f"Installing/updating { project_name } " )
159
- cmd , env = _install_cmd_and_env ( installer , python )
194
+ cmd = installer . install_cmd ( python )
160
195
if installer_options :
161
196
cmd .extend (installer_options )
162
197
cmd .extend (
@@ -181,7 +216,7 @@ def pip_upgrade_project(
181
216
log_debug (textwrap .indent (constraints , prefix = " " ))
182
217
else :
183
218
log_debug (f"with empty { constraints_without_editables_filename } ." )
184
- check_call (cmd , env = dict ( os . environ , ** env ) )
219
+ check_call (cmd )
185
220
186
221
187
222
def _pip_list__env_info_json (python : str ) -> InstalledDistributions :
@@ -222,14 +257,18 @@ def pip_list(python: str) -> InstalledDistributions:
222
257
return _pip_list__env_info_json (python )
223
258
224
259
225
- def pip_freeze (python : str ) -> Iterable [str ]:
260
+ def pip_freeze (installer : Installer , python : str ) -> Iterable [str ]:
226
261
"""Run pip freeze."""
227
- cmd = [* get_pip_command (python ), "freeze" , "--all" ]
262
+ cmd = installer .freeze_cmd (python )
263
+ log_debug (f"Running { shlex .join (cmd )} " )
228
264
return check_output (cmd ).splitlines ()
229
265
230
266
231
267
def pip_freeze_dependencies (
232
- python : str , project_root : Path , extras : Optional [Sequence [NormalizedName ]] = None
268
+ installer : Installer ,
269
+ python : str ,
270
+ project_root : Path ,
271
+ extras : Optional [Sequence [NormalizedName ]] = None ,
233
272
) -> Tuple [List [str ], List [str ]]:
234
273
"""Run pip freeze, returning only dependencies of the project.
235
274
@@ -241,7 +280,7 @@ def pip_freeze_dependencies(
241
280
"""
242
281
project_name = get_project_name (python , project_root )
243
282
dependencies_names = list_installed_depends (pip_list (python ), project_name , extras )
244
- frozen_reqs = pip_freeze (python )
283
+ frozen_reqs = pip_freeze (installer , python )
245
284
dependencies_reqs = []
246
285
unneeded_reqs = []
247
286
for frozen_req in frozen_reqs :
@@ -258,7 +297,10 @@ def pip_freeze_dependencies(
258
297
259
298
260
299
def pip_freeze_dependencies_by_extra (
261
- python : str , project_root : Path , extras : Sequence [NormalizedName ]
300
+ installer : Installer ,
301
+ python : str ,
302
+ project_root : Path ,
303
+ extras : Sequence [NormalizedName ],
262
304
) -> Tuple [Dict [Optional [NormalizedName ], List [str ]], List [str ]]:
263
305
"""Run pip freeze, returning only dependencies of the project.
264
306
@@ -272,7 +314,7 @@ def pip_freeze_dependencies_by_extra(
272
314
dependencies_by_extras = list_installed_depends_by_extra (
273
315
pip_list (python ), project_name
274
316
)
275
- frozen_reqs = pip_freeze (python )
317
+ frozen_reqs = pip_freeze (installer , python )
276
318
dependencies_reqs = {} # type: Dict[Optional[NormalizedName], List[str]]
277
319
for extra in extras :
278
320
if extra not in dependencies_by_extras :
@@ -301,12 +343,15 @@ def pip_freeze_dependencies_by_extra(
301
343
return dependencies_reqs , unneeded_reqs
302
344
303
345
304
- def pip_uninstall (python : str , requirements : Iterable [str ]) -> None :
346
+ def pip_uninstall (
347
+ installer : Installer , python : str , requirements : Iterable [str ]
348
+ ) -> None :
305
349
"""Uninstall packages."""
306
350
reqs = list (requirements )
307
351
if not reqs :
308
352
return
309
- cmd = [* get_pip_command (python ), "uninstall" , "--yes" , * reqs ]
353
+ cmd = [* installer .uninstall_cmd (python ), * reqs ]
354
+ log_debug (f"Running { shlex .join (cmd )} " )
310
355
check_call (cmd )
311
356
312
357
0 commit comments