pystand 1.8__py3-none-any.whl → 1.10__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.8
3
+ Version: 1.10
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
@@ -86,14 +86,17 @@ $ pystand list
86
86
  Here are some examples showing how to use an installed version ..
87
87
 
88
88
  ```sh
89
- # Use uv to create a virtual environment to be run with pystand installed python:
90
- $ uv venv -p $(pystand path 3.12) myenv
89
+ # Use uv to create a virtual environment to be run with latest pystand
90
+ # installed python:
91
+ $ uv venv -p $(pystand path) myenv
91
92
 
92
- # Create a regular virtual environment to be run with pystand installed python:
93
- $ $(pystand path -p 3.12) -m venv myenv
93
+ # Create a regular virtual environment to be run with latest pystand
94
+ # installed python:
95
+ $ $(pystand path -p) -m venv myenv
94
96
 
95
- # Use pipx to install a package to be run with pystand installed python:
96
- $ pipx install --python $(pystand path -p 3.12) cowsay
97
+ # Use pipx to install a package to be run with pystand installed python
98
+ # specific version:
99
+ $ pipx install --python $(pystand path -p 3.11) cowsay
97
100
  ```
98
101
 
99
102
  See detailed usage information in the [Usage](#usage) section that
@@ -255,29 +258,30 @@ options:
255
258
  ### Command `show`
256
259
 
257
260
  ```
258
- usage: pystand show [-h] [-d] [release]
261
+ usage: pystand show [-h] [-D] [release]
259
262
 
260
263
  Show versions available from a release.
261
264
 
262
265
  positional arguments:
263
- release python-build-standalone YYYYMMDD release to show (e.g.
264
- 20240415), default is latest release
266
+ release python-build-standalone YYYYMMDD release to show (e.g.
267
+ 20240415), default is latest release
265
268
 
266
269
  options:
267
- -h, --help show this help message and exit
268
- -d, --distributions also show all available distributions for each version
269
- from the release
270
+ -h, --help show this help message and exit
271
+ -D, --distribution also show all available distributions for each version
272
+ from the release
270
273
  ```
271
274
 
272
275
  ### Command `path`
273
276
 
274
277
  ```
275
- usage: pystand path [-h] [-p] version
278
+ usage: pystand path [-h] [-p] [version]
276
279
 
277
280
  Show path prefix to installed version base directory.
278
281
 
279
282
  positional arguments:
280
- version version to return path for
283
+ version version to return path for, or latest release version if
284
+ not specified
281
285
 
282
286
  options:
283
287
  -h, --help show this help message and exit
@@ -291,7 +295,7 @@ from the AUR](https://aur.archlinux.org/packages/pystand) and skip this
291
295
  section.
292
296
 
293
297
  The easiest way to install [`pystand`][pystand] is to use [`pipx`][pipx]
294
- (or [`pipxu`][pipxu]).
298
+ (or [`pipxu`][pipxu], or [`uv tool`][uvtool]).
295
299
 
296
300
  ```sh
297
301
  $ pipx install pystand
@@ -349,6 +353,18 @@ The global options: `--distribution`, `--base-dir`, `--cache-minutes`,
349
353
  `--no-extra-strip` are the only sensible candidates to consider setting
350
354
  as defaults.
351
355
 
356
+ ## Github API Rate Limiting
357
+
358
+ This tool minimises and caches Github API responses from the
359
+ [`python-build-standalone`][pbs] repository. However, if you install
360
+ many different versions particularly across various releases, you may
361
+ get rate limited by Github so the command will block and you will see
362
+ "backoff" messages reported. You can create a Github access token to
363
+ gain increased rate limits. Create a token in your Github account under
364
+ `Settings -> Developer settings -> Personal access tokens`. Specify the
365
+ token on the command line with `--github-access-token`, or set that as a
366
+ [default option](#command-default-options).
367
+
352
368
  ## Command Line Tab Completion
353
369
 
354
370
  Command line shell [tab
@@ -375,6 +391,7 @@ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at
375
391
  [pbs-rel]: https://github.com/indygreg/python-build-standalone/releases
376
392
  [pipx]: https://github.com/pypa/pipx
377
393
  [pipxu]: https://github.com/bulletmark/pipxu
394
+ [uvtool]: https://docs.astral.sh/uv/guides/tools/#installing-tools
378
395
  [pyenv]: https://github.com/pyenv/pyenv
379
396
  [pdm]: https://pdm-project.org/
380
397
  [pdmpy]: https://pdm-project.org/en/latest/usage/project/#install-python-interpreters-with-pdm
@@ -0,0 +1,6 @@
1
+ pystand.py,sha256=KJbvxHMzi28M4HqgGS9A4YaGYrpDn-T4hhuaQqbGW2E,31942
2
+ pystand-1.10.dist-info/METADATA,sha256=f_l5yws3MCMJicGVN-o40gj1RlehBZ_Hg9jeYxLzLb4,15916
3
+ pystand-1.10.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
4
+ pystand-1.10.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
5
+ pystand-1.10.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
6
+ pystand-1.10.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pystand.py CHANGED
@@ -21,7 +21,7 @@ from argparse import ArgumentParser, Namespace
21
21
  from collections import defaultdict
22
22
  from datetime import date
23
23
  from pathlib import Path
24
- from typing import Any, Iterable, Iterator, Optional
24
+ from typing import Any, Iterable, Iterator
25
25
 
26
26
  import argcomplete
27
27
  import platformdirs
@@ -92,7 +92,7 @@ def get_json(file: Path) -> dict:
92
92
 
93
93
  return {}
94
94
 
95
- def set_json(file: Path, data: dict) -> Optional[str]:
95
+ def set_json(file: Path, data: dict) -> str | None:
96
96
  'Set JSON data to given file'
97
97
  try:
98
98
  with file.open('w') as fp:
@@ -132,26 +132,47 @@ def rm_path(path: Path) -> None:
132
132
  elif path.exists():
133
133
  path.unlink()
134
134
 
135
+ def is_release_version(version: str) -> bool:
136
+ 'Check if a string is a formal Python release tag'
137
+ return version.replace('.', '').isdigit()
138
+
135
139
  class VersionMatcher:
136
140
  'Match a version string to a list of versions'
137
141
  def __init__(self, seq: Iterable[str]) -> None:
138
142
  self.seq = sorted(seq, key=parse_version, reverse=True)
139
143
 
140
- def match(self, version: str, *,
141
- upconvert_minor: bool = False) -> Optional[str]:
144
+ def match(self, version: str | None, *,
145
+ upgrade: bool = False) -> str | None:
142
146
  'Return full version string given a [possibly] part version prefix'
147
+
148
+ # If no version specified, return the latest release version
149
+ if not version:
150
+ for version in self.seq:
151
+ if is_release_version(version):
152
+ return version
153
+ return None
154
+
143
155
  if version in self.seq:
144
156
  return version
145
157
 
146
- if upconvert_minor:
158
+ is_release = is_release_version(version)
159
+ major_only = '.' not in version
160
+
161
+ if major_only:
162
+ upgrade = True
163
+ elif upgrade:
147
164
  version = version.rsplit('.', 1)[0]
148
165
 
149
166
  if not version.endswith('.'):
150
167
  version += '.'
151
168
 
169
+ # Only allow upgrade of formal release to another formal
170
+ # release, or alpha/beta release to another alpha/beta release.
152
171
  for full_version in self.seq:
153
172
  if full_version.startswith(version):
154
- return full_version
173
+ if not upgrade \
174
+ or is_release_version(full_version) == is_release:
175
+ return full_version
155
176
 
156
177
  return None
157
178
 
@@ -191,7 +212,7 @@ def get_version_names(args: Namespace) -> list[str]:
191
212
  if args.all else versions
192
213
 
193
214
  def check_release_tag(release: str, *,
194
- check_first: bool = True) -> Optional[str]:
215
+ check_first: bool = True) -> str | None:
195
216
  'Check the specified release tag is valid'
196
217
  if not release.isdigit() or len(release) != len(FIRST_RELEASE):
197
218
  return 'Release must be a YYYYMMDD string.'
@@ -253,7 +274,7 @@ def merge(files: dict, name: str, url: str, stripped: bool) -> None:
253
274
  else:
254
275
  files[impl][ver][distrib] = ('', url) if stripped else url
255
276
 
256
- def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
277
+ def get_release_files(args, tag, implementation: str | None = None) -> dict:
257
278
  'Return the release files for the given tag'
258
279
  # Look for tag data in our release cache
259
280
  jfile = args._releases / tag
@@ -295,7 +316,7 @@ def update_version_symlinks(args: Namespace) -> None:
295
316
  if not base.exists():
296
317
  return
297
318
 
298
- # Record of all the existing symlinks and version dirs
319
+ # Record all the existing symlinks and version dirs
299
320
  oldlinks = {}
300
321
  vers = []
301
322
  for path in base.iterdir():
@@ -303,19 +324,30 @@ def update_version_symlinks(args: Namespace) -> None:
303
324
  if path.is_symlink():
304
325
  oldlinks[path.name] = os.readlink(str(path))
305
326
  else:
306
- vers.append(path)
327
+ vers.append(path.name)
307
328
 
308
329
  # Create a map of all the new major version links
309
- newlinks_all = defaultdict(list)
310
- for path in vers:
311
- namevers = path.name
330
+ newlinks_all = defaultdict(set)
331
+ pre_releases = set(v for v in vers if not is_release_version(v))
332
+ for namevers in vers:
312
333
  while '.' in namevers[:-1]:
313
334
  namevers_major = namevers.rsplit('.', maxsplit=1)[0]
314
- newlinks_all[namevers_major].append(namevers)
335
+ newlinks_all[namevers_major].add(namevers)
336
+
337
+ if namevers in pre_releases:
338
+ pre_releases.add(namevers_major)
339
+
315
340
  namevers = namevers_major
316
341
 
317
- newlinks = {k: sorted(v, key=parse_version)[-1] for k, v in
318
- newlinks_all.items()}
342
+ # Find the latest version for each major version, but ensure we
343
+ # don't link a major release to pre-released version, if it also can
344
+ # point to released versions.
345
+ newlinks = {}
346
+ for ver, cands in newlinks_all.items():
347
+ if ver in pre_releases and (rels := (cands - pre_releases)):
348
+ cands = rels
349
+
350
+ newlinks[ver] = sorted(cands, key=parse_version)[-1]
319
351
 
320
352
  # Remove all old or invalid existing links
321
353
  for name, tgt in oldlinks.items():
@@ -403,7 +435,7 @@ def strip_binaries(vdir: Path, distribution: str) -> bool:
403
435
  return was_stripped
404
436
 
405
437
  def install(args: Namespace, vdir: Path, release: str, distribution: str,
406
- files: dict) -> Optional[str]:
438
+ files: dict) -> str | None:
407
439
  'Install a version'
408
440
  version = vdir.name
409
441
 
@@ -460,7 +492,7 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
460
492
  shutil.rmtree(tmpdir)
461
493
  return error
462
494
 
463
- def main() -> Optional[str]:
495
+ def main() -> str | None:
464
496
  'Main code'
465
497
  distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
466
498
  distro_help = distro_default or '?unknown?'
@@ -583,7 +615,7 @@ class _install(COMMAND):
583
615
  help='version to install. E.g. 3.12 or 3.12.3')
584
616
 
585
617
  @staticmethod
586
- def run(args: Namespace) -> Optional[str]:
618
+ def run(args: Namespace) -> str | None:
587
619
  release = get_release_tag(args)
588
620
  files = get_release_files(args, release, 'cpython')
589
621
  if not files:
@@ -627,7 +659,7 @@ class _update(COMMAND):
627
659
  '--all --skip)')
628
660
 
629
661
  @staticmethod
630
- def run(args: Namespace) -> Optional[str]:
662
+ def run(args: Namespace) -> str | None:
631
663
  release_target = get_release_tag(args)
632
664
  files = get_release_files(args, release_target, 'cpython')
633
665
  if not files:
@@ -642,7 +674,7 @@ class _update(COMMAND):
642
674
  if release == release_target:
643
675
  continue
644
676
 
645
- nextver = matcher.match(version, upconvert_minor=True)
677
+ nextver = matcher.match(version, upgrade=True)
646
678
 
647
679
  distribution = data.get('distribution')
648
680
  if not distribution or distribution not in files.get(nextver, {}):
@@ -687,7 +719,7 @@ class _remove(COMMAND):
687
719
  '--all --skip)')
688
720
 
689
721
  @staticmethod
690
- def run(args: Namespace) -> Optional[str]:
722
+ def run(args: Namespace) -> str | None:
691
723
  release_del = args.release
692
724
  if release_del and \
693
725
  (err := check_release_tag(release_del, check_first=False)):
@@ -716,7 +748,7 @@ class _list(COMMAND):
716
748
  help='only list specified version, else all')
717
749
 
718
750
  @staticmethod
719
- def run(args: Namespace) -> Optional[str]:
751
+ def run(args: Namespace) -> str | None:
720
752
  release_target = get_release_tag(args)
721
753
  files = get_release_files(args, release_target, 'cpython')
722
754
  if not files:
@@ -735,7 +767,7 @@ class _list(COMMAND):
735
767
  upd = ''
736
768
  app = ''
737
769
  if release_target and release != release_target:
738
- nextver = matcher.match(version, upconvert_minor=True)
770
+ nextver = matcher.match(version, upgrade=True)
739
771
  new_vdir = args._versions / nextver
740
772
  if nextver != version and new_vdir.exists():
741
773
  if args.verbose:
@@ -762,7 +794,7 @@ class _show(COMMAND):
762
794
  'Show versions available from a release.'
763
795
  @staticmethod
764
796
  def init(parser: ArgumentParser) -> None:
765
- parser.add_argument('-d', '--distributions', action='store_true',
797
+ parser.add_argument('-D', '--distribution', action='store_true',
766
798
  help='also show all available distributions for '
767
799
  'each version from the release')
768
800
  parser.add_argument('release', nargs='?',
@@ -789,7 +821,7 @@ class _show(COMMAND):
789
821
  for distribution in files[version]:
790
822
  app = ' (installed)' \
791
823
  if distribution == installed_distribution else ''
792
- if args.distributions or app \
824
+ if args.distribution or app \
793
825
  or distribution == args._distribution:
794
826
  if distribution == args._distribution:
795
827
  installable = True
@@ -807,10 +839,12 @@ class _path(COMMAND):
807
839
  def init(parser: ArgumentParser) -> None:
808
840
  parser.add_argument('-p', '--python-path', action='store_true',
809
841
  help='return full path to python executable')
810
- parser.add_argument('version', help='version to return path for')
842
+ parser.add_argument('version', nargs='?',
843
+ help='version to return path for, or latest '
844
+ 'release version if not specified')
811
845
 
812
846
  @staticmethod
813
- def run(args: Namespace) -> Optional[str]:
847
+ def run(args: Namespace) -> str | None:
814
848
  matcher = VersionMatcher([f.name for f in iter_versions(args)])
815
849
  version = matcher.match(args.version) or args.version
816
850
  path = args._versions / version
@@ -1,6 +0,0 @@
1
- pystand.py,sha256=0yqrw_cnYnidYkCOCP5rDy-UgDZv9klpFURHxFANC5I,30695
2
- pystand-1.8.dist-info/METADATA,sha256=KqUUnga3dbLKMyqmttZk-YLLt-10nVIoNvpDfTR7mcc,15105
3
- pystand-1.8.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
4
- pystand-1.8.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
5
- pystand-1.8.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
6
- pystand-1.8.dist-info/RECORD,,