pystand 2.12__py3-none-any.whl → 2.14__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.12.dist-info → pystand-2.14.dist-info}/METADATA +21 -22
- pystand-2.14.dist-info/RECORD +6 -0
- {pystand-2.12.dist-info → pystand-2.14.dist-info}/WHEEL +1 -1
- pystand.py +363 -222
- pystand-2.12.dist-info/RECORD +0 -6
- {pystand-2.12.dist-info → pystand-2.14.dist-info}/entry_points.txt +0 -0
- {pystand-2.12.dist-info → pystand-2.14.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: pystand
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.14
|
4
4
|
Summary: Install Python versions from python-build-standalone project
|
5
5
|
Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
|
6
|
-
License:
|
6
|
+
License-Expression: GPL-3.0-or-later
|
7
7
|
Project-URL: Homepage, https://github.com/bulletmark/pystand
|
8
8
|
Keywords: python-build-standalone,pyenv,hatch,pdm
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
@@ -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
|
@@ -425,8 +426,6 @@ Note you can set a different default distribution by specifying
|
|
425
426
|
The `show` command can be used to search for distributions as seen in the
|
426
427
|
following examples.
|
427
428
|
|
428
|
-
```sh
|
429
|
-
|
430
429
|
List all the versions installed on this system (at the default location):
|
431
430
|
|
432
431
|
```sh
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=gku268WNQw2VqYrjZa7R37k15a4Hkz5tBJ2_E75lBWw,36823
|
2
|
+
pystand-2.14.dist-info/METADATA,sha256=U5ybEsQsHJ5yn7M1mGtQg50RbOr6sjAhvQ3cMqTN0OU,24824
|
3
|
+
pystand-2.14.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
4
|
+
pystand-2.14.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-2.14.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-2.14.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,22 +385,22 @@ 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
|
-
from github.GithubException import UnknownObjectException
|
368
399
|
gh = get_gh(args)
|
369
400
|
try:
|
370
401
|
release = gh.get_repo(GITHUB_REPO).get_release(tag)
|
371
|
-
except
|
402
|
+
except Exception as e:
|
403
|
+
print(f'Error: {str(e)}', file=sys.stderr)
|
372
404
|
return {}
|
373
405
|
|
374
406
|
# Iterate over the release assets and store pertinent files in a
|
@@ -384,8 +416,9 @@ def get_release_files(args, tag, implementation: str | None = None) -> dict:
|
|
384
416
|
|
385
417
|
return files.get(implementation, {}) if implementation else files
|
386
418
|
|
419
|
+
|
387
420
|
def update_version_symlinks(args: Namespace) -> None:
|
388
|
-
|
421
|
+
"Create/update symlinks pointing to latest version"
|
389
422
|
base = args._versions
|
390
423
|
if not base.exists():
|
391
424
|
return
|
@@ -435,11 +468,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
435
468
|
if not old_tgt or old_tgt != tgt:
|
436
469
|
(base / name).symlink_to(tgt, target_is_directory=True)
|
437
470
|
|
471
|
+
|
438
472
|
def purge_unused_releases(args: Namespace) -> None:
|
439
|
-
|
473
|
+
"Purge old releases that are no longer needed and have expired"
|
440
474
|
# Want to keep releases for versions that we currently have installed
|
441
|
-
keep = {
|
442
|
-
|
475
|
+
keep = {
|
476
|
+
r for v in iter_versions(args) if (r := get_json(v / args._data).get('release'))
|
477
|
+
}
|
443
478
|
|
444
479
|
# Add current release to keep list (even if not currently installed)
|
445
480
|
if args._latest_release.exists():
|
@@ -460,8 +495,10 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
460
495
|
if path.name not in keep:
|
461
496
|
rm_path(path)
|
462
497
|
|
498
|
+
|
463
499
|
def show_list(args: Namespace) -> None:
|
464
|
-
|
500
|
+
"Show a list of available releases"
|
501
|
+
latest = parse_version(get_release_tag(args))
|
465
502
|
releases = {r: d for r, d in fetch_tags()}
|
466
503
|
cached = set(p.name for p in args._releases.iterdir())
|
467
504
|
for release in sorted(cached.union(releases)):
|
@@ -469,43 +506,40 @@ def show_list(args: Namespace) -> None:
|
|
469
506
|
continue
|
470
507
|
|
471
508
|
if dt_str := releases.get(release):
|
472
|
-
dts =
|
473
|
-
|
509
|
+
dts = (
|
510
|
+
datetime.fromisoformat(dt_str)
|
511
|
+
.astimezone()
|
512
|
+
.isoformat(sep='_', timespec='minutes')
|
513
|
+
)
|
474
514
|
else:
|
475
515
|
dts = '......................'
|
476
516
|
|
477
517
|
if release in cached:
|
478
518
|
ddir = args._downloads / release
|
479
519
|
count = len(list(ddir.iterdir())) if ddir.exists() else 0
|
480
|
-
app = f' cached + {count} downloaded files'
|
481
|
-
if count > 0 else ' cached'
|
520
|
+
app = f' cached + {count} downloaded files' if count > 0 else ' cached'
|
482
521
|
else:
|
483
522
|
app = ''
|
484
523
|
|
485
|
-
|
524
|
+
pre = ' pre-release' if parse_version(release) > latest else ''
|
486
525
|
|
487
|
-
|
488
|
-
'Base class for all commands'
|
489
|
-
commands = []
|
526
|
+
print(f'{release} {dts}{app}{pre}')
|
490
527
|
|
491
|
-
@classmethod
|
492
|
-
def add(cls, parent) -> None:
|
493
|
-
'Append parent command to internal list'
|
494
|
-
cls.commands.append(parent)
|
495
528
|
|
496
529
|
def get_title(desc: str) -> str:
|
497
|
-
|
530
|
+
"Return single title line from description"
|
498
531
|
res = []
|
499
532
|
for line in desc.splitlines():
|
500
533
|
line = line.strip()
|
501
534
|
res.append(line)
|
502
535
|
if line.endswith('.'):
|
503
|
-
return ' '.
|
536
|
+
return ' '.join(res)
|
504
537
|
|
505
538
|
sys.exit('Must end description with a full stop.')
|
506
539
|
|
540
|
+
|
507
541
|
def remove(args: Namespace, version: str) -> None:
|
508
|
-
|
542
|
+
"Remove a version"
|
509
543
|
vdir = args._versions / version
|
510
544
|
if not vdir.exists():
|
511
545
|
return
|
@@ -517,8 +551,9 @@ def remove(args: Namespace, version: str) -> None:
|
|
517
551
|
|
518
552
|
shutil.rmtree(vdir)
|
519
553
|
|
554
|
+
|
520
555
|
def strip_binaries(vdir: Path, distribution: str) -> bool:
|
521
|
-
|
556
|
+
"Strip binaries from files in a version directory"
|
522
557
|
from subprocess import DEVNULL, run
|
523
558
|
|
524
559
|
# Only run the strip command on Linux hosts and for Linux distributions
|
@@ -541,14 +576,17 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
|
|
541
576
|
|
542
577
|
return was_stripped
|
543
578
|
|
544
|
-
|
545
|
-
|
546
|
-
|
579
|
+
|
580
|
+
def install(
|
581
|
+
args: Namespace, vdir: Path, release: str, distribution: str, files: dict
|
582
|
+
) -> str | None:
|
583
|
+
"Install a version"
|
547
584
|
version = vdir.name
|
548
585
|
|
549
586
|
if not (url := files[version].get(distribution)):
|
550
|
-
return
|
551
|
-
|
587
|
+
return (
|
588
|
+
f'Arch "{distribution}" not found for release {release} version {version}.'
|
589
|
+
)
|
552
590
|
|
553
591
|
tmpdir = args._versions / f'.{version}-tmp'
|
554
592
|
rm_path(tmpdir)
|
@@ -560,7 +598,7 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
560
598
|
if not args.no_strip and strip_binaries(tmpdir, distribution):
|
561
599
|
data['stripped'] = 'true'
|
562
600
|
|
563
|
-
if
|
601
|
+
if error := set_json(tmpdir / args._data, data):
|
564
602
|
error = f'Failed to write {version} data file: {error}'
|
565
603
|
|
566
604
|
if error:
|
@@ -571,8 +609,9 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
571
609
|
|
572
610
|
return error
|
573
611
|
|
612
|
+
|
574
613
|
def main() -> str | None:
|
575
|
-
|
614
|
+
"Main code"
|
576
615
|
distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
|
577
616
|
distro_help = distro_default or '?unknown?'
|
578
617
|
|
@@ -581,41 +620,67 @@ def main() -> str | None:
|
|
581
620
|
cache_dir = platformdirs.user_cache_path() / PROG
|
582
621
|
|
583
622
|
# Parse arguments
|
584
|
-
opt = ArgumentParser(
|
585
|
-
|
586
|
-
|
587
|
-
|
623
|
+
opt = ArgumentParser(
|
624
|
+
description=__doc__,
|
625
|
+
epilog='Some commands offer aliases as shown in parentheses above. '
|
626
|
+
'Note you can set default starting global options in '
|
627
|
+
f'{CNFFILE}.',
|
628
|
+
)
|
588
629
|
|
589
630
|
# Set up main/global arguments
|
590
|
-
opt.add_argument(
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
opt.add_argument(
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
631
|
+
opt.add_argument(
|
632
|
+
'-D',
|
633
|
+
'--distribution',
|
634
|
+
help=f'{REPO} distribution. Default is "{distro_help} for this host',
|
635
|
+
)
|
636
|
+
opt.add_argument(
|
637
|
+
'-P',
|
638
|
+
'--prefix-dir',
|
639
|
+
default=prefix_dir,
|
640
|
+
help='specify prefix dir for storing versions. Default is "%(default)s"',
|
641
|
+
)
|
642
|
+
opt.add_argument(
|
643
|
+
'-C',
|
644
|
+
'--cache-dir',
|
645
|
+
default=str(cache_dir),
|
646
|
+
help='specify cache dir for downloads. Default is "%(default)s"',
|
647
|
+
)
|
648
|
+
opt.add_argument(
|
649
|
+
'-M',
|
650
|
+
'--cache-minutes',
|
651
|
+
default=60,
|
652
|
+
type=float,
|
653
|
+
help='cache latest YYYYMMDD release tag fetch for this '
|
654
|
+
'many minutes, before rechecking for latest. '
|
655
|
+
'Default is %(default)d minutes',
|
656
|
+
)
|
657
|
+
opt.add_argument(
|
658
|
+
'--purge-days',
|
659
|
+
default=90,
|
660
|
+
type=int,
|
661
|
+
help='cache YYYYMMDD release file lists and downloads for '
|
662
|
+
'this number of days after last version referencing that '
|
663
|
+
'release is removed. Default is %(default)d days',
|
664
|
+
)
|
665
|
+
opt.add_argument(
|
666
|
+
'--github-access-token',
|
667
|
+
help='optional Github access token. Can specify to reduce rate limiting.',
|
668
|
+
)
|
669
|
+
opt.add_argument(
|
670
|
+
'--no-strip', action='store_true', help='do not strip downloaded binaries'
|
671
|
+
)
|
672
|
+
opt.add_argument(
|
673
|
+
'-V', '--version', action='store_true', help=f'just show {PROG} version'
|
674
|
+
)
|
614
675
|
cmd = opt.add_subparsers(title='Commands', dest='cmdname')
|
615
676
|
|
616
677
|
# Add each command ..
|
617
|
-
for
|
618
|
-
name
|
678
|
+
for name in globals():
|
679
|
+
if not name[0].islower() or not name.endswith('_'):
|
680
|
+
continue
|
681
|
+
|
682
|
+
cls = globals()[name]
|
683
|
+
name = name[:-1]
|
619
684
|
|
620
685
|
if hasattr(cls, 'doc'):
|
621
686
|
desc = cls.doc.strip()
|
@@ -626,8 +691,7 @@ def main() -> str | None:
|
|
626
691
|
|
627
692
|
aliases = cls.aliases if hasattr(cls, 'aliases') else []
|
628
693
|
title = get_title(desc)
|
629
|
-
cmdopt = cmd.add_parser(name, description=desc, help=title,
|
630
|
-
aliases=aliases)
|
694
|
+
cmdopt = cmd.add_parser(name, description=desc, help=title, aliases=aliases)
|
631
695
|
|
632
696
|
# Set up this commands own arguments, if it has any
|
633
697
|
if hasattr(cls, 'init'):
|
@@ -661,8 +725,10 @@ def main() -> str | None:
|
|
661
725
|
|
662
726
|
distribution = args.distribution or distro_default
|
663
727
|
if not distribution:
|
664
|
-
sys.exit(
|
665
|
-
|
728
|
+
sys.exit(
|
729
|
+
'Unknown system + machine distribution. Please specify '
|
730
|
+
'using -D/--distribution option.'
|
731
|
+
)
|
666
732
|
|
667
733
|
# Keep some useful info in the namespace passed to the command
|
668
734
|
prefix_dir = Path(args.prefix_dir).expanduser().resolve()
|
@@ -685,23 +751,35 @@ def main() -> str | None:
|
|
685
751
|
update_version_symlinks(args)
|
686
752
|
return result
|
687
753
|
|
688
|
-
|
689
|
-
|
754
|
+
|
755
|
+
# COMMAND
|
756
|
+
class install_:
|
690
757
|
doc = f'Install one or more versions from a {REPO} release.'
|
691
758
|
|
692
759
|
@staticmethod
|
693
760
|
def init(parser: ArgumentParser) -> None:
|
694
|
-
parser.add_argument(
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
761
|
+
parser.add_argument(
|
762
|
+
'-r',
|
763
|
+
'--release',
|
764
|
+
help=f'install from specified {REPO} '
|
765
|
+
f'YYYYMMDD release (e.g. {SAMPL_RELEASE}), '
|
766
|
+
'default is latest release',
|
767
|
+
)
|
768
|
+
parser.add_argument(
|
769
|
+
'-f',
|
770
|
+
'--force',
|
771
|
+
action='store_true',
|
772
|
+
help='force install even if already installed',
|
773
|
+
)
|
774
|
+
parser.add_argument(
|
775
|
+
'-s',
|
776
|
+
'--include-source',
|
777
|
+
action='store_true',
|
778
|
+
help='also install source files if available in distribution download',
|
779
|
+
)
|
780
|
+
parser.add_argument(
|
781
|
+
'version', nargs='+', help='version to install. E.g. 3.12 or 3.12.3'
|
782
|
+
)
|
705
783
|
|
706
784
|
@staticmethod
|
707
785
|
def run(args: Namespace) -> str | None:
|
@@ -727,27 +805,40 @@ class _install(COMMAND):
|
|
727
805
|
|
728
806
|
print(f'Version {fmt(version, release)} installed.')
|
729
807
|
|
730
|
-
|
731
|
-
|
732
|
-
|
808
|
+
|
809
|
+
# COMMAND
|
810
|
+
class update_:
|
811
|
+
"Update one, more, or all versions to another release."
|
812
|
+
|
733
813
|
aliases = ['upgrade']
|
734
814
|
|
735
815
|
@staticmethod
|
736
816
|
def init(parser: ArgumentParser) -> None:
|
737
|
-
parser.add_argument(
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
817
|
+
parser.add_argument(
|
818
|
+
'-r',
|
819
|
+
'--release',
|
820
|
+
help='update to specified YYYMMDD release (e.g. '
|
821
|
+
f'{SAMPL_RELEASE}), default is latest release',
|
822
|
+
)
|
823
|
+
parser.add_argument(
|
824
|
+
'-a', '--all', action='store_true', help='update ALL versions'
|
825
|
+
)
|
826
|
+
parser.add_argument(
|
827
|
+
'--skip',
|
828
|
+
action='store_true',
|
829
|
+
help='skip the specified versions when '
|
830
|
+
'updating all (only can be specified with --all)',
|
831
|
+
)
|
832
|
+
parser.add_argument(
|
833
|
+
'-k',
|
834
|
+
'--keep',
|
835
|
+
action='store_true',
|
836
|
+
help='keep old version after updating (but only '
|
837
|
+
'if different version number)',
|
838
|
+
)
|
839
|
+
parser.add_argument(
|
840
|
+
'version', nargs='*', help='version to update (or to skip for --all --skip)'
|
841
|
+
)
|
751
842
|
|
752
843
|
@staticmethod
|
753
844
|
def run(args: Namespace) -> str | None:
|
@@ -773,54 +864,66 @@ class _update(COMMAND):
|
|
773
864
|
continue
|
774
865
|
|
775
866
|
if nextver == version and args.keep:
|
776
|
-
print(
|
777
|
-
|
778
|
-
|
867
|
+
print(
|
868
|
+
f'Error: {fmt(version, release)} would not be kept '
|
869
|
+
f'if update to {fmt(nextver, release_target)} '
|
870
|
+
f'distribution="{distribution}"',
|
871
|
+
file=sys.stderr,
|
872
|
+
)
|
779
873
|
continue
|
780
874
|
|
781
875
|
new_vdir = args._versions / nextver
|
782
876
|
if nextver != version and new_vdir.exists():
|
783
877
|
continue
|
784
878
|
|
785
|
-
print(
|
786
|
-
|
787
|
-
|
879
|
+
print(
|
880
|
+
f'{fmt(version, release)} updating to '
|
881
|
+
f'{fmt(nextver, release_target)} '
|
882
|
+
f'distribution="{distribution}" ..'
|
883
|
+
)
|
788
884
|
|
789
885
|
# If the source was originally included, then include it in
|
790
886
|
# the update.
|
791
887
|
args.include_source = (vdir / 'src').is_dir()
|
792
888
|
|
793
|
-
if error := install(args, new_vdir, release_target, distribution,
|
794
|
-
files):
|
889
|
+
if error := install(args, new_vdir, release_target, distribution, files):
|
795
890
|
return error
|
796
891
|
|
797
892
|
if nextver != version and not args.keep:
|
798
893
|
remove(args, version)
|
799
894
|
|
800
|
-
|
801
|
-
|
802
|
-
|
895
|
+
|
896
|
+
# COMMAND
|
897
|
+
class remove_:
|
898
|
+
"Remove/uninstall one, more, or all versions."
|
899
|
+
|
803
900
|
aliases = ['uninstall']
|
804
901
|
|
805
902
|
@staticmethod
|
806
903
|
def init(parser: ArgumentParser) -> None:
|
807
|
-
parser.add_argument(
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
904
|
+
parser.add_argument(
|
905
|
+
'-a', '--all', action='store_true', help='remove ALL versions'
|
906
|
+
)
|
907
|
+
parser.add_argument(
|
908
|
+
'--skip',
|
909
|
+
action='store_true',
|
910
|
+
help='skip the specified versions when '
|
911
|
+
'removing all (only can be specified with --all)',
|
912
|
+
)
|
913
|
+
parser.add_argument(
|
914
|
+
'-r',
|
915
|
+
'--release',
|
916
|
+
help='only remove versions if from specified '
|
917
|
+
f'YYYMMDD release (e.g. {SAMPL_RELEASE})',
|
918
|
+
)
|
919
|
+
parser.add_argument(
|
920
|
+
'version', nargs='*', help='version to remove (or to skip for --all --skip)'
|
921
|
+
)
|
818
922
|
|
819
923
|
@staticmethod
|
820
924
|
def run(args: Namespace) -> str | None:
|
821
925
|
release_del = args.release
|
822
|
-
if release_del and
|
823
|
-
(err := check_release_tag(release_del)):
|
926
|
+
if release_del and (err := check_release_tag(release_del)):
|
824
927
|
return err
|
825
928
|
|
826
929
|
for version in get_version_names(args):
|
@@ -830,20 +933,29 @@ class _remove(COMMAND):
|
|
830
933
|
remove(args, version)
|
831
934
|
print(f'Version {fmt(version, release)} removed.')
|
832
935
|
|
833
|
-
|
834
|
-
|
835
|
-
|
936
|
+
|
937
|
+
# COMMAND
|
938
|
+
class list_:
|
939
|
+
"List installed versions and show which have an update available."
|
940
|
+
|
836
941
|
@staticmethod
|
837
942
|
def init(parser: ArgumentParser) -> None:
|
838
|
-
parser.add_argument(
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
943
|
+
parser.add_argument(
|
944
|
+
'-v',
|
945
|
+
'--verbose',
|
946
|
+
action='store_true',
|
947
|
+
help='explicitly report why a version is not eligible for update',
|
948
|
+
)
|
949
|
+
parser.add_argument(
|
950
|
+
'-r',
|
951
|
+
'--release',
|
952
|
+
help='use specified YYYYMMDD release '
|
953
|
+
f'(e.g. {SAMPL_RELEASE}) for verbose compare, '
|
954
|
+
'default is latest release',
|
955
|
+
)
|
956
|
+
parser.add_argument(
|
957
|
+
'version', nargs='*', help='only list specified version, else all'
|
958
|
+
)
|
847
959
|
|
848
960
|
@staticmethod
|
849
961
|
def run(args: Namespace) -> str | None:
|
@@ -868,56 +980,72 @@ class _list(COMMAND):
|
|
868
980
|
nextver = matcher.match(version, upgrade=True)
|
869
981
|
if not nextver:
|
870
982
|
if args.verbose:
|
871
|
-
app =
|
872
|
-
|
873
|
-
|
983
|
+
app = (
|
984
|
+
' not eligible for update because '
|
985
|
+
f'release {release_target} does not provide '
|
986
|
+
'this version.'
|
987
|
+
)
|
874
988
|
else:
|
875
989
|
new_vdir = args._versions / nextver
|
876
990
|
if nextver != version and new_vdir.exists():
|
877
991
|
if args.verbose:
|
878
|
-
nrelease = get_json(
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
992
|
+
nrelease = get_json(new_vdir / args._data).get(
|
993
|
+
'release', '?'
|
994
|
+
)
|
995
|
+
app = (
|
996
|
+
f' not eligible for '
|
997
|
+
f'update because {fmt(nextver, nrelease)} '
|
998
|
+
'is already installed.'
|
999
|
+
)
|
883
1000
|
else:
|
884
1001
|
# May not be updatable if newer release does not support
|
885
1002
|
# this same distribution anymore
|
886
1003
|
if nextver and distribution in files.get(nextver, {}):
|
887
|
-
upd = ' updatable to '
|
888
|
-
f'{fmt(nextver, release_target)}'
|
1004
|
+
upd = f' updatable to {fmt(nextver, release_target)}'
|
889
1005
|
elif args.verbose:
|
890
|
-
app =
|
891
|
-
|
892
|
-
|
893
|
-
|
1006
|
+
app = (
|
1007
|
+
' not eligible for update because '
|
1008
|
+
f'{fmt(nextver, release_target)} does '
|
1009
|
+
'not provide '
|
1010
|
+
f'distribution="{distribution}".'
|
1011
|
+
)
|
1012
|
+
|
1013
|
+
print(f'{fmt(version, release)}{upd} distribution="{distribution}"{app}')
|
894
1014
|
|
895
|
-
print(f'{fmt(version, release)}{upd} '
|
896
|
-
f'distribution="{distribution}"{app}')
|
897
1015
|
|
898
|
-
|
899
|
-
class
|
900
|
-
doc = f
|
1016
|
+
# COMMAND
|
1017
|
+
class show_:
|
1018
|
+
doc = f"""
|
901
1019
|
Show versions available from a release.
|
902
1020
|
|
903
1021
|
View available releases and their distributions at
|
904
1022
|
{GITHUB_SITE}/releases.
|
905
|
-
|
1023
|
+
"""
|
906
1024
|
|
907
1025
|
@staticmethod
|
908
1026
|
def init(parser: ArgumentParser) -> None:
|
909
1027
|
group = parser.add_mutually_exclusive_group()
|
910
|
-
group.add_argument(
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
1028
|
+
group.add_argument(
|
1029
|
+
'-l', '--list', action='store_true', help='just list recent releases'
|
1030
|
+
)
|
1031
|
+
group.add_argument(
|
1032
|
+
'-r',
|
1033
|
+
'--release',
|
1034
|
+
help=f'{REPO} YYYYMMDD release to show (e.g. '
|
1035
|
+
f'{SAMPL_RELEASE}), default is latest release',
|
1036
|
+
)
|
1037
|
+
parser.add_argument(
|
1038
|
+
'-a',
|
1039
|
+
'--all',
|
1040
|
+
action='store_true',
|
1041
|
+
help='show all available distributions for each version from the release',
|
1042
|
+
)
|
1043
|
+
parser.add_argument(
|
1044
|
+
're_match',
|
1045
|
+
nargs='?',
|
1046
|
+
help='show only versions+distributions '
|
1047
|
+
'matching this regular expression pattern',
|
1048
|
+
)
|
921
1049
|
|
922
1050
|
@staticmethod
|
923
1051
|
def run(args: Namespace) -> str | None:
|
@@ -925,6 +1053,7 @@ class _show(COMMAND):
|
|
925
1053
|
args.parser.error('Can not specify --all with --list.')
|
926
1054
|
|
927
1055
|
if args.list:
|
1056
|
+
args.release = False
|
928
1057
|
show_list(args)
|
929
1058
|
return None
|
930
1059
|
|
@@ -936,44 +1065,56 @@ class _show(COMMAND):
|
|
936
1065
|
installed = {}
|
937
1066
|
for vdir in iter_versions(args):
|
938
1067
|
data = get_json(vdir / args._data)
|
939
|
-
if data.get('release') == release and
|
940
|
-
(distro := data.get('distribution')):
|
1068
|
+
if data.get('release') == release and (distro := data.get('distribution')):
|
941
1069
|
installed[vdir.name] = distro
|
942
1070
|
|
943
1071
|
installable = False
|
944
1072
|
for version in sorted(files, key=parse_version):
|
945
1073
|
installed_distribution = installed.get(version)
|
946
1074
|
for distribution in files[version]:
|
947
|
-
app = ' (installed)'
|
948
|
-
|
949
|
-
if args.all or app \
|
950
|
-
or distribution == args._distribution:
|
1075
|
+
app = ' (installed)' if distribution == installed_distribution else ''
|
1076
|
+
if args.all or app or distribution == args._distribution:
|
951
1077
|
if distribution == args._distribution:
|
952
1078
|
installable = True
|
953
1079
|
|
954
|
-
if not args.re_match or
|
955
|
-
|
956
|
-
|
957
|
-
print(
|
958
|
-
f'
|
1080
|
+
if not args.re_match or re.search(
|
1081
|
+
args.re_match, f'{version}+{distribution}'
|
1082
|
+
):
|
1083
|
+
print(
|
1084
|
+
f'{fmt(version, release)} '
|
1085
|
+
f'distribution="{distribution}"{app}'
|
1086
|
+
)
|
959
1087
|
if not installable:
|
960
|
-
print(
|
961
|
-
|
1088
|
+
print(
|
1089
|
+
f'Warning: no distribution="{args._distribution}" '
|
1090
|
+
'versions found in '
|
1091
|
+
f'release "{release}".'
|
1092
|
+
)
|
1093
|
+
|
1094
|
+
|
1095
|
+
# COMMAND
|
1096
|
+
class path_:
|
1097
|
+
"Show path prefix to installed version base directory."
|
962
1098
|
|
963
|
-
@COMMAND.add
|
964
|
-
class _path(COMMAND):
|
965
|
-
'Show path prefix to installed version base directory.'
|
966
1099
|
@staticmethod
|
967
1100
|
def init(parser: ArgumentParser) -> None:
|
968
|
-
parser.add_argument(
|
969
|
-
|
970
|
-
|
971
|
-
|
1101
|
+
parser.add_argument(
|
1102
|
+
'-p',
|
1103
|
+
'--python-path',
|
1104
|
+
action='store_true',
|
1105
|
+
help='add path to python executable',
|
1106
|
+
)
|
1107
|
+
parser.add_argument(
|
1108
|
+
'-r', '--resolve', action='store_true', help='fully resolve given version'
|
1109
|
+
)
|
972
1110
|
group = parser.add_mutually_exclusive_group()
|
973
|
-
group.add_argument(
|
974
|
-
|
975
|
-
|
976
|
-
|
1111
|
+
group.add_argument(
|
1112
|
+
'-c',
|
1113
|
+
'--cache-path',
|
1114
|
+
action='store_true',
|
1115
|
+
help='just show path to cache dir',
|
1116
|
+
)
|
1117
|
+
group.add_argument('version', nargs='?', help='version number to show path for')
|
977
1118
|
|
978
1119
|
@staticmethod
|
979
1120
|
def run(args: Namespace) -> str | None:
|
@@ -997,10 +1138,10 @@ class _path(COMMAND):
|
|
997
1138
|
if not path.exists():
|
998
1139
|
path = basepath / 'python.exe'
|
999
1140
|
if not path.exists():
|
1000
|
-
return 'Error: Can not find python executable in '
|
1001
|
-
f'"{basepath}"'
|
1141
|
+
return f'Error: Can not find python executable in "{basepath}"'
|
1002
1142
|
|
1003
1143
|
print(path)
|
1004
1144
|
|
1145
|
+
|
1005
1146
|
if __name__ == '__main__':
|
1006
1147
|
sys.exit(main())
|
pystand-2.12.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=l-ecHjOFK35q5EiZY-FMufu_C_jz7wUom2EvVtSTAiw,36826
|
2
|
-
pystand-2.12.dist-info/METADATA,sha256=F1TMDKy9Invr4mCU08z1-yWEpL0SFHJQFBRnAak-7yU,24843
|
3
|
-
pystand-2.12.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
4
|
-
pystand-2.12.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-2.12.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-2.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|