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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pystand
3
- Version: 1.3
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], [`pdm
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] [-V]
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 minutes,
140
- before rechecking for latest. Default is 60 minutes
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 days after
143
- last version referencing it is removed. Default is 90
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
- Optional Github access token. Can specify to reduce
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 release
182
- (e.g. 20240415), default is latest release
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), default
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 (e.g.
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 verbose
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
- version of `3.12`, e.g. `3.12.3` (at the time of writing), and will install
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 sensible candidates
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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 not release.isdigit() or len(release) != len(FIRST_RELEASE):
190
- sys.exit('Release must be a YYYYMMDD string.')
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
- path = Path(base / name)
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
- path = Path(base / name)
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
- for release in releases - keep:
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
- stat = rdir.stat()
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 many '
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='Optional Github access token. Can specify to reduce '
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. 20240415), '
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. 20240415), '
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
- 'specified release (e.g. 20240415)')
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 args.release or release == args.release:
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 (e.g. 20240415) for '
629
- 'verbose compare, default is latest release')
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. 20240415), '
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:
@@ -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,,