mirror of https://github.com/astral-sh/uv
List and uninstall legacy editables (#3415)
This commit is contained in:
parent
94cf604574
commit
18516b4e41
|
|
@ -325,7 +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 features that are considered legacy or deprecated in `pip`. For example,
|
||||||
uv does not support `.egg`-style distributions.
|
uv does not support `.egg`-style distributions.
|
||||||
|
|
||||||
However, uv does have partial support for `.egg-info`-style distributions, which are occasionally
|
However, uv does have partial support for (1) `.egg-info`-style distributions (which are
|
||||||
found in Docker images and Conda environments. Specifically, uv does not support installing new
|
occasionally found in Docker images and Conda environments) and (2) legacy editable
|
||||||
`.egg-info`-style distributions, but it will respect any existing `.egg-info`-style distributions
|
`.egg-link`-style distributions.
|
||||||
during resolution, and can uninstall `.egg-info` distributions with `uv pip uninstall`.
|
|
||||||
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ pub enum InstalledDist {
|
||||||
Url(InstalledDirectUrlDist),
|
Url(InstalledDirectUrlDist),
|
||||||
/// The distribution was derived from pre-existing `.egg-info` directory.
|
/// The distribution was derived from pre-existing `.egg-info` directory.
|
||||||
EggInfo(InstalledEggInfo),
|
EggInfo(InstalledEggInfo),
|
||||||
|
/// The distribution was derived from an `.egg-link` pointer.
|
||||||
|
LegacyEditable(InstalledLegacyEditable),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -48,11 +50,14 @@ pub struct InstalledEggInfo {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format of the distribution.
|
#[derive(Debug, Clone)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
pub struct InstalledLegacyEditable {
|
||||||
pub enum Format {
|
pub name: PackageName,
|
||||||
DistInfo,
|
pub version: Version,
|
||||||
EggInfo,
|
pub egg_link: PathBuf,
|
||||||
|
pub target: PathBuf,
|
||||||
|
pub target_url: Url,
|
||||||
|
pub egg_info: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstalledDist {
|
impl InstalledDist {
|
||||||
|
|
@ -125,16 +130,63 @@ 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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_else(|| anyhow!("Invalid `.egg-link` path: {}", path.user_display()))?
|
||||||
|
.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: {}", target.user_display()))?;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`Format`] of the distribution.
|
Ok(None)
|
||||||
pub fn format(&self) -> Format {
|
|
||||||
match self {
|
|
||||||
Self::Registry(_) => Format::DistInfo,
|
|
||||||
Self::Url(_) => Format::DistInfo,
|
|
||||||
Self::EggInfo(_) => Format::EggInfo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`Path`] at which the distribution is stored on-disk.
|
/// Return the [`Path`] at which the distribution is stored on-disk.
|
||||||
|
|
@ -143,6 +195,7 @@ impl InstalledDist {
|
||||||
Self::Registry(dist) => &dist.path,
|
Self::Registry(dist) => &dist.path,
|
||||||
Self::Url(dist) => &dist.path,
|
Self::Url(dist) => &dist.path,
|
||||||
Self::EggInfo(dist) => &dist.path,
|
Self::EggInfo(dist) => &dist.path,
|
||||||
|
Self::LegacyEditable(dist) => &dist.egg_info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,6 +205,7 @@ impl InstalledDist {
|
||||||
Self::Registry(dist) => &dist.version,
|
Self::Registry(dist) => &dist.version,
|
||||||
Self::Url(dist) => &dist.version,
|
Self::Url(dist) => &dist.version,
|
||||||
Self::EggInfo(dist) => &dist.version,
|
Self::EggInfo(dist) => &dist.version,
|
||||||
|
Self::LegacyEditable(dist) => &dist.version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,8 +221,8 @@ impl InstalledDist {
|
||||||
|
|
||||||
/// Read the `METADATA` file from a `.dist-info` directory.
|
/// Read the `METADATA` file from a `.dist-info` directory.
|
||||||
pub fn metadata(&self) -> Result<pypi_types::Metadata23> {
|
pub fn metadata(&self) -> Result<pypi_types::Metadata23> {
|
||||||
match self.format() {
|
match self {
|
||||||
Format::DistInfo => {
|
Self::Registry(_) | Self::Url(_) => {
|
||||||
let path = self.path().join("METADATA");
|
let path = self.path().join("METADATA");
|
||||||
let contents = fs::read(&path)?;
|
let contents = fs::read(&path)?;
|
||||||
// TODO(zanieb): Update this to use thiserror so we can unpack parse errors downstream
|
// TODO(zanieb): Update this to use thiserror so we can unpack parse errors downstream
|
||||||
|
|
@ -179,8 +233,12 @@ impl InstalledDist {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Format::EggInfo => {
|
Self::EggInfo(_) | Self::LegacyEditable(_) => {
|
||||||
let path = self.path().join("PKG-INFO");
|
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)?;
|
let contents = fs::read(&path)?;
|
||||||
pypi_types::Metadata23::parse_metadata(&contents).with_context(|| {
|
pypi_types::Metadata23::parse_metadata(&contents).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
|
|
@ -208,6 +266,7 @@ impl InstalledDist {
|
||||||
Self::Registry(_) => false,
|
Self::Registry(_) => false,
|
||||||
Self::Url(dist) => dist.editable,
|
Self::Url(dist) => dist.editable,
|
||||||
Self::EggInfo(_) => false,
|
Self::EggInfo(_) => false,
|
||||||
|
Self::LegacyEditable(_) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,6 +276,7 @@ impl InstalledDist {
|
||||||
Self::Registry(_) => None,
|
Self::Registry(_) => None,
|
||||||
Self::Url(dist) => dist.editable.then_some(&dist.url),
|
Self::Url(dist) => dist.editable.then_some(&dist.url),
|
||||||
Self::EggInfo(_) => None,
|
Self::EggInfo(_) => None,
|
||||||
|
Self::LegacyEditable(dist) => Some(&dist.target_url),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,12 +305,19 @@ impl Name for InstalledEggInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Name for InstalledLegacyEditable {
|
||||||
|
fn name(&self) -> &PackageName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Name for InstalledDist {
|
impl Name for InstalledDist {
|
||||||
fn name(&self) -> &PackageName {
|
fn name(&self) -> &PackageName {
|
||||||
match self {
|
match self {
|
||||||
Self::Registry(dist) => dist.name(),
|
Self::Registry(dist) => dist.name(),
|
||||||
Self::Url(dist) => dist.name(),
|
Self::Url(dist) => dist.name(),
|
||||||
Self::EggInfo(dist) => dist.name(),
|
Self::EggInfo(dist) => dist.name(),
|
||||||
|
Self::LegacyEditable(dist) => dist.name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,12 +340,19 @@ impl InstalledMetadata for InstalledEggInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InstalledMetadata for InstalledLegacyEditable {
|
||||||
|
fn installed_version(&self) -> InstalledVersion {
|
||||||
|
InstalledVersion::Version(&self.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl InstalledMetadata for InstalledDist {
|
impl InstalledMetadata for InstalledDist {
|
||||||
fn installed_version(&self) -> InstalledVersion {
|
fn installed_version(&self) -> InstalledVersion {
|
||||||
match self {
|
match self {
|
||||||
Self::Registry(dist) => dist.installed_version(),
|
Self::Registry(dist) => dist.installed_version(),
|
||||||
Self::Url(dist) => dist.installed_version(),
|
Self::Url(dist) => dist.installed_version(),
|
||||||
Self::EggInfo(dist) => dist.installed_version(),
|
Self::EggInfo(dist) => dist.installed_version(),
|
||||||
|
Self::LegacyEditable(dist) => dist.installed_version(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use zip::result::ZipError;
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use platform_tags::{Arch, Os};
|
use platform_tags::{Arch, Os};
|
||||||
use pypi_types::Scheme;
|
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_fs::Simplified;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
|
|
@ -108,4 +108,6 @@ pub enum Error {
|
||||||
MismatchedName(PackageName, PackageName),
|
MismatchedName(PackageName, PackageName),
|
||||||
#[error("Wheel version does not match filename: {0} != {1}")]
|
#[error("Wheel version does not match filename: {0} != {1}")]
|
||||||
MismatchedVersion(Version, Version),
|
MismatchedVersion(Version, Version),
|
||||||
|
#[error("Invalid egg-link")]
|
||||||
|
InvalidEggLink(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ use std::collections::BTreeSet;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::sync::Mutex;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
use uv_fs::write_atomic_sync;
|
||||||
|
|
||||||
use crate::wheel::read_record_file;
|
use crate::wheel::read_record_file;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
@ -208,6 +211,71 @@ pub fn uninstall_egg(egg_info: &Path) -> Result<Uninstall, Error> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static EASY_INSTALL_PTH: Lazy<Mutex<i32>> = Lazy::new(Mutex::default);
|
||||||
|
|
||||||
|
/// Uninstall the legacy editable represented by the `.egg-link` file.
|
||||||
|
///
|
||||||
|
/// See: <https://github.com/pypa/pip/blob/41587f5e0017bcd849f42b314dc8a34a7db75621/src/pip/_internal/req/req_uninstall.py#L534-L552>
|
||||||
|
pub fn uninstall_legacy_editable(egg_link: &Path) -> Result<Uninstall, Error> {
|
||||||
|
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 = 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(()) => {
|
||||||
|
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");
|
||||||
|
|
||||||
|
// 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::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 !removed && line.trim() == target_line {
|
||||||
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Uninstall {
|
pub struct Uninstall {
|
||||||
/// The number of files that were removed during the uninstallation.
|
/// The number of files that were removed during the uninstallation.
|
||||||
|
|
|
||||||
|
|
@ -50,16 +50,21 @@ impl<'a> SitePackages<'a> {
|
||||||
let site_packages = match fs::read_dir(site_packages) {
|
let site_packages = match fs::read_dir(site_packages) {
|
||||||
Ok(site_packages) => {
|
Ok(site_packages) => {
|
||||||
// Collect sorted directory paths; `read_dir` is not stable across platforms
|
// 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 {
|
.filter_map(|read_dir| match read_dir {
|
||||||
Ok(entry) => match entry.file_type() {
|
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)),
|
||||||
},
|
},
|
||||||
Err(err) => Some(Err(err)),
|
Err(err) => Some(Err(err)),
|
||||||
})
|
})
|
||||||
.collect::<Result<_, std::io::Error>>()?;
|
.collect::<Result<_, std::io::Error>>()?;
|
||||||
directories
|
dist_likes
|
||||||
}
|
}
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use distribution_types::{Format, 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(
|
pub async fn uninstall(
|
||||||
dist: &InstalledDist,
|
dist: &InstalledDist,
|
||||||
) -> Result<install_wheel_rs::Uninstall, UninstallError> {
|
) -> 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 dist = dist.clone();
|
||||||
let format = dist.format();
|
move || match dist {
|
||||||
move || match format {
|
InstalledDist::Registry(_) | InstalledDist::Url(_) => {
|
||||||
Format::DistInfo => install_wheel_rs::uninstall_wheel(&path),
|
install_wheel_rs::uninstall_wheel(dist.path())
|
||||||
Format::EggInfo => install_wheel_rs::uninstall_egg(&path),
|
}
|
||||||
|
InstalledDist::EggInfo(_) => install_wheel_rs::uninstall_egg(dist.path()),
|
||||||
|
InstalledDist::LegacyEditable(dist) => {
|
||||||
|
install_wheel_rs::uninstall_legacy_editable(&dist.egg_link)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ pub(crate) fn pip_freeze(
|
||||||
InstalledDist::EggInfo(dist) => {
|
InstalledDist::EggInfo(dist) => {
|
||||||
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
||||||
}
|
}
|
||||||
|
InstalledDist::LegacyEditable(dist) => {
|
||||||
|
writeln!(printer.stdout(), "-e {}", dist.target.display())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -263,3 +263,41 @@ fn freeze_with_egg_info() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use assert_fs::fixture::ChildPath;
|
||||||
use assert_fs::fixture::FileWriteStr;
|
use assert_fs::fixture::FileWriteStr;
|
||||||
use assert_fs::fixture::PathChild;
|
use assert_fs::fixture::PathChild;
|
||||||
|
use assert_fs::prelude::*;
|
||||||
|
|
||||||
use common::uv_snapshot;
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -473,3 +473,60 @@ fn uninstall_egg_info() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue