Skip to content

Commit

Permalink
Re-sync editables on-change
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Feb 25, 2024
1 parent 8d706b0 commit 0481b59
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 13 deletions.
12 changes: 6 additions & 6 deletions crates/uv-installer/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,16 +444,16 @@ fn not_modified_cache(cache_entry: &CacheEntry, artifact: &Path) -> Result<bool,
/// not-modified based on the modification time of the installed distribution.
fn not_modified_install(dist: &InstalledDirectUrlDist, artifact: &Path) -> Result<bool, io::Error> {
// Determine the modification time of the installed distribution.
let dist_metadata = fs_err::metadata(&dist.path)?;
let dist_metadata = fs_err::metadata(dist.path.join("METADATA"))?;
let dist_timestamp = Timestamp::from_metadata(&dist_metadata);

// Determine the modification time of the wheel.
if let Some(artifact_timestamp) = ArchiveTimestamp::from_path(artifact)? {
Ok(dist_timestamp >= artifact_timestamp.timestamp())
} else {
let Some(artifact_timestamp) = ArchiveTimestamp::from_path(artifact)? else {
// The artifact doesn't exist, so it's not fresh.
Ok(false)
}
return Ok(false);
};

Ok(dist_timestamp >= artifact_timestamp.timestamp())
}

#[derive(Debug, Default)]
Expand Down
6 changes: 4 additions & 2 deletions crates/uv/src/commands/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,14 +496,16 @@ async fn install(
) -> Result<(), Error> {
let start = std::time::Instant::now();

// Partition into those that should be linked from the cache (`local`), those that need to be
// downloaded (`remote`), and those that should be removed (`extraneous`).
let requirements = resolution.requirements();

// Map the built editables to their resolved form.
let editables = built_editables
.into_iter()
.map(ResolvedEditable::Built)
.collect::<Vec<_>>();

// Partition into those that should be linked from the cache (`local`), those that need to be
// downloaded (`remote`), and those that should be removed (`extraneous`).
let Plan {
local,
remote,
Expand Down
35 changes: 30 additions & 5 deletions crates/uv/src/commands/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::debug;

use distribution_types::{IndexLocations, InstalledMetadata, LocalDist, LocalEditable, Name};
use distribution_types::{
IndexLocations, InstalledDist, InstalledMetadata, LocalDist, LocalEditable, Name,
};
use install_wheel_rs::linker::LinkMode;
use platform_host::Platform;
use platform_tags::Tags;
use pypi_types::Yanked;
use requirements_txt::EditableRequirement;
use uv_cache::Cache;
use uv_cache::{ArchiveTimestamp, Cache};
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder};
use uv_dispatch::BuildDispatch;
use uv_fs::Normalized;
Expand Down Expand Up @@ -392,6 +394,19 @@ async fn resolve_editables(
build_dispatch: &BuildDispatch<'_>,
mut printer: Printer,
) -> Result<ResolvedEditables> {
/// Returns `true` if the installed distribution is up-to-date.
fn not_modified(editable: &EditableRequirement, installed: &InstalledDist) -> bool {
// Is the editable out-of-date?
let Ok(Some(installed_at)) = ArchiveTimestamp::from_path(installed.path().join("METADATA"))
else {
return false;
};
let Ok(Some(modified_at)) = ArchiveTimestamp::from_path(&editable.path) else {
return false;
};
installed_at > modified_at
}

// Partition the editables into those that are already installed, and those that must be built.
let mut installed = Vec::with_capacity(editables.len());
let mut uninstalled = Vec::with_capacity(editables.len());
Expand All @@ -401,7 +416,13 @@ async fn resolve_editables(
let existing = site_packages.get_editables(editable.raw());
match existing.as_slice() {
[] => uninstalled.push(editable),
[dist] => installed.push((*dist).clone()),
[dist] => {
if not_modified(&editable, dist) {
installed.push((*dist).clone());
} else {
uninstalled.push(editable);
}
}
_ => {
uninstalled.push(editable);
}
Expand All @@ -414,11 +435,15 @@ async fn resolve_editables(
let existing = site_packages.get_editables(editable.raw());
match existing.as_slice() {
[] => uninstalled.push(editable),
[dist] => {
[dist] if not_modified(&editable, dist) => {
if packages.contains(dist.name()) {
uninstalled.push(editable);
} else {
installed.push((*dist).clone());
if not_modified(&editable, dist) {
installed.push((*dist).clone());
} else {
uninstalled.push(editable);
}
}
}
_ => {
Expand Down
83 changes: 83 additions & 0 deletions crates/uv/tests/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2799,3 +2799,86 @@ fn pip_entrypoints() -> Result<()> {

Ok(())
}

#[test]
fn invalidate_on_change() -> Result<()> {
let context = TestContext::new("3.12");

// Create an editable 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"
"#,
)?;

// Write to a requirements file.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?;

let filters = [(r"\(from file://.*\)", "(from [WORKSPACE_DIR])")]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect::<Vec<_>>();

uv_snapshot!(filters, command(&context)
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Installed 1 package in [TIME]
+ example==0.0.0 (from [WORKSPACE_DIR])
"###
);

// Re-installing should be a no-op.
uv_snapshot!(filters, command(&context)
.arg("requirements.in"), @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("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Built 1 editable in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- example==0.0.0 (from [WORKSPACE_DIR])
+ example==0.0.0 (from [WORKSPACE_DIR])
"###
);

Ok(())
}

0 comments on commit 0481b59

Please sign in to comment.