/
commands.py
370 lines (327 loc) · 12.4 KB
/
commands.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
"""The implementation of pipx commands"""
import logging
import sys
import textwrap
from pathlib import Path
from shutil import which
from typing import List
import userpath # type: ignore
from pipx import constants
from pipx.colors import bold
from pipx.commands.common import expose_apps_globally, get_package_summary
from pipx.emojies import hazard, stars
from pipx.util import WINDOWS, PipxError, rmdir
from pipx.venv import Venv, VenvContainer, PackageInstallFailureError
def install(
venv_dir: Path,
package: str,
package_or_url: str,
local_bin_dir: Path,
python: str,
pip_args: List[str],
venv_args: List[str],
verbose: bool,
*,
force: bool,
include_dependencies: bool,
):
try:
exists = venv_dir.exists() and next(venv_dir.iterdir())
except StopIteration:
exists = False
if exists:
if force:
print(f"Installing to existing directory {str(venv_dir)!r}")
else:
print(
f"{package!r} already seems to be installed. "
f"Not modifying existing installation in {str(venv_dir)!r}. "
"Pass '--force' to force installation."
)
return
venv = Venv(venv_dir, python=python, verbose=verbose)
try:
venv.create_venv(venv_args, pip_args)
try:
venv.install_package(
package=package,
package_or_url=package_or_url,
pip_args=pip_args,
include_dependencies=include_dependencies,
include_apps=True,
is_main_package=True,
)
except PackageInstallFailureError:
venv.remove_venv()
raise PipxError(
f"Could not install package {package}. Is the name or spec correct?"
)
_run_post_install_actions(
venv, package, local_bin_dir, venv_dir, include_dependencies, force=force
)
except (Exception, KeyboardInterrupt):
print("")
venv.remove_venv()
raise
def _run_post_install_actions(
venv: Venv,
package: str,
local_bin_dir: Path,
venv_dir: Path,
include_dependencies: bool,
*,
force: bool,
):
package_metadata = venv.package_metadata[package]
if not package_metadata.app_paths and not include_dependencies:
# No apps associated with this package and we aren't including dependencies.
# This package has nothing for pipx to use, so this is an error.
for dep, dependent_apps in package_metadata.app_paths_of_dependencies.items():
print(
f"Note: Dependent package '{dep}' contains {len(dependent_apps)} apps"
)
for app in dependent_apps:
print(f" - {app.name}")
if venv.safe_to_remove():
venv.remove_venv()
if len(package_metadata.app_paths_of_dependencies.keys()):
raise PipxError(
f"No apps associated with package {package}. "
"Try again with '--include-deps' to include apps of dependent packages, "
"which are listed above. "
"If you are attempting to install a library, pipx should not be used. "
"Consider using pip or a similar tool instead."
)
else:
raise PipxError(
f"No apps associated with package {package}. "
"If you are attempting to install a library, pipx should not be used. "
"Consider using pip or a similar tool instead."
)
if package_metadata.apps:
pass
elif package_metadata.apps_of_dependencies and include_dependencies:
pass
else:
# No apps associated with this package and we aren't including dependencies.
# This package has nothing for pipx to use, so this is an error.
if venv.safe_to_remove():
venv.remove_venv()
raise PipxError(
f"No apps associated with package {package} or its dependencies."
"If you are attempting to install a library, pipx should not be used. "
"Consider using pip or a similar tool instead."
)
expose_apps_globally(
local_bin_dir, package_metadata.app_paths, package, force=force
)
if include_dependencies:
for _, app_paths in package_metadata.app_paths_of_dependencies.items():
expose_apps_globally(local_bin_dir, app_paths, package, force=force)
print(get_package_summary(venv_dir, package=package, new_install=True))
_warn_if_not_on_path(local_bin_dir)
print(f"done! {stars}", file=sys.stderr)
def _warn_if_not_on_path(local_bin_dir: Path):
if not userpath.in_current_path(str(local_bin_dir)):
logging.warning(
f"{hazard} Note: {str(local_bin_dir)!r} is not on your PATH environment "
"variable. These apps will not be globally accessible until "
"your PATH is updated. Run `pipx ensurepath` to "
"automatically add it, or manually modify your PATH in your shell's "
"config file (i.e. ~/.bashrc)."
)
def inject(
venv_dir: Path,
package: str,
package_or_url: str,
pip_args: List[str],
*,
verbose: bool,
include_apps: bool,
include_dependencies: bool,
force: bool,
):
if not venv_dir.exists() or not next(venv_dir.iterdir()):
raise PipxError(
textwrap.dedent(
f"""\
Can't inject {package!r} into nonexistent Virtual Environment {str(venv_dir)!r}.
Be sure to install the package first with pipx install {venv_dir.name!r}
before injecting into it."""
)
)
venv = Venv(venv_dir, verbose=verbose)
try:
venv.install_package(
package=package,
package_or_url=package_or_url,
pip_args=pip_args,
include_dependencies=include_dependencies,
include_apps=include_apps,
is_main_package=False,
)
except PackageInstallFailureError:
raise PipxError(
f"Could not inject package {package}. Is the name or spec correct?"
)
if include_apps:
_run_post_install_actions(
venv,
package,
constants.LOCAL_BIN_DIR,
venv_dir,
include_dependencies,
force=force,
)
print(f" injected package {bold(package)} into venv {bold(venv_dir.name)}")
print(f"done! {stars}", file=sys.stderr)
def uninstall(venv_dir: Path, package: str, local_bin_dir: Path, verbose: bool):
"""Uninstall entire venv_dir, including main package and all injected
packages.
"""
if not venv_dir.exists():
print(f"Nothing to uninstall for {package} 😴")
app = which(package)
if app:
print(
f"{hazard} Note: '{app}' still exists on your system and is on your PATH"
)
return
venv = Venv(venv_dir, verbose=verbose)
if venv.pipx_metadata.main_package is not None:
app_paths: List[Path] = []
for viewed_package in venv.package_metadata.values():
app_paths += viewed_package.app_paths
for dep_paths in viewed_package.app_paths_of_dependencies.values():
app_paths += dep_paths
else:
# fallback if not metadata from pipx_metadata.json
if venv.python_path.is_file():
# has a valid python interpreter and can get metadata about the package
metadata = venv.get_venv_metadata_for_package(package)
app_paths = metadata.app_paths
for dep_paths in metadata.app_paths_of_dependencies.values():
app_paths += dep_paths
else:
# Doesn't have a valid python interpreter. We'll take our best guess on what to uninstall
# here based on symlink location. pipx doesn't use symlinks on windows, so this is for
# non-windows only.
# The heuristic here is any symlink in ~/.local/bin pointing to .local/pipx/venvs/PACKAGE/bin
# should be uninstalled.
if WINDOWS:
app_paths = []
else:
apps_linking_to_venv_bin_dir = [
f
for f in constants.LOCAL_BIN_DIR.iterdir()
if str(f.resolve()).startswith(str(venv.bin_path))
]
app_paths = apps_linking_to_venv_bin_dir
for file in local_bin_dir.iterdir():
if WINDOWS:
for b in app_paths:
if file.name == b.name:
file.unlink()
else:
symlink = file
for b in app_paths:
if symlink.exists() and b.exists() and symlink.samefile(b):
logging.info(f"removing symlink {str(symlink)}")
symlink.unlink()
rmdir(venv_dir)
print(f"uninstalled {package}! {stars}")
def uninstall_all(venv_container: VenvContainer, local_bin_dir: Path, verbose: bool):
for venv_dir in venv_container.iter_venv_dirs():
package = venv_dir.name
uninstall(venv_dir, package, local_bin_dir, verbose)
def reinstall_all(
venv_container: VenvContainer,
local_bin_dir: Path,
python: str,
verbose: bool,
*,
skip: List[str],
):
for venv_dir in venv_container.iter_venv_dirs():
package = venv_dir.name
if package in skip:
continue
venv = Venv(venv_dir, verbose=verbose)
if venv.pipx_metadata.main_package.package_or_url is not None:
package_or_url = venv.pipx_metadata.main_package.package_or_url
else:
package_or_url = package
uninstall(venv_dir, package, local_bin_dir, verbose)
# install main package first
install(
venv_dir,
package,
package_or_url,
local_bin_dir,
python,
venv.pipx_metadata.main_package.pip_args,
venv.pipx_metadata.venv_args,
verbose,
force=True,
include_dependencies=venv.pipx_metadata.main_package.include_dependencies,
)
# now install injected packages
for (
injected_name,
injected_package,
) in venv.pipx_metadata.injected_packages.items():
if injected_package.package_or_url is None:
# This should never happen, but package_or_url is type
# Optional[str] so mypy thinks it could be None
raise PipxError("Internal Error injecting package")
inject(
venv_dir,
injected_name,
injected_package.package_or_url,
injected_package.pip_args,
verbose=verbose,
include_apps=injected_package.include_apps,
include_dependencies=injected_package.include_dependencies,
force=True,
)
def run_pip(package: str, venv_dir: Path, pip_args: List[str], verbose: bool):
venv = Venv(venv_dir, verbose=verbose)
if not venv.python_path.exists():
raise PipxError(
f"venv for {package!r} was not found. Was {package!r} installed with pipx?"
)
venv.verbose = True
venv._run_pip(pip_args)
def ensurepath(location: Path, *, force: bool):
location_str = str(location)
post_install_message = (
"You likely need to open a new terminal or re-login for "
"the changes to take effect."
)
if userpath.in_current_path(location_str) or userpath.need_shell_restart(
location_str
):
if not force:
if userpath.need_shell_restart(location_str):
print(
f"{location_str} has been already been added to PATH. "
f"{post_install_message}"
)
else:
logging.warning(
(
f"The directory `{location_str}` is already in PATH. If you "
"are sure you want to proceed, try again with "
"the '--force' flag.\n\n"
f"Otherwise pipx is ready to go! {stars}"
)
)
return
userpath.append(location_str)
print(f"Success! Added {location_str} to the PATH environment variable.")
print(
"Consider adding shell completions for pipx. "
"Run 'pipx completions' for instructions."
)
print()
print(f"{post_install_message} {stars}")