Respect local freshness when auditing installed environment (#2169)

## Summary

Ensures that local dependencies function similarly to editables, in that
if they're `uv pip install`ed, we invalidate them.

Closes https://github.com/astral-sh/uv/issues/1651.
This commit is contained in:
Charlie Marsh 2024-03-04 11:40:52 -08:00 committed by GitHub
parent 8c51b59298
commit fda691401a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 7 deletions

View File

@ -175,9 +175,12 @@ impl<'a> Planner<'a> {
[distribution] => { [distribution] => {
// Filter out already-installed packages. // Filter out already-installed packages.
match requirement.version_or_url.as_ref() { match requirement.version_or_url.as_ref() {
// Accept any version of the package.
None => continue,
// If the requirement comes from a registry, check by name. // If the requirement comes from a registry, check by name.
None | Some(VersionOrUrl::VersionSpecifier(_)) => { Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
if requirement.is_satisfied_by(distribution.version()) { if version_specifier.contains(distribution.version()) {
debug!("Requirement already satisfied: {distribution}"); debug!("Requirement already satisfied: {distribution}");
continue; continue;
} }
@ -196,6 +199,7 @@ impl<'a> Planner<'a> {
debug!("Requirement already satisfied (and up-to-date): {installed}"); debug!("Requirement already satisfied (and up-to-date): {installed}");
continue; continue;
} }
debug!("Requirement already satisfied (but not up-to-date): {installed}");
} else { } else {
// Otherwise, assume the requirement is up-to-date. // Otherwise, assume the requirement is up-to-date.
debug!("Requirement already satisfied (assumed up-to-date): {installed}"); debug!("Requirement already satisfied (assumed up-to-date): {installed}");

View File

@ -323,7 +323,30 @@ impl<'a> SitePackages<'a> {
[distribution] => { [distribution] => {
// Validate that the installed version matches the requirement. // Validate that the installed version matches the requirement.
match &requirement.version_or_url { match &requirement.version_or_url {
None | Some(pep508_rs::VersionOrUrl::Url(_)) => {} // Accept any installed version.
None => {}
// If the requirement comes from a URL, verify by URL.
Some(pep508_rs::VersionOrUrl::Url(url)) => {
let InstalledDist::Url(installed) = &distribution else {
return Ok(false);
};
if &installed.url != url.raw() {
return Ok(false);
}
// If the requirement came from a local path, check freshness.
if let Ok(archive) = url.to_file_path() {
if !ArchiveTimestamp::up_to_date_with(
&archive,
ArchiveTarget::Install(distribution),
)? {
return Ok(false);
}
}
}
Some(pep508_rs::VersionOrUrl::VersionSpecifier(version_specifier)) => { Some(pep508_rs::VersionOrUrl::VersionSpecifier(version_specifier)) => {
// The installed version doesn't satisfy the requirement. // The installed version doesn't satisfy the requirement.
if !version_specifier.contains(distribution.version()) { if !version_specifier.contains(distribution.version()) {
@ -343,9 +366,32 @@ impl<'a> SitePackages<'a> {
} }
match &constraint.version_or_url { match &constraint.version_or_url {
None | Some(pep508_rs::VersionOrUrl::Url(_)) => {} // Accept any installed version.
None => {}
// If the requirement comes from a URL, verify by URL.
Some(pep508_rs::VersionOrUrl::Url(url)) => {
let InstalledDist::Url(installed) = &distribution else {
return Ok(false);
};
if &installed.url != url.raw() {
return Ok(false);
}
// If the requirement came from a local path, check freshness.
if let Ok(archive) = url.to_file_path() {
if !ArchiveTimestamp::up_to_date_with(
&archive,
ArchiveTarget::Install(distribution),
)? {
return Ok(false);
}
}
}
Some(pep508_rs::VersionOrUrl::VersionSpecifier(version_specifier)) => { Some(pep508_rs::VersionOrUrl::VersionSpecifier(version_specifier)) => {
// The installed version doesn't satisfy the constraint. // The installed version doesn't satisfy the requirement.
if !version_specifier.contains(distribution.version()) { if !version_specifier.contains(distribution.version()) {
return Ok(false); return Ok(false);
} }

View File

@ -56,6 +56,13 @@ fn command_without_exclude_newer(context: &TestContext) -> Command {
.arg(context.cache_dir.path()) .arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str()) .env("VIRTUAL_ENV", context.venv.as_os_str())
.current_dir(&context.temp_dir); .current_dir(&context.temp_dir);
if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB
command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string());
}
command command
} }
@ -69,6 +76,13 @@ fn uninstall_command(context: &TestContext) -> Command {
.arg(context.cache_dir.path()) .arg(context.cache_dir.path())
.env("VIRTUAL_ENV", context.venv.as_os_str()) .env("VIRTUAL_ENV", context.venv.as_os_str())
.current_dir(&context.temp_dir); .current_dir(&context.temp_dir);
if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB
command.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string());
}
command command
} }
@ -1920,7 +1934,7 @@ fn install_symlink() {
} }
#[test] #[test]
fn invalidate_on_change() -> Result<()> { fn invalidate_editable_on_change() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
// Create an editable package. // Create an editable package.
@ -2010,7 +2024,7 @@ requires-python = ">=3.8"
} }
#[test] #[test]
fn invalidate_dynamic() -> Result<()> { fn invalidate_editable_dynamic() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
// Create an editable package with dynamic metadata // Create an editable package with dynamic metadata
@ -2098,3 +2112,91 @@ dependencies = {file = ["requirements.txt"]}
Ok(()) Ok(())
} }
#[test]
fn invalidate_path_on_change() -> Result<()> {
let context = TestContext::new("3.12");
// Create a local package.
let editable_dir = assert_fs::TempDir::new()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==4.0.0"
]
requires-python = ">=3.8"
"#,
)?;
let filters = [(r"\(from file://.*\)", "(from [WORKSPACE_DIR])")]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect::<Vec<_>>();
uv_snapshot!(filters, command(&context)
.arg("example @ .")
.current_dir(editable_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.0.0
+ example==0.0.0 (from [WORKSPACE_DIR])
+ idna==3.4
+ sniffio==1.3.0
"###
);
// Re-installing should be a no-op.
uv_snapshot!(filters, command(&context)
.arg("example @ .")
.current_dir(editable_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Audited 1 package in [TIME]
"###
);
// Modify the editable package.
pyproject_toml.write_str(
r#"[project]
name = "example"
version = "0.0.0"
dependencies = [
"anyio==3.7.1"
]
requires-python = ">=3.8"
"#,
)?;
// Re-installing should update the package.
uv_snapshot!(filters, command(&context)
.arg("example @ .")
.current_dir(editable_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
- anyio==4.0.0
+ anyio==3.7.1
- example==0.0.0 (from [WORKSPACE_DIR])
+ example==0.0.0 (from [WORKSPACE_DIR])
"###
);
Ok(())
}