pystand 2.35__tar.gz → 2.37__tar.gz

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,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystand
3
- Version: 2.35
3
+ Version: 2.37
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-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/bulletmark/pystand
8
8
  Keywords: python-build-standalone,pyenv,hatch,pdm
9
- Requires-Python: >=3.8
9
+ Requires-Python: >=3.9
10
10
  Description-Content-Type: text/markdown
11
11
  Requires-Dist: packaging
12
12
  Requires-Dist: platformdirs
@@ -247,7 +247,7 @@ positional arguments:
247
247
  options:
248
248
  -h, --help show this help message and exit
249
249
  -r, --release RELEASE
250
- update to specified YYYMMDD release (e.g. 20260807),
250
+ update to specified YYYYMMDD release (e.g. 20260807),
251
251
  default is latest release
252
252
  -a, --all update ALL versions
253
253
  --pre-release update release version to a later superceding pre-
@@ -276,8 +276,8 @@ options:
276
276
  --skip skip the specified versions when removing all (only
277
277
  can be specified with --all)
278
278
  -r, --release RELEASE
279
- only remove versions if from specified YYYMMDD release
280
- (e.g. 20260807)
279
+ only remove versions if from specified YYYYMMDD
280
+ release (e.g. 20260807)
281
281
 
282
282
  aliases: r, rm
283
283
  ```
@@ -412,7 +412,7 @@ options:
412
412
 
413
413
  ## Installation and Upgrade
414
414
 
415
- Python 3.8 or later is required. Arch Linux users can install [`pystand`
415
+ Python 3.9 or later is required. Arch Linux users can install [`pystand`
416
416
  from the AUR](https://aur.archlinux.org/packages/pystand) and skip this
417
417
  section.
418
418
 
@@ -229,7 +229,7 @@ positional arguments:
229
229
  options:
230
230
  -h, --help show this help message and exit
231
231
  -r, --release RELEASE
232
- update to specified YYYMMDD release (e.g. 20260807),
232
+ update to specified YYYYMMDD release (e.g. 20260807),
233
233
  default is latest release
234
234
  -a, --all update ALL versions
235
235
  --pre-release update release version to a later superceding pre-
@@ -258,8 +258,8 @@ options:
258
258
  --skip skip the specified versions when removing all (only
259
259
  can be specified with --all)
260
260
  -r, --release RELEASE
261
- only remove versions if from specified YYYMMDD release
262
- (e.g. 20260807)
261
+ only remove versions if from specified YYYYMMDD
262
+ release (e.g. 20260807)
263
263
 
264
264
  aliases: r, rm
265
265
  ```
@@ -394,7 +394,7 @@ options:
394
394
 
395
395
  ## Installation and Upgrade
396
396
 
