Skip to content

Commit da4b885

Browse files
Eagerly error when parsing pyproject.toml requirements (#9704)
## Summary Small thing I noticed while working on another change: if we error when extracting `requires-dist`, we go through the full metadata build. We need to distinguish between fatal errors and "the data isn't static".
1 parent 508a6bc commit da4b885

File tree

4 files changed

+81
-19
lines changed

4 files changed

+81
-19
lines changed

crates/uv-distribution/src/distribution_database.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
472472
}
473473

474474
/// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
475-
pub async fn requires_dist(&self, project_root: &Path) -> Result<RequiresDist, Error> {
476-
self.builder.requires_dist(project_root).await
475+
pub async fn requires_dist(&self, project_root: &Path) -> Result<Option<RequiresDist>, Error> {
476+
self.builder.source_tree_requires_dist(project_root).await
477477
}
478478

479479
/// Stream a wheel from a URL, unzipping it into the cache as it's downloaded.

crates/uv-distribution/src/source/mod.rs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -360,21 +360,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
360360
Ok(metadata)
361361
}
362362

363-
/// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
364-
pub(crate) async fn requires_dist(&self, project_root: &Path) -> Result<RequiresDist, Error> {
365-
let requires_dist = read_requires_dist(project_root).await?;
366-
let requires_dist = RequiresDist::from_project_maybe_workspace(
367-
requires_dist,
368-
project_root,
369-
None,
370-
self.build_context.locations(),
371-
self.build_context.sources(),
372-
self.build_context.bounds(),
373-
)
374-
.await?;
375-
Ok(requires_dist)
376-
}
377-
378363
/// Build a source distribution from a remote URL.
379364
async fn url<'data>(
380365
&self,
@@ -1250,6 +1235,48 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
12501235
Ok(pointer)
12511236
}
12521237

1238+
/// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
1239+
pub(crate) async fn source_tree_requires_dist(
1240+
&self,
1241+
project_root: &Path,
1242+
) -> Result<Option<RequiresDist>, Error> {
1243+
// Attempt to read static metadata from the `pyproject.toml`.
1244+
match read_requires_dist(project_root).await {
1245+
Ok(requires_dist) => {
1246+
debug!(
1247+
"Found static `requires-dist` for: {}",
1248+
project_root.display()
1249+
);
1250+
let requires_dist = RequiresDist::from_project_maybe_workspace(
1251+
requires_dist,
1252+
project_root,
1253+
None,
1254+
self.build_context.locations(),
1255+
self.build_context.sources(),
1256+
self.build_context.bounds(),
1257+
)
1258+
.await?;
1259+
Ok(Some(requires_dist))
1260+
}
1261+
Err(
1262+
err @ (Error::MissingPyprojectToml
1263+
| Error::PyprojectToml(
1264+
uv_pypi_types::MetadataError::Pep508Error(_)
1265+
| uv_pypi_types::MetadataError::DynamicField(_)
1266+
| uv_pypi_types::MetadataError::FieldNotFound(_)
1267+
| uv_pypi_types::MetadataError::PoetrySyntax,
1268+
)),
1269+
) => {
1270+
debug!(
1271+
"No static `requires-dist` available for: {} ({err:?})",
1272+
project_root.display()
1273+
);
1274+
Ok(None)
1275+
}
1276+
Err(err) => Err(err),
1277+
}
1278+
}
1279+
12531280
/// Build a source distribution from a Git repository.
12541281
async fn git(
12551282
&self,

crates/uv-requirements/src/source_tree.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,12 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
191191
)
192192
})?;
193193

194-
// If the path is a `pyproject.toml`, attempt to extract the requirements statically.
195-
if let Ok(metadata) = self.database.requires_dist(source_tree).await {
194+
// If the path is a `pyproject.toml`, attempt to extract the requirements statically. The
195+
// distribution database will do this too, but we can be even more aggressive here since we
196+
// _only_ need the requirements. So, for example, even if the version is dynamic, we can
197+
// still extract the requirements without performing a build, unlike in the database where
198+
// we typically construct a "complete" metadata object.
199+
if let Some(metadata) = self.database.requires_dist(source_tree).await? {
196200
return Ok(metadata);
197201
}
198202

crates/uv/tests/it/pip_compile.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,37 @@ dependencies = [
293293
Ok(())
294294
}
295295

296+
#[test]
297+
fn compile_pyproject_toml_eager_validation() -> Result<()> {
298+
let context = TestContext::new("3.12");
299+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
300+
pyproject_toml.write_str(indoc! {r#"
301+
[project]
302+
name = "project"
303+
dynamic = ["version"]
304+
requires-python = ">=3.10"
305+
dependencies = ["anyio==4.7.0"]
306+
307+
[tool.uv.sources]
308+
anyio = { workspace = true }
309+
"#})?;
310+
311+
// This should fail without attempting to build the package.
312+
uv_snapshot!(context
313+
.pip_compile()
314+
.arg("pyproject.toml"), @r###"
315+
success: false
316+
exit_code: 2
317+
----- stdout -----
318+
319+
----- stderr -----
320+
error: Failed to parse entry: `anyio`
321+
Caused by: `anyio` references a workspace in `tool.uv.sources` (e.g., `anyio = { workspace = true }`), but is not a workspace member
322+
"###);
323+
324+
Ok(())
325+
}
326+
296327
/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file.
297328
#[test]
298329
fn compile_constraints_txt() -> Result<()> {

0 commit comments

Comments
 (0)