mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
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:
parent
32ad3323a1
commit
d798bb3973
6 changed files with 10 additions and 100 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4867,11 +4867,9 @@ dependencies = [
|
|||
"rayon",
|
||||
"rustc-hash 2.0.0",
|
||||
"same-file",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"uv-cache",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
))
|
||||
|
|
|
@ -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]
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue