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.
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/.gitignore +1 -0
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/PKG-INFO +3 -3
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/hatch_build.py +119 -94
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/pact_ffi/__init__.py +474 -291
- pact_python_ffi-0.5.12.0/pact_ffi/__version__.py +5 -0
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/pyproject.toml +16 -12
- pact_python_ffi-0.5.8.0/pact_ffi/__version__.py +0 -10
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/LICENSE +0 -0
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/README.md +0 -0
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/pact_ffi/ffi.pyi +0 -0
- {pact_python_ffi-0.5.8.0 → pact_python_ffi-0.5.12.0}/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.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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
|
224
|
+
def _target(self) -> str:
|
|
208
225
|
"""
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
275
|
-
|
|
258
|
+
A mismatched artefact is removed so that the next build downloads it
|
|
259
|
+
again.
|
|
276
260
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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,
|
|
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
|
-
|
|
292
|
-
The
|
|
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 /
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
`
|
|
391
|
-
|
|
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
|
|
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
|
|
432
|
+
self.app.display_debug("Patching Option_LogCallback in pact.h")
|
|
407
433
|
return source.replace(
|
|
408
434
|
opaque,
|
|
409
|
-
"typedef void (*
|
|
410
|
-
"const char *
|
|
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
|
|
417
|
-
"
|
|
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:
|