Disallow `pyproject.toml` from `pip uninstall` (#2663)

## Summary

Passing `pyproject.toml` or `setup.py` to `pip uninstall` is a bit
strange, since it will often require running a resolution to resolve the
dependencies (e.g., build the project), which means we also need to
accept `--index-url` and friends.
This commit is contained in:
Charlie Marsh 2024-03-25 21:35:43 -04:00 committed by GitHub
parent 5270624b11
commit 12846c2c85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 149 additions and 141 deletions

View File

@ -39,22 +39,44 @@ impl RequirementsSource {
}
}
/// Parse a [`RequirementsSource`] from a constraints file.
pub fn from_constraints_file(path: PathBuf) -> Self {
if path.ends_with("pyproject.toml") {
warn_user!(
"The file `{}` appears to be a `pyproject.toml` file, but constraints must be specified in `requirements.txt` format.", path.user_display()
);
/// Parse a [`RequirementsSource`] from a `requirements.txt` file.
pub fn from_requirements_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
warn_user!(
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.",
path.user_display(),
filename
);
}
}
Self::RequirementsTxt(path)
}
/// Parse a [`RequirementsSource`] from an overrides file.
pub fn from_overrides_file(path: PathBuf) -> Self {
if path.ends_with("pyproject.toml") {
warn_user!(
"The file `{}` appears to be a `pyproject.toml` file, but overrides must be specified in `requirements.txt` format.", path.user_display()
);
/// Parse a [`RequirementsSource`] from a `constraints.txt` file.
pub fn from_constraints_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
warn_user!(
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.",
path.user_display(),
filename
);
}
}
Self::RequirementsTxt(path)
}
/// Parse a [`RequirementsSource`] from an `overrides.txt` file.
pub fn from_overrides_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
warn_user!(
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.",
path.user_display(),
filename
);
}
}
Self::RequirementsTxt(path)
}

View File

@ -22,6 +22,10 @@ use uv_installer::{
is_dynamic, Downloader, NoBinary, Plan, Planner, Reinstall, ResolvedEditable, SitePackages,
};
use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_requirements::{
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
SourceTreeResolver,
};
use uv_resolver::InMemoryIndex;
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
use uv_warnings::warn_user;
@ -31,10 +35,6 @@ use crate::commands::reporters::{
};
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
use crate::printer::Printer;
use uv_requirements::{
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
SourceTreeResolver,
};
/// Install a set of locked requirements into the current Python environment.
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]

View File

@ -1469,12 +1469,12 @@ async fn run() -> Result<ExitStatus> {
let constraints = args
.constraint
.into_iter()
.map(RequirementsSource::from_constraints_file)
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.r#override
.into_iter()
.map(RequirementsSource::from_overrides_file)
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let index_urls = IndexLocations::new(
args.index_url.and_then(Maybe::into_option),
@ -1624,12 +1624,12 @@ async fn run() -> Result<ExitStatus> {
let constraints = args
.constraint
.into_iter()
.map(RequirementsSource::from_constraints_file)
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.r#override
.into_iter()
.map(RequirementsSource::from_overrides_file)
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let index_urls = IndexLocations::new(
args.index_url.and_then(Maybe::into_option),
@ -1714,7 +1714,7 @@ async fn run() -> Result<ExitStatus> {
.chain(
args.requirement
.into_iter()
.map(RequirementsSource::from_requirements_file),
.map(RequirementsSource::from_requirements_txt),
)
.collect::<Vec<_>>();
commands::pip_uninstall(

View File

@ -132,6 +132,112 @@ fn empty_requirements_txt() -> Result<()> {
Ok(())
}
#[test]
fn missing_pyproject_toml() {
let context = TestContext::new("3.12");
uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to read from file `pyproject.toml`
Caused by: No such file or directory (os error 2)
"###
);
}
#[test]
fn invalid_pyproject_toml_syntax() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str("123 - 456")?;
uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 5
|
1 | 123 - 456
| ^
expected `.`, `=`
"###
);
Ok(())
}
#[test]
fn invalid_pyproject_toml_schema() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str("[project]")?;
uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^^^^^^^^^
missing field `name`
"###
);
Ok(())
}
#[test]
fn invalid_pyproject_toml_requirement() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = ["flask==1.0.x"]
"#,
)?;
uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 3, column 16
|
3 | dependencies = ["flask==1.0.x"]
| ^^^^^^^^^^^^^^^^
after parsing 1.0, found ".x" after it, which is not part of a valid version
flask==1.0.x
^^^^^^^
"###
);
Ok(())
}
#[test]
fn no_solution() {
let context = TestContext::new("3.12");

View File

@ -149,126 +149,6 @@ fn invalid_requirements_txt_requirement() -> Result<()> {
Ok(())
}
#[test]
fn missing_pyproject_toml() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to read from file `pyproject.toml`
Caused by: No such file or directory (os error 2)
"###
);
Ok(())
}
#[test]
fn invalid_pyproject_toml_syntax() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str("123 - 456")?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 5
|
1 | 123 - 456
| ^
expected `.`, `=`
"###);
Ok(())
}
#[test]
fn invalid_pyproject_toml_schema() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str("[project]")?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^^^^^^^^^
missing field `name`
"###);
Ok(())
}
#[test]
fn invalid_pyproject_toml_requirement() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = ["flask==1.0.x"]
"#,
)?;
uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 3, column 16
|
3 | dependencies = ["flask==1.0.x"]
| ^^^^^^^^^^^^^^^^
after parsing 1.0, found ".x" after it, which is not part of a valid version
flask==1.0.x
^^^^^^^
"###);
Ok(())
}
#[test]
fn uninstall() -> Result<()> {
let context = TestContext::new("3.12");