Skip to content

Commit 1ee619e

Browse files
committed
feat: support PEP 639 (except normalization)
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 7a603c4 commit 1ee619e

File tree

8 files changed

+151
-27
lines changed

8 files changed

+151
-27
lines changed

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ version.source = "vcs"
126126
build.hooks.vcs.version-file = "src/scikit_build_core/_version.py"
127127

128128

129+
[tool.uv]
130+
dev-dependencies = ["scikit-build-core[test]"]
131+
environments = ["python_version >= '3.10'"]
132+
129133
[tool.uv.pip]
130134
reinstall-package = ["scikit-build-core"]
131135

src/scikit_build_core/build/wheel.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ def _build_wheel_impl_impl(
235235
msg = "project.version is not specified, must be statically present or tool.scikit-build metadata.version.provider configured when dynamic"
236236
raise AssertionError(msg)
237237

238+
# Verify PEP 639 replaces license-files
239+
if metadata.license_files is not None and settings.wheel.license_files:
240+
msg = "Both project.license-files and tool.scikit-build.wheel.license-files are set, use only one"
241+
raise AssertionError(msg)
242+
238243
# Get the closest (normally) importable name
239244
normalized_name = metadata.name.replace("-", "_").replace(".", "_")
240245

@@ -313,20 +318,30 @@ def _build_wheel_impl_impl(
313318
install_dir = wheel_dirs[targetlib] / settings.wheel.install_dir
314319

315320
# Include the metadata license.file entry if provided
316-
license_file_globs = list(settings.wheel.license_files)
317-
if (
318-
metadata.license
319-
and not isinstance(metadata.license, str)
320-
and metadata.license.file
321-
):
322-
license_file_globs.append(str(metadata.license.file))
323-
324-
for y in license_file_globs:
325-
for x in Path().glob(y):
326-
if x.is_file():
327-
path = wheel_dirs["metadata"] / "licenses" / x
328-
path.parent.mkdir(parents=True, exist_ok=True)
329-
shutil.copy(x, path)
321+
if metadata.license_files:
322+
license_paths = metadata.license_files
323+
else:
324+
license_file_globs = settings.wheel.license_files or [
325+
"LICEN[CS]E*",
326+
"COPYING*",
327+
"NOTICE*",
328+
"AUTHORS*",
329+
]
330+
if (
331+
metadata.license
332+
and not isinstance(metadata.license, str)
333+
and metadata.license.file
334+
):
335+
license_file_globs.append(str(metadata.license.file))
336+
337+
license_paths = [
338+
x for y in license_file_globs for x in Path().glob(y) if x.is_file()
339+
]
340+
341+
for x in license_paths:
342+
path = wheel_dirs["metadata"] / "licenses" / x
343+
path.parent.mkdir(parents=True, exist_ok=True)
344+
shutil.copy(x, path)
330345

331346
if (
332347
settings.wheel.license_files

src/scikit_build_core/settings/skbuild_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,11 @@ class WheelSettings:
199199
root, giving access to "/platlib", "/data", "/headers", and "/scripts".
200200
"""
201201

202-
license_files: List[str] = dataclasses.field(
203-
default_factory=lambda: ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]
204-
)
202+
license_files: Optional[List[str]] = None
205203
"""
206204
A list of license files to include in the wheel. Supports glob patterns.
205+
The default is ``["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]``.
206+
Must not be set if ``project.license-files`` is set.
207207
"""
208208

209209
cmake: bool = True

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,15 @@ def package_simple_purelib_package(
334334
return package
335335

336336

337+
@pytest.fixture
338+
def package_pep639_pure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> PackageInfo:
339+
package = PackageInfo(
340+
"pep639_pure",
341+
)
342+
process_package(package, tmp_path, monkeypatch)
343+
return package
344+
345+
337346
def which_mock(name: str) -> str | None:
338347
if name in {"ninja", "ninja-build", "cmake3", "samu", "gmake", "make"}:
339348
return None

tests/packages/pep639_pure/LICENSE1.txt

Whitespace-only changes.

tests/packages/pep639_pure/nested/more/LICENSE2.txt

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "pep639_pure"
7+
version = "0.1.0"
8+
license = "MIT"
9+
license-files = ["LICENSE1.txt", "nested/more/LICENSE2.txt"]
10+
11+
[tool.scikit-build]
12+
wheel.cmake = false

tests/test_pyproject_pep517.py

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import gzip
22
import hashlib
3+
import inspect
34
import shutil
45
import sys
56
import tarfile
@@ -27,15 +28,6 @@
2728
2829
[gui_scripts]
2930
guithing = a.b:c
30-
"""
31-
METADATA = """\
32-
Metadata-Version: 2.1
33-
Name: CMake.Example
34-
Version: 0.0.1
35-
Requires-Python: >=3.7
36-
Provides-Extra: test
37-
Requires-Dist: pytest>=6.0; extra == "test"
38-
3931
"""
4032

4133
mark_hashes_different = pytest.mark.xfail(
@@ -52,6 +44,19 @@ def compute_uncompressed_hash(inp: Path) -> str:
5244

5345
@pytest.mark.usefixtures("package_simple_pyproject_ext")
5446
def test_pep517_sdist():
47+
expected_metadata = (
48+
inspect.cleandoc(
49+
"""
50+
Metadata-Version: 2.1
51+
Name: CMake.Example
52+
Version: 0.0.1
53+
Requires-Python: >=3.7
54+
Provides-Extra: test
55+
Requires-Dist: pytest>=6.0; extra == "test"
56+
"""
57+
)
58+
+ "\n\n"
59+
)
5560
dist = Path("dist")
5661
out = build_sdist("dist")
5762

@@ -74,7 +79,7 @@ def test_pep517_sdist():
7479
pkg_info = f.extractfile("cmake_example-0.0.1/PKG-INFO")
7580
assert pkg_info
7681
pkg_info_contents = pkg_info.read().decode()
77-
assert pkg_info_contents == METADATA
82+
assert pkg_info_contents == expected_metadata
7883

7984

8085
@mark_hashes_different
@@ -360,3 +365,82 @@ def test_prepare_metdata_for_build_wheel_by_hand(tmp_path):
360365
assert metadata.get(k, None) == b
361366

362367
assert len(metadata) == len(answer)
368+
369+
370+
@pytest.mark.usefixtures("package_pep639_pure")
371+
def test_pep639_license_files_metadata():
372+
metadata = build.util.project_wheel_metadata(str(Path.cwd()), isolated=False)
373+
answer = {
374+
"Metadata-Version": ["2.4"],
375+
"Name": ["pep639_pure"],
376+
"Version": ["0.1.0"],
377+
"License-Expression": ["MIT"],
378+
"License-File": ["LICENSE1.txt", "nested/more/LICENSE2.txt"],
379+
}
380+
381+
for k, b in answer.items():
382+
assert metadata.get_all(k, None) == b
383+
384+
assert len(metadata) == sum(len(v) for v in answer.values())
385+
386+
387+
@pytest.mark.usefixtures("package_pep639_pure")
388+
def test_pep639_license_files_sdist():
389+
expected_metadata = (
390+
inspect.cleandoc(
391+
"""
392+
Metadata-Version: 2.4
393+
Name: pep639_pure
394+
Version: 0.1.0
395+
License-Expression: MIT
396+
License-File: LICENSE1.txt
397+
License-File: nested/more/LICENSE2.txt
398+
"""
399+
)
400+
+ "\n\n"
401+
)
402+
403+
dist = Path("dist")
404+
out = build_sdist("dist")
405+
406+
(sdist,) = dist.iterdir()
407+
assert sdist.name == "pep639_pure-0.1.0.tar.gz"
408+
assert sdist == dist / out
409+
410+
with tarfile.open(sdist) as f:
411+
file_names = set(f.getnames())
412+
assert file_names == {
413+
f"pep639_pure-0.1.0/{x}"
414+
for x in (
415+
"pyproject.toml",
416+
"PKG-INFO",
417+
"LICENSE1.txt",
418+
"nested/more/LICENSE2.txt",
419+
)
420+
}
421+
pkg_info = f.extractfile("pep639_pure-0.1.0/PKG-INFO")
422+
assert pkg_info
423+
pkg_info_contents = pkg_info.read().decode()
424+
assert pkg_info_contents == expected_metadata
425+
426+
427+
@pytest.mark.usefixtures("package_pep639_pure")
428+
def test_pep639_license_files_wheel():
429+
dist = Path("dist")
430+
out = build_wheel("dist", {})
431+
(wheel,) = dist.glob("pep639_pure-0.1.0-*.whl")
432+
assert wheel == dist / out
433+
434+
with zipfile.ZipFile(wheel) as zf:
435+
file_paths = {Path(p) for p in zf.namelist()}
436+
with zf.open("pep639_pure-0.1.0.dist-info/METADATA") as f:
437+
metadata = f.read().decode("utf-8")
438+
439+
assert Path("pep639_pure-0.1.0.dist-info/licenses/LICENSE1.txt") in file_paths
440+
assert (
441+
Path("pep639_pure-0.1.0.dist-info/licenses/nested/more/LICENSE2.txt")
442+
in file_paths
443+
)
444+
445+
assert "LICENSE1.txt" in metadata
446+
assert "nested/more/LICENSE2.txt" in metadata

0 commit comments

Comments
 (0)