pbs-installer 2024.3.22__py3-none-any.whl → 2024.3.23__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.
pbs_installer/__main__.py CHANGED
@@ -50,10 +50,15 @@ def main():
50
50
  archs, platforms = get_available_arch_platforms()
51
51
  parser = ArgumentParser("pbs-install", description="Installer for Python Build Standalone")
52
52
  install_group = parser.add_argument_group("Install Arguments")
53
- install_group.add_argument("version", help="The version of Python to install, e.g. 3.8,3.10.4")
53
+ install_group.add_argument(
54
+ "version", help="The version of Python to install, e.g. 3.8, 3.10.4, pypy@3.10"
55
+ )
54
56
  install_group.add_argument(
55
57
  "--version-dir", help="Install to a subdirectory named by the version", action="store_true"
56
58
  )
59
+ install_group.add_argument(
60
+ "--build-dir", help="Include the build directory", action="store_true"
61
+ )
57
62
  install_group.add_argument(
58
63
  "-d", "--destination", help="The directory to install to", required=True
59
64
  )
@@ -66,12 +71,17 @@ def main():
66
71
 
67
72
  args = parser.parse_args()
68
73
  _setup_logger(args.verbose)
74
+ impl, has_amp, version = args.version.rpartition("@")
75
+ if not has_amp:
76
+ impl = "cpython"
69
77
  install(
70
- args.version,
78
+ version,
71
79
  args.destination,
72
80
  version_dir=args.version_dir,
73
81
  arch=args.arch,
74
82
  platform=args.platform,
83
+ implementation=impl,
84
+ build_dir=args.build_dir,
75
85
  )
76
86
  print("Done!")
77
87
 
pbs_installer/_install.py CHANGED
@@ -4,18 +4,18 @@ import hashlib
4
4
  import logging
5
5
  import os
6
6
  import tempfile
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Tuple
8
8
  from urllib.parse import unquote
9
9
 
10
- from ._utils import PythonVersion, get_arch_platform, unpack_tar
11
-
12
- logger = logging.getLogger(__name__)
10
+ from ._utils import PythonVersion, get_arch_platform
13
11
 
14
12
  if TYPE_CHECKING:
15
13
  import httpx
16
14
  from _typeshed import StrPath
17
15
 
16
+ logger = logging.getLogger(__name__)
18
17
  THIS_ARCH, THIS_PLATFORM = get_arch_platform()
18
+ PythonFile = Tuple[str, str | None]
19
19
 
20
20
 
21
21
  def _get_headers() -> dict[str, str] | None:
@@ -29,14 +29,18 @@ def _get_headers() -> dict[str, str] | None:
29
29
 
30
30
 
31
31
  def get_download_link(
32
- request: str, arch: str | None = None, platform: str | None = None
33
- ) -> tuple[PythonVersion, str]:
32
+ request: str,
33
+ arch: str = THIS_ARCH,
34
+ platform: str = THIS_PLATFORM,
35
+ implementation: str = "cpython",
36
+ ) -> tuple[PythonVersion, PythonFile]:
34
37
  """Get the download URL matching the given requested version.
35
38
 
36
39
  Parameters:
37
40
  request: The version of Python to install, e.g. 3.8,3.10.4
38
41
  arch: The architecture to install, e.g. x86_64, arm64
39
42
  platform: The platform to install, e.g. linux, macos
43
+ implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
40
44
 
41
45
  Returns:
42
46
  A tuple of the PythonVersion and the download URL
@@ -48,47 +52,35 @@ def get_download_link(
48
52
  """
49
53
  from ._versions import PYTHON_VERSIONS
50
54
 
51
- if arch is None:
52
- arch = THIS_ARCH
53
- if platform is None:
54
- platform = THIS_PLATFORM
55
-
56
55
  for py_ver, urls in PYTHON_VERSIONS.items():
57
- if not py_ver.matches(request):
56
+ if not py_ver.matches(request, implementation):
58
57
  continue
59
58
 
60
- for iarch, iplatform, url in urls:
61
- logger.debug(
62
- "Checking %s %s with requested system %s %s", iarch, iplatform, arch, platform
63
- )
64
- if (iarch, iplatform) == (arch, platform):
65
- return py_ver, url
66
- break
67
- raise ValueError(f"Could not find a CPython {request!r} matching this system")
68
-
69
-
70
- def _read_sha256(url: str, client: httpx.Client) -> str | None:
71
- resp = client.get(url + ".sha256", headers=_get_headers())
72
- if not resp.is_success:
73
- logger.warning("No checksum found for %s, this would be insecure", url)
74
- return None
75
- return resp.text.strip()
59
+ matched = urls.get((platform, arch))
60
+ if matched is not None:
61
+ return py_ver, matched
62
+ raise ValueError(
63
+ f"Could not find a version matching version={request!r}, implementation={implementation}"
64
+ )
76
65
 
77
66
 
78
- def download(url: str, destination: StrPath, client: httpx.Client | None = None) -> str:
67
+ def download(
68
+ python_file: PythonFile, destination: StrPath, client: httpx.Client | None = None
69
+ ) -> str:
79
70
  """Download the given url to the destination.
80
71
 
81
72
  Note: Extras required
82
73
  `pbs-installer[download]` must be installed to use this function.
83
74
 
84
75
  Parameters:
85
- url: The url to download
76
+ python_file: The (url, checksum) tuple to download
86
77
  destination: The file path to download to
87
78
  client: A http.Client to use for downloading, or None to create a new one
88
79
 
89
80
  Returns:
90
81
  The original filename of the downloaded file
91
82
  """
83
+ url, checksum = python_file
92
84
  logger.debug("Downloading url %s to %s", url, destination)
93
85
  try:
94
86
  import httpx
@@ -100,9 +92,13 @@ def download(url: str, destination: StrPath, client: httpx.Client | None = None)
100
92
 
101
93
  filename = unquote(url.rsplit("/")[-1])
102
94
  hasher = hashlib.sha256()
103
- checksum = _read_sha256(url, client)
95
+ if not checksum:
96
+ logger.warning("No checksum found for %s, this would be insecure", url)
104
97
 
105
- with open(destination, "wb") as f, client.stream("GET", url, headers=_get_headers()) as resp:
98
+ with (
99
+ open(destination, "wb") as f,
100
+ client.stream("GET", url, headers=_get_headers()) as resp,
101
+ ):
106
102
  resp.raise_for_status()
107
103
  for chunk in resp.iter_bytes(chunk_size=8192):
108
104
  if checksum:
@@ -115,7 +111,10 @@ def download(url: str, destination: StrPath, client: httpx.Client | None = None)
115
111
 
116
112
 
117
113
  def install_file(
118
- filename: StrPath, destination: StrPath, original_filename: str | None = None
114
+ filename: StrPath,
115
+ destination: StrPath,
116
+ original_filename: str | None = None,
117
+ build_dir: bool = False,
119
118
  ) -> None:
120
119
  """Unpack the downloaded file to the destination.
121
120
 
@@ -126,11 +125,10 @@ def install_file(
126
125
  filename: The file to unpack
127
126
  destination: The directory to unpack to
128
127
  original_filename: The original filename of the file, if it was renamed
128
+ build_dir: Whether to include the `build/` directory from indygreg builds
129
129
  """
130
130
 
131
- import tarfile
132
-
133
- import zstandard as zstd
131
+ from ._utils import unpack_tar, unpack_zip, unpack_zst
134
132
 
135
133
  if original_filename is None:
136
134
  original_filename = str(filename)
@@ -141,17 +139,11 @@ def install_file(
141
139
  original_filename,
142
140
  )
143
141
  if original_filename.endswith(".zst"):
144
- dctx = zstd.ZstdDecompressor()
145
- with tempfile.TemporaryFile(suffix=".tar") as ofh:
146
- with open(filename, "rb") as ifh:
147
- dctx.copy_stream(ifh, ofh)
148
- ofh.seek(0)
149
- with tarfile.open(fileobj=ofh) as z:
150
- unpack_tar(z, destination, 1)
151
-
142
+ unpack_zst(filename, destination, build_dir)
143
+ elif original_filename.endswith(".zip"):
144
+ unpack_zip(filename, destination, build_dir)
152
145
  else:
153
- with tarfile.open(filename) as z:
154
- unpack_tar(z, destination, 1)
146
+ unpack_tar(filename, destination, build_dir)
155
147
 
156
148
 
157
149
  def install(
@@ -161,6 +153,8 @@ def install(
161
153
  client: httpx.Client | None = None,
162
154
  arch: str | None = None,
163
155
  platform: str | None = None,
156
+ implementation: str = "cpython",
157
+ build_dir: bool = False,
164
158
  ) -> None:
165
159
  """Download and install the requested python version.
166
160
 
@@ -174,6 +168,8 @@ def install(
174
168
  client: A httpx.Client to use for downloading
175
169
  arch: The architecture to install, e.g. x86_64, arm64
176
170
  platform: The platform to install, e.g. linux, macos
171
+ implementation: The implementation of Python to install, allowed values are 'cpython' and 'pypy'
172
+ build_dir: Whether to include the `build/` directory from indygreg builds
177
173
 
178
174
  Examples:
179
175
  >>> install("3.10", "./python")
@@ -181,12 +177,19 @@ def install(
181
177
  >>> install("3.10", "./python", version_dir=True)
182
178
  Installing cpython@3.10.4 to ./python/cpython@3.10.4
183
179
  """
184
- ver, url = get_download_link(request, arch=arch, platform=platform)
180
+ if platform is None:
181
+ platform = THIS_PLATFORM
182
+ if arch is None:
183
+ arch = THIS_ARCH
184
+
185
+ ver, python_file = get_download_link(
186
+ request, arch=arch, platform=platform, implementation=implementation
187
+ )
185
188
  if version_dir:
186
189
  destination = os.path.join(destination, str(ver))
187
190
  logger.debug("Installing %s to %s", ver, destination)
188
191
  os.makedirs(destination, exist_ok=True)
189
192
  with tempfile.NamedTemporaryFile() as tf:
190
193
  tf.close()
191
- original_filename = download(url, tf.name, client)
192
- install_file(tf.name, destination, original_filename)
194
+ original_filename = download(python_file, tf.name, client)
195
+ install_file(tf.name, destination, original_filename, build_dir)
pbs_installer/_utils.py CHANGED
@@ -7,21 +7,25 @@ if TYPE_CHECKING:
7
7
  from _typeshed import StrPath
8
8
 
9
9
  ARCH_MAPPING = {
10
- "aarch64": "arm64",
10
+ "arm64": "aarch64",
11
11
  "amd64": "x86_64",
12
+ "i686": "x86",
12
13
  }
14
+ PLATFORM_MAPPING = {"darwin": "macos"}
13
15
 
14
16
 
15
17
  class PythonVersion(NamedTuple):
16
- kind: str
18
+ implementation: str
17
19
  major: int
18
20
  minor: int
19
21
  micro: int
20
22
 
21
23
  def __str__(self) -> str:
22
- return f"{self.kind}@{self.major}.{self.minor}.{self.micro}"
24
+ return f"{self.implementation}@{self.major}.{self.minor}.{self.micro}"
23
25
 
24
- def matches(self, request: str) -> bool:
26
+ def matches(self, request: str, implementation: str) -> bool:
27
+ if implementation != self.implementation:
28
+ return False
25
29
  try:
26
30
  parts = tuple(int(v) for v in request.split("."))
27
31
  except ValueError:
@@ -46,17 +50,68 @@ def get_arch_platform() -> tuple[str, str]:
46
50
 
47
51
  plat = platform.system().lower()
48
52
  arch = platform.machine().lower()
49
- return ARCH_MAPPING.get(arch, arch), plat
53
+ return ARCH_MAPPING.get(arch, arch), PLATFORM_MAPPING.get(plat, plat)
50
54
 
51
55
 
52
- def unpack_tar(tf: tarfile.TarFile, destination: StrPath, skip_parts: int = 0) -> None:
56
+ def _unpack_tar(tf: tarfile.TarFile, destination: StrPath, build_dir: bool = False) -> None:
53
57
  """Unpack the tarfile to the destination, with the first skip_parts parts of the path removed"""
58
+ members: list[tarfile.TarInfo] = []
59
+ has_build = any(
60
+ (p := fn.lstrip("/").split("/")) and len(p) > 1 and p[1] == "build" for fn in tf.getnames()
61
+ )
54
62
  for member in tf.getmembers():
55
- fn = member.name.lstrip("/")
56
- parts = fn.split("/")
57
- fn = "/".join(parts[skip_parts:])
58
- member.name = fn
59
- tf.extract(member, destination)
63
+ parts = member.name.lstrip("/").split("/")
64
+ if build_dir or not has_build:
65
+ member.name = "/".join(parts[1:])
66
+ elif len(parts) > 1 and parts[1] == "install":
67
+ member.name = "/".join(parts[2:])
68
+ else:
69
+ continue
70
+ if member.name:
71
+ members.append(member)
72
+ tf.extractall(destination, members=members)
73
+
74
+
75
+ def unpack_tar(filename: str, destination: StrPath, build_dir: bool = False) -> None:
76
+ """Unpack the tarfile to the destination"""
77
+ with tarfile.open(filename) as z:
78
+ _unpack_tar(z, destination, build_dir=build_dir)
79
+
80
+
81
+ def unpack_zst(filename: str, destination: StrPath, build_dir: bool = False) -> None:
82
+ """Unpack the zstd compressed tarfile to the destination"""
83
+ import tempfile
84
+
85
+ import zstandard as zstd
86
+
87
+ dctx = zstd.ZstdDecompressor()
88
+ with tempfile.TemporaryFile(suffix=".tar") as ofh:
89
+ with open(filename, "rb") as ifh:
90
+ dctx.copy_stream(ifh, ofh)
91
+ ofh.seek(0)
92
+ with tarfile.open(fileobj=ofh) as z:
93
+ _unpack_tar(z, destination, build_dir=build_dir)
94
+
95
+
96
+ def unpack_zip(filename: str, destination: StrPath, build_dir: bool = False) -> None:
97
+ """Unpack the zip file to the destination"""
98
+ import zipfile
99
+
100
+ with zipfile.ZipFile(filename) as z:
101
+ members: list[zipfile.ZipInfo] = []
102
+ has_build = any(fn.lstrip("/").split("/")[1] == "build" for fn in z.namelist())
103
+ for member in z.infolist():
104
+ parts = member.filename.lstrip("/").split("/")
105
+ if (build_dir or not has_build) and len(parts) > 1:
106
+ member.filename = "/".join(parts[1:])
107
+ elif len(parts) > 1 and parts[1] == "install":
108
+ member.filename = "/".join(parts[2:])
109
+ else:
110
+ continue
111
+ if member.filename:
112
+ members.append(member)
113
+
114
+ z.extractall(destination, members=members)
60
115
 
61
116
 
62
117
  def get_available_arch_platforms() -> tuple[list[str], list[str]]:
@@ -66,6 +121,6 @@ def get_available_arch_platforms() -> tuple[list[str], list[str]]:
66
121
  platforms: set[str] = set()
67
122
  for items in PYTHON_VERSIONS.values():
68
123
  for item in items:
69
- archs.add(item[0])
70
- platforms.add(item[1])
124
+ platforms.add(item[0])
125
+ archs.add(item[1])
71
126
  return sorted(archs), sorted(platforms)