pystand 1.3__py3-none-any.whl → 1.5__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.3.dist-info → pystand-1.5.dist-info}/METADATA +29 -24
- pystand-1.5.dist-info/RECORD +6 -0
- {pystand-1.3.dist-info → pystand-1.5.dist-info}/WHEEL +1 -1
- pystand.py +75 -29
- pystand-1.3.dist-info/RECORD +0 -6
- {pystand-1.3.dist-info → pystand-1.5.dist-info}/entry_points.txt +0 -0
- {pystand-1.3.dist-info → pystand-1.5.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.5
|
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
|
@@ -33,7 +33,7 @@ provided:
|
|
33
33
|
|`path` |Show path prefix to installed version base directory |
|
34
34
|
|
35
35
|
By default, Python versions are sourced from the latest
|
36
|
-
`python-build-standalone` [release][pbs-rel] available but you can
|
36
|
+
`python-build-standalone` [release][pbs-rel] available (e.g. "`20240415`") but you can
|
37
37
|
optionally specify any older release. The required
|
38
38
|
[distribution](https://gregoryszorc.com/docs/python-build-standalone/main/running.html)
|
39
39
|
for your machine architecture is normally auto-detected but can be
|
@@ -99,8 +99,8 @@ $ pipx install --python $(pystand path -p 3.12) cowsay
|
|
99
99
|
See detailed usage information in the [Usage](#usage) section that
|
100
100
|
follows.
|
101
101
|
|
102
|
-
Note that unlike nearly all similar tools such as [`pyenv`][pyenv],
|
103
|
-
python`][pdmpy], and [`hatch python`][hatchpy], `pystand` directly
|
102
|
+
Note that unlike nearly all similar tools such as [`pyenv`][pyenv],
|
103
|
+
[`pdm python`][pdmpy], and [`hatch python`][hatchpy], `pystand` directly
|
104
104
|
checks the [`python-build-standalone`][pbs] github site to fetch for new
|
105
105
|
[releases][pbs-rel] but those other tools require a software update
|
106
106
|
before they can see new releases. This means that Python updates are
|
@@ -118,7 +118,8 @@ Type `pystand` or `pystand -h` to view the usage summary:
|
|
118
118
|
```
|
119
119
|
usage: pystand [-h] [-D DISTRIBUTION] [-B BASE_DIR] [-C CACHE_MINUTES]
|
120
120
|
[--purge-days PURGE_DAYS]
|
121
|
-
[--github-access-token GITHUB_ACCESS_TOKEN]
|
121
|
+
[--github-access-token GITHUB_ACCESS_TOKEN]
|
122
|
+
[--do-not-strip DO_NOT_STRIP] [-V]
|
122
123
|
{install,update,remove,list,show,path} ...
|
123
124
|
|
124
125
|
Command line tool to download, install, and update pre-built Python versions
|
@@ -136,15 +137,18 @@ options:
|
|
136
137
|
specify pystand base dir for storing versions and
|
137
138
|
metadata. Default is "$HOME/.local/share/pystand"
|
138
139
|
-C CACHE_MINUTES, --cache-minutes CACHE_MINUTES
|
139
|
-
cache latest release tag fetch for this many
|
140
|
-
before rechecking for latest. Default is 60
|
140
|
+
cache latest YYYYMMDD release tag fetch for this many
|
141
|
+
minutes, before rechecking for latest. Default is 60
|
142
|
+
minutes
|
141
143
|
--purge-days PURGE_DAYS
|
142
|
-
cache release file lists for this number of
|
143
|
-
last version referencing it is removed.
|
144
|
-
days
|
144
|
+
cache YYYYMMDD release file lists for this number of
|
145
|
+
days after last version referencing it is removed.
|
146
|
+
Default is 90 days
|
145
147
|
--github-access-token GITHUB_ACCESS_TOKEN
|
146
|
-
|
148
|
+
optional Github access token. Can specify to reduce
|
147
149
|
rate limiting.
|
150
|
+
--do-not-strip DO_NOT_STRIP
|
151
|
+
Do not strip unneeded symbols from binaries
|
148
152
|
-V show pystand version
|
149
153
|
|
150
154
|
Commands:
|
@@ -178,8 +182,9 @@ positional arguments:
|
|
178
182
|
options:
|
179
183
|
-h, --help show this help message and exit
|
180
184
|
-r RELEASE, --release RELEASE
|
181
|
-
install from specified python-build-standalone
|
182
|
-
(e.g. 20240415), default is latest
|
185
|
+
install from specified python-build-standalone
|
186
|
+
YYYYMMDD release (e.g. 20240415), default is latest
|
187
|
+
release
|
183
188
|
-f, --force force install even if already installed
|
184
189
|
```
|
185
190
|
|
@@ -196,8 +201,8 @@ positional arguments:
|
|
196
201
|
options:
|
197
202
|
-h, --help show this help message and exit
|
198
203
|
-r RELEASE, --release RELEASE
|
199
|
-
update to specified release (e.g. 20240415),
|
200
|
-
is latest release
|
204
|
+
update to specified YYYMMDD release (e.g. 20240415),
|
205
|
+
default is latest release
|
201
206
|
-a, --all update ALL versions
|
202
207
|
--skip skip the specified versions when updating all (only
|
203
208
|
can be specified with --all)
|
@@ -221,8 +226,8 @@ options:
|
|
221
226
|
--skip skip the specified versions when removing all (only
|
222
227
|
can be specified with --all)
|
223
228
|
-r RELEASE, --release RELEASE
|
224
|
-
only remove versions if from specified release
|
225
|
-
20240415)
|
229
|
+
only remove versions if from specified YYYMMDD release
|
230
|
+
(e.g. 20240415)
|
226
231
|
```
|
227
232
|
|
228
233
|
### Command `list`
|
@@ -240,8 +245,8 @@ options:
|
|
240
245
|
-v, --verbose explicitly report why a version is not eligible for
|
241
246
|
update
|
242
247
|
-r RELEASE, --release RELEASE
|
243
|
-
use specified release (e.g. 20240415) for
|
244
|
-
compare, default is latest release
|
248
|
+
use specified YYYYMMDD release (e.g. 20240415) for
|
249
|
+
verbose compare, default is latest release
|
245
250
|
```
|
246
251
|
|
247
252
|
### Command `show`
|
@@ -252,7 +257,7 @@ usage: pystand show [-h] [-d] [release]
|
|
252
257
|
Show versions available from a release.
|
253
258
|
|
254
259
|
positional arguments:
|
255
|
-
release python-build-standalone release to show (e.g.
|
260
|
+
release python-build-standalone YYYYMMDD release to show (e.g.
|
256
261
|
20240415), default is latest release
|
257
262
|
|
258
263
|
options:
|
@@ -306,8 +311,8 @@ $ pipx uninstall pystand
|
|
306
311
|
`pystand` extrapolates any version text you specify on the command line
|
307
312
|
to the latest available corresponding installed or release version. For
|
308
313
|
example, if you specify `pystand install 3.12` then `pystand` will look
|
309
|
-
in the release files to find the latest (i.e. highest) available
|
310
|
-
|
314
|
+
in the release files to find the latest (i.e. highest) available version
|
315
|
+
of `3.12`, e.g. `3.12.3` (at the time of writing), and will install
|
311
316
|
that. Of course you can specify the exact version if you wish, e.g.
|
312
317
|
`3.12.3` but generally you don't need to bother. This is true for any
|
313
318
|
command that takes a version argument so be aware that this may be
|
@@ -337,8 +342,8 @@ anything after on a line) are ignored. Type `pystand` to see all
|
|
337
342
|
supported options.
|
338
343
|
|
339
344
|
The global options: `--distribution`, `--base-dir`, `--cache-minutes`,
|
340
|
-
`--purge-days`, `--github-access-token` are the only
|
341
|
-
to consider setting as defaults.
|
345
|
+
`--purge-days`, `--github-access-token`, `--do-not-strip` are the only
|
346
|
+
sensible candidates to consider setting as defaults.
|
342
347
|
|
343
348
|
## Command Line Tab Completion
|
344
349
|
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=L0SAV5snhVrNrhFWTb4xuLHp3Mr5eNYF6ke_AWdxtq0,29019
|
2
|
+
pystand-1.5.dist-info/METADATA,sha256=j9q9s0eDv4reAYspvONxUrFvduSaeef8tFx9IzEfVaQ,14843
|
3
|
+
pystand-1.5.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
|
4
|
+
pystand-1.5.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-1.5.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-1.5.dist-info/RECORD,,
|
pystand.py
CHANGED
@@ -13,11 +13,13 @@ import platform
|
|
13
13
|
import re
|
14
14
|
import shlex
|
15
15
|
import shutil
|
16
|
+
import subprocess
|
16
17
|
import sys
|
17
18
|
import time
|
18
19
|
import urllib.request
|
19
20
|
from argparse import ArgumentParser, Namespace
|
20
21
|
from collections import defaultdict
|
22
|
+
from datetime import date
|
21
23
|
from pathlib import Path
|
22
24
|
from typing import Any, Iterable, Iterator, Optional
|
23
25
|
|
@@ -35,6 +37,9 @@ LATEST_RELEASE_URL = f'https://raw.githubusercontent.com/{GITHUB_REPO}'\
|
|
35
37
|
# builds so this tool does not support any releases before this.
|
36
38
|
FIRST_RELEASE = '20210724'
|
37
39
|
|
40
|
+
# Sample release tag for documentation/usage examples
|
41
|
+
SAMPL_RELEASE = '20240415'
|
42
|
+
|
38
43
|
# The following regexp pattern is used to match the end of a release file name
|
39
44
|
ENDPAT = r'-install_only[-\dT]*.tar.gz$'
|
40
45
|
|
@@ -183,15 +188,27 @@ def get_version_names(args: Namespace) -> list[str]:
|
|
183
188
|
return sorted(all_names - given, key=parse_version) \
|
184
189
|
if args.all else versions
|
185
190
|
|
191
|
+
def check_release_tag(release: str, *,
|
192
|
+
check_first: bool = True) -> Optional[str]:
|
193
|
+
'Check the specified release tag is valid'
|
194
|
+
if not release.isdigit() or len(release) != len(FIRST_RELEASE):
|
195
|
+
return 'Release must be a YYYYMMDD string.'
|
196
|
+
|
197
|
+
try:
|
198
|
+
_ = date.fromisoformat(f'{release[:4]}-{release[4:6]}-{release[6:]}')
|
199
|
+
except Exception:
|
200
|
+
return 'Release must be a YYYYMMDD date string.'
|
201
|
+
|
202
|
+
if check_first and release < FIRST_RELEASE:
|
203
|
+
return f'Releases before "{FIRST_RELEASE}" do not include '\
|
204
|
+
'"*-install_only" builds so are not supported.'
|
205
|
+
return None
|
206
|
+
|
186
207
|
def get_release_tag(args: Namespace) -> str:
|
187
208
|
'Return the release tag, or latest if not specified'
|
188
209
|
if release := args.release:
|
189
|
-
if
|
190
|
-
sys.exit(
|
191
|
-
if release < FIRST_RELEASE:
|
192
|
-
sys.exit(f'Releases before "{FIRST_RELEASE}" do not '
|
193
|
-
'include "*-install_only" builds so are '
|
194
|
-
'not supported.')
|
210
|
+
if err := check_release_tag(release):
|
211
|
+
sys.exit(err)
|
195
212
|
|
196
213
|
return release
|
197
214
|
|
@@ -207,12 +224,12 @@ def get_release_tag(args: Namespace) -> str:
|
|
207
224
|
with urllib.request.urlopen(LATEST_RELEASE_URL) as url:
|
208
225
|
data = json.load(url)
|
209
226
|
except Exception:
|
210
|
-
sys.exit('Failed to fetch latest release tag.')
|
227
|
+
sys.exit('Failed to fetch latest YYYYMMDD release tag.')
|
211
228
|
|
212
229
|
tag = data.get('tag')
|
213
230
|
|
214
231
|
if not tag:
|
215
|
-
sys.exit('Latest release tag timestamp file is corrupted.')
|
232
|
+
sys.exit('Latest YYYYMMDD release tag timestamp file is corrupted.')
|
216
233
|
|
217
234
|
args._latest_release.write_text(tag + '\n')
|
218
235
|
return tag
|
@@ -292,15 +309,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
292
309
|
for name, tgt in oldlinks.items():
|
293
310
|
new_tgt = newlinks.get(name)
|
294
311
|
if not new_tgt or new_tgt != tgt:
|
295
|
-
|
296
|
-
path.unlink()
|
312
|
+
Path(base / name).unlink()
|
297
313
|
|
298
314
|
# Create all needed new links
|
299
315
|
for name, tgt in newlinks.items():
|
300
316
|
old_tgt = oldlinks.get(name)
|
301
317
|
if not old_tgt or old_tgt != tgt:
|
302
|
-
|
303
|
-
path.symlink_to(tgt, target_is_directory=True)
|
318
|
+
Path(base / name).symlink_to(tgt, target_is_directory=True)
|
304
319
|
|
305
320
|
def purge_unused_releases(args: Namespace) -> None:
|
306
321
|
'Purge old releases that are no longer needed and have expired'
|
@@ -313,10 +328,11 @@ def purge_unused_releases(args: Namespace) -> None:
|
|
313
328
|
if (release := get_json(version / args._data).get('release')):
|
314
329
|
keep.add(release)
|
315
330
|
|
316
|
-
|
331
|
+
now_secs = time.time()
|
332
|
+
end_secs = args.purge_days * 86400
|
333
|
+
for release in (releases - keep):
|
317
334
|
rdir = args._releases / release
|
318
|
-
|
319
|
-
if time.time() > (stat.st_mtime + args.purge_days * 86400):
|
335
|
+
if (rdir.stat().st_mtime + end_secs) < now_secs:
|
320
336
|
rdir.unlink()
|
321
337
|
|
322
338
|
class COMMAND:
|
@@ -352,6 +368,25 @@ def remove(args: Namespace, version: str) -> None:
|
|
352
368
|
|
353
369
|
shutil.rmtree(vdir)
|
354
370
|
|
371
|
+
def strip(vdir: Path, distribution: str) -> None:
|
372
|
+
'Strip binaries from files in a version directory'
|
373
|
+
# Only run the strip command on Linux hosts and for Linux distributions
|
374
|
+
if platform.system() != 'Linux' or '-linux-' not in distribution:
|
375
|
+
return
|
376
|
+
|
377
|
+
for path in ('bin', 'lib'):
|
378
|
+
base = vdir / path
|
379
|
+
if not base.is_dir():
|
380
|
+
continue
|
381
|
+
|
382
|
+
for file in base.iterdir():
|
383
|
+
if not file.is_symlink() and file.is_file():
|
384
|
+
cmd = f'strip -p --strip-unneeded {file}'.split()
|
385
|
+
try:
|
386
|
+
subprocess.run(cmd, stderr=subprocess.DEVNULL)
|
387
|
+
except Exception:
|
388
|
+
pass
|
389
|
+
|
355
390
|
def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
356
391
|
files: dict) -> Optional[str]:
|
357
392
|
'Install a version'
|
@@ -378,6 +413,9 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
378
413
|
if (error := set_json(tmpdir_py / args._data, data)):
|
379
414
|
error = f'Failed to write {version} data file: {error}'
|
380
415
|
|
416
|
+
if not args.do_not_strip:
|
417
|
+
strip(tmpdir_py, distribution)
|
418
|
+
|
381
419
|
if not error:
|
382
420
|
remove(args, version)
|
383
421
|
tmpdir_py.replace(vdir)
|
@@ -408,16 +446,18 @@ def main() -> Optional[str]:
|
|
408
446
|
help=f'specify {PROG} base dir for storing '
|
409
447
|
'versions and metadata. Default is "%(default)s"')
|
410
448
|
opt.add_argument('-C', '--cache-minutes', default=60, type=float,
|
411
|
-
help='cache latest release tag fetch for this
|
412
|
-
'minutes, before rechecking for latest. '
|
449
|
+
help='cache latest YYYYMMDD release tag fetch for this '
|
450
|
+
'many minutes, before rechecking for latest. '
|
413
451
|
'Default is %(default)d minutes')
|
414
452
|
opt.add_argument('--purge-days', default=90, type=int,
|
415
|
-
help='cache release file lists for this number '
|
453
|
+
help='cache YYYYMMDD release file lists for this number '
|
416
454
|
'of days after last version referencing it is removed. '
|
417
455
|
'Default is %(default)d days')
|
418
456
|
opt.add_argument('--github-access-token',
|
419
|
-
help='
|
457
|
+
help='optional Github access token. Can specify to reduce '
|
420
458
|
'rate limiting.')
|
459
|
+
opt.add_argument('--do-not-strip',
|
460
|
+
help='Do not strip unneeded symbols from binaries')
|
421
461
|
opt.add_argument('-V', action='store_true',
|
422
462
|
help=f'show {PROG} version')
|
423
463
|
cmd = opt.add_subparsers(title='Commands', dest='cmdname')
|
@@ -496,7 +536,7 @@ class _install(COMMAND):
|
|
496
536
|
def init(parser: ArgumentParser) -> None:
|
497
537
|
parser.add_argument('-r', '--release',
|
498
538
|
help=f'install from specified {REPO} '
|
499
|
-
'release (e.g.
|
539
|
+
f'YYYYMMDD release (e.g. {SAMPL_RELEASE}), '
|
500
540
|
'default is latest release')
|
501
541
|
parser.add_argument('-f', '--force', action='store_true',
|
502
542
|
help='force install even if already installed')
|
@@ -533,8 +573,8 @@ class _update(COMMAND):
|
|
533
573
|
@staticmethod
|
534
574
|
def init(parser: ArgumentParser) -> None:
|
535
575
|
parser.add_argument('-r', '--release',
|
536
|
-
help='update to specified release (e.g.
|
537
|
-
'default is latest release')
|
576
|
+
help='update to specified YYYMMDD release (e.g. '
|
577
|
+
f'{SAMPL_RELEASE}), default is latest release')
|
538
578
|
parser.add_argument('-a', '--all', action='store_true',
|
539
579
|
help='update ALL versions')
|
540
580
|
parser.add_argument('--skip', action='store_true',
|
@@ -601,18 +641,23 @@ class _remove(COMMAND):
|
|
601
641
|
help='skip the specified versions when '
|
602
642
|
'removing all (only can be specified with --all)')
|
603
643
|
parser.add_argument('-r', '--release',
|
604
|
-
help='only remove versions if from '
|
605
|
-
'
|
644
|
+
help='only remove versions if from specified '
|
645
|
+
f'YYYMMDD release (e.g. {SAMPL_RELEASE})')
|
606
646
|
parser.add_argument('version', nargs='*',
|
607
647
|
help='version to remove (or to skip for '
|
608
648
|
'--all --skip)')
|
609
649
|
|
610
650
|
@staticmethod
|
611
651
|
def run(args: Namespace) -> Optional[str]:
|
652
|
+
release_del = args.release
|
653
|
+
if release_del and \
|
654
|
+
(err := check_release_tag(release_del, check_first=False)):
|
655
|
+
return err
|
656
|
+
|
612
657
|
for version in get_version_names(args):
|
613
658
|
dfile = args._versions / version / args._data
|
614
659
|
release = get_json(dfile).get('release') or '?'
|
615
|
-
if not
|
660
|
+
if not release_del or release == release_del:
|
616
661
|
remove(args, version)
|
617
662
|
print(f'Version {fmt(version, release)} removed.')
|
618
663
|
|
@@ -625,8 +670,9 @@ class _list(COMMAND):
|
|
625
670
|
help='explicitly report why a version is '
|
626
671
|
'not eligible for update')
|
627
672
|
parser.add_argument('-r', '--release',
|
628
|
-
help='use specified release
|
629
|
-
'verbose compare,
|
673
|
+
help='use specified YYYYMMDD release '
|
674
|
+
f'(e.g. {SAMPL_RELEASE}) for verbose compare, '
|
675
|
+
'default is latest release')
|
630
676
|
parser.add_argument('version', nargs='*',
|
631
677
|
help='only list specified version, else all')
|
632
678
|
|
@@ -681,8 +727,8 @@ class _show(COMMAND):
|
|
681
727
|
help='also show all available distributions for '
|
682
728
|
'each version from the release')
|
683
729
|
parser.add_argument('release', nargs='?',
|
684
|
-
help=f'{REPO} release to show (e.g.
|
685
|
-
'default is latest release')
|
730
|
+
help=f'{REPO} YYYYMMDD release to show (e.g. '
|
731
|
+
f'{SAMPL_RELEASE}), default is latest release')
|
686
732
|
|
687
733
|
@staticmethod
|
688
734
|
def run(args: Namespace) -> None:
|
pystand-1.3.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=OI2SWREnCD8Hyu8Xk6z6FZqPDM6NfRtku_Djb6nJ7gM,27357
|
2
|
-
pystand-1.3.dist-info/METADATA,sha256=aXLnhaYuWDwnbWEbo5eZb2mzAOklpihUEuWuAzoaztk,14550
|
3
|
-
pystand-1.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
4
|
-
pystand-1.3.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-1.3.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|