pystand 2.11__py3-none-any.whl → 2.13.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pystand-2.11.dist-info → pystand-2.13.1.dist-info}/METADATA +22 -21
- pystand-2.13.1.dist-info/RECORD +6 -0
- {pystand-2.11.dist-info → pystand-2.13.1.dist-info}/WHEEL +1 -1
- pystand.py +384 -225
- pystand-2.11.dist-info/RECORD +0 -6
- {pystand-2.11.dist-info → pystand-2.13.1.dist-info}/entry_points.txt +0 -0
- {pystand-2.11.dist-info → pystand-2.13.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: pystand
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.13.1
|
4
4
|
Summary: Install Python versions from python-build-standalone project
|
5
5
|
Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
|
6
6
|
License: GPLv3
|
@@ -27,8 +27,8 @@ provided:
|
|
27
27
|
|Command |Description |
|
28
28
|
|---------|----------------------------------------------------------------------|
|
29
29
|
|`install`|Install one or more versions from a python-build-standalone release |
|
30
|
-
|`update` |Update one, more, or all versions to another release
|
31
|
-
|`remove` |Remove/uninstall one, more, or all versions
|
30
|
+
|`update` (or `upgrade`) |Update one, more, or all versions to another release |
|
31
|
+
|`remove` (or `uninstall`) |Remove/uninstall one, more, or all versions |
|
32
32
|
|`list` |List installed versions and show which have an update available |
|
33
33
|
|`show` |Show versions available from a release |
|
34
34
|
|`path` |Show path prefix to installed version base directory |
|
@@ -99,9 +99,9 @@ $ uv venv -p $(pystand path 3.12) myenv
|
|
99
99
|
# installed python 3.12:
|
100
100
|
$ $(pystand path -p 3.12) -m venv myenv
|
101
101
|
|
102
|
-
# Use
|
102
|
+
# Use uv tool to install a package to be run with pystand installed python
|
103
103
|
# specific version 3.11.1:
|
104
|
-
$
|
104
|
+
$ uv tool install -p $(pystand path 3.11.1) cowsay
|
105
105
|
```
|
106
106
|
|
107
107
|
See detailed usage information in the [Usage](#usage) section that
|
@@ -139,17 +139,17 @@ sh/python-build-standalone.
|
|
139
139
|
|
140
140
|
options:
|
141
141
|
-h, --help show this help message and exit
|
142
|
-
-D
|
142
|
+
-D, --distribution DISTRIBUTION
|
143
143
|
python-build-standalone distribution. Default is
|
144
144
|
"x86_64_v3-unknown-linux-gnu-install_only_stripped for
|
145
145
|
this host
|
146
|
-
-P
|
146
|
+
-P, --prefix-dir PREFIX_DIR
|
147
147
|
specify prefix dir for storing versions. Default is
|
148
148
|
"$HOME/.local/share/pystand"
|
149
|
-
-C
|
149
|
+
-C, --cache-dir CACHE_DIR
|
150
150
|
specify cache dir for downloads. Default is
|
151
151
|
"$HOME/.cache/pystand"
|
152
|
-
-M
|
152
|
+
-M, --cache-minutes CACHE_MINUTES
|
153
153
|
cache latest YYYYMMDD release tag fetch for this many
|
154
154
|
minutes, before rechecking for latest. Default is 60
|
155
155
|
minutes
|
@@ -174,7 +174,7 @@ Commands:
|
|
174
174
|
show Show versions available from a release.
|
175
175
|
path Show path prefix to installed version base directory.
|
176
176
|
|
177
|
-
Some commands offer aliases as shown in
|
177
|
+
Some commands offer aliases as shown in parentheses above. Note you can set
|
178
178
|
default starting global options in $HOME/.config/pystand-flags.conf.
|
179
179
|
```
|
180
180
|
|
@@ -193,7 +193,7 @@ positional arguments:
|
|
193
193
|
|
194
194
|
options:
|
195
195
|
-h, --help show this help message and exit
|
196
|
-
-r
|
196
|
+
-r, --release RELEASE
|
197
197
|
install from specified python-build-standalone
|
198
198
|
YYYYMMDD release (e.g. 20240415), default is latest
|
199
199
|
release
|
@@ -214,7 +214,7 @@ positional arguments:
|
|
214
214
|
|
215
215
|
options:
|
216
216
|
-h, --help show this help message and exit
|
217
|
-
-r
|
217
|
+
-r, --release RELEASE
|
218
218
|
update to specified YYYMMDD release (e.g. 20240415),
|
219
219
|
default is latest release
|
220
220
|
-a, --all update ALL versions
|
@@ -241,7 +241,7 @@ options:
|
|
241
241
|
-a, --all remove ALL versions
|
242
242
|
--skip skip the specified versions when removing all (only
|
243
243
|
can be specified with --all)
|
244
|
-
-r
|
244
|
+
-r, --release RELEASE
|
245
245
|
only remove versions if from specified YYYMMDD release
|
246
246
|
(e.g. 20240415)
|
247
247
|
|
@@ -262,7 +262,7 @@ options:
|
|
262
262
|
-h, --help show this help message and exit
|
263
263
|
-v, --verbose explicitly report why a version is not eligible for
|
264
264
|
update
|
265
|
-
-r
|
265
|
+
-r, --release RELEASE
|
266
266
|
use specified YYYYMMDD release (e.g. 20240415) for
|
267
267
|
verbose compare, default is latest release
|
268
268
|
```
|
@@ -283,7 +283,7 @@ positional arguments:
|
|
283
283
|
options:
|
284
284
|
-h, --help show this help message and exit
|
285
285
|
-l, --list just list recent releases
|
286
|
-
-r
|
286
|
+
-r, --release RELEASE
|
287
287
|
python-build-standalone YYYYMMDD release to show (e.g.
|
288
288
|
20240415), default is latest release
|
289
289
|
-a, --all show all available distributions for each version from
|
@@ -313,23 +313,24 @@ Python 3.8 or later is required. Arch Linux users can install [`pystand`
|
|
313
313
|
from the AUR](https://aur.archlinux.org/packages/pystand) and skip this
|
314
314
|
section.
|
315
315
|
|
316
|
-
|
317
|
-
|
316
|
+
Note [pystand is on PyPI](https://pypi.org/project/pystand/) so the
|
317
|
+
easiest way to install it is to use [`uv tool`][uvtool] (or
|
318
|
+
[`pipx`][pipx] or [`pipxu`][pipxu]).
|
318
319
|
|
319
320
|
```sh
|
320
|
-
$
|
321
|
+
$ uv tool install pystand
|
321
322
|
```
|
322
323
|
|
323
324
|
To upgrade:
|
324
325
|
|
325
326
|
```sh
|
326
|
-
$
|
327
|
+
$ uv tool upgrade pystand
|
327
328
|
```
|
328
329
|
|
329
330
|
To uninstall:
|
330
331
|
|
331
332
|
```sh
|
332
|
-
$
|
333
|
+
$ uv tool uninstall pystand
|
333
334
|
```
|
334
335
|
|
335
336
|
## Extrapolation of Python Versions
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=I7VERu60r7H6iCUy5SJvFv5_fT36MeNrIEZUzsoq-RM,36843
|
2
|
+
pystand-2.13.1.dist-info/METADATA,sha256=LsY47B2HgxjzssPumImNJmjcex6-uq4q5Z_6y022KNo,24811
|
3
|
+
pystand-2.13.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
4
|
+
pystand-2.13.1.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-2.13.1.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-2.13.1.dist-info/RECORD,,
|
pystand.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
#!/usr/bin/python3
|
2
2
|
# PYTHON_ARGCOMPLETE_OK
|
3
|
-
|
3
|
+
"""
|
4
4
|
Command line tool to download, install, and update pre-built Python
|
5
5
|
versions from the python-build-standalone project at
|
6
6
|
https://github.com/astral-sh/python-build-standalone.
|
7
|
-
|
7
|
+
"""
|
8
|
+
|
8
9
|
from __future__ import annotations
|
9
10
|
|
10
11
|
import os
|
@@ -44,22 +45,25 @@ DISTRIBUTIONS = {
|
|
44
45
|
('Linux', 'armv8l'): 'armv7-unknown-linux-gnueabihf-install_only_stripped',
|
45
46
|
('Darwin', 'x86_64'): 'x86_64-apple-darwin-install_only_stripped',
|
46
47
|
('Darwin', 'aarch64'): 'aarch64-apple-darwin-install_only_stripped',
|
47
|
-
('Windows', 'x86_64'):
|
48
|
-
'x86_64-pc-windows-msvc-shared-install_only_stripped',
|
48
|
+
('Windows', 'x86_64'): 'x86_64-pc-windows-msvc-shared-install_only_stripped',
|
49
49
|
('Windows', 'i686'): 'i686-pc-windows-msvc-shared-install_only_stripped',
|
50
50
|
}
|
51
51
|
|
52
|
+
|
52
53
|
def is_admin() -> bool:
|
53
|
-
|
54
|
+
"Check if we are running as root"
|
54
55
|
if platform.system() == 'Windows':
|
55
56
|
import ctypes
|
57
|
+
|
56
58
|
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore
|
57
59
|
|
58
60
|
return os.geteuid() == 0
|
59
61
|
|
62
|
+
|
60
63
|
def get_version() -> str:
|
61
|
-
|
64
|
+
"Return the version of this package"
|
62
65
|
from importlib.metadata import version
|
66
|
+
|
63
67
|
try:
|
64
68
|
ver = version(PROG)
|
65
69
|
except Exception:
|
@@ -67,12 +71,15 @@ def get_version() -> str:
|
|
67
71
|
|
68
72
|
return ver
|
69
73
|
|
74
|
+
|
70
75
|
def fmt(version, release) -> str:
|
71
|
-
|
76
|
+
"Return a formatted release version string"
|
72
77
|
return f'{version} @ {release}'
|
73
78
|
|
79
|
+
|
74
80
|
def get_json(file: Path) -> dict:
|
75
81
|
from json import load
|
82
|
+
|
76
83
|
'Get JSON data from given file'
|
77
84
|
try:
|
78
85
|
with file.open() as fp:
|
@@ -82,9 +89,11 @@ def get_json(file: Path) -> dict:
|
|
82
89
|
|
83
90
|
return {}
|
84
91
|
|
92
|
+
|
85
93
|
def set_json(file: Path, data: dict) -> str | None:
|
86
|
-
|
94
|
+
"Set JSON data to given file"
|
87
95
|
from json import dump
|
96
|
+
|
88
97
|
try:
|
89
98
|
with file.open('w') as fp:
|
90
99
|
dump(data, fp, indent=2)
|
@@ -93,11 +102,13 @@ def set_json(file: Path, data: dict) -> str | None:
|
|
93
102
|
|
94
103
|
return None
|
95
104
|
|
105
|
+
|
96
106
|
# The gh handle is an opaque github instance handle
|
97
107
|
get_gh_handle = None
|
98
108
|
|
109
|
+
|
99
110
|
def get_gh(args: Namespace) -> Any:
|
100
|
-
|
111
|
+
"Return a GitHub handle"
|
101
112
|
# The gh handle is a global to lazily create it only if/when needed
|
102
113
|
global get_gh_handle
|
103
114
|
if get_gh_handle:
|
@@ -105,17 +116,20 @@ def get_gh(args: Namespace) -> Any:
|
|
105
116
|
|
106
117
|
if args.github_access_token:
|
107
118
|
from github.Auth import Token
|
119
|
+
|
108
120
|
auth = Token(args.github_access_token)
|
109
121
|
else:
|
110
122
|
auth = None
|
111
123
|
|
112
124
|
# Save this handle globally for future use
|
113
125
|
from github import Github
|
126
|
+
|
114
127
|
get_gh_handle = Github(auth=auth) # type: ignore
|
115
128
|
return get_gh_handle
|
116
129
|
|
130
|
+
|
117
131
|
def rm_path(path: Path) -> None:
|
118
|
-
|
132
|
+
"Remove the given path"
|
119
133
|
if path.is_symlink():
|
120
134
|
path.unlink()
|
121
135
|
elif path.is_dir():
|
@@ -123,21 +137,25 @@ def rm_path(path: Path) -> None:
|
|
123
137
|
elif path.exists():
|
124
138
|
path.unlink()
|
125
139
|
|
140
|
+
|
126
141
|
def unpack_zst(filename: str, extract_dir: str) -> None:
|
127
|
-
|
142
|
+
"Unpack a zstandard compressed tar"
|
128
143
|
import tarfile
|
129
144
|
|
130
145
|
import zstandard
|
146
|
+
|
131
147
|
with open(filename, 'rb') as compressed:
|
132
148
|
dctx = zstandard.ZstdDecompressor()
|
133
149
|
with dctx.stream_reader(compressed) as reader:
|
134
150
|
with tarfile.open(fileobj=reader, mode='r|') as tar:
|
135
151
|
tar.extractall(path=extract_dir)
|
136
152
|
|
153
|
+
|
137
154
|
def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
138
|
-
|
155
|
+
"Fetch and unpack a release file"
|
139
156
|
from urllib.parse import unquote, urlparse
|
140
157
|
from urllib.request import urlretrieve
|
158
|
+
|
141
159
|
error = None
|
142
160
|
tmpdir = tdir.with_name(f'{tdir.name}-tmp')
|
143
161
|
rm_path(tmpdir)
|
@@ -184,18 +202,20 @@ def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
|
|
184
202
|
rm_path(tmpdir)
|
185
203
|
return error
|
186
204
|
|
205
|
+
|
187
206
|
def is_release_version(version: str) -> bool:
|
188
|
-
|
207
|
+
"Check if a string is a formal Python release tag"
|
189
208
|
return version.replace('.', '').isdigit()
|
190
209
|
|
210
|
+
|
191
211
|
class VersionMatcher:
|
192
|
-
|
212
|
+
"Match a version string to a list of versions"
|
213
|
+
|
193
214
|
def __init__(self, seq: Iterable[str]) -> None:
|
194
215
|
self.seq = sorted(seq, key=parse_version, reverse=True)
|
195
216
|
|
196
|
-
def match(self, version: str | None, *,
|
197
|
-
|
198
|
-
'Return full version string given a [possibly] part version prefix'
|
217
|
+
def match(self, version: str | None, *, upgrade: bool = False) -> str | None:
|
218
|
+
"Return full version string given a [possibly] part version prefix"
|
199
219
|
|
200
220
|
# If no version specified, return the latest release version
|
201
221
|
if not version:
|
@@ -222,25 +242,31 @@ class VersionMatcher:
|
|
222
242
|
# release.
|
223
243
|
for full_version in self.seq:
|
224
244
|
if full_version.startswith(version):
|
225
|
-
if not upgrade or not is_release
|
226
|
-
or is_release_version(full_version):
|
245
|
+
if not upgrade or not is_release or is_release_version(full_version):
|
227
246
|
return full_version
|
228
247
|
|
229
248
|
return None
|
230
249
|
|
250
|
+
|
231
251
|
def iter_versions(args: Namespace) -> Iterator[Path]:
|
232
|
-
|
252
|
+
"Iterate over all version dirs"
|
233
253
|
for f in args._versions.iterdir():
|
234
|
-
if
|
235
|
-
|
254
|
+
if (
|
255
|
+
f.is_dir()
|
256
|
+
and not f.is_symlink()
|
257
|
+
and f.name[0] != '.'
|
258
|
+
and f.name[0].isdigit()
|
259
|
+
):
|
236
260
|
yield f
|
237
261
|
|
262
|
+
|
238
263
|
def get_version_names(args: Namespace) -> list[str]:
|
239
|
-
|
264
|
+
"Return a list of validated version names based on command line args"
|
240
265
|
if args.all:
|
241
266
|
if not args.skip and args.version:
|
242
|
-
args.parser.error(
|
243
|
-
|
267
|
+
args.parser.error(
|
268
|
+
'Can not specify versions with --all unless also specifying --skip.'
|
269
|
+
)
|
244
270
|
else:
|
245
271
|
if args.skip:
|
246
272
|
args.parser.error('--skip can only be specified with --all.')
|
@@ -256,16 +282,16 @@ def get_version_names(args: Namespace) -> list[str]:
|
|
256
282
|
|
257
283
|
given = set(versions)
|
258
284
|
|
259
|
-
if
|
285
|
+
if unknown := given - all_names:
|
260
286
|
s = 's' if len(unknown) > 1 else ''
|
261
287
|
unknowns = [f'"{u}"' for u in unknown]
|
262
288
|
sys.exit(f'Error: version{s} {", ".join(unknowns)} not found.')
|
263
289
|
|
264
|
-
return sorted(all_names - given, key=parse_version)
|
265
|
-
|
290
|
+
return sorted(all_names - given, key=parse_version) if args.all else versions
|
291
|
+
|
266
292
|
|
267
293
|
def check_release_tag(release: str) -> str | None:
|
268
|
-
|
294
|
+
"Check the specified release tag is valid"
|
269
295
|
if not release.isdigit() or len(release) != len(SAMPL_RELEASE):
|
270
296
|
return 'Release must be a YYYYMMDD string.'
|
271
297
|
|
@@ -276,13 +302,15 @@ def check_release_tag(release: str) -> str | None:
|
|
276
302
|
|
277
303
|
return None
|
278
304
|
|
305
|
+
|
279
306
|
# Note we use a simple direct URL fetch to get the latest tag info
|
280
307
|
# because it is much faster than using the GitHub API, and has no
|
281
308
|
# rate-limits.
|
282
309
|
def fetch_tags() -> Iterator[tuple[str, str]]:
|
283
|
-
|
310
|
+
"Fetch the latest release tags from the GitHub release atom feed"
|
284
311
|
import xml.etree.ElementTree as et
|
285
312
|
from urllib.request import urlopen
|
313
|
+
|
286
314
|
try:
|
287
315
|
with urlopen(LATEST_RELEASES) as url:
|
288
316
|
data = et.parse(url).getroot()
|
@@ -296,9 +324,11 @@ def fetch_tags() -> Iterator[tuple[str, str]]:
|
|
296
324
|
if tl and dt:
|
297
325
|
yield tl, dt
|
298
326
|
|
327
|
+
|
299
328
|
def fetch_tag_latest() -> str:
|
300
|
-
|
329
|
+
"Fetch the latest release tag from the GitHub"
|
301
330
|
from urllib.request import urlopen
|
331
|
+
|
302
332
|
try:
|
303
333
|
with urlopen(LATEST_RELEASE_TAG) as url:
|
304
334
|
data = url.geturl()
|
@@ -307,8 +337,9 @@ def fetch_tag_latest() -> str:
|
|
307
337
|
|
308
338
|
return data.split('/')[-1]
|
309
339
|
|
340
|
+
|
310
341
|
def get_release_tag(args: Namespace) -> str:
|
311
|
-
|
342
|
+
"Return the release tag, or latest if not specified"
|
312
343
|
if release := args.release:
|
313
344
|
if err := check_release_tag(release):
|
314
345
|
sys.exit(err)
|
@@ -326,8 +357,9 @@ def get_release_tag(args: Namespace) -> str:
|
|
326
357
|
args._latest_release.write_text(tag + '\n')
|
327
358
|
return tag
|
328
359
|
|
360
|
+
|
329
361
|
def add_file(files: dict, tag: str, name: str, url: str) -> None:
|
330
|
-
|
362
|
+
"Extract the implementation, version, and architecture from a filename"
|
331
363
|
if name.endswith('.tar.zst'):
|
332
364
|
name = name[:-8]
|
333
365
|
elif name.endswith('.tar.gz'):
|
@@ -353,18 +385,19 @@ def add_file(files: dict, tag: str, name: str, url: str) -> None:
|
|
353
385
|
|
354
386
|
vers[ver][arch] = url
|
355
387
|
|
388
|
+
|
356
389
|
def get_release_files(args, tag, implementation: str | None = None) -> dict:
|
357
|
-
|
390
|
+
"Return the release files for the given tag"
|
358
391
|
# Look for tag data in our release cache
|
359
392
|
jfile = args._releases / tag
|
360
393
|
if not (files := get_json(jfile)):
|
361
|
-
|
362
394
|
# May have read this release before but it has no assets
|
363
395
|
if jfile.exists():
|
364
396
|
return {}
|
365
397
|
|
366
398
|
# Not in cache so fetch it (and also store in cache)
|
367
399
|
from github.GithubException import UnknownObjectException
|
400
|
+
|
368
401
|
gh = get_gh(args)
|
369
402
|
try:
|
370
403
|
release = gh.get_repo(GITHUB_REPO).get_release(tag)
|
@@ -384,8 +417,9 @@ def get_release_files(args, tag, implementation: str | None = None) -> dict:
|
|
384
417
|
|
385
418
|
return files.get(implementation, {}) if implementation else files
|
386
419
|
|
420
|
+
|
387
421
|
def update_version_symlinks(args: Namespace) -> None:
|
388
|
-
|
422
|
+
"Create/update symlinks pointing to latest version"
|
389
423
|
base = args._versions
|
390
424
|
if not base.exists():
|
391
425
|
return
|
@@ -435,11 +469,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
435
469
|
if not old_tgt or old_tgt != tgt:
|
436
470
|
(base / name).symlink_to(tgt, target_is_directory=True)
|
437
471
|
|
472
|
+
|
438
473
|
def purge_unused_releases(args: Namespace) -> None:
|
439
|
-
|
474
|
+
"Purge old releases that are no longer needed and have expired"
|
440
475
|
# Want to keep releases for versions that we currently have installed
|
441
|
-
keep = {
|
442
|
-
|
476
|
+
keep = {
|
477
|
+
r for v in iter_versions(args) if (r := get_json(v / args._data).get('release'))
|
478
|
+
}
|
443
479
|
|
444
480
|
# Add current release to keep list (even if not currently installed)
|
445
481
|
if args._latest_release.exists():
|
@@ -460,28 +496,51 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
460
496
|
if path.name not in keep:
|
461
497
|
rm_path(path)
|
462
498
|
|
463
|
-
class COMMAND:
|
464
|
-
'Base class for all commands'
|
465
|
-
commands = []
|
466
499
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
500
|
+
def show_list(args: Namespace) -> None:
|
501
|
+
"Show a list of available releases"
|
502
|
+
latest = parse_version(get_release_tag(args))
|
503
|
+
releases = {r: d for r, d in fetch_tags()}
|
504
|
+
cached = set(p.name for p in args._releases.iterdir())
|
505
|
+
for release in sorted(cached.union(releases)):
|
506
|
+
if args.re_match and not re.search(args.re_match, release):
|
507
|
+
continue
|
508
|
+
|
509
|
+
if dt_str := releases.get(release):
|
510
|
+
dts = (
|
511
|
+
datetime.fromisoformat(dt_str)
|
512
|
+
.astimezone()
|
513
|
+
.isoformat(sep='_', timespec='minutes')
|
514
|
+
)
|
515
|
+
else:
|
516
|
+
dts = '......................'
|
517
|
+
|
518
|
+
if release in cached:
|
519
|
+
ddir = args._downloads / release
|
520
|
+
count = len(list(ddir.iterdir())) if ddir.exists() else 0
|
521
|
+
app = f' cached + {count} downloaded files' if count > 0 else ' cached'
|
522
|
+
else:
|
523
|
+
app = ''
|
524
|
+
|
525
|
+
pre = ' pre-release' if parse_version(release) > latest else ''
|
526
|
+
|
527
|
+
print(f'{release} {dts}{app}{pre}')
|
528
|
+
|
471
529
|
|
472
530
|
def get_title(desc: str) -> str:
|
473
|
-
|
531
|
+
"Return single title line from description"
|
474
532
|
res = []
|
475
533
|
for line in desc.splitlines():
|
476
534
|
line = line.strip()
|
477
535
|
res.append(line)
|
478
536
|
if line.endswith('.'):
|
479
|
-
return ' '.
|
537
|
+
return ' '.join(res)
|
480
538
|
|
481
539
|
sys.exit('Must end description with a full stop.')
|
482
540
|
|
541
|
+
|
483
542
|
def remove(args: Namespace, version: str) -> None:
|
484
|
-
|
543
|
+
"Remove a version"
|
485
544
|
vdir = args._versions / version
|
486
545
|
if not vdir.exists():
|
487
546
|
return
|
@@ -493,8 +552,9 @@ def remove(args: Namespace, version: str) -> None:
|
|
493
552
|
|
494
553
|
shutil.rmtree(vdir)
|
495
554
|
|
555
|
+
|
496
556
|
def strip_binaries(vdir: Path, distribution: str) -> bool:
|
497
|
-
|
557
|
+
"Strip binaries from files in a version directory"
|
498
558
|
from subprocess import DEVNULL, run
|
499
559
|
|
500
560
|
# Only run the strip command on Linux hosts and for Linux distributions
|
@@ -517,14 +577,17 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
|
|
517
577
|
|
518
578
|
return was_stripped
|
519
579
|
|
520
|
-
|
521
|
-
|
522
|
-
|
580
|
+
|
581
|
+
def install(
|
582
|
+
args: Namespace, vdir: Path, release: str, distribution: str, files: dict
|
583
|
+
) -> str | None:
|
584
|
+
"Install a version"
|
523
585
|
version = vdir.name
|
524
586
|
|
525
587
|
if not (url := files[version].get(distribution)):
|
526
|
-
return
|
527
|
-
|
588
|
+
return (
|
589
|
+
f'Arch "{distribution}" not found for release {release} version {version}.'
|
590
|
+
)
|
528
591
|
|
529
592
|
tmpdir = args._versions / f'.{version}-tmp'
|
530
593
|
rm_path(tmpdir)
|
@@ -536,7 +599,7 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
536
599
|
if not args.no_strip and strip_binaries(tmpdir, distribution):
|
537
600
|
data['stripped'] = 'true'
|
538
601
|
|
539
|
-
if
|
602
|
+
if error := set_json(tmpdir / args._data, data):
|
540
603
|
error = f'Failed to write {version} data file: {error}'
|
541
604
|
|
542
605
|
if error:
|
@@ -547,8 +610,9 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
547
610
|
|
548
611
|
return error
|
549
612
|
|
613
|
+
|
550
614
|
def main() -> str | None:
|
551
|
-
|
615
|
+
"Main code"
|
552
616
|
distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
|
553
617
|
distro_help = distro_default or '?unknown?'
|
554
618
|
|
@@ -557,41 +621,67 @@ def main() -> str | None:
|
|
557
621
|
cache_dir = platformdirs.user_cache_path() / PROG
|
558
622
|
|
559
623
|
# Parse arguments
|
560
|
-
opt = ArgumentParser(
|
561
|
-
|
562
|
-
|
563
|
-
|
624
|
+
opt = ArgumentParser(
|
625
|
+
description=__doc__,
|
626
|
+
epilog='Some commands offer aliases as shown in parentheses above. '
|
627
|
+
'Note you can set default starting global options in '
|
628
|
+
f'{CNFFILE}.',
|
629
|
+
)
|
564
630
|
|
565
631
|
# Set up main/global arguments
|
566
|
-
opt.add_argument(
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
opt.add_argument(
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
632
|
+
opt.add_argument(
|
633
|
+
'-D',
|
634
|
+
'--distribution',
|
635
|
+
help=f'{REPO} distribution. Default is "{distro_help} for this host',
|
636
|
+
)
|
637
|
+
opt.add_argument(
|
638
|
+
'-P',
|
639
|
+
'--prefix-dir',
|
640
|
+
default=prefix_dir,
|
641
|
+
help='specify prefix dir for storing versions. Default is "%(default)s"',
|
642
|
+
)
|
643
|
+
opt.add_argument(
|
644
|
+
'-C',
|
645
|
+
'--cache-dir',
|
646
|
+
default=str(cache_dir),
|
647
|
+
help='specify cache dir for downloads. Default is "%(default)s"',
|
648
|
+
)
|
649
|
+
opt.add_argument(
|
650
|
+
'-M',
|
651
|
+
'--cache-minutes',
|
652
|
+
default=60,
|
653
|
+
type=float,
|
654
|
+
help='cache latest YYYYMMDD release tag fetch for this '
|
655
|
+
'many minutes, before rechecking for latest. '
|
656
|
+
'Default is %(default)d minutes',
|
657
|
+
)
|
658
|
+
opt.add_argument(
|
659
|
+
'--purge-days',
|
660
|
+
default=90,
|
661
|
+
type=int,
|
662
|
+
help='cache YYYYMMDD release file lists and downloads for '
|
663
|
+
'this number of days after last version referencing that '
|
664
|
+
'release is removed. Default is %(default)d days',
|
665
|
+
)
|
666
|
+
opt.add_argument(
|
667
|
+
'--github-access-token',
|
668
|
+
help='optional Github access token. Can specify to reduce rate limiting.',
|
669
|
+
)
|
670
|
+
opt.add_argument(
|
671
|
+
'--no-strip', action='store_true', help='do not strip downloaded binaries'
|
672
|
+
)
|
673
|
+
opt.add_argument(
|
674
|
+
'-V', '--version', action='store_true', help=f'just show {PROG} version'
|
675
|
+
)
|
590
676
|
cmd = opt.add_subparsers(title='Commands', dest='cmdname')
|
591
677
|
|
592
678
|
# Add each command ..
|
593
|
-
for
|
594
|
-
name
|
679
|
+
for name in globals():
|
680
|
+
if not name[0].islower() or not name.endswith('_'):
|
681
|
+
continue
|
682
|
+
|
683
|
+
cls = globals()[name]
|
684
|
+
name = name[:-1]
|
595
685
|
|
596
686
|
if hasattr(cls, 'doc'):
|
597
687
|
desc = cls.doc.strip()
|
@@ -602,8 +692,7 @@ def main() -> str | None:
|
|
602
692
|
|
603
693
|
aliases = cls.aliases if hasattr(cls, 'aliases') else []
|
604
694
|
title = get_title(desc)
|
605
|
-
cmdopt = cmd.add_parser(name, description=desc, help=title,
|
606
|
-
aliases=aliases)
|
695
|
+
cmdopt = cmd.add_parser(name, description=desc, help=title, aliases=aliases)
|
607
696
|
|
608
697
|
# Set up this commands own arguments, if it has any
|
609
698
|
if hasattr(cls, 'init'):
|
@@ -637,8 +726,10 @@ def main() -> str | None:
|
|
637
726
|
|
638
727
|
distribution = args.distribution or distro_default
|
639
728
|
if not distribution:
|
640
|
-
sys.exit(
|
641
|
-
|
729
|
+
sys.exit(
|
730
|
+
'Unknown system + machine distribution. Please specify '
|
731
|
+
'using -D/--distribution option.'
|
732
|
+
)
|
642
733
|
|
643
734
|
# Keep some useful info in the namespace passed to the command
|
644
735
|
prefix_dir = Path(args.prefix_dir).expanduser().resolve()
|
@@ -661,23 +752,35 @@ def main() -> str | None:
|
|
661
752
|
update_version_symlinks(args)
|
662
753
|
return result
|
663
754
|
|
664
|
-
|
665
|
-
|
755
|
+
|
756
|
+
# COMMAND
|
757
|
+
class install_:
|
666
758
|
doc = f'Install one or more versions from a {REPO} release.'
|
667
759
|
|
668
760
|
@staticmethod
|
669
761
|
def init(parser: ArgumentParser) -> None:
|
670
|
-
parser.add_argument(
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
762
|
+
parser.add_argument(
|
763
|
+
'-r',
|
764
|
+
'--release',
|
765
|
+
help=f'install from specified {REPO} '
|
766
|
+
f'YYYYMMDD release (e.g. {SAMPL_RELEASE}), '
|
767
|
+
'default is latest release',
|
768
|
+
)
|
769
|
+
parser.add_argument(
|
770
|
+
'-f',
|
771
|
+
'--force',
|
772
|
+
action='store_true',
|
773
|
+
help='force install even if already installed',
|
774
|
+
)
|
775
|
+
parser.add_argument(
|
776
|
+
'-s',
|
777
|
+
'--include-source',
|
778
|
+
action='store_true',
|
779
|
+
help='also install source files if available in distribution download',
|
780
|
+
)
|
781
|
+
parser.add_argument(
|
782
|
+
'version', nargs='+', help='version to install. E.g. 3.12 or 3.12.3'
|
783
|
+
)
|
681
784
|
|
682
785
|
@staticmethod
|
683
786
|
def run(args: Namespace) -> str | None:
|
@@ -703,27 +806,40 @@ class _install(COMMAND):
|
|
703
806
|
|
704
807
|
print(f'Version {fmt(version, release)} installed.')
|
705
808
|
|
706
|
-
|
707
|
-
|
708
|
-
|
809
|
+
|
810
|
+
# COMMAND
|
811
|
+
class update_:
|
812
|
+
"Update one, more, or all versions to another release."
|
813
|
+
|
709
814
|
aliases = ['upgrade']
|
710
815
|
|
711
816
|
@staticmethod
|
712
817
|
def init(parser: ArgumentParser) -> None:
|
713
|
-
parser.add_argument(
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
818
|
+
parser.add_argument(
|
819
|
+
'-r',
|
820
|
+
'--release',
|
821
|
+
help='update to specified YYYMMDD release (e.g. '
|
822
|
+
f'{SAMPL_RELEASE}), default is latest release',
|
823
|
+
)
|
824
|
+
parser.add_argument(
|
825
|
+
'-a', '--all', action='store_true', help='update ALL versions'
|
826
|
+
)
|
827
|
+
parser.add_argument(
|
828
|
+
'--skip',
|
829
|
+
action='store_true',
|
830
|
+
help='skip the specified versions when '
|
831
|
+
'updating all (only can be specified with --all)',
|
832
|
+
)
|
833
|
+
parser.add_argument(
|
834
|
+
'-k',
|
835
|
+
'--keep',
|
836
|
+
action='store_true',
|
837
|
+
help='keep old version after updating (but only '
|
838
|
+
'if different version number)',
|
839
|
+
)
|
840
|
+
parser.add_argument(
|
841
|
+
'version', nargs='*', help='version to update (or to skip for --all --skip)'
|
842
|
+
)
|
727
843
|
|
728
844
|
@staticmethod
|
729
845
|
def run(args: Namespace) -> str | None:
|
@@ -749,54 +865,66 @@ class _update(COMMAND):
|
|
749
865
|
continue
|
750
866
|
|
751
867
|
if nextver == version and args.keep:
|
752
|
-
print(
|
753
|
-
|
754
|
-
|
868
|
+
print(
|
869
|
+
f'Error: {fmt(version, release)} would not be kept '
|
870
|
+
f'if update to {fmt(nextver, release_target)} '
|
871
|
+
f'distribution="{distribution}"',
|
872
|
+
file=sys.stderr,
|
873
|
+
)
|
755
874
|
continue
|
756
875
|
|
757
876
|
new_vdir = args._versions / nextver
|
758
877
|
if nextver != version and new_vdir.exists():
|
759
878
|
continue
|
760
879
|
|
761
|
-
print(
|
762
|
-
|
763
|
-
|
880
|
+
print(
|
881
|
+
f'{fmt(version, release)} updating to '
|
882
|
+
f'{fmt(nextver, release_target)} '
|
883
|
+
f'distribution="{distribution}" ..'
|
884
|
+
)
|
764
885
|
|
765
886
|
# If the source was originally included, then include it in
|
766
887
|
# the update.
|
767
888
|
args.include_source = (vdir / 'src').is_dir()
|
768
889
|
|
769
|
-
if error := install(args, new_vdir, release_target, distribution,
|
770
|
-
files):
|
890
|
+
if error := install(args, new_vdir, release_target, distribution, files):
|
771
891
|
return error
|
772
892
|
|
773
893
|
if nextver != version and not args.keep:
|
774
894
|
remove(args, version)
|
775
895
|
|
776
|
-
|
777
|
-
|
778
|
-
|
896
|
+
|
897
|
+
# COMMAND
|
898
|
+
class remove_:
|
899
|
+
"Remove/uninstall one, more, or all versions."
|
900
|
+
|
779
901
|
aliases = ['uninstall']
|
780
902
|
|
781
903
|
@staticmethod
|
782
904
|
def init(parser: ArgumentParser) -> None:
|
783
|
-
parser.add_argument(
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
905
|
+
parser.add_argument(
|
906
|
+
'-a', '--all', action='store_true', help='remove ALL versions'
|
907
|
+
)
|
908
|
+
parser.add_argument(
|
909
|
+
'--skip',
|
910
|
+
action='store_true',
|
911
|
+
help='skip the specified versions when '
|
912
|
+
'removing all (only can be specified with --all)',
|
913
|
+
)
|
914
|
+
parser.add_argument(
|
915
|
+
'-r',
|
916
|
+
'--release',
|
917
|
+
help='only remove versions if from specified '
|
918
|
+
f'YYYMMDD release (e.g. {SAMPL_RELEASE})',
|
919
|
+
)
|
920
|
+
parser.add_argument(
|
921
|
+
'version', nargs='*', help='version to remove (or to skip for --all --skip)'
|
922
|
+
)
|
794
923
|
|
795
924
|
@staticmethod
|
796
925
|
def run(args: Namespace) -> str | None:
|
797
926
|
release_del = args.release
|
798
|
-
if release_del and
|
799
|
-
(err := check_release_tag(release_del)):
|
927
|
+
if release_del and (err := check_release_tag(release_del)):
|
800
928
|
return err
|
801
929
|
|
802
930
|
for version in get_version_names(args):
|
@@ -806,20 +934,29 @@ class _remove(COMMAND):
|
|
806
934
|
remove(args, version)
|
807
935
|
print(f'Version {fmt(version, release)} removed.')
|
808
936
|
|
809
|
-
|
810
|
-
|
811
|
-
|
937
|
+
|
938
|
+
# COMMAND
|
939
|
+
class list_:
|
940
|
+
"List installed versions and show which have an update available."
|
941
|
+
|
812
942
|
@staticmethod
|
813
943
|
def init(parser: ArgumentParser) -> None:
|
814
|
-
parser.add_argument(
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
944
|
+
parser.add_argument(
|
945
|
+
'-v',
|
946
|
+
'--verbose',
|
947
|
+
action='store_true',
|
948
|
+
help='explicitly report why a version is not eligible for update',
|
949
|
+
)
|
950
|
+
parser.add_argument(
|
951
|
+
'-r',
|
952
|
+
'--release',
|
953
|
+
help='use specified YYYYMMDD release '
|
954
|
+
f'(e.g. {SAMPL_RELEASE}) for verbose compare, '
|
955
|
+
'default is latest release',
|
956
|
+
)
|
957
|
+
parser.add_argument(
|
958
|
+
'version', nargs='*', help='only list specified version, else all'
|
959
|
+
)
|
823
960
|
|
824
961
|
@staticmethod
|
825
962
|
def run(args: Namespace) -> str | None:
|
@@ -844,119 +981,141 @@ class _list(COMMAND):
|
|
844
981
|
nextver = matcher.match(version, upgrade=True)
|
845
982
|
if not nextver:
|
846
983
|
if args.verbose:
|
847
|
-
app =
|
848
|
-
|
849
|
-
|
984
|
+
app = (
|
985
|
+
' not eligible for update because '
|
986
|
+
f'release {release_target} does not provide '
|
987
|
+
'this version.'
|
988
|
+
)
|
850
989
|
else:
|
851
990
|
new_vdir = args._versions / nextver
|
852
991
|
if nextver != version and new_vdir.exists():
|
853
992
|
if args.verbose:
|
854
|
-
nrelease = get_json(
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
993
|
+
nrelease = get_json(new_vdir / args._data).get(
|
994
|
+
'release', '?'
|
995
|
+
)
|
996
|
+
app = (
|
997
|
+
f' not eligible for '
|
998
|
+
f'update because {fmt(nextver, nrelease)} '
|
999
|
+
'is already installed.'
|
1000
|
+
)
|
859
1001
|
else:
|
860
1002
|
# May not be updatable if newer release does not support
|
861
1003
|
# this same distribution anymore
|
862
1004
|
if nextver and distribution in files.get(nextver, {}):
|
863
|
-
upd = ' updatable to '
|
864
|
-
f'{fmt(nextver, release_target)}'
|
1005
|
+
upd = f' updatable to {fmt(nextver, release_target)}'
|
865
1006
|
elif args.verbose:
|
866
|
-
app =
|
867
|
-
|
868
|
-
|
869
|
-
|
1007
|
+
app = (
|
1008
|
+
' not eligible for update because '
|
1009
|
+
f'{fmt(nextver, release_target)} does '
|
1010
|
+
'not provide '
|
1011
|
+
f'distribution="{distribution}".'
|
1012
|
+
)
|
1013
|
+
|
1014
|
+
print(f'{fmt(version, release)}{upd} distribution="{distribution}"{app}')
|
870
1015
|
|
871
|
-
print(f'{fmt(version, release)}{upd} '
|
872
|
-
f'distribution="{distribution}"{app}')
|
873
1016
|
|
874
|
-
|
875
|
-
class
|
876
|
-
doc = f
|
1017
|
+
# COMMAND
|
1018
|
+
class show_:
|
1019
|
+
doc = f"""
|
877
1020
|
Show versions available from a release.
|
878
1021
|
|
879
1022
|
View available releases and their distributions at
|
880
1023
|
{GITHUB_SITE}/releases.
|
881
|
-
|
1024
|
+
"""
|
882
1025
|
|
883
1026
|
@staticmethod
|
884
1027
|
def init(parser: ArgumentParser) -> None:
|
885
1028
|
group = parser.add_mutually_exclusive_group()
|
886
|
-
group.add_argument(
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
1029
|
+
group.add_argument(
|
1030
|
+
'-l', '--list', action='store_true', help='just list recent releases'
|
1031
|
+
)
|
1032
|
+
group.add_argument(
|
1033
|
+
'-r',
|
1034
|
+
'--release',
|
1035
|
+
help=f'{REPO} YYYYMMDD release to show (e.g. '
|
1036
|
+
f'{SAMPL_RELEASE}), default is latest release',
|
1037
|
+
)
|
1038
|
+
parser.add_argument(
|
1039
|
+
'-a',
|
1040
|
+
'--all',
|
1041
|
+
action='store_true',
|
1042
|
+
help='show all available distributions for each version from the release',
|
1043
|
+
)
|
1044
|
+
parser.add_argument(
|
1045
|
+
're_match',
|
1046
|
+
nargs='?',
|
1047
|
+
help='show only versions+distributions '
|
1048
|
+
'matching this regular expression pattern',
|
1049
|
+
)
|
897
1050
|
|
898
1051
|
@staticmethod
|
899
|
-
def run(args: Namespace) -> None:
|
1052
|
+
def run(args: Namespace) -> str | None:
|
900
1053
|
if args.all and args.list:
|
901
1054
|
args.parser.error('Can not specify --all with --list.')
|
902
1055
|
|
903
1056
|
if args.list:
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
dts = datetime.fromisoformat(datestr).astimezone().isoformat(
|
909
|
-
sep='_', timespec='minutes')
|
910
|
-
print(title, dts)
|
911
|
-
|
912
|
-
return
|
1057
|
+
args.release = False
|
1058
|
+
show_list(args)
|
1059
|
+
return None
|
913
1060
|
|
914
1061
|
release = get_release_tag(args)
|
915
1062
|
files = get_release_files(args, release, 'cpython')
|
916
1063
|
if not files:
|
917
|
-
|
1064
|
+
return f'Error: release "{release}" not found.'
|
918
1065
|
|
919
1066
|
installed = {}
|
920
1067
|
for vdir in iter_versions(args):
|
921
1068
|
data = get_json(vdir / args._data)
|
922
|
-
if data.get('release') == release and
|
923
|
-
(distro := data.get('distribution')):
|
1069
|
+
if data.get('release') == release and (distro := data.get('distribution')):
|
924
1070
|
installed[vdir.name] = distro
|
925
1071
|
|
926
1072
|
installable = False
|
927
1073
|
for version in sorted(files, key=parse_version):
|
928
1074
|
installed_distribution = installed.get(version)
|
929
1075
|
for distribution in files[version]:
|
930
|
-
app = ' (installed)'
|
931
|
-
|
932
|
-
if args.all or app \
|
933
|
-
or distribution == args._distribution:
|
1076
|
+
app = ' (installed)' if distribution == installed_distribution else ''
|
1077
|
+
if args.all or app or distribution == args._distribution:
|
934
1078
|
if distribution == args._distribution:
|
935
1079
|
installable = True
|
936
1080
|
|
937
|
-
if not args.re_match or
|
938
|
-
|
939
|
-
|
940
|
-
print(
|
941
|
-
f'
|
1081
|
+
if not args.re_match or re.search(
|
1082
|
+
args.re_match, f'{version}+{distribution}'
|
1083
|
+
):
|
1084
|
+
print(
|
1085
|
+
f'{fmt(version, release)} '
|
1086
|
+
f'distribution="{distribution}"{app}'
|
1087
|
+
)
|
942
1088
|
if not installable:
|
943
|
-
print(
|
944
|
-
|
1089
|
+
print(
|
1090
|
+
f'Warning: no distribution="{args._distribution}" '
|
1091
|
+
'versions found in '
|
1092
|
+
f'release "{release}".'
|
1093
|
+
)
|
1094
|
+
|
1095
|
+
|
1096
|
+
# COMMAND
|
1097
|
+
class path_:
|
1098
|
+
"Show path prefix to installed version base directory."
|
945
1099
|
|
946
|
-
@COMMAND.add
|
947
|
-
class _path(COMMAND):
|
948
|
-
'Show path prefix to installed version base directory.'
|
949
1100
|
@staticmethod
|
950
1101
|
def init(parser: ArgumentParser) -> None:
|
951
|
-
parser.add_argument(
|
952
|
-
|
953
|
-
|
954
|
-
|
1102
|
+
parser.add_argument(
|
1103
|
+
'-p',
|
1104
|
+
'--python-path',
|
1105
|
+
action='store_true',
|
1106
|
+
help='add path to python executable',
|
1107
|
+
)
|
1108
|
+
parser.add_argument(
|
1109
|
+
'-r', '--resolve', action='store_true', help='fully resolve given version'
|
1110
|
+
)
|
955
1111
|
group = parser.add_mutually_exclusive_group()
|
956
|
-
group.add_argument(
|
957
|
-
|
958
|
-
|
959
|
-
|
1112
|
+
group.add_argument(
|
1113
|
+
'-c',
|
1114
|
+
'--cache-path',
|
1115
|
+
action='store_true',
|
1116
|
+
help='just show path to cache dir',
|
1117
|
+
)
|
1118
|
+
group.add_argument('version', nargs='?', help='version number to show path for')
|
960
1119
|
|
961
1120
|
@staticmethod
|
962
1121
|
def run(args: Namespace) -> str | None:
|
@@ -980,10 +1139,10 @@ class _path(COMMAND):
|
|
980
1139
|
if not path.exists():
|
981
1140
|
path = basepath / 'python.exe'
|
982
1141
|
if not path.exists():
|
983
|
-
return 'Error: Can not find python executable in '
|
984
|
-
f'"{basepath}"'
|
1142
|
+
return f'Error: Can not find python executable in "{basepath}"'
|
985
1143
|
|
986
1144
|
print(path)
|
987
1145
|
|
1146
|
+
|
988
1147
|
if __name__ == '__main__':
|
989
1148
|
sys.exit(main())
|
pystand-2.11.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=6yFhPrfS19fvrqxZKujgfWvbE0BxlKyexaX2-uXVD8E,36230
|
2
|
-
pystand-2.11.dist-info/METADATA,sha256=Be4WcqhlUAXtn_PbXtG1hAlklpEQ3bspNTvhuKuOTJg,24843
|
3
|
-
pystand-2.11.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
4
|
-
pystand-2.11.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-2.11.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-2.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|