Avoid always rebuilding dynamic metadata (#5206)

## Summary

I don't think that "always reinstall" is tenable for `uv run`. My
perspective on this is that if you want "always reinstall", you can now
set it persistently in your `pyproject.toml` or `uv.toml`.

As a smaller change, we could instead disable this _only_ for the
Project API.

Closes https://github.com/astral-sh/uv/issues/4946.
This commit is contained in:
Charlie Marsh 2024-07-21 20:04:03 -04:00 committed by GitHub
parent 32ad3323a1
commit d798bb3973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 10 additions and 100 deletions

2
Cargo.lock generated
View file

@ -4867,11 +4867,9 @@ dependencies = [
"rayon",
"rustc-hash 2.0.0",
"same-file",
"serde",
"tempfile",
"thiserror",
"tokio",
"toml",
"tracing",
"url",
"uv-cache",

View file

@ -39,11 +39,9 @@ futures = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
same-file = { workspace = true }
serde = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
walkdir = { workspace = true }

View file

@ -123,9 +123,6 @@ impl<'a> Planner<'a> {
RequirementSatisfaction::OutOfDate => {
debug!("Requirement installed, but not fresh: {distribution}");
}
RequirementSatisfaction::Dynamic => {
debug!("Requirement installed, but dynamic: {distribution}");
}
}
reinstalls.push(distribution.clone());
}

View file

@ -1,9 +1,6 @@
use std::fmt::Debug;
use std::path::Path;
use anyhow::Result;
use same_file::is_same_file;
use serde::Deserialize;
use tracing::{debug, trace};
use url::Url;
@ -17,14 +14,16 @@ pub(crate) enum RequirementSatisfaction {
Mismatch,
Satisfied,
OutOfDate,
Dynamic,
}
impl RequirementSatisfaction {
/// Returns true if a requirement is satisfied by an installed distribution.
///
/// Returns an error if IO fails during a freshness check for a local path.
pub(crate) fn check(distribution: &InstalledDist, source: &RequirementSource) -> Result<Self> {
pub(crate) fn check(
distribution: &InstalledDist,
source: &RequirementSource,
) -> anyhow::Result<Self> {
trace!(
"Comparing installed with source: {:?} {:?}",
distribution,
@ -250,59 +249,8 @@ impl RequirementSatisfaction {
return Ok(Self::OutOfDate);
}
// Does the package have dynamic metadata?
if is_dynamic(requested_path) {
trace!("Dependency is dynamic");
return Ok(Self::Dynamic);
}
Ok(Self::Satisfied)
}
}
}
}
/// Returns `true` if the source tree at the given path contains dynamic metadata.
fn is_dynamic(path: &Path) -> bool {
// If there's no `pyproject.toml`, we assume it's dynamic.
let Ok(contents) = fs_err::read_to_string(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)]
#[serde(rename_all = "kebab-case")]
struct Project {
dynamic: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
struct Tool {
poetry: Option<ToolPoetry>,
}
#[derive(Deserialize, Debug)]
struct ToolPoetry {
#[allow(dead_code)]
name: Option<String>,
}

View file

@ -311,9 +311,7 @@ impl SitePackages {
distribution,
entry.requirement.source().as_ref(),
)? {
RequirementSatisfaction::Mismatch
| RequirementSatisfaction::OutOfDate
| RequirementSatisfaction::Dynamic => {
RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => {
return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string()))
}
RequirementSatisfaction::Satisfied => {}
@ -323,8 +321,7 @@ impl SitePackages {
for constraint in constraints.get(&distribution.name()).into_iter().flatten() {
match RequirementSatisfaction::check(distribution, &constraint.source)? {
RequirementSatisfaction::Mismatch
| RequirementSatisfaction::OutOfDate
| RequirementSatisfaction::Dynamic => {
| RequirementSatisfaction::OutOfDate => {
return Ok(SatisfiesResult::Unsatisfied(
entry.requirement.to_string(),
))

View file

@ -3051,10 +3051,10 @@ requires-python = ">=3.8"
}
#[test]
fn invalidate_editable_dynamic() -> Result<()> {
fn editable_dynamic() -> Result<()> {
let context = TestContext::new("3.12");
// Create an editable package with dynamic metadata
// Create an editable package with dynamic metadata.
let editable_dir = context.temp_dir.child("editable");
editable_dir.create_dir_all()?;
let pyproject_toml = editable_dir.child("pyproject.toml");
@ -3092,7 +3092,7 @@ dependencies = {file = ["requirements.txt"]}
"###
);
// Re-installing should re-install.
// Re-installing should not re-install, as we don't special-case dynamic metadata.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--editable")
.arg(editable_dir.path()), @r###"
@ -3101,35 +3101,7 @@ dependencies = {file = ["requirements.txt"]}
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- example==0.1.0 (from file://[TEMP_DIR]/editable)
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
"###
);
// Modify the requirements.
requirements_txt.write_str("anyio==3.7.1")?;
// Re-installing should update the package.
uv_snapshot!(context.filters(), context.pip_install()
.arg("--editable")
.arg(editable_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 2 packages in [TIME]
Uninstalled 2 packages in [TIME]
Installed 2 packages in [TIME]
- anyio==4.0.0
+ anyio==3.7.1
- example==0.1.0 (from file://[TEMP_DIR]/editable)
+ example==0.1.0 (from file://[TEMP_DIR]/editable)
Audited 1 package in [TIME]
"###
);