mirror of https://github.com/astral-sh/uv
527 lines
13 KiB
Rust
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(())
|
|
}
|