Rebuild dynamic distribution when version changes with --no-cache (#17387)

## Summary

Well-explained in https://github.com/astral-sh/uv/issues/17370. I don't
see a reason _not_ to fix this.

Closes https://github.com/astral-sh/uv/issues/17370.
This commit is contained in:
Charlie Marsh
2026-01-09 15:43:20 -05:00
committed by GitHub
parent 161b99c546
commit ef83fc34bc
4 changed files with 130 additions and 0 deletions

View File

@@ -126,6 +126,7 @@ impl<'a> Planner<'a> {
dist.name(),
installed,
&source,
dist.version(),
installation,
tags,
config_settings,

View File

@@ -15,6 +15,7 @@ use uv_distribution_types::{
};
use uv_git_types::{GitLfs, GitOid};
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
use uv_pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind};
@@ -36,6 +37,7 @@ impl RequirementSatisfaction {
name: &PackageName,
distribution: &InstalledDist,
source: &RequirementSource,
version: Option<&Version>,
installation: InstallationStrategy,
tags: &Tags,
config_settings: &ConfigSettings,
@@ -358,6 +360,21 @@ impl RequirementSatisfaction {
}
}
// If a resolved version is provided, check that it matches the installed version.
// This is needed for sources that don't include explicit version specifiers (e.g.,
// directory dependencies with dynamic versioning), where the resolver may have determined
// a new version should be installed.
if let Some(version) = version {
if distribution.version() != version {
debug!(
"Installed version does not match resolved version for {name}: {} vs. {}",
distribution.version(),
version
);
return Self::OutOfDate;
}
}
// Otherwise, assume the requirement is up-to-date.
Self::Satisfied
}

View File

@@ -485,6 +485,7 @@ impl SitePackages {
name,
distribution,
&requirement.source,
None,
installation,
tags,
config_settings,
@@ -508,6 +509,7 @@ impl SitePackages {
name,
distribution,
&constraint.source,
None,
installation,
tags,
config_settings,

View File

@@ -15376,3 +15376,113 @@ fn sync_fails_ambiguous_url() -> Result<()> {
Ok(())
}
/// Test that when a local directory dependency's version changes, the planner reinstalls it
/// even if the source directory content (cache info) hasn't changed.
///
/// Regression test for: <https://github.com/astral-sh/uv/issues/17370>
#[test]
fn sync_reinstalls_on_version_change() -> Result<()> {
let context = TestContext::new("3.12");
// Create a workspace with a local directory dependency.
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[tool.uv.sources]
child = { path = "packages/child" }
"#,
)?;
// Create the child package with version 0.1.0.
let child = context.temp_dir.child("packages/child");
child.create_dir_all()?;
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Lock and sync (installs child v0.1.0).
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
");
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/packages/child)
");
// Now bump the child's version to 0.1.1.
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.1"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
// Lock again; lockfile should show v0.1.1.
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Updated child v0.1.0 -> v0.1.1
");
// Sync should reinstall child with the new version. Before the fix for #17370,
// this would incorrectly say "Audited 2 packages" and not reinstall the child package.
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- child==0.1.0 (from file://[TEMP_DIR]/packages/child)
+ child==0.1.1 (from file://[TEMP_DIR]/packages/child)
");
Ok(())
}