mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
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:
parent
cd2fb6fd60
commit
5adb08a304
14 changed files with 174 additions and 198 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: ®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(),
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<_>>()?;
|
||||
|
||||
|
|
|
@ -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<_>>()?;
|
||||
|
||||
|
|
|
@ -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<_>>()?;
|
||||
|
||||
|
|
|
@ -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)
|
||||
"###);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
"###);
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
"###);
|
||||
});
|
||||
|
||||
|
|
|
@ -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!(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue