mirror of https://github.com/astral-sh/uv
Un-cache editable requirements with dynamic metadata (#2029)
Closes https://github.com/astral-sh/uv/issues/1991.
This commit is contained in:
parent
8214bfe080
commit
72a5ebada3
|
|
@ -4489,12 +4489,15 @@ dependencies = [
|
|||
"pep508_rs",
|
||||
"platform-tags",
|
||||
"pypi-types",
|
||||
"pyproject-toml",
|
||||
"rayon",
|
||||
"requirements-txt",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"uv-cache",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ once-map = { path = "../once-map" }
|
|||
pep440_rs = { path = "../pep440-rs" }
|
||||
pep508_rs = { path = "../pep508-rs" }
|
||||
platform-tags = { path = "../platform-tags" }
|
||||
pypi-types = { path = "../pypi-types" }
|
||||
requirements-txt = { path = "../requirements-txt" }
|
||||
uv-cache = { path = "../uv-cache" }
|
||||
uv-client = { path = "../uv-client" }
|
||||
uv-distribution = { path = "../uv-distribution" }
|
||||
|
|
@ -29,16 +31,17 @@ uv-git = { path = "../uv-git", features = ["vendored-openssl"] }
|
|||
uv-interpreter = { path = "../uv-interpreter" }
|
||||
uv-normalize = { path = "../uv-normalize" }
|
||||
uv-traits = { path = "../uv-traits" }
|
||||
pypi-types = { path = "../pypi-types" }
|
||||
requirements-txt = { path = "../requirements-txt" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
pyproject-toml = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
use pyproject_toml::Project;
|
||||
use serde::Deserialize;
|
||||
|
||||
use distribution_types::{
|
||||
CachedDist, InstalledDist, InstalledMetadata, InstalledVersion, LocalEditable, Name,
|
||||
};
|
||||
use pypi_types::Metadata21;
|
||||
use requirements_txt::EditableRequirement;
|
||||
use uv_cache::ArchiveTimestamp;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
/// An editable distribution that has been built.
|
||||
|
|
@ -63,3 +68,54 @@ impl std::fmt::Display for ResolvedEditable {
|
|||
write!(f, "{}{}", self.name(), self.installed_version())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the installed distribution is up-to-date with the [`EditableRequirement`].
|
||||
pub fn not_modified(editable: &EditableRequirement, installed: &InstalledDist) -> bool {
|
||||
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
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`EditableRequirement`] contains dynamic metadata.
|
||||
pub fn is_dynamic(editable: &EditableRequirement) -> bool {
|
||||
// If there's no `pyproject.toml`, we assume it's dynamic.
|
||||
let Ok(contents) = fs_err::read_to_string(editable.path.join("pyproject.toml")) else {
|
||||
return true;
|
||||
};
|
||||
let Ok(pyproject_toml) = toml::from_str::<PyProjectToml>(&contents) else {
|
||||
return true;
|
||||
};
|
||||
// If `[project]` is not present, we assume it's dynamic.
|
||||
let Some(project) = pyproject_toml.project else {
|
||||
// ...unless it appears to be a Poetry project.
|
||||
return pyproject_toml
|
||||
.tool
|
||||
.map_or(true, |tool| tool.poetry.is_none());
|
||||
};
|
||||
// `[project.dynamic]` must be present and non-empty.
|
||||
project.dynamic.is_some_and(|dynamic| !dynamic.is_empty())
|
||||
}
|
||||
|
||||
/// A pyproject.toml as specified in PEP 517.
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct PyProjectToml {
|
||||
project: Option<Project>,
|
||||
tool: Option<Tool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Tool {
|
||||
poetry: Option<ToolPoetry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ToolPoetry {
|
||||
#[allow(dead_code)]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
pub use downloader::{Downloader, Reporter as DownloadReporter};
|
||||
pub use editable::{BuiltEditable, ResolvedEditable};
|
||||
pub use editable::{is_dynamic, not_modified, BuiltEditable, ResolvedEditable};
|
||||
pub use installer::{Installer, Reporter as InstallReporter};
|
||||
pub use plan::{Plan, Planner, Reinstall};
|
||||
// TODO(zanieb): Just import this properly everywhere else
|
||||
pub use site_packages::SitePackages;
|
||||
pub use uninstall::uninstall;
|
||||
pub use uv_traits::NoBinary;
|
||||
|
||||
mod downloader;
|
||||
mod editable;
|
||||
mod installer;
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ use distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Nam
|
|||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use pep508_rs::{Requirement, VerbatimUrl};
|
||||
use requirements_txt::EditableRequirement;
|
||||
use uv_cache::ArchiveTimestamp;
|
||||
use uv_interpreter::Virtualenv;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::{is_dynamic, not_modified};
|
||||
|
||||
/// An index over the packages installed in an environment.
|
||||
///
|
||||
/// Packages are indexed by both name and (for editable installs) URL.
|
||||
|
|
@ -275,16 +276,12 @@ impl<'a> SitePackages<'a> {
|
|||
}
|
||||
[distribution] => {
|
||||
// Is the editable out-of-date?
|
||||
let Ok(Some(installed_at)) =
|
||||
ArchiveTimestamp::from_path(distribution.path().join("METADATA"))
|
||||
else {
|
||||
if !not_modified(requirement, distribution) {
|
||||
return Ok(false);
|
||||
};
|
||||
let Ok(Some(modified_at)) = ArchiveTimestamp::from_path(&requirement.path)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
if modified_at > installed_at {
|
||||
}
|
||||
|
||||
// Does the editable have dynamic metadata?
|
||||
if is_dynamic(requirement) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,20 +5,19 @@ use itertools::Itertools;
|
|||
use owo_colors::OwoColorize;
|
||||
use tracing::debug;
|
||||
|
||||
use distribution_types::{
|
||||
IndexLocations, InstalledDist, InstalledMetadata, LocalDist, LocalEditable, Name,
|
||||
};
|
||||
use distribution_types::{IndexLocations, 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::{ArchiveTimestamp, Cache};
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_fs::Normalized;
|
||||
use uv_installer::{
|
||||
Downloader, NoBinary, Plan, Planner, Reinstall, ResolvedEditable, SitePackages,
|
||||
is_dynamic, not_modified, Downloader, NoBinary, Plan, Planner, Reinstall, ResolvedEditable,
|
||||
SitePackages,
|
||||
};
|
||||
use uv_interpreter::Virtualenv;
|
||||
use uv_resolver::InMemoryIndex;
|
||||
|
|
@ -393,18 +392,6 @@ 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 {
|
||||
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());
|
||||
|
|
@ -415,7 +402,7 @@ async fn resolve_editables(
|
|||
match existing.as_slice() {
|
||||
[] => uninstalled.push(editable),
|
||||
[dist] => {
|
||||
if not_modified(&editable, dist) {
|
||||
if not_modified(&editable, dist) && !is_dynamic(&editable) {
|
||||
installed.push((*dist).clone());
|
||||
} else {
|
||||
uninstalled.push(editable);
|
||||
|
|
@ -433,10 +420,10 @@ async fn resolve_editables(
|
|||
let existing = site_packages.get_editables(editable.raw());
|
||||
match existing.as_slice() {
|
||||
[] => uninstalled.push(editable),
|
||||
[dist] if not_modified(&editable, dist) => {
|
||||
[dist] => {
|
||||
if packages.contains(dist.name()) {
|
||||
uninstalled.push(editable);
|
||||
} else if not_modified(&editable, dist) {
|
||||
} else if not_modified(&editable, dist) && !is_dynamic(&editable) {
|
||||
installed.push((*dist).clone());
|
||||
} else {
|
||||
uninstalled.push(editable);
|
||||
|
|
|
|||
|
|
@ -1937,3 +1937,93 @@ requires-python = ">=3.8"
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalidate_dynamic() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Create an editable package with dynamic metadata
|
||||
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.1.0"
|
||||
dynamic = ["dependencies"]
|
||||
requires-python = ">=3.11,<3.13"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
dependencies = {file = ["requirements.txt"]}
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let requirements_txt = editable_dir.child("requirements.txt");
|
||||
requirements_txt.write_str("anyio==4.0.0")?;
|
||||
|
||||
let filters = [(r"\(from file://.*\)", "(from [WORKSPACE_DIR])")]
|
||||
.into_iter()
|
||||
.chain(INSTA_FILTERS.to_vec())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
uv_snapshot!(filters, command(&context)
|
||||
.arg("--editable")
|
||||
.arg(editable_dir.path()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Built 1 editable in [TIME]
|
||||
Resolved 4 packages in [TIME]
|
||||
Downloaded 3 packages in [TIME]
|
||||
Installed 4 packages in [TIME]
|
||||
+ anyio==4.0.0
|
||||
+ example==0.1.0 (from [WORKSPACE_DIR])
|
||||
+ idna==3.4
|
||||
+ sniffio==1.3.0
|
||||
"###
|
||||
);
|
||||
|
||||
// Re-installing should re-install.
|
||||
uv_snapshot!(filters, command(&context)
|
||||
.arg("--editable")
|
||||
.arg(editable_dir.path()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Built 1 editable in [TIME]
|
||||
Resolved 4 packages in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- example==0.1.0 (from [WORKSPACE_DIR])
|
||||
+ example==0.1.0 (from [WORKSPACE_DIR])
|
||||
"###
|
||||
);
|
||||
|
||||
// Modify the requirements.
|
||||
requirements_txt.write_str("anyio==3.7.1")?;
|
||||
|
||||
// Re-installing should update the package.
|
||||
uv_snapshot!(filters, command(&context)
|
||||
.arg("--editable")
|
||||
.arg(editable_dir.path()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Built 1 editable in [TIME]
|
||||
Resolved 4 packages in [TIME]
|
||||
Downloaded 1 package in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
- anyio==4.0.0
|
||||
+ anyio==3.7.1
|
||||
- example==0.1.0 (from [WORKSPACE_DIR])
|
||||
+ example==0.1.0 (from [WORKSPACE_DIR])
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue