Remove unnecessary `DisplaySafeUrl::from` (#16689)

For #16622
This commit is contained in:
konsti 2025-11-11 20:12:20 +01:00 committed by GitHub
parent 63ab247765
commit 92c2bfcca0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 99 additions and 93 deletions

View File

@ -591,7 +591,7 @@ mod tests {
#[test] #[test]
fn test_is_known_url() { fn test_is_known_url() {
let api_url = DisplaySafeUrl::from(Url::parse("https://api.pyx.dev").unwrap()); let api_url = DisplaySafeUrl::parse("https://api.pyx.dev").unwrap();
let cdn_domain = "astralhosted.com"; let cdn_domain = "astralhosted.com";
// Same realm as API. // Same realm as API.
@ -646,7 +646,7 @@ mod tests {
#[test] #[test]
fn test_is_known_domain() { fn test_is_known_domain() {
let api_url = DisplaySafeUrl::from(Url::parse("https://api.pyx.dev").unwrap()); let api_url = DisplaySafeUrl::parse("https://api.pyx.dev").unwrap();
let cdn_domain = "astralhosted.com"; let cdn_domain = "astralhosted.com";
// Same realm as API. // Same realm as API.

View File

@ -1,6 +1,7 @@
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::{fmt::Display, fmt::Formatter}; use std::{fmt::Display, fmt::Formatter};
use url::Url; use url::Url;
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString; use uv_small_str::SmallString;
/// Used to determine if authentication information should be retained on a new URL. /// Used to determine if authentication information should be retained on a new URL.
@ -29,6 +30,12 @@ pub struct Realm {
port: Option<u16>, port: Option<u16>,
} }
impl From<&DisplaySafeUrl> for Realm {
fn from(url: &DisplaySafeUrl) -> Self {
Self::from(&**url)
}
}
impl From<&Url> for Realm { impl From<&Url> for Realm {
fn from(url: &Url) -> Self { fn from(url: &Url) -> Self {
Self { Self {

View File

@ -5,7 +5,6 @@ use fs_err as fs;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use url::Url;
use uv_fs::{LockedFile, with_added_extension}; use uv_fs::{LockedFile, with_added_extension};
use uv_preview::{Preview, PreviewFeatures}; use uv_preview::{Preview, PreviewFeatures};
use uv_redacted::DisplaySafeUrl; use uv_redacted::DisplaySafeUrl;
@ -310,13 +309,17 @@ impl TextCredentialStore {
/// Get credentials for a given URL and username. /// Get credentials for a given URL and username.
/// ///
/// The most specific URL prefix match in the same [`Realm`] is returned, if any. /// The most specific URL prefix match in the same [`Realm`] is returned, if any.
pub fn get_credentials(&self, url: &Url, username: Option<&str>) -> Option<&Credentials> { pub fn get_credentials(
&self,
url: &DisplaySafeUrl,
username: Option<&str>,
) -> Option<&Credentials> {
let request_realm = Realm::from(url); let request_realm = Realm::from(url);
// Perform an exact lookup first // Perform an exact lookup first
// TODO(zanieb): Consider adding `DisplaySafeUrlRef` so we can avoid this clone // TODO(zanieb): Consider adding `DisplaySafeUrlRef` so we can avoid this clone
// TODO(zanieb): We could also return early here if we can't normalize to a `Service` // TODO(zanieb): We could also return early here if we can't normalize to a `Service`
if let Ok(url_service) = Service::try_from(DisplaySafeUrl::from(url.clone())) { if let Ok(url_service) = Service::try_from(url.clone()) {
if let Some(credential) = self.credentials.get(&( if let Some(credential) = self.credentials.get(&(
url_service.clone(), url_service.clone(),
Username::from(username.map(str::to_string)), Username::from(username.map(str::to_string)),
@ -430,10 +433,10 @@ mod tests {
let service = Service::from_str("https://example.com").unwrap(); let service = Service::from_str("https://example.com").unwrap();
store.insert(service.clone(), credentials.clone()); store.insert(service.clone(), credentials.clone());
let url = Url::parse("https://example.com/").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/").unwrap();
assert!(store.get_credentials(&url, None).is_some()); assert!(store.get_credentials(&url, None).is_some());
let url = Url::parse("https://example.com/path").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/path").unwrap();
let retrieved = store.get_credentials(&url, None).unwrap(); let retrieved = store.get_credentials(&url, None).unwrap();
assert_eq!(retrieved.username(), Some("user")); assert_eq!(retrieved.username(), Some("user"));
assert_eq!(retrieved.password(), Some("pass")); assert_eq!(retrieved.password(), Some("pass"));
@ -443,7 +446,7 @@ mod tests {
.remove(&service, Username::from(Some("user".to_string()))) .remove(&service, Username::from(Some("user".to_string())))
.is_some() .is_some()
); );
let url = Url::parse("https://example.com/").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/").unwrap();
assert!(store.get_credentials(&url, None).is_none()); assert!(store.get_credentials(&url, None).is_none());
} }
@ -469,12 +472,12 @@ password = "pass2"
let store = TextCredentialStore::from_file(temp_file.path()).unwrap(); let store = TextCredentialStore::from_file(temp_file.path()).unwrap();
let url = Url::parse("https://example.com/").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/").unwrap();
assert!(store.get_credentials(&url, None).is_some()); assert!(store.get_credentials(&url, None).is_some());
let url = Url::parse("https://test.org/").unwrap(); let url = DisplaySafeUrl::parse("https://test.org/").unwrap();
assert!(store.get_credentials(&url, None).is_some()); assert!(store.get_credentials(&url, None).is_some());
let url = Url::parse("https://example.com").unwrap(); let url = DisplaySafeUrl::parse("https://example.com").unwrap();
let cred = store.get_credentials(&url, None).unwrap(); let cred = store.get_credentials(&url, None).unwrap();
assert_eq!(cred.username(), Some("testuser")); assert_eq!(cred.username(), Some("testuser"));
assert_eq!(cred.password(), Some("testpass")); assert_eq!(cred.password(), Some("testpass"));
@ -510,7 +513,7 @@ password = "pass2"
]; ];
for url_str in matching_urls { for url_str in matching_urls {
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None); let cred = store.get_credentials(&url, None);
assert!(cred.is_some(), "Failed to match URL with prefix: {url_str}"); assert!(cred.is_some(), "Failed to match URL with prefix: {url_str}");
} }
@ -523,7 +526,7 @@ password = "pass2"
]; ];
for url_str in non_matching_urls { for url_str in non_matching_urls {
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None); let cred = store.get_credentials(&url, None);
assert!(cred.is_none(), "Should not match non-prefix URL: {url_str}"); assert!(cred.is_none(), "Should not match non-prefix URL: {url_str}");
} }
@ -547,7 +550,7 @@ password = "pass2"
]; ];
for url_str in matching_urls { for url_str in matching_urls {
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None); let cred = store.get_credentials(&url, None);
assert!( assert!(
cred.is_some(), cred.is_some(),
@ -563,7 +566,7 @@ password = "pass2"
]; ];
for url_str in non_matching_urls { for url_str in non_matching_urls {
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None); let cred = store.get_credentials(&url, None);
assert!( assert!(
cred.is_none(), cred.is_none(),
@ -587,12 +590,12 @@ password = "pass2"
store.insert(specific_service.clone(), specific_cred); store.insert(specific_service.clone(), specific_cred);
// Should match the most specific prefix // Should match the most specific prefix
let url = Url::parse("https://example.com/api/v1/users").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/api/v1/users").unwrap();
let cred = store.get_credentials(&url, None).unwrap(); let cred = store.get_credentials(&url, None).unwrap();
assert_eq!(cred.username(), Some("specific")); assert_eq!(cred.username(), Some("specific"));
// Should match the general prefix for non-specific paths // Should match the general prefix for non-specific paths
let url = Url::parse("https://example.com/api/v2").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/api/v2").unwrap();
let cred = store.get_credentials(&url, None).unwrap(); let cred = store.get_credentials(&url, None).unwrap();
assert_eq!(cred.username(), Some("general")); assert_eq!(cred.username(), Some("general"));
} }
@ -600,7 +603,7 @@ password = "pass2"
#[test] #[test]
fn test_username_exact_url_match() { fn test_username_exact_url_match() {
let mut store = TextCredentialStore::default(); let mut store = TextCredentialStore::default();
let url = Url::parse("https://example.com").unwrap(); let url = DisplaySafeUrl::parse("https://example.com").unwrap();
let service = Service::from_str("https://example.com").unwrap(); let service = Service::from_str("https://example.com").unwrap();
let user1_creds = Credentials::basic(Some("user1".to_string()), Some("pass1".to_string())); let user1_creds = Credentials::basic(Some("user1".to_string()), Some("pass1".to_string()));
store.insert(service.clone(), user1_creds.clone()); store.insert(service.clone(), user1_creds.clone());
@ -641,7 +644,7 @@ password = "pass2"
store.insert(general_service, general_creds); store.insert(general_service, general_creds);
store.insert(specific_service, specific_creds); store.insert(specific_service, specific_creds);
let url = Url::parse("https://example.com/api/v1/users").unwrap(); let url = DisplaySafeUrl::parse("https://example.com/api/v1/users").unwrap();
// Should match specific credentials when username matches // Should match specific credentials when username matches
let result = store.get_credentials(&url, Some("specific_user")); let result = store.get_credentials(&url, Some("specific_user"));

View File

@ -3,7 +3,6 @@ use std::str::FromStr;
use jiff::Timestamp; use jiff::Timestamp;
use tl::HTMLTag; use tl::HTMLTag;
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use url::Url;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
@ -23,13 +22,13 @@ pub(crate) struct SimpleDetailHTML {
impl SimpleDetailHTML { impl SimpleDetailHTML {
/// Parse the list of [`PypiFile`]s from the simple HTML page returned by the given URL. /// Parse the list of [`PypiFile`]s from the simple HTML page returned by the given URL.
#[instrument(skip_all, fields(url = % url))] #[instrument(skip_all, fields(url = % url))]
pub(crate) fn parse(text: &str, url: &Url) -> Result<Self, Error> { pub(crate) fn parse(text: &str, url: &DisplaySafeUrl) -> Result<Self, Error> {
let dom = tl::parse(text, tl::ParserOptions::default())?; let dom = tl::parse(text, tl::ParserOptions::default())?;
// Parse the first `<base>` tag, if any, to determine the base URL to which all // Parse the first `<base>` tag, if any, to determine the base URL to which all
// relative URLs should be resolved. The HTML spec requires that the `<base>` tag // relative URLs should be resolved. The HTML spec requires that the `<base>` tag
// appear before other tags with attribute values of URLs. // appear before other tags with attribute values of URLs.
let base = BaseUrl::from(DisplaySafeUrl::from( let base = BaseUrl::from(
dom.nodes() dom.nodes()
.iter() .iter()
.filter_map(|node| node.as_tag()) .filter_map(|node| node.as_tag())
@ -39,7 +38,7 @@ impl SimpleDetailHTML {
.transpose()? .transpose()?
.flatten() .flatten()
.unwrap_or_else(|| url.clone()), .unwrap_or_else(|| url.clone()),
)); );
// Parse each `<a>` tag, to extract the filename, hash, and URL. // Parse each `<a>` tag, to extract the filename, hash, and URL.
let mut files: Vec<PypiFile> = dom let mut files: Vec<PypiFile> = dom
@ -68,12 +67,13 @@ impl SimpleDetailHTML {
} }
/// Parse the `href` from a `<base>` tag. /// Parse the `href` from a `<base>` tag.
fn parse_base(base: &HTMLTag) -> Result<Option<Url>, Error> { fn parse_base(base: &HTMLTag) -> Result<Option<DisplaySafeUrl>, Error> {
let Some(Some(href)) = base.attributes().get("href") else { let Some(Some(href)) = base.attributes().get("href") else {
return Ok(None); return Ok(None);
}; };
let href = std::str::from_utf8(href.as_bytes())?; let href = std::str::from_utf8(href.as_bytes())?;
let url = Url::parse(href).map_err(|err| Error::UrlParse(href.to_string(), err))?; let url =
DisplaySafeUrl::parse(href).map_err(|err| Error::UrlParse(href.to_string(), err))?;
Ok(Some(url)) Ok(Some(url))
} }
@ -325,7 +325,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -382,7 +382,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -442,7 +442,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -499,7 +499,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -556,7 +556,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -613,7 +613,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -668,7 +668,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -723,7 +723,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"; ";
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -761,7 +761,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -799,7 +799,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -854,7 +854,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -909,7 +909,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base); let result = SimpleDetailHTML::parse(text, &base);
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
Ok( Ok(
@ -966,7 +966,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base); let result = SimpleDetailHTML::parse(text, &base);
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
Ok( Ok(
@ -1023,7 +1023,7 @@ mod tests {
</html> </html>
<!--TIMESTAMP 1703347410--> <!--TIMESTAMP 1703347410-->
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap_err(); let result = SimpleDetailHTML::parse(text, &base).unwrap_err();
insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`"); insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`");
} }
@ -1040,8 +1040,10 @@ mod tests {
</body> </body>
</html> </html>
"#; "#;
let base = Url::parse("https://storage.googleapis.com/jax-releases/jax_cuda_releases.html") let base = DisplaySafeUrl::parse(
.unwrap(); "https://storage.googleapis.com/jax-releases/jax_cuda_releases.html",
)
.unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -1122,7 +1124,7 @@ mod tests {
</body> </body>
</html> </html>
"#; "#;
let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") let base = DisplaySafeUrl::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/")
.unwrap(); .unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
@ -1226,7 +1228,7 @@ mod tests {
</body> </body>
</html> </html>
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = DisplaySafeUrl::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleDetailHTML { SimpleDetailHTML {
@ -1298,7 +1300,7 @@ mod tests {
</body> </body>
</html> </html>
"#; "#;
let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") let base = DisplaySafeUrl::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/")
.unwrap(); .unwrap();
let result = SimpleDetailHTML::parse(text, &base).unwrap(); let result = SimpleDetailHTML::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"

View File

@ -185,14 +185,14 @@ impl InstalledDist {
let build_info = Self::read_build_info(path)?; let build_info = Self::read_build_info(path)?;
return if let Some(direct_url) = Self::read_direct_url(path)? { return if let Some(direct_url) = Self::read_direct_url(path)? {
match Url::try_from(&direct_url) { match DisplaySafeUrl::try_from(&direct_url) {
Ok(url) => Ok(Some(Self::from(InstalledDistKind::Url( Ok(url) => Ok(Some(Self::from(InstalledDistKind::Url(
InstalledDirectUrlDist { InstalledDirectUrlDist {
name, name,
version, version,
editable: matches!(&direct_url, DirectUrl::LocalDirectory { dir_info, .. } if dir_info.editable == Some(true)), editable: matches!(&direct_url, DirectUrl::LocalDirectory { dir_info, .. } if dir_info.editable == Some(true)),
direct_url: Box::new(direct_url), direct_url: Box::new(direct_url),
url: DisplaySafeUrl::from(url), url,
path: path.to_path_buf().into_boxed_path(), path: path.to_path_buf().into_boxed_path(),
cache_info, cache_info,
build_info, build_info,
@ -323,7 +323,7 @@ impl InstalledDist {
// Normalisation comes from `pkg_resources.to_filename`. // Normalisation comes from `pkg_resources.to_filename`.
let egg_info = target.join(file_stem.replace('-', "_") + ".egg-info"); let egg_info = target.join(file_stem.replace('-', "_") + ".egg-info");
let url = Url::from_file_path(&target) let url = DisplaySafeUrl::from_file_path(&target)
.map_err(|()| InstalledDistError::InvalidEggLinkTarget(path.to_path_buf()))?; .map_err(|()| InstalledDistError::InvalidEggLinkTarget(path.to_path_buf()))?;
// Mildly unfortunate that we must read metadata to get the version. // Mildly unfortunate that we must read metadata to get the version.
@ -337,7 +337,7 @@ impl InstalledDist {
version: Version::from_str(&egg_metadata.version)?, version: Version::from_str(&egg_metadata.version)?,
egg_link: path.to_path_buf().into_boxed_path(), egg_link: path.to_path_buf().into_boxed_path(),
target: target.into_boxed_path(), target: target.into_boxed_path(),
target_url: DisplaySafeUrl::from(url), target_url: url,
egg_info: egg_info.into_boxed_path(), egg_info: egg_info.into_boxed_path(),
}, },
)))); ))));

View File

@ -555,7 +555,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
async fn url_metadata<'data>( async fn url_metadata<'data>(
&self, &self,
source: &BuildableSource<'data>, source: &BuildableSource<'data>,
url: &'data Url, url: &'data DisplaySafeUrl,
index: Option<&'data IndexUrl>, index: Option<&'data IndexUrl>,
cache_shard: &CacheShard, cache_shard: &CacheShard,
subdirectory: Option<&'data Path>, subdirectory: Option<&'data Path>,
@ -637,7 +637,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
if let Some(subdirectory) = subdirectory { if let Some(subdirectory) = subdirectory {
if !source_dist_entry.path().join(subdirectory).is_dir() { if !source_dist_entry.path().join(subdirectory).is_dir() {
return Err(Error::MissingSubdirectory( return Err(Error::MissingSubdirectory(
DisplaySafeUrl::from(url.clone()), url.clone(),
subdirectory.to_path_buf(), subdirectory.to_path_buf(),
)); ));
} }
@ -738,7 +738,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
&self, &self,
source: &BuildableSource<'_>, source: &BuildableSource<'_>,
ext: SourceDistExtension, ext: SourceDistExtension,
url: &Url, url: &DisplaySafeUrl,
index: Option<&IndexUrl>, index: Option<&IndexUrl>,
cache_shard: &CacheShard, cache_shard: &CacheShard,
hashes: HashPolicy<'_>, hashes: HashPolicy<'_>,
@ -786,7 +786,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
.boxed_local() .boxed_local()
.instrument(info_span!("download", source_dist = %source)) .instrument(info_span!("download", source_dist = %source))
}; };
let req = Self::request(DisplaySafeUrl::from(url.clone()), client.unmanaged)?; let req = Self::request(url.clone(), client.unmanaged)?;
let revision = client let revision = client
.managed(|client| { .managed(|client| {
client.cached_client().get_serde_with_retry( client.cached_client().get_serde_with_retry(
@ -811,7 +811,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
client client
.cached_client() .cached_client()
.skip_cache_with_retry( .skip_cache_with_retry(
Self::request(DisplaySafeUrl::from(url.clone()), client)?, Self::request(url.clone(), client)?,
&cache_entry, &cache_entry,
cache_control, cache_control,
download, download,
@ -2146,7 +2146,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
&self, &self,
source: &BuildableSource<'_>, source: &BuildableSource<'_>,
ext: SourceDistExtension, ext: SourceDistExtension,
url: &Url, url: &DisplaySafeUrl,
index: Option<&IndexUrl>, index: Option<&IndexUrl>,
entry: &CacheEntry, entry: &CacheEntry,
revision: Revision, revision: Revision,
@ -2208,7 +2208,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
client client
.cached_client() .cached_client()
.skip_cache_with_retry( .skip_cache_with_retry(
Self::request(DisplaySafeUrl::from(url.clone()), client)?, Self::request(url.clone(), client)?,
&cache_entry, &cache_entry,
cache_control, cache_control,
download, download,

View File

@ -58,11 +58,8 @@ impl VerbatimUrl {
/// Parse a URL from a string. /// Parse a URL from a string.
pub fn parse_url(given: impl AsRef<str>) -> Result<Self, ParseError> { pub fn parse_url(given: impl AsRef<str>) -> Result<Self, ParseError> {
let url = Url::parse(given.as_ref())?; let url = DisplaySafeUrl::parse(given.as_ref())?;
Ok(Self { Ok(Self { url, given: None })
url: DisplaySafeUrl::from(url),
given: None,
})
} }
/// Convert a [`VerbatimUrl`] from a path or a URL. /// Convert a [`VerbatimUrl`] from a path or a URL.
@ -189,10 +186,8 @@ impl VerbatimUrl {
let (path, fragment) = split_fragment(path); let (path, fragment) = split_fragment(path);
// Convert to a URL. // Convert to a URL.
let mut url = DisplaySafeUrl::from( let mut url = DisplaySafeUrl::from_file_path(path.clone())
Url::from_file_path(path.clone()) .unwrap_or_else(|()| panic!("path is absolute: {}", path.display()));
.unwrap_or_else(|()| panic!("path is absolute: {}", path.display())),
);
// Set the fragment, if it exists. // Set the fragment, if it exists.
if let Some(fragment) = fragment { if let Some(fragment) = fragment {

View File

@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use uv_redacted::DisplaySafeUrl;
/// Metadata for a distribution that was installed via a direct URL. /// Metadata for a distribution that was installed via a direct URL.
/// ///
@ -92,7 +92,7 @@ impl std::fmt::Display for VcsKind {
} }
} }
impl TryFrom<&DirectUrl> for Url { impl TryFrom<&DirectUrl> for DisplaySafeUrl {
type Error = url::ParseError; type Error = url::ParseError;
fn try_from(value: &DirectUrl) -> Result<Self, Self::Error> { fn try_from(value: &DirectUrl) -> Result<Self, Self::Error> {
@ -126,9 +126,11 @@ impl TryFrom<&DirectUrl> for Url {
} => { } => {
let mut url = Self::parse(&format!("{}+{}", vcs_info.vcs, url))?; let mut url = Self::parse(&format!("{}+{}", vcs_info.vcs, url))?;
if let Some(commit_id) = &vcs_info.commit_id { if let Some(commit_id) = &vcs_info.commit_id {
url.set_path(&format!("{}@{commit_id}", url.path())); let path = format!("{}@{commit_id}", url.path());
url.set_path(&path);
} else if let Some(requested_revision) = &vcs_info.requested_revision { } else if let Some(requested_revision) = &vcs_info.requested_revision {
url.set_path(&format!("{}@{requested_revision}", url.path())); let path = format!("{}@{requested_revision}", url.path());
url.set_path(&path);
} }
if let Some(subdirectory) = subdirectory { if let Some(subdirectory) = subdirectory {
url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display()))); url.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));

View File

@ -77,7 +77,7 @@ pub enum Error {
#[error("Invalid download URL")] #[error("Invalid download URL")]
InvalidUrl(#[from] url::ParseError), InvalidUrl(#[from] url::ParseError),
#[error("Invalid download URL: {0}")] #[error("Invalid download URL: {0}")]
InvalidUrlFormat(Url), InvalidUrlFormat(DisplaySafeUrl),
#[error("Invalid path in file URL: `{0}`")] #[error("Invalid path in file URL: `{0}`")]
InvalidFileUrl(String), InvalidFileUrl(String),
#[error("Failed to create download directory")] #[error("Failed to create download directory")]
@ -109,7 +109,7 @@ pub enum Error {
#[error("An offline Python installation was requested, but {file} (from {url}) is missing in {}", python_builds_dir.user_display())] #[error("An offline Python installation was requested, but {file} (from {url}) is missing in {}", python_builds_dir.user_display())]
OfflinePythonMissing { OfflinePythonMissing {
file: Box<PythonInstallationKey>, file: Box<PythonInstallationKey>,
url: Box<Url>, url: Box<DisplaySafeUrl>,
python_builds_dir: PathBuf, python_builds_dir: PathBuf,
}, },
#[error(transparent)] #[error(transparent)]
@ -1198,7 +1198,7 @@ impl ManagedPythonDownload {
/// Download the managed Python archive into the cache directory. /// Download the managed Python archive into the cache directory.
async fn download_archive( async fn download_archive(
&self, &self,
url: &Url, url: &DisplaySafeUrl,
client: &BaseClient, client: &BaseClient,
reporter: Option<&dyn Reporter>, reporter: Option<&dyn Reporter>,
python_builds_dir: &Path, python_builds_dir: &Path,
@ -1300,7 +1300,7 @@ impl ManagedPythonDownload {
&self, &self,
python_install_mirror: Option<&str>, python_install_mirror: Option<&str>,
pypy_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>,
) -> Result<Url, Error> { ) -> Result<DisplaySafeUrl, Error> {
match self.key.implementation { match self.key.implementation {
LenientImplementationName::Known(ImplementationName::CPython) => { LenientImplementationName::Known(ImplementationName::CPython) => {
if let Some(mirror) = python_install_mirror { if let Some(mirror) = python_install_mirror {
@ -1312,7 +1312,7 @@ impl ManagedPythonDownload {
self.url.to_string(), self.url.to_string(),
)); ));
}; };
return Ok(Url::parse( return Ok(DisplaySafeUrl::parse(
format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(), format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
)?); )?);
} }
@ -1327,7 +1327,7 @@ impl ManagedPythonDownload {
self.url.to_string(), self.url.to_string(),
)); ));
}; };
return Ok(Url::parse( return Ok(DisplaySafeUrl::parse(
format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(), format!("{}/{}", mirror.trim_end_matches('/'), suffix).as_str(),
)?); )?);
} }
@ -1336,7 +1336,7 @@ impl ManagedPythonDownload {
_ => {} _ => {}
} }
Ok(Url::parse(&self.url)?) Ok(DisplaySafeUrl::parse(&self.url)?)
} }
} }
@ -1558,10 +1558,9 @@ where
/// Convert a [`Url`] into an [`AsyncRead`] stream. /// Convert a [`Url`] into an [`AsyncRead`] stream.
async fn read_url( async fn read_url(
url: &Url, url: &DisplaySafeUrl,
client: &BaseClient, client: &BaseClient,
) -> Result<(impl AsyncRead + Unpin, Option<u64>), Error> { ) -> Result<(impl AsyncRead + Unpin, Option<u64>), Error> {
let url = DisplaySafeUrl::from(url.clone());
if url.scheme() == "file" { if url.scheme() == "file" {
// Loads downloaded distribution from the given `file://` URL. // Loads downloaded distribution from the given `file://` URL.
let path = url let path = url
@ -1574,7 +1573,7 @@ async fn read_url(
Ok((Either::Left(reader), Some(size))) Ok((Either::Left(reader), Some(size)))
} else { } else {
let response = client let response = client
.for_host(&url) .for_host(url)
.get(Url::from(url.clone())) .get(Url::from(url.clone()))
.send() .send()
.await .await
@ -1588,7 +1587,7 @@ async fn read_url(
// Check the status code. // Check the status code.
let response = response let response = response
.error_for_status() .error_for_status()
.map_err(|err| Error::from_reqwest(url, err, retry_count))?; .map_err(|err| Error::from_reqwest(url.clone(), err, retry_count))?;
let size = response.content_length(); let size = response.content_length();
let stream = response let stream = response

View File

@ -60,7 +60,7 @@ impl DisplaySafeUrl {
/// Parse a string as an URL, with this URL as the base URL. /// Parse a string as an URL, with this URL as the base URL.
#[inline] #[inline]
pub fn join(&self, input: &str) -> Result<Self, url::ParseError> { pub fn join(&self, input: &str) -> Result<Self, url::ParseError> {
self.0.join(input).map(Self::from) self.0.join(input).map(Self)
} }
/// Serialize with Serde using the internal representation of the `Url` struct. /// Serialize with Serde using the internal representation of the `Url` struct.
@ -78,7 +78,7 @@ impl DisplaySafeUrl {
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
Url::deserialize_internal(deserializer).map(Self::from) Url::deserialize_internal(deserializer).map(Self)
} }
#[allow(clippy::result_unit_err)] #[allow(clippy::result_unit_err)]
@ -250,8 +250,8 @@ mod tests {
#[test] #[test]
fn from_url_no_credentials() { fn from_url_no_credentials() {
let url_str = "https://pypi-proxy.fly.dev/basic-auth/simple"; let url_str = "https://pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrl::from(url); let log_safe_url = url;
assert_eq!(log_safe_url.username(), ""); assert_eq!(log_safe_url.username(), "");
assert!(log_safe_url.password().is_none()); assert!(log_safe_url.password().is_none());
assert_eq!(log_safe_url.to_string(), url_str); assert_eq!(log_safe_url.to_string(), url_str);
@ -260,8 +260,8 @@ mod tests {
#[test] #[test]
fn from_url_username_and_password() { fn from_url_username_and_password() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple"; let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrl::from(url); let log_safe_url = url;
assert_eq!(log_safe_url.username(), "user"); assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_some_and(|p| p == "pass")); assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!( assert_eq!(
@ -273,8 +273,8 @@ mod tests {
#[test] #[test]
fn from_url_just_password() { fn from_url_just_password() {
let url_str = "https://:pass@pypi-proxy.fly.dev/basic-auth/simple"; let url_str = "https://:pass@pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrl::from(url); let log_safe_url = url;
assert_eq!(log_safe_url.username(), ""); assert_eq!(log_safe_url.username(), "");
assert!(log_safe_url.password().is_some_and(|p| p == "pass")); assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!( assert_eq!(
@ -286,8 +286,8 @@ mod tests {
#[test] #[test]
fn from_url_just_username() { fn from_url_just_username() {
let url_str = "https://user@pypi-proxy.fly.dev/basic-auth/simple"; let url_str = "https://user@pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrl::from(url); let log_safe_url = url;
assert_eq!(log_safe_url.username(), "user"); assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_none()); assert!(log_safe_url.password().is_none());
assert_eq!( assert_eq!(
@ -374,7 +374,7 @@ mod tests {
#[test] #[test]
fn log_safe_url_ref() { fn log_safe_url_ref() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple"; let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap(); let url = DisplaySafeUrl::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrl::ref_cast(&url); let log_safe_url = DisplaySafeUrl::ref_cast(&url);
assert_eq!(log_safe_url.username(), "user"); assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_some_and(|p| p == "pass")); assert!(log_safe_url.password().is_some_and(|p| p == "pass"));

View File

@ -11,7 +11,6 @@ use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap}; use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::{debug, warn}; use tracing::{debug, warn};
use url::Url;
use uv_cache::Cache; use uv_cache::Cache;
use uv_cache_key::RepositoryUrl; use uv_cache_key::RepositoryUrl;
@ -1104,8 +1103,7 @@ async fn lock_and_sync(
// Invalidate the project metadata. // Invalidate the project metadata.
if let AddTarget::Project(VirtualProject::Project(ref project), _) = target { if let AddTarget::Project(VirtualProject::Project(ref project), _) = target {
let url = Url::from_file_path(project.project_root()) let url = DisplaySafeUrl::from_file_path(project.project_root())
.map(DisplaySafeUrl::from)
.expect("project root is a valid URL"); .expect("project root is a valid URL");
let version_id = VersionId::from_url(&url); let version_id = VersionId::from_url(&url);
let existing = lock_state.index().distributions().remove(&version_id); let existing = lock_state.index().distributions().remove(&version_id);