pact-python-ffi 0.5.8.0__tar.gz → 0.5.12.0__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,3 +1,4 @@
1
+ src/pact_ffi/__version__.py
1
2
  src/pact_ffi/data
2
3
  src/pact_ffi/*.pyd
3
4
  src/pact_ffi/*.so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pact-python-ffi
3
- Version: 0.5.8.0
3
+ Version: 0.5.12.0
4
4
  Summary: Python bindings for the Pact FFI library
5
5
  Project-URL: Bug Tracker, https://github.com/pact-foundation/pact-python/issues
6
6
  Project-URL: Changelog, https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md
@@ -21,13 +21,13 @@ Classifier: Operating System :: Microsoft :: Windows
21
21
  Classifier: Operating System :: POSIX :: Linux
22
22
  Classifier: Programming Language :: Python
23
23
  Classifier: Programming Language :: Python :: 3 :: Only
24
- Classifier: Programming Language :: Python :: 3.10
25
24
  Classifier: Programming Language :: Python :: 3.11
26
25
  Classifier: Programming Language :: Python :: 3.12
27
26
  Classifier: Programming Language :: Python :: 3.13
28
27
  Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Programming Language :: Python :: 3.15
29
29
  Classifier: Topic :: Software Development :: Testing
30
- Requires-Python: >=3.10
30
+ Requires-Python: >=3.11
31
31
  Requires-Dist: cffi~=2.0
32
32
  Description-Content-Type: text/markdown
33
33
 
@@ -7,13 +7,15 @@ CFFI bindings for it.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import gzip
10
+ import hashlib
11
11
  import os
12
12
  import shutil
13
13
  import subprocess
14
14
  import sys
15
+ import tarfile
15
16
  import tempfile
16
17
  import urllib.request
18
+ import zipfile
17
19
  from pathlib import Path
18
20
  from typing import TYPE_CHECKING, Any
19
21
 
@@ -25,7 +27,7 @@ if TYPE_CHECKING:
25
27
  from collections.abc import Mapping, MutableMapping, Sequence
26
28
 
27
29
  PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi"
28
- PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{platform}{suffix}.{ext}"
30
+ PACT_RELEASE_URL = "https://github.com/pact-foundation/pact-reference/releases/download/pact_ffi-v{version}"
29
31
 
30
32
 
31
33
  class UnsupportedPlatformError(RuntimeError):
@@ -169,15 +171,30 @@ class PactBuildHook(BuildHookInterface[Any]):
169
171
  `dst` is the corresponding path within the wheel.
170
172
  """
171
173
  # Download the Pact library binary and header file
172
- lib_url = self._lib_url(version)
173
- header = self._download(lib_url.rsplit("/", 1)[0] + "/pact.h")
174
- lib = self._extract_lib(self._download(lib_url))
175
- if lib.suffix == ".dll":
176
- dll_lib = self._extract_lib(
177
- self._download(lib_url.replace(".dll.gz", ".dll.lib.gz"))
178
- )
174
+ release_url = PACT_RELEASE_URL.format(version=version)
175
+ target = self._target()
176
+ header = self._download(f"{release_url}/pact.h")
177
+ if "windows" in target:
178
+ archive_url = f"{release_url}/pact_ffi-{target}.zip"
179
+ else:
180
+ archive_url = f"{release_url}/pact_ffi-{target}.tar.xz"
181
+ archive = self._download(archive_url)
182
+ self._verify(archive, self._download(f"{archive_url}.sha256"))
183
+
184
+ dll_lib: Path | None = None
185
+ if "windows" in target:
186
+ lib = self._extract_lib(archive, "pact_ffi.dll", "pact_ffi.dll")
187
+ # MSVC resolves `pact_ffi` to `pact_ffi.lib`. The archive's own
188
+ # `pact_ffi.lib` is the static library, so the import library
189
+ # takes its name.
190
+ dll_lib = self._extract_lib(archive, "pact_ffi.dll.lib", "pact_ffi.lib")
191
+ elif "darwin" in target:
192
+ lib = self._extract_lib(archive, "libpact_ffi.dylib", "libpact_ffi.dylib")
193
+ elif "musl" in target:
194
+ # MUSL uses static library
195
+ lib = self._extract_lib(archive, "libpact_ffi.a", "libpact_ffi.a")
179
196
  else:
180
- dll_lib = None
197
+ lib = self._extract_lib(archive, "libpact_ffi.so", "libpact_ffi.so")
181
198
 
182
199
  # Compile the FFI extension
183
200
  extension = self._compile(lib, header)
@@ -204,16 +221,12 @@ class PactBuildHook(BuildHookInterface[Any]):
204
221
 
205
222
  return {str(extension): f"pact_ffi/{extension_dest}"}
206
223
 
207
- def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912
224
+ def _target(self) -> str:
208
225
  """
209
- Generate the download URL for the Pact library.
210
-
211
- Args:
212
- version:
213
- The upstream Pact version.
226
+ Determine the Rust target triple matching the wheel platform.
214
227
 
215
228
  Returns:
216
- The URL to download the Pact library from.
229
+ The Rust target triple of the upstream Pact library archive.
217
230
 
218
231
  Raises:
219
232
  UnsupportedPlatformError:
@@ -221,83 +234,96 @@ class PactBuildHook(BuildHookInterface[Any]):
221
234
  """
222
235
  wheel_platform = self._sys_tag_platform()
223
236
 
224
- aarch64 = ("_arm64", "_aarch64")
225
- x86_64 = ("_x86_64", "_amd64")
237
+ if wheel_platform.endswith(("_arm64", "_aarch64")):
238
+ arch = "aarch64"
239
+ elif wheel_platform.endswith(("_x86_64", "_amd64")):
240
+ arch = "x86_64"
241
+ else:
242
+ raise UnsupportedPlatformError(wheel_platform)
226
243
 
227
- # Simplified platform and architecture detection
228
244
  if wheel_platform.startswith("macosx"):
229
- os, ext = "macos", "dylib.gz"
230
- prefix = "lib"
231
- suffix = ""
232
- if wheel_platform.endswith(aarch64):
233
- platform = "aarch64"
234
- elif wheel_platform.endswith(x86_64):
235
- platform = "x86_64"
236
- else:
237
- raise UnsupportedPlatformError(wheel_platform)
238
-
239
- elif wheel_platform.startswith("musllinux"):
240
- os, ext = "linux", "a.gz" # MUSL uses static library
241
- prefix = "lib"
242
- suffix = "-musl"
243
- if wheel_platform.endswith(aarch64):
244
- platform = "aarch64"
245
- elif wheel_platform.endswith(x86_64):
246
- platform = "x86_64"
247
- else:
248
- raise UnsupportedPlatformError(wheel_platform)
249
-
250
- elif wheel_platform.startswith("manylinux"):
251
- os, ext = "linux", "so.gz"
252
- prefix = "lib"
253
- suffix = ""
254
- if wheel_platform.endswith(aarch64):
255
- platform = "aarch64"
256
- elif wheel_platform.endswith(x86_64):
257
- platform = "x86_64"
258
- else:
259
- raise UnsupportedPlatformError(wheel_platform)
260
-
261
- elif wheel_platform.startswith("win"):
262
- # TODO: Switch to using `dll.gz`
263
- # https://github.com/python-cffi/cffi/issues/182
264
- os, ext = "windows", "dll.gz"
265
- prefix = ""
266
- suffix = ""
267
- if wheel_platform.endswith(aarch64):
268
- platform = "aarch64"
269
- elif wheel_platform.endswith(x86_64):
270
- platform = "x86_64"
271
- else:
272
- raise UnsupportedPlatformError(wheel_platform)
245
+ return f"{arch}-apple-darwin"
246
+ if wheel_platform.startswith("musllinux"):
247
+ return f"{arch}-unknown-linux-musl"
248
+ if wheel_platform.startswith("manylinux"):
249
+ return f"{arch}-unknown-linux-gnu"
250
+ if wheel_platform.startswith("win"):
251
+ return f"{arch}-pc-windows-msvc"
252
+ raise UnsupportedPlatformError(wheel_platform)
253
+
254
+ def _verify(self, artefact: Path, checksum: Path) -> None:
255
+ """
256
+ Verify an artefact against its SHA-256 checksum file.
273
257
 
274
- else:
275
- raise UnsupportedPlatformError(wheel_platform)
258
+ A mismatched artefact is removed so that the next build downloads it
259
+ again.
276
260
 
277
- return PACT_LIB_URL.format(
278
- version=version,
279
- prefix=prefix,
280
- os=os,
281
- platform=platform,
282
- suffix=suffix,
283
- ext=ext,
284
- )
261
+ Args:
262
+ artefact:
263
+ The path to the downloaded artefact.
264
+
265
+ checksum:
266
+ The path to the checksum file, in the `sha256sum` format
267
+ `<hex> *<filename>`.
268
+
269
+ Raises:
270
+ ValueError:
271
+ If the checksum does not match.
272
+ """
273
+ expected = checksum.read_text().split()[0].lower()
274
+ digest = hashlib.sha256()
275
+ with artefact.open("rb") as f:
276
+ for chunk in iter(lambda: f.read(1 << 20), b""):
277
+ digest.update(chunk)
278
+ if digest.hexdigest() != expected:
279
+ artefact.unlink()
280
+ checksum.unlink()
281
+ msg = (
282
+ f"Checksum mismatch for {artefact.name}: "
283
+ f"expected {expected}, got {digest.hexdigest()}"
284
+ )
285
+ self.app.display_error(msg)
286
+ raise ValueError(msg)
287
+ self.app.display_debug(f"Verified checksum of {artefact.name}")
285
288
 
286
- def _extract_lib(self, artefact: Path) -> Path:
289
+ def _extract_lib(self, archive: Path, member: str, name: str) -> Path:
287
290
  """
288
- Extract the Pact library.
291
+ Extract a file from the Pact library archive.
292
+
293
+ The `.tar.xz` archives nest their files under a `pact_ffi-<target>`
294
+ directory, while the `.zip` archives keep them at the root.
289
295
 
290
296
  Args:
291
- artefact:
292
- The URL to download the Pact binaries from.
297
+ archive:
298
+ The path to the downloaded archive.
299
+
300
+ member:
301
+ The file name within the archive.
302
+
303
+ name:
304
+ The file name to extract to within the package directory.
305
+
306
+ Returns:
307
+ The path to the extracted file.
293
308
  """
294
- target = PKG_DIR / (artefact.name.split("-")[0] + artefact.suffixes[-2])
295
- with (
296
- gzip.open(artefact, "rb") as f_in,
297
- target.open("wb") as f_out,
298
- ):
299
- shutil.copyfileobj(f_in, f_out)
300
- self.app.display_debug(f"Extracted Pact library to {target}")
309
+ target = PKG_DIR / name
310
+ if archive.suffix == ".zip":
311
+ with (
312
+ zipfile.ZipFile(archive) as zf,
313
+ zf.open(member) as f_in,
314
+ target.open("wb") as f_out,
315
+ ):
316
+ shutil.copyfileobj(f_in, f_out)
317
+ else:
318
+ prefix = archive.name.removesuffix(".tar.xz")
319
+ with tarfile.open(archive, "r:xz") as tf:
320
+ f_tar = tf.extractfile(f"{prefix}/{member}")
321
+ if f_tar is None:
322
+ msg = f"{member} in {archive.name} is not a regular file"
323
+ raise ValueError(msg)
324
+ with f_tar, target.open("wb") as f_out:
325
+ shutil.copyfileobj(f_tar, f_out)
326
+ self.app.display_debug(f"Extracted {member} from {archive.name} to {target}")
301
327
  return target
302
328
 
303
329
  def _compile(self, lib: Path, header: Path) -> Path:
@@ -387,8 +413,8 @@ class PactBuildHook(BuildHookInterface[Any]):
387
413
  Patch the upstream header so that CFFI can compile it.
388
414
 
389
415
  The upstream `pact.h` emits the callback parameter of
390
- `pactffi_register_plugin_log_callback` as an opaque struct passed by
391
- value, which cannot be compiled. The Rust signature is
416
+ `pactffi_register_log_callback` as an opaque struct passed by value,
417
+ which cannot be compiled. The Rust signature is
392
418
  `Option<extern "C" fn(...)>`, whose ABI is a nullable function pointer.
393
419
  See https://github.com/pact-foundation/pact-reference/issues/544.
394
420
 
@@ -400,22 +426,21 @@ class PactBuildHook(BuildHookInterface[Any]):
400
426
  The header contents with the callback declared as a function
401
427
  pointer.
402
428
  """
403
- opaque = "typedef struct Option_PluginLogCallback Option_PluginLogCallback;"
429
+ opaque = "typedef struct Option_LogCallback Option_LogCallback;"
404
430
  if opaque not in source:
405
431
  return source
406
- self.app.display_debug("Patching Option_PluginLogCallback in pact.h")
432
+ self.app.display_debug("Patching Option_LogCallback in pact.h")
407
433
  return source.replace(
408
434
  opaque,
409
- "typedef void (*PluginLogCallback)("
410
- "const char *plugin_instance_id, "
435
+ "typedef void (*LogCallback)("
436
+ "const char *log_id, "
411
437
  "const char *test_run_id, "
412
438
  "const char *level, "
413
439
  "const char *target, "
414
440
  "const char *message);",
415
441
  ).replace(
416
- "void pactffi_register_plugin_log_callback("
417
- "struct Option_PluginLogCallback callback);",
418
- "void pactffi_register_plugin_log_callback(PluginLogCallback callback);",
442
+ "void pactffi_register_log_callback(struct Option_LogCallback callback);",
443
+ "void pactffi_register_log_callback(LogCallback callback);",
419
444
  )
420
445
 
421
446
  def _download(self, url: str) -> Path: