From f7319ae391d8a8a04fa1aef47430a524fe0e220d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 25 Mar 2024 20:52:43 -0400 Subject: [PATCH] Disallow pyproject.toml in pip sync and pip uninstall --- crates/uv-requirements/src/sources.rs | 46 +++++++--- crates/uv/src/commands/pip_sync.rs | 8 +- crates/uv/src/main.rs | 10 +-- crates/uv/tests/pip_install.rs | 106 +++++++++++++++++++++++ crates/uv/tests/pip_uninstall.rs | 120 -------------------------- 5 files changed, 149 insertions(+), 141 deletions(-) diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index fd5b8d807754..f6e98709e91f 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -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) } diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index a75076ae5f11..4a61ca7102e4 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -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)] diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 43d4da1242d3..9f7939c667d3 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -1469,12 +1469,12 @@ async fn run() -> Result { let constraints = args .constraint .into_iter() - .map(RequirementsSource::from_constraints_file) + .map(RequirementsSource::from_constraints_txt) .collect::>(); let overrides = args .r#override .into_iter() - .map(RequirementsSource::from_overrides_file) + .map(RequirementsSource::from_overrides_txt) .collect::>(); let index_urls = IndexLocations::new( args.index_url.and_then(Maybe::into_option), @@ -1624,12 +1624,12 @@ async fn run() -> Result { let constraints = args .constraint .into_iter() - .map(RequirementsSource::from_constraints_file) + .map(RequirementsSource::from_constraints_txt) .collect::>(); let overrides = args .r#override .into_iter() - .map(RequirementsSource::from_overrides_file) + .map(RequirementsSource::from_overrides_txt) .collect::>(); let index_urls = IndexLocations::new( args.index_url.and_then(Maybe::into_option), @@ -1714,7 +1714,7 @@ async fn run() -> Result { .chain( args.requirement .into_iter() - .map(RequirementsSource::from_requirements_file), + .map(RequirementsSource::from_requirements_txt), ) .collect::>(); commands::pip_uninstall( diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 197e7aa186d5..824ae5188594 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -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"); diff --git a/crates/uv/tests/pip_uninstall.rs b/crates/uv/tests/pip_uninstall.rs index fd7c380ec735..390af1bd072f 100644 --- a/crates/uv/tests/pip_uninstall.rs +++ b/crates/uv/tests/pip_uninstall.rs @@ -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");