pact-python-ffi 0.5.10.0__tar.gz → 0.5.12.1__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.
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/.gitignore +1 -0
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/PKG-INFO +2 -1
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/hatch_build.py +162 -125
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/pact_ffi/__init__.py +346 -269
- pact_python_ffi-0.5.12.1/pact_ffi/__version__.py +5 -0
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/pyproject.toml +23 -5
- pact_python_ffi-0.5.10.0/pact_ffi/__version__.py +0 -10
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/LICENSE +0 -0
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/README.md +0 -0
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/pact_ffi/ffi.pyi +0 -0
- {pact_python_ffi-0.5.10.0 → pact_python_ffi-0.5.12.1}/pact_ffi/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pact-python-ffi
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.12.1
|
|
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
|
|
@@ -28,6 +28,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
28
28
|
Classifier: Programming Language :: Python :: 3.15
|
|
29
29
|
Classifier: Topic :: Software Development :: Testing
|
|
30
30
|
Requires-Python: >=3.11
|
|
31
|
+
Requires-Dist: cffi>=2.1.1; python_version >= '3.15'
|
|
31
32
|
Requires-Dist: cffi~=2.0
|
|
32
33
|
Description-Content-Type: text/markdown
|
|
33
34
|
|
|
@@ -7,13 +7,16 @@ CFFI bindings for it.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import hashlib
|
|
11
11
|
import os
|
|
12
12
|
import shutil
|
|
13
13
|
import subprocess
|
|
14
14
|
import sys
|
|
15
|
+
import sysconfig
|
|
16
|
+
import tarfile
|
|
15
17
|
import tempfile
|
|
16
18
|
import urllib.request
|
|
19
|
+
import zipfile
|
|
17
20
|
from pathlib import Path
|
|
18
21
|
from typing import TYPE_CHECKING, Any
|
|
19
22
|
|
|
@@ -25,7 +28,22 @@ if TYPE_CHECKING:
|
|
|
25
28
|
from collections.abc import Mapping, MutableMapping, Sequence
|
|
26
29
|
|
|
27
30
|
PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi"
|
|
28
|
-
|
|
31
|
+
PACT_RELEASE_URL = "https://github.com/pact-foundation/pact-reference/releases/download/pact_ffi-v{version}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _stable_abi() -> str:
|
|
35
|
+
"""
|
|
36
|
+
Determine the stable ABI targeted by the current interpreter.
|
|
37
|
+
|
|
38
|
+
CFFI compiles against `abi3t` on free-threaded CPython 3.15+ and against
|
|
39
|
+
`abi3` otherwise. Free-threaded interpreters do not load `abi3` extensions.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Either `abi3` or `abi3t`.
|
|
43
|
+
"""
|
|
44
|
+
if sysconfig.get_config_var("Py_GIL_DISABLED"):
|
|
45
|
+
return "abi3t"
|
|
46
|
+
return "abi3"
|
|
29
47
|
|
|
30
48
|
|
|
31
49
|
class UnsupportedPlatformError(RuntimeError):
|
|
@@ -169,25 +187,40 @@ class PactBuildHook(BuildHookInterface[Any]):
|
|
|
169
187
|
`dst` is the corresponding path within the wheel.
|
|
170
188
|
"""
|
|
171
189
|
# Download the Pact library binary and header file
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
release_url = PACT_RELEASE_URL.format(version=version)
|
|
191
|
+
target = self._target()
|
|
192
|
+
header = self._download(f"{release_url}/pact.h")
|
|
193
|
+
if "windows" in target:
|
|
194
|
+
archive_url = f"{release_url}/pact_ffi-{target}.zip"
|
|
195
|
+
else:
|
|
196
|
+
archive_url = f"{release_url}/pact_ffi-{target}.tar.xz"
|
|
197
|
+
archive = self._download(archive_url)
|
|
198
|
+
self._verify(archive, self._download(f"{archive_url}.sha256"))
|
|
199
|
+
|
|
200
|
+
dll_lib: Path | None = None
|
|
201
|
+
if "windows" in target:
|
|
202
|
+
lib = self._extract_lib(archive, "pact_ffi.dll", "pact_ffi.dll")
|
|
203
|
+
# MSVC resolves `pact_ffi` to `pact_ffi.lib`. The archive's own
|
|
204
|
+
# `pact_ffi.lib` is the static library, so the import library
|
|
205
|
+
# takes its name.
|
|
206
|
+
dll_lib = self._extract_lib(archive, "pact_ffi.dll.lib", "pact_ffi.lib")
|
|
207
|
+
elif "darwin" in target:
|
|
208
|
+
lib = self._extract_lib(archive, "libpact_ffi.dylib", "libpact_ffi.dylib")
|
|
209
|
+
elif "musl" in target:
|
|
210
|
+
# MUSL uses static library
|
|
211
|
+
lib = self._extract_lib(archive, "libpact_ffi.a", "libpact_ffi.a")
|
|
179
212
|
else:
|
|
180
|
-
|
|
213
|
+
lib = self._extract_lib(archive, "libpact_ffi.so", "libpact_ffi.so")
|
|
181
214
|
|
|
182
215
|
# Compile the FFI extension
|
|
183
216
|
extension = self._compile(lib, header)
|
|
184
217
|
|
|
185
|
-
# Copy into the package directory, using the
|
|
186
|
-
# compatibility.
|
|
218
|
+
# Copy into the package directory, using the stable ABI marking for
|
|
219
|
+
# broad compatibility.
|
|
187
220
|
# NOTE: Windows does _not_ use the version infixation
|
|
188
221
|
name = extension.name.split(".")[0]
|
|
189
222
|
suffix = extension.suffix
|
|
190
|
-
infix = ".
|
|
223
|
+
infix = f".{_stable_abi()}" if os.name != "nt" else ""
|
|
191
224
|
extension_dest = f"{name}{infix}{suffix}"
|
|
192
225
|
shutil.copy(extension, PKG_DIR / extension_dest)
|
|
193
226
|
|
|
@@ -204,16 +237,12 @@ class PactBuildHook(BuildHookInterface[Any]):
|
|
|
204
237
|
|
|
205
238
|
return {str(extension): f"pact_ffi/{extension_dest}"}
|
|
206
239
|
|
|
207
|
-
def
|
|
240
|
+
def _target(self) -> str:
|
|
208
241
|
"""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Args:
|
|
212
|
-
version:
|
|
213
|
-
The upstream Pact version.
|
|
242
|
+
Determine the Rust target triple matching the wheel platform.
|
|
214
243
|
|
|
215
244
|
Returns:
|
|
216
|
-
The
|
|
245
|
+
The Rust target triple of the upstream Pact library archive.
|
|
217
246
|
|
|
218
247
|
Raises:
|
|
219
248
|
UnsupportedPlatformError:
|
|
@@ -221,83 +250,96 @@ class PactBuildHook(BuildHookInterface[Any]):
|
|
|
221
250
|
"""
|
|
222
251
|
wheel_platform = self._sys_tag_platform()
|
|
223
252
|
|
|
224
|
-
|
|
225
|
-
|
|
253
|
+
if wheel_platform.endswith(("_arm64", "_aarch64")):
|
|
254
|
+
arch = "aarch64"
|
|
255
|
+
elif wheel_platform.endswith(("_x86_64", "_amd64")):
|
|
256
|
+
arch = "x86_64"
|
|
257
|
+
else:
|
|
258
|
+
raise UnsupportedPlatformError(wheel_platform)
|
|
226
259
|
|
|
227
|
-
# Simplified platform and architecture detection
|
|
228
260
|
if wheel_platform.startswith("macosx"):
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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)
|
|
261
|
+
return f"{arch}-apple-darwin"
|
|
262
|
+
if wheel_platform.startswith("musllinux"):
|
|
263
|
+
return f"{arch}-unknown-linux-musl"
|
|
264
|
+
if wheel_platform.startswith("manylinux"):
|
|
265
|
+
return f"{arch}-unknown-linux-gnu"
|
|
266
|
+
if wheel_platform.startswith("win"):
|
|
267
|
+
return f"{arch}-pc-windows-msvc"
|
|
268
|
+
raise UnsupportedPlatformError(wheel_platform)
|
|
269
|
+
|
|
270
|
+
def _verify(self, artefact: Path, checksum: Path) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Verify an artefact against its SHA-256 checksum file.
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
A mismatched artefact is removed so that the next build downloads it
|
|
275
|
+
again.
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
277
|
+
Args:
|
|
278
|
+
artefact:
|
|
279
|
+
The path to the downloaded artefact.
|
|
280
|
+
|
|
281
|
+
checksum:
|
|
282
|
+
The path to the checksum file, in the `sha256sum` format
|
|
283
|
+
`<hex> *<filename>`.
|
|
285
284
|
|
|
286
|
-
|
|
285
|
+
Raises:
|
|
286
|
+
ValueError:
|
|
287
|
+
If the checksum does not match.
|
|
287
288
|
"""
|
|
288
|
-
|
|
289
|
+
expected = checksum.read_text().split()[0].lower()
|
|
290
|
+
digest = hashlib.sha256()
|
|
291
|
+
with artefact.open("rb") as f:
|
|
292
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
293
|
+
digest.update(chunk)
|
|
294
|
+
if digest.hexdigest() != expected:
|
|
295
|
+
artefact.unlink()
|
|
296
|
+
checksum.unlink()
|
|
297
|
+
msg = (
|
|
298
|
+
f"Checksum mismatch for {artefact.name}: "
|
|
299
|
+
f"expected {expected}, got {digest.hexdigest()}"
|
|
300
|
+
)
|
|
301
|
+
self.app.display_error(msg)
|
|
302
|
+
raise ValueError(msg)
|
|
303
|
+
self.app.display_debug(f"Verified checksum of {artefact.name}")
|
|
304
|
+
|
|
305
|
+
def _extract_lib(self, archive: Path, member: str, name: str) -> Path:
|
|
306
|
+
"""
|
|
307
|
+
Extract a file from the Pact library archive.
|
|
308
|
+
|
|
309
|
+
The `.tar.xz` archives nest their files under a `pact_ffi-<target>`
|
|
310
|
+
directory, while the `.zip` archives keep them at the root.
|
|
289
311
|
|
|
290
312
|
Args:
|
|
291
|
-
|
|
292
|
-
The
|
|
313
|
+
archive:
|
|
314
|
+
The path to the downloaded archive.
|
|
315
|
+
|
|
316
|
+
member:
|
|
317
|
+
The file name within the archive.
|
|
318
|
+
|
|
319
|
+
name:
|
|
320
|
+
The file name to extract to within the package directory.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
The path to the extracted file.
|
|
293
324
|
"""
|
|
294
|
-
target = PKG_DIR /
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
325
|
+
target = PKG_DIR / name
|
|
326
|
+
if archive.suffix == ".zip":
|
|
327
|
+
with (
|
|
328
|
+
zipfile.ZipFile(archive) as zf,
|
|
329
|
+
zf.open(member) as f_in,
|
|
330
|
+
target.open("wb") as f_out,
|
|
331
|
+
):
|
|
332
|
+
shutil.copyfileobj(f_in, f_out)
|
|
333
|
+
else:
|
|
334
|
+
prefix = archive.name.removesuffix(".tar.xz")
|
|
335
|
+
with tarfile.open(archive, "r:xz") as tf:
|
|
336
|
+
f_tar = tf.extractfile(f"{prefix}/{member}")
|
|
337
|
+
if f_tar is None:
|
|
338
|
+
msg = f"{member} in {archive.name} is not a regular file"
|
|
339
|
+
raise ValueError(msg)
|
|
340
|
+
with f_tar, target.open("wb") as f_out:
|
|
341
|
+
shutil.copyfileobj(f_tar, f_out)
|
|
342
|
+
self.app.display_debug(f"Extracted {member} from {archive.name} to {target}")
|
|
301
343
|
return target
|
|
302
344
|
|
|
303
345
|
def _compile(self, lib: Path, header: Path) -> Path:
|
|
@@ -386,46 +428,36 @@ class PactBuildHook(BuildHookInterface[Any]):
|
|
|
386
428
|
"""
|
|
387
429
|
Patch the upstream header so that CFFI can compile it.
|
|
388
430
|
|
|
389
|
-
The upstream `pact.h` emits the callback
|
|
390
|
-
`
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
https://github.com/pact-foundation/pact-reference/issues/544.
|
|
431
|
+
The upstream `pact.h` emits the callback parameter of
|
|
432
|
+
`pactffi_register_log_callback` as an opaque struct passed by value,
|
|
433
|
+
which cannot be compiled. The Rust signature is
|
|
434
|
+
`Option<extern "C" fn(...)>`, whose ABI is a nullable function pointer.
|
|
435
|
+
See https://github.com/pact-foundation/pact-reference/issues/544.
|
|
395
436
|
|
|
396
437
|
Args:
|
|
397
438
|
source:
|
|
398
439
|
The contents of the upstream `pact.h`.
|
|
399
440
|
|
|
400
441
|
Returns:
|
|
401
|
-
The header contents with the
|
|
402
|
-
|
|
442
|
+
The header contents with the callback declared as a function
|
|
443
|
+
pointer.
|
|
403
444
|
"""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"const char *test_run_id, "
|
|
421
|
-
"const char *level, "
|
|
422
|
-
"const char *target, "
|
|
423
|
-
"const char *message);",
|
|
424
|
-
).replace(
|
|
425
|
-
f"void {register}(struct Option_{callback} callback);",
|
|
426
|
-
f"void {register}({callback} callback);",
|
|
427
|
-
)
|
|
428
|
-
return source
|
|
445
|
+
opaque = "typedef struct Option_LogCallback Option_LogCallback;"
|
|
446
|
+
if opaque not in source:
|
|
447
|
+
return source
|
|
448
|
+
self.app.display_debug("Patching Option_LogCallback in pact.h")
|
|
449
|
+
return source.replace(
|
|
450
|
+
opaque,
|
|
451
|
+
"typedef void (*LogCallback)("
|
|
452
|
+
"const char *log_id, "
|
|
453
|
+
"const char *test_run_id, "
|
|
454
|
+
"const char *level, "
|
|
455
|
+
"const char *target, "
|
|
456
|
+
"const char *message);",
|
|
457
|
+
).replace(
|
|
458
|
+
"void pactffi_register_log_callback(struct Option_LogCallback callback);",
|
|
459
|
+
"void pactffi_register_log_callback(LogCallback callback);",
|
|
460
|
+
)
|
|
429
461
|
|
|
430
462
|
def _download(self, url: str) -> Path:
|
|
431
463
|
"""
|
|
@@ -458,13 +490,17 @@ class PactBuildHook(BuildHookInterface[Any]):
|
|
|
458
490
|
"""
|
|
459
491
|
Infer the tag for the current build.
|
|
460
492
|
|
|
461
|
-
The bindings are built to target
|
|
462
|
-
Python versions. As a result, we generate
|
|
463
|
-
tags for the wheels.
|
|
493
|
+
The bindings are built to target the stable ABI, which is compatible
|
|
494
|
+
with multiple Python versions. As a result, we generate
|
|
495
|
+
`cp{version}-abi3-{platform}` tags for the wheels.
|
|
464
496
|
|
|
465
497
|
While the ABI3 interface was introduced in Python 3.2, we target the
|
|
466
498
|
earliest supported version of Python in the Python wrapper.
|
|
467
499
|
|
|
500
|
+
A free-threaded build targets `abi3t`, which requires Python 3.15. Its
|
|
501
|
+
wheel takes the `cp{version}-abi3.abi3t-{platform}` tag, as the
|
|
502
|
+
extension also loads on GIL-enabled builds.
|
|
503
|
+
|
|
468
504
|
Returns:
|
|
469
505
|
The tag for the current build.
|
|
470
506
|
"""
|
|
@@ -485,4 +521,5 @@ class PactBuildHook(BuildHookInterface[Any]):
|
|
|
485
521
|
else:
|
|
486
522
|
raise UnsupportedPlatformError(platform)
|
|
487
523
|
|
|
488
|
-
|
|
524
|
+
abi = "abi3.abi3t" if _stable_abi() == "abi3t" else "abi3"
|
|
525
|
+
return f"cp{python_version}-{abi}-{platform}"
|