mirror of https://github.com/astral-sh/uv
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:
parent
8c51b59298
commit
fda691401a
|
|
@ -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}");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue