mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-22 08:12:44 +00:00
Warn-and-ignore for unsupported requirements.txt
options (#10420)
## Summary Closes https://github.com/astral-sh/uv/issues/10366.
This commit is contained in:
parent
a0494bb059
commit
14b685d9fb
4 changed files with 135 additions and 15 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5485,6 +5485,7 @@ dependencies = [
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
"uv-pep508",
|
"uv-pep508",
|
||||||
"uv-pypi-types",
|
"uv-pypi-types",
|
||||||
|
"uv-warnings",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -16,13 +16,14 @@ doctest = false
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uv-distribution-types = { workspace = true }
|
|
||||||
uv-pep508 = { workspace = true }
|
|
||||||
uv-pypi-types = { workspace = true }
|
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
|
uv-configuration = { workspace = true }
|
||||||
|
uv-distribution-types = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
uv-normalize = { 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 }
|
fs-err = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
|
|
@ -88,6 +88,8 @@ enum RequirementsTxtStatement {
|
||||||
NoBinary(NoBinary),
|
NoBinary(NoBinary),
|
||||||
/// `--only-binary`
|
/// `--only-binary`
|
||||||
OnlyBinary(NoBuild),
|
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
|
/// A [Requirement] with additional metadata from the `requirements.txt`, currently only hashes but in
|
||||||
|
@ -384,6 +386,28 @@ impl RequirementsTxt {
|
||||||
RequirementsTxtStatement::OnlyBinary(only_binary) => {
|
RequirementsTxtStatement::OnlyBinary(only_binary) => {
|
||||||
data.only_binary.extend(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)
|
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.
|
/// Returns `true` if the character is a newline or a comment character.
|
||||||
const fn is_terminal(c: char) -> bool {
|
const fn is_terminal(c: char) -> bool {
|
||||||
matches!(c, '\n' | '\r' | '#')
|
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
|
/// Consumes all preceding trivia (whitespace and comments). If it returns `None`, we've reached
|
||||||
/// the end of file
|
/// the end of file.
|
||||||
fn parse_entry(
|
fn parse_entry(
|
||||||
s: &mut Scanner,
|
s: &mut Scanner,
|
||||||
content: &str,
|
content: &str,
|
||||||
|
@ -595,14 +674,20 @@ fn parse_entry(
|
||||||
hashes,
|
hashes,
|
||||||
})
|
})
|
||||||
} else if let Some(char) = s.peek() {
|
} else if let Some(char) = s.peek() {
|
||||||
let (line, column) = calculate_row_column(content, s.cursor());
|
// Identify an unsupported option, like `--trusted-host`.
|
||||||
return Err(RequirementsTxtParserError::Parser {
|
if let Some(option) = UnsupportedOption::iter().find(|option| s.eat_if(option.name())) {
|
||||||
message: format!(
|
s.eat_while(|c: char| !is_terminal(c));
|
||||||
"Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement"
|
RequirementsTxtStatement::UnsupportedOption(option)
|
||||||
),
|
} else {
|
||||||
line,
|
let (line, column) = calculate_row_column(content, s.cursor());
|
||||||
column,
|
return Err(RequirementsTxtParserError::Parser {
|
||||||
});
|
message: format!(
|
||||||
|
"Unexpected '{char}', expected '-c', '-e', '-r' or the start of a requirement"
|
||||||
|
),
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// EOF
|
// EOF
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
|
@ -486,6 +486,39 @@ fn install_requirements_txt() -> Result<()> {
|
||||||
Ok(())
|
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
|
/// 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.
|
/// This is likely to occur in the real world when compiled on one platform then installed on another.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue