mirror of https://github.com/astral-sh/uv
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:
parent
3ace372158
commit
11d00d21f7
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue