Skip to content

Commit 191f955

Browse files
Avoid building packages with dynamic versions (#4058)
## Summary This PR separates "gathering the requirements" from the rest of the metadata (e.g., version), which isn't required when installing a package's _dependencies_ (as opposed to installing the package itself). It thus ensures that we don't need to build a package when a static `pyproject.toml` is provided in `pip compile`. Closes #4040.
1 parent a017376 commit 191f955

File tree

8 files changed

+597
-370
lines changed

8 files changed

+597
-370
lines changed

crates/pypi-types/src/metadata.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,73 @@ fn parse_version(metadata_version: &str) -> Result<(u8, u8), MetadataError> {
327327
Ok((major, minor))
328328
}
329329

330+
/// Python Package Metadata 2.3 as specified in
331+
/// <https://packaging.python.org/specifications/core-metadata/>.
332+
///
333+
/// This is a subset of [`Metadata23`]; specifically, it omits the `version` and `requires-python`
334+
/// fields, which aren't necessary when extracting the requirements of a package without installing
335+
/// the package itself.
336+
#[derive(Serialize, Deserialize, Debug, Clone)]
337+
#[serde(rename_all = "kebab-case")]
338+
pub struct RequiresDist {
339+
pub name: PackageName,
340+
pub requires_dist: Vec<Requirement<VerbatimParsedUrl>>,
341+
pub provides_extras: Vec<ExtraName>,
342+
}
343+
344+
impl RequiresDist {
345+
/// Extract the [`RequiresDist`] from a `pyproject.toml` file, as specified in PEP 621.
346+
pub fn parse_pyproject_toml(contents: &str) -> Result<Self, MetadataError> {
347+
let pyproject_toml: PyProjectToml = toml::from_str(contents)?;
348+
349+
let project = pyproject_toml
350+
.project
351+
.ok_or(MetadataError::FieldNotFound("project"))?;
352+
353+
// If any of the fields we need were declared as dynamic, we can't use the `pyproject.toml`
354+
// file.
355+
let dynamic = project.dynamic.unwrap_or_default();
356+
for field in dynamic {
357+
match field.as_str() {
358+
"dependencies" => return Err(MetadataError::DynamicField("dependencies")),
359+
"optional-dependencies" => {
360+
return Err(MetadataError::DynamicField("optional-dependencies"))
361+
}
362+
_ => (),
363+
}
364+
}
365+
366+
let name = project.name;
367+
368+
// Extract the requirements.
369+
let mut requires_dist = project
370+
.dependencies
371+
.unwrap_or_default()
372+
.into_iter()
373+
.map(Requirement::from)
374+
.collect::<Vec<_>>();
375+
376+
// Extract the optional dependencies.
377+
let mut provides_extras: Vec<ExtraName> = Vec::new();
378+
for (extra, requirements) in project.optional_dependencies.unwrap_or_default() {
379+
requires_dist.extend(
380+
requirements
381+
.into_iter()
382+
.map(Requirement::from)
383+
.map(|requirement| requirement.with_extra_marker(&extra))
384+
.collect::<Vec<_>>(),
385+
);
386+
provides_extras.push(extra);
387+
}
388+
389+
Ok(Self {
390+
name,
391+
requires_dist,
392+
provides_extras,
393+
})
394+
}
395+
}
396+
330397
/// The headers of a distribution metadata file.
331398
#[derive(Debug)]
332399
struct Headers<'a>(Vec<mailparse::MailHeader<'a>>);

crates/uv-distribution/src/distribution_database.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::archive::Archive;
3434
use crate::locks::Locks;
3535
use crate::metadata::{ArchiveMetadata, Metadata};
3636
use crate::source::SourceDistributionBuilder;
37-
use crate::{Error, LocalWheel, Reporter};
37+
use crate::{Error, LocalWheel, Reporter, RequiresDist};
3838

3939
/// A cached high-level interface to convert distributions (a requirement resolved to a location)
4040
/// to a wheel or wheel metadata.
@@ -434,6 +434,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
434434
Ok(metadata)
435435
}
436436

437+
/// Return the [`RequiresDist`] from a `pyproject.toml`, if it can be statically extracted.
438+
pub async fn requires_dist(&self, project_root: &Path) -> Result<RequiresDist, Error> {
439+
self.builder.requires_dist(project_root).await
440+
}
441+
437442
/// Stream a wheel from a URL, unzipping it into the cache as it's downloaded.
438443
async fn stream_wheel(
439444
&self,

crates/uv-distribution/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalA
22
pub use download::LocalWheel;
33
pub use error::Error;
44
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
5-
pub use metadata::{ArchiveMetadata, Metadata};
5+
pub use metadata::{ArchiveMetadata, Metadata, RequiresDist};
66
pub use reporter::Reporter;
77
pub use workspace::{ProjectWorkspace, Workspace, WorkspaceError, WorkspaceMember};
88

0 commit comments

Comments
 (0)