diff --git a/crates/distribution-types/src/buildable.rs b/crates/distribution-types/src/buildable.rs index 01c26bcae..3b8756b46 100644 --- a/crates/distribution-types/src/buildable.rs +++ b/crates/distribution-types/src/buildable.rs @@ -56,6 +56,14 @@ impl BuildableSource<'_> { Self::Url(url) => url.is_editable(), } } + + /// Return true if the source refers to a local source tree (i.e., a directory). + pub fn is_source_tree(&self) -> bool { + match self { + Self::Dist(dist) => matches!(dist, SourceDist::Directory(_)), + Self::Url(url) => matches!(url, SourceUrl::Directory(_)), + } + } } impl std::fmt::Display for BuildableSource<'_> { @@ -94,6 +102,11 @@ impl<'a> SourceUrl<'a> { Self::Directory(DirectorySourceUrl { editable: true, .. }) ) } + + /// Return true if the source refers to a local file or directory. + pub fn is_local(&self) -> bool { + matches!(self, Self::Path(_) | Self::Directory(_)) + } } impl std::fmt::Display for SourceUrl<'_> { diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index af40aec90..413ee2da1 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1529,6 +1529,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Err(err) => return Err(err), } + // If the source distribution is a source tree, avoid reading `PKG-INFO` or `egg-info`, + // since they could be out-of-date. + if source.is_source_tree() { + return Ok(None); + } + // Attempt to read static metadata from the `PKG-INFO` file. match read_pkg_info(source_root, subdirectory).await { Ok(metadata) => { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 3c95b3849..712c0d26f 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -6386,3 +6386,72 @@ fn switch_platform() -> Result<()> { Ok(()) } + +/// See: +#[test] +fn stale_egg_info() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create a project with dynamic metadata (version). + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + dynamic = ["version"] + + dependencies = ["iniconfig"] + "# + })?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("-e") + .arg("."), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.0.0 (from file://[TEMP_DIR]/) + "### + ); + + // Ensure that `.egg-info` exists. + let egg_info = context.temp_dir.child("project.egg-info"); + egg_info.assert(predicates::path::is_dir()); + + // Change the metadata. + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + dynamic = ["version"] + + dependencies = ["anyio"] + "# + })?; + + // Reinstall. Ensure that the metadata is updated. + uv_snapshot!(context.filters(), context.pip_install() + .arg("-e") + .arg("."), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + ~ project==0.0.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "### + ); + + Ok(()) +}