mirror of https://github.com/astral-sh/uv
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:
parent
5270624b11
commit
12846c2c85
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue