diff --git a/Cargo.lock b/Cargo.lock index e0744345a..530957828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2047,6 +2047,7 @@ name = "pep508_rs" version = "0.2.3" dependencies = [ "derivative", + "fs-err", "indoc", "log", "once_cell", diff --git a/crates/distribution-types/src/editable.rs b/crates/distribution-types/src/editable.rs index 3d746219b..3603e00c6 100644 --- a/crates/distribution-types/src/editable.rs +++ b/crates/distribution-types/src/editable.rs @@ -1,45 +1,40 @@ use std::borrow::Cow; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use url::Url; use pep508_rs::VerbatimUrl; -use requirements_txt::EditableRequirement; use crate::Verbatim; #[derive(Debug, Clone)] pub struct LocalEditable { - pub requirement: EditableRequirement, + /// The underlying [`EditableRequirement`] from the `requirements.txt` file. + pub url: VerbatimUrl, /// Either the path to the editable or its checkout. pub path: PathBuf, } impl LocalEditable { - /// Return the [`VerbatimUrl`] of the editable. + /// Return the editable as a [`Url`]. pub fn url(&self) -> &VerbatimUrl { - self.requirement.url() - } - - /// Return the underlying [`Url`] of the editable. - pub fn raw(&self) -> &Url { - self.requirement.raw() + &self.url } /// Return the resolved path to the editable. - pub fn path(&self) -> &Path { - &self.path + pub fn raw(&self) -> &Url { + self.url.raw() } } impl Verbatim for LocalEditable { fn verbatim(&self) -> Cow<'_, str> { - self.url().verbatim() + self.url.verbatim() } } impl std::fmt::Display for LocalEditable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.requirement.fmt(f) + std::fmt::Display::fmt(&self.url, f) } } diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 3cab4778c..2ca6fb4d4 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -43,7 +43,6 @@ use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use pep440_rs::Version; use pep508_rs::VerbatimUrl; use puffin_normalize::PackageName; -use requirements_txt::EditableRequirement; pub use crate::any::*; pub use crate::cached::*; @@ -272,24 +271,13 @@ impl Dist { /// Create a [`Dist`] for a local editable distribution. pub fn from_editable(name: PackageName, editable: LocalEditable) -> Result { - match editable.requirement { - EditableRequirement::Path { url, path } => { - Ok(Self::Source(SourceDist::Path(PathSourceDist { - name, - url, - path, - editable: true, - }))) - } - EditableRequirement::Url { url, path } => { - Ok(Self::Source(SourceDist::Path(PathSourceDist { - name, - path, - url, - editable: true, - }))) - } - } + let LocalEditable { url, path } = editable; + Ok(Self::Source(SourceDist::Path(PathSourceDist { + name, + url, + path, + editable: true, + }))) } /// Returns the [`File`] instance, if this dist is from a registry with simple json api support @@ -353,11 +341,7 @@ impl SourceDist { url: VerbatimUrl::unknown(url), ..dist }), - SourceDist::Path(dist) => SourceDist::Path(PathSourceDist { - url: VerbatimUrl::unknown(url), - ..dist - }), - dist @ SourceDist::Registry(_) => dist, + dist => dist, } } } diff --git a/crates/pep508-rs/Cargo.toml b/crates/pep508-rs/Cargo.toml index 3439d3de7..33bb79dbe 100644 --- a/crates/pep508-rs/Cargo.toml +++ b/crates/pep508-rs/Cargo.toml @@ -21,6 +21,7 @@ pep440_rs = { path = "../pep440-rs" } puffin-normalize = { path = "../puffin-normalize" } derivative = { workspace = true } +fs-err = { workspace = true } once_cell = { workspace = true } pyo3 = { workspace = true, optional = true, features = ["abi3", "extension-module"] } pyo3-log = { workspace = true, optional = true } diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 4ef989ec1..109349be3 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -44,13 +44,13 @@ 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::{VerbatimUrl, VerbatimUrlError}; mod marker; mod verbatim_url; /// Error with a span attached. Not that those aren't `String` but `Vec` indices. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug)] pub struct Pep508Error { /// Either we have an error string from our parser or an upstream error from `url` pub message: Pep508ErrorSource, @@ -63,14 +63,14 @@ pub struct Pep508Error { } /// Either we have an error string from our parser or an upstream error from `url` -#[derive(Debug, Error, Clone, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum Pep508ErrorSource { /// An error from our parser. #[error("{0}")] String(String), /// A URL parsing error. #[error(transparent)] - UrlError(#[from] verbatim_url::Error), + UrlError(#[from] verbatim_url::VerbatimUrlError), } impl Display for Pep508Error { diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index e5610a159..c3d516bd5 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; +use std::fmt::Debug; use std::ops::Deref; -use std::path::Path; +use std::path::{Path, PathBuf}; use once_cell::sync::Lazy; use regex::Regex; @@ -26,8 +27,9 @@ 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))?; + 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, @@ -35,10 +37,30 @@ impl VerbatimUrl { } /// Parse a URL from a path. - #[allow(clippy::result_unit_err)] - pub fn from_path(path: impl AsRef, given: String) -> Result { + pub fn from_path( + path: impl AsRef, + working_dir: impl AsRef, + given: String, + ) -> 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 { + working_dir.as_ref().join(path) + }; + + // Canonicalize the path. + let path = + fs_err::canonicalize(path).map_err(|err| VerbatimUrlError::Path(given.clone(), err))?; + + // Convert to a URL. + let url = Url::from_file_path(path).expect("path is absolute"); + Ok(Self { - url: Url::from_directory_path(path)?, + url, given: Some(given), }) } @@ -68,7 +90,7 @@ impl VerbatimUrl { } impl std::str::FromStr for VerbatimUrl { - type Err = Error; + type Err = VerbatimUrlError; fn from_str(s: &str) -> Result { Self::parse(s.to_owned()) @@ -77,7 +99,7 @@ impl std::str::FromStr for VerbatimUrl { impl std::fmt::Display for VerbatimUrl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.url.fmt(f) + std::fmt::Display::fmt(&self.url, f) } } @@ -89,10 +111,16 @@ impl Deref for VerbatimUrl { } } -#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -pub enum Error { - #[error(transparent)] - Url(#[from] url::ParseError), +/// An error that can occur when parsing a [`VerbatimUrl`]. +#[derive(thiserror::Error, Debug)] +pub enum VerbatimUrlError { + /// Failed to canonicalize a path. + #[error("{0}")] + Path(String, #[source] std::io::Error), + + /// Failed to parse a URL. + #[error("{0}")] + Url(String, #[source] url::ParseError), } /// Expand all available environment variables. @@ -110,12 +138,12 @@ pub enum Error { /// Valid characters in variable names follow the `POSIX standard /// `_ and are limited /// to uppercase letter, digits and the `_` (underscore). -fn expand_env_vars(s: &str) -> Cow<'_, str> { - // Generate a URL-escaped project root, to be used via the `${PROJECT_ROOT}` - // environment variable. Ensure that it's URL-escaped. +fn expand_env_vars(s: &str, escape: bool) -> Cow<'_, str> { + // Generate the project root, to be used via the `${PROJECT_ROOT}` + // environment variable. static PROJECT_ROOT_FRAGMENT: Lazy = Lazy::new(|| { let project_root = std::env::current_dir().unwrap(); - project_root.to_string_lossy().replace(' ', "%20") + project_root.to_string_lossy().to_string() }); static RE: Lazy = @@ -124,7 +152,14 @@ fn expand_env_vars(s: &str) -> Cow<'_, str> { RE.replace_all(s, |caps: ®ex::Captures<'_>| { let name = caps.name("name").unwrap().as_str(); std::env::var(name).unwrap_or_else(|_| match name { - "PROJECT_ROOT" => PROJECT_ROOT_FRAGMENT.clone(), + // Ensure that the variable is URL-escaped, if necessary. + "PROJECT_ROOT" => { + if escape { + PROJECT_ROOT_FRAGMENT.replace(' ', "%20") + } else { + PROJECT_ROOT_FRAGMENT.to_string() + } + } _ => caps["var"].to_owned(), }) }) diff --git a/crates/puffin/src/commands/pip_compile.rs b/crates/puffin/src/commands/pip_compile.rs index 5ef776f1e..721671e68 100644 --- a/crates/puffin/src/commands/pip_compile.rs +++ b/crates/puffin/src/commands/pip_compile.rs @@ -196,15 +196,9 @@ pub(crate) async fn pip_compile( let editables: Vec = editables .into_iter() - .map(|editable| match &editable { - EditableRequirement::Path { path, .. } => Ok(LocalEditable { - path: path.clone(), - requirement: editable, - }), - EditableRequirement::Url { path, .. } => Ok(LocalEditable { - path: path.clone(), - requirement: editable, - }), + .map(|editable| { + let EditableRequirement { path, url } = editable; + Ok(LocalEditable { url, path }) }) .collect::>()?; diff --git a/crates/puffin/src/commands/pip_install.rs b/crates/puffin/src/commands/pip_install.rs index 57d2f365a..60e5a6f6c 100644 --- a/crates/puffin/src/commands/pip_install.rs +++ b/crates/puffin/src/commands/pip_install.rs @@ -315,15 +315,12 @@ async fn build_editables( let editables: Vec = editables .iter() - .map(|editable| match editable { - EditableRequirement::Path { path, .. } => Ok(LocalEditable { + .map(|editable| { + let EditableRequirement { path, url } = editable; + Ok(LocalEditable { path: path.clone(), - requirement: editable.clone(), - }), - EditableRequirement::Url { path, .. } => Ok(LocalEditable { - path: path.clone(), - requirement: editable.clone(), - }), + url: url.clone(), + }) }) .collect::>()?; diff --git a/crates/puffin/src/commands/pip_sync.rs b/crates/puffin/src/commands/pip_sync.rs index 5303b74f3..00fff66bf 100644 --- a/crates/puffin/src/commands/pip_sync.rs +++ b/crates/puffin/src/commands/pip_sync.rs @@ -417,15 +417,12 @@ async fn resolve_editables( let local_editables: Vec = uninstalled .iter() - .map(|editable| match editable { - EditableRequirement::Path { path, .. } => Ok(LocalEditable { + .map(|editable| { + let EditableRequirement { path, url } = editable; + Ok(LocalEditable { path: path.clone(), - requirement: editable.clone(), - }), - EditableRequirement::Url { path, .. } => Ok(LocalEditable { - path: path.clone(), - requirement: editable.clone(), - }), + url: url.clone(), + }) }) .collect::>()?; diff --git a/crates/puffin/tests/pip_compile.rs b/crates/puffin/tests/pip_compile.rs index 8793d8a80..c1f7b09ff 100644 --- a/crates/puffin/tests/pip_compile.rs +++ b/crates/puffin/tests/pip_compile.rs @@ -2634,7 +2634,8 @@ fn compile_editable() -> Result<()> { let requirements_in = temp_dir.child("requirements.in"); requirements_in.write_str(indoc! {r" -e ../../scripts/editable-installs/poetry_editable - -e ../../scripts/editable-installs/maturin_editable + -e ${PROJECT_ROOT}/../../scripts/editable-installs/maturin_editable + -e file://../../scripts/editable-installs/black_editable boltons # normal dependency for comparison " })?; @@ -2661,15 +2662,16 @@ fn compile_editable() -> Result<()> { ----- stdout ----- # This file was autogenerated by Puffin v[VERSION] via the following command: # puffin pip compile requirements.in --cache-dir [CACHE_DIR] + -e file://../../scripts/editable-installs/black_editable boltons==23.1.1 - -e ../../scripts/editable-installs/maturin_editable + -e ${PROJECT_ROOT}/../../scripts/editable-installs/maturin_editable numpy==1.26.2 # via poetry-editable -e ../../scripts/editable-installs/poetry_editable ----- stderr ----- - Built 2 editables in [TIME] - Resolved 4 packages in [TIME] + Built 3 editables in [TIME] + Resolved 5 packages in [TIME] "###); }); @@ -3491,7 +3493,8 @@ fn missing_editable_requirement() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: failed to canonicalize path `[WORKSPACE_DIR]/../tmp/django-3.2.8.tar.gz` + error: Invalid editable path in requirements.in: ../tmp/django-3.2.8.tar.gz + Caused by: failed to canonicalize path `[WORKSPACE_DIR]/../tmp/django-3.2.8.tar.gz` Caused by: No such file or directory (os error 2) "###); }); diff --git a/crates/puffin/tests/pip_install.rs b/crates/puffin/tests/pip_install.rs index 1ef1dac96..23430fb27 100644 --- a/crates/puffin/tests/pip_install.rs +++ b/crates/puffin/tests/pip_install.rs @@ -490,7 +490,7 @@ fn install_editable() -> Result<()> { Downloaded 1 package in [TIME] Installed 2 packages in [TIME] + numpy==1.26.2 - + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); @@ -551,8 +551,8 @@ fn install_editable() -> Result<()> { + packaging==23.2 + pathspec==0.11.2 + platformdirs==4.0.0 - - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) - + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) + + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); @@ -629,7 +629,7 @@ fn install_editable_and_registry() -> Result<()> { Resolved 1 package in [TIME] Installed 1 package in [TIME] - black==23.11.0 - + black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) + + black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable) "###); }); @@ -681,7 +681,7 @@ fn install_editable_and_registry() -> Result<()> { Resolved 6 packages in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - - black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) + - black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable) + black==23.10.0 "###); }); diff --git a/crates/puffin/tests/pip_sync.rs b/crates/puffin/tests/pip_sync.rs index 47a2c7761..06ea3f9ed 100644 --- a/crates/puffin/tests/pip_sync.rs +++ b/crates/puffin/tests/pip_sync.rs @@ -2531,7 +2531,7 @@ fn sync_editable() -> Result<()> { Downloaded 2 packages in [TIME] Installed 4 packages in [TIME] + boltons==23.1.1 - + maturin-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/maturin_editable/) + + maturin-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/maturin_editable) + numpy==1.26.2 + poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); @@ -2720,7 +2720,7 @@ fn sync_editable_and_registry() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - black==24.1a1 - + black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) + + black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable) "###); }); @@ -2799,7 +2799,7 @@ fn sync_editable_and_registry() -> Result<()> { Downloaded 1 package in [TIME] Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - - black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable/) + - black==0.1.0+editable (from file://[WORKSPACE_DIR]/scripts/editable-installs/black_editable) + black==23.10.0 warning: The package `black` requires `click >=8.0.0`, but it's not installed. warning: The package `black` requires `mypy-extensions >=0.4.3`, but it's not installed. diff --git a/crates/puffin/tests/pip_uninstall.rs b/crates/puffin/tests/pip_uninstall.rs index d8b033669..ab662a74a 100644 --- a/crates/puffin/tests/pip_uninstall.rs +++ b/crates/puffin/tests/pip_uninstall.rs @@ -403,7 +403,7 @@ fn uninstall_editable_by_name() -> Result<()> { ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); @@ -467,7 +467,7 @@ fn uninstall_editable_by_path() -> Result<()> { ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); @@ -532,7 +532,7 @@ fn uninstall_duplicate_editable() -> Result<()> { ----- stderr ----- Uninstalled 1 package in [TIME] - - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable/) + - poetry-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/poetry_editable) "###); }); diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 468aa5c43..f4f927ea4 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -46,7 +46,7 @@ use tracing::warn; use unscanny::{Pattern, Scanner}; use url::Url; -use pep508_rs::{Pep508Error, Requirement, VerbatimUrl}; +use pep508_rs::{Pep508Error, Requirement, VerbatimUrl, VerbatimUrlError}; /// We emit one of those for each requirements.txt entry enum RequirementsTxtStatement { @@ -69,26 +69,18 @@ enum RequirementsTxtStatement { } #[derive(Debug, Clone, Eq, PartialEq)] -pub enum EditableRequirement { - Path { path: PathBuf, url: VerbatimUrl }, - Url { path: PathBuf, url: VerbatimUrl }, +pub struct EditableRequirement { + pub url: VerbatimUrl, + pub path: PathBuf, } impl EditableRequirement { - /// Return the [`VerbatimUrl`] of the editable. pub fn url(&self) -> &VerbatimUrl { - match self { - EditableRequirement::Path { url, .. } => url, - EditableRequirement::Url { url, .. } => url, - } + &self.url } - /// Return the underlying [`Url`] of the editable. pub fn raw(&self) -> &Url { - match self { - EditableRequirement::Path { url, .. } => url.raw(), - EditableRequirement::Url { url, .. } => url.raw(), - } + self.url.raw() } } @@ -96,101 +88,83 @@ impl FromStr for EditableRequirement { type Err = RequirementsTxtParserError; fn from_str(s: &str) -> Result { - let editable_requirement = ParsedEditableRequirement::from_str(s)?; + let editable_requirement = ParsedEditableRequirement::from(s.to_string()); editable_requirement.with_working_dir(".") } } -/// Relative paths aren't resolved with the current dir yet +/// 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/` #[derive(Debug, Clone, Eq, PartialEq)] -pub enum ParsedEditableRequirement { - Path(String), - Url(VerbatimUrl), -} +pub struct ParsedEditableRequirement(String); impl ParsedEditableRequirement { pub fn with_working_dir( self, working_dir: impl AsRef, ) -> Result { - Ok(match self { - ParsedEditableRequirement::Path(given) => { - let path = PathBuf::from(&given); - if path.is_absolute() { - EditableRequirement::Path { - url: VerbatimUrl::from_path(&path, given) - .map_err(|()| RequirementsTxtParserError::InvalidPath(path.clone()))?, - path, - } - } else { - // Avoid paths like `/home/ferris/project/scripts/../editable/` - let path = fs::canonicalize(working_dir.as_ref().join(&path))?; - EditableRequirement::Path { - url: VerbatimUrl::from_path(&path, given) - .map_err(|()| RequirementsTxtParserError::InvalidPath(path.clone()))?, - path, - } - } - } - ParsedEditableRequirement::Url(url) => { - // Require, e.g., `file:///home/ferris/project/scripts/...`. - if url.scheme() != "file" { - return Err(RequirementsTxtParserError::UnsupportedUrl(url.clone())); - } - EditableRequirement::Url { - path: fs::canonicalize( - url.to_file_path().map_err(|()| { - RequirementsTxtParserError::UnsupportedUrl(url.clone()) - })?, - )?, - url, - } - } - }) + let given = self.0; + + // Validate against some common mistakes. If we're passed a URL with some other scheme, + // it will fail to canonicalize below, but this is a better error message for these common + // cases. + let s = given.trim(); + if s.starts_with("http://") + || s.starts_with("https://") + || s.starts_with("git+") + || s.starts_with("hg+") + || s.starts_with("svn+") + || s.starts_with("bzr+") + { + return Err(RequirementsTxtParserError::UnsupportedUrl( + given.to_string(), + )); + } + + // Create a `VerbatimUrl` to represent the editable requirement. + let url = if let Some(path) = s.strip_prefix("file://") { + // Ex) `file:///home/ferris/project/scripts/...` + VerbatimUrl::from_path(path, working_dir, s.to_string()) + .map_err(RequirementsTxtParserError::InvalidEditablePath)? + } else if let Some(path) = s.strip_prefix("file:") { + // Ex) `file:../editable/` + VerbatimUrl::from_path(path, working_dir, s.to_string()) + .map_err(RequirementsTxtParserError::InvalidEditablePath)? + } else { + // Ex) `../editable/` + VerbatimUrl::from_path(s, working_dir, s.to_string()) + .map_err(RequirementsTxtParserError::InvalidEditablePath)? + }; + + // Create a `PathBuf`. + let path = url + .to_file_path() + .expect("file:// URLs should be valid paths"); + + Ok(EditableRequirement { url, path }) } } -impl FromStr for ParsedEditableRequirement { - type Err = RequirementsTxtParserError; - - fn from_str(s: &str) -> Result { - if s.trim_start().starts_with("file://") { - // Ex) `file:///home/ferris/project/scripts/...` - if let Ok(url) = VerbatimUrl::from_str(s) { - Ok(ParsedEditableRequirement::Url(url)) - } else { - Err(RequirementsTxtParserError::UnsupportedUrl( - VerbatimUrl::from_str(s).unwrap(), - )) - } - } else if let Some(s) = s.trim_start().strip_prefix("file:") { - // Ex) `file:../editable/` - Ok(ParsedEditableRequirement::Path(s.to_string())) - } else if let Ok(url) = VerbatimUrl::from_str(s) { - // Ex) `http://example.com/` - Ok(ParsedEditableRequirement::Url(url)) - } else { - // Ex) `../editable/` - Ok(ParsedEditableRequirement::Path(s.to_string())) - } +impl From for ParsedEditableRequirement { + fn from(s: String) -> Self { + Self(s) } } impl Display for ParsedEditableRequirement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ParsedEditableRequirement::Path(path) => path.fmt(f), - ParsedEditableRequirement::Url(url) => url.fmt(f), - } + Display::fmt(&self.0, f) } } impl Display for EditableRequirement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - EditableRequirement::Path { url, .. } => url.fmt(f), - EditableRequirement::Url { url, .. } => url.fmt(f), - } + Display::fmt(&self.url, f) } } @@ -366,7 +340,7 @@ 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_str(path_or_url)?; + let editable_requirement = ParsedEditableRequirement::from(path_or_url.to_string()); RequirementsTxtStatement::EditableRequirement(editable_requirement) } else if s.at(char::is_ascii_alphanumeric) { let (requirement, hashes) = parse_requirement_and_hashes(s, content)?; @@ -534,8 +508,8 @@ pub struct RequirementsTxtFileError { #[derive(Debug)] pub enum RequirementsTxtParserError { IO(io::Error), - InvalidPath(PathBuf), - UnsupportedUrl(VerbatimUrl), + InvalidEditablePath(VerbatimUrlError), + UnsupportedUrl(String), Parser { message: String, location: usize, @@ -556,8 +530,8 @@ impl Display for RequirementsTxtParserError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RequirementsTxtParserError::IO(err) => err.fmt(f), - RequirementsTxtParserError::InvalidPath(path) => { - write!(f, "Invalid path: {}", path.display()) + RequirementsTxtParserError::InvalidEditablePath(err) => { + write!(f, "Invalid editable path: {err}") } RequirementsTxtParserError::UnsupportedUrl(url) => { write!(f, "Unsupported URL (expected a `file://` scheme): {url}") @@ -582,7 +556,7 @@ impl std::error::Error for RequirementsTxtParserError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match &self { RequirementsTxtParserError::IO(err) => err.source(), - RequirementsTxtParserError::InvalidPath(_) => None, + RequirementsTxtParserError::InvalidEditablePath(err) => err.source(), RequirementsTxtParserError::UnsupportedUrl(_) => None, RequirementsTxtParserError::Pep508 { source, .. } => Some(source), RequirementsTxtParserError::Subfile { source, .. } => Some(source.as_ref()), @@ -595,13 +569,8 @@ impl Display for RequirementsTxtFileError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.error { RequirementsTxtParserError::IO(err) => err.fmt(f), - RequirementsTxtParserError::InvalidPath(path) => { - write!( - f, - "Invalid path in {}: {}", - self.file.display(), - path.display() - ) + RequirementsTxtParserError::InvalidEditablePath(err) => { + write!(f, "Invalid editable path in {}: {err}", self.file.display()) } RequirementsTxtParserError::UnsupportedUrl(url) => { write!(