mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-22 16:22:09 +00:00
Add dedicated error message for direct filesystem paths in requirements (#2369)
## Summary This is analogous to #669, but for cases in which the package name is a filesystem path. In such cases, we'll fail when parsing the _package name_, since it doesn't start with a valid character, as opposed to failing when we go to parse the remaining version specifier. Inspired by https://github.com/astral-sh/uv/issues/2356.
This commit is contained in:
parent
6d67c93e0b
commit
ebca3197dd
1 changed files with 61 additions and 6 deletions
|
@ -564,19 +564,33 @@ impl Display for Cursor<'_> {
|
|||
fn parse_name(cursor: &mut Cursor) -> Result<PackageName, Pep508Error> {
|
||||
// https://peps.python.org/pep-0508/#names
|
||||
// ^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$ with re.IGNORECASE
|
||||
let start = cursor.pos();
|
||||
let mut name = String::new();
|
||||
|
||||
if let Some((index, char)) = cursor.next() {
|
||||
if matches!(char, 'A'..='Z' | 'a'..='z' | '0'..='9') {
|
||||
name.push(char);
|
||||
} else {
|
||||
return Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
// Check if the user added a filesystem path 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 looks_like_file_path(&mut clone) {
|
||||
Err(Pep508Error {
|
||||
message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`).".to_string()),
|
||||
start,
|
||||
len: clone.pos() - start,
|
||||
input: clone.to_string(),
|
||||
})
|
||||
} else {
|
||||
Err(Pep508Error {
|
||||
message: Pep508ErrorSource::String(format!(
|
||||
"Expected package name starting with an alphanumeric character, found '{char}'"
|
||||
)),
|
||||
start: index,
|
||||
len: char.len_utf8(),
|
||||
input: cursor.to_string(),
|
||||
});
|
||||
start: index,
|
||||
len: char.len_utf8(),
|
||||
input: cursor.to_string(),
|
||||
})
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return Err(Pep508Error {
|
||||
|
@ -752,6 +766,35 @@ fn parse_url(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result<Verbatim
|
|||
Ok(url)
|
||||
}
|
||||
|
||||
/// Parse a filesystem path from the [`Cursor`], advancing the [`Cursor`] to the end of the path.
|
||||
///
|
||||
/// Returns `false` if the path is not a clear and unambiguous filesystem path.
|
||||
fn looks_like_file_path(cursor: &mut Cursor) -> bool {
|
||||
let Some((_, first_char)) = cursor.next() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Ex) `/bin/ls`
|
||||
if first_char == '\\' || first_char == '/' || first_char == '.' {
|
||||
// Read until the end of the path.
|
||||
cursor.take_while(|char| !char.is_whitespace());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ex) `C:`
|
||||
if first_char.is_alphabetic() {
|
||||
if let Some((_, second_char)) = cursor.next() {
|
||||
if second_char == ':' {
|
||||
// Read until the end of the path.
|
||||
cursor.take_while(|char| !char.is_whitespace());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Create a `VerbatimUrl` to represent the requirement.
|
||||
fn preprocess_url(
|
||||
url: &str,
|
||||
|
@ -1491,6 +1534,18 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_bare_file_path() {
|
||||
assert_snapshot!(
|
||||
parse_err(r"/path/to/flask.tar.gz"),
|
||||
@r###"
|
||||
URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`).
|
||||
/path/to/flask.tar.gz
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_no_comma_between_extras() {
|
||||
assert_snapshot!(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue