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 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<Self> {
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<Self> {
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<Self> {
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<Self> {
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());
}
}
}

View File

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

View File

@ -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");