From c1f8475a5af0af38ca0805f5d9a26c073e8ca951 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sun, 5 May 2024 17:36:36 -0700 Subject: [PATCH 1/4] List and uninstall legacy editables --- PIP_COMPATIBILITY.md | 9 +- crates/distribution-types/src/installed.rs | 106 +++++++++++++++++---- crates/install-wheel-rs/src/lib.rs | 4 +- crates/install-wheel-rs/src/uninstall.rs | 53 +++++++++++ crates/uv-installer/src/site_packages.rs | 11 ++- crates/uv-installer/src/uninstall.rs | 16 ++-- crates/uv/src/commands/pip_freeze.rs | 3 + crates/uv/tests/pip_freeze.rs | 38 ++++++++ crates/uv/tests/pip_list.rs | 61 ++++++++++++ crates/uv/tests/pip_uninstall.rs | 57 +++++++++++ 10 files changed, 327 insertions(+), 31 deletions(-) diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index ccd48473359b..a1e780504fe7 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -325,7 +325,8 @@ authentication. uv attaches authentication to all requests for hosts with creden uv does not support features that are considered legacy or deprecated in `pip`. For example, uv does not support `.egg`-style distributions. -However, uv does have partial support for `.egg-info`-style distributions, which are occasionally -found in Docker images and Conda environments. Specifically, uv does not support installing new -`.egg-info`-style distributions, but it will respect any existing `.egg-info`-style distributions -during resolution, and can uninstall `.egg-info` distributions with `uv pip uninstall`. +However, uv does have partial support for `.egg-info`-style distributions (which are occasionally +found in Docker images and Conda environments) and legacy editable `.egg-link`-style distributions. +Specifically, uv does not support installing new `.egg-info`-style distributions, but it will +respect any existing `.egg-info`-style distributions during resolution, and can uninstall +`.egg-info` distributions with `uv pip uninstall`. diff --git a/crates/distribution-types/src/installed.rs b/crates/distribution-types/src/installed.rs index 648506a649b8..81055b46fc5b 100644 --- a/crates/distribution-types/src/installed.rs +++ b/crates/distribution-types/src/installed.rs @@ -22,6 +22,8 @@ pub enum InstalledDist { Url(InstalledDirectUrlDist), /// The distribution was derived from pre-existing `.egg-info` directory. EggInfo(InstalledEggInfo), + /// The distribution was derived from an `.egg-link` pointer + LegacyEditable(InstalledLegacyEditable), } #[derive(Debug, Clone)] @@ -48,11 +50,14 @@ pub struct InstalledEggInfo { pub path: PathBuf, } -/// The format of the distribution. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Format { - DistInfo, - EggInfo, +#[derive(Debug, Clone)] +pub struct InstalledLegacyEditable { + pub name: PackageName, + pub version: Version, + pub egg_link: PathBuf, + pub target: PathBuf, + pub target_url: Url, + pub egg_info: PathBuf, } impl InstalledDist { @@ -125,16 +130,61 @@ impl InstalledDist { }))); } - Ok(None) - } + // Ex) `zstandard.egg-link` + if path.extension().is_some_and(|ext| ext == "egg-link") { + let Some(file_stem) = path.file_stem() else { + return Ok(None); + }; + let Some(file_stem) = file_stem.to_str() else { + return Ok(None); + }; - /// Return the [`Format`] of the distribution. - pub fn format(&self) -> Format { - match self { - Self::Registry(_) => Format::DistInfo, - Self::Url(_) => Format::DistInfo, - Self::EggInfo(_) => Format::EggInfo, + // https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#egg-links + // https://github.com/pypa/pip/blob/946f95d17431f645da8e2e0bf4054a72db5be766/src/pip/_internal/metadata/importlib/_envs.py#L86-L108 + let contents = fs::read_to_string(path)?; + let target = if let Some(line) = contents.lines().find(|line| !line.is_empty()) { + PathBuf::from(line.trim()) + } else { + warn!("Invalid .egg-link file: {path:?}"); + return Ok(None); + }; + // Match pip, but note setuptools only puts absolute paths in .egg-link files + let target = path + .parent() + .ok_or(anyhow!("Invalid egg-link path"))? + .join(target); + // Normalisation comes from `pkg_resources.to_filename` + let egg_info = target.join(file_stem.replace('-', "_") + ".egg-info"); + let url = + Url::from_file_path(&target).map_err(|()| anyhow!("Invalid egg-link target"))?; + + // Mildly unfortunate that we must read metadata to get the version + let content = match fs::read(egg_info.join("PKG-INFO")) { + Ok(content) => content, + Err(err) => { + warn!("Failed to read metadata for {path:?}: {err}"); + return Ok(None); + } + }; + let metadata = match pypi_types::Metadata23::parse_pkg_info(&content) { + Ok(metadata) => metadata, + Err(err) => { + warn!("Failed to parse metadata for {path:?}: {err}"); + return Ok(None); + } + }; + + return Ok(Some(Self::LegacyEditable(InstalledLegacyEditable { + name: metadata.name, + version: metadata.version, + egg_link: path.to_path_buf(), + target, + target_url: url, + egg_info, + }))); } + + Ok(None) } /// Return the [`Path`] at which the distribution is stored on-disk. @@ -143,6 +193,7 @@ impl InstalledDist { Self::Registry(dist) => &dist.path, Self::Url(dist) => &dist.path, Self::EggInfo(dist) => &dist.path, + Self::LegacyEditable(dist) => &dist.egg_info, } } @@ -152,6 +203,7 @@ impl InstalledDist { Self::Registry(dist) => &dist.version, Self::Url(dist) => &dist.version, Self::EggInfo(dist) => &dist.version, + Self::LegacyEditable(dist) => &dist.version, } } @@ -167,8 +219,8 @@ impl InstalledDist { /// Read the `METADATA` file from a `.dist-info` directory. pub fn metadata(&self) -> Result { - match self.format() { - Format::DistInfo => { + match self { + Self::Registry(_) | Self::Url(_) => { let path = self.path().join("METADATA"); let contents = fs::read(&path)?; // TODO(zanieb): Update this to use thiserror so we can unpack parse errors downstream @@ -179,8 +231,12 @@ impl InstalledDist { ) }) } - Format::EggInfo => { - let path = self.path().join("PKG-INFO"); + Self::EggInfo(_) | Self::LegacyEditable(_) => { + let path = match self { + Self::EggInfo(dist) => dist.path.join("PKG-INFO"), + Self::LegacyEditable(dist) => dist.egg_info.join("PKG-INFO"), + _ => unreachable!(), + }; let contents = fs::read(&path)?; pypi_types::Metadata23::parse_metadata(&contents).with_context(|| { format!( @@ -208,6 +264,7 @@ impl InstalledDist { Self::Registry(_) => false, Self::Url(dist) => dist.editable, Self::EggInfo(_) => false, + Self::LegacyEditable(_) => true, } } @@ -217,6 +274,7 @@ impl InstalledDist { Self::Registry(_) => None, Self::Url(dist) => dist.editable.then_some(&dist.url), Self::EggInfo(_) => None, + Self::LegacyEditable(dist) => Some(&dist.target_url), } } } @@ -245,12 +303,19 @@ impl Name for InstalledEggInfo { } } +impl Name for InstalledLegacyEditable { + fn name(&self) -> &PackageName { + &self.name + } +} + impl Name for InstalledDist { fn name(&self) -> &PackageName { match self { Self::Registry(dist) => dist.name(), Self::Url(dist) => dist.name(), Self::EggInfo(dist) => dist.name(), + Self::LegacyEditable(dist) => dist.name(), } } } @@ -273,12 +338,19 @@ impl InstalledMetadata for InstalledEggInfo { } } +impl InstalledMetadata for InstalledLegacyEditable { + fn installed_version(&self) -> InstalledVersion { + InstalledVersion::Version(&self.version) + } +} + impl InstalledMetadata for InstalledDist { fn installed_version(&self) -> InstalledVersion { match self { Self::Registry(dist) => dist.installed_version(), Self::Url(dist) => dist.installed_version(), Self::EggInfo(dist) => dist.installed_version(), + Self::LegacyEditable(dist) => dist.installed_version(), } } } diff --git a/crates/install-wheel-rs/src/lib.rs b/crates/install-wheel-rs/src/lib.rs index da2a01d3779a..8f2e9e33c563 100644 --- a/crates/install-wheel-rs/src/lib.rs +++ b/crates/install-wheel-rs/src/lib.rs @@ -11,7 +11,7 @@ use zip::result::ZipError; use pep440_rs::Version; use platform_tags::{Arch, Os}; use pypi_types::Scheme; -pub use uninstall::{uninstall_egg, uninstall_wheel, Uninstall}; +pub use uninstall::{uninstall_egg, uninstall_legacy_editable, uninstall_wheel, Uninstall}; use uv_fs::Simplified; use uv_normalize::PackageName; @@ -108,4 +108,6 @@ pub enum Error { MismatchedName(PackageName, PackageName), #[error("Wheel version does not match filename: {0} != {1}")] MismatchedVersion(Version, Version), + #[error("Invalid egg-link")] + InvalidEggLink(PathBuf), } diff --git a/crates/install-wheel-rs/src/uninstall.rs b/crates/install-wheel-rs/src/uninstall.rs index 34c9962d2a4c..766d6788203f 100644 --- a/crates/install-wheel-rs/src/uninstall.rs +++ b/crates/install-wheel-rs/src/uninstall.rs @@ -3,6 +3,7 @@ use std::path::{Component, Path, PathBuf}; use fs_err as fs; use tracing::debug; +use uv_fs::write_atomic_sync; use crate::wheel::read_record_file; use crate::Error; @@ -208,6 +209,58 @@ pub fn uninstall_egg(egg_info: &Path) -> Result { }) } +/// Uninstall the legacy editable represented by the `.egg-link` file. +/// +/// See: +pub fn uninstall_legacy_editable(egg_link: &Path) -> Result { + let mut file_count = 0usize; + + let contents = fs::read_to_string(egg_link)?; + + let target_line = if let Some(line) = contents.lines().find(|line| !line.is_empty()) { + line.trim() + } else { + return Err(Error::InvalidEggLink(egg_link.to_path_buf())); + }; + + match fs::remove_file(egg_link) { + Ok(()) => { + debug!("Removed file: {}", egg_link.display()); + file_count += 1; + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + + let site_package = egg_link.parent().ok_or(Error::BrokenVenv( + "egg-link file is not in a directory".to_string(), + ))?; + let easy_install = site_package.join("easy-install.pth"); + + // Note concurrent use of `easy-install.pth` may result in lost writes + let content = fs::read_to_string(&easy_install)?; + let mut new_content = String::new(); + let mut removed = false; + // https://github.com/pypa/pip/blob/41587f5e0017bcd849f42b314dc8a34a7db75621/src/pip/_internal/req/req_uninstall.py#L634 + for line in content.lines() { + if line.trim() == target_line && !removed { + removed = true; + } else { + new_content.push_str(line); + new_content.push('\n'); + } + } + if removed { + write_atomic_sync(&easy_install, new_content)?; + debug!("Removed line from easy-install.pth: {}", target_line); + } + + Ok(Uninstall { + file_count, + dir_count: 0usize, + }) +} + #[derive(Debug, Default)] pub struct Uninstall { /// The number of files that were removed during the uninstallation. diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 2acb8154abc0..77bb7ca4031b 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -50,16 +50,21 @@ impl<'a> SitePackages<'a> { let site_packages = match fs::read_dir(site_packages) { Ok(site_packages) => { // Collect sorted directory paths; `read_dir` is not stable across platforms - let directories: BTreeSet<_> = site_packages + let dist_likes: BTreeSet<_> = site_packages .filter_map(|read_dir| match read_dir { Ok(entry) => match entry.file_type() { - Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())), + Ok(file_type) => (file_type.is_dir() + || entry + .path() + .extension() + .map_or(false, |ext| ext == "egg-link")) + .then_some(Ok(entry.path())), Err(err) => Some(Err(err)), }, Err(err) => Some(Err(err)), }) .collect::>()?; - directories + dist_likes } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Ok(Self { diff --git a/crates/uv-installer/src/uninstall.rs b/crates/uv-installer/src/uninstall.rs index 63954d985075..a6f239de426c 100644 --- a/crates/uv-installer/src/uninstall.rs +++ b/crates/uv-installer/src/uninstall.rs @@ -1,17 +1,21 @@ use anyhow::Result; -use distribution_types::{Format, InstalledDist}; +use distribution_types::InstalledDist; /// Uninstall a package from the specified Python environment. pub async fn uninstall( dist: &InstalledDist, ) -> Result { let uninstall = tokio::task::spawn_blocking({ - let path = dist.path().to_owned(); - let format = dist.format(); - move || match format { - Format::DistInfo => install_wheel_rs::uninstall_wheel(&path), - Format::EggInfo => install_wheel_rs::uninstall_egg(&path), + let dist = dist.clone(); + move || match dist { + InstalledDist::Registry(_) | InstalledDist::Url(_) => { + install_wheel_rs::uninstall_wheel(dist.path()) + } + InstalledDist::EggInfo(_) => install_wheel_rs::uninstall_egg(dist.path()), + InstalledDist::LegacyEditable(dist) => { + install_wheel_rs::uninstall_legacy_editable(&dist.egg_link) + } } }) .await??; diff --git a/crates/uv/src/commands/pip_freeze.rs b/crates/uv/src/commands/pip_freeze.rs index a02101eee4a6..0cf862702105 100644 --- a/crates/uv/src/commands/pip_freeze.rs +++ b/crates/uv/src/commands/pip_freeze.rs @@ -65,6 +65,9 @@ pub(crate) fn pip_freeze( InstalledDist::EggInfo(dist) => { writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?; } + InstalledDist::LegacyEditable(dist) => { + writeln!(printer.stdout(), "-e {}", dist.target.display())?; + } } } diff --git a/crates/uv/tests/pip_freeze.rs b/crates/uv/tests/pip_freeze.rs index 3e88577e99a0..d4640fed3bba 100644 --- a/crates/uv/tests/pip_freeze.rs +++ b/crates/uv/tests/pip_freeze.rs @@ -263,3 +263,41 @@ fn freeze_with_egg_info() -> Result<()> { Ok(()) } + +#[test] +fn freeze_with_legacy_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let site_packages = ChildPath::new(context.site_packages()); + + let target = context.temp_dir.child("zstandard_project"); + target.child("zstd").create_dir_all()?; + target.child("zstd").child("__init__.py").write_str("")?; + + target.child("zstandard.egg-info").create_dir_all()?; + target + .child("zstandard.egg-info") + .child("PKG-INFO") + .write_str( + "Metadata-Version: 2.2 +Name: zstandard +Version: 0.22.0 +", + )?; + + site_packages + .child("zstandard.egg-link") + .write_str(target.path().to_str().unwrap())?; + + // Run `pip freeze`. + uv_snapshot!(context.filters(), command(&context), @r###" + success: true + exit_code: 0 + ----- stdout ----- + -e [TEMP_DIR]/zstandard_project + + ----- stderr ----- + "###); + + Ok(()) +} diff --git a/crates/uv/tests/pip_list.rs b/crates/uv/tests/pip_list.rs index 4b4fb7b36770..88e5e86054c7 100644 --- a/crates/uv/tests/pip_list.rs +++ b/crates/uv/tests/pip_list.rs @@ -1,8 +1,10 @@ use std::process::Command; use anyhow::Result; +use assert_fs::fixture::ChildPath; use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; +use assert_fs::prelude::*; use common::uv_snapshot; @@ -565,3 +567,62 @@ fn list_format_freeze() { "### ); } + +#[test] +fn list_legacy_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let site_packages = ChildPath::new(context.site_packages()); + + let target = context.temp_dir.child("zstandard_project"); + target.child("zstd").create_dir_all()?; + target.child("zstd").child("__init__.py").write_str("")?; + + target.child("zstandard.egg-info").create_dir_all()?; + target + .child("zstandard.egg-info") + .child("PKG-INFO") + .write_str( + "Metadata-Version: 2.2 +Name: zstandard +Version: 0.22.0 +", + )?; + + site_packages + .child("zstandard.egg-link") + .write_str(target.path().to_str().unwrap())?; + + site_packages.child("easy-install.pth").write_str(&format!( + "something\n{}\nanother thing\n", + target.path().to_str().unwrap() + ))?; + + let filters = context + .filters() + .into_iter() + .chain(vec![(r"\-\-\-\-\-\-+.*", "[UNDERLINE]"), (" +", " ")]) + .collect::>(); + + uv_snapshot!(filters, Command::new(get_bin()) + .arg("pip") + .arg("list") + .arg("--editable") + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") + .current_dir(&context.temp_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Package Version Editable project location + [UNDERLINE] + zstandard 0.22.0 [TEMP_DIR]/zstandard_project + + ----- stderr ----- + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/pip_uninstall.rs b/crates/uv/tests/pip_uninstall.rs index b6eca9003f84..d3ceca9fe422 100644 --- a/crates/uv/tests/pip_uninstall.rs +++ b/crates/uv/tests/pip_uninstall.rs @@ -473,3 +473,60 @@ fn uninstall_egg_info() -> Result<()> { Ok(()) } + +/// Uninstall a legacy editable package in a virtual environment. +#[test] +fn uninstall_legacy_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let site_packages = ChildPath::new(context.site_packages()); + + let target = context.temp_dir.child("zstandard_project"); + target.child("zstd").create_dir_all()?; + target.child("zstd").child("__init__.py").write_str("")?; + + target.child("zstandard.egg-info").create_dir_all()?; + target + .child("zstandard.egg-info") + .child("PKG-INFO") + .write_str( + "Metadata-Version: 2.2 +Name: zstandard +Version: 0.22.0 +", + )?; + + site_packages + .child("zstandard.egg-link") + .write_str(target.path().to_str().unwrap())?; + + site_packages.child("easy-install.pth").write_str(&format!( + "something\n{}\nanother thing\n", + target.path().to_str().unwrap() + ))?; + + // Run `pip uninstall`. + uv_snapshot!(uninstall_command(&context) + .arg("zstandard"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + - zstandard==0.22.0 + "###); + + // The entry in `easy-install.pth` should be removed. + assert_eq!( + fs_err::read_to_string(site_packages.child("easy-install.pth"))?, + "something\nanother thing\n", + "easy-install.pth should not contain the path to the uninstalled package" + ); + // The `.egg-link` file should be removed. + assert!(!site_packages.child("zstandard.egg-link").exists()); + // The `.egg-info` directory should still exist. + assert!(target.child("zstandard.egg-info").exists()); + + Ok(()) +} From ad389b4951a0fd72fc1781fbef9b52eb88cda46c Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 6 May 2024 14:55:27 -0700 Subject: [PATCH 2/4] use a mutex --- crates/install-wheel-rs/src/uninstall.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/install-wheel-rs/src/uninstall.rs b/crates/install-wheel-rs/src/uninstall.rs index 766d6788203f..460d25eca93a 100644 --- a/crates/install-wheel-rs/src/uninstall.rs +++ b/crates/install-wheel-rs/src/uninstall.rs @@ -2,6 +2,8 @@ use std::collections::BTreeSet; use std::path::{Component, Path, PathBuf}; use fs_err as fs; +use once_cell::sync::Lazy; +use std::sync::Mutex; use tracing::debug; use uv_fs::write_atomic_sync; @@ -209,6 +211,8 @@ pub fn uninstall_egg(egg_info: &Path) -> Result { }) } +static EASY_INSTALL_PTH: Lazy> = Lazy::new(Mutex::default); + /// Uninstall the legacy editable represented by the `.egg-link` file. /// /// See: @@ -237,7 +241,11 @@ pub fn uninstall_legacy_editable(egg_link: &Path) -> Result { ))?; let easy_install = site_package.join("easy-install.pth"); - // Note concurrent use of `easy-install.pth` may result in lost writes + // Since uv has an environment lock, it's enough to add a mutex here to ensure we never + // lose writes to easy-install.pth (this is the only place in uv where easy-install.pth + // is modified) + let _guard = EASY_INSTALL_PTH.lock().unwrap(); + let content = fs::read_to_string(&easy_install)?; let mut new_content = String::new(); let mut removed = false; From e23c91f52e7df1a5230fc3cb7ca6d3d4ca0b24a2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 6 May 2024 23:34:20 -0400 Subject: [PATCH 3/4] Minor tweaks --- crates/distribution-types/src/installed.rs | 18 +++++++------ crates/install-wheel-rs/src/uninstall.rs | 31 +++++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/distribution-types/src/installed.rs b/crates/distribution-types/src/installed.rs index 81055b46fc5b..2eb382534560 100644 --- a/crates/distribution-types/src/installed.rs +++ b/crates/distribution-types/src/installed.rs @@ -22,7 +22,7 @@ pub enum InstalledDist { Url(InstalledDirectUrlDist), /// The distribution was derived from pre-existing `.egg-info` directory. EggInfo(InstalledEggInfo), - /// The distribution was derived from an `.egg-link` pointer + /// The distribution was derived from an `.egg-link` pointer. LegacyEditable(InstalledLegacyEditable), } @@ -145,20 +145,22 @@ impl InstalledDist { let target = if let Some(line) = contents.lines().find(|line| !line.is_empty()) { PathBuf::from(line.trim()) } else { - warn!("Invalid .egg-link file: {path:?}"); + warn!("Invalid `.egg-link` file: {path:?}"); return Ok(None); }; - // Match pip, but note setuptools only puts absolute paths in .egg-link files + + // Match pip, but note setuptools only puts absolute paths in `.egg-link` files. let target = path .parent() - .ok_or(anyhow!("Invalid egg-link path"))? + .ok_or_else(|| anyhow!("Invalid `.egg-link` path: {}", path.user_display()))? .join(target); - // Normalisation comes from `pkg_resources.to_filename` + + // Normalisation comes from `pkg_resources.to_filename`. let egg_info = target.join(file_stem.replace('-', "_") + ".egg-info"); - let url = - Url::from_file_path(&target).map_err(|()| anyhow!("Invalid egg-link target"))?; + let url = Url::from_file_path(&target) + .map_err(|()| anyhow!("Invalid `.egg-link` target: {}", target.user_display()))?; - // Mildly unfortunate that we must read metadata to get the version + // Mildly unfortunate that we must read metadata to get the version. let content = match fs::read(egg_info.join("PKG-INFO")) { Ok(content) => content, Err(err) => { diff --git a/crates/install-wheel-rs/src/uninstall.rs b/crates/install-wheel-rs/src/uninstall.rs index 460d25eca93a..760ee6a3aa85 100644 --- a/crates/install-wheel-rs/src/uninstall.rs +++ b/crates/install-wheel-rs/src/uninstall.rs @@ -219,13 +219,19 @@ static EASY_INSTALL_PTH: Lazy> = Lazy::new(Mutex::default); pub fn uninstall_legacy_editable(egg_link: &Path) -> Result { let mut file_count = 0usize; + // Find the target line in the `.egg-link` file. let contents = fs::read_to_string(egg_link)?; - - let target_line = if let Some(line) = contents.lines().find(|line| !line.is_empty()) { - line.trim() - } else { - return Err(Error::InvalidEggLink(egg_link.to_path_buf())); - }; + let target_line = contents + .lines() + .find_map(|line| { + let line = line.trim(); + if line.is_empty() { + None + } else { + Some(line) + } + }) + .ok_or_else(|| Error::InvalidEggLink(egg_link.to_path_buf()))?; match fs::remove_file(egg_link) { Ok(()) => { @@ -237,21 +243,22 @@ pub fn uninstall_legacy_editable(egg_link: &Path) -> Result { } let site_package = egg_link.parent().ok_or(Error::BrokenVenv( - "egg-link file is not in a directory".to_string(), + "`.egg-link` file is not in a directory".to_string(), ))?; let easy_install = site_package.join("easy-install.pth"); // Since uv has an environment lock, it's enough to add a mutex here to ensure we never - // lose writes to easy-install.pth (this is the only place in uv where easy-install.pth - // is modified) + // lose writes to `easy-install.pth` (this is the only place in uv where `easy-install.pth` + // is modified). let _guard = EASY_INSTALL_PTH.lock().unwrap(); let content = fs::read_to_string(&easy_install)?; - let mut new_content = String::new(); + let mut new_content = String::with_capacity(content.len()); let mut removed = false; + // https://github.com/pypa/pip/blob/41587f5e0017bcd849f42b314dc8a34a7db75621/src/pip/_internal/req/req_uninstall.py#L634 for line in content.lines() { - if line.trim() == target_line && !removed { + if !removed && line.trim() == target_line { removed = true; } else { new_content.push_str(line); @@ -260,7 +267,7 @@ pub fn uninstall_legacy_editable(egg_link: &Path) -> Result { } if removed { write_atomic_sync(&easy_install, new_content)?; - debug!("Removed line from easy-install.pth: {}", target_line); + debug!("Removed line from `easy-install.pth`: {target_line}"); } Ok(Uninstall { From 42db30246717edf9bdf730b52f7021e362f90990 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 6 May 2024 23:43:11 -0400 Subject: [PATCH 4/4] Tweak docs --- PIP_COMPATIBILITY.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index a1e780504fe7..3c1db903c329 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -325,8 +325,10 @@ authentication. uv attaches authentication to all requests for hosts with creden uv does not support features that are considered legacy or deprecated in `pip`. For example, uv does not support `.egg`-style distributions. -However, uv does have partial support for `.egg-info`-style distributions (which are occasionally -found in Docker images and Conda environments) and legacy editable `.egg-link`-style distributions. -Specifically, uv does not support installing new `.egg-info`-style distributions, but it will -respect any existing `.egg-info`-style distributions during resolution, and can uninstall -`.egg-info` distributions with `uv pip uninstall`. +However, uv does have partial support for (1) `.egg-info`-style distributions (which are +occasionally found in Docker images and Conda environments) and (2) legacy editable +`.egg-link`-style distributions. + +Specifically, uv does not support installing new `.egg-info`- or `.egg-link`-style distributions, +but will respect any such existing distributions during resolution, list them with `uv pip list` and +`uv pip freeze`, and uninstall them with `uv pip uninstall`.