mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Preserve trailing slash on find-links URLs
This commit is contained in:
parent
52338214e3
commit
6012b9de8f
4 changed files with 104 additions and 55 deletions
|
@ -990,7 +990,7 @@ fn parse_find_links(input: &str) -> Result<Maybe<PipFindLinks>, String> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
Ok(Maybe::None)
|
Ok(Maybe::None)
|
||||||
} else {
|
} else {
|
||||||
IndexUrl::from_str(input)
|
IndexUrl::parse_preserving_trailing_slash(input)
|
||||||
.map(Index::from_find_links)
|
.map(Index::from_find_links)
|
||||||
.map(|index| Index {
|
.map(|index| Index {
|
||||||
origin: Some(Origin::Cli),
|
origin: Some(Origin::Cli),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
|
@ -150,20 +149,10 @@ impl<'a> FlatIndexClient<'a> {
|
||||||
Self::read_from_directory(&path, index)
|
Self::read_from_directory(&path, index)
|
||||||
.map_err(|err| FlatIndexError::FindLinksDirectory(path.clone(), err))
|
.map_err(|err| FlatIndexError::FindLinksDirectory(path.clone(), err))
|
||||||
}
|
}
|
||||||
IndexUrl::Pypi(url) | IndexUrl::Url(url) => {
|
IndexUrl::Pypi(url) | IndexUrl::Url(url) => self
|
||||||
// If the URL was originally provided with a slash, we restore that slash
|
.read_from_url(url, index)
|
||||||
// before making a request.
|
.await
|
||||||
let url_with_original_slash =
|
.map_err(|err| FlatIndexError::FindLinksUrl(url.to_url(), err)),
|
||||||
if url.given().is_some_and(|given| given.ends_with('/')) {
|
|
||||||
index.url_with_trailing_slash()
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(index.url())
|
|
||||||
};
|
|
||||||
|
|
||||||
self.read_from_url(&url_with_original_slash, index)
|
|
||||||
.await
|
|
||||||
.map_err(|err| FlatIndexError::FindLinksUrl(url.to_url(), err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,30 @@ impl IndexUrl {
|
||||||
///
|
///
|
||||||
/// Normalizes non-file URLs by removing trailing slashes for consistency.
|
/// Normalizes non-file URLs by removing trailing slashes for consistency.
|
||||||
pub fn parse(path: &str, root_dir: Option<&Path>) -> Result<Self, IndexUrlError> {
|
pub fn parse(path: &str, root_dir: Option<&Path>) -> Result<Self, IndexUrlError> {
|
||||||
|
Self::parse_with_trailing_slash_policy(path, root_dir, TrailingSlashPolicy::Remove)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an [`IndexUrl`] from a string, relative to an optional root directory.
|
||||||
|
///
|
||||||
|
/// If no root directory is provided, relative paths are resolved against the current working
|
||||||
|
/// directory.
|
||||||
|
///
|
||||||
|
/// Preserves trailing slash if present in `path`.
|
||||||
|
pub fn parse_preserving_trailing_slash(path: &str) -> Result<Self, IndexUrlError> {
|
||||||
|
Self::parse_with_trailing_slash_policy(path, None, TrailingSlashPolicy::Preserve)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an [`IndexUrl`] from a string, relative to an optional root directory.
|
||||||
|
///
|
||||||
|
/// If no root directory is provided, relative paths are resolved against the current working
|
||||||
|
/// directory.
|
||||||
|
///
|
||||||
|
/// Applies trailing slash policy to non-file URLs.
|
||||||
|
fn parse_with_trailing_slash_policy(
|
||||||
|
path: &str,
|
||||||
|
root_dir: Option<&Path>,
|
||||||
|
slash_policy: TrailingSlashPolicy,
|
||||||
|
) -> Result<Self, IndexUrlError> {
|
||||||
let url = match split_scheme(path) {
|
let url = match split_scheme(path) {
|
||||||
Some((scheme, ..)) => {
|
Some((scheme, ..)) => {
|
||||||
match Scheme::parse(scheme) {
|
match Scheme::parse(scheme) {
|
||||||
|
@ -67,7 +91,10 @@ impl IndexUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(Self::from(url.with_given(path)))
|
Ok(Self::from_verbatim_url_with_trailing_slash_policy(
|
||||||
|
url.with_given(path),
|
||||||
|
slash_policy,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the root [`Url`] of the index, if applicable.
|
/// Return the root [`Url`] of the index, if applicable.
|
||||||
|
@ -91,6 +118,34 @@ impl IndexUrl {
|
||||||
url.path_segments_mut().ok()?.pop_if_empty().pop();
|
url.path_segments_mut().ok()?.pop_if_empty().pop();
|
||||||
Some(url)
|
Some(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct an [`IndexUrl`] from a [`VerbatimUrl`], preserving a trailing
|
||||||
|
/// slash if present.
|
||||||
|
pub fn from_verbatim_url_preserving_trailing_slash(url: VerbatimUrl) -> Self {
|
||||||
|
Self::from_verbatim_url_with_trailing_slash_policy(url, TrailingSlashPolicy::Preserve)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct an [`IndexUrl`] from a [`VerbatimUrl`], applying a [`TrailingSlashPolicy`]
|
||||||
|
/// to non-file URLs.
|
||||||
|
fn from_verbatim_url_with_trailing_slash_policy(
|
||||||
|
mut url: VerbatimUrl,
|
||||||
|
slash_policy: TrailingSlashPolicy,
|
||||||
|
) -> Self {
|
||||||
|
if url.scheme() == "file" {
|
||||||
|
return Self::Path(Arc::new(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if slash_policy == TrailingSlashPolicy::Remove {
|
||||||
|
if let Ok(mut path_segments) = url.raw_mut().path_segments_mut() {
|
||||||
|
path_segments.pop_if_empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *url.raw() == *PYPI_URL {
|
||||||
|
Self::Pypi(Arc::new(url))
|
||||||
|
} else {
|
||||||
|
Self::Url(Arc::new(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
|
@ -117,18 +172,6 @@ impl IndexUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the raw URL for the index with a trailing slash.
|
|
||||||
pub fn url_with_trailing_slash(&self) -> Cow<'_, DisplaySafeUrl> {
|
|
||||||
let path = self.url().path();
|
|
||||||
if path.ends_with('/') {
|
|
||||||
Cow::Borrowed(self.url())
|
|
||||||
} else {
|
|
||||||
let mut url = self.url().clone();
|
|
||||||
url.set_path(&format!("{path}/"));
|
|
||||||
Cow::Owned(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert the index URL into a [`DisplaySafeUrl`].
|
/// Convert the index URL into a [`DisplaySafeUrl`].
|
||||||
pub fn into_url(self) -> DisplaySafeUrl {
|
pub fn into_url(self) -> DisplaySafeUrl {
|
||||||
match self {
|
match self {
|
||||||
|
@ -196,6 +239,16 @@ impl Verbatim for IndexUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to preserve or remove a trailing slash from a non-file URL.
|
||||||
|
#[derive(Default, Clone, Copy, Eq, PartialEq)]
|
||||||
|
enum TrailingSlashPolicy {
|
||||||
|
/// Preserve trailing slash if present.
|
||||||
|
#[default]
|
||||||
|
Preserve,
|
||||||
|
/// Remove trailing slash if present.
|
||||||
|
Remove,
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if a path is disambiguated.
|
/// Checks if a path is disambiguated.
|
||||||
///
|
///
|
||||||
/// Disambiguated paths are absolute paths, paths with valid schemes,
|
/// Disambiguated paths are absolute paths, paths with valid schemes,
|
||||||
|
@ -270,21 +323,10 @@ impl<'de> serde::de::Deserialize<'de> for IndexUrl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<VerbatimUrl> for IndexUrl {
|
impl From<VerbatimUrl> for IndexUrl {
|
||||||
fn from(mut url: VerbatimUrl) -> Self {
|
fn from(url: VerbatimUrl) -> Self {
|
||||||
if url.scheme() == "file" {
|
// Remove trailing slashes for consistency. They'll be re-added if necessary when
|
||||||
Self::Path(Arc::new(url))
|
// querying the Simple API.
|
||||||
} else {
|
Self::from_verbatim_url_with_trailing_slash_policy(url, TrailingSlashPolicy::Remove)
|
||||||
// Remove trailing slashes for consistency. They'll be re-added if necessary when
|
|
||||||
// querying the Simple API.
|
|
||||||
if let Ok(mut path_segments) = url.raw_mut().path_segments_mut() {
|
|
||||||
path_segments.pop_if_empty();
|
|
||||||
}
|
|
||||||
if *url.raw() == *PYPI_URL {
|
|
||||||
Self::Pypi(Arc::new(url))
|
|
||||||
} else {
|
|
||||||
Self::Url(Arc::new(url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -727,21 +769,40 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_index_url_with_trailing_slash() {
|
fn test_index_url_trailing_slash_policies() {
|
||||||
let url_with_trailing_slash = DisplaySafeUrl::parse("https://example.com/path/").unwrap();
|
let url_with_trailing_slash = DisplaySafeUrl::parse("https://example.com/path/").unwrap();
|
||||||
|
let url_without_trailing_slash = DisplaySafeUrl::parse("https://example.com/path").unwrap();
|
||||||
|
let verbatim_url_with_trailing_slash =
|
||||||
|
VerbatimUrl::from_url(url_with_trailing_slash.clone());
|
||||||
|
let verbatim_url_without_trailing_slash =
|
||||||
|
VerbatimUrl::from_url(url_without_trailing_slash.clone());
|
||||||
|
|
||||||
let index_url_with_given_slash =
|
// Test `From<VerbatimUrl>` implementation.
|
||||||
IndexUrl::parse("https://example.com/path/", None).unwrap();
|
// Trailing slash should be removed if present.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&*index_url_with_given_slash.url_with_trailing_slash(),
|
IndexUrl::from(verbatim_url_with_trailing_slash.clone()).url(),
|
||||||
&url_with_trailing_slash
|
&url_without_trailing_slash
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndexUrl::from(verbatim_url_without_trailing_slash.clone()).url(),
|
||||||
|
&url_without_trailing_slash
|
||||||
);
|
);
|
||||||
|
|
||||||
let index_url_without_given_slash =
|
// Test `from_verbatim_url_preserving_trailing_slash`.
|
||||||
IndexUrl::parse("https://example.com/path", None).unwrap();
|
// Trailing slash should be preserved if present.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&*index_url_without_given_slash.url_with_trailing_slash(),
|
IndexUrl::from_verbatim_url_preserving_trailing_slash(
|
||||||
|
verbatim_url_with_trailing_slash.clone()
|
||||||
|
)
|
||||||
|
.url(),
|
||||||
&url_with_trailing_slash
|
&url_with_trailing_slash
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
IndexUrl::from_verbatim_url_preserving_trailing_slash(
|
||||||
|
verbatim_url_without_trailing_slash.clone()
|
||||||
|
)
|
||||||
|
.url(),
|
||||||
|
&url_without_trailing_slash
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,8 @@ use tracing::instrument;
|
||||||
use uv_cache_key::CanonicalUrl;
|
use uv_cache_key::CanonicalUrl;
|
||||||
use uv_client::BaseClientBuilder;
|
use uv_client::BaseClientBuilder;
|
||||||
use uv_configuration::{DependencyGroups, NoBinary, NoBuild};
|
use uv_configuration::{DependencyGroups, NoBinary, NoBuild};
|
||||||
use uv_distribution_types::Requirement;
|
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
IndexUrl, NameRequirementSpecification, UnresolvedRequirement,
|
IndexUrl, NameRequirementSpecification, Requirement, UnresolvedRequirement,
|
||||||
UnresolvedRequirementSpecification,
|
UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
use uv_fs::{CWD, Simplified};
|
use uv_fs::{CWD, Simplified};
|
||||||
|
@ -152,7 +151,7 @@ impl RequirementsSpecification {
|
||||||
find_links: requirements_txt
|
find_links: requirements_txt
|
||||||
.find_links
|
.find_links
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(IndexUrl::from)
|
.map(IndexUrl::from_verbatim_url_preserving_trailing_slash)
|
||||||
.collect(),
|
.collect(),
|
||||||
no_binary: requirements_txt.no_binary,
|
no_binary: requirements_txt.no_binary,
|
||||||
no_build: requirements_txt.only_binary,
|
no_build: requirements_txt.only_binary,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue