pyproject-api 1.12.4__tar.gz → 1.12.5__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.4 → pyproject_api-1.12.5}/.github/workflows/check.yaml +19 -11
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/workflows/prepare-release.yaml +9 -2
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/CONTRIBUTING.md +4 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/PKG-INFO +1 -1
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/api.rst +8 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/changelog.rst +16 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/_frontend.py +2 -5
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/_util.py +5 -2
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/_version.py +2 -2
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/test_frontend.py +27 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/test_frontend_setuptools.py +1 -2
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/test_util.py +41 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/FUNDING.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/SECURITY.md +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/THREAT_MODEL.md +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/dependabot.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/release.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/workflows/codeql.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/workflows/news.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/workflows/release.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/workflows/scorecard.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.github/workflows/upgrade-release-tools.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.gitignore +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.pre-commit-config.yaml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/.readthedocs.yml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/CODE_OF_CONDUCT.md +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/LICENSE +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/README.md +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/changelog.d/.gitkeep +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/changelog.d/template.jinja2 +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/conf.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/contributing.rst +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/docs/index.rst +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/pylock.toml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/pyproject.toml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/__init__.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/__main__.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/_backend.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/_backend.pyi +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/_via_fresh_subprocess.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/src/pyproject_api/py.typed +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/_build_sdist.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/demo_pkg_inline/build.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/demo_pkg_inline/pyproject.toml +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/test_backend.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/test_main.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tests/test_version.py +0 -0
- {pyproject_api-1.12.4 → pyproject_api-1.12.5}/tox.toml +0 -0
|
@@ -7,16 +7,16 @@ on:
|
|
|
7
7
|
pull_request:
|
|
8
8
|
types: [opened, synchronize, reopened]
|
|
9
9
|
schedule:
|
|
10
|
-
- cron: "
|
|
10
|
+
- cron: "43 7 * * *"
|
|
11
11
|
concurrency:
|
|
12
12
|
group: check-${{ github.ref }}
|
|
13
|
-
cancel-in-progress:
|
|
13
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
14
14
|
permissions:
|
|
15
15
|
contents: read
|
|
16
16
|
jobs:
|
|
17
17
|
test:
|
|
18
18
|
name: 🧪 Test
|
|
19
|
-
uses: tox-dev/github-workflows/.github/workflows/check.yaml@
|
|
19
|
+
uses: tox-dev/github-workflows/.github/workflows/check.yaml@13381289334b15375958be7f7438f3c3d860e575 # 6.5.5
|
|
20
20
|
permissions:
|
|
21
21
|
contents: read
|
|
22
22
|
with:
|
|
@@ -40,20 +40,28 @@ jobs:
|
|
|
40
40
|
{"env": "type", "os": "windows-2025-vs2026"},
|
|
41
41
|
{"env": "dev", "os": "windows-2025-vs2026"},
|
|
42
42
|
{"env": "pkg_meta", "os": "windows-2025-vs2026"},
|
|
43
|
-
{"env": "3.15", "os": "macos-26"}
|
|
44
|
-
{"env": "3.15t", "python": "3.15t", "os": "macos-26"},
|
|
45
|
-
{"env": "3.14", "python": "3.14", "os": "macos-26"},
|
|
46
|
-
{"env": "3.13", "python": "3.13", "os": "macos-26"},
|
|
47
|
-
{"env": "3.12", "python": "3.12", "os": "macos-26"},
|
|
48
|
-
{"env": "3.11", "python": "3.11", "os": "macos-26"}
|
|
43
|
+
{"env": "3.15", "os": "macos-26"}
|
|
49
44
|
]
|
|
45
|
+
release:
|
|
46
|
+
name: 🏷️ Release dry run
|
|
47
|
+
uses: $/.github/workflows/prepare-release.yaml
|
|
48
|
+
permissions:
|
|
49
|
+
contents: read
|
|
50
|
+
pull-requests: read
|
|
51
|
+
with:
|
|
52
|
+
dry-run: true
|
|
53
|
+
hooks:
|
|
54
|
+
name: 🧹 Hooks under the cooldown
|
|
55
|
+
uses: tox-dev/github-workflows/.github/workflows/cooled-hooks.yaml@0c4cffd63033d38196319524d101caae1eef925b # 6.7.2
|
|
56
|
+
permissions:
|
|
57
|
+
contents: read
|
|
50
58
|
gate:
|
|
51
59
|
name: 🚦 ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Non-PR checks pass' }}
|
|
52
60
|
if: ${{ !cancelled() }} # codespell:ignore cancelled
|
|
53
|
-
needs: [test]
|
|
61
|
+
needs: [test, release, hooks]
|
|
54
62
|
runs-on: ubuntu-26.04
|
|
55
63
|
steps:
|
|
56
64
|
- name: 🚦 Require all needed jobs to pass
|
|
57
|
-
uses: tox-dev/github-workflows/required-checks@
|
|
65
|
+
uses: tox-dev/github-workflows/required-checks@13381289334b15375958be7f7438f3c3d860e575 # 6.5.5
|
|
58
66
|
with:
|
|
59
67
|
needs: ${{ toJSON(needs) }}
|
|
@@ -12,15 +12,22 @@ on:
|
|
|
12
12
|
- minor
|
|
13
13
|
- patch
|
|
14
14
|
default: auto
|
|
15
|
+
workflow_call:
|
|
16
|
+
inputs:
|
|
17
|
+
dry-run:
|
|
18
|
+
description: Assemble the release without pushing, for the pull request gate
|
|
19
|
+
type: boolean
|
|
20
|
+
required: true
|
|
15
21
|
permissions: {}
|
|
16
22
|
jobs:
|
|
17
23
|
prepare:
|
|
18
24
|
name: 🏷️ Prepare Towncrier release
|
|
19
|
-
uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@
|
|
25
|
+
uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@0c4cffd63033d38196319524d101caae1eef925b # 6.7.2
|
|
20
26
|
permissions:
|
|
21
27
|
contents: read
|
|
22
28
|
pull-requests: read
|
|
23
29
|
secrets:
|
|
24
30
|
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
|
25
31
|
with:
|
|
26
|
-
bump: ${{ inputs.bump }}
|
|
32
|
+
bump: ${{ inputs.bump || 'auto' }}
|
|
33
|
+
dry-run: ${{ inputs.dry-run == true }}
|
|
@@ -2,3 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
See the [contributing guide](https://pyproject-api.readthedocs.io/en/latest/contributing.html) for release note rules
|
|
4
4
|
and local checks.
|
|
5
|
+
|
|
6
|
+
## Tests
|
|
7
|
+
|
|
8
|
+
Add a test for every new feature and bug fix. A change without a test needs a reason in the pull request description.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyproject-api
|
|
3
|
-
Version: 1.12.
|
|
3
|
+
Version: 1.12.5
|
|
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
|
|
@@ -16,6 +16,14 @@ backend runs from the project directory. Result paths retain the spelling of the
|
|
|
16
16
|
directory argument. Configuration settings and generic command arguments are
|
|
17
17
|
passed through unchanged.
|
|
18
18
|
|
|
19
|
+
Metadata preparation replaces a symlink used as the output directory with a real directory, leaving the link target
|
|
20
|
+
untouched. Dangling links are replaced too. Paths resolving to the project root or one of its ancestors are still
|
|
21
|
+
rejected before cleanup.
|
|
22
|
+
|
|
23
|
+
Metadata preparation clears the output directory before calling the metadata hook. If cleanup fails, the filesystem
|
|
24
|
+
error propagates and the hook is not called. When extracting metadata from a built wheel, cleanup errors stop extraction
|
|
25
|
+
after the wheel has been built.
|
|
26
|
+
|
|
19
27
|
.. autoclass:: OptionalHooks
|
|
20
28
|
|
|
21
29
|
Exceptions
|
|
@@ -5,6 +5,22 @@ Release History
|
|
|
5
5
|
|
|
6
6
|
.. towncrier release notes start
|
|
7
7
|
|
|
8
|
+
.. _release-1-12-5:
|
|
9
|
+
|
|
10
|
+
v1.12.5 (2026-11-07)
|
|
11
|
+
--------------------
|
|
12
|
+
|
|
13
|
+
.. _release-1-12-5-fixed:
|
|
14
|
+
|
|
15
|
+
Fixed
|
|
16
|
+
^^^^^
|
|
17
|
+
|
|
18
|
+
- Propagate filesystem errors when clearing nested metadata directories (:pr:`357`) by :user:`Quinnod345` and
|
|
19
|
+
:user:`gaborbernat`.
|
|
20
|
+
- Replace a symlink passed to ``ensure_empty_dir`` instead of following it, so a dangling link no longer raises
|
|
21
|
+
``FileExistsError`` and a link to a directory no longer has its target emptied (:pr:`354`) by :user:`abhishekcpp007`
|
|
22
|
+
and :user:`gaborbernat`.
|
|
23
|
+
|
|
8
24
|
.. _release-1-12-4:
|
|
9
25
|
|
|
10
26
|
v1.12.4 (2026-11-04)
|
|
@@ -362,7 +362,7 @@ class Frontend(ABC):
|
|
|
362
362
|
if metadata_path in project_root.parents:
|
|
363
363
|
ancestor_message: Final[str] = f"the metadata directory can't contain the project root {self._root}"
|
|
364
364
|
raise RuntimeError(ancestor_message)
|
|
365
|
-
if metadata_directory.exists():
|
|
365
|
+
if metadata_directory.is_symlink() or metadata_directory.exists():
|
|
366
366
|
ensure_empty_dir(metadata_directory)
|
|
367
367
|
metadata_directory.mkdir(parents=True, exist_ok=True)
|
|
368
368
|
|
|
@@ -502,10 +502,7 @@ class Frontend(ABC):
|
|
|
502
502
|
if not root.endswith(".dist-info"):
|
|
503
503
|
continue
|
|
504
504
|
if basename is None:
|
|
505
|
-
|
|
506
|
-
if destination.is_symlink(): # pragma: win32 no cover
|
|
507
|
-
destination.unlink()
|
|
508
|
-
ensure_empty_dir(destination)
|
|
505
|
+
ensure_empty_dir(metadata_directory / root)
|
|
509
506
|
basename = root
|
|
510
507
|
zip_file.extract(name, extract_to)
|
|
511
508
|
if basename is None: # pragma: no branch
|
|
@@ -8,11 +8,14 @@ if TYPE_CHECKING:
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def ensure_empty_dir(path: Path) -> None:
|
|
11
|
-
if path.
|
|
11
|
+
if path.is_symlink():
|
|
12
|
+
path.unlink()
|
|
13
|
+
path.mkdir()
|
|
14
|
+
elif path.exists():
|
|
12
15
|
if path.is_dir():
|
|
13
16
|
for sub_path in path.iterdir():
|
|
14
17
|
if sub_path.is_dir() and not sub_path.is_symlink():
|
|
15
|
-
rmtree(sub_path
|
|
18
|
+
rmtree(sub_path)
|
|
16
19
|
else:
|
|
17
20
|
sub_path.unlink()
|
|
18
21
|
else:
|
|
@@ -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.5'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 12, 5)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -585,6 +585,33 @@ def test_prepare_metadata_allows_sibling_directory(tmp_path: Path, hook: str) ->
|
|
|
585
585
|
assert list(metadata_directory.iterdir()) == []
|
|
586
586
|
|
|
587
587
|
|
|
588
|
+
@pytest.mark.parametrize(
|
|
589
|
+
"hook",
|
|
590
|
+
[
|
|
591
|
+
pytest.param("prepare_metadata_for_build_wheel", id="wheel"),
|
|
592
|
+
pytest.param("prepare_metadata_for_build_editable", id="editable"),
|
|
593
|
+
],
|
|
594
|
+
)
|
|
595
|
+
@pytest.mark.parametrize(
|
|
596
|
+
"target_name", [pytest.param("missing", id="dangling"), pytest.param("shared", id="directory")]
|
|
597
|
+
)
|
|
598
|
+
def test_prepare_metadata_replaces_symlink(tmp_path: Path, hook: str, target_name: str) -> None:
|
|
599
|
+
root: Final[Path] = tmp_path / "project"
|
|
600
|
+
root.mkdir()
|
|
601
|
+
(root / "build_tester.py").write_text("")
|
|
602
|
+
shared: Final[Path] = tmp_path / "shared"
|
|
603
|
+
shared.mkdir()
|
|
604
|
+
(shared / "keep.txt").write_text("keep me")
|
|
605
|
+
metadata_directory: Final[Path] = tmp_path / "project-metadata"
|
|
606
|
+
metadata_directory.symlink_to(tmp_path / target_name, target_is_directory=True)
|
|
607
|
+
|
|
608
|
+
assert getattr(SubprocessFrontend(root, (root,), "build_tester", None, ()), hook)(metadata_directory) is None
|
|
609
|
+
|
|
610
|
+
assert not metadata_directory.is_symlink()
|
|
611
|
+
assert list(metadata_directory.iterdir()) == []
|
|
612
|
+
assert (shared / "keep.txt").read_text() == "keep me"
|
|
613
|
+
|
|
614
|
+
|
|
588
615
|
def test_no_wheel_prepare_metadata_for_build_wheel(local_builder: Callable[[str], Path]) -> None:
|
|
589
616
|
txt = "def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return 'out'"
|
|
590
617
|
tmp_path = local_builder(txt)
|
|
@@ -135,8 +135,7 @@ def test_setuptools_exception(frontend_setuptools: SubprocessFrontend) -> None:
|
|
|
135
135
|
|
|
136
136
|
def test_bad_message(frontend_setuptools: SubprocessFrontend, tmp_path: Path) -> None:
|
|
137
137
|
with frontend_setuptools._send_msg("bad_cmd", tmp_path / "a", "{{") as status: # ruff:ignore[private-member-access]
|
|
138
|
-
|
|
139
|
-
pass
|
|
138
|
+
status.join()
|
|
140
139
|
out, err = status.out_err()
|
|
141
140
|
assert out
|
|
142
141
|
assert "Backend: incorrect request to backend: bytearray(b'{{')" in err
|
|
@@ -2,11 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
5
7
|
from pyproject_api._util import ensure_empty_dir
|
|
6
8
|
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
|
|
12
|
+
from pytest_mock import MockerFixture
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
def test_ensure_empty_dir_on_empty(tmp_path: Path) -> None:
|
|
12
16
|
ensure_empty_dir(tmp_path)
|
|
@@ -45,6 +49,29 @@ def test_ensure_empty_dir_on_path_folder(tmp_path: Path) -> None:
|
|
|
45
49
|
assert list(tmp_path.iterdir()) == []
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
@pytest.mark.parametrize(
|
|
53
|
+
("target_name", "target_is_directory"),
|
|
54
|
+
[
|
|
55
|
+
pytest.param("missing", False, id="dangling-file"),
|
|
56
|
+
pytest.param("missing", True, id="dangling-directory"),
|
|
57
|
+
pytest.param("file", False, id="file"),
|
|
58
|
+
pytest.param("folder", True, id="directory"),
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
def test_ensure_empty_dir_replaces_symlink(tmp_path: Path, target_name: str, target_is_directory: bool) -> None:
|
|
62
|
+
(tmp_path / "file").write_text("keep file")
|
|
63
|
+
folder = tmp_path / "folder"
|
|
64
|
+
folder.mkdir()
|
|
65
|
+
(folder / "c").write_text("keep folder contents")
|
|
66
|
+
path = tmp_path / "a"
|
|
67
|
+
path.symlink_to(tmp_path / target_name, target_is_directory=target_is_directory)
|
|
68
|
+
ensure_empty_dir(path)
|
|
69
|
+
assert not path.is_symlink()
|
|
70
|
+
assert list(path.iterdir()) == []
|
|
71
|
+
assert (tmp_path / "file").read_text() == "keep file"
|
|
72
|
+
assert (folder / "c").read_text() == "keep folder contents"
|
|
73
|
+
|
|
74
|
+
|
|
48
75
|
def test_ensure_empty_dir_on_path_symlink_to_folder(tmp_path: Path) -> None:
|
|
49
76
|
target = tmp_path / "target"
|
|
50
77
|
target.mkdir()
|
|
@@ -55,3 +82,17 @@ def test_ensure_empty_dir_on_path_symlink_to_folder(tmp_path: Path) -> None:
|
|
|
55
82
|
ensure_empty_dir(path)
|
|
56
83
|
assert list(path.iterdir()) == []
|
|
57
84
|
assert list(target.iterdir()) == [target / "c"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_nested_cleanup_error_propagates(tmp_path: Path, mocker: MockerFixture) -> None:
|
|
88
|
+
blocked = tmp_path / "blocked"
|
|
89
|
+
blocked.mkdir()
|
|
90
|
+
stale = blocked / "stale.txt"
|
|
91
|
+
stale.write_text("stale metadata")
|
|
92
|
+
failure = PermissionError("file removal refused")
|
|
93
|
+
mocker.patch("os.unlink", autospec=True, side_effect=failure)
|
|
94
|
+
with pytest.raises(PermissionError) as captured:
|
|
95
|
+
ensure_empty_dir(tmp_path)
|
|
96
|
+
|
|
97
|
+
assert captured.value is failure
|
|
98
|
+
assert stale.read_text() == "stale metadata"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|