mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-30 14:01:13 +00:00
Treat uninstallable packages as warnings, rather than errors (#2557)
## Summary Closes https://github.com/astral-sh/uv/issues/2467.
This commit is contained in:
parent
baa30697a4
commit
cfd18aa1a2
5 changed files with 124 additions and 20 deletions
|
@ -4,7 +4,7 @@ pub use editable::{is_dynamic, BuiltEditable, ResolvedEditable};
|
||||||
pub use installer::{Installer, Reporter as InstallReporter};
|
pub use installer::{Installer, Reporter as InstallReporter};
|
||||||
pub use plan::{Plan, Planner, Reinstall};
|
pub use plan::{Plan, Planner, Reinstall};
|
||||||
pub use site_packages::{Diagnostic, SitePackages};
|
pub use site_packages::{Diagnostic, SitePackages};
|
||||||
pub use uninstall::uninstall;
|
pub use uninstall::{uninstall, UninstallError};
|
||||||
pub use uv_traits::NoBinary;
|
pub use uv_traits::NoBinary;
|
||||||
|
|
||||||
mod compile;
|
mod compile;
|
||||||
|
|
|
@ -3,7 +3,9 @@ use anyhow::Result;
|
||||||
use distribution_types::InstalledDist;
|
use distribution_types::InstalledDist;
|
||||||
|
|
||||||
/// Uninstall a package from the specified Python environment.
|
/// Uninstall a package from the specified Python environment.
|
||||||
pub async fn uninstall(dist: &InstalledDist) -> Result<install_wheel_rs::Uninstall> {
|
pub async fn uninstall(
|
||||||
|
dist: &InstalledDist,
|
||||||
|
) -> Result<install_wheel_rs::Uninstall, UninstallError> {
|
||||||
let uninstall = tokio::task::spawn_blocking({
|
let uninstall = tokio::task::spawn_blocking({
|
||||||
let path = dist.path().to_owned();
|
let path = dist.path().to_owned();
|
||||||
move || install_wheel_rs::uninstall_wheel(&path)
|
move || install_wheel_rs::uninstall_wheel(&path)
|
||||||
|
@ -12,3 +14,11 @@ pub async fn uninstall(dist: &InstalledDist) -> Result<install_wheel_rs::Uninsta
|
||||||
|
|
||||||
Ok(uninstall)
|
Ok(uninstall)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum UninstallError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Uninstall(#[from] install_wheel_rs::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Join(#[from] tokio::task::JoinError),
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ use uv_resolver::{
|
||||||
ResolutionGraph, ResolutionMode, Resolver,
|
ResolutionGraph, ResolutionMode, Resolver,
|
||||||
};
|
};
|
||||||
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
||||||
|
@ -671,7 +672,8 @@ async fn install(
|
||||||
// Remove any existing installations.
|
// Remove any existing installations.
|
||||||
if !reinstalls.is_empty() {
|
if !reinstalls.is_empty() {
|
||||||
for dist_info in &reinstalls {
|
for dist_info in &reinstalls {
|
||||||
let summary = uv_installer::uninstall(dist_info).await?;
|
match uv_installer::uninstall(dist_info).await {
|
||||||
|
Ok(summary) => {
|
||||||
debug!(
|
debug!(
|
||||||
"Uninstalled {} ({} file{}, {} director{})",
|
"Uninstalled {} ({} file{}, {} director{})",
|
||||||
dist_info.name(),
|
dist_info.name(),
|
||||||
|
@ -681,6 +683,17 @@ async fn install(
|
||||||
if summary.dir_count == 1 { "y" } else { "ies" },
|
if summary.dir_count == 1 { "y" } else { "ies" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Err(uv_installer::UninstallError::Uninstall(
|
||||||
|
install_wheel_rs::Error::MissingRecord(_),
|
||||||
|
)) => {
|
||||||
|
warn_user!(
|
||||||
|
"Failed to uninstall package at {} due to missing RECORD file. Installation may result in an incomplete environment.",
|
||||||
|
dist_info.path().simplified_display().cyan(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install the resolved distributions.
|
// Install the resolved distributions.
|
||||||
|
@ -939,6 +952,9 @@ enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Resolve(#[from] uv_resolver::ResolveError),
|
Resolve(#[from] uv_resolver::ResolveError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Uninstall(#[from] uv_installer::UninstallError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Client(#[from] uv_client::Error),
|
Client(#[from] uv_client::Error),
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ use uv_installer::{
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
use uv_resolver::InMemoryIndex;
|
use uv_resolver::InMemoryIndex;
|
||||||
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter};
|
use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter};
|
||||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
||||||
|
@ -286,7 +287,8 @@ pub(crate) async fn pip_sync(
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
for dist_info in extraneous.iter().chain(reinstalls.iter()) {
|
for dist_info in extraneous.iter().chain(reinstalls.iter()) {
|
||||||
let summary = uv_installer::uninstall(dist_info).await?;
|
match uv_installer::uninstall(dist_info).await {
|
||||||
|
Ok(summary) => {
|
||||||
debug!(
|
debug!(
|
||||||
"Uninstalled {} ({} file{}, {} director{})",
|
"Uninstalled {} ({} file{}, {} director{})",
|
||||||
dist_info.name(),
|
dist_info.name(),
|
||||||
|
@ -296,6 +298,17 @@ pub(crate) async fn pip_sync(
|
||||||
if summary.dir_count == 1 { "y" } else { "ies" },
|
if summary.dir_count == 1 { "y" } else { "ies" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Err(uv_installer::UninstallError::Uninstall(
|
||||||
|
install_wheel_rs::Error::MissingRecord(_),
|
||||||
|
)) => {
|
||||||
|
warn_user!(
|
||||||
|
"Failed to uninstall package at {} due to missing RECORD file. Installation may result in an incomplete environment.",
|
||||||
|
dist_info.path().simplified_display().cyan(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let s = if extraneous.len() + reinstalls.len() == 1 {
|
let s = if extraneous.len() + reinstalls.len() == 1 {
|
||||||
""
|
""
|
||||||
|
|
|
@ -434,6 +434,71 @@ fn reinstall_extras() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Warn, but don't fail, when uninstalling incomplete packages.
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn reinstall_incomplete() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Install anyio.
|
||||||
|
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||||
|
requirements_txt.touch()?;
|
||||||
|
requirements_txt.write_str("anyio==3.7.0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(command(&context)
|
||||||
|
.arg("-r")
|
||||||
|
.arg("requirements.txt"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
Downloaded 3 packages in [TIME]
|
||||||
|
Installed 3 packages in [TIME]
|
||||||
|
+ anyio==3.7.0
|
||||||
|
+ idna==3.4
|
||||||
|
+ sniffio==1.3.0
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
// Manually remove the `RECORD` file.
|
||||||
|
fs_err::remove_file(
|
||||||
|
context
|
||||||
|
.venv
|
||||||
|
.join("lib/python3.12/site-packages/anyio-3.7.0.dist-info/RECORD"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Re-install anyio.
|
||||||
|
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||||
|
requirements_txt.touch()?;
|
||||||
|
requirements_txt.write_str("anyio==4.0.0")?;
|
||||||
|
|
||||||
|
let filters = [(r"Failed to uninstall package at .* due to missing RECORD", "Failed to uninstall package at .venv/lib/python3.12/site-packages/anyio-3.7.0.dist-info due to missing RECORD")]
|
||||||
|
.into_iter()
|
||||||
|
.chain(INSTA_FILTERS.to_vec())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
uv_snapshot!(filters, command(&context)
|
||||||
|
.arg("-r")
|
||||||
|
.arg("requirements.txt"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
warning: Failed to uninstall package at .venv/lib/python3.12/site-packages/anyio-3.7.0.dist-info due to missing RECORD file. Installation may result in an incomplete environment.
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
- anyio==3.7.0
|
||||||
|
+ anyio==4.0.0
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Like `pip`, we (unfortunately) allow incompatible environments.
|
/// Like `pip`, we (unfortunately) allow incompatible environments.
|
||||||
#[test]
|
#[test]
|
||||||
fn allow_incompatibilities() -> Result<()> {
|
fn allow_incompatibilities() -> Result<()> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue