Allow relative paths and environment variables in all editable representations (#1000)

## Summary

I don't know if this is actually a good change, but it tries to make the
editable install experience more consistent. Specifically, we now
support...

```
# Use a relative path with a `file://` prefix.
# Prior to this PR, we supported `file:../foo`, but not `file://../foo`, which felt inconsistent.
-e file://../foo

# Use environment variables with paths, not just URLs.
# Prior to this PR, we supported `file://${PROJECT_ROOT}/../foo`, but not the below.
-e ${PROJECT_ROOT}/../foo
```

Importantly, `-e file://../foo` is actually not supported by pip... `-e
file:../foo` _is_ supported though. We support both, as of this PR. Open
to feedback.
This commit is contained in:
Charlie Marsh 2024-01-19 09:00:37 -05:00 committed by GitHub
parent cd2fb6fd60
commit 5adb08a304
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 174 additions and 198 deletions

View file

@ -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)
}
}

View file

@ -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<Self, Error> {
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,
}
}
}

View file

@ -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 }

View file

@ -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<char>` 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 {

View file

@ -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<Self, Error> {
let url = Url::parse(&expand_env_vars(&given))?;
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,
@ -35,10 +37,30 @@ impl VerbatimUrl {
}
/// Parse a URL from a path.
#[allow(clippy::result_unit_err)]
pub fn from_path(path: impl AsRef<Path>, given: String) -> Result<Self, ()> {
pub fn from_path(
path: impl AsRef<str>,
working_dir: impl AsRef<Path>,
given: String,
) -> 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 {
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, Self::Err> {
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
/// <http://pubs.opengroup.org/onlinepubs/9699919799/>`_ 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<String> = 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<Regex> =
@ -124,7 +152,14 @@ fn expand_env_vars(s: &str) -> Cow<'_, str> {
RE.replace_all(s, |caps: &regex::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(),
})
})

View file

@ -196,15 +196,9 @@ pub(crate) async fn pip_compile(
let editables: Vec<LocalEditable> = 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::<Result<_>>()?;

View file

@ -315,15 +315,12 @@ async fn build_editables(
let editables: Vec<LocalEditable> = 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::<Result<_>>()?;

View file

@ -417,15 +417,12 @@ async fn resolve_editables(
let local_editables: Vec<LocalEditable> = 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::<Result<_>>()?;

View file

@ -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)
"###);
});

View file

@ -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
"###);
});

View file

@ -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.

View file

@ -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)
"###);
});

View file

@ -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<Self, Self::Err> {
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 <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/`
#[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<Path>,
) -> Result<EditableRequirement, RequirementsTxtParserError> {
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<Self, Self::Err> {
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<String> 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!(