pystand 1.0__py3-none-any.whl → 1.2__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-1.0.dist-info → pystand-1.2.dist-info}/METADATA +16 -6
- pystand-1.2.dist-info/RECORD +6 -0
- pystand.py +74 -14
- pystand-1.0.dist-info/RECORD +0 -6
- {pystand-1.0.dist-info → pystand-1.2.dist-info}/WHEEL +0 -0
- {pystand-1.0.dist-info → pystand-1.2.dist-info}/entry_points.txt +0 -0
- {pystand-1.0.dist-info → pystand-1.2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pystand
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2
|
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
|
@@ -18,8 +18,8 @@ Requires-Dist: pygithub
|
|
18
18
|
[](https://pypi.org/project/pystand/)
|
19
19
|
[](https://aur.archlinux.org/packages/pystand/)
|
20
20
|
|
21
|
-
[`pystand`][pystand] is a command line tool to facilitate the
|
22
|
-
installation and update of pre-built Python versions from the
|
21
|
+
[`pystand`][pystand] is a command line tool to facilitate the download,
|
22
|
+
installation, and update of pre-built Python versions from the
|
23
23
|
[`python-build-standalone`][pbs] project. The following commands are
|
24
24
|
provided:
|
25
25
|
|
@@ -120,8 +120,9 @@ usage: pystand [-h] [-D DISTRIBUTION] [-B BASE_DIR] [-C CACHE_MINUTES]
|
|
120
120
|
[--purge-days PURGE_DAYS] [-V]
|
121
121
|
{install,update,remove,list,show,path} ...
|
122
122
|
|
123
|
-
Command line tool to install pre-built Python versions
|
124
|
-
standalone project
|
123
|
+
Command line tool to download, install, and update pre-built Python versions
|
124
|
+
from the python-build-standalone project at
|
125
|
+
https://github.com/indygreg/python-build-standalone.
|
125
126
|
|
126
127
|
options:
|
127
128
|
-h, --help show this help message and exit
|
@@ -181,7 +182,7 @@ options:
|
|
181
182
|
### Command `update`
|
182
183
|
|
183
184
|
```
|
184
|
-
usage: pystand update [-h] [-r RELEASE] [-a] [--skip] [version ...]
|
185
|
+
usage: pystand update [-h] [-r RELEASE] [-a] [--skip] [-k] [version ...]
|
185
186
|
|
186
187
|
Update one, more, or all versions to another release.
|
187
188
|
|
@@ -196,6 +197,8 @@ options:
|
|
196
197
|
-a, --all update ALL versions
|
197
198
|
--skip skip the specified versions when updating all (only
|
198
199
|
can be specified with --all)
|
200
|
+
-k, --keep keep old version after updating (but only if different
|
201
|
+
version number)
|
199
202
|
```
|
200
203
|
|
201
204
|
### Command `remove`
|
@@ -313,6 +316,13 @@ Note, consistent with this, you actually don't need to specify a
|
|
313
316
|
minor version, e.g. `pystand install 3` would also install `3.12.3`
|
314
317
|
(assuming `3.12.3` is the latest available version for Python 3).
|
315
318
|
|
319
|
+
After installs or updates or removals,`pystand` also maintains symbolic
|
320
|
+
links to each latest installed version in it's version directory, e.g. a
|
321
|
+
symlink `~/.local/share/pystand/versions/3.12` will be created pointing
|
322
|
+
to `~/.local/share/pystand/versions/3.12.3` so that you can optionally
|
323
|
+
hard code the symlink directory in places where it can not be set
|
324
|
+
dynamically (i.e. where using `pystand path` is not an option).
|
325
|
+
|
316
326
|
## Command Default Options
|
317
327
|
|
318
328
|
You can add default global options to a personal configuration file
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=5X--S1zNpaEauP4FkubSe9J5FM3qQ1OO92ZcPlk__4I,26248
|
2
|
+
pystand-1.2.dist-info/METADATA,sha256=V72dn_zsBb0W0ym4lYh3EljiaOqY0s2l9fuszbK_EFg,14304
|
3
|
+
pystand-1.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
4
|
+
pystand-1.2.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-1.2.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-1.2.dist-info/RECORD,,
|
pystand.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
#!/usr/bin/python3
|
2
2
|
# PYTHON_ARGCOMPLETE_OK
|
3
3
|
'''
|
4
|
-
Command line tool to install pre-built Python
|
5
|
-
python-build-standalone project
|
4
|
+
Command line tool to download, install, and update pre-built Python
|
5
|
+
versions from the python-build-standalone project at
|
6
|
+
https://github.com/indygreg/python-build-standalone.
|
6
7
|
'''
|
7
8
|
from __future__ import annotations
|
8
9
|
|
@@ -18,11 +19,11 @@ import urllib.request
|
|
18
19
|
from argparse import ArgumentParser, Namespace
|
19
20
|
from collections import defaultdict
|
20
21
|
from pathlib import Path
|
21
|
-
from typing import Iterable, Iterator, Optional
|
22
|
+
from typing import Any, Iterable, Iterator, Optional
|
22
23
|
|
23
24
|
import argcomplete
|
24
25
|
import platformdirs
|
25
|
-
from packaging.version import
|
26
|
+
from packaging.version import parse as parse_version
|
26
27
|
|
27
28
|
REPO_OWNER = 'indygreg'
|
28
29
|
REPO = 'python-build-standalone'
|
@@ -90,7 +91,7 @@ def set_json(file: Path, data: dict) -> Optional[str]:
|
|
90
91
|
# The gh handle is an opaque github instance handle
|
91
92
|
get_gh_handle = None
|
92
93
|
|
93
|
-
def get_gh(args: Namespace):
|
94
|
+
def get_gh(args: Namespace) -> Any:
|
94
95
|
'Return a GitHub handle'
|
95
96
|
# The gh handle is a global to lazily create it only if/when needed
|
96
97
|
global get_gh_handle
|
@@ -113,7 +114,7 @@ def rm_path(path: Path) -> None:
|
|
113
114
|
class VersionMatcher:
|
114
115
|
'Match a version string to a list of versions'
|
115
116
|
def __init__(self, seq: Iterable[str]) -> None:
|
116
|
-
self.seq = sorted(seq, key=
|
117
|
+
self.seq = sorted(seq, key=parse_version, reverse=True)
|
117
118
|
|
118
119
|
def match(self, version: str, *,
|
119
120
|
upconvert_minor: bool = False) -> Optional[str]:
|
@@ -165,7 +166,8 @@ def get_version_names(args: Namespace) -> list[str]:
|
|
165
166
|
unknowns = [f'"{u}"' for u in unknown]
|
166
167
|
sys.exit(f'Error: version{s} {", ".join(unknowns)} not found.')
|
167
168
|
|
168
|
-
return sorted(all_names - given, key=
|
169
|
+
return sorted(all_names - given, key=parse_version) \
|
170
|
+
if args.all else versions
|
169
171
|
|
170
172
|
def get_latest_release_tag(args: Namespace) -> str:
|
171
173
|
'Return the latest release tag'
|
@@ -191,11 +193,16 @@ def get_latest_release_tag(args: Namespace) -> str:
|
|
191
193
|
args._latest_release.write_text(tag + '\n')
|
192
194
|
return tag
|
193
195
|
|
194
|
-
def get_release_files(args, tag, implementation: str = None) -> dict:
|
196
|
+
def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
|
195
197
|
'Return the release files for the given tag'
|
196
198
|
# Look for tag data in our release cache
|
197
199
|
jfile = args._releases / tag
|
198
200
|
if not (files := get_json(jfile)):
|
201
|
+
|
202
|
+
# May have read this release before but it has no assets
|
203
|
+
if jfile.exists():
|
204
|
+
return {}
|
205
|
+
|
199
206
|
# Not in cache so fetch it (and also store in cache)
|
200
207
|
gh = get_gh(args)
|
201
208
|
try:
|
@@ -226,6 +233,48 @@ def get_release_files(args, tag, implementation: str = None) -> dict:
|
|
226
233
|
|
227
234
|
return files.get(implementation, {}) if implementation else files
|
228
235
|
|
236
|
+
def update_version_symlinks(args: Namespace) -> None:
|
237
|
+
'Create/update symlinks pointing to latest version'
|
238
|
+
base = args._versions
|
239
|
+
if not base.exists():
|
240
|
+
return
|
241
|
+
|
242
|
+
# Record of all the existing symlinks and version dirs
|
243
|
+
oldlinks = {}
|
244
|
+
vers = []
|
245
|
+
for path in base.iterdir():
|
246
|
+
if not path.name.startswith('.'):
|
247
|
+
if path.is_symlink():
|
248
|
+
oldlinks[path.name] = os.readlink(str(path))
|
249
|
+
else:
|
250
|
+
vers.append(path)
|
251
|
+
|
252
|
+
# Create a map of all the new major version links
|
253
|
+
newlinks_all = defaultdict(list)
|
254
|
+
for path in vers:
|
255
|
+
namevers = path.name
|
256
|
+
while '.' in namevers[:-1]:
|
257
|
+
namevers_major = namevers.rsplit('.', maxsplit=1)[0]
|
258
|
+
newlinks_all[namevers_major].append(namevers)
|
259
|
+
namevers = namevers_major
|
260
|
+
|
261
|
+
newlinks = {k: sorted(v, key=parse_version)[-1] for k, v in
|
262
|
+
newlinks_all.items()}
|
263
|
+
|
264
|
+
# Remove all old or invalid existing links
|
265
|
+
for name, tgt in oldlinks.items():
|
266
|
+
new_tgt = newlinks.get(name)
|
267
|
+
if not new_tgt or new_tgt != tgt:
|
268
|
+
path = Path(base / name)
|
269
|
+
path.unlink()
|
270
|
+
|
271
|
+
# Create all needed new links
|
272
|
+
for name, tgt in newlinks.items():
|
273
|
+
old_tgt = oldlinks.get(name)
|
274
|
+
if not old_tgt or old_tgt != tgt:
|
275
|
+
path = Path(base / name)
|
276
|
+
path.symlink_to(tgt, target_is_directory=True)
|
277
|
+
|
229
278
|
def purge_unused_releases(args: Namespace) -> None:
|
230
279
|
'Purge old releases that are no longer needed and have expired'
|
231
280
|
releases = set(f.name for f in args._releases.iterdir())
|
@@ -405,6 +454,7 @@ def main() -> Optional[str]:
|
|
405
454
|
args._releases.mkdir(parents=True, exist_ok=True)
|
406
455
|
|
407
456
|
result = args.func(args)
|
457
|
+
update_version_symlinks(args)
|
408
458
|
purge_unused_releases(args)
|
409
459
|
return result
|
410
460
|
|
@@ -428,7 +478,7 @@ class _install(COMMAND):
|
|
428
478
|
release = args.release or get_latest_release_tag(args)
|
429
479
|
files = get_release_files(args, release, 'cpython')
|
430
480
|
if not files:
|
431
|
-
return f'Release "{release}" not found.'
|
481
|
+
return f'Release "{release}" not found, or has no compatible files.'
|
432
482
|
|
433
483
|
matcher = VersionMatcher(files)
|
434
484
|
for version in args.version:
|
@@ -460,6 +510,9 @@ class _update(COMMAND):
|
|
460
510
|
parser.add_argument('--skip', action='store_true',
|
461
511
|
help='skip the specified versions when '
|
462
512
|
'updating all (only can be specified with --all)')
|
513
|
+
parser.add_argument('-k', '--keep', action='store_true',
|
514
|
+
help='keep old version after updating (but only '
|
515
|
+
'if different version number)')
|
463
516
|
parser.add_argument('version', nargs='*',
|
464
517
|
help='version to update (or to skip for '
|
465
518
|
'--all --skip)')
|
@@ -481,14 +534,21 @@ class _update(COMMAND):
|
|
481
534
|
continue
|
482
535
|
|
483
536
|
nextver = matcher.match(version, upconvert_minor=True)
|
484
|
-
new_vdir = args._versions / nextver
|
485
|
-
if nextver != version and new_vdir.exists():
|
486
|
-
continue
|
487
537
|
|
488
538
|
distribution = data.get('distribution')
|
489
539
|
if not distribution or distribution not in files.get(nextver, {}):
|
490
540
|
continue
|
491
541
|
|
542
|
+
if nextver == version and args.keep:
|
543
|
+
print(f'Error: {fmt(version, release)} would not be kept '
|
544
|
+
f'if update to {fmt(nextver, release_target)} '
|
545
|
+
f'distribution="{distribution}"', file=sys.stderr)
|
546
|
+
continue
|
547
|
+
|
548
|
+
new_vdir = args._versions / nextver
|
549
|
+
if nextver != version and new_vdir.exists():
|
550
|
+
continue
|
551
|
+
|
492
552
|
print(f'{fmt(version, release)} updating to '
|
493
553
|
f'{fmt(nextver, release_target)} '
|
494
554
|
f'distribution="{distribution}" ..')
|
@@ -497,7 +557,7 @@ class _update(COMMAND):
|
|
497
557
|
files):
|
498
558
|
return error
|
499
559
|
|
500
|
-
if nextver != version:
|
560
|
+
if nextver != version and not args.keep:
|
501
561
|
remove(args, version)
|
502
562
|
|
503
563
|
@COMMAND.add
|
@@ -609,7 +669,7 @@ class _show(COMMAND):
|
|
609
669
|
installed[vdir.name] = distro
|
610
670
|
|
611
671
|
installable = False
|
612
|
-
for version in sorted(files, key=
|
672
|
+
for version in sorted(files, key=parse_version):
|
613
673
|
installed_distribution = installed.get(version)
|
614
674
|
for distribution in files[version]:
|
615
675
|
app = ' (installed)' \
|
pystand-1.0.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=fWBjn-KMOGv-oR3Q2XYfaKE5ZtHUDXRq2Kx__mnd6Es,23997
|
2
|
-
pystand-1.0.dist-info/METADATA,sha256=_GdO43TfIj8kSEHfU2suk-xq0Dh07rKsa29Z6vIfnhA,13674
|
3
|
-
pystand-1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
4
|
-
pystand-1.0.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-1.0.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|