Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eagerly error when parsing pyproject.toml requirements #9704

Merged
merged 1 commit into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/uv-distribution/src/distribution_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
}

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

/// Stream a wheel from a URL, unzipping it into the cache as it's downloaded.
Expand Down
57 changes: 42 additions & 15 deletions crates/uv-distribution/src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,21 +360,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Ok(metadata)
}

/// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
pub(crate) async fn requires_dist(&self, project_root: &Path) -> Result<RequiresDist, Error> {
let requires_dist = read_requires_dist(project_root).await?;
let requires_dist = RequiresDist::from_project_maybe_workspace(
requires_dist,
project_root,
None,
self.build_context.locations(),
self.build_context.sources(),
self.build_context.bounds(),
)
.await?;
Ok(requires_dist)
}

/// Build a source distribution from a remote URL.
async fn url<'data>(
&self,
Expand Down Expand Up @@ -1250,6 +1235,48 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
Ok(pointer)
}

/// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
pub(crate) async fn source_tree_requires_dist(
&self,
project_root: &Path,
) -> Result<Option<RequiresDist>, Error> {
// Attempt to read static metadata from the `pyproject.toml`.
match read_requires_dist(project_root).await {
Ok(requires_dist) => {
debug!(
"Found static `requires-dist` for: {}",
project_root.display()
);
let requires_dist = RequiresDist::from_project_maybe_workspace(
requires_dist,
project_root,
None,
self.build_context.locations(),
self.build_context.sources(),
self.build_context.bounds(),
)
.await?;
Ok(Some(requires_dist))
}
Err(
err @ (Error::MissingPyprojectToml
| Error::PyprojectToml(
uv_pypi_types::MetadataError::Pep508Error(_)
| uv_pypi_types::MetadataError::DynamicField(_)
| uv_pypi_types::MetadataError::FieldNotFound(_)
| uv_pypi_types::MetadataError::PoetrySyntax,
)),
) => {
debug!(
"No static `requires-dist` available for: {} ({err:?})",
project_root.display()
);
Ok(None)
}
Err(err) => Err(err),
}
}

/// Build a source distribution from a Git repository.
async fn git(
&self,
Expand Down
8 changes: 6 additions & 2 deletions crates/uv-requirements/src/source_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,12 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
)
})?;

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

Expand Down
31 changes: 31 additions & 0 deletions crates/uv/tests/it/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,37 @@ dependencies = [
Ok(())
}

#[test]
fn compile_pyproject_toml_eager_validation() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
dynamic = ["version"]
requires-python = ">=3.10"
dependencies = ["anyio==4.7.0"]

[tool.uv.sources]
anyio = { workspace = true }
"#})?;

// This should fail without attempting to build the package.
uv_snapshot!(context
.pip_compile()
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse entry: `anyio`
Caused by: `anyio` references a workspace in `tool.uv.sources` (e.g., `anyio = { workspace = true }`), but is not a workspace member
"###);

Ok(())
}

/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file.
#[test]
fn compile_constraints_txt() -> Result<()> {
Expand Down
Loading