Skip to content

Commit

Permalink
Warn-and-ignore for unsupported requirements.txt options
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jan 9, 2025
1 parent 19589e0 commit 9ea0a22
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 15 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions crates/uv-requirements-txt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
107 changes: 96 additions & 11 deletions crates/uv-requirements-txt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = format!("`{flag}`").green());
} else {
uv_warnings::warn_user!(
"Ignoring unsupported option from stdin: {flag}",
flag = format!("`{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 = format!("`{flag}`").green());
} else {
uv_warnings::warn_user!(
"Ignoring unsupported option in `{path}`: {flag}",
path = requirements_txt.user_display().cyan(),
flag = format!("`{flag}`").green()
);
}
}
}
}
}
Ok(data)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 9ea0a22

Please sign in to comment.