pystand 1.5__py3-none-any.whl → 1.7__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.5
3
+ Version: 1.7
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
@@ -99,12 +99,15 @@ $ 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
- [`pdm python`][pdmpy], and [`hatch python`][hatchpy], `pystand` directly
104
- checks the [`python-build-standalone`][pbs] github site to fetch for new
105
- [releases][pbs-rel] but those other tools require a software update
106
- before they can see new releases. This means that Python updates are
107
- available more quickly from `pystand` than other tools.
102
+ Note that similar tools such as [`pdm python`][pdmpy], [`hatch
103
+ python`][hatchpy], and [`rye toolchain`][ryepy] also use
104
+ [`python-build-standalone`][pbs] build releases. However, `pystand` is
105
+ unique because it directly checks the [`python-build-standalone`][pbs]
106
+ github site for new [releases][pbs-rel]. Those other tools
107
+ require a software update before they can fetch and use new
108
+ [`python-build-standalone`][pbs] releases. This means that new Python
109
+ versions and updates are always available more quickly from `pystand`
110
+ than those other tools.
108
111
 
109
112
  This utility has been developed and tested on Linux but should also work
110
113
  on macOS and Windows although has not been tried on those platforms. The
@@ -118,8 +121,8 @@ Type `pystand` or `pystand -h` to view the usage summary:
118
121
  ```
119
122
  usage: pystand [-h] [-D DISTRIBUTION] [-B BASE_DIR] [-C CACHE_MINUTES]
120
123
  [--purge-days PURGE_DAYS]
121
- [--github-access-token GITHUB_ACCESS_TOKEN]
122
- [--do-not-strip DO_NOT_STRIP] [-V]
124
+ [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
125
+ [--no-extra-strip] [-V]
123
126
  {install,update,remove,list,show,path} ...
124
127
 
125
128
  Command line tool to download, install, and update pre-built Python versions
@@ -147,8 +150,8 @@ options:
147
150
  --github-access-token GITHUB_ACCESS_TOKEN
148
151
  optional Github access token. Can specify to reduce
149
152
  rate limiting.
150
- --do-not-strip DO_NOT_STRIP
151
- Do not strip unneeded symbols from binaries
153
+ --no-strip do not use or create stripped binaries
154
+ --no-extra-strip do not restrip already stripped source binaries
152
155
  -V show pystand version
153
156
 
154
157
  Commands:
@@ -342,8 +345,9 @@ anything after on a line) are ignored. Type `pystand` to see all
342
345
  supported options.
343
346
 
344
347
  The global options: `--distribution`, `--base-dir`, `--cache-minutes`,
345
- `--purge-days`, `--github-access-token`, `--do-not-strip` are the only
346
- sensible candidates to consider setting as defaults.
348
+ `--purge-days`, `--github-access-token`, `--no-strip`,
349
+ `--no-extra-strip` are the only sensible candidates to consider setting
350
+ as defaults.
347
351
 
348
352
  ## Command Line Tab Completion
349
353
 
@@ -376,5 +380,6 @@ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at
376
380
  [pdmpy]: https://pdm-project.org/en/latest/usage/project/#install-python-interpreters-with-pdm
377
381
  [hatch]: https://hatch.pypa.io/
378
382
  [hatchpy]: https://hatch.pypa.io/latest/tutorials/python/manage/
383
+ [ryepy]: https://rye.astral.sh/guide/toolchains/#fetching-toolchains
379
384
 
380
385
  <!-- vim: se ai syn=markdown: -->
@@ -0,0 +1,6 @@
1
+ pystand.py,sha256=UoQBHL_ORiI56c3Z3j2Sw6FubW4h8kAxx5dZ6ZIfty0,30499
2
+ pystand-1.7.dist-info/METADATA,sha256=NhfQfiLnwzegtPlSnORtFAG1L3usbwFAWTBO3-hbZsA,15105
3
+ pystand-1.7.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
4
+ pystand-1.7.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
5
+ pystand-1.7.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
6
+ pystand-1.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.1)
2
+ Generator: setuptools (71.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pystand.py CHANGED
@@ -40,6 +40,8 @@ FIRST_RELEASE = '20210724'
40
40
  # Sample release tag for documentation/usage examples
41
41
  SAMPL_RELEASE = '20240415'
42
42
 
43
+ STRIP_EXT = '_stripped'
44
+
43
45
  # The following regexp pattern is used to match the end of a release file name
44
46
  ENDPAT = r'-install_only[-\dT]*.tar.gz$'
45
47
 
@@ -234,14 +236,22 @@ def get_release_tag(args: Namespace) -> str:
234
236
  args._latest_release.write_text(tag + '\n')
235
237
  return tag
236
238
 
237
- def get_release_name(filename: str) -> Optional[str]:
238
- 'Search for and strip the release name from the file name'
239
- if not (m := re.search(ENDPAT, filename)):
240
- return None
241
-
242
- # Strip of end of file name and also strip any +8 digit embedded
243
- # release date
244
- return re.sub(r'\+\d{8}', '', filename[:m.start()])
239
+ def merge(files: dict, name: str, url: str, stripped: bool) -> None:
240
+ 'Merge a file into the files dict'
241
+ # We store: A string url when there is no stripped version; or a
242
+ # tuple of string urls, the first unstripped and the second
243
+ # stripped. It's a bit messy but we are keeping compatibility with
244
+ # earlier releases (and their cached release files) which did not
245
+ # have stripped versions.
246
+ impl, ver, distrib = name.split('-', maxsplit=2)
247
+ if impl not in files:
248
+ files[impl] = defaultdict(dict)
249
+
250
+ if exist := files[impl].get(ver, {}).get(distrib, {}):
251
+ files[impl][ver][distrib] = (exist, url) \
252
+ if stripped else (url, exist[1])
253
+ else:
254
+ files[impl][ver][distrib] = ('', url) if stripped else url
245
255
 
246
256
  def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
247
257
  'Return the release files for the given tag'
@@ -260,17 +270,19 @@ def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
260
270
  except Exception:
261
271
  return {}
262
272
 
263
- # Iterate over the release assets and store the files in a dict to
264
- # return
265
- for file in release.get_assets():
266
- if not (name := get_release_name(file.name)):
267
- continue
273
+ # Iterate over the release assets and store pertinent files in a
274
+ # dict to return.
275
+ for asset in release.get_assets():
276
+ name = asset.name
277
+ if is_stripped := (STRIP_EXT in name):
278
+ name = name.replace(STRIP_EXT, '')
268
279
 
269
- impl, ver, rest = name.split('-', maxsplit=2)
270
- if impl not in files:
271
- files[impl] = defaultdict(dict)
280
+ if m := re.search(ENDPAT, name):
281
+ name = re.sub(r'\+\d{8}', '', name[:m.start()])
282
+ merge(files, name, asset.browser_download_url, is_stripped)
272
283
 
273
- files[impl][ver][rest] = file.browser_download_url
284
+ if not files:
285
+ sys.exit(f'Failed to fetch any files for release {tag}')
274
286
 
275
287
  if error := set_json(jfile, files):
276
288
  sys.exit(f'Failed to write release {tag} file {jfile}: {error}')
@@ -309,13 +321,13 @@ def update_version_symlinks(args: Namespace) -> None:
309
321
  for name, tgt in oldlinks.items():
310
322
  new_tgt = newlinks.get(name)
311
323
  if not new_tgt or new_tgt != tgt:
312
- Path(base / name).unlink()
324
+ (base / name).unlink()
313
325
 
314
326
  # Create all needed new links
315
327
  for name, tgt in newlinks.items():
316
328
  old_tgt = oldlinks.get(name)
317
329
  if not old_tgt or old_tgt != tgt:
318
- Path(base / name).symlink_to(tgt, target_is_directory=True)
330
+ (base / name).symlink_to(tgt, target_is_directory=True)
319
331
 
320
332
  def purge_unused_releases(args: Namespace) -> None:
321
333
  'Purge old releases that are no longer needed and have expired'
@@ -368,34 +380,43 @@ def remove(args: Namespace, version: str) -> None:
368
380
 
369
381
  shutil.rmtree(vdir)
370
382
 
371
- def strip(vdir: Path, distribution: str) -> None:
383
+ def strip_binaries(vdir: Path, distribution: str) -> bool:
372
384
  'Strip binaries from files in a version directory'
373
385
  # Only run the strip command on Linux hosts and for Linux distributions
374
- if platform.system() != 'Linux' or '-linux-' not in distribution:
375
- return
386
+ was_stripped = False
387
+ if platform.system() == 'Linux' and '-linux-' in distribution:
388
+ for path in ('bin', 'lib'):
389
+ base = vdir / path
390
+ if not base.is_dir():
391
+ continue
376
392
 
377
- for path in ('bin', 'lib'):
378
- base = vdir / path
379
- if not base.is_dir():
380
- continue
393
+ for file in base.iterdir():
394
+ if not file.is_symlink() and file.is_file():
395
+ cmd = f'strip -p --strip-unneeded {file}'.split()
396
+ try:
397
+ subprocess.run(cmd, stderr=subprocess.DEVNULL)
398
+ except Exception:
399
+ pass
400
+ else:
401
+ was_stripped = True
381
402
 
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
403
+ return was_stripped
389
404
 
390
405
  def install(args: Namespace, vdir: Path, release: str, distribution: str,
391
406
  files: dict) -> Optional[str]:
392
407
  'Install a version'
393
408
  version = vdir.name
394
409
 
395
- if not (file := files[version].get(distribution)):
410
+ if not (fileurl := files[version].get(distribution)):
396
411
  return f'Arch "{distribution}" not found for release '\
397
412
  f'{release} version {version}.'
398
413
 
414
+ if isinstance(fileurl, tuple):
415
+ stripped = not args.no_strip
416
+ fileurl = fileurl[stripped]
417
+ else:
418
+ stripped = False
419
+
399
420
  tmpdir = args._versions / f'.{version}-tmp'
400
421
  rm_path(tmpdir)
401
422
  tmpdir.mkdir()
@@ -403,19 +424,32 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
403
424
  error = None
404
425
 
405
426
  try:
406
- urllib.request.urlretrieve(file, tmpdir / 'tmp.tar.gz')
427
+ urllib.request.urlretrieve(fileurl, tmpdir / 'tmp.tar.gz')
428
+ except Exception as e:
429
+ error = f'Failed to fetch "{fileurl}": {e}'
430
+
431
+ try:
407
432
  shutil.unpack_archive(tmpdir / 'tmp.tar.gz', tmpdir)
408
433
  except Exception as e:
409
- error = f'Failed to fetch "{version}": {e}'
434
+ error = f'Failed to unpack "{fileurl}": {e}'
410
435
 
411
436
  if not error:
412
437
  data = {'release': release, 'distribution': distribution}
438
+
439
+ if args.no_strip:
440
+ data['stripped'] = 'forced off'
441
+ elif stripped and args.no_extra_strip:
442
+ data['stripped'] = 'at_source_only'
443
+ else:
444
+ if strip_binaries(tmpdir_py, distribution):
445
+ data['stripped'] = 'at_source_and_manually' \
446
+ if stripped else 'manually'
447
+ else:
448
+ data['stripped'] = 'at_source' if stripped else 'n/a'
449
+
413
450
  if (error := set_json(tmpdir_py / args._data, data)):
414
451
  error = f'Failed to write {version} data file: {error}'
415
452
 
416
- if not args.do_not_strip:
417
- strip(tmpdir_py, distribution)
418
-
419
453
  if not error:
420
454
  remove(args, version)
421
455
  tmpdir_py.replace(vdir)
@@ -426,7 +460,7 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
426
460
  def main() -> Optional[str]:
427
461
  'Main code'
428
462
  distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
429
- distro_help = distro_default if distro_default else '?unknown?'
463
+ distro_help = distro_default or '?unknown?'
430
464
 
431
465
  base_dir = Path('/opt' if is_admin() else
432
466
  platformdirs.user_data_dir()) / PROG
@@ -456,8 +490,10 @@ def main() -> Optional[str]:
456
490
  opt.add_argument('--github-access-token',
457
491
  help='optional Github access token. Can specify to reduce '
458
492
  'rate limiting.')
459
- opt.add_argument('--do-not-strip',
460
- help='Do not strip unneeded symbols from binaries')
493
+ opt.add_argument('--no-strip', action='store_true',
494
+ help='do not use or create stripped binaries')
495
+ opt.add_argument('--no-extra-strip', action='store_true',
496
+ help='do not restrip already stripped source binaries')
461
497
  opt.add_argument('-V', action='store_true',
462
498
  help=f'show {PROG} version')
463
499
  cmd = opt.add_subparsers(title='Commands', dest='cmdname')
@@ -1,6 +0,0 @@
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,,