diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index c4c80a49f..03f136410 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -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 { - 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 { - parse(input) + pub fn parse(input: &str, working_dir: Option<&Path>) -> Result { + parse(&mut Cursor::new(input), working_dir) } } @@ -690,7 +691,15 @@ fn parse_extras(cursor: &mut Cursor) -> Result>, Pep508Err Ok(Some(extras)) } -fn parse_url(cursor: &mut Cursor) -> Result { +/// 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 { // wsp* cursor.eat_whitespace(); // @@ -704,12 +713,64 @@ fn parse_url(cursor: &mut Cursor) -> Result { 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 { +fn parse(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result { let start = cursor.pos(); // Technically, the grammar is: @@ -835,7 +896,7 @@ fn parse(cursor: &mut Cursor) -> Result { 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 { // 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, diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index 9c5474fb5..74494b134 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -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 { - 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) -> Result { + 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, working_dir: impl AsRef) -> 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) -> Result { + // 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::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)> { + /// + #[inline] + fn c0_control_or_space(ch: char) -> bool { + ch <= ' ' // U+0000 to U+0020 + } + + /// + #[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", ""))); + } +} diff --git a/crates/puffin/src/requirements.rs b/crates/puffin/src/requirements.rs index 59482ddc9..131ce8c98 100644 --- a/crates/puffin/src/requirements.rs +++ b/crates/puffin/src/requirements.rs @@ -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, diff --git a/crates/puffin/tests/pip_compile.rs b/crates/puffin/tests/pip_compile.rs index 2e3dca6a1..89b856680 100644 --- a/crates/puffin/tests/pip_compile.rs +++ b/crates/puffin/tests/pip_compile.rs @@ -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(()) } diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 21aa77385..6afbd1b33 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -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 { - static CWD: Lazy = 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 `), 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)> { - /// - #[inline] - fn c0_control_or_space(ch: char) -> bool { - ch <= ' ' // U+0000 to U+0020 - } - - /// - #[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 `), 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, ) -> Result { - 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 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, + working_dir: &Path, ) -> Result { 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, 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), 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", ""))); - } }