397
- Python 3.8 or later is required. Arch Linux users can install [`pystand`
397
+ Python 3.9 or later is required. Arch Linux users can install [`pystand`
398
398
  from the AUR](https://aur.archlinux.org/packages/pystand) and skip this
399
399
  section.
400
400
 
@@ -7,7 +7,7 @@ name = "pystand"
7
7
  description = "Install Python versions from python-build-standalone project"
8
8
  readme = "README.md"
9
9
  license = "GPL-3.0-or-later"
10
- requires-python = ">=3.8"
10
+ requires-python = ">=3.9"
11
11
  keywords = ["python-build-standalone", "pyenv", "hatch", "pdm"]
12
12
  dynamic = ["version"]
13
13
  dependencies = [
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystand
3
- Version: 2.35
3
+ Version: 2.37
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-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/bulletmark/pystand
8
8
  Keywords: python-build-standalone,pyenv,hatch,pdm
9
- Requires-Python: >=3.8
9
+ Requires-Python: >=3.9
10
10
  Description-Content-Type: text/markdown
11
11
  Requires-Dist: packaging
12
12
  Requires-Dist: platformdirs
@@ -247,7 +247,7 @@ positional arguments:
247
247
  options:
248
248
  -h, --help show this help message and exit
249
249
  -r, --release RELEASE
250
- update to specified YYYMMDD release (e.g. 20260807),
250
+ update to specified YYYYMMDD release (e.g. 20260807),
251
251
  default is latest release
252
252
  -a, --all update ALL versions
253
253
  --pre-release update release version to a later superceding pre-
@@ -276,8 +276,8 @@ options:
276
276
  --skip skip the specified versions when removing all (only
277
277
  can be specified with --all)
278
278
  -r, --release RELEASE
279
- only remove versions if from specified YYYMMDD release
280
- (e.g. 20260807)
279
+ only remove versions if from specified YYYYMMDD
280
+ release (e.g. 20260807)
281
281
 
282
282
  aliases: r, rm
283
283
  ```
@@ -412,7 +412,7 @@ options:
412
412
 
413
413
  ## Installation and Upgrade
414
414
 
415
- Python 3.8 or later is required. Arch Linux users can install [`pystand`
415
+ Python 3.9 or later is required. Arch Linux users can install [`pystand`
416
416
  from the AUR](https://aur.archlinux.org/packages/pystand) and skip this
417
417
  section.
418
418
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.37",
3
+ "distance": 0,
4
+ "node": "g0c04602c6321af5be0ab5b6b6e0337415036645d",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-09-11"
8
+ }
@@ -10,6 +10,7 @@ import itertools
10
10
  import os
11
11
  import platform
12
12
  import re
13
+ import shlex
13
14
  import shutil
14
15
  import ssl
15
16
  import subprocess
@@ -254,7 +255,7 @@ def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
254
255
  else:
255
256
  pdir = tmpdir / 'python'
256
257
  idir = pdir / 'install'
257
- if idir.exists():
258
+ if idir.is_dir():
258
259
  # This is a source distribution, copy the source if
259
260
  # requested
260
261
  if args.include_source:
@@ -266,7 +267,10 @@ def fetch(args: Namespace, release: str, url: str, tdir: Path) -> str | None:
266
267
 
267
268
  pdir = idir
268
269
 
269
- pdir.replace(tdir)
270
+ if pdir.is_dir():
271
+ pdir.replace(tdir)
272
+ else:
273
+ error = f'Failed to find unpacked python directory in "{url}".'
270
274
 
271
275
  rm_path(tmpdir)
272
276
  return error
@@ -426,15 +430,18 @@ def get_release_tag(args: Namespace) -> str:
426
430
 
427
431
  return release
428
432
 
429
- if args._latest_release.exists():
430
- stat = args._latest_release.stat()
431
- if time.time() < (stat.st_mtime + int(args.cache_minutes * 60)):
432
- return args._latest_release.read_text().strip()
433
+ latest_release = args._latest_release
434
+
435
+ if latest_release.is_file() and (
436
+ not args.check_latest
437
+ or time.time() < (latest_release.stat().st_mtime + int(args.cache_minutes * 60))
438
+ ):
439
+ return latest_release.read_text().strip()
433
440
 
434
441
  if not (tag := fetch_tag_latest(args)):
435
442
  sys.exit('Latest YYYYMMDD release tag timestamp file is unavailable.')
436
443
 
437
- args._latest_release.write_text(tag + '\n')
444
+ latest_release.write_text(tag + '\n')
438
445
  return tag
439
446
 
440
447
 
@@ -559,8 +566,8 @@ def keeplist(args: Namespace) -> set[str]:
559
566
  }
560
567
 
561
568
  # Add current release to keep list (even if not currently installed)
562
- if args._latest_release.exists():
563
- keep.add(args._latest_release.read_text().strip())
569
+ if (latest_release := args._latest_release).exists():
570
+ keep.add(latest_release.read_text().strip())
564
571
 
565
572
  return keep
566
573
 
@@ -577,7 +584,7 @@ def purge_unused_releases(args: Namespace) -> None:
577
584
  for path in args._releases.iterdir():
578
585
  if path.name not in keep:
579
586
  if (path.stat().st_mtime + end_secs) < now_secs:
580
- path.unlink()
587
+ rm_path(path)
581
588
  else:
582
589
  keep.add(path.name)
583
590
 
@@ -588,13 +595,16 @@ def purge_unused_releases(args: Namespace) -> None:
588
595
  rm_path(path)
589
596
 
590
597
 
591
- def show_list(args: Namespace) -> None:
598
+ def show_list(args: Namespace, re_match: re.Pattern | None) -> None:
592
599
  "Show a list of available releases"
593
- latest = parse_version(args._release)
600
+ try:
601
+ latest = parse_version(args._release)
602
+ except InvalidVersion:
603
+ latest = None
594
604
  releases = fetch_tags(args)
595
605
  cached = {p.name for p in args._releases.iterdir()}
596
606
  for release in sorted(cached.union(releases)):
597
- if args._re_match and not args._re_match.search(release):
607
+ if re_match and not re_match.search(release):
598
608
  continue
599
609
 
600
610
  # Ignore any bogus releases that don't parse as versions
@@ -605,6 +615,7 @@ def show_list(args: Namespace) -> None:
605
615
  continue
606
616
 
607
617
  if dt_str := releases.get(release):
618
+ dt_str = dt_str.replace('Z', '+00:00')
608
619
  dts = (
609
620
  datetime.fromisoformat(dt_str)
610
621
  .astimezone()
@@ -620,7 +631,7 @@ def show_list(args: Namespace) -> None:
620
631
  else:
621
632
  app = ''
622
633
 
623
- pre = ' pre-release' if this_version > latest else ''
634
+ pre = ' pre-release' if (latest and this_version > latest) else ''
624
635
  print(f'{release} {dts}{app}{pre}')
625
636
 
626
637
 
@@ -636,7 +647,7 @@ def get_title(desc: str, name: str) -> str:
636
647
  sys.exit(f'Must end {name} command description with a full stop.')
637
648
 
638
649
 
639
- def remove(args: Namespace, version: str) -> None:
650
+ def remove_version(args: Namespace, version: str) -> None:
640
651
  "Remove a version"
641
652
  vdir = args._versions / version
642
653
  if not vdir.exists():
@@ -665,7 +676,7 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
665
676
 
666
677
  for file in base.iterdir():
667
678
  if not file.is_symlink() and file.is_file():
668
- cmd = f'strip -p --strip-unneeded {file}'.split()
679
+ cmd = ('strip', '-p', '--strip-unneeded', file)
669
680
  try:
670
681
  res = subprocess.run(cmd, stderr=subprocess.DEVNULL)
671
682
  except Exception:
@@ -703,7 +714,7 @@ def compile_bytecode(srcdir: Path, tgtdir: Path) -> str | None:
703
714
  return None
704
715
 
705
716
 
706
- def install(
717
+ def install_version(
707
718
  args: Namespace, vdir: Path, release: str, distribution: str, files: dict[str, Any]
708
719
  ) -> str | None:
709
720
  "Install a version"
@@ -728,14 +739,14 @@ def install(
728
739
  error = f'Failed to write {version} data file: {error}'
729
740
  elif c_error := compile_bytecode(tmpdir, vdir):
730
741
  print(
731
- f'Warning: Could not compile bytecode for {version}: {c_error}',
742
+ f'Warning: Error compiling bytecode for {version}: {c_error}',
732
743
  file=sys.stderr,
733
744
  )
734
745
 
735
746
  if error:
736
747
  shutil.rmtree(tmpdir)
737
748
  else:
738
- remove(args, version)
749
+ remove_version(args, version)
739
750
  tmpdir.replace(vdir)
740
751
 
741
752
  return error
@@ -802,7 +813,14 @@ def run_uv(args: Namespace, cmd: list[str], cmdopts: list[str]) -> str | None:
802
813
  py = py.resolve()
803
814
  py = py.with_name(py.stem)
804
815
 
805
- subprocess.run(cmd + ['-p', py] + cmdopts)
816
+ cmd = cmd + ['-p', str(py)] + cmdopts
817
+ res = subprocess.run(cmd)
818
+ if res.returncode != 0:
819
+ return f'Command "{shlex.join(cmd)}" failed with exit code {res.returncode}.'
820
+
821
+
822
+ # List of internal command classes, populated by @Command decorator
823
+ commands = []
806
824
 
807
825
 
808
826
  def main() -> str | None:
@@ -878,12 +896,8 @@ def main() -> str | None:
878
896
  cmd = opt.add_subparsers(title='Commands', dest='cmdname')
879
897
 
880
898
  # Add each command ..
881
- for name in globals():
882
- if not name[0].islower() or not name.endswith('_'):
883
- continue
884
-
885
- cls = globals()[name]
886
- name = name[:-1]
899
+ for cls in commands:
900
+ name = cls.__name__.rstrip('_')
887
901
 
888
902
  if hasattr(cls, 'doc'):
889
903
  desc = cls.doc.strip()
@@ -901,7 +915,12 @@ def main() -> str | None:
901
915
  cls.init(cmdopt)
902
916
 
903
917
  # Set the function to call
904
- cmdopt.set_defaults(func=cls.run, name=name, parser=cmdopt)
918
+ cmdopt.set_defaults(
919
+ func=cls.run,
920
+ name=name,
921
+ parser=cmdopt,
922
+ check_latest=getattr(cls, 'check_latest', False),
923
+ )
905
924
 
906
925
  args = opt.parse_args()
907
926
 
@@ -955,10 +974,17 @@ def main() -> str | None:
955
974
  return result
956
975
 
957
976
 
958
- # COMMAND
959
- class install_:
977
+ def Command(command: type) -> type:
978
+ "Decorator to add given command class to list of commands"
979
+ commands.append(command)
980
+ return command
981
+
982
+
983
+ @Command
984
+ class install:
960
985
  doc = f'Install one, more, or all versions from a {REPO} release.'
961
986
  aliases = ('i',)
987
+ check_latest = True
962
988
 
963
989
  @staticmethod
964
990
  def init(parser: ArgumentParser) -> None:
@@ -1048,24 +1074,25 @@ class install_:
1048
1074
  )
1049
1075
  continue
1050
1076
 
1051
- if error := install(args, vdir, release, args._distribution, files):
1077
+ if error := install_version(args, vdir, release, args._distribution, files):
1052
1078
  return error
1053
1079
 
1054
1080
  print(f'Version {args._fmtrel(version, release)} installed.')
1055
1081
 
1056
1082
 
1057
- # COMMAND
1058
- class update_:
1083
+ @Command
1084
+ class update:
1059
1085
  "Update one, more, or all versions to another release."
1060
1086
 
1061
1087
  aliases = ('u', 'up')
1088
+ check_latest = True
1062
1089
 
1063
1090
  @staticmethod
1064
1091
  def init(parser: ArgumentParser) -> None:
1065
1092
  parser.add_argument(
1066
1093
  '-r',
1067
1094
  '--release',
1068
- help='update to specified YYYMMDD release (e.g. '
1095
+ help='update to specified YYYYMMDD release (e.g. '
1069
1096
  f'{SAMPL_RELEASE}), default is latest release',
1070
1097
  )
1071
1098
  parser.add_argument(
@@ -1143,15 +1170,17 @@ class update_:
1143
1170
  # the update.
1144
1171
  args.include_source = (vdir / 'src').is_dir()
1145
1172
 
1146
- if error := install(args, new_vdir, release_target, distribution, files):
1173
+ if error := install_version(
1174
+ args, new_vdir, release_target, distribution, files
1175
+ ):
1147
1176
  return error
1148
1177
 
1149
1178
  if nextver != version and not args.keep:
1150
- remove(args, version)
1179
+ remove_version(args, version)
1151
1180
 
1152
1181
 
1153
- # COMMAND
1154
- class remove_:
1182
+ @Command
1183
+ class remove:
1155
1184
  "Remove/uninstall one, more, or all versions."
1156
1185
 
1157
1186
  aliases = ('r', 'rm')
@@ -1171,7 +1200,7 @@ class remove_:
1171
1200
  '-r',
1172
1201
  '--release',
1173
1202
  help='only remove versions if from specified '
1174
- f'YYYMMDD release (e.g. {SAMPL_RELEASE})',
1203
+ f'YYYYMMDD release (e.g. {SAMPL_RELEASE})',
1175
1204
  )
1176
1205
  parser.add_argument(
1177
1206
  'version', nargs='*', help='version to remove (or to skip for --all --skip)'
@@ -1192,10 +1221,12 @@ class remove_:
1192
1221
 
1193
1222
 
1194
1223
  # COMMAND
1224
+ @Command
1195
1225
  class list_:
1196
1226
  "List installed versions and show which have an update available."
1197
1227
 
1198
1228
  aliases = ('l',)
1229
+ check_latest = True
1199
1230
 
1200
1231
  @staticmethod
1201
1232
  def init(parser: ArgumentParser) -> None:
@@ -1278,7 +1309,8 @@ class list_:
1278
1309
 
1279
1310
 
1280
1311
  # COMMAND
1281
- class show_:
1312
+ @Command
1313
+ class show:
1282
1314
  doc = f"""
1283
1315
  Show versions available from a release.
1284
1316
 
@@ -1286,6 +1318,7 @@ class show_:
1286
1318
  {GITHUB_SITE}/releases.
1287
1319
  """
1288
1320
  aliases = ('s',)
1321
+ check_latest = True
1289
1322
 
1290
1323
  @staticmethod
1291
1324
  def init(parser: ArgumentParser) -> None:
@@ -1321,13 +1354,11 @@ class show_:
1321
1354
  except re.error as error:
1322
1355
  args.parser.error(f'Invalid regular expression: {error}')
1323
1356
 
1324
- if args.all and args.list:
1325
- args.parser.error('Can not specify --all with --list.')
1326
-
1327
1357
  if args.list:
1328
- args.release = False
1329
- args._re_match = re_match
1330
- show_list(args)
1358
+ if args.all:
1359
+ args.parser.error('Can not specify --all with --list.')
1360
+
1361
+ show_list(args, re_match)
1331
1362
  return None
1332
1363
 
1333
1364
  release = args._release
@@ -1362,8 +1393,8 @@ class show_:
1362
1393
  )
1363
1394
 
1364
1395
 
1365
- # COMMAND
1366
- class path_:
1396
+ @Command
1397
+ class path:
1367
1398
  "Show path prefix to installed version base directory."
1368
1399
 
1369
1400
  aliases = ('p',)
@@ -1410,8 +1441,8 @@ class path_:
1410
1441
  print(path)
1411
1442
 
1412
1443
 
1413
- # COMMAND
1414
- class cache_:
1444
+ @Command
1445
+ class cache:
1415
1446
  "Show size of release download caches."
1416
1447
 
1417
1448
  aliases = ('c',)
@@ -1477,6 +1508,10 @@ class cache_:
1477
1508
  for release in args.release:
1478
1509
  # Allow user to include cache path in release name
1479
1510
  path = (args._downloads / release).expanduser()
1511
+ if not path.is_dir():
1512
+ print(f'No caches found for release {release}.', file=sys.stderr)
1513
+ continue
1514
+
1480
1515
  release = path.name
1481
1516
 
1482
1517
  if err := check_release_tag(release):
@@ -1507,8 +1542,8 @@ class cache_:
1507
1542
  show_cache_size(args._downloads, args)
1508
1543
 
1509
1544
 
1510
- # COMMAND
1511
- class uv_:
1545
+ @Command
1546
+ class uv:
1512
1547
  __doc__ = f'Run a uv command using a version of python installed by {PROG}.'
1513
1548
 
1514
1549
  @staticmethod
@@ -1537,8 +1572,8 @@ class uv_:
1537
1572
  return run_uv(args, cmd, args.uv_args_for_command)
1538
1573
 
1539
1574
 
1540
- # COMMAND
1541
- class uvx_:
1575
+ @Command
1576
+ class uvx:
1542
1577
  __doc__ = f'Run a program using uvx and a version of python installed by {PROG}.'
1543
1578
 
1544
1579
  @staticmethod
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.35",
3
- "distance": 0,
4
- "node": "gf90437d7f2e68e78d07fcefc226c22bbc3c067be",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-09-04"
8
- }
File without changes
File without changes
File without changes
File without changes