pystand 2.34__tar.gz → 2.36__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,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystand
3
- Version: 2.34
3
+ Version: 2.36
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
- Classifier: Programming Language :: Python :: 3
10
9
  Requires-Python: >=3.8
11
10
  Description-Content-Type: text/markdown
12
11
  Requires-Dist: packaging
@@ -141,10 +140,10 @@ Type `pystand` or `pystand -h` to view the usage summary:
141
140
 
142
141
  ```
143
142
  usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
144
- [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
145
- [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
146
- [--no-color] [--cert {system,certifi,none}] [-V]
147
- {install,i,update,u,up,remove,r,rm,list,l,show,s,path,p,cache,c,uv,uvx} ...
143
+ [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
144
+ [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
145
+ [--no-color] [--cert {system,certifi,none}] [-V]
146
+ {install,i,update,u,up,remove,r,rm,list,l,show,s,path,p,cache,c,uv,uvx} ...
148
147
 
149
148
  Command line tool to download, install, and update pre-built Python versions
150
149
  from the python-build-standalone project at https://github.com/astral-
@@ -203,14 +202,13 @@ Some commands offer aliases as shown in parentheses above. Note you can set
203
202
  default starting global options in ~/.config/pystand-flags.conf.
204
203
  ```
205
204
 
206
- Type `pystand <command> -h` to see specific help/usage for any
207
- individual command:
205
+ Type `pystand <command> -h` to see specific help/usage for any individual command:
208
206
 
209
207
  ### Command `install`
210
208
 
211
209
  ```
212
210
  usage: pystand install [-h] [-r RELEASE] [-a] [-A] [--skip] [-f] [-s]
213
- [version ...]
211
+ [version ...]
214
212
 
215
213
  Install one, more, or all versions from a python-build-standalone release.
216
214
 
@@ -239,7 +237,7 @@ aliases: i
239
237
 
240
238
  ```
241
239
  usage: pystand update [-h] [-r RELEASE] [-a] [--pre-release] [--skip] [-k]
242
- [version ...]
240
+ [version ...]
243
241
 
244
242
  Update one, more, or all versions to another release.
245
243
 
@@ -378,7 +376,7 @@ aliases: c
378
376
 
379
377
  ```
380
378
  usage: pystand uv [-h] [-p PYTHON]
381
- command [subcommand] [uv_args_for_command ...]
379
+ command [subcommand] [uv_args_for_command ...]
382
380
 
383
381
  Run a uv command using a version of python installed by pystand.
384
382
 
@@ -122,10 +122,10 @@ Type `pystand` or `pystand -h` to view the usage summary:
122
122
 
123
123
  ```
124
124
  usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
125
- [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
126
- [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
127
- [--no-color] [--cert {system,certifi,none}] [-V]
128
- {install,i,update,u,up,remove,r,rm,list,l,show,s,path,p,cache,c,uv,uvx} ...
125
+ [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
126
+ [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
127
+ [--no-color] [--cert {system,certifi,none}] [-V]
128
+ {install,i,update,u,up,remove,r,rm,list,l,show,s,path,p,cache,c,uv,uvx} ...
129
129
 
130
130
  Command line tool to download, install, and update pre-built Python versions
131
131
  from the python-build-standalone project at https://github.com/astral-
@@ -184,14 +184,13 @@ Some commands offer aliases as shown in parentheses above. Note you can set
184
184
  default starting global options in ~/.config/pystand-flags.conf.
185
185
  ```
186
186
 
187
- Type `pystand <command> -h` to see specific help/usage for any
188
- individual command:
187
+ Type `pystand <command> -h` to see specific help/usage for any individual command:
189
188
 
190
189
  ### Command `install`
191
190
 
192
191
  ```
193
192
  usage: pystand install [-h] [-r RELEASE] [-a] [-A] [--skip] [-f] [-s]
194
- [version ...]
193
+ [version ...]
195
194
 
196
195
  Install one, more, or all versions from a python-build-standalone release.
197
196
 
@@ -220,7 +219,7 @@ aliases: i
220
219
 
221
220
  ```
222
221
  usage: pystand update [-h] [-r RELEASE] [-a] [--pre-release] [--skip] [-k]
223
- [version ...]
222
+ [version ...]
224
223
 
225
224
  Update one, more, or all versions to another release.
226
225
 
@@ -359,7 +358,7 @@ aliases: c
359
358
 
360
359
  ```
361
360
  usage: pystand uv [-h] [-p PYTHON]
362
- command [subcommand] [uv_args_for_command ...]
361
+ command [subcommand] [uv_args_for_command ...]
363
362
 
364
363
  Run a uv command using a version of python installed by pystand.
365
364
 
@@ -15,7 +15,7 @@ upload: build
15
15
  uv-publish
16
16
 
17
17
  doc:
18
- update-readme-usage -A
18
+ update-readme-usage
19
19
 
20
20
  format:
21
21
  ruff check --select I --fix {{PYFILES}} && ruff format {{PYFILES}}
@@ -9,9 +9,6 @@ readme = "README.md"
9
9
  license = "GPL-3.0-or-later"
10
10
  requires-python = ">=3.8"
11
11
  keywords = ["python-build-standalone", "pyenv", "hatch", "pdm"]
12
- classifiers = [
13
- "Programming Language :: Python :: 3",
14
- ]
15
12
  dynamic = ["version"]
16
13
  dependencies = [
17
14
  "packaging",
@@ -36,17 +33,10 @@ pystand = "pystand:main"
36
33
  [tool.setuptools_scm]
37
34
  version_scheme = "post-release"
38
35
 
39
- [tool.mypy]
40
- implicit_optional = true
41
- warn_no_return = false
42
- allow_untyped_globals = true
43
- allow_redefinition = true
44
- ignore_missing_imports = true
45
-
46
36
  [tool.ruff.format]
47
37
  quote-style = "single"
48
38
 
49
39
  [tool.ruff.lint]
50
- ignore = ["SIM115", "SIM905", "BLE001", "PLW1510", "S110"]
40
+ ignore = ["SIM115", "SIM905", "BLE001", "PLW1510", "S110", "DTZ", "C408"]
51
41
 
52
42
  # vim:se sw=2:
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystand
3
- Version: 2.34
3
+ Version: 2.36
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
- Classifier: Programming Language :: Python :: 3
10
9
  Requires-Python: >=3.8
11
10
  Description-Content-Type: text/markdown
12
11
  Requires-Dist: packaging
@@ -141,10 +140,10 @@ Type `pystand` or `pystand -h` to view the usage summary:
141
140
 
142
141
  ```
143
142
  usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
144
- [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
145
- [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
146
- [--no-color] [--cert {system,certifi,none}] [-V]
147
- {install,i,update,u,up,remove,r,rm,list,l,show,s,path,p,cache,c,uv,uvx} ...
143
+ [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
144
+ [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
145
+ [--no-color] [--cert {system,certifi,none}] [-V]
146
+ {install,i,update,u,up,remove,r,rm,list,l,show,s,path,p,cache,c,uv,uvx} ...
148
147
 
149
148
  Command line tool to download, install, and update pre-built Python versions
150
149
  from the python-build-standalone project at https://github.com/astral-
@@ -203,14 +202,13 @@ Some commands offer aliases as shown in parentheses above. Note you can set
203
202
  default starting global options in ~/.config/pystand-flags.conf.
204
203
  ```
205
204
 
206
- Type `pystand <command> -h` to see specific help/usage for any
207
- individual command:
205
+ Type `pystand <command> -h` to see specific help/usage for any individual command:
208
206
 
209
207
  ### Command `install`
210
208
 
211
209
  ```
212
210
  usage: pystand install [-h] [-r RELEASE] [-a] [-A] [--skip] [-f] [-s]
213
- [version ...]
211
+ [version ...]
214
212
 
215
213
  Install one, more, or all versions from a python-build-standalone release.
216
214
 
@@ -239,7 +237,7 @@ aliases: i
239
237
 
240
238
  ```
241
239
  usage: pystand update [-h] [-r RELEASE] [-a] [--pre-release] [--skip] [-k]
242
- [version ...]
240
+ [version ...]
243
241
 
244
242
  Update one, more, or all versions to another release.
245
243
 
@@ -378,7 +376,7 @@ aliases: c
378
376
 
379
377
  ```
380
378
  usage: pystand uv [-h] [-p PYTHON]
381
- command [subcommand] [uv_args_for_command ...]
379
+ command [subcommand] [uv_args_for_command ...]
382
380
 
383
381
  Run a uv command using a version of python installed by pystand.
384
382
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.36",
3
+ "distance": 0,
4
+ "node": "g4d3f67f4313887dc9ada0e74d5186f253c66c1f0",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-09-04"
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
@@ -447,7 +451,10 @@ def add_file(files: dict[str, Any], tag: str, name: str, url: str) -> None:
447
451
  else:
448
452
  return
449
453
 
450
- impl, ver, arch = name.split('-', 2)
454
+ try:
455
+ impl, ver, arch = name.split('-', 2)
456
+ except ValueError:
457
+ return
451
458
 
452
459
  # Modern releases have a '+' in the name to separate the version
453
460
  if '+' in ver:
@@ -574,7 +581,7 @@ def purge_unused_releases(args: Namespace) -> None:
574
581
  for path in args._releases.iterdir():
575
582
  if path.name not in keep:
576
583
  if (path.stat().st_mtime + end_secs) < now_secs:
577
- path.unlink()
584
+ rm_path(path)
578
585
  else:
579
586
  keep.add(path.name)
580
587
 
@@ -587,11 +594,14 @@ def purge_unused_releases(args: Namespace) -> None:
587
594
 
588
595
  def show_list(args: Namespace) -> None:
589
596
  "Show a list of available releases"
590
- latest = parse_version(args._release)
597
+ try:
598
+ latest = parse_version(args._release)
599
+ except InvalidVersion:
600
+ latest = None
591
601
  releases = fetch_tags(args)
592
602
  cached = {p.name for p in args._releases.iterdir()}
593
603
  for release in sorted(cached.union(releases)):
594
- if args.re_match and not re.search(args.re_match, release):
604
+ if args._re_match and not args._re_match.search(release):
595
605
  continue
596
606
 
597
607
  # Ignore any bogus releases that don't parse as versions
@@ -602,6 +612,7 @@ def show_list(args: Namespace) -> None:
602
612
  continue
603
613
 
604
614
  if dt_str := releases.get(release):
615
+ dt_str = dt_str.replace('Z', '+00:00')
605
616
  dts = (
606
617
  datetime.fromisoformat(dt_str)
607
618
  .astimezone()
@@ -617,20 +628,20 @@ def show_list(args: Namespace) -> None:
617
628
  else:
618
629
  app = ''
619
630
 
620
- pre = ' pre-release' if this_version > latest else ''
631
+ pre = ' pre-release' if (latest and this_version > latest) else ''
621
632
  print(f'{release} {dts}{app}{pre}')
622
633
 
623
634
 
624
- def get_title(desc: str) -> str:
625
- "Return single title line from description"
635
+ def get_title(desc: str, name: str) -> str:
636
+ "Return single title line from command description"
626
637
  res = []
627
638
  for line in desc.splitlines():
628
- line = line.strip()
629
- res.append(line)
630
- if line.endswith('.'):
631
- return ' '.join(res)
639
+ if line := line.strip():
640
+ res.append(line)
641
+ if line.endswith('.'):
642
+ return ' '.join(res)
632
643
 
633
- sys.exit('Must end description with a full stop.')
644
+ sys.exit(f'Must end {name} command description with a full stop.')
634
645
 
635
646
 
636
647
  def remove(args: Namespace, version: str) -> None:
@@ -642,7 +653,9 @@ def remove(args: Namespace, version: str) -> None:
642
653
  # Touch the associated release file to ensure it lives until the
643
654
  # full purge time has expired if this was the last version using it
644
655
  if release := get_json(vdir / args._data).get('release'):
645
- (args._releases / release).touch()
656
+ release_file = args._releases / release
657
+ if release_file.is_file():
658
+ release_file.touch()
646
659
 
647
660
  shutil.rmtree(vdir)
648
661
 
@@ -660,17 +673,44 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
660
673
 
661
674
  for file in base.iterdir():
662
675
  if not file.is_symlink() and file.is_file():
663
- cmd = f'strip -p --strip-unneeded {file}'.split()
676
+ cmd = ('strip', '-p', '--strip-unneeded', file)
664
677
  try:
665
- subprocess.run(cmd, stderr=subprocess.DEVNULL)
678
+ res = subprocess.run(cmd, stderr=subprocess.DEVNULL)
666
679
  except Exception:
667
680
  pass
668
681
  else:
669
- was_stripped = True
682
+ was_stripped |= res.returncode == 0
670
683
 
671
684
  return was_stripped
672
685
 
673
686
 
687
+ def find_python(vdir: Path) -> Path | None:
688
+ "Find the python executable in a version directory"
689
+ for path in (vdir / 'bin' / 'python', vdir / 'python.exe'):
690
+ if path.is_file():
691
+ return path
692
+
693
+ return None
694
+
695
+
696
+ def compile_bytecode(srcdir: Path, tgtdir: Path) -> str | None:
697
+ "Compile all Python bytecode in a version directory"
698
+ if not (python := find_python(srcdir)):
699
+ return f'Python executable not found in {srcdir}'
700
+
701
+ cmd = (python, '-m', 'compileall', '-qd', str(tgtdir), str(srcdir))
702
+
703
+ try:
704
+ res = subprocess.run(cmd)
705
+ except Exception as e:
706
+ return f'Compile error: {e}'
707
+
708
+ if res.returncode != 0:
709
+ return f'Compile error code: {res.returncode}'
710
+
711
+ return None
712
+
713
+
674
714
  def install(
675
715
  args: Namespace, vdir: Path, release: str, distribution: str, files: dict[str, Any]
676
716
  ) -> str | None:
@@ -694,6 +734,11 @@ def install(
694
734
 
695
735
  if error := set_json(tmpdir / args._data, data):
696
736
  error = f'Failed to write {version} data file: {error}'
737
+ elif c_error := compile_bytecode(tmpdir, vdir):
738
+ print(
739
+ f'Warning: Error compiling bytecode for {version}: {c_error}',
740
+ file=sys.stderr,
741
+ )
697
742
 
698
743
  if error:
699
744
  shutil.rmtree(tmpdir)
@@ -765,7 +810,10 @@ def run_uv(args: Namespace, cmd: list[str], cmdopts: list[str]) -> str | None:
765
810
  py = py.resolve()
766
811
  py = py.with_name(py.stem)
767
812
 
768
- subprocess.run(cmd + ['-p', py] + cmdopts)
813
+ cmd = cmd + ['-p', str(py)] + cmdopts
814
+ res = subprocess.run(cmd)
815
+ if res.returncode != 0:
816
+ return f'Command "{shlex.join(cmd)}" failed with exit code {res.returncode}.'
769
817
 
770
818
 
771
819
  def main() -> str | None:
@@ -856,7 +904,7 @@ def main() -> str | None:
856
904
  return f'Must define a docstring for command class "{name}".'
857
905
 
858
906
  aliases = cls.aliases if hasattr(cls, 'aliases') else ()
859
- title = get_title(desc)
907
+ title = get_title(desc, name)
860
908
  cmdopt = cmd.add_parser(name, description=desc, help=title, aliases=aliases)
861
909
 
862
910
  # Set up this commands own arguments, if it has any
@@ -1028,7 +1076,7 @@ class update_:
1028
1076
  parser.add_argument(
1029
1077
  '-r',
1030
1078
  '--release',
1031
- help='update to specified YYYMMDD release (e.g. '
1079
+ help='update to specified YYYYMMDD release (e.g. '
1032
1080
  f'{SAMPL_RELEASE}), default is latest release',
1033
1081
  )
1034
1082
  parser.add_argument(
@@ -1134,7 +1182,7 @@ class remove_:
1134
1182
  '-r',
1135
1183
  '--release',
1136
1184
  help='only remove versions if from specified '
1137
- f'YYYMMDD release (e.g. {SAMPL_RELEASE})',
1185
+ f'YYYYMMDD release (e.g. {SAMPL_RELEASE})',
1138
1186
  )
1139
1187
  parser.add_argument(
1140
1188
  'version', nargs='*', help='version to remove (or to skip for --all --skip)'
@@ -1277,11 +1325,19 @@ class show_:
1277
1325
 
1278
1326
  @staticmethod
1279
1327
  def run(args: Namespace) -> str | None:
1328
+ re_match: re.Pattern | None = None
1329
+ if args.re_match:
1330
+ try:
1331
+ re_match = re.compile(args.re_match)
1332
+ except re.error as error:
1333
+ args.parser.error(f'Invalid regular expression: {error}')
1334
+
1280
1335
  if args.all and args.list:
1281
1336
  args.parser.error('Can not specify --all with --list.')
1282
1337
 
1283
1338
  if args.list:
1284
1339
  args.release = False
1340
+ args._re_match = re_match
1285
1341
  show_list(args)
1286
1342
  return None
1287
1343
 
@@ -1305,9 +1361,7 @@ class show_:
1305
1361
  if distribution == args._distribution:
1306
1362
  installable = True
1307
1363
 
1308
- if not args.re_match or re.search(
1309
- args.re_match, f'{version}+{distribution}'
1310
- ):
1364
+ if not re_match or re_match.search(f'{version}+{distribution}'):
1311
1365
  print(
1312
1366
  f'{args._fmtrel(version, release)} '
1313
1367
  f'{args._fmtdist(distribution)}{app}'
@@ -1361,13 +1415,8 @@ class path_:
1361
1415
  if args.resolve:
1362
1416
  path = path.resolve()
1363
1417
 
1364
- if args.python_path:
1365
- basepath = path
1366
- path = basepath / 'bin' / 'python'
1367
- if not path.exists():
1368
- path = basepath / 'python.exe'
1369
- if not path.exists():
1370
- return f'Error: Can not find python executable in "{basepath}"'
1418
+ if args.python_path and not (path := find_python(basepath := path)):
1419
+ return f'Error: Can not find python executable in "{basepath}"'
1371
1420
 
1372
1421
  print(path)
1373
1422
 
@@ -1439,6 +1488,10 @@ class cache_:
1439
1488
  for release in args.release:
1440
1489
  # Allow user to include cache path in release name
1441
1490
  path = (args._downloads / release).expanduser()
1491
+ if not path.is_dir():
1492
+ print(f'No caches found for release {release}.', file=sys.stderr)
1493
+ continue
1494
+
1442
1495
  release = path.name
1443
1496
 
1444
1497
  if err := check_release_tag(release):
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.34",
3
- "distance": 0,
4
- "node": "ge147d95a4c421da907727e05ae933d512bcd48fb",
5
- "dirty": false,
6
- "branch": "main",
7
- "node_date": "2026-08-10"
8
- }
File without changes
File without changes
File without changes