Reject non-PEP 751 TOML files in install commands (#13120)

If you pass a TOML file to `uv pip install` that isn't recognized, we
should just reject it instead of assuming `requirements.txt`. I just
don't see a real case where it's better to let the command proceed.
This commit is contained in:
Charlie Marsh 2025-04-28 16:51:23 -04:00 committed by Zanie Blue
parent 3ace372158
commit 11d00d21f7
3 changed files with 136 additions and 73 deletions

View File

@ -1,3 +1,4 @@
use std::ffi::OsStr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -5,7 +6,6 @@ use console::Term;
use uv_fs::{Simplified, CWD}; use uv_fs::{Simplified, CWD};
use uv_requirements_txt::RequirementsTxtRequirement; use uv_requirements_txt::RequirementsTxtRequirement;
use uv_warnings::warn_user;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RequirementsSource { pub enum RequirementsSource {
@ -32,89 +32,127 @@ pub enum RequirementsSource {
impl RequirementsSource { impl RequirementsSource {
/// Parse a [`RequirementsSource`] from a [`PathBuf`]. The file type is determined by the file /// Parse a [`RequirementsSource`] from a [`PathBuf`]. The file type is determined by the file
/// extension. /// extension.
pub fn from_requirements_file(path: PathBuf) -> Self { pub fn from_requirements_file(path: PathBuf) -> Result<Self> {
if path.ends_with("pyproject.toml") { if path.ends_with("pyproject.toml") {
Self::PyprojectToml(path) Ok(Self::PyprojectToml(path))
} else if path.ends_with("setup.py") { } else if path.ends_with("setup.py") {
Self::SetupPy(path) Ok(Self::SetupPy(path))
} else if path.ends_with("setup.cfg") { } else if path.ends_with("setup.cfg") {
Self::SetupCfg(path) Ok(Self::SetupCfg(path))
} else if path.ends_with("environment.yml") { } else if path.ends_with("environment.yml") {
Self::EnvironmentYml(path) Ok(Self::EnvironmentYml(path))
} else if path } else if path
.file_name() .file_name()
.is_some_and(|file_name| file_name.to_str().is_some_and(is_pylock_toml)) .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 { } else {
Self::RequirementsTxt(path) Ok(Self::RequirementsTxt(path))
} }
} }
/// Parse a [`RequirementsSource`] from a `requirements.txt` file. /// Parse a [`RequirementsSource`] from a `requirements.txt` file.
pub fn from_requirements_txt(path: PathBuf) -> Self { pub fn from_requirements_txt(path: PathBuf) -> Result<Self> {
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(file_name) { if path.ends_with(file_name) {
warn_user!( return Err(anyhow::anyhow!(
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.", "The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format",
path.user_display(), path.user_display(),
file_name file_name
); ));
} }
} }
if let Some(file_name) = path.file_name() { if path
if file_name.to_str().is_some_and(is_pylock_toml) { .file_name()
warn_user!( .and_then(OsStr::to_str)
"The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format.", .is_some_and(is_pylock_toml)
path.user_display(), {
); 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. /// Parse a [`RequirementsSource`] from a `constraints.txt` file.
pub fn from_constraints_txt(path: PathBuf) -> Self { pub fn from_constraints_txt(path: PathBuf) -> Result<Self> {
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(file_name) { if path.ends_with(file_name) {
warn_user!( return Err(anyhow::anyhow!(
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.", "The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format",
path.user_display(), path.user_display(),
file_name file_name
); ));
} }
} }
if let Some(file_name) = path.file_name() { if path
if file_name.to_str().is_some_and(is_pylock_toml) { .file_name()
warn_user!( .and_then(OsStr::to_str)
"The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format.", .is_some_and(is_pylock_toml)
path.user_display(), {
); 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. /// Parse a [`RequirementsSource`] from an `overrides.txt` file.
pub fn from_overrides_txt(path: PathBuf) -> Self { pub fn from_overrides_txt(path: PathBuf) -> Result<Self> {
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] { for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(file_name) { if path.ends_with(file_name) {
warn_user!( return Err(anyhow::anyhow!(
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.", "The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format",
path.user_display(), path.user_display(),
file_name file_name
); ));
} }
} }
if let Some(file_name) = path.file_name() { if path
if file_name.to_str().is_some_and(is_pylock_toml) { .file_name()
warn_user!( .and_then(OsStr::to_str)
"The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format.", .is_some_and(is_pylock_toml)
path.user_display(), {
); 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 /// 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)?; let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation { 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)?; let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation { 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)?; let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation { 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)?; let confirmation = uv_console::confirm(&prompt, &term, true)?;
if confirmation { if confirmation {
return Ok(Self::from_requirements_file(name.into())); return Self::from_requirements_file(name.into());
} }
} }
} }

View File

@ -407,22 +407,22 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.src_file .src_file
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file) .map(RequirementsSource::from_requirements_file)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let constraints = args let constraints = args
.constraints .constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let overrides = args let overrides = args
.overrides .overrides
.into_iter() .into_iter()
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let build_constraints = args let build_constraints = args
.build_constraints .build_constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let mut groups = BTreeMap::new(); let mut groups = BTreeMap::new();
for group in args.settings.groups { for group in args.settings.groups {
@ -516,17 +516,17 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.src_file .src_file
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file) .map(RequirementsSource::from_requirements_file)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let constraints = args let constraints = args
.constraints .constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let build_constraints = args let build_constraints = args
.build_constraints .build_constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
commands::pip_sync( commands::pip_sync(
&requirements, &requirements,
@ -588,23 +588,24 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
requirements.extend( requirements.extend(
args.requirements args.requirements
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file), .map(RequirementsSource::from_requirements_file)
.collect::<Result<Vec<_>, _>>()?,
); );
let constraints = args let constraints = args
.constraints .constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let overrides = args let overrides = args
.overrides .overrides
.into_iter() .into_iter()
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let build_constraints = args let build_constraints = args
.build_constraints .build_constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let mut groups = BTreeMap::new(); let mut groups = BTreeMap::new();
for group in args.settings.groups { for group in args.settings.groups {
@ -735,7 +736,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
sources.extend( sources.extend(
args.requirements args.requirements
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file), .map(RequirementsSource::from_requirements_file)
.collect::<Result<Vec<_>, _>>()?,
); );
commands::pip_uninstall( commands::pip_uninstall(
&sources, &sources,
@ -908,7 +910,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.build_constraints .build_constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
commands::build_frontend( commands::build_frontend(
&project_dir, &project_dir,
@ -1121,7 +1123,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
requirements.extend( requirements.extend(
args.with_requirements args.with_requirements
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file), .map(RequirementsSource::from_requirements_file)
.collect::<Result<Vec<_>, _>>()?,
); );
requirements requirements
}; };
@ -1129,18 +1132,18 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.constraints .constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let overrides = args let overrides = args
.overrides .overrides
.into_iter() .into_iter()
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let build_constraints = args let build_constraints = args
.build_constraints .build_constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
Box::pin(commands::tool_run( Box::pin(commands::tool_run(
args.command, args.command,
@ -1195,24 +1198,25 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
requirements.extend( requirements.extend(
args.with_requirements args.with_requirements
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file), .map(RequirementsSource::from_requirements_file)
.collect::<Result<Vec<_>, _>>()?,
); );
let constraints = args let constraints = args
.constraints .constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let overrides = args let overrides = args
.overrides .overrides
.into_iter() .into_iter()
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
let build_constraints = args let build_constraints = args
.build_constraints .build_constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
Box::pin(commands::tool_install( Box::pin(commands::tool_install(
args.package, args.package,
@ -1639,7 +1643,8 @@ async fn run_project(
requirements.extend( requirements.extend(
args.with_requirements args.with_requirements
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file), .map(RequirementsSource::from_requirements_file)
.collect::<Result<Vec<_>, _>>()?,
); );
Box::pin(commands::run( Box::pin(commands::run(
@ -1803,15 +1808,14 @@ async fn run_project(
.chain( .chain(
args.requirements args.requirements
.into_iter() .into_iter()
.map(RequirementsSource::from_requirements_file) .map(RequirementsSource::from_requirements_file),
.map(Ok),
) )
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
let constraints = args let constraints = args
.constraints .constraints
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Result<Vec<_>, _>>()?;
Box::pin(commands::add( Box::pin(commands::add(
project_dir, project_dir,

View File

@ -245,6 +245,27 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> {
Ok(()) 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] #[test]
fn invalid_uv_toml_option_disallowed() -> Result<()> { fn invalid_uv_toml_option_disallowed() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");