pyproject-api 1.12.1__tar.gz → 1.12.3__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.
- pyproject_api-1.12.3/.github/SECURITY.md +51 -0
- pyproject_api-1.12.3/.github/THREAT_MODEL.md +101 -0
- pyproject_api-1.12.3/.github/dependabot.yaml +30 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/workflows/news.yaml +3 -2
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/workflows/prepare-release.yaml +1 -1
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/workflows/release.yaml +7 -7
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/workflows/upgrade-release-tools.yaml +1 -1
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.pre-commit-config.yaml +1 -1
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/PKG-INFO +1 -1
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/changelog.rst +34 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/pylock.toml +45 -48
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/_backend.py +2 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/_backend.pyi +0 -2
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/_frontend.py +13 -3
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/_version.py +2 -2
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/_via_fresh_subprocess.py +8 -3
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/test_backend.py +13 -2
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/test_frontend.py +130 -1
- pyproject_api-1.12.1/.github/SECURITY.md +0 -9
- pyproject_api-1.12.1/.github/dependabot.yaml +0 -27
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/FUNDING.yaml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/release.yaml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/workflows/check.yaml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.github/workflows/codeql.yaml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.gitignore +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/.readthedocs.yml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/CODE_OF_CONDUCT.md +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/CONTRIBUTING.md +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/LICENSE +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/README.md +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/api.rst +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/changelog.d/.gitkeep +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/changelog.d/template.jinja2 +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/conf.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/contributing.rst +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/docs/index.rst +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/pyproject.toml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/__init__.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/__main__.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/_util.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/src/pyproject_api/py.typed +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/_build_sdist.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/demo_pkg_inline/build.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/demo_pkg_inline/pyproject.toml +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/test_frontend_setuptools.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/test_main.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/test_util.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tests/test_version.py +0 -0
- {pyproject_api-1.12.1 → pyproject_api-1.12.3}/tox.toml +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
pyproject-api is a Python library that drives a project's PEP 517 build backend from a separate process. This policy
|
|
4
|
+
says how to report a security problem and how we decide whether a report is one.
|
|
5
|
+
|
|
6
|
+
## Reporting a vulnerability
|
|
7
|
+
|
|
8
|
+
**Do not open a public issue for a security problem.** Use
|
|
9
|
+
[GitHub's private vulnerability reporting](https://github.com/tox-dev/pyproject-api/security/advisories/new). If neither
|
|
10
|
+
works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com).
|
|
11
|
+
|
|
12
|
+
Include what the attacker controls and what they gain, the steps or a proof of concept, and the versions and platform
|
|
13
|
+
you tested. Write the report yourself; unreviewed tool or model output takes longer to triage.
|
|
14
|
+
|
|
15
|
+
## What to expect
|
|
16
|
+
|
|
17
|
+
Volunteers maintain pyproject-api, with no on-call rotation. We aim to acknowledge a report within **5 working days**
|
|
18
|
+
and assess it within **15 working days**. Fixes are developed in a private fork attached to the draft advisory, ship in
|
|
19
|
+
a new release, and the advisory, with a CVE requested through GitHub, goes out with that release. The default embargo is
|
|
20
|
+
**90 days** from the report; we agree any change with the reporter. We credit reporters in the advisory under the name
|
|
21
|
+
they choose, or not at all. There is no bug bounty.
|
|
22
|
+
|
|
23
|
+
## Supported versions
|
|
24
|
+
|
|
25
|
+
Only the latest release receives fixes. Confirm the problem reproduces on it before reporting.
|
|
26
|
+
|
|
27
|
+
## Vulnerability or bug
|
|
28
|
+
|
|
29
|
+
A report is a vulnerability when an attacker who controls only an input that the [threat model](THREAT_MODEL.md) lists
|
|
30
|
+
as untrusted breaks a guarantee pyproject-api makes. A report that first needs control of something the threat model
|
|
31
|
+
trusts, such as the project being built and its build backend, is a bug: we fix it in the open and publish no advisory.
|
|
32
|
+
Classify by who supplies the dangerous input, not by how bad the outcome sounds.
|
|
33
|
+
|
|
34
|
+
**In scope**
|
|
35
|
+
|
|
36
|
+
- A `build-system.backend-path` entry that resolves outside the project being accepted as an import location.
|
|
37
|
+
- A frontend call writing a result or artifact outside the directory the caller named, including while extracting
|
|
38
|
+
`.dist-info` from a built wheel.
|
|
39
|
+
- Clearing a metadata directory deleting the project root, one of its ancestors, or anything reached through a symlink
|
|
40
|
+
inside it.
|
|
41
|
+
- A hook result naming a path outside its output directory, returned to the caller as the artifact or metadata path.
|
|
42
|
+
- Results of one backend call being read as another call's, for example through a predictable result file.
|
|
43
|
+
|
|
44
|
+
**Out of scope**
|
|
45
|
+
|
|
46
|
+
- Code a build backend runs: building a project runs its backend, so building untrusted source runs untrusted code, as
|
|
47
|
+
PEP 517 intends.
|
|
48
|
+
- What the backend writes into the artifacts it returns.
|
|
49
|
+
- The interpreter, environment and `requires` the caller provisions for the backend.
|
|
50
|
+
|
|
51
|
+
If you cannot tell which side a finding falls on, report it privately and we will decide.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Threat model
|
|
2
|
+
|
|
3
|
+
This model says what pyproject-api trusts, where untrusted input enters, and which guarantees hold at each boundary, so
|
|
4
|
+
a report can be classified as a vulnerability or a bug. [SECURITY.md](SECURITY.md) covers reporting; if the two
|
|
5
|
+
disagree, SECURITY.md wins and we fix this file.
|
|
6
|
+
|
|
7
|
+
## How pyproject-api works
|
|
8
|
+
|
|
9
|
+
A frontend such as tox points pyproject-api at a project folder. pyproject-api reads `[build-system]` from
|
|
10
|
+
`pyproject.toml`, checks that every `backend-path` entry stays inside the project, and starts the backend in a
|
|
11
|
+
subprocess of the interpreter the caller chose: `SubprocessFrontend` starts a fresh one per call, while other frontends,
|
|
12
|
+
such as tox's, can keep one alive across calls (`reuse_backend`). Each hook call (`build_wheel`,
|
|
13
|
+
`prepare_metadata_for_build_wheel` and so on) goes to the backend's stdin as JSON, and the backend writes its answer to
|
|
14
|
+
a temporary JSON file that the frontend reads back. Without a metadata hook, pyproject-api builds a wheel and extracts
|
|
15
|
+
its `.dist-info` into the caller's metadata directory.
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart LR
|
|
19
|
+
fe["Frontend, e.g. tox<br/>(trusted)"] -->|project dir, python| api["pyproject-api"]
|
|
20
|
+
api -->|"reads [build-system]"| proj[("Project source")]
|
|
21
|
+
api -->|"spawn, JSON over stdin"| be["Backend subprocess<br/>runs project-chosen code"]
|
|
22
|
+
be -->|"result JSON in temp file,<br/>artifacts in output dir"| api
|
|
23
|
+
classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a;
|
|
24
|
+
classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00;
|
|
25
|
+
class fe,api trusted;
|
|
26
|
+
class proj,be boundary;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Compared with similar tools
|
|
30
|
+
|
|
31
|
+
We drew the boundary where comparable projects in other ecosystems draw theirs. The shared rule, stated by the
|
|
32
|
+
[Node.js threat model](https://github.com/nodejs/node/blob/main/SECURITY.md#the-nodejs-threat-model), is that a report
|
|
33
|
+
which first needs a trusted element compromised is out of scope; the first triage question, as
|
|
34
|
+
[DOMPurify](https://github.com/cure53/DOMPurify/blob/main/SECURITY.md#what-we-consider-a-vulnerability) puts it, is what
|
|
35
|
+
the attacker controls.
|
|
36
|
+
|
|
37
|
+
| Project | Language | Where it draws the line |
|
|
38
|
+
| ----------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
39
|
+
| [Cargo build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) | Rust | A package's `build.rs` runs with full rights during a build; building a crate means trusting it. |
|
|
40
|
+
| [Hatch](https://github.com/pypa/hatch/security/policy) | Python | Assumes the user has full control over the build environment. |
|
|
41
|
+
| [pnpm](https://github.com/pnpm/pnpm/security/policy) | JavaScript | Draws its boundary at filesystem permissions: the store, project, lockfile and configuration are trusted, and running it in a repository means trusting that repository. |
|
|
42
|
+
|
|
43
|
+
## Trusted
|
|
44
|
+
|
|
45
|
+
An attacker who controls any of these already has what pyproject-api could give them:
|
|
46
|
+
|
|
47
|
+
- The frontend and the project folder, interpreter, environment and output directories it passes.
|
|
48
|
+
- The project's build backend and its `requires`; the backend runs with the user's rights.
|
|
49
|
+
- The filesystem and temporary directory.
|
|
50
|
+
|
|
51
|
+
## Untrusted inputs
|
|
52
|
+
|
|
53
|
+
| Input | Comes from | What pyproject-api guarantees |
|
|
54
|
+
| -------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `build-system.backend-path` | The project's `pyproject.toml` | Each entry must be relative and resolve inside the project; otherwise the call fails. |
|
|
56
|
+
| Backend stdout and stderr | The backend and anything it imports | Captured and returned to the caller as text; hook results come only from the result file. |
|
|
57
|
+
| Malformed `[build-system]` | The project's `pyproject.toml` | Wrong types raise a `ValueError` naming the key; a missing table falls back to the legacy setuptools backend, as PEP 517 specifies. |
|
|
58
|
+
| Members of a built wheel | The backend | Only members under a top-level `.dist-info` directory are extracted, through `zipfile`'s path sanitizing. |
|
|
59
|
+
| Entries inside a metadata directory to clear | Earlier builds, or the backend | Refused when it is the project root or one of its ancestors; symlinks inside it are unlinked, never followed. |
|
|
60
|
+
|
|
61
|
+
## Threats and controls
|
|
62
|
+
|
|
63
|
+
| STRIDE | Threat | Control |
|
|
64
|
+
| ---------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
65
|
+
| Tampering | `backend-path = ["../other"]` loads a backend from outside the project. | Entries outside the project are rejected. |
|
|
66
|
+
| Spoofing | Backend output printed to stdout is taken as a hook's result. | Results travel through a per-call temporary JSON file, not stdout. |
|
|
67
|
+
| Elevation of privilege | A backend runs code during a build. | Accepted by design; PEP 517 builds run the project's code. |
|
|
68
|
+
|
|
69
|
+
## Release chain
|
|
70
|
+
|
|
71
|
+
Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release
|
|
72
|
+
App, not a person, pushes the release commit and tag; the build installs its tools from a hash-checked `pylock.toml`
|
|
73
|
+
resolved with a seven-day upload cooldown by default; PyPI receives the files through
|
|
74
|
+
[trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable.
|
|
75
|
+
Every workflow pins third-party actions to a commit SHA.
|
|
76
|
+
|
|
77
|
+
## Vulnerability or bug
|
|
78
|
+
|
|
79
|
+
| Report | Verdict | Why |
|
|
80
|
+
| -------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
|
|
81
|
+
| `backend-path` with `../` imports a module from the parent directory | Vulnerability | The in-tree guarantee of PEP 517 is broken. |
|
|
82
|
+
| Building a malicious sdist runs its `setup.py` | Not a vulnerability | The backend is trusted; build untrusted source in a sandbox. |
|
|
83
|
+
| A backend that prints JSON confuses `get_requires_for_build_wheel` | Bug, or vulnerability if the result file is bypassed | Results must come only from the result file. |
|
|
84
|
+
|
|
85
|
+
## Severity
|
|
86
|
+
|
|
87
|
+
- **High:** a frontend call deletes or writes outside the directory the caller named, or clearing a metadata directory
|
|
88
|
+
removes the project or follows a symlink inside it.
|
|
89
|
+
- **Medium:** a `backend-path` outside the project is accepted, a hook's returned path escapes its directory, or one
|
|
90
|
+
call's result is read as another's.
|
|
91
|
+
- **Low:** a malformed backend response crashes or hangs the frontend instead of failing with an error.
|
|
92
|
+
|
|
93
|
+
## Accepted risks
|
|
94
|
+
|
|
95
|
+
- Every hook call runs project code; isolation from the host is the caller's job.
|
|
96
|
+
- The backend subprocess inherits the environment the caller passes, secrets included.
|
|
97
|
+
|
|
98
|
+
## Keeping this current
|
|
99
|
+
|
|
100
|
+
Revisit this model when pyproject-api gains a new input source, network access, a new way to run code, or a new place it
|
|
101
|
+
writes to, and whenever an advisory is published.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
multi-ecosystem-groups:
|
|
3
|
+
dependencies:
|
|
4
|
+
schedule:
|
|
5
|
+
interval: weekly
|
|
6
|
+
day: tuesday
|
|
7
|
+
labels:
|
|
8
|
+
- dependencies
|
|
9
|
+
- skip news
|
|
10
|
+
updates:
|
|
11
|
+
- package-ecosystem: github-actions
|
|
12
|
+
directory: /
|
|
13
|
+
patterns:
|
|
14
|
+
- '*'
|
|
15
|
+
multi-ecosystem-group: dependencies
|
|
16
|
+
labels:
|
|
17
|
+
- dependencies
|
|
18
|
+
- skip news
|
|
19
|
+
cooldown:
|
|
20
|
+
default-days: 7
|
|
21
|
+
- package-ecosystem: pre-commit
|
|
22
|
+
directory: /
|
|
23
|
+
patterns:
|
|
24
|
+
- '*'
|
|
25
|
+
multi-ecosystem-group: dependencies
|
|
26
|
+
labels:
|
|
27
|
+
- dependencies
|
|
28
|
+
- skip news
|
|
29
|
+
cooldown:
|
|
30
|
+
default-days: 7
|
|
@@ -7,6 +7,7 @@ concurrency:
|
|
|
7
7
|
cancel-in-progress: true
|
|
8
8
|
permissions:
|
|
9
9
|
contents: read
|
|
10
|
+
pull-requests: read
|
|
10
11
|
jobs:
|
|
11
12
|
changelog:
|
|
12
13
|
name: 📝 News fragment
|
|
@@ -24,9 +25,9 @@ jobs:
|
|
|
24
25
|
python-version: "3.15"
|
|
25
26
|
enable-cache: false
|
|
26
27
|
- name: 📦 Install locked release tools
|
|
27
|
-
uses: tox-dev/github-workflows/install-locked-tools@
|
|
28
|
+
uses: tox-dev/github-workflows/install-locked-tools@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
28
29
|
with:
|
|
29
30
|
groups: release-news
|
|
30
31
|
venv: ${{ runner.temp }}/release-tools
|
|
31
32
|
- name: 🔍 Validate news fragments
|
|
32
|
-
uses: tox-dev/github-workflows/validate-news@
|
|
33
|
+
uses: tox-dev/github-workflows/validate-news@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
@@ -16,7 +16,7 @@ permissions: {}
|
|
|
16
16
|
jobs:
|
|
17
17
|
prepare:
|
|
18
18
|
name: 🏷️ Prepare Towncrier release
|
|
19
|
-
uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@
|
|
19
|
+
uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
20
20
|
permissions:
|
|
21
21
|
contents: read
|
|
22
22
|
pull-requests: read
|
|
@@ -10,7 +10,7 @@ concurrency:
|
|
|
10
10
|
jobs:
|
|
11
11
|
build:
|
|
12
12
|
name: 🏗️ Build
|
|
13
|
-
uses: tox-dev/github-workflows/.github/workflows/validate-python-release.yaml@
|
|
13
|
+
uses: tox-dev/github-workflows/.github/workflows/validate-python-release.yaml@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
14
14
|
permissions:
|
|
15
15
|
contents: read
|
|
16
16
|
github-notes:
|
|
@@ -33,17 +33,17 @@ jobs:
|
|
|
33
33
|
python-version: "3.15"
|
|
34
34
|
enable-cache: false
|
|
35
35
|
- name: 📦 Install locked release tools
|
|
36
|
-
uses: tox-dev/github-workflows/install-locked-tools@
|
|
36
|
+
uses: tox-dev/github-workflows/install-locked-tools@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
37
37
|
with:
|
|
38
38
|
groups: release-notes
|
|
39
39
|
venv: ${{ runner.temp }}/release-tools
|
|
40
40
|
- name: 🏷️ Select previous tag
|
|
41
41
|
id: previous
|
|
42
|
-
uses: tox-dev/github-workflows/select-previous-tag@
|
|
42
|
+
uses: tox-dev/github-workflows/select-previous-tag@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
43
43
|
with:
|
|
44
44
|
tag: ${{ github.ref_name }}
|
|
45
45
|
- name: 📝 Render release notes
|
|
46
|
-
uses: tox-dev/github-workflows/render-release-notes@
|
|
46
|
+
uses: tox-dev/github-workflows/render-release-notes@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
47
47
|
with:
|
|
48
48
|
tag: ${{ github.ref_name }}
|
|
49
49
|
previous: ${{ steps.previous.outputs.previous }}
|
|
@@ -73,7 +73,7 @@ jobs:
|
|
|
73
73
|
artifact-ids: ${{ needs.build.outputs.distributions-artifact-id }}
|
|
74
74
|
path: dist/
|
|
75
75
|
- name: 🚀 Publish to PyPI
|
|
76
|
-
uses: pypa/gh-action-pypi-publish@
|
|
76
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
77
77
|
with:
|
|
78
78
|
packages-dir: dist
|
|
79
79
|
attestations: true
|
|
@@ -102,13 +102,13 @@ jobs:
|
|
|
102
102
|
artifact-ids: ${{ needs.github-notes.outputs.artifact-id }}
|
|
103
103
|
path: ${{ runner.temp }}
|
|
104
104
|
- name: 🔍 Verify PyPI artifacts
|
|
105
|
-
uses: tox-dev/github-workflows/reconcile-pypi-artifacts@
|
|
105
|
+
uses: tox-dev/github-workflows/reconcile-pypi-artifacts@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
106
106
|
with:
|
|
107
107
|
project: pyproject-api
|
|
108
108
|
version: ${{ github.ref_name }}
|
|
109
109
|
dist: dist
|
|
110
110
|
- name: 🚀 Publish GitHub release
|
|
111
|
-
uses: tox-dev/github-workflows/publish-github-release@
|
|
111
|
+
uses: tox-dev/github-workflows/publish-github-release@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
112
112
|
with:
|
|
113
113
|
tag: ${{ github.ref_name }}
|
|
114
114
|
commit: ${{ github.sha }}
|
|
@@ -10,7 +10,7 @@ concurrency:
|
|
|
10
10
|
jobs:
|
|
11
11
|
upgrade:
|
|
12
12
|
name: 🔀 Upgrade release tools
|
|
13
|
-
uses: tox-dev/github-workflows/.github/workflows/upgrade-release-tools.yaml@
|
|
13
|
+
uses: tox-dev/github-workflows/.github/workflows/upgrade-release-tools.yaml@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1
|
|
14
14
|
permissions:
|
|
15
15
|
contents: read
|
|
16
16
|
secrets:
|
|
@@ -44,7 +44,7 @@ repos:
|
|
|
44
44
|
^docs/changelog[.]rst$ # Only the generated release history.
|
|
45
45
|
args: ["-l", "120", "--preserve-adornments"]
|
|
46
46
|
additional_dependencies:
|
|
47
|
-
- "sphinx==9.1.
|
|
47
|
+
- "sphinx==9.1.1"
|
|
48
48
|
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
|
49
49
|
rev: c92afdf2c6ca336aaa8f39e2bd8d48f62dbd8e1f # frozen: v1.31.1
|
|
50
50
|
hooks:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyproject-api
|
|
3
|
-
Version: 1.12.
|
|
3
|
+
Version: 1.12.3
|
|
4
4
|
Import-Name: pyproject_api
|
|
5
5
|
Summary: API to interact with the python pyproject.toml based projects
|
|
6
6
|
Project-URL: Changelog, https://github.com/tox-dev/pyproject-api/blob/main/docs/changelog.rst
|
|
@@ -5,6 +5,40 @@ Release History
|
|
|
5
5
|
|
|
6
6
|
.. towncrier release notes start
|
|
7
7
|
|
|
8
|
+
.. _release-1-12-3:
|
|
9
|
+
|
|
10
|
+
v1.12.3 (2026-11-02)
|
|
11
|
+
--------------------
|
|
12
|
+
|
|
13
|
+
.. _release-1-12-3-fixed:
|
|
14
|
+
|
|
15
|
+
Fixed
|
|
16
|
+
^^^^^
|
|
17
|
+
|
|
18
|
+
- Clear stale files from reused ``.dist-info`` directories when building wheel or editable metadata (:pr:`345`) by
|
|
19
|
+
:user:`subotac` and :user:`gaborbernat`.
|
|
20
|
+
|
|
21
|
+
.. _release-1-12-2:
|
|
22
|
+
|
|
23
|
+
v1.12.2 (2026-10-28)
|
|
24
|
+
--------------------
|
|
25
|
+
|
|
26
|
+
.. _release-1-12-2-fixed:
|
|
27
|
+
|
|
28
|
+
Fixed
|
|
29
|
+
^^^^^
|
|
30
|
+
|
|
31
|
+
- Drop the backend proxy stub declarations for its private commands; one named a method the proxy does not have
|
|
32
|
+
(:pr:`331`) by :user:`tridev-tej` and :user:`gaborbernat`.
|
|
33
|
+
- Keep a trailing space in the last backend path when putting it on ``PYTHONPATH`` (:pr:`329`) by :user:`jdotc1` and
|
|
34
|
+
:user:`gaborbernat`.
|
|
35
|
+
- Preserve ``BackendFailed`` details when copying or pickling an exception, including when a build runs in a process
|
|
36
|
+
pool (:pr:`337`) by :user:`jdotc1`.
|
|
37
|
+
- Stop the backend from crashing when it prints a path its standard output encoding cannot represent (:pr:`330`) by
|
|
38
|
+
:user:`nk-batman21` and :user:`gaborbernat`.
|
|
39
|
+
- Stop the backend process and join its output collection thread when a frontend call is interrupted (:pr:`332`) by
|
|
40
|
+
:user:`zfz2005` and :user:`gaborbernat`.
|
|
41
|
+
|
|
8
42
|
.. _release-1-12-1:
|
|
9
43
|
|
|
10
44
|
v1.12.1 (2026-10-27)
|
|
@@ -180,28 +180,28 @@ sha256 = "d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9"
|
|
|
180
180
|
|
|
181
181
|
[[packages]]
|
|
182
182
|
name = "certifi"
|
|
183
|
-
version = "2026.10.
|
|
183
|
+
version = "2026.10.19"
|
|
184
184
|
marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups"
|
|
185
185
|
requires-python = ">=3.7"
|
|
186
186
|
index = "https://pypi.org/simple/"
|
|
187
187
|
|
|
188
188
|
[packages.sdist]
|
|
189
|
-
name = "certifi-2026.10.
|
|
190
|
-
upload-time = 2026-10-
|
|
191
|
-
url = "https://files.pythonhosted.org/packages/
|
|
192
|
-
size =
|
|
189
|
+
name = "certifi-2026.10.19.tar.gz"
|
|
190
|
+
upload-time = 2026-10-20 02:24:16.604828+00:00
|
|
191
|
+
url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz"
|
|
192
|
+
size = 137598
|
|
193
193
|
|
|
194
194
|
[packages.sdist.hashes]
|
|
195
|
-
sha256 = "
|
|
195
|
+
sha256 = "035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae"
|
|
196
196
|
|
|
197
197
|
[[packages.wheels]]
|
|
198
|
-
name = "certifi-2026.10.
|
|
199
|
-
upload-time = 2026-10-
|
|
200
|
-
url = "https://files.pythonhosted.org/packages/
|
|
201
|
-
size =
|
|
198
|
+
name = "certifi-2026.10.19-py3-none-any.whl"
|
|
199
|
+
upload-time = 2026-10-20 02:24:15.072385+00:00
|
|
200
|
+
url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl"
|
|
201
|
+
size = 136464
|
|
202
202
|
|
|
203
203
|
[packages.wheels.hashes]
|
|
204
|
-
sha256 = "
|
|
204
|
+
sha256 = "42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b"
|
|
205
205
|
|
|
206
206
|
[[packages]]
|
|
207
207
|
name = "cffi"
|
|
@@ -1942,28 +1942,28 @@ sha256 = "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"
|
|
|
1942
1942
|
|
|
1943
1943
|
[[packages]]
|
|
1944
1944
|
name = "setuptools"
|
|
1945
|
-
version = "84.
|
|
1945
|
+
version = "84.2.0"
|
|
1946
1946
|
marker = "\"build\" in dependency_groups or \"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups"
|
|
1947
1947
|
requires-python = ">=3.10"
|
|
1948
1948
|
index = "https://pypi.org/simple/"
|
|
1949
1949
|
|
|
1950
1950
|
[packages.sdist]
|
|
1951
|
-
name = "setuptools-84.
|
|
1952
|
-
upload-time = 2026-
|
|
1953
|
-
url = "https://files.pythonhosted.org/packages/
|
|
1954
|
-
size =
|
|
1951
|
+
name = "setuptools-84.2.0.tar.gz"
|
|
1952
|
+
upload-time = 2026-10-19 20:38:03.407523+00:00
|
|
1953
|
+
url = "https://files.pythonhosted.org/packages/63/40/e2c41d00a4bfea5548c31420a74c6cf149cc0e7c36b45c4c82202ce867aa/setuptools-84.2.0.tar.gz"
|
|
1954
|
+
size = 1227656
|
|
1955
1955
|
|
|
1956
1956
|
[packages.sdist.hashes]
|
|
1957
|
-
sha256 = "
|
|
1957
|
+
sha256 = "6f445ca2ce864605f81e223f262b4c94ffcb9a4a63473bae0ed7be5aab0291da"
|
|
1958
1958
|
|
|
1959
1959
|
[[packages.wheels]]
|
|
1960
|
-
name = "setuptools-84.
|
|
1961
|
-
upload-time = 2026-
|
|
1962
|
-
url = "https://files.pythonhosted.org/packages/
|
|
1963
|
-
size =
|
|
1960
|
+
name = "setuptools-84.2.0-py3-none-any.whl"
|
|
1961
|
+
upload-time = 2026-10-19 20:38:01.629093+00:00
|
|
1962
|
+
url = "https://files.pythonhosted.org/packages/dd/a2/2e1af99d56c8145aa69496026c7e5fd7b6ec759b500c3ad697e2062832dc/setuptools-84.2.0-py3-none-any.whl"
|
|
1963
|
+
size = 875816
|
|
1964
1964
|
|
|
1965
1965
|
[packages.wheels.hashes]
|
|
1966
|
-
sha256 = "
|
|
1966
|
+
sha256 = "8cc107ac7a403105dc8d6477664171f36135fab26adf8b5e0edaebc313f39b21"
|
|
1967
1967
|
|
|
1968
1968
|
[[packages]]
|
|
1969
1969
|
name = "setuptools-scm"
|
|
@@ -2047,7 +2047,7 @@ sha256 = "fa30e3ba4809cb81ce1f3209f2fbe3e779fc445f0439bc147a0d7c4601743f21"
|
|
|
2047
2047
|
|
|
2048
2048
|
[[packages]]
|
|
2049
2049
|
name = "sphinx"
|
|
2050
|
-
version = "9.1.
|
|
2050
|
+
version = "9.1.1"
|
|
2051
2051
|
marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups"
|
|
2052
2052
|
requires-python = ">=3.12"
|
|
2053
2053
|
dependencies = [
|
|
@@ -2071,22 +2071,22 @@ dependencies = [
|
|
|
2071
2071
|
index = "https://pypi.org/simple/"
|
|
2072
2072
|
|
|
2073
2073
|
[packages.sdist]
|
|
2074
|
-
name = "sphinx-9.1.
|
|
2075
|
-
upload-time =
|
|
2076
|
-
url = "https://files.pythonhosted.org/packages/
|
|
2077
|
-
size =
|
|
2074
|
+
name = "sphinx-9.1.1.tar.gz"
|
|
2075
|
+
upload-time = 2026-10-20 00:41:13.960930+00:00
|
|
2076
|
+
url = "https://files.pythonhosted.org/packages/fc/73/567727fc8c8d85bc3adc88a246e12937cfda8d565cf1d80e85bceaa61fe5/sphinx-9.1.1.tar.gz"
|
|
2077
|
+
size = 8725638
|
|
2078
2078
|
|
|
2079
2079
|
[packages.sdist.hashes]
|
|
2080
|
-
sha256 = "
|
|
2080
|
+
sha256 = "48b627f02908f22c601b3ba9c7b17265b4f315dd05c8bc64cd4da41183f44fbc"
|
|
2081
2081
|
|
|
2082
2082
|
[[packages.wheels]]
|
|
2083
|
-
name = "sphinx-9.1.
|
|
2084
|
-
upload-time =
|
|
2085
|
-
url = "https://files.pythonhosted.org/packages/
|
|
2086
|
-
size =
|
|
2083
|
+
name = "sphinx-9.1.1-py3-none-any.whl"
|
|
2084
|
+
upload-time = 2026-10-20 00:41:11.288695+00:00
|
|
2085
|
+
url = "https://files.pythonhosted.org/packages/47/18/3c66ea76da7de47e716b181c35bdbfafbd5e9af0bb247b14578e9de348e9/sphinx-9.1.1-py3-none-any.whl"
|
|
2086
|
+
size = 3923559
|
|
2087
2087
|
|
|
2088
2088
|
[packages.wheels.hashes]
|
|
2089
|
-
sha256 = "
|
|
2089
|
+
sha256 = "f8b60013ce25017be41a509e5c2d265bc1918410061315c36b809c7f335646c0"
|
|
2090
2090
|
|
|
2091
2091
|
[[packages]]
|
|
2092
2092
|
name = "sphinx-autodoc-typehints"
|
|
@@ -2627,28 +2627,28 @@ sha256 = "0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"
|
|
|
2627
2627
|
|
|
2628
2628
|
[[packages]]
|
|
2629
2629
|
name = "uv"
|
|
2630
|
-
version = "0.13.
|
|
2630
|
+
version = "0.13.3"
|
|
2631
2631
|
marker = "\"dev\" in dependency_groups or \"lint\" in dependency_groups or \"pkg-meta\" in dependency_groups"
|
|
2632
2632
|
requires-python = ">=3.8"
|
|
2633
2633
|
index = "https://pypi.org/simple/"
|
|
2634
2634
|
|
|
2635
2635
|
[packages.sdist]
|
|
2636
|
-
name = "uv-0.13.
|
|
2637
|
-
upload-time = 2026-10-
|
|
2638
|
-
url = "https://files.pythonhosted.org/packages/
|
|
2639
|
-
size =
|
|
2636
|
+
name = "uv-0.13.3.tar.gz"
|
|
2637
|
+
upload-time = 2026-10-20 01:13:40.241842+00:00
|
|
2638
|
+
url = "https://files.pythonhosted.org/packages/b2/0d/0cf59876b94f537055fdcc6a32126fd9abb841620b6589ae3fcbf6e21f69/uv-0.13.3.tar.gz"
|
|
2639
|
+
size = 7477898
|
|
2640
2640
|
|
|
2641
2641
|
[packages.sdist.hashes]
|
|
2642
|
-
sha256 = "
|
|
2642
|
+
sha256 = "fe8587cb414b324753eb2c1b2a54d2f93b2aed7e9cf02e9c1efa3776547a3aae"
|
|
2643
2643
|
|
|
2644
2644
|
[[packages.wheels]]
|
|
2645
|
-
name = "uv-0.13.
|
|
2646
|
-
upload-time = 2026-10-
|
|
2647
|
-
url = "https://files.pythonhosted.org/packages/
|
|
2648
|
-
size =
|
|
2645
|
+
name = "uv-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
|
2646
|
+
upload-time = 2026-10-20 01:13:11.825804+00:00
|
|
2647
|
+
url = "https://files.pythonhosted.org/packages/7e/fa/45e1751beda39226d5472e6bb068747115a06f19c2559a8810aac9b881b7/uv-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
|
2648
|
+
size = 17301030
|
|
2649
2649
|
|
|
2650
2650
|
[packages.wheels.hashes]
|
|
2651
|
-
sha256 = "
|
|
2651
|
+
sha256 = "8d9ba65db55f3701908b09394cb7d3248be28496aab6bd9a96e8e870261ad372"
|
|
2652
2652
|
|
|
2653
2653
|
[[packages]]
|
|
2654
2654
|
name = "vcs-versioning"
|
|
@@ -2771,7 +2771,7 @@ sha256 = "3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3"
|
|
|
2771
2771
|
|
|
2772
2772
|
[tool.nab]
|
|
2773
2773
|
nab-version = "0.0.22"
|
|
2774
|
-
created-at = 2026-10-
|
|
2774
|
+
created-at = 2026-10-27 13:49:49.388379+00:00
|
|
2775
2775
|
command-line = [
|
|
2776
2776
|
"nab",
|
|
2777
2777
|
"lock",
|
|
@@ -2779,9 +2779,6 @@ command-line = [
|
|
|
2779
2779
|
"--all-extras",
|
|
2780
2780
|
"--no-cache",
|
|
2781
2781
|
"--upgrade",
|
|
2782
|
-
"pyproject.toml",
|
|
2783
|
-
"--output",
|
|
2784
|
-
"pylock.toml",
|
|
2785
2782
|
]
|
|
2786
2783
|
input-path = "pyproject.toml"
|
|
2787
2784
|
mode = "universal"
|
|
@@ -150,4 +150,6 @@ def read_line(fd=0):
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
if __name__ == "__main__":
|
|
153
|
+
# printed paths may hold characters the stdout encoding cannot represent
|
|
154
|
+
sys.stdout.reconfigure(errors="backslashreplace") # type: ignore[unresolved-attribute] # stub says TextIO
|
|
153
155
|
sys.exit(run(sys.argv[1:]))
|
|
@@ -10,8 +10,6 @@ class BackendProxy:
|
|
|
10
10
|
|
|
11
11
|
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
|
|
12
12
|
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any: ...
|
|
13
|
-
def _exit(self) -> None: ...
|
|
14
|
-
def _optional_commands(self) -> dict[str, bool]: ...
|
|
15
13
|
|
|
16
14
|
def run(argv: Sequence[str]) -> int: ...
|
|
17
15
|
def read_line(fd: int = 0) -> bytearray: ...
|
|
@@ -152,6 +152,10 @@ class BackendFailed(RuntimeError): # ruff:ignore[error-suffix-on-exception-name
|
|
|
152
152
|
#: the string representation of the exception thrown
|
|
153
153
|
self.exc_msg: str = result.get("exc_msg", "missing Exception message")
|
|
154
154
|
|
|
155
|
+
def __reduce__(self) -> tuple[type[BackendFailed], tuple[dict[str, Any], str, str], dict[str, Any]]:
|
|
156
|
+
result = {"code": self.code, "exc_type": self.exc_type, "exc_msg": self.exc_msg}
|
|
157
|
+
return self.__class__, (result, self.out, self.err), self.__dict__
|
|
158
|
+
|
|
155
159
|
def __str__(self) -> str:
|
|
156
160
|
return (
|
|
157
161
|
f"packaging backend failed{'' if self.code is None else f' (code={self.code})'}, "
|
|
@@ -495,9 +499,15 @@ class Frontend(ABC):
|
|
|
495
499
|
with ZipFile(str(wheel), "r") as zip_file:
|
|
496
500
|
for name in zip_file.namelist(): # pragma: no branch
|
|
497
501
|
root = Path(name).parts[0]
|
|
498
|
-
if root.endswith(".dist-info"):
|
|
499
|
-
|
|
500
|
-
|
|
502
|
+
if not root.endswith(".dist-info"):
|
|
503
|
+
continue
|
|
504
|
+
if basename is None:
|
|
505
|
+
destination: Final[Path] = metadata_directory / root
|
|
506
|
+
if destination.is_symlink(): # pragma: win32 no cover
|
|
507
|
+
destination.unlink()
|
|
508
|
+
ensure_empty_dir(destination)
|
|
509
|
+
basename = root
|
|
510
|
+
zip_file.extract(name, extract_to)
|
|
501
511
|
if basename is None: # pragma: no branch
|
|
502
512
|
msg = f"no .dist-info found inside generated wheel {wheel}"
|
|
503
513
|
raise RuntimeError(msg)
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.12.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 12,
|
|
21
|
+
__version__ = version = '1.12.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 12, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from subprocess import PIPE, Popen # ruff:ignore[suspicious-subprocess-import]
|
|
7
7
|
from threading import Thread
|
|
8
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
9
9
|
|
|
10
10
|
from ._frontend import CmdStatus, Frontend
|
|
11
11
|
|
|
@@ -64,7 +64,7 @@ class SubprocessFrontend(Frontend):
|
|
|
64
64
|
@contextmanager
|
|
65
65
|
def _send_msg(self, cmd: str, result_file: Path, msg: str) -> Generator[SubprocessCmdStatus, None, None]: # ruff:ignore[unused-method-argument]
|
|
66
66
|
env = os.environ.copy()
|
|
67
|
-
backend = os.pathsep.join(str(i) for i in self._backend_paths)
|
|
67
|
+
backend = os.pathsep.join(str(i) for i in self._backend_paths)
|
|
68
68
|
if backend:
|
|
69
69
|
env["PYTHONPATH"] = backend
|
|
70
70
|
process = Popen( # ruff:ignore[subprocess-without-shell-equals-true] # fixed argv, no shell
|
|
@@ -77,7 +77,12 @@ class SubprocessFrontend(Frontend):
|
|
|
77
77
|
cwd=self._root,
|
|
78
78
|
env=env,
|
|
79
79
|
)
|
|
80
|
-
|
|
80
|
+
status: Final = SubprocessCmdStatus(process, f"{os.linesep}{msg}{os.linesep}")
|
|
81
|
+
try:
|
|
82
|
+
yield status
|
|
83
|
+
finally:
|
|
84
|
+
process.kill()
|
|
85
|
+
status.join()
|
|
81
86
|
|
|
82
87
|
def send_cmd(self, cmd: str, **kwargs: Any) -> tuple[Any, str, str]:
|
|
83
88
|
"""
|
|
@@ -5,11 +5,11 @@ import os
|
|
|
5
5
|
import subprocess # ruff:ignore[suspicious-subprocess-import]
|
|
6
6
|
import sys
|
|
7
7
|
from textwrap import dedent
|
|
8
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from pyproject_api import _backend
|
|
12
|
+
from pyproject_api import SubprocessFrontend, _backend
|
|
13
13
|
from pyproject_api._backend import BackendProxy, read_line, run
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
@@ -222,3 +222,14 @@ def test_read_line_eof_at_the_beginning() -> None:
|
|
|
222
222
|
read_line(fd=r)
|
|
223
223
|
finally:
|
|
224
224
|
os.close(r)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_backend_output_with_unencodable_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
228
|
+
monkeypatch.setenv("PYTHONIOENCODING", "ascii") # like a Windows ANSI code page that lacks a path character
|
|
229
|
+
root: Final[Path] = tmp_path / "pröject"
|
|
230
|
+
root.mkdir()
|
|
231
|
+
(root / "build_tester.py").write_text("def build_sdist(sdist_directory, config_settings=None): return 'out.tar.gz'")
|
|
232
|
+
|
|
233
|
+
result: Final = SubprocessFrontend(root, (root,), "build_tester", None, ()).build_sdist(root / "dist")
|
|
234
|
+
|
|
235
|
+
assert (result.sdist, "pr\\xf6ject" in result.out) == (root / "dist" / "out.tar.gz", True)
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import pickle # ruff:ignore[suspicious-pickle-import]
|
|
3
4
|
import re
|
|
4
5
|
import subprocess # ruff:ignore[suspicious-subprocess-import]
|
|
5
6
|
import sys
|
|
7
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
8
|
+
from multiprocessing import get_context
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from textwrap import dedent
|
|
8
|
-
from typing import TYPE_CHECKING, Final, Literal
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Final, Literal
|
|
9
12
|
|
|
10
13
|
import pytest
|
|
11
14
|
from packaging.requirements import Requirement
|
|
12
15
|
|
|
16
|
+
from pyproject_api import _via_fresh_subprocess
|
|
13
17
|
from pyproject_api._frontend import BackendFailed
|
|
14
18
|
from pyproject_api._via_fresh_subprocess import SubprocessFrontend
|
|
15
19
|
|
|
@@ -55,6 +59,18 @@ def test_relative_project_folder(local_builder: Callable[[str], Path], monkeypat
|
|
|
55
59
|
assert frontend.get_requires_for_build_wheel().requires == (Requirement("wheel"),)
|
|
56
60
|
|
|
57
61
|
|
|
62
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Windows drops trailing spaces from names")
|
|
63
|
+
def test_backend_path_with_trailing_space(tmp_path: Path) -> None: # pragma: win32 no cover
|
|
64
|
+
backend: Final = tmp_path / "backend "
|
|
65
|
+
backend.mkdir()
|
|
66
|
+
(backend / "build_tester.py").write_text(
|
|
67
|
+
"def get_requires_for_build_wheel(config_settings=None):\n return ['wheel']\n"
|
|
68
|
+
)
|
|
69
|
+
frontend: Final = SubprocessFrontend(tmp_path, (backend,), "build_tester", None, ())
|
|
70
|
+
|
|
71
|
+
assert frontend.get_requires_for_build_wheel().requires == (Requirement("wheel"),)
|
|
72
|
+
|
|
73
|
+
|
|
58
74
|
@pytest.mark.parametrize(
|
|
59
75
|
"hook",
|
|
60
76
|
[
|
|
@@ -161,6 +177,39 @@ def test_large_command_with_failed_backend(local_builder: Callable[[str], Path])
|
|
|
161
177
|
assert "RuntimeError: backend import failed" in context.value.err
|
|
162
178
|
|
|
163
179
|
|
|
180
|
+
@pytest.mark.parametrize(
|
|
181
|
+
"error_type",
|
|
182
|
+
[
|
|
183
|
+
pytest.param(KeyboardInterrupt, id="keyboard-interrupt"),
|
|
184
|
+
pytest.param(SystemExit, id="system-exit"),
|
|
185
|
+
pytest.param(RuntimeError, id="runtime-error"),
|
|
186
|
+
],
|
|
187
|
+
)
|
|
188
|
+
def test_interrupted_frontend_cleans_up_backend(
|
|
189
|
+
error_type: type[BaseException], local_builder: Callable[[str], Path], mocker: pytest_mock.MockerFixture
|
|
190
|
+
) -> None:
|
|
191
|
+
root: Final = local_builder("""
|
|
192
|
+
import time
|
|
193
|
+
|
|
194
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
195
|
+
time.sleep(60)
|
|
196
|
+
return "demo-1.0-py3-none-any.whl"
|
|
197
|
+
""")
|
|
198
|
+
status_factory: Final = mocker.spy(_via_fresh_subprocess, "SubprocessCmdStatus")
|
|
199
|
+
mocker.patch("pyproject_api._frontend.sleep", side_effect=error_type("build interrupted"))
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
with pytest.raises(error_type):
|
|
203
|
+
SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(root)[:-1]).build_wheel(root / "dist")
|
|
204
|
+
|
|
205
|
+
assert not status_factory.spy_return.is_alive()
|
|
206
|
+
finally:
|
|
207
|
+
# Keep a regression from leaving the backend and its non-daemon thread running.
|
|
208
|
+
status: Final = status_factory.spy_return
|
|
209
|
+
status.process.kill()
|
|
210
|
+
status.join(timeout=10)
|
|
211
|
+
|
|
212
|
+
|
|
164
213
|
@pytest.mark.parametrize("cmd", ["build_wheel", "build_sdist"])
|
|
165
214
|
def test_missing_required_cmd(cmd: str, local_builder: Callable[[str], Path]) -> None:
|
|
166
215
|
tmp_path = local_builder("")
|
|
@@ -561,6 +610,43 @@ def test_metadata_from_built_wheel(
|
|
|
561
610
|
assert not err
|
|
562
611
|
|
|
563
612
|
|
|
613
|
+
@pytest.mark.parametrize("target", [pytest.param("wheel", id="wheel"), pytest.param("editable", id="editable")])
|
|
614
|
+
def test_metadata_from_built_removes_stale_files(
|
|
615
|
+
demo_pkg_inline: Path, tmp_path: Path, target: Literal["wheel", "editable"]
|
|
616
|
+
) -> None:
|
|
617
|
+
metadata: Final[Path] = tmp_path / "demo_pkg_inline-1.0.0.dist-info"
|
|
618
|
+
metadata.mkdir()
|
|
619
|
+
(metadata / "entry_points.txt").write_text("obsolete entry points")
|
|
620
|
+
(tmp_path / "unrelated.txt").write_text("keep")
|
|
621
|
+
|
|
622
|
+
metadata_path: Final[Path] = SubprocessFrontend(
|
|
623
|
+
*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]
|
|
624
|
+
).metadata_from_built(tmp_path, target)[0]
|
|
625
|
+
|
|
626
|
+
assert {entry.name for entry in metadata_path.iterdir()} == {"top_level.txt", "WHEEL", "RECORD", "METADATA"}
|
|
627
|
+
assert (tmp_path / "unrelated.txt").read_text() == "keep"
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Creating symlinks requires privileges")
|
|
631
|
+
def test_metadata_from_built_replaces_directory_symlink(
|
|
632
|
+
demo_pkg_inline: Path, tmp_path: Path
|
|
633
|
+
) -> None: # pragma: win32 no cover
|
|
634
|
+
external: Final[Path] = tmp_path / "external"
|
|
635
|
+
external.mkdir()
|
|
636
|
+
(external / "keep.txt").write_text("keep")
|
|
637
|
+
output: Final[Path] = tmp_path / "metadata"
|
|
638
|
+
output.mkdir()
|
|
639
|
+
(output / "demo_pkg_inline-1.0.0.dist-info").symlink_to(external, target_is_directory=True)
|
|
640
|
+
|
|
641
|
+
metadata_path: Final[Path] = SubprocessFrontend(
|
|
642
|
+
*SubprocessFrontend.create_args_from_folder(demo_pkg_inline)[:-1]
|
|
643
|
+
).metadata_from_built(output, "wheel")[0]
|
|
644
|
+
|
|
645
|
+
assert not metadata_path.is_symlink()
|
|
646
|
+
assert (metadata_path / "METADATA").is_file()
|
|
647
|
+
assert {entry.name: entry.read_text() for entry in external.iterdir()} == {"keep.txt": "keep"}
|
|
648
|
+
|
|
649
|
+
|
|
564
650
|
def test_bad_wheel_metadata_from_built_wheel(local_builder: Callable[[str], Path]) -> None:
|
|
565
651
|
txt = """
|
|
566
652
|
import sys
|
|
@@ -721,3 +807,46 @@ def test_backend_failed_str_repr() -> None:
|
|
|
721
807
|
|
|
722
808
|
exc = BackendFailed({"code": 3, "exc_type": "Error", "exc_msg": "msg"}, "out\n", "err\n")
|
|
723
809
|
assert str(exc) == "packaging backend failed (code=3), with Error: msg\nerr\nout"
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
|
|
813
|
+
@pytest.mark.parametrize("result", [{}, {"code": None}, {"code": 1, "exc_type": "TypeError", "exc_msg": "bad"}])
|
|
814
|
+
def test_backend_failed_pickle(protocol: int, result: dict[str, Any]) -> None:
|
|
815
|
+
exc = BackendFailed(result, "backend stdout", "backend stderr")
|
|
816
|
+
exc.add_note("building a wheel")
|
|
817
|
+
restored = pickle.loads( # ruff:ignore[suspicious-pickle-usage]
|
|
818
|
+
pickle.dumps(exc, protocol=protocol)
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
assert type(restored) is BackendFailed
|
|
822
|
+
assert restored.args == exc.args == ()
|
|
823
|
+
assert restored.__dict__ == exc.__dict__
|
|
824
|
+
assert str(restored) == str(exc)
|
|
825
|
+
assert repr(restored) == repr(exc)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _build_wheel_in_process(root: Path) -> str:
|
|
829
|
+
frontend = SubprocessFrontend(*SubprocessFrontend.create_args_from_folder(root)[:-1])
|
|
830
|
+
return str(frontend.build_wheel(root / "dist").wheel)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def test_backend_failed_process_pool(local_builder: Callable[[str], Path]) -> None:
|
|
834
|
+
root = local_builder("""
|
|
835
|
+
import sys
|
|
836
|
+
|
|
837
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
838
|
+
print("backend stdout")
|
|
839
|
+
print("backend stderr", file=sys.stderr)
|
|
840
|
+
raise RuntimeError("build failed")
|
|
841
|
+
""")
|
|
842
|
+
with ProcessPoolExecutor(max_workers=1, mp_context=get_context("spawn")) as executor:
|
|
843
|
+
future = executor.submit(_build_wheel_in_process, root)
|
|
844
|
+
with pytest.raises(BackendFailed) as context:
|
|
845
|
+
future.result(timeout=30)
|
|
846
|
+
exc = context.value
|
|
847
|
+
assert exc.args == ()
|
|
848
|
+
assert exc.code == 1
|
|
849
|
+
assert exc.exc_type == "RuntimeError"
|
|
850
|
+
assert exc.exc_msg == "build failed"
|
|
851
|
+
assert "backend stdout" in exc.out
|
|
852
|
+
assert "backend stderr" in exc.err
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Security Policy
|
|
2
|
-
|
|
3
|
-
## Supported Versions
|
|
4
|
-
|
|
5
|
-
Only the latest released version is supported with security updates. Older versions will not receive patches.
|
|
6
|
-
|
|
7
|
-
## Reporting a Vulnerability
|
|
8
|
-
|
|
9
|
-
Please report security vulnerabilities via [GitHub's private vulnerability reporting](../../security/advisories/new).
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
updates:
|
|
3
|
-
- package-ecosystem: "github-actions"
|
|
4
|
-
directory: "/"
|
|
5
|
-
labels:
|
|
6
|
-
- dependencies
|
|
7
|
-
- github_actions
|
|
8
|
-
- skip news
|
|
9
|
-
schedule:
|
|
10
|
-
interval: "weekly"
|
|
11
|
-
day: "tuesday"
|
|
12
|
-
groups:
|
|
13
|
-
github-actions:
|
|
14
|
-
patterns:
|
|
15
|
-
- "*"
|
|
16
|
-
cooldown:
|
|
17
|
-
default-days: 7
|
|
18
|
-
- package-ecosystem: "pre-commit"
|
|
19
|
-
directory: "/"
|
|
20
|
-
labels:
|
|
21
|
-
- dependencies
|
|
22
|
-
- skip news
|
|
23
|
-
schedule:
|
|
24
|
-
interval: "weekly"
|
|
25
|
-
day: "tuesday"
|
|
26
|
-
cooldown:
|
|
27
|
-
default-days: 7
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|