mirror of https://github.com/astral-sh/uv
Respect and enable uninstalls of existing `.egg-info` packages (#3380)
## Summary Users often find themselves dropped into environments that contain `.egg-info` packages. While we won't support installing these, it's not hard to support identifying them (e.g., in `pip freeze`) and _uninstalling_ them. Closes https://github.com/astral-sh/uv/issues/2841. Closes #2928. Closes #3341. ## Test Plan Ran `cargo run pip freeze --python /opt/homebrew/Caskroom/miniforge/base/envs/TEST/bin/python`, with an environment that includes `pip` as an `.egg-info` (`/opt/homebrew/Caskroom/miniforge/base/envs/TEST/lib/python3.12/site-packages/pip-24.0-py3.12.egg-info`): ``` cffi @ file:///Users/runner/miniforge3/conda-bld/cffi_1696001825047/work pip==24.0 pycparser @ file:///home/conda/feedstock_root/build_artifacts/pycparser_1711811537435/work setuptools==69.5.1 wheel==0.43.0 ``` Then ran `cargo run pip uninstall`, verified that `pip` was uninstalled, and no longer listed in `pip freeze`.
This commit is contained in:
parent
098944fc7d
commit
26045e5f59
|
|
@ -320,10 +320,12 @@ Unlike `pip`, uv does not enable keyring authentication by default.
|
||||||
Unlike `pip`, uv does not wait until a request returns a HTTP 401 before searching for
|
Unlike `pip`, uv does not wait until a request returns a HTTP 401 before searching for
|
||||||
authentication. uv attaches authentication to all requests for hosts with credentials available.
|
authentication. uv attaches authentication to all requests for hosts with credentials available.
|
||||||
|
|
||||||
## Legacy features
|
## `egg` support
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
uv does not plan to support features that the `pip` maintainers explicitly recommend against,
|
However, uv does have partial support for `.egg-info`-style distributions, which are occasionally
|
||||||
like `--target`.
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ pub enum InstalledDist {
|
||||||
Registry(InstalledRegistryDist),
|
Registry(InstalledRegistryDist),
|
||||||
/// The distribution was derived from an arbitrary URL.
|
/// The distribution was derived from an arbitrary URL.
|
||||||
Url(InstalledDirectUrlDist),
|
Url(InstalledDirectUrlDist),
|
||||||
|
/// The distribution was derived from pre-existing `.egg-info` directory.
|
||||||
|
EggInfo(InstalledEggInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -39,11 +41,26 @@ pub struct InstalledDirectUrlDist {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InstalledEggInfo {
|
||||||
|
pub name: PackageName,
|
||||||
|
pub version: Version,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The format of the distribution.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Format {
|
||||||
|
DistInfo,
|
||||||
|
EggInfo,
|
||||||
|
}
|
||||||
|
|
||||||
impl InstalledDist {
|
impl InstalledDist {
|
||||||
/// Try to parse a distribution from a `.dist-info` directory name (like `django-5.0a1.dist-info`).
|
/// Try to parse a distribution from a `.dist-info` directory name (like `django-5.0a1.dist-info`).
|
||||||
///
|
///
|
||||||
/// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
|
/// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
|
||||||
pub fn try_from_path(path: &Path) -> Result<Option<Self>> {
|
pub fn try_from_path(path: &Path) -> Result<Option<Self>> {
|
||||||
|
// Ex) `cffi-1.16.0.dist-info`
|
||||||
if path.extension().is_some_and(|ext| ext == "dist-info") {
|
if path.extension().is_some_and(|ext| ext == "dist-info") {
|
||||||
let Some(file_stem) = path.file_stem() else {
|
let Some(file_stem) = path.file_stem() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -84,14 +101,48 @@ impl InstalledDist {
|
||||||
})))
|
})))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ex) `zstandard-0.22.0-py3.12.egg-info`
|
||||||
|
if path.extension().is_some_and(|ext| ext == "egg-info") {
|
||||||
|
let Some(file_stem) = path.file_stem() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(file_stem) = file_stem.to_str() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some((name, version_python)) = file_stem.split_once('-') else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some((version, _)) = version_python.split_once('-') else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let name = PackageName::from_str(name)?;
|
||||||
|
let version = Version::from_str(version).map_err(|err| anyhow!(err))?;
|
||||||
|
return Ok(Some(Self::EggInfo(InstalledEggInfo {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(None)
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the [`Path`] at which the distribution is stored on-disk.
|
/// Return the [`Path`] at which the distribution is stored on-disk.
|
||||||
pub fn path(&self) -> &Path {
|
pub fn path(&self) -> &Path {
|
||||||
match self {
|
match self {
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +151,7 @@ impl InstalledDist {
|
||||||
match self {
|
match self {
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,11 +167,29 @@ 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> {
|
||||||
let path = self.path().join("METADATA");
|
match self.format() {
|
||||||
let contents = fs::read(&path)?;
|
Format::DistInfo => {
|
||||||
// TODO(zanieb): Update this to use thiserror so we can unpack parse errors downstream
|
let path = self.path().join("METADATA");
|
||||||
pypi_types::Metadata23::parse_metadata(&contents)
|
let contents = fs::read(&path)?;
|
||||||
.with_context(|| format!("Failed to parse METADATA file at: {}", path.user_display()))
|
// TODO(zanieb): Update this to use thiserror so we can unpack parse errors downstream
|
||||||
|
pypi_types::Metadata23::parse_metadata(&contents).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to parse `METADATA` file at: {}",
|
||||||
|
path.user_display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Format::EggInfo => {
|
||||||
|
let path = self.path().join("PKG-INFO");
|
||||||
|
let contents = fs::read(&path)?;
|
||||||
|
pypi_types::Metadata23::parse_metadata(&contents).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to parse `PKG-INFO` file at: {}",
|
||||||
|
path.user_display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the `INSTALLER` of the distribution.
|
/// Return the `INSTALLER` of the distribution.
|
||||||
|
|
@ -137,6 +207,7 @@ impl InstalledDist {
|
||||||
match self {
|
match self {
|
||||||
Self::Registry(_) => false,
|
Self::Registry(_) => false,
|
||||||
Self::Url(dist) => dist.editable,
|
Self::Url(dist) => dist.editable,
|
||||||
|
Self::EggInfo(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,6 +216,7 @@ impl InstalledDist {
|
||||||
match self {
|
match self {
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,11 +239,18 @@ impl Name for InstalledDirectUrlDist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Name for InstalledEggInfo {
|
||||||
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,11 +267,18 @@ impl InstalledMetadata for InstalledDirectUrlDist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InstalledMetadata for InstalledEggInfo {
|
||||||
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_wheel, Uninstall};
|
pub use uninstall::{uninstall_egg, uninstall_wheel, Uninstall};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
|
|
@ -82,8 +82,10 @@ pub enum Error {
|
||||||
DirectUrlJson(#[from] serde_json::Error),
|
DirectUrlJson(#[from] serde_json::Error),
|
||||||
#[error("No .dist-info directory found")]
|
#[error("No .dist-info directory found")]
|
||||||
MissingDistInfo,
|
MissingDistInfo,
|
||||||
#[error("Cannot uninstall package; RECORD file not found at: {}", _0.user_display())]
|
#[error("Cannot uninstall package; `RECORD` file not found at: {}", _0.user_display())]
|
||||||
MissingRecord(PathBuf),
|
MissingRecord(PathBuf),
|
||||||
|
#[error("Cannot uninstall package; `top_level.txt` file not found at: {}", _0.user_display())]
|
||||||
|
MissingTopLevel(PathBuf),
|
||||||
#[error("Multiple .dist-info directories found: {0}")]
|
#[error("Multiple .dist-info directories found: {0}")]
|
||||||
MultipleDistInfo(String),
|
MultipleDistInfo(String),
|
||||||
#[error(
|
#[error(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use tracing::debug;
|
||||||
use crate::wheel::read_record_file;
|
use crate::wheel::read_record_file;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// Uninstall the wheel represented by the given `dist_info` directory.
|
/// Uninstall the wheel represented by the given `.dist-info` directory.
|
||||||
pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
|
pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
|
||||||
let Some(site_packages) = dist_info.parent() else {
|
let Some(site_packages) = dist_info.parent() else {
|
||||||
return Err(Error::BrokenVenv(
|
return Err(Error::BrokenVenv(
|
||||||
|
|
@ -118,6 +118,96 @@ pub fn uninstall_wheel(dist_info: &Path) -> Result<Uninstall, Error> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Uninstall the egg represented by the `.egg-info` directory.
|
||||||
|
///
|
||||||
|
/// See: <https://github.com/pypa/pip/blob/41587f5e0017bcd849f42b314dc8a34a7db75621/src/pip/_internal/req/req_uninstall.py#L483>
|
||||||
|
pub fn uninstall_egg(egg_info: &Path) -> Result<Uninstall, Error> {
|
||||||
|
let mut file_count = 0usize;
|
||||||
|
let mut dir_count = 0usize;
|
||||||
|
|
||||||
|
let dist_location = egg_info
|
||||||
|
.parent()
|
||||||
|
.expect("egg-info directory is not in a site-packages directory");
|
||||||
|
|
||||||
|
// Read the `namespace_packages.txt` file.
|
||||||
|
let namespace_packages = {
|
||||||
|
let namespace_packages_path = egg_info.join("namespace_packages.txt");
|
||||||
|
match fs_err::read_to_string(namespace_packages_path) {
|
||||||
|
Ok(namespace_packages) => namespace_packages
|
||||||
|
.lines()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the `top_level.txt` file, ignoring anything in `namespace_packages.txt`.
|
||||||
|
let top_level = {
|
||||||
|
let top_level_path = egg_info.join("top_level.txt");
|
||||||
|
match fs_err::read_to_string(&top_level_path) {
|
||||||
|
Ok(top_level) => top_level
|
||||||
|
.lines()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.filter(|line| !namespace_packages.contains(line))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
return Err(Error::MissingTopLevel(top_level_path));
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove everything in `top_level.txt`.
|
||||||
|
for entry in top_level {
|
||||||
|
let path = dist_location.join(&entry);
|
||||||
|
|
||||||
|
// Remove as a directory.
|
||||||
|
match fs_err::remove_dir_all(&path) {
|
||||||
|
Ok(()) => {
|
||||||
|
debug!("Removed directory: {}", path.display());
|
||||||
|
dir_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove as a `.py`, `.pyc`, or `.pyo` file.
|
||||||
|
for exten in &["py", "pyc", "pyo"] {
|
||||||
|
let path = path.with_extension(exten);
|
||||||
|
match fs_err::remove_file(&path) {
|
||||||
|
Ok(()) => {
|
||||||
|
debug!("Removed file: {}", path.display());
|
||||||
|
file_count += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the `.egg-info` directory.
|
||||||
|
match fs_err::remove_dir_all(egg_info) {
|
||||||
|
Ok(()) => {
|
||||||
|
debug!("Removed directory: {}", egg_info.display());
|
||||||
|
dir_count += 1;
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Uninstall {
|
||||||
|
file_count,
|
||||||
|
dir_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[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.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use distribution_types::InstalledDist;
|
use distribution_types::{Format, 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(
|
||||||
|
|
@ -8,7 +8,11 @@ pub async fn uninstall(
|
||||||
) -> 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 path = dist.path().to_owned();
|
||||||
move || install_wheel_rs::uninstall_wheel(&path)
|
let format = dist.format();
|
||||||
|
move || match format {
|
||||||
|
Format::DistInfo => install_wheel_rs::uninstall_wheel(&path),
|
||||||
|
Format::EggInfo => install_wheel_rs::uninstall_egg(&path),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@ pub(crate) fn pip_freeze(
|
||||||
writeln!(printer.stdout(), "{} @ {}", dist.name().bold(), dist.url)?;
|
writeln!(printer.stdout(), "{} @ {}", dist.name().bold(), dist.url)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
InstalledDist::EggInfo(dist) => {
|
||||||
|
writeln!(printer.stdout(), "{}=={}", dist.name().bold(), dist.version)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::process::Command;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
|
use assert_fs::fixture::ChildPath;
|
||||||
use assert_fs::prelude::*;
|
use assert_fs::prelude::*;
|
||||||
|
|
||||||
use crate::common::{get_bin, uv_snapshot, TestContext};
|
use crate::common::{get_bin, uv_snapshot, TestContext};
|
||||||
|
|
@ -210,3 +211,55 @@ fn freeze_with_editable() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show an `.egg-info` package in a virtual environment.
|
||||||
|
#[test]
|
||||||
|
fn freeze_with_egg_info() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let site_packages = ChildPath::new(context.site_packages());
|
||||||
|
|
||||||
|
// Manually create a `.egg-info` directory.
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.create_dir_all()?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("top_level.txt")
|
||||||
|
.write_str("zstd")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("SOURCES.txt")
|
||||||
|
.write_str("")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("PKG-INFO")
|
||||||
|
.write_str("")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("dependency_links.txt")
|
||||||
|
.write_str("")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("entry_points.txt")
|
||||||
|
.write_str("")?;
|
||||||
|
|
||||||
|
// Manually create the package directory.
|
||||||
|
site_packages.child("zstd").create_dir_all()?;
|
||||||
|
site_packages
|
||||||
|
.child("zstd")
|
||||||
|
.child("__init__.py")
|
||||||
|
.write_str("")?;
|
||||||
|
|
||||||
|
// Run `pip freeze`.
|
||||||
|
uv_snapshot!(context.filters(), command(&context), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
zstandard==0.22.0
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::process::Command;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
|
use assert_fs::fixture::ChildPath;
|
||||||
use assert_fs::prelude::*;
|
use assert_fs::prelude::*;
|
||||||
|
|
||||||
use common::uv_snapshot;
|
use common::uv_snapshot;
|
||||||
|
|
@ -220,7 +221,7 @@ fn missing_record() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Cannot uninstall package; RECORD file not found at: [SITE_PACKAGES]/MarkupSafe-2.1.3.dist-info/RECORD
|
error: Cannot uninstall package; `RECORD` file not found at: [SITE_PACKAGES]/MarkupSafe-2.1.3.dist-info/RECORD
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -418,3 +419,57 @@ fn uninstall_duplicate() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Uninstall a `.egg-info` package in a virtual environment.
|
||||||
|
#[test]
|
||||||
|
fn uninstall_egg_info() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let site_packages = ChildPath::new(context.site_packages());
|
||||||
|
|
||||||
|
// Manually create a `.egg-info` directory.
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.create_dir_all()?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("top_level.txt")
|
||||||
|
.write_str("zstd")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("SOURCES.txt")
|
||||||
|
.write_str("")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("PKG-INFO")
|
||||||
|
.write_str("")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("dependency_links.txt")
|
||||||
|
.write_str("")?;
|
||||||
|
site_packages
|
||||||
|
.child("zstandard-0.22.0-py3.12.egg-info")
|
||||||
|
.child("entry_points.txt")
|
||||||
|
.write_str("")?;
|
||||||
|
|
||||||
|
// Manually create the package directory.
|
||||||
|
site_packages.child("zstd").create_dir_all()?;
|
||||||
|
site_packages
|
||||||
|
.child("zstd")
|
||||||
|
.child("__init__.py")
|
||||||
|
.write_str("")?;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue