Normalize trailing slashes only during lockfile validation

This commit is contained in:
John Mumm 2025-07-08 11:19:02 +02:00
parent ddb1577a93
commit 1f8217f7bb
No known key found for this signature in database
GPG Key ID: 73D2271AFDC26EA8
8 changed files with 211 additions and 182 deletions

View File

@ -176,6 +176,10 @@ impl UrlString {
/// it's the only path segment, e.g., `https://example.com/` would be unchanged.
#[must_use]
pub fn without_trailing_slash(&self) -> Cow<'_, Self> {
if !self.as_ref().ends_with('/') {
return Cow::Borrowed(self);
}
self.as_ref()
.strip_suffix('/')
.filter(|path| {

View File

@ -258,20 +258,13 @@ impl<'de> serde::de::Deserialize<'de> for IndexUrl {
}
impl From<VerbatimUrl> for IndexUrl {
fn from(mut url: VerbatimUrl) -> Self {
fn from(url: VerbatimUrl) -> Self {
if url.scheme() == "file" {
Self::Path(Arc::new(url))
} else if *url.raw() == *PYPI_URL {
Self::Pypi(Arc::new(url))
} else {
// 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))
}
Self::Url(Arc::new(url))
}
}
}

View File

@ -51,11 +51,8 @@ impl VerbatimUrl {
/// Parse a URL from a string.
pub fn parse_url(given: impl AsRef<str>) -> Result<Self, ParseError> {
let url = Url::parse(given.as_ref())?;
Ok(Self {
url: DisplaySafeUrl::from(url),
given: None,
})
let url = DisplaySafeUrl::parse(given.as_ref())?;
Ok(Self { url, given: None })
}
/// Parse a URL from an absolute or relative path.
@ -193,6 +190,32 @@ impl VerbatimUrl {
.to_file_path()
.map_err(|()| VerbatimUrlError::UrlConversion(self.url.to_file_path().unwrap()))
}
/// Return the [`VerbatimUrl`] (as a [`Cow`]) with trailing slash removed.
///
/// This matches the semantics of [`Url::pop_if_empty`], which will not trim a trailing slash if
/// it's the only path segment, e.g., `https://example.com/` would be unchanged.
#[must_use]
pub fn without_trailing_slash(&self) -> Cow<'_, Self> {
if !self.as_ref().ends_with('/') {
return Cow::Borrowed(self);
}
self.as_ref()
.strip_suffix('/')
.filter(|path| {
// Only strip the trailing slash if there's _another_ trailing slash that isn't a
// part of the scheme.
path.split_once("://")
.map(|(_scheme, rest)| rest)
.unwrap_or(path)
.contains('/')
})
.map(|path| {
Cow::Owned(VerbatimUrl::parse_url(path).expect("URL should still be parseable"))
})
.unwrap_or(Cow::Borrowed(self))
}
}
impl Ord for VerbatimUrl {

View File

@ -1433,7 +1433,8 @@ impl Lock {
.into_iter()
.filter_map(|index| match index.url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
Some(UrlString::from(index.url().without_credentials().as_ref()))
let url = UrlString::from(index.url().without_credentials().as_ref());
Some(url.without_trailing_slash().into_owned())
}
IndexUrl::Path(_) => None,
})
@ -4825,7 +4826,13 @@ fn normalize_requirement(
index.remove_credentials();
index
})
.map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
.map(|index| {
IndexMetadata::from(IndexUrl::from(
VerbatimUrl::from_url(index)
.without_trailing_slash()
.into_owned(),
))
});
Ok(Requirement {
name: requirement.name,
extras: requirement.extras,
@ -4867,7 +4874,9 @@ fn normalize_requirement(
location,
subdirectory,
ext,
url: VerbatimUrl::from_url(url),
url: VerbatimUrl::from_url(url)
.without_trailing_slash()
.into_owned(),
},
origin: None,
})

View File

@ -4384,7 +4384,7 @@ fn add_lower_bound_local() -> Result<()> {
]
[[tool.uv.index]]
url = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html"
url = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/"
"#
);
});
@ -4403,7 +4403,7 @@ fn add_lower_bound_local() -> Result<()> {
[[package]]
name = "local-simple-a"
version = "1.2.3+foo"
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html" }
source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/local_simple_a-1.2.3+foo.tar.gz", hash = "sha256:ebd55c4a79d0a5759126657cb289ff97558902abcfb142e036b993781497edac" }
wheels = [
{ url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/local_simple_a-1.2.3+foo-py3-none-any.whl", hash = "sha256:6f30e2e709b3e171cd734bb58705229a582587c29e0a7041227435583c7224cc" },
@ -9272,7 +9272,7 @@ fn add_index_with_trailing_slash() -> Result<()> {
constraint-dependencies = ["markupsafe<3"]
[[tool.uv.index]]
url = "https://pypi.org/simple"
url = "https://pypi.org/simple/"
"#
);
});
@ -9297,7 +9297,7 @@ fn add_index_with_trailing_slash() -> Result<()> {
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
@ -11204,7 +11204,7 @@ fn repeated_index_cli_reversed() -> Result<()> {
]
[[tool.uv.index]]
url = "https://test.pypi.org/simple"
url = "https://test.pypi.org/simple/"
"#
);
});
@ -11226,7 +11226,7 @@ fn repeated_index_cli_reversed() -> Result<()> {
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://test.pypi.org/simple" }
source = { registry = "https://test.pypi.org/simple/" }
sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:16.826Z" }
wheels = [
{ url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:14.843Z" },

View File

@ -15543,7 +15543,7 @@ fn lock_trailing_slash() -> Result<()> {
[[package]]
name = "anyio"
version = "3.7.0"
source = { registry = "https://pypi.org/simple" }
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
@ -15556,7 +15556,7 @@ fn lock_trailing_slash() -> Result<()> {
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" },
@ -15576,7 +15576,7 @@ fn lock_trailing_slash() -> Result<()> {
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },

File diff suppressed because it is too large Load Diff

View File

@ -9939,7 +9939,7 @@ fn sync_required_environment_hint() -> Result<()> {
----- stderr -----
Resolved 2 packages in [TIME]
error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html` can't be installed because it doesn't have a source distribution or wheel for the current platform
error: Distribution `no-sdist-no-wheels-with-matching-platform-a==1.0.0 @ registry+https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/` can't be installed because it doesn't have a source distribution or wheel for the current platform
hint: You're on [PLATFORM] (`[TAG]`), but `no-sdist-no-wheels-with-matching-platform-a` (v1.0.0) only has wheels for the following platform: `macosx_10_0_ppc64`; consider adding your platform to `tool.uv.required-environments` to ensure uv resolves to a version with compatible wheels
");