Warn-and-ignore for unsupported requirements.txt options (#10420)

## Summary

Closes https://github.com/astral-sh/uv/issues/10366.
This commit is contained in:
Charlie Marsh 2025-01-09 13:19:51 -05:00 committed by GitHub
parent a0494bb059
commit 14b685d9fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 135 additions and 15 deletions

1
Cargo.lock generated
View file

@ -5485,6 +5485,7 @@ dependencies = [
"uv-normalize",
"uv-pep508",
"uv-pypi-types",
"uv-warnings",
]
[[package]]

View file

@ -16,13 +16,14 @@ doctest = false
workspace = true
[dependencies]
uv-distribution-types = { workspace = true }
uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true }
uv-client = { workspace = true }
uv-configuration = { workspace = true }
uv-distribution-types = { workspace = true }
uv-fs = { workspace = true }
uv-normalize = { workspace = true }
uv-configuration = { workspace = true }
uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true }
uv-warnings = { workspace = true }
fs-err = { workspace = true }
regex = { workspace = true }

View file

@ -88,6 +88,8 @@ enum RequirementsTxtStatement {
NoBinary(NoBinary),
/// `--only-binary`
OnlyBinary(NoBuild),
/// An unsupported option (e.g., `--trusted-host`).
UnsupportedOption(UnsupportedOption),
}
/// A [Requirement] with additional metadata from the `requirements.txt`, currently only hashes but in
@ -384,6 +386,28 @@ impl RequirementsTxt {
RequirementsTxtStatement::OnlyBinary(only_binary) => {
data.only_binary.extend(only_binary);
}
RequirementsTxtStatement::UnsupportedOption(flag) => {
if requirements_txt == Path::new("-") {
if flag.cli() {
uv_warnings::warn_user!("Ignoring unsupported option from stdin: `{flag}` (hint: pass `{flag}` on the command line instead)", flag = flag.green());
} else {
uv_warnings::warn_user!(
"Ignoring unsupported option from stdin: `{flag}`",
flag = flag.green()
);
}
} else {
if flag.cli() {
uv_warnings::warn_user!("Ignoring unsupported option in `{path}`: `{flag}` (hint: pass `{flag}` on the command line instead)", path = requirements_txt.user_display().cyan(), flag = flag.green());
} else {
uv_warnings::warn_user!(
"Ignoring unsupported option in `{path}`: `{flag}`",
path = requirements_txt.user_display().cyan(),
flag = flag.green()
);
}
}
}
}
}
Ok(data)
@ -416,15 +440,70 @@ impl RequirementsTxt {
}
}
/// An unsupported option (e.g., `--trusted-host`).
///
/// See: <https://pip.pypa.io/en/stable/reference/requirements-file-format/#global-options>
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UnsupportedOption {
PreferBinary,
RequireHashes,
Pre,
TrustedHost,
UseFeature,
}
impl UnsupportedOption {
/// The name of the unsupported option.
fn name(self) -> &'static str {
match self {
UnsupportedOption::PreferBinary => "--prefer-binary",
UnsupportedOption::RequireHashes => "--require-hashes",
UnsupportedOption::Pre => "--pre",
UnsupportedOption::TrustedHost => "--trusted-host",
UnsupportedOption::UseFeature => "--use-feature",
}
}
/// Returns `true` if the option is supported on the CLI.
fn cli(self) -> bool {
match self {
UnsupportedOption::PreferBinary => false,
UnsupportedOption::RequireHashes => true,
UnsupportedOption::Pre => true,
UnsupportedOption::TrustedHost => true,
UnsupportedOption::UseFeature => false,
}
}
/// Returns an iterator over all unsupported options.
fn iter() -> impl Iterator<Item = UnsupportedOption> {
[
UnsupportedOption::PreferBinary,
UnsupportedOption::RequireHashes,
UnsupportedOption::Pre,
UnsupportedOption::TrustedHost,
UnsupportedOption::UseFeature,
]
.iter()
.copied()
}
}
impl Display for UnsupportedOption {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
/// Returns `true` if the character is a newline or a comment character.
const fn is_terminal(c: char) -> bool {
matches!(c, '\n' | '\r' | '#')
}
/// Parse a single entry, that is a requirement, an inclusion or a comment line
/// Parse a single entry, that is a requirement, an inclusion or a comment line.
///
/// Consumes all preceding trivia (whitespace and comments). If it returns None, we've reached
/// the end of file
/// Consumes all preceding trivia (whitespace and comments). If it returns `None`, we've reached
/// the end of file.
fn parse_entry(
s: &mut Scanner,
content: &str,
@ -595,14 +674,20 @@ fn parse_entry(
hashes,
})
} else if let Some(char) = s.peek() {
let (line, column) = calculate_row_column(content, s.cursor());
return Err(RequirementsTxtParserError::Parser {
message: format!(
"Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement"
),
line,
column,
});
// Identify an unsupported option, like `--trusted-host`.
if let Some(option) = UnsupportedOption::iter().find(|option| s.eat_if(option.name())) {
s.eat_while(|c: char| !is_terminal(c));
RequirementsTxtStatement::UnsupportedOption(option)
} else {
let (line, column) = calculate_row_column(content, s.cursor());
return Err(RequirementsTxtParserError::Parser {
message: format!(
"Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement"
),
line,
column,
});
}
} else {
// EOF
return Ok(None);

View file

@ -486,6 +486,39 @@ fn install_requirements_txt() -> Result<()> {
Ok(())
}
/// Warn (but don't fail) when unsupported flags are set in the `requirements.txt`.
#[test]
fn install_unsupported_flag() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc! {r"
--pre
--prefer-binary :all:
iniconfig
"})?;
uv_snapshot!(context.pip_install()
.arg("-r")
.arg("requirements.txt")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Ignoring unsupported option in `requirements.txt`: `--pre` (hint: pass `--pre` on the command line instead)
warning: Ignoring unsupported option in `requirements.txt`: `--prefer-binary`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###
);
Ok(())
}
/// Install a requirements file with pins that conflict
///
/// This is likely to occur in the real world when compiled on one platform then installed on another.