pystand 2.20__tar.gz → 2.22__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystand
3
- Version: 2.20
3
+ Version: 2.22
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
@@ -133,7 +133,7 @@ Type `pystand` or `pystand -h` to view the usage summary:
133
133
  usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
134
134
  [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
135
135
  [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
136
- [--cert {system,certifi,none}] [-V]
136
+ [--no-color] [--cert {system,certifi,none}] [-V]
137
137
  {install,update,upgrade,remove,uninstall,list,show,path,cache} ...
138
138
 
139
139
  Command line tool to download, install, and update pre-built Python versions
@@ -167,6 +167,7 @@ options:
167
167
  optional Github access token. Can specify to reduce
168
168
  rate limiting.
169
169
  --no-strip do not strip downloaded binaries
170
+ --no-color do not use color in output
170
171
  --cert {system,certifi,none}
171
172
  specify which SSL certificates to use for HTTPS
172
173
  requests. Default="system".
@@ -185,7 +186,7 @@ Commands:
185
186
  cache Show size of release download caches.
186
187
 
187
188
  Some commands offer aliases as shown in parentheses above. Note you can set
188
- default starting global options in $HOME/.config/pystand-flags.conf.
189
+ default starting global options in ~/.config/pystand-flags.conf.
189
190
  ```
190
191
 
191
192
  Type `pystand <command> -h` to see specific help/usage for any
@@ -113,7 +113,7 @@ Type `pystand` or `pystand -h` to view the usage summary:
113
113
  usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
114
114
  [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
115
115
  [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
116
- [--cert {system,certifi,none}] [-V]
116
+ [--no-color] [--cert {system,certifi,none}] [-V]
117
117
  {install,update,upgrade,remove,uninstall,list,show,path,cache} ...
118
118
 
119
119
  Command line tool to download, install, and update pre-built Python versions
@@ -147,6 +147,7 @@ options:
147
147
  optional Github access token. Can specify to reduce
148
148
  rate limiting.
149
149
  --no-strip do not strip downloaded binaries
150
+ --no-color do not use color in output
150
151
  --cert {system,certifi,none}
151
152
  specify which SSL certificates to use for HTTPS
152
153
  requests. Default="system".
@@ -165,7 +166,7 @@ Commands:
165
166
  cache Show size of release download caches.
166
167
 
167
168
  Some commands offer aliases as shown in parentheses above. Note you can set
168
- default starting global options in $HOME/.config/pystand-flags.conf.
169
+ default starting global options in ~/.config/pystand-flags.conf.
169
170
  ```
170
171
 
171
172
  Type `pystand <command> -h` to see specific help/usage for any
pystand-2.22/justfile ADDED
@@ -0,0 +1,23 @@
1
+ PYFILES := file_name(justfile_dir()) + '.py'
2
+
3
+ check:
4
+ ruff check {{PYFILES}}
5
+ ty check --python /usr/bin/python {{PYFILES}}
6
+ vermin -vv --no-tips -i {{PYFILES}}
7
+ md-link-checker
8
+
9
+ build:
10
+ rm -rf dist
11
+ uv build
12
+
13
+ upload: build
14
+ uv-publish
15
+
16
+ doc:
17
+ update-readme-usage -A
18
+
19
+ format:
20
+ ruff check --select I --fix {{PYFILES}} && ruff format {{PYFILES}}
21
+
22
+ clean:
23
+ @rm -vrf *.egg-info build/ dist/ __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystand
3
- Version: 2.20
3
+ Version: 2.22
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
@@ -133,7 +133,7 @@ Type `pystand` or `pystand -h` to view the usage summary:
133
133
  usage: pystand [-h] [-D DISTRIBUTION] [-P PREFIX_DIR] [-C CACHE_DIR]
134
134
  [-M CACHE_MINUTES] [--purge-days PURGE_DAYS]
135
135
  [--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
136
- [--cert {system,certifi,none}] [-V]
136
+ [--no-color] [--cert {system,certifi,none}] [-V]
137
137
  {install,update,upgrade,remove,uninstall,list,show,path,cache} ...
138
138
 
139
139
  Command line tool to download, install, and update pre-built Python versions
@@ -167,6 +167,7 @@ options:
167
167
  optional Github access token. Can specify to reduce
168
168
  rate limiting.
169
169
  --no-strip do not strip downloaded binaries
170
+ --no-color do not use color in output
170
171
  --cert {system,certifi,none}
171
172
  specify which SSL certificates to use for HTTPS
172
173
  requests. Default="system".
@@ -185,7 +186,7 @@ Commands:
185
186
  cache Show size of release download caches.
186
187
 
187
188
  Some commands offer aliases as shown in parentheses above. Note you can set
188
- default starting global options in $HOME/.config/pystand-flags.conf.
189
+ default starting global options in ~/.config/pystand-flags.conf.
189
190
  ```
190
191
 
191
192
  Type `pystand <command> -h` to see specific help/usage for any
@@ -1,6 +1,6 @@
1
1
  .gitignore
2
- Makefile
3
2
  README.md
3
+ justfile
4
4
  pyproject.toml
5
5
  pystand.py
6
6
  pystand.egg-info/PKG-INFO
@@ -8,6 +8,7 @@ https://github.com/astral-sh/python-build-standalone.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import itertools
11
12
  import os
12
13
  import platform
13
14
  import re
@@ -17,6 +18,7 @@ import sys
17
18
  import time
18
19
  from collections import defaultdict
19
20
  from collections.abc import Iterable, Iterator
21
+ from dataclasses import dataclass, field
20
22
  from datetime import date, datetime
21
23
  from pathlib import Path
22
24
  from typing import Any
@@ -55,6 +57,29 @@ DISTRIBUTIONS = {
55
57
 
56
58
  CERTS = ('system', 'certifi', 'none')
57
59
 
60
+ # Define ANSI escape sequences for colors
61
+ # Refer https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
62
+ COLORS = [
63
+ '\033[32m', # green
64
+ '\033[33m', # yellow
65
+ '\033[35m', # magenta
66
+ '\033[34m', # blue
67
+ '\033[36m', # cyan
68
+ '\033[31m', # red
69
+ '\033[37m', # white
70
+ '\033[39m', # reset to default color
71
+ ]
72
+
73
+
74
+ @dataclass
75
+ class ColorTable:
76
+ next: Iterator[str] = field(default_factory=lambda: itertools.cycle(COLORS[1:-1]))
77
+ table: dict[str, str] = field(default_factory=dict)
78
+
79
+
80
+ colors_rel = ColorTable()
81
+ colors_dist = ColorTable()
82
+
58
83
 
59
84
  def is_admin() -> bool:
60
85
  "Check if we are running as root"
@@ -78,11 +103,36 @@ def get_version() -> str:
78
103
  return ver
79
104
 
80
105
 
81
- def fmt(version, release) -> str:
106
+ def fmtrel(version: str, release: str, args: Namespace) -> str:
82
107
  "Return a formatted release version string"
108
+ if not args.no_color:
109
+ if release == args._release:
110
+ color = COLORS[0]
111
+ elif not (color := colors_rel.table.get(release)):
112
+ # Assign a new color for this release by cycling through the
113
+ # available colors.
114
+ colors_rel.table[release] = color = next(colors_rel.next)
115
+
116
+ release = f'{color}{release}{COLORS[-1]}'
117
+
83
118
  return f'{version} @ {release}'
84
119
 
85
120
 
121
+ def fmtdist(dist: str, args: Namespace) -> str:
122
+ "Return a formatted distribution string"
123
+ if not args.no_color:
124
+ if dist == args._distribution:
125
+ color = COLORS[0]
126
+ elif not (color := colors_dist.table.get(dist)):
127
+ # Assign a new color for this distribution by cycling through the
128
+ # available colors.
129
+ colors_dist.table[dist] = color = next(colors_dist.next)
130
+
131
+ dist = f'{color}{dist}{COLORS[-1]}'
132
+
133
+ return f'distribution="{dist}"'
134
+
135
+
86
136
  def get_json(file: Path) -> dict[str, Any]:
87
137
  from json import load
88
138
 
@@ -146,14 +196,14 @@ def rm_path(path: Path) -> None:
146
196
 
147
197
  def register_zst() -> None:
148
198
  "Register custom zstandard unpacker"
149
- def unpack_zst(filename: str, extract_dir: str) -> None:
150
- "Unpack a zstandard compressed tar"
151
- import tarfile
199
+ import tarfile
152
200
 
153
- import zstandard
201
+ from zstandard import ZstdDecompressor
154
202
 
203
+ def unpack_zst(filename: str, extract_dir: str) -> None:
204
+ "Unpack a zstandard compressed tar"
155
205
  with open(filename, 'rb') as compressed:
156
- dctx = zstandard.ZstdDecompressor()
206
+ dctx = ZstdDecompressor()
157
207
  with dctx.stream_reader(compressed) as reader:
158
208
  with tarfile.open(fileobj=reader, mode='r|') as tar:
159
209
  tar.extractall(path=extract_dir)
@@ -348,7 +398,7 @@ def fetch_tag_latest(args: Namespace) -> str:
348
398
 
349
399
  def get_release_tag(args: Namespace) -> str:
350
400
  "Return the release tag, or latest if not specified"
351
- if release := args.release:
401
+ if hasattr(args, 'release') and (release := args.release):
352
402
  if err := check_release_tag(release):
353
403
  sys.exit(err)
354
404
 
@@ -507,7 +557,7 @@ def purge_unused_releases(args: Namespace) -> None:
507
557
 
508
558
  def show_list(args: Namespace) -> None:
509
559
  "Show a list of available releases"
510
- latest = parse_version(get_release_tag(args))
560
+ latest = parse_version(args._release)
511
561
  releases = {r: d for r, d in fetch_tags(args)}
512
562
  cached = set(p.name for p in args._releases.iterdir())
513
563
  for release in sorted(cached.union(releases)):
@@ -723,6 +773,9 @@ def main() -> str | None:
723
773
  opt.add_argument(
724
774
  '--no-strip', action='store_true', help='do not strip downloaded binaries'
725
775
  )
776
+ opt.add_argument(
777
+ '--no-color', action='store_true', help='do not use color in output'
778
+ )
726
779
  opt.add_argument(
727
780
  '--cert',
728
781
  choices=CERTS,
@@ -775,6 +828,10 @@ def main() -> str | None:
775
828
  if not distribution:
776
829
  return 'Unknown system + machine distribution. Please specify using -D/--distribution option.'
777
830
 
831
+ # Only use color for terminal output
832
+ if not sys.stdout.isatty():
833
+ args.no_color = True
834
+
778
835
  # Keep some useful info in the namespace passed to the command
779
836
  prefix_dir = Path(args.prefix_dir).expanduser().resolve()
780
837
  cache_dir = Path(args.cache_dir).expanduser().resolve()
@@ -798,6 +855,7 @@ def main() -> str | None:
798
855
 
799
856
  try:
800
857
  with p_lock.acquire(blocking=False), c_lock.acquire(blocking=False):
858
+ args._release = get_release_tag(args)
801
859
  result = args.func(args)
802
860
  purge_unused_releases(args)
803
861
  update_version_symlinks(args)
@@ -838,7 +896,7 @@ class install_:
838
896
 
839
897
  @staticmethod
840
898
  def run(args: Namespace) -> str | None:
841
- release = get_release_tag(args)
899
+ release = args._release
842
900
  files = get_release_files(args, release)
843
901
  if not files:
844
902
  return f'Release "{release}" not found, or has no compatible files.'
@@ -847,7 +905,7 @@ class install_:
847
905
  for version in args.version:
848
906
  full_version = matcher.match(version)
849
907
  if not full_version:
850
- return f'Version {fmt(version, release)} not found.'
908
+ return f'Version {fmtrel(version, release, args)} not found.'
851
909
 
852
910
  version = full_version
853
911
  vdir = args._versions / version
@@ -858,7 +916,7 @@ class install_:
858
916
  if error := install(args, vdir, release, args._distribution, files):
859
917
  return error
860
918
 
861
- print(f'Version {fmt(version, release)} installed.')
919
+ print(f'Version {fmtrel(version, release, args)} installed.')
862
920
 
863
921
 
864
922
  # COMMAND
@@ -897,7 +955,7 @@ class update_:
897
955
 
898
956
  @staticmethod
899
957
  def run(args: Namespace) -> str | None:
900
- release_target = get_release_tag(args)
958
+ release_target = args._release
901
959
  files = get_release_files(args, release_target)
902
960
  if not files:
903
961
  return f'Release "{release_target}" not found.'
@@ -918,23 +976,23 @@ class update_:
918
976
  if not distribution or distribution not in files.get(nextver, {}):
919
977
  continue
920
978
 
921
- if nextver == version and args.keep:
979
+ if nextver == version and args.keep and release:
922
980
  print(
923
- f'Error: {fmt(version, release)} would not be kept '
924
- f'if update to {fmt(nextver, release_target)} '
925
- f'distribution="{distribution}"',
981
+ f'Error: {fmtrel(version, release, args)} would not be kept '
982
+ f'if update to {fmtrel(nextver, release_target, args)} '
983
+ f'{fmtdist(distribution, args)}',
926
984
  file=sys.stderr,
927
985
  )
928
986
  continue
929
987
 
930
988
  new_vdir = args._versions / nextver
931
- if nextver != version and new_vdir.exists():
989
+ if not release or (nextver != version and new_vdir.exists()):
932
990
  continue
933
991
 
934
992
  print(
935
- f'{fmt(version, release)} updating to '
936
- f'{fmt(nextver, release_target)} '
937
- f'distribution="{distribution}" ..'
993
+ f'{fmtrel(version, release, args)} updating to '
994
+ f'{fmtrel(nextver, release_target, args)} '
995
+ f'{fmtdist(distribution, args)} ..'
938
996
  )
939
997
 
940
998
  # If the source was originally included, then include it in
@@ -986,7 +1044,7 @@ class remove_:
986
1044
  release = get_json(dfile).get('release') or '?'
987
1045
  if not release_del or release == release_del:
988
1046
  remove(args, version)
989
- print(f'Version {fmt(version, release)} removed.')
1047
+ print(f'Version {fmtrel(version, release, args)} removed.')
990
1048
 
991
1049
 
992
1050
  # COMMAND
@@ -1014,7 +1072,7 @@ class list_:
1014
1072
 
1015
1073
  @staticmethod
1016
1074
  def run(args: Namespace) -> str | None:
1017
- release_target = get_release_tag(args)
1075
+ release_target = args._release
1018
1076
  files = get_release_files(args, release_target)
1019
1077
  if not files:
1020
1078
  return f'Release "{release_target}" not found.'
@@ -1028,12 +1086,12 @@ class list_:
1028
1086
  continue
1029
1087
 
1030
1088
  release = data.get('release')
1031
- distribution = data.get('distribution')
1089
+ if not (distribution := data.get('distribution')):
1090
+ distribution = '?'
1032
1091
  upd = ''
1033
1092
  app = ''
1034
1093
  if release_target and release != release_target:
1035
- nextver = matcher.match(version, upgrade=True)
1036
- if not nextver:
1094
+ if not (nextver := matcher.match(version, upgrade=True)):
1037
1095
  if args.verbose:
1038
1096
  app = (
1039
1097
  ' not eligible for update because '
@@ -1049,23 +1107,28 @@ class list_:
1049
1107
  )
1050
1108
  app = (
1051
1109
  f' not eligible for '
1052
- f'update because {fmt(nextver, nrelease)} '
1110
+ f'update because {fmtrel(nextver, nrelease, args)} '
1053
1111
  'is already installed.'
1054
1112
  )
1055
1113
  else:
1056
1114
  # May not be updatable if newer release does not support
1057
1115
  # this same distribution anymore
1058
1116
  if nextver and distribution in files.get(nextver, {}):
1059
- upd = f' updatable to {fmt(nextver, release_target)}'
1117
+ upd = (
1118
+ f' updatable to {fmtrel(nextver, release_target, args)}'
1119
+ )
1060
1120
  elif args.verbose:
1061
1121
  app = (
1062
1122
  ' not eligible for update because '
1063
- f'{fmt(nextver, release_target)} does '
1064
- 'not provide '
1065
- f'distribution="{distribution}".'
1123
+ f'{fmtrel(nextver, release_target, args)} does '
1124
+ f'not provide {fmtdist(distribution, args)}.'
1066
1125
  )
1067
1126
 
1068
- print(f'{fmt(version, release)}{upd} distribution="{distribution}"{app}')
1127
+ if release:
1128
+ print(
1129
+ f'{fmtrel(version, release, args)}{upd} '
1130
+ f'{fmtdist(distribution, args)}{app}'
1131
+ )
1069
1132
 
1070
1133
 
1071
1134
  # COMMAND
@@ -1112,7 +1175,7 @@ class show_:
1112
1175
  show_list(args)
1113
1176
  return None
1114
1177
 
1115
- release = get_release_tag(args)
1178
+ release = args._release
1116
1179
  files = get_release_files(args, release)
1117
1180
  if not files:
1118
1181
  return f'Error: release "{release}" not found.'
@@ -1136,14 +1199,13 @@ class show_:
1136
1199
  args.re_match, f'{version}+{distribution}'
1137
1200
  ):
1138
1201
  print(
1139
- f'{fmt(version, release)} '
1140
- f'distribution="{distribution}"{app}'
1202
+ f'{fmtrel(version, release, args)} '
1203
+ f'{fmtdist(distribution, args)}{app}'
1141
1204
  )
1142
1205
  if not installable:
1143
1206
  print(
1144
- f'Warning: no distribution="{args._distribution}" '
1145
- 'versions found in '
1146
- f'release "{release}".'
1207
+ f'Warning: no {fmtdist(args._distribution, args)} '
1208
+ f'versions found in release "{release}".'
1147
1209
  )
1148
1210
 
1149
1211
 
pystand-2.20/Makefile DELETED
@@ -1,23 +0,0 @@
1
- PYFILES = $(wildcard *.py)
2
- check:
3
- ruff check $(PYFILES)
4
- mypy $(PYFILES)
5
- pyright $(PYFILES)
6
- vermin -vv --no-tips -i $(PYFILES)
7
- md-link-checker
8
-
9
- build:
10
- rm -rf dist
11
- uv build
12
-
13
- upload: build
14
- uv-publish
15
-
16
- doc:
17
- update-readme-usage -A
18
-
19
- format:
20
- ruff check --select I --fix $(PYFILES) && ruff format $(PYFILES)
21
-
22
- clean:
23
- @rm -vrf *.egg-info build/ dist/ __pycache__
File without changes
File without changes
File without changes