Skip to content

Commit 5204fc2

Browse files
authored
refactor(uv): warn instead of silently skipping pyproject parsing without tomllib (#1247)
detect_native silently returned empty [build-system] requires on pre-3.11 interpreters, misdetecting modern sdists. Warn on stderr when pyproject.toml is present but tomllib is not, keep the rest of the tool (native detection, setup.cfg/setup.py parsing) working, and pin the unit test to the 3.13 toolchain the fetch-time inspection actually uses. ### Changes are visible to end-users: no ### Test plan - Covered by existing test cases - New test cases added
1 parent b1725c7 commit 5204fc2

3 files changed

Lines changed: 32 additions & 36 deletions

File tree

uv/private/sdist_configure/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ py_test(
1717
"detect_native_test.py",
1818
],
1919
main = "detect_native_test.py",
20+
# detect_native.py requires Python >= 3.11 (tomllib); at fetch time it
21+
# runs under the PBS interpreter provisioned for sdist inspection.
22+
python_version = "3.13",
2023
)
2124

2225
bzl_library(

uv/private/sdist_configure/detect_native.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
1010
See //uv/private/sdist_configure:defs.bzl for the full interface contract.
1111
12-
Requires Python >= 3.11 (for tomllib).
12+
Requires Python >= 3.11 (tomllib) to parse pyproject.toml; on older
13+
interpreters [build-system] metadata is skipped with a warning.
1314
"""
1415

1516
import configparser
16-
import importlib
1717
import os
1818
import importlib.abc
1919
import importlib.machinery
@@ -112,6 +112,12 @@ def _parse_pyproject_build_system(content):
112112
- backend_path: the backend-path list, or None
113113
"""
114114
if tomllib is None:
115+
print(
116+
"WARNING: pyproject.toml present but tomllib is unavailable "
117+
"(Python < 3.11); [build-system] build requires will be missed. "
118+
"Run the sdist configure tool with Python >= 3.11.",
119+
file=sys.stderr,
120+
)
115121
return [], None, None
116122
data = tomllib.loads(content)
117123
build_system = data.get("build-system", {})

uv/private/sdist_configure/detect_native_test.py

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for detect_native.py sdist configure tool."""
22

33
import io
4-
import json
54
import os
65
import sys
76
import tarfile
@@ -10,26 +9,6 @@
109

1110
from uv.private.sdist_configure.detect_native import detect
1211

13-
try:
14-
import tomllib
15-
HAS_TOMLLIB = True
16-
except ModuleNotFoundError:
17-
HAS_TOMLLIB = False
18-
19-
20-
class _Skip(Exception):
21-
"""Raised to skip a test."""
22-
23-
24-
def requires_tomllib(fn):
25-
"""Skip test if tomllib is not available (Python < 3.11)."""
26-
def wrapper():
27-
if not HAS_TOMLLIB:
28-
raise _Skip("requires Python >= 3.11 (tomllib)")
29-
return fn()
30-
wrapper.__name__ = fn.__name__
31-
return wrapper
32-
3312

3413
def _make_tar_gz(members):
3514
"""Create a .tar.gz archive in a temp file from a dict of {name: content}.
@@ -142,7 +121,6 @@ def test_rust():
142121
# --- pyproject.toml parsing ---
143122

144123

145-
@requires_tomllib
146124
def test_pyproject_build_requires():
147125
pyproject = """\
148126
[build-system]
@@ -160,6 +138,25 @@ def test_pyproject_build_requires():
160138
assert "cython" in result["build_requires"]
161139

162140

141+
def test_pyproject_degrades_without_tomllib():
142+
"""On a pre-3.11 interpreter, pyproject parsing is skipped with a warning
143+
while the rest of the tool (native detection etc.) keeps working."""
144+
from uv.private.sdist_configure import detect_native
145+
archive = _make_tar_gz({
146+
"pkg-1.0/": None,
147+
"pkg-1.0/pyproject.toml": '[build-system]\nrequires = ["cython"]\n',
148+
"pkg-1.0/pkg/fast.pyx": "# cython source",
149+
})
150+
saved = detect_native.tomllib
151+
detect_native.tomllib = None
152+
try:
153+
result = detect(archive, {})
154+
finally:
155+
detect_native.tomllib = saved
156+
assert result["build_requires"] == []
157+
assert result["is_native"] is True
158+
159+
163160
# --- setup.cfg parsing ---
164161

165162

@@ -195,7 +192,6 @@ def test_legacy_setup_py_infers_setuptools():
195192
assert "wheel" in result["build_requires"]
196193

197194

198-
@requires_tomllib
199195
def test_pyproject_does_not_infer_setuptools():
200196
"""A package with pyproject.toml should NOT get implicit setuptools."""
201197
archive = _make_tar_gz({
@@ -215,7 +211,6 @@ def test_pyproject_does_not_infer_setuptools():
215211
# --- extra_deps resolution ---
216212

217213

218-
@requires_tomllib
219214
def test_extra_deps_resolved_from_available():
220215
"""Declared build deps that exist in available_deps become extra_deps."""
221216
pyproject = """\
@@ -240,7 +235,6 @@ def test_extra_deps_resolved_from_available():
240235
assert "cython" in result["extra_deps"]
241236

242237

243-
@requires_tomllib
244238
def test_extra_deps_excludes_already_provided():
245239
"""Deps already in the explicit deps list are not reported as extra."""
246240
pyproject = """\
@@ -266,7 +260,6 @@ def test_extra_deps_excludes_already_provided():
266260
assert "cython" in result["extra_deps"]
267261

268262

269-
@requires_tomllib
270263
def test_extra_deps_unresolvable_not_included():
271264
"""Deps not in available_deps are silently omitted from extra_deps."""
272265
pyproject = """\
@@ -324,7 +317,6 @@ def test_zip_archive():
324317
assert "pkg-1.0/pkg/ext.c" in result["native_files"]
325318

326319

327-
@requires_tomllib
328320
def test_zip_archive_with_pyproject():
329321
archive = _make_zip({
330322
"pkg-1.0/pkg/__init__.py": "",
@@ -342,7 +334,6 @@ def test_zip_archive_with_pyproject():
342334
# --- Config files at root (no top-level directory) ---
343335

344336

345-
@requires_tomllib
346337
def test_flat_archive():
347338
"""Some archives don't have a top-level directory prefix."""
348339
archive = _make_tar_gz({
@@ -757,22 +748,18 @@ def test_setup_cfg_and_setup_py_merged():
757748

758749
if __name__ == "__main__":
759750
failures = []
760-
skipped = []
761751
test_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
762752
for fn in test_fns:
763753
try:
764754
fn()
765755
print(f" PASS {fn.__name__}")
766-
except _Skip as e:
767-
print(f" SKIP {fn.__name__}: {e}")
768-
skipped.append(fn.__name__)
769756
except Exception as e:
770757
print(f" FAIL {fn.__name__}: {e}")
771758
failures.append(fn.__name__)
772759

773760
total = len(test_fns)
774-
passed = total - len(failures) - len(skipped)
775-
print(f"\n{passed} passed, {len(skipped)} skipped, {len(failures)} failed (of {total})")
761+
passed = total - len(failures)
762+
print(f"\n{passed} passed, {len(failures)} failed (of {total})")
776763
if failures:
777764
print(f"Failures: {', '.join(failures)}")
778765
sys.exit(1)

0 commit comments

Comments
 (0)