pact-python-ffi 0.0.0__tar.gz → 0.4.22.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.4.22.0/.gitignore +2 -0
- pact_python_ffi-0.4.22.0/LICENSE +21 -0
- pact_python_ffi-0.4.22.0/PKG-INFO +88 -0
- pact_python_ffi-0.4.22.0/README.md +43 -0
- pact_python_ffi-0.4.22.0/cliff.toml +113 -0
- pact_python_ffi-0.4.22.0/hatch_build.py +354 -0
- pact_python_ffi-0.4.22.0/pyproject.toml +226 -0
- pact_python_ffi-0.4.22.0/src/pact_ffi/__init__.py +7820 -0
- pact_python_ffi-0.4.22.0/src/pact_ffi/__version__.py +21 -0
- pact_python_ffi-0.4.22.0/src/pact_ffi/ffi.pyi +6 -0
- pact_python_ffi-0.4.22.0/src/pact_ffi/py.typed +0 -0
- pact_python_ffi-0.4.22.0/tests/.ruff.toml +10 -0
- pact_python_ffi-0.4.22.0/tests/test_init.py +76 -0
- pact_python_ffi-0.0.0/PKG-INFO +0 -7
- pact_python_ffi-0.0.0/pact_python_ffi.egg-info/PKG-INFO +0 -7
- pact_python_ffi-0.0.0/pact_python_ffi.egg-info/SOURCES.txt +0 -5
- pact_python_ffi-0.0.0/pact_python_ffi.egg-info/dependency_links.txt +0 -1
- pact_python_ffi-0.0.0/pact_python_ffi.egg-info/top_level.txt +0 -1
- pact_python_ffi-0.0.0/pyproject.toml +0 -10
- pact_python_ffi-0.0.0/setup.cfg +0 -4
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Pact Foundation
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,88 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pact-python-ffi
|
3
|
+
Version: 0.4.22.0
|
4
|
+
Summary: Python bindings for the Pact FFI library
|
5
|
+
Project-URL: Bug Tracker, https://github.com/pact-foundation/pact-python/issues
|
6
|
+
Project-URL: Changelog, https://github.com/pact-foundation/pact-python/blob/main/pact-python-ffi/CHANGELOG.md
|
7
|
+
Project-URL: Documentation, https://docs.pact.io
|
8
|
+
Project-URL: Homepage, https://pact.io
|
9
|
+
Project-URL: Repository, https://github.com/pact-foundation/pact-python
|
10
|
+
Author-email: Joshua Ellis <josh@jpellis.me>
|
11
|
+
Maintainer-email: Joshua Ellis <josh@jpellis.me>
|
12
|
+
License-Expression: MIT
|
13
|
+
License-File: LICENSE
|
14
|
+
Keywords: contract-testing,ffi,pact,pact-python
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
16
|
+
Classifier: Environment :: Console
|
17
|
+
Classifier: Intended Audience :: Developers
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
19
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
20
|
+
Classifier: Operating System :: Microsoft :: Windows
|
21
|
+
Classifier: Operating System :: POSIX :: Linux
|
22
|
+
Classifier: Programming Language :: Python
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
24
|
+
Classifier: Programming Language :: Python :: 3.9
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
27
|
+
Classifier: Programming Language :: Python :: 3.12
|
28
|
+
Classifier: Programming Language :: Python :: 3.13
|
29
|
+
Classifier: Topic :: Software Development :: Testing
|
30
|
+
Requires-Python: >=3.9
|
31
|
+
Requires-Dist: cffi~=1.0
|
32
|
+
Provides-Extra: devel
|
33
|
+
Requires-Dist: mypy==1.17.0; extra == 'devel'
|
34
|
+
Requires-Dist: pytest-cov~=6.0; extra == 'devel'
|
35
|
+
Requires-Dist: pytest-mock~=3.0; extra == 'devel'
|
36
|
+
Requires-Dist: pytest~=8.0; extra == 'devel'
|
37
|
+
Requires-Dist: ruff==0.12.4; extra == 'devel'
|
38
|
+
Provides-Extra: devel-test
|
39
|
+
Requires-Dist: pytest-cov~=6.0; extra == 'devel-test'
|
40
|
+
Requires-Dist: pytest-mock~=3.0; extra == 'devel-test'
|
41
|
+
Requires-Dist: pytest~=8.0; extra == 'devel-test'
|
42
|
+
Provides-Extra: devel-types
|
43
|
+
Requires-Dist: mypy==1.17.0; extra == 'devel-types'
|
44
|
+
Description-Content-Type: text/markdown
|
45
|
+
|
46
|
+
# Pact Python FFI
|
47
|
+
|
48
|
+
> [!NOTE]
|
49
|
+
>
|
50
|
+
> This package provides direct access to the Pact Foreign Function Interface (FFI) with minimal abstraction. It is intended for advanced users who need low-level control over Pact operations in Python.
|
51
|
+
|
52
|
+
---
|
53
|
+
|
54
|
+
This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists to expose the [Pact FFI](https://github.com/pact-foundation/pact-reference) directly to Python. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python).
|
55
|
+
|
56
|
+
## Overview
|
57
|
+
|
58
|
+
- The module provides a thin Python wrapper around the Pact FFI (C API).
|
59
|
+
- Most classes correspond directly to structs from the FFI, and are designed to wrap the underlying C pointers.
|
60
|
+
- Many classes implement the `__del__` method to ensure memory allocated by the Rust library is freed when the Python object is destroyed, preventing memory leaks.
|
61
|
+
- Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo(...)`.
|
62
|
+
- The API is not guaranteed to be stable and is intended for use by advanced users or for building higher-level libraries. For typical contract testing, use the main Pact Python client library.
|
63
|
+
|
64
|
+
## Installation
|
65
|
+
|
66
|
+
You can install this package via pip:
|
67
|
+
|
68
|
+
```console
|
69
|
+
pip install pact-python-ffi
|
70
|
+
```
|
71
|
+
|
72
|
+
## Usage
|
73
|
+
|
74
|
+
This package exposes the raw FFI bindings for Pact. It is suitable for advanced use cases, custom integrations, or for building higher-level libraries. For typical contract testing, prefer using the main Pact Python library.
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
As this is a relatively thin wrapper around the Pact FFI, the code is unlikely to change frequently; however, contributions to improve the coverage of the FFI bindings or to improve existing functionality are welcome. See the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md) for details.
|
79
|
+
|
80
|
+
To release a new version of `pact-python-ffi`, simply push a tag in the format `pact-python-ffi/x.y.z.w`. This will automatically trigger a release process, pulling in version `x.y.z` of the underlying Pact FFI. Before creating and pushing such a tag, please ensure that the Python wrapper has been updated to reflect any changes or updates in the corresponding FFI version.
|
81
|
+
|
82
|
+
Higher-level abstractions or utilities should be implemented in separate libraries (such as [`pact-python`](https://github.com/pact-foundation/pact-python)).
|
83
|
+
|
84
|
+
---
|
85
|
+
|
86
|
+
For questions or support, please visit the [Pact Foundation Slack](https://slack.pact.io) or [GitHub Discussions](https://github.com/pact-foundation/pact-python/discussions)
|
87
|
+
|
88
|
+
---
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Pact Python FFI
|
2
|
+
|
3
|
+
> [!NOTE]
|
4
|
+
>
|
5
|
+
> This package provides direct access to the Pact Foreign Function Interface (FFI) with minimal abstraction. It is intended for advanced users who need low-level control over Pact operations in Python.
|
6
|
+
|
7
|
+
---
|
8
|
+
|
9
|
+
This sub-package is part of the [Pact Python](https://github.com/pact-foundation/pact-python) project and exists to expose the [Pact FFI](https://github.com/pact-foundation/pact-reference) directly to Python. If you are looking for the main Pact Python library for contract testing, please see the [root package](https://github.com/pact-foundation/pact-python#pact-python).
|
10
|
+
|
11
|
+
## Overview
|
12
|
+
|
13
|
+
- The module provides a thin Python wrapper around the Pact FFI (C API).
|
14
|
+
- Most classes correspond directly to structs from the FFI, and are designed to wrap the underlying C pointers.
|
15
|
+
- Many classes implement the `__del__` method to ensure memory allocated by the Rust library is freed when the Python object is destroyed, preventing memory leaks.
|
16
|
+
- Functions from the FFI are exposed directly: if a function `foo` exists in the FFI, it is accessible as `pact_ffi.foo(...)`.
|
17
|
+
- The API is not guaranteed to be stable and is intended for use by advanced users or for building higher-level libraries. For typical contract testing, use the main Pact Python client library.
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
You can install this package via pip:
|
22
|
+
|
23
|
+
```console
|
24
|
+
pip install pact-python-ffi
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
This package exposes the raw FFI bindings for Pact. It is suitable for advanced use cases, custom integrations, or for building higher-level libraries. For typical contract testing, prefer using the main Pact Python library.
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
As this is a relatively thin wrapper around the Pact FFI, the code is unlikely to change frequently; however, contributions to improve the coverage of the FFI bindings or to improve existing functionality are welcome. See the [main contributing guide](https://github.com/pact-foundation/pact-python/blob/main/CONTRIBUTING.md) for details.
|
34
|
+
|
35
|
+
To release a new version of `pact-python-ffi`, simply push a tag in the format `pact-python-ffi/x.y.z.w`. This will automatically trigger a release process, pulling in version `x.y.z` of the underlying Pact FFI. Before creating and pushing such a tag, please ensure that the Python wrapper has been updated to reflect any changes or updates in the corresponding FFI version.
|
36
|
+
|
37
|
+
Higher-level abstractions or utilities should be implemented in separate libraries (such as [`pact-python`](https://github.com/pact-foundation/pact-python)).
|
38
|
+
|
39
|
+
---
|
40
|
+
|
41
|
+
For questions or support, please visit the [Pact Foundation Slack](https://slack.pact.io) or [GitHub Discussions](https://github.com/pact-foundation/pact-python/discussions)
|
42
|
+
|
43
|
+
---
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#:schema https://json.schemastore.org/any.json
|
2
|
+
# git-cliff configuration file
|
3
|
+
# https://git-cliff.org/docs/configuration
|
4
|
+
|
5
|
+
[changelog]
|
6
|
+
# template for the changelog header
|
7
|
+
header = """
|
8
|
+
# Pact Python FFI Changelog
|
9
|
+
|
10
|
+
All notable changes to this project will be documented in this file.
|
11
|
+
|
12
|
+
Note that this _only_ includes changes to the Python FFI interface. For changes to the Pact FFI itself, see the [Pact FFI changelog](https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/CHANGELOG.md).
|
13
|
+
|
14
|
+
<!-- markdownlint-disable no-duplicate-heading -->
|
15
|
+
<!-- markdownlint-disable emph-style -->
|
16
|
+
<!-- markdownlint-disable strong-style -->
|
17
|
+
|
18
|
+
"""
|
19
|
+
|
20
|
+
# template for the changelog body
|
21
|
+
# https://keats.github.io/tera/docs/#introduction
|
22
|
+
body = """
|
23
|
+
{% if version %}\
|
24
|
+
## [{{ version | trim_start_matches(pat="v") }}] _{{ timestamp | date(format="%Y-%m-%d") }}_
|
25
|
+
{% else %}\
|
26
|
+
## [unreleased]
|
27
|
+
{% endif %}\
|
28
|
+
{% for group, commits in commits | group_by(attribute="group") %}
|
29
|
+
### {{ group | striptags | trim | upper_first }}
|
30
|
+
{% for commit in commits %}
|
31
|
+
- {% if commit.scope %}_({{ commit.scope }})_ {% endif %}\
|
32
|
+
{% if commit.breaking %}[**breaking**] {% endif %}\
|
33
|
+
{{ commit.message | upper_first }}\
|
34
|
+
{% if commit.breaking and commit.breaking_description %}
|
35
|
+
{{ " " }}\
|
36
|
+
> {{
|
37
|
+
commit.breaking_description
|
38
|
+
| split(pat="\n")
|
39
|
+
| join(sep=" ")
|
40
|
+
| replace(from=" ", to=" ")
|
41
|
+
| replace(from=" ", to=" ")
|
42
|
+
| replace(from=" ", to=" ")
|
43
|
+
| upper_first
|
44
|
+
}}\
|
45
|
+
{% endif %}\
|
46
|
+
{% endfor %}
|
47
|
+
{% endfor %}
|
48
|
+
{% if github.contributors %}\
|
49
|
+
### Contributors
|
50
|
+
{% for contributor in github.contributors %}\
|
51
|
+
{% if contributor.username and contributor.username is ending_with("[bot]") %}{% continue %}{% endif %}
|
52
|
+
- @{{ contributor.username }}\
|
53
|
+
{% endfor %}
|
54
|
+
{% endif %}
|
55
|
+
|
56
|
+
"""
|
57
|
+
|
58
|
+
# template for the changelog footer
|
59
|
+
footer = """\
|
60
|
+
<!-- generated by git-cliff on {{ now() | date(format="%Y-%m-%d") }}-->
|
61
|
+
"""
|
62
|
+
|
63
|
+
# remove the leading and trailing s
|
64
|
+
trim = true
|
65
|
+
# postprocessors
|
66
|
+
postprocessors = []
|
67
|
+
# render body even when there are no releases to process
|
68
|
+
# render_always = true
|
69
|
+
# output file path
|
70
|
+
output = "CHANGELOG.md"
|
71
|
+
|
72
|
+
[git]
|
73
|
+
tag_pattern = "pact-python-ffi/.*"
|
74
|
+
# parse the commits based on https://www.conventionalcommits.org
|
75
|
+
conventional_commits = true
|
76
|
+
# filter out the commits that are not conventional
|
77
|
+
filter_unconventional = true
|
78
|
+
# process each line of a commit as an individual commit
|
79
|
+
split_commits = false
|
80
|
+
# regex for preprocessing the commit messages
|
81
|
+
commit_preprocessors = [
|
82
|
+
# Remove the PR number added by GitHub when merging PR in UI
|
83
|
+
{ pattern = '\s*\(#([0-9]+)\)$', replace = "" },
|
84
|
+
# Check spelling of the commit with https://github.com/crate-ci/typos
|
85
|
+
{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
86
|
+
]
|
87
|
+
# regex for parsing and grouping commits
|
88
|
+
commit_parsers = [
|
89
|
+
# Ignore deps commits from the changelog
|
90
|
+
{ message = "^(chore|fix)\\(deps.*\\)", skip = true },
|
91
|
+
{ message = "^chore: update changelog.*", skip = true },
|
92
|
+
# Group commits by type
|
93
|
+
{ group = "<!-- 00 -->🚀 Features", message = "^feat" },
|
94
|
+
{ group = "<!-- 01 -->🐛 Bug Fixes", message = "^fix" },
|
95
|
+
{ group = "<!-- 50 -->🚜 Refactor", message = "^refactor" },
|
96
|
+
{ group = "<!-- 51 -->⚡ Performance", message = "^perf" },
|
97
|
+
{ group = "<!-- 52 -->🎨 Styling", message = "^style" },
|
98
|
+
{ group = "<!-- 60 -->📚 Documentation", message = "^docs" },
|
99
|
+
{ group = "<!-- 80 -->🧪 Testing", message = "^test" },
|
100
|
+
{ group = "<!-- 98 -->◀️ Revert", message = "^revert" },
|
101
|
+
{ group = "<!-- 99 -->⚙️ Miscellaneous Tasks", message = "^chore" },
|
102
|
+
{ group = "<!-- 99 -->� Other", message = ".*" },
|
103
|
+
]
|
104
|
+
# filter out the commits that are not matched by commit parsers
|
105
|
+
filter_commits = false
|
106
|
+
# sort the tags topologically
|
107
|
+
topo_order = false
|
108
|
+
# sort the commits inside sections by oldest/newest order
|
109
|
+
sort_commits = "oldest"
|
110
|
+
|
111
|
+
[remote.github]
|
112
|
+
owner = "pact-foundation"
|
113
|
+
repo = "pact-python"
|
@@ -0,0 +1,354 @@
|
|
1
|
+
"""
|
2
|
+
Hatchling build hook for binary downloads.
|
3
|
+
|
4
|
+
Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library.
|
5
|
+
This build script downloads the binaries and library for the current platform
|
6
|
+
and installs them in the `pact` directory under `/bin` and `/lib`.
|
7
|
+
|
8
|
+
The version of the binaries and library can be controlled with the
|
9
|
+
`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are
|
10
|
+
not set, a pinned version will be used instead.
|
11
|
+
"""
|
12
|
+
|
13
|
+
from __future__ import annotations
|
14
|
+
|
15
|
+
import gzip
|
16
|
+
import os
|
17
|
+
import shutil
|
18
|
+
import sys
|
19
|
+
import tempfile
|
20
|
+
import urllib.request
|
21
|
+
from pathlib import Path
|
22
|
+
from typing import Any
|
23
|
+
|
24
|
+
import cffi
|
25
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
26
|
+
from packaging.tags import sys_tags
|
27
|
+
|
28
|
+
PKG_DIR = Path(__file__).parent.resolve() / "src" / "pact_ffi"
|
29
|
+
PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{platform}{suffix}.{ext}"
|
30
|
+
|
31
|
+
|
32
|
+
class UnsupportedPlatformError(RuntimeError):
|
33
|
+
"""Raised when the current platform is not supported."""
|
34
|
+
|
35
|
+
def __init__(self, platform: str) -> None:
|
36
|
+
"""
|
37
|
+
Initialize the exception.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
platform: The unsupported platform.
|
41
|
+
"""
|
42
|
+
self.platform = platform
|
43
|
+
super().__init__(f"Unsupported platform {platform}")
|
44
|
+
|
45
|
+
|
46
|
+
class PactBuildHook(BuildHookInterface[Any]):
|
47
|
+
"""Custom hook to download Pact binaries."""
|
48
|
+
|
49
|
+
PLUGIN_NAME = "pact-ffi"
|
50
|
+
|
51
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
|
52
|
+
"""
|
53
|
+
Initialize the build hook.
|
54
|
+
|
55
|
+
For this hook, we additionally define the lib extension based on the
|
56
|
+
current platform.
|
57
|
+
"""
|
58
|
+
super().__init__(*args, **kwargs)
|
59
|
+
self.tmpdir = Path(tempfile.TemporaryDirectory().name)
|
60
|
+
self.tmpdir.mkdir(parents=True, exist_ok=True)
|
61
|
+
|
62
|
+
def clean(self, versions: list[str]) -> None: # noqa: ARG002
|
63
|
+
"""Clean up any files created by the build hook."""
|
64
|
+
for ffi in (PKG_DIR / "v3").glob("__init__.*"):
|
65
|
+
if ffi.suffix in (".so", ".dylib", ".dll", ".a", ".pyd"):
|
66
|
+
ffi.unlink()
|
67
|
+
|
68
|
+
def initialize(
|
69
|
+
self,
|
70
|
+
version: str, # noqa: ARG002
|
71
|
+
build_data: dict[str, Any],
|
72
|
+
) -> None:
|
73
|
+
"""Hook into Hatchling's build process."""
|
74
|
+
ffi_version = ".".join(self.metadata.version.split(".")[:3])
|
75
|
+
if not ffi_version:
|
76
|
+
self.app.display_error("Failed to determine Pact FFI version.")
|
77
|
+
|
78
|
+
try:
|
79
|
+
build_data["force_include"] = self._install(ffi_version)
|
80
|
+
except UnsupportedPlatformError as err:
|
81
|
+
msg = f"Pact FFI library is not available for {err.platform}"
|
82
|
+
self.app.display_error(msg)
|
83
|
+
|
84
|
+
self.app.display_debug(f"Wheel artifacts: {build_data['force_include']}")
|
85
|
+
build_data["tag"] = self._infer_tag()
|
86
|
+
|
87
|
+
def _sys_tag_platform(self) -> str:
|
88
|
+
"""
|
89
|
+
Get the platform tag from the current system tags.
|
90
|
+
|
91
|
+
This is used to determine the target platform for the Pact library.
|
92
|
+
"""
|
93
|
+
return next(t.platform for t in sys_tags())
|
94
|
+
|
95
|
+
def _install(self, version: str) -> dict[str, str]:
|
96
|
+
"""
|
97
|
+
Install the Pact library binary.
|
98
|
+
|
99
|
+
This will download the Pact library binary for the current platform and
|
100
|
+
build the CFFI bindings for it.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
version: The Pact version to install.
|
104
|
+
"""
|
105
|
+
# Download the Pact library binary and header file
|
106
|
+
lib_url = self._lib_url(version)
|
107
|
+
header = self._download(lib_url.rsplit("/", 1)[0] + "/pact.h")
|
108
|
+
lib = self._extract_lib(self._download(lib_url))
|
109
|
+
if lib.suffix == ".dll":
|
110
|
+
dll_lib = self._extract_lib(
|
111
|
+
self._download(lib_url.replace(".dll.gz", ".dll.lib.gz"))
|
112
|
+
)
|
113
|
+
else:
|
114
|
+
dll_lib = None
|
115
|
+
|
116
|
+
# Compile the FFI extension
|
117
|
+
extension = self._compile(lib, header)
|
118
|
+
|
119
|
+
# Copy into the package directory, using the ABI3 marking for broad
|
120
|
+
# compatibility.
|
121
|
+
# NOTE: Windows does _not_ use the version infixation
|
122
|
+
extension_name, _, suffix = extension.name.split(".")
|
123
|
+
infix = ".abi3" if os.name != "nt" else ""
|
124
|
+
extension_dest = f"{extension_name}{infix}.{suffix}"
|
125
|
+
shutil.copy(extension, PKG_DIR / extension_dest)
|
126
|
+
|
127
|
+
if pact_lib_dir := os.getenv("PACT_LIB_DIR"):
|
128
|
+
# Copy the library to make it available by other processes (such as
|
129
|
+
# the wheel repair).
|
130
|
+
dir_path = Path(pact_lib_dir)
|
131
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
132
|
+
self.app.display_debug(f"Copying {lib.name} into {dir_path}")
|
133
|
+
shutil.copy(lib, dir_path / lib.name)
|
134
|
+
if dll_lib:
|
135
|
+
self.app.display_debug(f"Copying {dll_lib.name} into {dir_path}")
|
136
|
+
shutil.copy(dll_lib, dir_path / dll_lib.name)
|
137
|
+
|
138
|
+
return {str(extension): f"pact_ffi/{extension_dest}"}
|
139
|
+
|
140
|
+
def _lib_url(self, version: str) -> str: # noqa: C901, PLR0912
|
141
|
+
"""
|
142
|
+
Generate the download URL for the Pact library.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
version: The upstream Pact version.
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
The URL to download the Pact library from.
|
149
|
+
|
150
|
+
Raises:
|
151
|
+
UnsupportedPlatformError:
|
152
|
+
If the current platform is not supported.
|
153
|
+
"""
|
154
|
+
wheel_platform = self._sys_tag_platform()
|
155
|
+
|
156
|
+
aarch64 = ("_arm64", "_aarch64")
|
157
|
+
x86_64 = ("_x86_64", "_amd64")
|
158
|
+
|
159
|
+
# Simplified platform and architecture detection
|
160
|
+
if wheel_platform.startswith("macosx"):
|
161
|
+
os, ext = "macos", "dylib.gz"
|
162
|
+
prefix = "lib"
|
163
|
+
suffix = ""
|
164
|
+
if wheel_platform.endswith(aarch64):
|
165
|
+
platform = "aarch64"
|
166
|
+
elif wheel_platform.endswith(x86_64):
|
167
|
+
platform = "x86_64"
|
168
|
+
else:
|
169
|
+
raise UnsupportedPlatformError(wheel_platform)
|
170
|
+
|
171
|
+
elif wheel_platform.startswith("musllinux"):
|
172
|
+
os, ext = "linux", "a.gz" # MUSL uses static library
|
173
|
+
prefix = "lib"
|
174
|
+
suffix = "-musl"
|
175
|
+
if wheel_platform.endswith(aarch64):
|
176
|
+
platform = "aarch64"
|
177
|
+
elif wheel_platform.endswith(x86_64):
|
178
|
+
platform = "x86_64"
|
179
|
+
else:
|
180
|
+
raise UnsupportedPlatformError(wheel_platform)
|
181
|
+
|
182
|
+
elif wheel_platform.startswith("manylinux"):
|
183
|
+
os, ext = "linux", "so.gz"
|
184
|
+
prefix = "lib"
|
185
|
+
suffix = ""
|
186
|
+
if wheel_platform.endswith(aarch64):
|
187
|
+
platform = "aarch64"
|
188
|
+
elif wheel_platform.endswith(x86_64):
|
189
|
+
platform = "x86_64"
|
190
|
+
else:
|
191
|
+
raise UnsupportedPlatformError(wheel_platform)
|
192
|
+
|
193
|
+
elif wheel_platform.startswith("win"):
|
194
|
+
# TODO: Switch to using `dll.gz`
|
195
|
+
# https://github.com/python-cffi/cffi/issues/182
|
196
|
+
os, ext = "windows", "dll.gz"
|
197
|
+
prefix = ""
|
198
|
+
suffix = ""
|
199
|
+
if wheel_platform.endswith(aarch64):
|
200
|
+
platform = "aarch64"
|
201
|
+
elif wheel_platform.endswith(x86_64):
|
202
|
+
platform = "x86_64"
|
203
|
+
else:
|
204
|
+
raise UnsupportedPlatformError(wheel_platform)
|
205
|
+
|
206
|
+
else:
|
207
|
+
raise UnsupportedPlatformError(wheel_platform)
|
208
|
+
|
209
|
+
return PACT_LIB_URL.format(
|
210
|
+
version=version,
|
211
|
+
prefix=prefix,
|
212
|
+
os=os,
|
213
|
+
platform=platform,
|
214
|
+
suffix=suffix,
|
215
|
+
ext=ext,
|
216
|
+
)
|
217
|
+
|
218
|
+
def _extract_lib(self, artifact: Path) -> Path:
|
219
|
+
"""
|
220
|
+
Extract the Pact library.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
artifact: The URL to download the Pact binaries from.
|
224
|
+
"""
|
225
|
+
target = PKG_DIR / (artifact.name.split("-")[0] + artifact.suffixes[-2])
|
226
|
+
with (
|
227
|
+
gzip.open(artifact, "rb") as f_in,
|
228
|
+
target.open("wb") as f_out,
|
229
|
+
):
|
230
|
+
shutil.copyfileobj(f_in, f_out)
|
231
|
+
self.app.display_debug(f"Extracted Pact library to {target}")
|
232
|
+
return target
|
233
|
+
|
234
|
+
def _compile(self, lib: Path, header: Path) -> Path:
|
235
|
+
"""
|
236
|
+
Build the CFFI bindings for the Pact library.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
lib:
|
240
|
+
The path to the Pact library binary.
|
241
|
+
|
242
|
+
header:
|
243
|
+
The path to the Pact library header file.
|
244
|
+
"""
|
245
|
+
if os.name == "nt":
|
246
|
+
extra_libs = [
|
247
|
+
"advapi32",
|
248
|
+
"bcrypt",
|
249
|
+
"crypt32",
|
250
|
+
"iphlpapi",
|
251
|
+
"ncrypt",
|
252
|
+
"netapi32",
|
253
|
+
"ntdll",
|
254
|
+
"ole32",
|
255
|
+
"oleaut32",
|
256
|
+
"pdh",
|
257
|
+
"powrprof",
|
258
|
+
"psapi",
|
259
|
+
"secur32",
|
260
|
+
"shell32",
|
261
|
+
"user32",
|
262
|
+
"userenv",
|
263
|
+
"ws2_32",
|
264
|
+
]
|
265
|
+
else:
|
266
|
+
extra_libs = []
|
267
|
+
|
268
|
+
ffibuilder = cffi.FFI()
|
269
|
+
ffibuilder.cdef(
|
270
|
+
"\n".join(
|
271
|
+
line
|
272
|
+
for line in header.read_text().splitlines()
|
273
|
+
if not line.strip().startswith("#")
|
274
|
+
)
|
275
|
+
)
|
276
|
+
|
277
|
+
linker_args: list[str] = []
|
278
|
+
if os.name == "posix":
|
279
|
+
linker_args.append(f"-Wl,-rpath,{lib.parent}")
|
280
|
+
elif os.name == "nt":
|
281
|
+
# Windows has no equivalent to rpath, instead, the end-user must
|
282
|
+
# ensure that the PATH environment variable is updated to include
|
283
|
+
# the directory containing the Pact library.
|
284
|
+
self.app.display_warning(
|
285
|
+
"On Windows, ensure that the PATH environment variable includes "
|
286
|
+
f"{lib.parent} to load the Pact library at runtime."
|
287
|
+
)
|
288
|
+
|
289
|
+
ffibuilder.set_source(
|
290
|
+
"ffi",
|
291
|
+
header.read_text(),
|
292
|
+
libraries=["pact_ffi", *extra_libs],
|
293
|
+
library_dirs=[str(lib.parent)],
|
294
|
+
extra_link_args=linker_args,
|
295
|
+
)
|
296
|
+
extension = Path(ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir)))
|
297
|
+
self.app.display_debug(f"Compiled CFFI bindings to {extension}")
|
298
|
+
return extension
|
299
|
+
|
300
|
+
def _download(self, url: str) -> Path:
|
301
|
+
"""
|
302
|
+
Download the target URL.
|
303
|
+
|
304
|
+
This will download the target URL to the `pact/data` directory. If the
|
305
|
+
download artifact is already present, its path will be returned.
|
306
|
+
|
307
|
+
If `extract` is True, the downloaded artifact will be extracted and the
|
308
|
+
path to the extract file will be returned instead.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
url: The URL to download
|
312
|
+
extract: Whether to extract the downloaded artifact.
|
313
|
+
|
314
|
+
Return:
|
315
|
+
The path to the downloaded artifact.
|
316
|
+
"""
|
317
|
+
filename = url.split("/")[-1]
|
318
|
+
artifact = PKG_DIR / "data" / filename
|
319
|
+
artifact.parent.mkdir(parents=True, exist_ok=True)
|
320
|
+
|
321
|
+
if not artifact.exists():
|
322
|
+
self.app.display_debug(f"Downloading {url} to {artifact}")
|
323
|
+
urllib.request.urlretrieve(url, artifact) # noqa: S310
|
324
|
+
else:
|
325
|
+
self.app.display_debug(f"Using cached artifact {artifact}")
|
326
|
+
|
327
|
+
return artifact
|
328
|
+
|
329
|
+
def _infer_tag(self) -> str:
|
330
|
+
"""
|
331
|
+
Infer the tag for the current build.
|
332
|
+
|
333
|
+
The bindings are built to target ABI3, which is compatible with multiple
|
334
|
+
Python versions. As a result, we generate `py3-abi3-{platform}` tags for
|
335
|
+
the wheels.
|
336
|
+
"""
|
337
|
+
python_version = f"{sys.version_info.major}{sys.version_info.minor}"
|
338
|
+
|
339
|
+
platform = self._sys_tag_platform()
|
340
|
+
|
341
|
+
# On macOS, the version needs to be set based on the deployment target
|
342
|
+
# (i.e., the version of the system libraries).
|
343
|
+
if sys.platform == "darwin" and (
|
344
|
+
deployment_target := os.getenv("MACOSX_DEPLOYMENT_TARGET")
|
345
|
+
):
|
346
|
+
target = deployment_target.replace(".", "_")
|
347
|
+
if platform.endswith("_arm64"):
|
348
|
+
platform = f"macosx_{target}_arm64"
|
349
|
+
elif platform.endswith("_x86_64"):
|
350
|
+
platform = f"macosx_{target}_x86_64"
|
351
|
+
else:
|
352
|
+
raise UnsupportedPlatformError(platform)
|
353
|
+
|
354
|
+
return f"cp{python_version}-abi3-{platform}"
|