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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pystand
3
- Version: 1.0
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
  [![PyPi](https://img.shields.io/pypi/v/pystand)](https://pypi.org/project/pystand/)
19
19
  [![AUR](https://img.shields.io/aur/version/pystand)](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 from the python-build-
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 versions from the
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 Version
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=Version, reverse=True)
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=Version) if args.all else versions
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=Version):
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)' \
@@ -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