diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index 5ee6d1426..c2055c585 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; @@ -5,7 +6,6 @@ use console::Term; use uv_fs::{Simplified, CWD}; use uv_requirements_txt::RequirementsTxtRequirement; -use uv_warnings::warn_user; #[derive(Debug, Clone)] pub enum RequirementsSource { @@ -32,89 +32,127 @@ pub enum RequirementsSource { impl RequirementsSource { /// Parse a [`RequirementsSource`] from a [`PathBuf`]. The file type is determined by the file /// extension. - pub fn from_requirements_file(path: PathBuf) -> Self { + pub fn from_requirements_file(path: PathBuf) -> Result { if path.ends_with("pyproject.toml") { - Self::PyprojectToml(path) + Ok(Self::PyprojectToml(path)) } else if path.ends_with("setup.py") { - Self::SetupPy(path) + Ok(Self::SetupPy(path)) } else if path.ends_with("setup.cfg") { - Self::SetupCfg(path) + Ok(Self::SetupCfg(path)) } else if path.ends_with("environment.yml") { - Self::EnvironmentYml(path) + Ok(Self::EnvironmentYml(path)) } else if path .file_name() .is_some_and(|file_name| file_name.to_str().is_some_and(is_pylock_toml)) { - Self::PylockToml(path) + Ok(Self::PylockToml(path)) + } else if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) + { + Err(anyhow::anyhow!( + "`{}` is not a valid PEP 751 filename: expected TOML file to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`)", + path.user_display(), + )) } else { - Self::RequirementsTxt(path) + Ok(Self::RequirementsTxt(path)) } } /// Parse a [`RequirementsSource`] from a `requirements.txt` file. - pub fn from_requirements_txt(path: PathBuf) -> Self { + pub fn from_requirements_txt(path: PathBuf) -> Result { for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { if path.ends_with(file_name) { - warn_user!( - "The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.", + return Err(anyhow::anyhow!( + "The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format", path.user_display(), file_name - ); + )); } } - if let Some(file_name) = path.file_name() { - if file_name.to_str().is_some_and(is_pylock_toml) { - warn_user!( - "The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format.", - path.user_display(), - ); - } + if path + .file_name() + .and_then(OsStr::to_str) + .is_some_and(is_pylock_toml) + { + return Err(anyhow::anyhow!( + "The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format", + path.user_display(), + )); + } else if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) + { + return Err(anyhow::anyhow!( + "The file `{}` appears to be a TOML file, but requirements must be specified in `requirements.txt` format", + path.user_display(), + )); } - Self::RequirementsTxt(path) + Ok(Self::RequirementsTxt(path)) } /// Parse a [`RequirementsSource`] from a `constraints.txt` file. - pub fn from_constraints_txt(path: PathBuf) -> Self { + pub fn from_constraints_txt(path: PathBuf) -> Result { for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { if path.ends_with(file_name) { - warn_user!( - "The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.", + return Err(anyhow::anyhow!( + "The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format", path.user_display(), file_name - ); + )); } } - if let Some(file_name) = path.file_name() { - if file_name.to_str().is_some_and(is_pylock_toml) { - warn_user!( - "The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format.", - path.user_display(), - ); - } + if path + .file_name() + .and_then(OsStr::to_str) + .is_some_and(is_pylock_toml) + { + return Err(anyhow::anyhow!( + "The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format", + path.user_display(), + )); + } else if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) + { + return Err(anyhow::anyhow!( + "The file `{}` appears to be a TOML file, but constraints must be specified in `requirements.txt` format", + path.user_display(), + )); } - Self::RequirementsTxt(path) + Ok(Self::RequirementsTxt(path)) } /// Parse a [`RequirementsSource`] from an `overrides.txt` file. - pub fn from_overrides_txt(path: PathBuf) -> Self { + pub fn from_overrides_txt(path: PathBuf) -> Result { for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { if path.ends_with(file_name) { - warn_user!( - "The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.", + return Err(anyhow::anyhow!( + "The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format", path.user_display(), file_name - ); + )); } } - if let Some(file_name) = path.file_name() { - if file_name.to_str().is_some_and(is_pylock_toml) { - warn_user!( - "The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format.", - path.user_display(), - ); - } + if path + .file_name() + .and_then(OsStr::to_str) + .is_some_and(is_pylock_toml) + { + return Err(anyhow::anyhow!( + "The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format", + path.user_display(), + )); + } else if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) + { + return Err(anyhow::anyhow!( + "The file `{}` appears to be a TOML file, but overrides must be specified in `requirements.txt` format", + path.user_display(), + )); } - Self::RequirementsTxt(path) + Ok(Self::RequirementsTxt(path)) } /// Parse a [`RequirementsSource`] from a user-provided string, assumed to be a positional @@ -134,7 +172,7 @@ impl RequirementsSource { ); let confirmation = uv_console::confirm(&prompt, &term, true)?; if confirmation { - return Ok(Self::from_requirements_file(name.into())); + return Self::from_requirements_file(name.into()); } } } @@ -154,7 +192,7 @@ impl RequirementsSource { ); let confirmation = uv_console::confirm(&prompt, &term, true)?; if confirmation { - return Ok(Self::from_requirements_file(name.into())); + return Self::from_requirements_file(name.into()); } } } @@ -182,7 +220,7 @@ impl RequirementsSource { ); let confirmation = uv_console::confirm(&prompt, &term, true)?; if confirmation { - return Ok(Self::from_requirements_file(name.into())); + return Self::from_requirements_file(name.into()); } } } @@ -202,7 +240,7 @@ impl RequirementsSource { ); let confirmation = uv_console::confirm(&prompt, &term, true)?; if confirmation { - return Ok(Self::from_requirements_file(name.into())); + return Self::from_requirements_file(name.into()); } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 1dd8b2464..281c0e91f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -407,22 +407,22 @@ async fn run(mut cli: Cli) -> Result { .src_file .into_iter() .map(RequirementsSource::from_requirements_file) - .collect::>(); + .collect::, _>>()?; let constraints = args .constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; let overrides = args .overrides .into_iter() .map(RequirementsSource::from_overrides_txt) - .collect::>(); + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; let mut groups = BTreeMap::new(); for group in args.settings.groups { @@ -516,17 +516,17 @@ async fn run(mut cli: Cli) -> Result { .src_file .into_iter() .map(RequirementsSource::from_requirements_file) - .collect::>(); + .collect::, _>>()?; let constraints = args .constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; commands::pip_sync( &requirements, @@ -588,23 +588,24 @@ async fn run(mut cli: Cli) -> Result { requirements.extend( args.requirements .into_iter() - .map(RequirementsSource::from_requirements_file), + .map(RequirementsSource::from_requirements_file) + .collect::, _>>()?, ); let constraints = args .constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; let overrides = args .overrides .into_iter() .map(RequirementsSource::from_overrides_txt) - .collect::>(); + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() .map(RequirementsSource::from_overrides_txt) - .collect::>(); + .collect::, _>>()?; let mut groups = BTreeMap::new(); for group in args.settings.groups { @@ -735,7 +736,8 @@ async fn run(mut cli: Cli) -> Result { sources.extend( args.requirements .into_iter() - .map(RequirementsSource::from_requirements_file), + .map(RequirementsSource::from_requirements_file) + .collect::, _>>()?, ); commands::pip_uninstall( &sources, @@ -908,7 +910,7 @@ async fn run(mut cli: Cli) -> Result { .build_constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; commands::build_frontend( &project_dir, @@ -1121,7 +1123,8 @@ async fn run(mut cli: Cli) -> Result { requirements.extend( args.with_requirements .into_iter() - .map(RequirementsSource::from_requirements_file), + .map(RequirementsSource::from_requirements_file) + .collect::, _>>()?, ); requirements }; @@ -1129,18 +1132,18 @@ async fn run(mut cli: Cli) -> Result { .constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; let overrides = args .overrides .into_iter() .map(RequirementsSource::from_overrides_txt) - .collect::>(); + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; Box::pin(commands::tool_run( args.command, @@ -1195,24 +1198,25 @@ async fn run(mut cli: Cli) -> Result { requirements.extend( args.with_requirements .into_iter() - .map(RequirementsSource::from_requirements_file), + .map(RequirementsSource::from_requirements_file) + .collect::, _>>()?, ); let constraints = args .constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; let overrides = args .overrides .into_iter() .map(RequirementsSource::from_overrides_txt) - .collect::>(); + .collect::, _>>()?; let build_constraints = args .build_constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; Box::pin(commands::tool_install( args.package, @@ -1639,7 +1643,8 @@ async fn run_project( requirements.extend( args.with_requirements .into_iter() - .map(RequirementsSource::from_requirements_file), + .map(RequirementsSource::from_requirements_file) + .collect::, _>>()?, ); Box::pin(commands::run( @@ -1803,15 +1808,14 @@ async fn run_project( .chain( args.requirements .into_iter() - .map(RequirementsSource::from_requirements_file) - .map(Ok), + .map(RequirementsSource::from_requirements_file), ) .collect::>>()?; let constraints = args .constraints .into_iter() .map(RequirementsSource::from_constraints_txt) - .collect::>(); + .collect::, _>>()?; Box::pin(commands::add( project_dir, diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 390c9c9b4..88f2088f3 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -245,6 +245,27 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { Ok(()) } +#[test] +fn invalid_toml_filename() -> Result<()> { + let context = TestContext::new("3.12"); + let test_toml = context.temp_dir.child("test.toml"); + test_toml.touch()?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("test.toml"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `test.toml` is not a valid PEP 751 filename: expected TOML file to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`) + " + ); + + Ok(()) +} + #[test] fn invalid_uv_toml_option_disallowed() -> Result<()> { let context = TestContext::new("3.12");