Add explicit error message for URLs without package names (#669)

`pip` supports installing packages without names (e.g.,
`git+https://github.com/pallets/flask.git`), but it doesn't adhere to
the PEP grammar, and we don't yet support it (and may never) (#313).

This PR adds a dedicated error path for such cases, to ensure that we
can give meaningful feedback to the user:

```
error: Couldn't parse requirement in requirements.in position 0 to 18
  Caused by: URL requirement is missing a package name; expected: `package_name @ https://google.com`
https://google.com
^^^^^^^^^^^^^^^^^^
```

Closes https://github.com/astral-sh/puffin/issues/650.
This commit is contained in:
Charlie Marsh 2023-12-16 16:14:34 -05:00 committed by GitHub
parent 71964ec7a8
commit f62458f600
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -415,6 +415,15 @@ impl<'a> Cursor<'a> {
}
}
/// Returns a new cursor starting at the given position.
pub fn at(self, pos: usize) -> Self {
Self {
input: self.input,
chars: self.input[pos..].chars(),
pos,
}
}
/// Returns the current byte position of the cursor.
fn pos(&self) -> usize {
self.pos
@ -778,6 +787,8 @@ fn parse_version_specifier_parentheses(
/// Parse a [dependency specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers)
fn parse(cursor: &mut Cursor) -> Result<Requirement, Pep508Error> {
let start = cursor.pos();
// Technically, the grammar is:
// ```text
// name_req = name wsp* extras? wsp* versionspec? wsp* quoted_marker?
@ -810,20 +821,22 @@ fn parse(cursor: &mut Cursor) -> Result<Requirement, Pep508Error> {
Some('(') => parse_version_specifier_parentheses(cursor)?,
Some('<' | '=' | '>' | '~' | '!') => parse_version_specifier(cursor)?,
Some(';') | None => None,
// Ex) `https://...` or `git+https://...`
Some(':') | Some('+') => {
return Err(Pep508Error {
message: Pep508ErrorSource::String(
"URL requirement is missing a package name; expected: `package_name @`"
.to_string(),
),
start: cursor.pos(),
len: 1,
input: cursor.to_string(),
});
}
Some(other) => {
return Err(Pep508Error {
// Rewind to the start of the version specifier, to see if the user added a URL without
// a package name. pip supports this in `requirements.txt`, but it doesn't adhere to
// the PEP 508 grammar.
let mut clone = cursor.clone().at(start);
return if let Ok(url) = parse_url(&mut clone) {
Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"URL requirement is missing a package name; expected: `package_name @ {url}`",
)),
start,
len: clone.pos() - start,
input: clone.to_string(),
})
} else {
Err(Pep508Error {
message: Pep508ErrorSource::String(format!(
"Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `{other}`"
)),
@ -831,6 +844,7 @@ fn parse(cursor: &mut Cursor) -> Result<Requirement, Pep508Error> {
len: other.len_utf8(),
input: cursor.to_string(),
})
};
}
};
@ -1286,6 +1300,18 @@ mod tests {
);
}
#[test]
fn error_bare_url() {
assert_err(
r#"git+https://github.com/pallets/flask.git"#,
indoc! {"
URL requirement is missing a package name; expected: `package_name @ git+https://github.com/pallets/flask.git`
git+https://github.com/pallets/flask.git
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"
},
);
}
#[test]
fn error_no_comma_between_extras() {
assert_err(