uv/crates/uv/tests/it/pip_uninstall.rs

527 lines
13 KiB
Rust

use std::process::Command;
use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::fixture::ChildPath;
use assert_fs::prelude::*;
use crate::common::{TestContext, get_bin, uv_snapshot};
#[test]
fn no_arguments() {
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.env_clear(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the following required arguments were not provided:
<PACKAGE|--requirements <REQUIREMENTS>>
Usage: uv pip uninstall <PACKAGE|--requirements <REQUIREMENTS>>
For more information, try '--help'.
"###
);
}
#[test]
fn invalid_requirement() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("flask==1.0.x")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `flask==1.0.x`
Caused by: after parsing `1.0`, found `.x`, which is not part of a valid version
flask==1.0.x
^^^^^^^
"###);
Ok(())
}
#[test]
fn missing_requirements_txt() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("requirements.txt")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: File not found: `requirements.txt`
"###
);
Ok(())
}
#[test]
fn invalid_requirements_txt_requirement() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let requirements_txt = temp_dir.child("requirements.txt");
requirements_txt.write_str("flask==1.0.x")?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("requirements.txt")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Couldn't parse requirement in `requirements.txt` at position 0
Caused by: after parsing `1.0`, found `.x`, which is not part of a valid version
flask==1.0.x
^^^^^^^
"###);
Ok(())
}
#[test]
#[cfg(feature = "pypi")]
fn uninstall() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3")?;
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
context.assert_command("import markupsafe").success();
uv_snapshot!(context.pip_uninstall()
.arg("MarkupSafe"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 1 package in [TIME]
- markupsafe==2.1.3
"###
);
context.assert_command("import markupsafe").failure();
Ok(())
}
#[test]
#[cfg(feature = "pypi")]
fn missing_record() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("MarkupSafe==2.1.3")?;
context
.pip_sync()
.arg("requirements.txt")
.assert()
.success();
context.assert_command("import markupsafe").success();
// Delete the RECORD file.
let dist_info = context.site_packages().join("MarkupSafe-2.1.3.dist-info");
fs_err::remove_file(dist_info.join("RECORD"))?;
uv_snapshot!(context.filters(), context.pip_uninstall()
.arg("MarkupSafe"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Cannot uninstall package; `RECORD` file not found at: [SITE_PACKAGES]/MarkupSafe-2.1.3.dist-info/RECORD
"
);
Ok(())
}
#[test]
#[cfg(feature = "pypi")]
fn uninstall_editable_by_name() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(&format!(
"-e {}",
context
.workspace_root
.join("test/packages/flit_editable")
.as_os_str()
.to_str()
.expect("Path is valid unicode")
))?;
context
.pip_sync()
.arg(requirements_txt.path())
.assert()
.success();
context.assert_command("import flit_editable").success();
// Uninstall the editable by name.
uv_snapshot!(context.filters(), context.pip_uninstall()
.arg("flit-editable"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 1 package in [TIME]
- flit-editable==0.1.0 (from file://[WORKSPACE]/test/packages/flit_editable)
"###
);
context.assert_command("import flit_editable").failure();
Ok(())
}
#[test]
#[cfg(feature = "pypi")]
fn uninstall_by_path() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(
context
.workspace_root
.join("test/packages/flit_editable")
.as_os_str()
.to_str()
.expect("Path is valid unicode"),
)?;
context
.pip_sync()
.arg(requirements_txt.path())
.assert()
.success();
context.assert_command("import flit_editable").success();
// Uninstall the editable by path.
uv_snapshot!(context.filters(), context.pip_uninstall()
.arg(context.workspace_root.join("test/packages/flit_editable")), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 1 package in [TIME]
- flit-editable==0.1.0 (from file://[WORKSPACE]/test/packages/flit_editable)
"###
);
context.assert_command("import flit_editable").failure();
Ok(())
}
#[test]
#[cfg(feature = "pypi")]
fn uninstall_duplicate_by_path() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(
context
.workspace_root
.join("test/packages/flit_editable")
.as_os_str()
.to_str()
.expect("Path is valid unicode"),
)?;
context
.pip_sync()
.arg(requirements_txt.path())
.assert()
.success();
context.assert_command("import flit_editable").success();
// Uninstall the editable by both path and name.
uv_snapshot!(context.filters(), context.pip_uninstall()
.arg("flit-editable")
.arg(context.workspace_root.join("test/packages/flit_editable")), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 1 package in [TIME]
- flit-editable==0.1.0 (from file://[WORKSPACE]/test/packages/flit_editable)
"###
);
context.assert_command("import flit_editable").failure();
Ok(())
}
/// Uninstall a duplicate package in a virtual environment.
#[test]
#[cfg(feature = "pypi")]
fn uninstall_duplicate() -> Result<()> {
use uv_fs::copy_dir_all;
// Sync a version of `pip` into a virtual environment.
let context1 = TestContext::new("3.12");
let requirements_txt = context1.temp_dir.child("requirements.txt");
requirements_txt.write_str("pip==21.3.1")?;
// Run `pip sync`.
context1
.pip_sync()
.arg(requirements_txt.path())
.assert()
.success();
// Sync a different version of `pip` into a virtual environment.
let context2 = TestContext::new("3.12");
let requirements_txt = context2.temp_dir.child("requirements.txt");
requirements_txt.write_str("pip==22.1.1")?;
// Run `pip sync`.
context2
.pip_sync()
.arg(requirements_txt.path())
.assert()
.success();
// Copy the virtual environment to a new location.
copy_dir_all(
context2.site_packages().join("pip-22.1.1.dist-info"),
context1.site_packages().join("pip-22.1.1.dist-info"),
)?;
// Run `pip uninstall`.
uv_snapshot!(context1.pip_uninstall()
.arg("pip"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 2 packages in [TIME]
- pip==21.3.1
- pip==22.1.1
"###
);
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!(context.pip_uninstall()
.arg("zstandard"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Uninstalled 1 package in [TIME]
- zstandard==0.22.0
"###);
Ok(())
}
fn normcase(s: &str) -> String {
if cfg!(windows) {
s.replace('/', "\\").to_lowercase()
} else {
s.to_owned()
}
}
/// 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.1
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",
normcase(target.path().to_str().unwrap())
))?;
// Run `pip uninstall`.
uv_snapshot!(context.pip_uninstall()
.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(())
}
#[test]
fn dry_run_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!(context.pip_uninstall()
.arg("--dry-run")
.arg("zstandard"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Would uninstall 1 package
- zstandard==0.22.0
"###);
// The `.egg-info` directory should still exist.
assert!(
site_packages
.child("zstandard-0.22.0-py3.12.egg-info")
.exists()
);
// The package directory should still exist.
assert!(site_packages.child("zstd").child("__init__.py").exists());
Ok(())
}