This commit is contained in:
Charlie Marsh 2025-10-28 22:04:52 -04:00
parent 902d7694ab
commit 61cd8402b5
4 changed files with 108 additions and 31 deletions

View File

@ -574,11 +574,6 @@ const fn is_terminal(c: char) -> bool {
matches!(c, '\n' | '\r' | '#')
}
/// Returns `true` if the character should terminate an option value (like `--extra-index-url`).
const fn is_option_terminal(c: char) -> bool {
matches!(c, '\n' | '\r' | '#' | ';')
}
/// 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
@ -599,11 +594,12 @@ fn parse_entry(
let start = s.cursor();
Ok(Some(if s.eat_if("-r") || s.eat_if("--requirement") {
let filename = parse_value("--requirement", content, s, |c: char| !is_option_terminal(c))?;
let filename = parse_value("--requirement", content, s, |c: char| !is_terminal(c))?;
let filename = unquote(filename)
.ok()
.flatten()
.unwrap_or_else(|| filename.to_string());
let filename = strip_markers(&filename).to_string();
let end = s.cursor();
RequirementsTxtStatement::Requirements {
filename,
@ -611,11 +607,12 @@ fn parse_entry(
end,
}
} else if s.eat_if("-c") || s.eat_if("--constraint") {
let filename = parse_value("--constraint", content, s, |c: char| !is_option_terminal(c))?;
let filename = parse_value("--constraint", content, s, |c: char| !is_terminal(c))?;
let filename = unquote(filename)
.ok()
.flatten()
.unwrap_or_else(|| filename.to_string());
let filename = strip_markers(&filename).to_string();
let end = s.cursor();
RequirementsTxtStatement::Constraint {
filename,
@ -658,13 +655,14 @@ fn parse_entry(
hashes,
})
} else if s.eat_if("-i") || s.eat_if("--index-url") {
let given = parse_value("--index-url", content, s, |c: char| !is_option_terminal(c))?;
let given = parse_value("--index-url", content, s, |c: char| !is_terminal(c))?;
let given = unquote(given)
.ok()
.flatten()
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(given));
let expanded = expand_env_vars(given.as_ref());
let stripped = strip_markers(given.as_ref());
let expanded = expand_env_vars(stripped);
let url = if let Some(path) = std::path::absolute(expanded.as_ref())
.ok()
.filter(|path| path.exists())
@ -687,17 +685,16 @@ fn parse_entry(
}
})?
};
// Consume any trailing content (like markers) until end of line.
s.eat_until(['\r', '\n']);
RequirementsTxtStatement::IndexUrl(url.with_given(given))
} else if s.eat_if("--extra-index-url") {
let given = parse_value("--extra-index-url", content, s, |c: char| !is_option_terminal(c))?;
let given = parse_value("--extra-index-url", content, s, |c: char| !is_terminal(c))?;
let given = unquote(given)
.ok()
.flatten()
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(given));
let expanded = expand_env_vars(given.as_ref());
let stripped = strip_markers(given.as_ref());
let expanded = expand_env_vars(stripped);
let url = if let Some(path) = std::path::absolute(expanded.as_ref())
.ok()
.filter(|path| path.exists())
@ -720,19 +717,18 @@ fn parse_entry(
}
})?
};
// Consume any trailing content (like markers) until end of line.
s.eat_until(['\r', '\n']);
RequirementsTxtStatement::ExtraIndexUrl(url.with_given(given))
} else if s.eat_if("--no-index") {
RequirementsTxtStatement::NoIndex
} else if s.eat_if("--find-links") || s.eat_if("-f") {
let given = parse_value("--find-links", content, s, |c: char| !is_option_terminal(c))?;
let given = parse_value("--find-links", content, s, |c: char| !is_terminal(c))?;
let given = unquote(given)
.ok()
.flatten()
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(given));
let expanded = expand_env_vars(given.as_ref());
let given = strip_markers(given.as_ref());
let expanded = expand_env_vars(given);
let url = if let Some(path) = std::path::absolute(expanded.as_ref())
.ok()
.filter(|path| path.exists())
@ -755,44 +751,40 @@ fn parse_entry(
}
})?
};
// Consume any trailing content (like markers) until end of line.
s.eat_until(['\r', '\n']);
RequirementsTxtStatement::FindLinks(url.with_given(given))
} else if s.eat_if("--no-binary") {
let given = parse_value("--no-binary", content, s, |c: char| !is_option_terminal(c))?;
let given = parse_value("--no-binary", content, s, |c: char| !is_terminal(c))?;
let given = unquote(given)
.ok()
.flatten()
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(given));
let specifier = PackageNameSpecifier::from_str(given.as_ref()).map_err(|err| {
let stripped = strip_markers(given.as_ref());
let specifier = PackageNameSpecifier::from_str(stripped).map_err(|err| {
RequirementsTxtParserError::NoBinary {
source: err,
specifier: given.to_string(),
specifier: stripped.to_string(),
start,
end: s.cursor(),
}
})?;
// Consume any trailing content (like markers) until end of line.
s.eat_until(['\r', '\n']);
RequirementsTxtStatement::NoBinary(NoBinary::from_pip_arg(specifier))
} else if s.eat_if("--only-binary") {
let given = parse_value("--only-binary", content, s, |c: char| !is_option_terminal(c))?;
let given = parse_value("--only-binary", content, s, |c: char| !is_terminal(c))?;
let given = unquote(given)
.ok()
.flatten()
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(given));
let specifier = PackageNameSpecifier::from_str(given.as_ref()).map_err(|err| {
let stripped = strip_markers(given.as_ref());
let specifier = PackageNameSpecifier::from_str(stripped).map_err(|err| {
RequirementsTxtParserError::NoBinary {
source: err,
specifier: given.to_string(),
specifier: stripped.to_string(),
start,
end: s.cursor(),
}
})?;
// Consume any trailing content (like markers) until end of line.
s.eat_until(['\r', '\n']);
RequirementsTxtStatement::OnlyBinary(NoBuild::from_pip_arg(specifier))
} else if s.at(char::is_ascii_alphanumeric) || s.at(|char| matches!(char, '.' | '/' | '$')) {
let source = if requirements_txt == Path::new("-") {
@ -828,6 +820,45 @@ fn parse_entry(
}))
}
/// Remove any trailing markers from an option value.
///
/// For example, given `--index-url https://pypi,org/simple ; python_version < "3.8"`,
/// remove the trailing marker and return `https://pypi,org/simple`.
fn strip_markers(content: &str) -> &str {
let mut escaped = false;
for (i, c) in content.char_indices() {
if escaped {
escaped = false;
continue;
}
// Respect escape characters.
if c == '\\' {
escaped = true;
continue;
}
// If we see a semicolon, check if it's followed by whitespace (marker syntax).
// If so, stop parsing and omit the semicolon.
if c == ';' {
let rest = &content[i + 1..];
if rest.chars().next().is_some_and(char::is_whitespace) {
return &content[..i];
}
}
// If we see whitespace followed by a semicolon, stop parsing at the whitespace.
if c.is_whitespace() {
let rest = &content[i..];
if rest.trim_start().starts_with(';') {
return &content[..i];
}
}
}
content
}
/// Eat whitespace and ignore newlines escaped with a backslash
fn eat_wrappable_whitespace<'a>(s: &mut Scanner<'a>) -> &'a str {
let start = s.cursor();

View File

@ -44,7 +44,47 @@ RequirementsTxt {
fragment: None,
},
given: Some(
"https://download.pytorch.org/whl/nightly/cpu",
"https://download.pytorch.org/whl/nightly/cpu ; sys_platform == darwin",
),
},
VerbatimUrl {
url: DisplaySafeUrl {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/nightly/cpu",
query: None,
fragment: None,
},
given: Some(
"https://download.pytorch.org/whl/nightly/cpu; sys_platform == darwin",
),
},
VerbatimUrl {
url: DisplaySafeUrl {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/cu129",
query: None,
fragment: None,
},
given: Some(
"https://download.pytorch.org/whl/cu129; sys_platform != 'darwin'",
),
},
],

View File

@ -1,3 +1,4 @@
# Test that command-line options stop at markers (semicolons)
--extra-index-url=https://download.pytorch.org/whl/nightly/cpu ; sys_platform == 'darwin'
--extra-index-url=https://download.pytorch.org/whl/nightly/cpu; sys_platform == 'darwin'
--extra-index-url="https://download.pytorch.org/whl/cu129; sys_platform != 'darwin'"
numpy

5
req.txt Normal file
View File

@ -0,0 +1,5 @@
--extra-index-url="https://download.pytorch.org/whl/cu129; sys_platform != 'darwin'"
torch==2.9.0 ; sys_platform != 'darwin'
torch ; sys_platform == 'darwin'
torchvision
torchaudio