Allow relative paths in requirements.txt (#1027)

This PR attempts to fix a common footgun in `requirements.txt` files.
Previously, to provide a file, you had to use `package_name @
file:///Users/crmarsh/...` -- in other words, an absolute path.

Now, these requirements follow the exact same rules as editables, so you
can do:
```
package_name @ ./file.zip
```

And similar.

The way the parsing is setup, this is intentionally _not_ supported when
reading metadata -- only when parsing `requirements.txt` directly.

Closes #984.
This commit is contained in:
Charlie Marsh 2024-01-22 09:20:30 -05:00 committed by GitHub
parent e09a51653e
commit 145ba0e5ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 356 additions and 167 deletions

View File

@ -22,6 +22,7 @@ use std::collections::HashSet;
use std::fmt::{Display, Formatter};
#[cfg(feature = "pyo3")]
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::str::{Chars, FromStr};
#[cfg(feature = "pyo3")]
@ -44,7 +45,7 @@ use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
#[cfg(feature = "pyo3")]
use puffin_normalize::InvalidNameError;
use puffin_normalize::{ExtraName, PackageName};
pub use verbatim_url::VerbatimUrl;
pub use verbatim_url::{split_scheme, VerbatimUrl};
mod marker;
mod verbatim_url;
@ -396,14 +397,14 @@ impl FromStr for Requirement {
/// Parse a [Dependency Specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/)
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse(&mut Cursor::new(input))
parse(&mut Cursor::new(input), None)
}
}
impl Requirement {
/// Parse a [Dependency Specifier](https://packaging.python.org/en/latest/specifications/dependency-specifiers/)
pub fn parse(input: &mut Cursor) -> Result<Self, Pep508Error> {
parse(input)
pub fn parse(input: &str, working_dir: Option<&Path>) -> Result<Self, Pep508Error> {
parse(&mut Cursor::new(input), working_dir)
}
}
@ -690,7 +691,15 @@ fn parse_extras(cursor: &mut Cursor) -> Result<Option<Vec<ExtraName>>, Pep508Err
Ok(Some(extras))
}
fn parse_url(cursor: &mut Cursor) -> Result<VerbatimUrl, Pep508Error> {
/// Parse a raw string for a URL requirement, which could be either a URL or a local path, and which
/// could contain unexpanded environment variables.
///
/// For example:
/// - `https://pypi.org/project/requests/...`
/// - `file:///home/ferris/project/scripts/...`
/// - `file:../editable/`
/// - `../editable/`
fn parse_url(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result<VerbatimUrl, Pep508Error> {
// wsp*
cursor.eat_whitespace();
// <URI_reference>
@ -704,12 +713,64 @@ fn parse_url(cursor: &mut Cursor) -> Result<VerbatimUrl, Pep508Error> {
input: cursor.to_string(),
});
}
let url = VerbatimUrl::from_str(url).map_err(|err| Pep508Error {
message: Pep508ErrorSource::UrlError(err),
start,
len,
input: cursor.to_string(),
})?;
// Create a `VerbatimUrl` to represent the requirement.
let url = if let Some((scheme, path)) = split_scheme(url) {
if scheme == "file" {
if let Some(path) = path.strip_prefix("//") {
// Ex) `file:///home/ferris/project/scripts/...`
if let Some(working_dir) = working_dir {
VerbatimUrl::from_path(path, working_dir).with_given(url.to_string())
} else {
VerbatimUrl::from_absolute_path(path)
.map_err(|err| Pep508Error {
message: Pep508ErrorSource::UrlError(err),
start,
len,
input: cursor.to_string(),
})?
.with_given(url.to_string())
}
} else {
// Ex) `file:../editable/`
if let Some(working_dir) = working_dir {
VerbatimUrl::from_path(path, working_dir).with_given(url.to_string())
} else {
VerbatimUrl::from_absolute_path(path)
.map_err(|err| Pep508Error {
message: Pep508ErrorSource::UrlError(err),
start,
len,
input: cursor.to_string(),
})?
.with_given(url.to_string())
}
}
} else {
// Ex) `https://...`
VerbatimUrl::from_str(url).map_err(|err| Pep508Error {
message: Pep508ErrorSource::UrlError(err),
start,
len,
input: cursor.to_string(),
})?
}
} else {
// Ex) `../editable/`
if let Some(working_dir) = working_dir {
VerbatimUrl::from_path(url, working_dir).with_given(url.to_string())
} else {
VerbatimUrl::from_absolute_path(url)
.map_err(|err| Pep508Error {
message: Pep508ErrorSource::UrlError(err),
start,
len,
input: cursor.to_string(),
})?
.with_given(url.to_string())
}
};
Ok(url)
}
@ -805,7 +866,7 @@ 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> {
fn parse(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result<Requirement, Pep508Error> {
let start = cursor.pos();
// Technically, the grammar is:
@ -835,7 +896,7 @@ fn parse(cursor: &mut Cursor) -> Result<Requirement, Pep508Error> {
let requirement_kind = match cursor.peek_char() {
Some('@') => {
cursor.next();
Some(VersionOrUrl::Url(parse_url(cursor)?))
Some(VersionOrUrl::Url(parse_url(cursor, working_dir)?))
}
Some('(') => parse_version_specifier_parentheses(cursor)?,
Some('<' | '=' | '>' | '~' | '!') => parse_version_specifier(cursor)?,
@ -845,7 +906,7 @@ fn parse(cursor: &mut Cursor) -> Result<Requirement, Pep508Error> {
// 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 parse_url(&mut clone).is_ok() {
return if parse_url(&mut clone, working_dir).is_ok() {
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 @ https://...`).".to_string()),
start,

View File

@ -29,16 +29,13 @@ pub struct VerbatimUrl {
impl VerbatimUrl {
/// Parse a URL from a string, expanding any environment variables.
pub fn parse(given: String) -> Result<Self, VerbatimUrlError> {
let url = Url::parse(&expand_env_vars(&given, true))
.map_err(|err| VerbatimUrlError::Url(given.clone(), err))?;
Ok(Self {
given: Some(given),
url,
})
pub fn parse(given: impl AsRef<str>) -> Result<Self, VerbatimUrlError> {
let url = Url::parse(&expand_env_vars(given.as_ref(), true))
.map_err(|err| VerbatimUrlError::Url(given.as_ref().to_owned(), err))?;
Ok(Self { url, given: None })
}
/// Parse a URL from a path.
/// Parse a URL from am absolute or relative path.
pub fn from_path(path: impl AsRef<str>, working_dir: impl AsRef<Path>) -> Self {
// Expand any environment variables.
let path = PathBuf::from(expand_env_vars(path.as_ref(), false).as_ref());
@ -59,6 +56,27 @@ impl VerbatimUrl {
Self { url, given: None }
}
/// Parse a URL from an absolute path.
pub fn from_absolute_path(path: impl AsRef<str>) -> Result<Self, VerbatimUrlError> {
// Expand any environment variables.
let path = PathBuf::from(expand_env_vars(path.as_ref(), false).as_ref());
// Convert the path to an absolute path, if necessary.
let path = if path.is_absolute() {
path
} else {
return Err(VerbatimUrlError::RelativePath(path));
};
// Normalize the path.
let path = normalize_path(&path);
// Convert to a URL.
let url = Url::from_file_path(path).expect("path is absolute");
Ok(Self { url, given: None })
}
/// Set the verbatim representation of the URL.
#[must_use]
pub fn with_given(self, given: String) -> Self {
@ -96,7 +114,7 @@ impl std::str::FromStr for VerbatimUrl {
type Err = VerbatimUrlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.to_owned())
Self::parse(s).map(|url| url.with_given(s.to_owned()))
}
}
@ -120,6 +138,10 @@ pub enum VerbatimUrlError {
/// Failed to parse a URL.
#[error("{0}")]
Url(String, #[source] url::ParseError),
/// Received a relative path, but no working directory was provided.
#[error("relative path without a working directory: {0}")]
RelativePath(PathBuf),
}
/// Expand all available environment variables.
@ -193,3 +215,60 @@ fn normalize_path(path: &Path) -> PathBuf {
}
ret
}
/// Like [`Url::parse`], but only splits the scheme. Derived from the `url` crate.
pub fn split_scheme(s: &str) -> Option<(&str, &str)> {
/// <https://url.spec.whatwg.org/#c0-controls-and-space>
#[inline]
fn c0_control_or_space(ch: char) -> bool {
ch <= ' ' // U+0000 to U+0020
}
/// <https://url.spec.whatwg.org/#ascii-alpha>
#[inline]
fn ascii_alpha(ch: char) -> bool {
ch.is_ascii_alphabetic()
}
// Trim control characters and spaces from the start and end.
let s = s.trim_matches(c0_control_or_space);
if s.is_empty() || !s.starts_with(ascii_alpha) {
return None;
}
// Find the `:` following any alpha characters.
let mut iter = s.char_indices();
let end = loop {
match iter.next() {
Some((_i, 'a'..='z' | 'A'..='Z' | '0'..='9' | '+' | '-' | '.')) => {}
Some((i, ':')) => break i,
_ => return None,
}
};
let scheme = &s[..end];
let rest = &s[end + 1..];
Some((scheme, rest))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scheme() {
assert_eq!(
split_scheme("file:///home/ferris/project/scripts"),
Some(("file", "///home/ferris/project/scripts"))
);
assert_eq!(
split_scheme("file:home/ferris/project/scripts"),
Some(("file", "home/ferris/project/scripts"))
);
assert_eq!(
split_scheme("https://example.com"),
Some(("https", "//example.com"))
);
assert_eq!(split_scheme("https:"), Some(("https", "")));
}
}

View File

@ -88,7 +88,7 @@ impl RequirementsSpecification {
}
}
RequirementsSource::Editable(name) => {
let requirement = EditableRequirement::from_str(name)
let requirement = EditableRequirement::parse(name, std::env::current_dir()?)
.with_context(|| format!("Failed to parse `{name}`"))?;
Self {
project: None,

View File

@ -2110,6 +2110,135 @@ fn compile_wheel_path_dependency() -> Result<()> {
"###);
});
// Run the same operation, but this time with a relative path.
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flask @ file:flask-3.0.0-py3-none-any.whl")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip")
.arg("compile")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v[VERSION] via the following command:
# puffin pip compile requirements.in --cache-dir [CACHE_DIR]
blinker==1.7.0
# via flask
click==8.1.7
# via flask
flask @ file:flask-3.0.0-py3-none-any.whl
itsdangerous==2.1.2
# via flask
jinja2==3.1.2
# via flask
markupsafe==2.1.3
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###);
});
// Run the same operation, but this time with a relative path.
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flask @ file://flask-3.0.0-py3-none-any.whl")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip")
.arg("compile")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v[VERSION] via the following command:
# puffin pip compile requirements.in --cache-dir [CACHE_DIR]
blinker==1.7.0
# via flask
click==8.1.7
# via flask
flask @ file://flask-3.0.0-py3-none-any.whl
itsdangerous==2.1.2
# via flask
jinja2==3.1.2
# via flask
markupsafe==2.1.3
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###);
});
// Run the same operation, but this time with a relative path.
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flask @ ./flask-3.0.0-py3-none-any.whl")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip")
.arg("compile")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v[VERSION] via the following command:
# puffin pip compile requirements.in --cache-dir [CACHE_DIR]
blinker==1.7.0
# via flask
click==8.1.7
# via flask
flask @ ./flask-3.0.0-py3-none-any.whl
itsdangerous==2.1.2
# via flask
jinja2==3.1.2
# via flask
markupsafe==2.1.3
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###);
});
Ok(())
}

View File

@ -38,16 +38,14 @@ use std::fmt::{Display, Formatter};
use std::io;
use std::io::Error;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use fs_err as fs;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use tracing::warn;
use unscanny::{Pattern, Scanner};
use url::Url;
use pep508_rs::{Pep508Error, Pep508ErrorSource, Requirement, VerbatimUrl};
use pep508_rs::{split_scheme, Pep508Error, Pep508ErrorSource, Requirement, VerbatimUrl};
/// We emit one of those for each requirements.txt entry
enum RequirementsTxtStatement {
@ -66,7 +64,7 @@ enum RequirementsTxtStatement {
/// PEP 508 requirement plus metadata
RequirementEntry(RequirementEntry),
/// `-e`
EditableRequirement(ParsedEditableRequirement),
EditableRequirement(EditableRequirement),
}
#[derive(Debug, Clone, Eq, PartialEq)]
@ -85,80 +83,29 @@ impl EditableRequirement {
}
}
impl FromStr for EditableRequirement {
type Err = RequirementsTxtParserError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static CWD: Lazy<PathBuf> = Lazy::new(|| std::env::current_dir().unwrap());
let editable_requirement = ParsedEditableRequirement::from(s.to_string());
editable_requirement.with_working_dir(&*CWD)
}
}
/// A raw string for an editable requirement (`pip install -e <editable>`), which could be a URL or
/// a local path, and could contain unexpanded environment variables.
///
/// For example:
/// - `file:///home/ferris/project/scripts/...`
/// - `file:../editable/`
/// - `../editable/`
///
/// We disallow URLs with schemes other than `file://` (e.g., `https://...`).
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ParsedEditableRequirement(String);
/// Like [`Url::parse`], but only splits the scheme. Derived from the `url` crate.
fn split_scheme(s: &str) -> Option<(&str, &str)> {
/// <https://url.spec.whatwg.org/#c0-controls-and-space>
#[inline]
fn c0_control_or_space(ch: char) -> bool {
ch <= ' ' // U+0000 to U+0020
}
/// <https://url.spec.whatwg.org/#ascii-alpha>
#[inline]
fn ascii_alpha(ch: char) -> bool {
ch.is_ascii_alphabetic()
}
// Trim control characters and spaces from the start and end.
let s = s.trim_matches(c0_control_or_space);
if s.is_empty() || !s.starts_with(ascii_alpha) {
return None;
}
// Find the `:` following any alpha characters.
let mut iter = s.char_indices();
let end = loop {
match iter.next() {
Some((_i, 'a'..='z' | 'A'..='Z' | '0'..='9' | '+' | '-' | '.')) => {}
Some((i, ':')) => break i,
_ => return None,
}
};
let scheme = &s[..end];
let rest = &s[end + 1..];
Some((scheme, rest))
}
impl ParsedEditableRequirement {
pub fn with_working_dir(
self,
impl EditableRequirement {
/// Parse a raw string for an editable requirement (`pip install -e <editable>`), which could be
/// a URL or a local path, and could contain unexpanded environment variables.
///
/// For example:
/// - `file:///home/ferris/project/scripts/...`
/// - `file:../editable/`
/// - `../editable/`
///
/// We disallow URLs with schemes other than `file://` (e.g., `https://...`).
pub fn parse(
given: &str,
working_dir: impl AsRef<Path>,
) -> Result<EditableRequirement, RequirementsTxtParserError> {
let given = self.0;
// Create a `VerbatimUrl` to represent the editable requirement.
let url = if let Some((scheme, path)) = split_scheme(&given) {
let url = if let Some((scheme, path)) = split_scheme(given) {
if scheme == "file" {
if let Some(path) = path.strip_prefix("//") {
// Ex) `file:///home/ferris/project/scripts/...`
VerbatimUrl::from_path(path, working_dir)
VerbatimUrl::from_path(path, working_dir.as_ref())
} else {
// Ex) `file:../editable/`
VerbatimUrl::from_path(path, working_dir)
VerbatimUrl::from_path(path, working_dir.as_ref())
}
} else {
// Ex) `https://...`
@ -168,33 +115,21 @@ impl ParsedEditableRequirement {
}
} else {
// Ex) `../editable/`
VerbatimUrl::from_path(&given, working_dir)
VerbatimUrl::from_path(given, working_dir.as_ref())
};
// Create a `PathBuf`.
let path = url
.to_file_path()
.map_err(|()| RequirementsTxtParserError::InvalidEditablePath(given.clone()))?;
.map_err(|()| RequirementsTxtParserError::InvalidEditablePath(given.to_string()))?;
// Add the verbatim representation of the URL to the `VerbatimUrl`.
let url = url.with_given(given);
let url = url.with_given(given.to_string());
Ok(EditableRequirement { url, path })
}
}
impl From<String> for ParsedEditableRequirement {
fn from(s: String) -> Self {
Self(s)
}
}
impl Display for ParsedEditableRequirement {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl Display for EditableRequirement {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.url, f)
@ -248,11 +183,12 @@ impl RequirementsTxt {
file: requirements_txt.as_ref().to_path_buf(),
error: RequirementsTxtParserError::IO(err),
})?;
let data =
Self::parse_inner(&content, working_dir).map_err(|err| RequirementsTxtFileError {
let data = Self::parse_inner(&content, working_dir.as_ref()).map_err(|err| {
RequirementsTxtFileError {
file: requirements_txt.as_ref().to_path_buf(),
error: err,
})?;
}
})?;
if data == Self::default() {
warn!(
"Requirements file {} does not contain any dependencies",
@ -268,27 +204,26 @@ impl RequirementsTxt {
/// of the file
pub fn parse_inner(
content: &str,
working_dir: impl AsRef<Path>,
working_dir: &Path,
) -> Result<Self, RequirementsTxtParserError> {
let mut s = Scanner::new(content);
let mut data = Self::default();
while let Some(statement) = parse_entry(&mut s, content)? {
while let Some(statement) = parse_entry(&mut s, content, working_dir)? {
match statement {
RequirementsTxtStatement::Requirements {
filename,
start,
end,
} => {
let sub_file = working_dir.as_ref().join(filename);
let sub_requirements =
Self::parse(&sub_file, working_dir.as_ref()).map_err(|err| {
RequirementsTxtParserError::Subfile {
source: Box::new(err),
start,
end,
}
})?;
let sub_file = working_dir.join(filename);
let sub_requirements = Self::parse(&sub_file, working_dir).map_err(|err| {
RequirementsTxtParserError::Subfile {
source: Box::new(err),
start,
end,
}
})?;
// Add each to the correct category
data.update_from(sub_requirements);
}
@ -297,15 +232,14 @@ impl RequirementsTxt {
start,
end,
} => {
let sub_file = working_dir.as_ref().join(filename);
let sub_constraints =
Self::parse(&sub_file, working_dir.as_ref()).map_err(|err| {
RequirementsTxtParserError::Subfile {
source: Box::new(err),
start,
end,
}
})?;
let sub_file = working_dir.join(filename);
let sub_constraints = Self::parse(&sub_file, working_dir).map_err(|err| {
RequirementsTxtParserError::Subfile {
source: Box::new(err),
start,
end,
}
})?;
// Treat any nested requirements or constraints as constraints. This differs
// from `pip`, which seems to treat `-r` requirements in constraints files as
// _requirements_, but we don't want to support that.
@ -321,8 +255,7 @@ impl RequirementsTxt {
data.requirements.push(requirement_entry);
}
RequirementsTxtStatement::EditableRequirement(editable) => {
data.editables
.push(editable.with_working_dir(&working_dir)?);
data.editables.push(editable);
}
}
}
@ -343,6 +276,7 @@ impl RequirementsTxt {
fn parse_entry(
s: &mut Scanner,
content: &str,
working_dir: &Path,
) -> Result<Option<RequirementsTxtStatement>, RequirementsTxtParserError> {
// Eat all preceding whitespace, this may run us to the end of file
eat_wrappable_whitespace(s);
@ -373,10 +307,10 @@ fn parse_entry(
}
} else if s.eat_if("-e") {
let path_or_url = parse_value(s, |c: char| !['\n', '\r'].contains(&c))?;
let editable_requirement = ParsedEditableRequirement::from(path_or_url.to_string());
let editable_requirement = EditableRequirement::parse(path_or_url, working_dir)?;
RequirementsTxtStatement::EditableRequirement(editable_requirement)
} else if s.at(char::is_ascii_alphanumeric) {
let (requirement, hashes) = parse_requirement_and_hashes(s, content)?;
let (requirement, hashes) = parse_requirement_and_hashes(s, content, working_dir)?;
RequirementsTxtStatement::RequirementEntry(RequirementEntry {
requirement,
hashes,
@ -435,6 +369,7 @@ fn eat_trailing_line(s: &mut Scanner) -> Result<(), RequirementsTxtParserError>
fn parse_requirement_and_hashes(
s: &mut Scanner,
content: &str,
working_dir: &Path,
) -> Result<(Requirement, Vec<String>), RequirementsTxtParserError> {
// PEP 508 requirement
let start = s.cursor();
@ -469,22 +404,24 @@ fn parse_requirement_and_hashes(
}
};
let requirement =
Requirement::from_str(&content[start..end]).map_err(|err| match err.message {
Pep508ErrorSource::String(_) => RequirementsTxtParserError::Pep508 {
source: err,
start,
end,
},
Pep508ErrorSource::UrlError(_) => RequirementsTxtParserError::Pep508 {
source: err,
start,
end,
},
Pep508ErrorSource::UnsupportedRequirement(_) => {
RequirementsTxtParserError::UnsupportedRequirement {
Requirement::parse(&content[start..end], Some(working_dir)).map_err(|err| {
match err.message {
Pep508ErrorSource::String(_) => RequirementsTxtParserError::Pep508 {
source: err,
start,
end,
},
Pep508ErrorSource::UrlError(_) => RequirementsTxtParserError::Pep508 {
source: err,
start,
end,
},
Pep508ErrorSource::UnsupportedRequirement(_) => {
RequirementsTxtParserError::UnsupportedRequirement {
source: err,
start,
end,
}
}
}
})?;
@ -690,7 +627,7 @@ mod test {
use tempfile::tempdir;
use test_case::test_case;
use crate::{split_scheme, RequirementsTxt};
use crate::RequirementsTxt;
#[test_case(Path::new("basic.txt"))]
#[test_case(Path::new("constraints-a.txt"))]
@ -814,21 +751,4 @@ mod test {
];
assert_eq!(errors, expected);
}
#[test]
fn scheme() {
assert_eq!(
split_scheme("file:///home/ferris/project/scripts"),
Some(("file", "///home/ferris/project/scripts"))
);
assert_eq!(
split_scheme("file:home/ferris/project/scripts"),
Some(("file", "home/ferris/project/scripts"))
);
assert_eq!(
split_scheme("https://example.com"),
Some(("https", "//example.com"))
);
assert_eq!(split_scheme("https:"), Some(("https", "")));
}
}