Add DisplaySafeUrl newtype to prevent leaking of credentials by default (#13560)

Prior to this PR, there were numerous places where uv would leak
credentials in logs. We had a way to mask credentials by calling methods
or a recently-added `redact_url` function, but this was not secure by
default. There were a number of other types (like `GitUrl`) that would
leak credentials on display.

This PR adds a `DisplaySafeUrl` newtype to prevent leaking credentials
when logging by default. It takes a maximalist approach, replacing the
use of `Url` almost everywhere. This includes when first parsing config
files, when storing URLs in types like `GitUrl`, and also when storing
URLs in types that in practice will never contain credentials (like
`DirectorySourceUrl`). The idea is to make it easy for developers to do
the right thing and for the compiler to support this (and to minimize
ever having to manually convert back and forth). Displaying credentials
now requires an active step. Note that despite this maximalist approach,
the use of the newtype should be zero cost.

One conspicuous place this PR does not use `DisplaySafeUrl` is in the
`uv-auth` crate. That would require new clones since there are calls to
`request.url()` that return a `&Url`. One option would have been to make
`DisplaySafeUrl` wrap a `Cow`, but this would lead to lifetime
annotations all over the codebase. I've created a separate PR based on
this one (#13576) that updates `uv-auth` to use `DisplaySafeUrl` with
one new clone. We can discuss the tradeoffs there.

Most of this PR just replaces `Url` with `DisplaySafeUrl`. The core is
`uv_redacted/lib.rs`, where the newtype is implemented. To make it
easier to review the rest, here are some points of note:

* `DisplaySafeUrl` has a `Display` implementation that masks
credentials. Currently, it will still display the username when there is
both a username and password. If we think is the wrong choice, it can
now be changed in one place.
* `DisplaySafeUrl` has a `remove_credentials()` method and also a
`.to_string_with_credentials()` method. This allows us to use it in a
variety of scenarios.
* `IndexUrl::redacted()` was renamed to
`IndexUrl::removed_credentials()` to make it clearer that we are not
masking.
* We convert from a `DisplaySafeUrl` to a `Url` when calling `reqwest`
methods like `.get()` and `.head()`.
* We convert from a `DisplaySafeUrl` to a `Url` when creating a
`uv_auth::Index`. That is because, as mentioned above, I will be
updating the `uv_auth` crate to use this newtype in a separate PR.
* A number of tests (e.g., in `pip_install.rs`) that formerly used
filters to mask tokens in the test output no longer need those filters
since tokens in URLs are now masked automatically.
* The one place we are still knowingly writing credentials to
`pyproject.toml` is when a URL with credentials is passed to `uv add`
with `--raw`. Since displaying credentials is no longer automatic, I
have added a `to_string_with_credentials()` method to the `Pep508Url`
trait. This is used when `--raw` is passed. Adding it to that trait is a
bit weird, but it's the simplest way to achieve the goal. I'm open to
suggestions on how to improve this, but note that because of the way
we're using generic bounds, it's not as simple as just creating a
separate trait for that method.
This commit is contained in:
John Mumm 2025-05-27 00:05:30 +02:00 committed by GitHub
parent b80cafd5e8
commit c19a294a48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 1266 additions and 2249 deletions

24
Cargo.lock generated
View file

@ -4671,9 +4671,9 @@ dependencies = [
"test-log", "test-log",
"tokio", "tokio",
"tracing", "tracing",
"tracing-test",
"url", "url",
"uv-once-map", "uv-once-map",
"uv-redacted",
"uv-small-str", "uv-small-str",
"uv-static", "uv-static",
"uv-warnings", "uv-warnings",
@ -4801,7 +4801,6 @@ dependencies = [
"serde", "serde",
"tempfile", "tempfile",
"tracing", "tracing",
"url",
"uv-cache-info", "uv-cache-info",
"uv-cache-key", "uv-cache-key",
"uv-dirs", "uv-dirs",
@ -4809,6 +4808,7 @@ dependencies = [
"uv-fs", "uv-fs",
"uv-normalize", "uv-normalize",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-static", "uv-static",
"walkdir", "walkdir",
] ]
@ -4836,6 +4836,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"seahash", "seahash",
"url", "url",
"uv-redacted",
] ]
[[package]] [[package]]
@ -4858,6 +4859,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-python", "uv-python",
"uv-redacted",
"uv-resolver", "uv-resolver",
"uv-settings", "uv-settings",
"uv-static", "uv-static",
@ -5087,6 +5089,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-platform-tags", "uv-platform-tags",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-types", "uv-types",
"uv-workspace", "uv-workspace",
"walkdir", "walkdir",
@ -5144,6 +5147,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-platform-tags", "uv-platform-tags",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-small-str", "uv-small-str",
"version-ranges", "version-ranges",
] ]
@ -5318,6 +5322,7 @@ dependencies = [
"uv-platform-tags", "uv-platform-tags",
"uv-pypi-types", "uv-pypi-types",
"uv-python", "uv-python",
"uv-redacted",
"uv-static", "uv-static",
"uv-types", "uv-types",
"uv-warnings", "uv-warnings",
@ -5413,6 +5418,7 @@ dependencies = [
"uv-fs", "uv-fs",
"uv-normalize", "uv-normalize",
"uv-pep440", "uv-pep440",
"uv-redacted",
"version-ranges", "version-ranges",
] ]
@ -5470,6 +5476,7 @@ dependencies = [
"uv-fs", "uv-fs",
"uv-metadata", "uv-metadata",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-static", "uv-static",
"uv-warnings", "uv-warnings",
] ]
@ -5501,6 +5508,7 @@ dependencies = [
"uv-normalize", "uv-normalize",
"uv-pep440", "uv-pep440",
"uv-pep508", "uv-pep508",
"uv-redacted",
"uv-small-str", "uv-small-str",
] ]
@ -5554,6 +5562,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-platform-tags", "uv-platform-tags",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-state", "uv-state",
"uv-static", "uv-static",
"uv-trampoline-builder", "uv-trampoline-builder",
@ -5568,6 +5577,8 @@ dependencies = [
name = "uv-redacted" name = "uv-redacted"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"schemars",
"serde",
"url", "url",
] ]
@ -5598,6 +5609,7 @@ dependencies = [
"uv-normalize", "uv-normalize",
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-requirements-txt", "uv-requirements-txt",
"uv-resolver", "uv-resolver",
"uv-types", "uv-types",
@ -5633,6 +5645,7 @@ dependencies = [
"uv-normalize", "uv-normalize",
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-warnings", "uv-warnings",
] ]
@ -5684,6 +5697,7 @@ dependencies = [
"uv-platform-tags", "uv-platform-tags",
"uv-pypi-types", "uv-pypi-types",
"uv-python", "uv-python",
"uv-redacted",
"uv-requirements-txt", "uv-requirements-txt",
"uv-small-str", "uv-small-str",
"uv-static", "uv-static",
@ -5707,6 +5721,7 @@ dependencies = [
"uv-pep440", "uv-pep440",
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-settings", "uv-settings",
"uv-workspace", "uv-workspace",
] ]
@ -5736,6 +5751,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-python", "uv-python",
"uv-redacted",
"uv-resolver", "uv-resolver",
"uv-static", "uv-static",
"uv-torch", "uv-torch",
@ -5851,7 +5867,6 @@ dependencies = [
"anyhow", "anyhow",
"rustc-hash", "rustc-hash",
"thiserror 2.0.12", "thiserror 2.0.12",
"url",
"uv-cache", "uv-cache",
"uv-configuration", "uv-configuration",
"uv-distribution-filename", "uv-distribution-filename",
@ -5863,6 +5878,7 @@ dependencies = [
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-python", "uv-python",
"uv-redacted",
"uv-workspace", "uv-workspace",
] ]
@ -5917,7 +5933,6 @@ dependencies = [
"toml", "toml",
"toml_edit", "toml_edit",
"tracing", "tracing",
"url",
"uv-build-backend", "uv-build-backend",
"uv-cache-key", "uv-cache-key",
"uv-distribution-types", "uv-distribution-types",
@ -5929,6 +5944,7 @@ dependencies = [
"uv-pep440", "uv-pep440",
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-redacted",
"uv-static", "uv-static",
"uv-warnings", "uv-warnings",
] ]

View file

@ -11,6 +11,7 @@ workspace = true
[dependencies] [dependencies]
uv-once-map = { workspace = true } uv-once-map = { workspace = true }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true } uv-small-str = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
@ -36,5 +37,4 @@ insta = { version = "1.40.0" }
tempfile = { workspace = true } tempfile = { workspace = true }
test-log = { version = "0.2.16", features = ["trace"], default-features = false } test-log = { version = "0.2.16", features = ["trace"], default-features = false }
tokio = { workspace = true } tokio = { workspace = true }
tracing-test = { workspace = true }
wiremock = { workspace = true } wiremock = { workspace = true }

View file

@ -9,6 +9,7 @@ use tracing::trace;
use url::Url; use url::Url;
use uv_once_map::OnceMap; use uv_once_map::OnceMap;
use uv_redacted::DisplaySafeUrl;
use crate::Realm; use crate::Realm;
use crate::credentials::{Credentials, Username}; use crate::credentials::{Credentials, Username};
@ -18,7 +19,7 @@ type FxOnceMap<K, V> = OnceMap<K, V, BuildHasherDefault<FxHasher>>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum FetchUrl { pub(crate) enum FetchUrl {
/// A full index URL /// A full index URL
Index(Url), Index(DisplaySafeUrl),
/// A realm URL /// A realm URL
Realm(Realm), Realm(Realm),
} }

View file

@ -3,6 +3,8 @@ use base64::read::DecoderReader;
use base64::write::EncoderWriter; use base64::write::EncoderWriter;
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt; use std::fmt;
use uv_redacted::DisplaySafeUrl;
use uv_redacted::DisplaySafeUrlRef;
use netrc::Netrc; use netrc::Netrc;
use reqwest::Request; use reqwest::Request;
@ -141,7 +143,11 @@ impl Credentials {
/// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any. /// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any.
/// ///
/// If a username is provided, it must match the login in the netrc file or [`None`] is returned. /// If a username is provided, it must match the login in the netrc file or [`None`] is returned.
pub(crate) fn from_netrc(netrc: &Netrc, url: &Url, username: Option<&str>) -> Option<Self> { pub(crate) fn from_netrc(
netrc: &Netrc,
url: &DisplaySafeUrlRef<'_>,
username: Option<&str>,
) -> Option<Self> {
let host = url.host_str()?; let host = url.host_str()?;
let entry = netrc let entry = netrc
.hosts .hosts
@ -299,7 +305,7 @@ impl Credentials {
/// ///
/// Any existing credentials will be overridden. /// Any existing credentials will be overridden.
#[must_use] #[must_use]
pub fn apply(&self, mut url: Url) -> Url { pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
if let Some(username) = self.username() { if let Some(username) = self.username() {
let _ = url.set_username(username); let _ = url.set_username(username);
} }

View file

@ -2,6 +2,7 @@ use std::fmt::{self, Display, Formatter};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use url::Url; use url::Url;
use uv_redacted::DisplaySafeUrl;
/// When to use authentication. /// When to use authentication.
#[derive( #[derive(
@ -53,10 +54,10 @@ impl Display for AuthPolicy {
// could potentially make sense for a future refactor. // could potentially make sense for a future refactor.
#[derive(Debug, Clone, Hash, Eq, PartialEq)] #[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct Index { pub struct Index {
pub url: Url, pub url: DisplaySafeUrl,
/// The root endpoint where authentication is applied. /// The root endpoint where authentication is applied.
/// For PEP 503 endpoints, this excludes `/simple`. /// For PEP 503 endpoints, this excludes `/simple`.
pub root_url: Url, pub root_url: DisplaySafeUrl,
pub auth_policy: AuthPolicy, pub auth_policy: AuthPolicy,
} }
@ -95,7 +96,7 @@ impl Indexes {
} }
/// Get the index URL prefix for a URL if one exists. /// Get the index URL prefix for a URL if one exists.
pub fn index_url_for(&self, url: &Url) -> Option<&Url> { pub fn index_url_for(&self, url: &Url) -> Option<&DisplaySafeUrl> {
self.find_prefix_index(url).map(|index| &index.url) self.find_prefix_index(url).map(|index| &index.url)
} }

View file

@ -1,7 +1,7 @@
use std::{io::Write, process::Stdio}; use std::{io::Write, process::Stdio};
use tokio::process::Command; use tokio::process::Command;
use tracing::{instrument, trace, warn}; use tracing::{instrument, trace, warn};
use url::Url; use uv_redacted::DisplaySafeUrlRef;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::credentials::Credentials; use crate::credentials::Credentials;
@ -36,7 +36,11 @@ impl KeyringProvider {
/// Returns [`None`] if no password was found for the username or if any errors /// Returns [`None`] if no password was found for the username or if any errors
/// are encountered in the keyring backend. /// are encountered in the keyring backend.
#[instrument(skip_all, fields(url = % url.to_string(), username))] #[instrument(skip_all, fields(url = % url.to_string(), username))]
pub async fn fetch(&self, url: &Url, username: Option<&str>) -> Option<Credentials> { pub async fn fetch(
&self,
url: &DisplaySafeUrlRef<'_>,
username: Option<&str>,
) -> Option<Credentials> {
// Validate the request // Validate the request
debug_assert!( debug_assert!(
url.host_str().is_some(), url.host_str().is_some(),
@ -217,15 +221,18 @@ impl KeyringProvider {
mod tests { mod tests {
use super::*; use super::*;
use futures::FutureExt; use futures::FutureExt;
use url::Url;
#[tokio::test] #[tokio::test]
async fn fetch_url_no_host() { async fn fetch_url_no_host() {
let url = Url::parse("file:/etc/bin/").unwrap(); let url = Url::parse("file:/etc/bin/").unwrap();
let keyring = KeyringProvider::empty(); let keyring = KeyringProvider::empty();
// Panics due to debug assertion; returns `None` in production // Panics due to debug assertion; returns `None` in production
let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, Some("user"))) let result = std::panic::AssertUnwindSafe(
.catch_unwind() keyring.fetch(&DisplaySafeUrlRef::from(&url), Some("user")),
.await; )
.catch_unwind()
.await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -234,9 +241,11 @@ mod tests {
let url = Url::parse("https://user:password@example.com").unwrap(); let url = Url::parse("https://user:password@example.com").unwrap();
let keyring = KeyringProvider::empty(); let keyring = KeyringProvider::empty();
// Panics due to debug assertion; returns `None` in production // Panics due to debug assertion; returns `None` in production
let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, Some(url.username()))) let result = std::panic::AssertUnwindSafe(
.catch_unwind() keyring.fetch(&DisplaySafeUrlRef::from(&url), Some(url.username())),
.await; )
.catch_unwind()
.await;
assert!(result.is_err()); assert!(result.is_err());
} }
@ -245,15 +254,18 @@ mod tests {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::empty(); let keyring = KeyringProvider::empty();
// Panics due to debug assertion; returns `None` in production // Panics due to debug assertion; returns `None` in production
let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, Some(url.username()))) let result = std::panic::AssertUnwindSafe(
.catch_unwind() keyring.fetch(&DisplaySafeUrlRef::from(&url), Some(url.username())),
.await; )
.catch_unwind()
.await;
assert!(result.is_err()); assert!(result.is_err());
} }
#[tokio::test] #[tokio::test]
async fn fetch_url_no_auth() { async fn fetch_url_no_auth() {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let url = DisplaySafeUrlRef::from(&url);
let keyring = KeyringProvider::empty(); let keyring = KeyringProvider::empty();
let credentials = keyring.fetch(&url, Some("user")); let credentials = keyring.fetch(&url, Some("user"));
assert!(credentials.await.is_none()); assert!(credentials.await.is_none());
@ -264,7 +276,9 @@ mod tests {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]);
assert_eq!( assert_eq!(
keyring.fetch(&url, Some("user")).await, keyring
.fetch(&DisplaySafeUrlRef::from(&url), Some("user"))
.await,
Some(Credentials::basic( Some(Credentials::basic(
Some("user".to_string()), Some("user".to_string()),
Some("password".to_string()) Some("password".to_string())
@ -272,7 +286,10 @@ mod tests {
); );
assert_eq!( assert_eq!(
keyring keyring
.fetch(&url.join("test").unwrap(), Some("user")) .fetch(
&DisplaySafeUrlRef::from(&url.join("test").unwrap()),
Some("user")
)
.await, .await,
Some(Credentials::basic( Some(Credentials::basic(
Some("user".to_string()), Some("user".to_string()),
@ -285,7 +302,9 @@ mod tests {
async fn fetch_url_no_match() { async fn fetch_url_no_match() {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([("other.com", "user", "password")]); let keyring = KeyringProvider::dummy([("other.com", "user", "password")]);
let credentials = keyring.fetch(&url, Some("user")).await; let credentials = keyring
.fetch(&DisplaySafeUrlRef::from(&url), Some("user"))
.await;
assert_eq!(credentials, None); assert_eq!(credentials, None);
} }
@ -297,21 +316,33 @@ mod tests {
(url.host_str().unwrap(), "user", "other-password"), (url.host_str().unwrap(), "user", "other-password"),
]); ]);
assert_eq!( assert_eq!(
keyring.fetch(&url.join("foo").unwrap(), Some("user")).await, keyring
.fetch(
&DisplaySafeUrlRef::from(&url.join("foo").unwrap()),
Some("user")
)
.await,
Some(Credentials::basic( Some(Credentials::basic(
Some("user".to_string()), Some("user".to_string()),
Some("password".to_string()) Some("password".to_string())
)) ))
); );
assert_eq!( assert_eq!(
keyring.fetch(&url, Some("user")).await, keyring
.fetch(&DisplaySafeUrlRef::from(&url), Some("user"))
.await,
Some(Credentials::basic( Some(Credentials::basic(
Some("user".to_string()), Some("user".to_string()),
Some("other-password".to_string()) Some("other-password".to_string())
)) ))
); );
assert_eq!( assert_eq!(
keyring.fetch(&url.join("bar").unwrap(), Some("user")).await, keyring
.fetch(
&DisplaySafeUrlRef::from(&url.join("bar").unwrap()),
Some("user")
)
.await,
Some(Credentials::basic( Some(Credentials::basic(
Some("user".to_string()), Some("user".to_string()),
Some("other-password".to_string()) Some("other-password".to_string())
@ -323,7 +354,9 @@ mod tests {
async fn fetch_url_username() { async fn fetch_url_username() {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]);
let credentials = keyring.fetch(&url, Some("user")).await; let credentials = keyring
.fetch(&DisplaySafeUrlRef::from(&url), Some("user"))
.await;
assert_eq!( assert_eq!(
credentials, credentials,
Some(Credentials::basic( Some(Credentials::basic(
@ -337,7 +370,7 @@ mod tests {
async fn fetch_url_no_username() { async fn fetch_url_no_username() {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]);
let credentials = keyring.fetch(&url, None).await; let credentials = keyring.fetch(&DisplaySafeUrlRef::from(&url), None).await;
assert_eq!( assert_eq!(
credentials, credentials,
Some(Credentials::basic( Some(Credentials::basic(
@ -351,12 +384,16 @@ mod tests {
async fn fetch_url_username_no_match() { async fn fetch_url_username_no_match() {
let url = Url::parse("https://example.com").unwrap(); let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "foo", "password")]); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "foo", "password")]);
let credentials = keyring.fetch(&url, Some("bar")).await; let credentials = keyring
.fetch(&DisplaySafeUrlRef::from(&url), Some("bar"))
.await;
assert_eq!(credentials, None); assert_eq!(credentials, None);
// Still fails if we have `foo` in the URL itself // Still fails if we have `foo` in the URL itself
let url = Url::parse("https://foo@example.com").unwrap(); let url = Url::parse("https://foo@example.com").unwrap();
let credentials = keyring.fetch(&url, Some("bar")).await; let credentials = keyring
.fetch(&DisplaySafeUrlRef::from(&url), Some("bar"))
.await;
assert_eq!(credentials, None); assert_eq!(credentials, None);
} }
} }

View file

@ -1,7 +1,6 @@
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use tracing::trace; use tracing::trace;
use url::Url;
use cache::CredentialsCache; use cache::CredentialsCache;
pub use credentials::Credentials; pub use credentials::Credentials;
@ -9,6 +8,7 @@ pub use index::{AuthPolicy, Index, Indexes};
pub use keyring::KeyringProvider; pub use keyring::KeyringProvider;
pub use middleware::AuthMiddleware; pub use middleware::AuthMiddleware;
use realm::Realm; use realm::Realm;
use uv_redacted::DisplaySafeUrl;
mod cache; mod cache;
mod credentials; mod credentials;
@ -28,7 +28,7 @@ pub(crate) static CREDENTIALS_CACHE: LazyLock<CredentialsCache> =
/// Populate the global authentication store with credentials on a URL, if there are any. /// Populate the global authentication store with credentials on a URL, if there are any.
/// ///
/// Returns `true` if the store was updated. /// Returns `true` if the store was updated.
pub fn store_credentials_from_url(url: &Url) -> bool { pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool {
if let Some(credentials) = Credentials::from_url(url) { if let Some(credentials) = Credentials::from_url(url) {
trace!("Caching credentials for {url}"); trace!("Caching credentials for {url}");
CREDENTIALS_CACHE.insert(url, Arc::new(credentials)); CREDENTIALS_CACHE.insert(url, Arc::new(credentials));
@ -41,7 +41,7 @@ pub fn store_credentials_from_url(url: &Url) -> bool {
/// Populate the global authentication store with credentials on a URL, if there are any. /// Populate the global authentication store with credentials on a URL, if there are any.
/// ///
/// Returns `true` if the store was updated. /// Returns `true` if the store was updated.
pub fn store_credentials(url: &Url, credentials: Arc<Credentials>) { pub fn store_credentials(url: &DisplaySafeUrl, credentials: Arc<Credentials>) {
trace!("Caching credentials for {url}"); trace!("Caching credentials for {url}");
CREDENTIALS_CACHE.insert(url, credentials); CREDENTIALS_CACHE.insert(url, credentials);
} }

View file

@ -1,7 +1,7 @@
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use http::{Extensions, StatusCode}; use http::{Extensions, StatusCode};
use url::Url; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlRef};
use crate::{ use crate::{
CREDENTIALS_CACHE, CredentialsCache, KeyringProvider, CREDENTIALS_CACHE, CredentialsCache, KeyringProvider,
@ -274,6 +274,7 @@ impl Middleware for AuthMiddleware {
trace!("Checking for credentials for {url}"); trace!("Checking for credentials for {url}");
(request, None) (request, None)
}; };
let retry_request_url = DisplaySafeUrlRef::from(retry_request.url());
let username = credentials let username = credentials
.as_ref() .as_ref()
@ -282,13 +283,13 @@ impl Middleware for AuthMiddleware {
let credentials = if let Some(index_url) = maybe_index_url { let credentials = if let Some(index_url) = maybe_index_url {
self.cache().get_url(index_url, &username).or_else(|| { self.cache().get_url(index_url, &username).or_else(|| {
self.cache() self.cache()
.get_realm(Realm::from(retry_request.url()), username) .get_realm(Realm::from(&*retry_request_url), username)
}) })
} else { } else {
// Since there is no known index for this URL, check if there are credentials in // Since there is no known index for this URL, check if there are credentials in
// the realm-level cache. // the realm-level cache.
self.cache() self.cache()
.get_realm(Realm::from(retry_request.url()), username) .get_realm(Realm::from(&*retry_request_url), username)
} }
.or(credentials); .or(credentials);
@ -307,7 +308,7 @@ impl Middleware for AuthMiddleware {
if let Some(credentials) = self if let Some(credentials) = self
.fetch_credentials( .fetch_credentials(
credentials.as_deref(), credentials.as_deref(),
retry_request.url(), retry_request_url,
maybe_index_url, maybe_index_url,
auth_policy, auth_policy,
) )
@ -362,7 +363,7 @@ impl AuthMiddleware {
// Nothing to insert into the cache if we don't have credentials // Nothing to insert into the cache if we don't have credentials
return next.run(request, extensions).await; return next.run(request, extensions).await;
}; };
let url = request.url().clone(); let url = DisplaySafeUrl::from(request.url().clone());
if matches!(auth_policy, AuthPolicy::Always) && credentials.password().is_none() { if matches!(auth_policy, AuthPolicy::Always) && credentials.password().is_none() {
return Err(Error::Middleware(format_err!("Missing password for {url}"))); return Err(Error::Middleware(format_err!("Missing password for {url}")));
} }
@ -387,8 +388,8 @@ impl AuthMiddleware {
mut request: Request, mut request: Request,
extensions: &mut Extensions, extensions: &mut Extensions,
next: Next<'_>, next: Next<'_>,
url: &str, url: &DisplaySafeUrl,
index_url: Option<&Url>, index_url: Option<&DisplaySafeUrl>,
auth_policy: AuthPolicy, auth_policy: AuthPolicy,
) -> reqwest_middleware::Result<Response> { ) -> reqwest_middleware::Result<Response> {
let credentials = Arc::new(credentials); let credentials = Arc::new(credentials);
@ -430,7 +431,12 @@ impl AuthMiddleware {
// Do not insert already-cached credentials // Do not insert already-cached credentials
None None
} else if let Some(credentials) = self } else if let Some(credentials) = self
.fetch_credentials(Some(&credentials), request.url(), index_url, auth_policy) .fetch_credentials(
Some(&credentials),
DisplaySafeUrlRef::from(request.url()),
index_url,
auth_policy,
)
.await .await
{ {
request = credentials.authenticate(request); request = credentials.authenticate(request);
@ -462,8 +468,8 @@ impl AuthMiddleware {
async fn fetch_credentials( async fn fetch_credentials(
&self, &self,
credentials: Option<&Credentials>, credentials: Option<&Credentials>,
url: &Url, url: DisplaySafeUrlRef<'_>,
maybe_index_url: Option<&Url>, maybe_index_url: Option<&DisplaySafeUrl>,
auth_policy: AuthPolicy, auth_policy: AuthPolicy,
) -> Option<Arc<Credentials>> { ) -> Option<Arc<Credentials>> {
let username = Username::from( let username = Username::from(
@ -475,7 +481,7 @@ impl AuthMiddleware {
let key = if let Some(index_url) = maybe_index_url { let key = if let Some(index_url) = maybe_index_url {
(FetchUrl::Index(index_url.clone()), username) (FetchUrl::Index(index_url.clone()), username)
} else { } else {
(FetchUrl::Realm(Realm::from(url)), username) (FetchUrl::Realm(Realm::from(&*url)), username)
}; };
if !self.cache().fetches.register(key.clone()) { if !self.cache().fetches.register(key.clone()) {
let credentials = self let credentials = self
@ -502,7 +508,7 @@ impl AuthMiddleware {
debug!("Checking netrc for credentials for {url}"); debug!("Checking netrc for credentials for {url}");
Credentials::from_netrc( Credentials::from_netrc(
netrc, netrc,
url, &url,
credentials credentials
.as_ref() .as_ref()
.and_then(|credentials| credentials.username()), .and_then(|credentials| credentials.username()),
@ -523,17 +529,17 @@ impl AuthMiddleware {
if let Some(username) = credentials.and_then(|credentials| credentials.username()) { if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
if let Some(index_url) = maybe_index_url { if let Some(index_url) = maybe_index_url {
debug!("Checking keyring for credentials for index URL {}@{}", username, index_url); debug!("Checking keyring for credentials for index URL {}@{}", username, index_url);
keyring.fetch(index_url, Some(username)).await keyring.fetch(&DisplaySafeUrlRef::from(index_url), Some(username)).await
} else { } else {
debug!("Checking keyring for credentials for full URL {}@{}", username, url); debug!("Checking keyring for credentials for full URL {}@{}", username, url);
keyring.fetch(url, Some(username)).await keyring.fetch(&url, Some(username)).await
} }
} else if matches!(auth_policy, AuthPolicy::Always) { } else if matches!(auth_policy, AuthPolicy::Always) {
if let Some(index_url) = maybe_index_url { if let Some(index_url) = maybe_index_url {
debug!( debug!(
"Checking keyring for credentials for index URL {index_url} without username due to `authenticate = always`" "Checking keyring for credentials for index URL {index_url} without username due to `authenticate = always`"
); );
keyring.fetch(index_url, None).await keyring.fetch(&DisplaySafeUrlRef::from(index_url), None).await
} else { } else {
None None
} }
@ -558,24 +564,17 @@ impl AuthMiddleware {
} }
} }
fn tracing_url(request: &Request, credentials: Option<&Credentials>) -> String { fn tracing_url(request: &Request, credentials: Option<&Credentials>) -> DisplaySafeUrl {
if !tracing::enabled!(tracing::Level::DEBUG) { let mut url = DisplaySafeUrl::from(request.url().clone());
return request.url().to_string();
}
let mut url = request.url().clone();
if let Some(creds) = credentials { if let Some(creds) = credentials {
if creds.password().is_some() { if let Some(username) = creds.username() {
if let Some(username) = creds.username() { let _ = url.set_username(username);
let _ = url.set_username(username); }
} if let Some(password) = creds.password() {
let _ = url.set_password(Some("****")); let _ = url.set_password(Some(password));
// A username on its own might be a secret token.
} else if creds.username().is_some() {
let _ = url.set_username("****");
} }
} }
url.to_string() url
} }
#[cfg(test)] #[cfg(test)]
@ -1749,13 +1748,13 @@ mod tests {
let base_url_2 = base_url.join("prefix_2")?; let base_url_2 = base_url.join("prefix_2")?;
let indexes = Indexes::from_indexes(vec![ let indexes = Indexes::from_indexes(vec![
Index { Index {
url: base_url_1.clone(), url: DisplaySafeUrl::from(base_url_1.clone()),
root_url: base_url_1.clone(), root_url: DisplaySafeUrl::from(base_url_1.clone()),
auth_policy: AuthPolicy::Auto, auth_policy: AuthPolicy::Auto,
}, },
Index { Index {
url: base_url_2.clone(), url: DisplaySafeUrl::from(base_url_2.clone()),
root_url: base_url_2.clone(), root_url: DisplaySafeUrl::from(base_url_2.clone()),
auth_policy: AuthPolicy::Auto, auth_policy: AuthPolicy::Auto,
}, },
]); ]);
@ -1857,8 +1856,8 @@ mod tests {
let base_url = Url::parse(&server.uri())?; let base_url = Url::parse(&server.uri())?;
let index_url = base_url.join("prefix_1")?; let index_url = base_url.join("prefix_1")?;
let indexes = Indexes::from_indexes(vec![Index { let indexes = Indexes::from_indexes(vec![Index {
url: index_url.clone(), url: DisplaySafeUrl::from(index_url.clone()),
root_url: index_url.clone(), root_url: DisplaySafeUrl::from(index_url.clone()),
auth_policy: AuthPolicy::Auto, auth_policy: AuthPolicy::Auto,
}]); }]);
@ -1912,7 +1911,7 @@ mod tests {
} }
fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes { fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
let mut url = url.clone(); let mut url = DisplaySafeUrl::from(url.clone());
url.set_password(None).ok(); url.set_password(None).ok();
url.set_username("").ok(); url.set_username("").ok();
Indexes::from_indexes(vec![Index { Indexes::from_indexes(vec![Index {
@ -2104,16 +2103,14 @@ mod tests {
} }
#[test] #[test]
#[tracing_test::traced_test(level = "debug")]
fn test_tracing_url() { fn test_tracing_url() {
// No credentials // No credentials
let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
assert_eq!( assert_eq!(
tracing_url(&req, None), tracing_url(&req, None),
"https://pypi-proxy.fly.dev/basic-auth/simple" DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
); );
// Mask username if there is a username but no password
let creds = Credentials::Basic { let creds = Credentials::Basic {
username: Username::new(Some(String::from("user"))), username: Username::new(Some(String::from("user"))),
password: None, password: None,
@ -2121,10 +2118,9 @@ mod tests {
let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
assert_eq!( assert_eq!(
tracing_url(&req, Some(&creds)), tracing_url(&req, Some(&creds)),
"https://****@pypi-proxy.fly.dev/basic-auth/simple" DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
); );
// Log username but mask password if a password is present
let creds = Credentials::Basic { let creds = Credentials::Basic {
username: Username::new(Some(String::from("user"))), username: Username::new(Some(String::from("user"))),
password: Some(Password::new(String::from("password"))), password: Some(Password::new(String::from("password"))),
@ -2132,7 +2128,8 @@ mod tests {
let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
assert_eq!( assert_eq!(
tracing_url(&req, Some(&creds)), tracing_url(&req, Some(&creds)),
"https://user:****@pypi-proxy.fly.dev/basic-auth/simple" DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
.unwrap()
); );
} }

View file

@ -17,6 +17,8 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
uv-redacted = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
memchr = { workspace = true } memchr = { workspace = true }
percent-encoding = { workspace = true } percent-encoding = { workspace = true }

View file

@ -4,6 +4,7 @@ use std::hash::{Hash, Hasher};
use std::ops::Deref; use std::ops::Deref;
use url::Url; use url::Url;
use uv_redacted::DisplaySafeUrl;
use crate::cache_key::{CacheKey, CacheKeyHasher}; use crate::cache_key::{CacheKey, CacheKeyHasher};
@ -16,10 +17,10 @@ use crate::cache_key::{CacheKey, CacheKeyHasher};
/// string value of the `Url` it contains. This is intentional, because all fetching should still /// string value of the `Url` it contains. This is intentional, because all fetching should still
/// happen within the context of the original URL. /// happen within the context of the original URL.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct CanonicalUrl(Url); pub struct CanonicalUrl(DisplaySafeUrl);
impl CanonicalUrl { impl CanonicalUrl {
pub fn new(url: &Url) -> Self { pub fn new(url: &DisplaySafeUrl) -> Self {
let mut url = url.clone(); let mut url = url.clone();
// If the URL cannot be a base, then it's not a valid URL anyway. // If the URL cannot be a base, then it's not a valid URL anyway.
@ -42,8 +43,8 @@ impl CanonicalUrl {
// almost certainly not using the same case conversion rules that GitHub // almost certainly not using the same case conversion rules that GitHub
// does. (See issue #84) // does. (See issue #84)
if url.host_str() == Some("github.com") { if url.host_str() == Some("github.com") {
url.set_scheme(url.scheme().to_lowercase().as_str()) let scheme = url.scheme().to_lowercase();
.unwrap(); url.set_scheme(&scheme).unwrap();
let path = url.path().to_lowercase(); let path = url.path().to_lowercase();
url.set_path(&path); url.set_path(&path);
} }
@ -56,7 +57,8 @@ impl CanonicalUrl {
.is_some_and(|ext| ext.eq_ignore_ascii_case("git")); .is_some_and(|ext| ext.eq_ignore_ascii_case("git"));
if needs_chopping { if needs_chopping {
let prefix = &prefix[..prefix.len() - 4]; let prefix = &prefix[..prefix.len() - 4];
url.set_path(&format!("{prefix}@{suffix}")); let path = format!("{prefix}@{suffix}");
url.set_path(&path);
} }
} else { } else {
// Ex) `git+https://github.com/pypa/sample-namespace-packages.git` // Ex) `git+https://github.com/pypa/sample-namespace-packages.git`
@ -97,7 +99,7 @@ impl CanonicalUrl {
} }
pub fn parse(url: &str) -> Result<Self, url::ParseError> { pub fn parse(url: &str) -> Result<Self, url::ParseError> {
Ok(Self::new(&Url::parse(url)?)) Ok(Self::new(&DisplaySafeUrl::parse(url)?))
} }
} }
@ -117,7 +119,7 @@ impl Hash for CanonicalUrl {
} }
} }
impl From<CanonicalUrl> for Url { impl From<CanonicalUrl> for DisplaySafeUrl {
fn from(value: CanonicalUrl) -> Self { fn from(value: CanonicalUrl) -> Self {
value.0 value.0
} }
@ -138,10 +140,10 @@ impl std::fmt::Display for CanonicalUrl {
/// [`CanonicalUrl`] values, but the same [`RepositoryUrl`], since they map to the same /// [`CanonicalUrl`] values, but the same [`RepositoryUrl`], since they map to the same
/// resource. /// resource.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct RepositoryUrl(Url); pub struct RepositoryUrl(DisplaySafeUrl);
impl RepositoryUrl { impl RepositoryUrl {
pub fn new(url: &Url) -> Self { pub fn new(url: &DisplaySafeUrl) -> Self {
let mut url = CanonicalUrl::new(url).0; let mut url = CanonicalUrl::new(url).0;
// If a Git URL ends in a reference (like a branch, tag, or commit), remove it. // If a Git URL ends in a reference (like a branch, tag, or commit), remove it.
@ -163,7 +165,7 @@ impl RepositoryUrl {
} }
pub fn parse(url: &str) -> Result<Self, url::ParseError> { pub fn parse(url: &str) -> Result<Self, url::ParseError> {
Ok(Self::new(&Url::parse(url)?)) Ok(Self::new(&DisplaySafeUrl::parse(url)?))
} }
} }

View file

@ -24,6 +24,7 @@ uv-distribution-types = { workspace = true }
uv-fs = { workspace = true, features = ["tokio"] } uv-fs = { workspace = true, features = ["tokio"] }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
clap = { workspace = true, features = ["derive", "env"], optional = true } clap = { workspace = true, features = ["derive", "env"], optional = true }
@ -35,5 +36,4 @@ same-file = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true } tempfile = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
url = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }

View file

@ -1,9 +1,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use url::Url;
use uv_cache_key::{CanonicalUrl, cache_digest}; use uv_cache_key::{CanonicalUrl, cache_digest};
use uv_distribution_types::IndexUrl; use uv_distribution_types::IndexUrl;
use uv_redacted::DisplaySafeUrl;
/// Cache wheels and their metadata, both from remote wheels and built from source distributions. /// Cache wheels and their metadata, both from remote wheels and built from source distributions.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -11,16 +10,16 @@ pub enum WheelCache<'a> {
/// Either PyPI or an alternative index, which we key by index URL. /// Either PyPI or an alternative index, which we key by index URL.
Index(&'a IndexUrl), Index(&'a IndexUrl),
/// A direct URL dependency, which we key by URL. /// A direct URL dependency, which we key by URL.
Url(&'a Url), Url(&'a DisplaySafeUrl),
/// A path dependency, which we key by URL. /// A path dependency, which we key by URL.
Path(&'a Url), Path(&'a DisplaySafeUrl),
/// An editable dependency, which we key by URL. /// An editable dependency, which we key by URL.
Editable(&'a Url), Editable(&'a DisplaySafeUrl),
/// A Git dependency, which we key by URL and SHA. /// A Git dependency, which we key by URL and SHA.
/// ///
/// Note that this variant only exists for source distributions; wheels can't be delivered /// Note that this variant only exists for source distributions; wheels can't be delivered
/// through Git. /// through Git.
Git(&'a Url, &'a str), Git(&'a DisplaySafeUrl, &'a str),
} }
impl WheelCache<'_> { impl WheelCache<'_> {
@ -30,7 +29,7 @@ impl WheelCache<'_> {
WheelCache::Index(IndexUrl::Pypi(_)) => WheelCacheKind::Pypi.root(), WheelCache::Index(IndexUrl::Pypi(_)) => WheelCacheKind::Pypi.root(),
WheelCache::Index(url) => WheelCacheKind::Index WheelCache::Index(url) => WheelCacheKind::Index
.root() .root()
.join(cache_digest(&CanonicalUrl::new(url))), .join(cache_digest(&CanonicalUrl::new(url.url()))),
WheelCache::Url(url) => WheelCacheKind::Url WheelCache::Url(url) => WheelCacheKind::Url
.root() .root()
.join(cache_digest(&CanonicalUrl::new(url))), .join(cache_digest(&CanonicalUrl::new(url))),

View file

@ -25,6 +25,7 @@ uv-normalize = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-python = { workspace = true, features = ["clap", "schemars"]} uv-python = { workspace = true, features = ["clap", "schemars"]}
uv-redacted = { workspace = true }
uv-resolver = { workspace = true, features = ["clap"] } uv-resolver = { workspace = true, features = ["clap"] }
uv-settings = { workspace = true, features = ["schemars"] } uv-settings = { workspace = true, features = ["schemars"] }
uv-static = { workspace = true } uv-static = { workspace = true }

View file

@ -8,7 +8,6 @@ use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects, Style}; use clap::builder::styling::{AnsiColor, Effects, Style};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use url::Url;
use uv_cache::CacheArgs; use uv_cache::CacheArgs;
use uv_configuration::{ use uv_configuration::{
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
@ -19,6 +18,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName};
use uv_pep508::{MarkerTree, Requirement}; use uv_pep508::{MarkerTree, Requirement};
use uv_pypi_types::VerbatimParsedUrl; use uv_pypi_types::VerbatimParsedUrl;
use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_torch::TorchMode; use uv_torch::TorchMode;
@ -5897,7 +5897,7 @@ pub struct PublishArgs {
/// ///
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>). /// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
#[arg(long, env = EnvVars::UV_PUBLISH_URL)] #[arg(long, env = EnvVars::UV_PUBLISH_URL)]
pub publish_url: Option<Url>, pub publish_url: Option<DisplaySafeUrl>,
/// Check an index URL for existing files to skip duplicate uploads. /// Check an index URL for existing files to skip duplicate uploads.
/// ///

View file

@ -21,6 +21,7 @@ use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_pep508::MarkerEnvironment; use uv_pep508::MarkerEnvironment;
use uv_platform_tags::Platform; use uv_platform_tags::Platform;
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_version::version; use uv_version::version;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
@ -407,7 +408,7 @@ enum Security {
impl BaseClient { impl BaseClient {
/// Selects the appropriate client based on the host's trustworthiness. /// Selects the appropriate client based on the host's trustworthiness.
pub fn for_host(&self, url: &Url) -> &ClientWithMiddleware { pub fn for_host(&self, url: &DisplaySafeUrl) -> &ClientWithMiddleware {
if self.disable_ssl(url) { if self.disable_ssl(url) {
&self.dangerous_client &self.dangerous_client
} else { } else {
@ -416,7 +417,7 @@ impl BaseClient {
} }
/// Returns `true` if the host is trusted to use the insecure client. /// Returns `true` if the host is trusted to use the insecure client.
pub fn disable_ssl(&self, url: &Url) -> bool { pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool {
self.allow_insecure_host self.allow_insecure_host
.iter() .iter()
.any(|allow_insecure_host| allow_insecure_host.matches(url)) .any(|allow_insecure_host| allow_insecure_host.matches(url))

View file

@ -12,6 +12,7 @@ use tracing::{Instrument, debug, info_span, instrument, trace, warn};
use uv_cache::{CacheEntry, Freshness}; use uv_cache::{CacheEntry, Freshness};
use uv_fs::write_atomic; use uv_fs::write_atomic;
use uv_redacted::DisplaySafeUrl;
use crate::BaseClient; use crate::BaseClient;
use crate::base_client::is_extended_transient_error; use crate::base_client::is_extended_transient_error;
@ -481,11 +482,11 @@ impl CachedClient {
cached: DataWithCachePolicy, cached: DataWithCachePolicy,
new_cache_policy_builder: CachePolicyBuilder, new_cache_policy_builder: CachePolicyBuilder,
) -> Result<CachedResponse, Error> { ) -> Result<CachedResponse, Error> {
let url = req.url().clone(); let url = DisplaySafeUrl::from(req.url().clone());
debug!("Sending revalidation request for: {url}"); debug!("Sending revalidation request for: {url}");
let response = self let response = self
.0 .0
.for_host(req.url()) .for_host(&url)
.execute(req) .execute(req)
.instrument(info_span!("revalidation_request", url = url.as_str())) .instrument(info_span!("revalidation_request", url = url.as_str()))
.await .await
@ -521,7 +522,7 @@ impl CachedClient {
&self, &self,
req: Request, req: Request,
) -> Result<(Response, Option<Box<CachePolicy>>), Error> { ) -> Result<(Response, Option<Box<CachePolicy>>), Error> {
let url = req.url().clone(); let url = DisplaySafeUrl::from(req.url().clone());
trace!("Sending fresh {} request for {}", req.method(), url); trace!("Sending fresh {} request for {}", req.method(), url);
let cache_policy_builder = CachePolicyBuilder::new(&req); let cache_policy_builder = CachePolicyBuilder::new(&req);
let response = self let response = self

View file

@ -3,11 +3,10 @@ use std::ops::Deref;
use async_http_range_reader::AsyncHttpRangeReaderError; use async_http_range_reader::AsyncHttpRangeReaderError;
use async_zip::error::ZipError; use async_zip::error::ZipError;
use url::Url;
use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_distribution_filename::{WheelFilename, WheelFilenameError};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_redacted::redacted_url; use uv_redacted::DisplaySafeUrl;
use crate::middleware::OfflineError; use crate::middleware::OfflineError;
use crate::{FlatIndexError, html}; use crate::{FlatIndexError, html};
@ -30,12 +29,12 @@ impl Error {
} }
/// Create a new error from a JSON parsing error. /// Create a new error from a JSON parsing error.
pub(crate) fn from_json_err(err: serde_json::Error, url: Url) -> Self { pub(crate) fn from_json_err(err: serde_json::Error, url: DisplaySafeUrl) -> Self {
ErrorKind::BadJson { source: err, url }.into() ErrorKind::BadJson { source: err, url }.into()
} }
/// Create a new error from an HTML parsing error. /// Create a new error from an HTML parsing error.
pub(crate) fn from_html_err(err: html::Error, url: Url) -> Self { pub(crate) fn from_html_err(err: html::Error, url: DisplaySafeUrl) -> Self {
ErrorKind::BadHtml { source: err, url }.into() ErrorKind::BadHtml { source: err, url }.into()
} }
@ -160,10 +159,10 @@ pub enum ErrorKind {
Flat(#[from] FlatIndexError), Flat(#[from] FlatIndexError),
#[error("Expected a file URL, but received: {0}")] #[error("Expected a file URL, but received: {0}")]
NonFileUrl(Url), NonFileUrl(DisplaySafeUrl),
#[error("Expected an index URL, but received non-base URL: {0}")] #[error("Expected an index URL, but received non-base URL: {0}")]
CannotBeABase(Url), CannotBeABase(DisplaySafeUrl),
#[error("Failed to read metadata: `{0}`")] #[error("Failed to read metadata: `{0}`")]
Metadata(String, #[source] uv_metadata::Error), Metadata(String, #[source] uv_metadata::Error),
@ -196,16 +195,22 @@ pub enum ErrorKind {
/// An error that happened while making a request or in a reqwest middleware. /// An error that happened while making a request or in a reqwest middleware.
#[error("Failed to fetch: `{0}`")] #[error("Failed to fetch: `{0}`")]
WrappedReqwestError(Url, #[source] WrappedReqwestError), WrappedReqwestError(DisplaySafeUrl, #[source] WrappedReqwestError),
#[error("Received some unexpected JSON from {}", redacted_url(url))] #[error("Received some unexpected JSON from {}", url)]
BadJson { source: serde_json::Error, url: Url }, BadJson {
source: serde_json::Error,
url: DisplaySafeUrl,
},
#[error("Received some unexpected HTML from {}", redacted_url(url))] #[error("Received some unexpected HTML from {}", url)]
BadHtml { source: html::Error, url: Url }, BadHtml {
source: html::Error,
url: DisplaySafeUrl,
},
#[error("Failed to read zip with range requests: `{0}`")] #[error("Failed to read zip with range requests: `{0}`")]
AsyncHttpRangeReader(Url, #[source] AsyncHttpRangeReaderError), AsyncHttpRangeReader(DisplaySafeUrl, #[source] AsyncHttpRangeReaderError),
#[error("{0} is not a valid wheel filename")] #[error("{0} is not a valid wheel filename")]
WheelFilename(#[source] WheelFilenameError), WheelFilename(#[source] WheelFilenameError),
@ -232,13 +237,13 @@ pub enum ErrorKind {
Encode(#[source] rmp_serde::encode::Error), Encode(#[source] rmp_serde::encode::Error),
#[error("Missing `Content-Type` header for {0}")] #[error("Missing `Content-Type` header for {0}")]
MissingContentType(Url), MissingContentType(DisplaySafeUrl),
#[error("Invalid `Content-Type` header for {0}")] #[error("Invalid `Content-Type` header for {0}")]
InvalidContentTypeHeader(Url, #[source] http::header::ToStrError), InvalidContentTypeHeader(DisplaySafeUrl, #[source] http::header::ToStrError),
#[error("Unsupported `Content-Type` \"{1}\" for {0}. Expected JSON or HTML.")] #[error("Unsupported `Content-Type` \"{1}\" for {0}. Expected JSON or HTML.")]
UnsupportedMediaType(Url, String), UnsupportedMediaType(DisplaySafeUrl, String),
#[error("Reading from cache archive failed: {0}")] #[error("Reading from cache archive failed: {0}")]
ArchiveRead(String), ArchiveRead(String),
@ -253,11 +258,14 @@ pub enum ErrorKind {
} }
impl ErrorKind { impl ErrorKind {
pub(crate) fn from_reqwest(url: Url, error: reqwest::Error) -> Self { pub(crate) fn from_reqwest(url: DisplaySafeUrl, error: reqwest::Error) -> Self {
Self::WrappedReqwestError(url, WrappedReqwestError::from(error)) Self::WrappedReqwestError(url, WrappedReqwestError::from(error))
} }
pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { pub(crate) fn from_reqwest_middleware(
url: DisplaySafeUrl,
err: reqwest_middleware::Error,
) -> Self {
if let reqwest_middleware::Error::Middleware(ref underlying) = err { if let reqwest_middleware::Error::Middleware(ref underlying) = err {
if let Some(err) = underlying.downcast_ref::<OfflineError>() { if let Some(err) = underlying.downcast_ref::<OfflineError>() {
return Self::Offline(err.url().to_string()); return Self::Offline(err.url().to_string());

View file

@ -10,7 +10,7 @@ use uv_cache_key::cache_digest;
use uv_distribution_filename::DistFilename; use uv_distribution_filename::DistFilename;
use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString}; use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString};
use uv_pypi_types::HashDigests; use uv_pypi_types::HashDigests;
use uv_redacted::redacted_url; use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString; use uv_small_str::SmallString;
use crate::cached_client::{CacheControl, CachedClientError}; use crate::cached_client::{CacheControl, CachedClientError};
@ -20,13 +20,13 @@ use crate::{CachedClient, Connectivity, Error, ErrorKind, OwnedArchive};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum FlatIndexError { pub enum FlatIndexError {
#[error("Expected a file URL, but received: {0}")] #[error("Expected a file URL, but received: {0}")]
NonFileUrl(Url), NonFileUrl(DisplaySafeUrl),
#[error("Failed to read `--find-links` directory: {0}")] #[error("Failed to read `--find-links` directory: {0}")]
FindLinksDirectory(PathBuf, #[source] FindLinksDirectoryError), FindLinksDirectory(PathBuf, #[source] FindLinksDirectoryError),
#[error("Failed to read `--find-links` URL: {0}")] #[error("Failed to read `--find-links` URL: {0}")]
FindLinksUrl(Url, #[source] Error), FindLinksUrl(DisplaySafeUrl, #[source] Error),
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -159,7 +159,7 @@ impl<'a> FlatIndexClient<'a> {
/// Read a flat remote index from a `--find-links` URL. /// Read a flat remote index from a `--find-links` URL.
async fn read_from_url( async fn read_from_url(
&self, &self,
url: &Url, url: &DisplaySafeUrl,
flat_index: &IndexUrl, flat_index: &IndexUrl,
) -> Result<FlatIndexEntries, Error> { ) -> Result<FlatIndexEntries, Error> {
let cache_entry = self.cache.entry( let cache_entry = self.cache.entry(
@ -180,7 +180,7 @@ impl<'a> FlatIndexClient<'a> {
.client .client
.uncached() .uncached()
.for_host(url) .for_host(url)
.get(url.clone()) .get(Url::from(url.clone()))
.header("Accept-Encoding", "gzip") .header("Accept-Encoding", "gzip")
.header("Accept", "text/html") .header("Accept", "text/html")
.build() .build()
@ -189,7 +189,7 @@ impl<'a> FlatIndexClient<'a> {
async { async {
// Use the response URL, rather than the request URL, as the base for relative URLs. // Use the response URL, rather than the request URL, as the base for relative URLs.
// This ensures that we handle redirects and other URL transformations correctly. // This ensures that we handle redirects and other URL transformations correctly.
let url = response.url().clone(); let url = DisplaySafeUrl::from(response.url().clone());
let text = response let text = response
.text() .text()
@ -208,7 +208,7 @@ impl<'a> FlatIndexClient<'a> {
Ok(file) => Some(file), Ok(file) => Some(file),
Err(err) => { Err(err) => {
// Ignore files with unparsable version specifiers. // Ignore files with unparsable version specifiers.
warn!("Skipping file in {}: {err}", redacted_url(&url)); warn!("Skipping file in {}: {err}", &url);
None None
} }
} }
@ -294,7 +294,7 @@ impl<'a> FlatIndexClient<'a> {
}; };
// SAFETY: The index path is itself constructed from a URL. // SAFETY: The index path is itself constructed from a URL.
let url = Url::from_file_path(entry.path()).unwrap(); let url = DisplaySafeUrl::from_file_path(entry.path()).unwrap();
let file = File { let file = File {
dist_info_metadata: false, dist_info_metadata: false,
@ -303,7 +303,7 @@ impl<'a> FlatIndexClient<'a> {
requires_python: None, requires_python: None,
size: None, size: None,
upload_time_utc_ms: None, upload_time_utc_ms: None,
url: FileLocation::AbsoluteUrl(UrlString::from(&url)), url: FileLocation::AbsoluteUrl(UrlString::from(url)),
yanked: None, yanked: None,
}; };

View file

@ -8,6 +8,7 @@ use url::Url;
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
use uv_pypi_types::{BaseUrl, CoreMetadata, File, Hashes, Yanked}; use uv_pypi_types::{BaseUrl, CoreMetadata, File, Hashes, Yanked};
use uv_pypi_types::{HashError, LenientVersionSpecifiers}; use uv_pypi_types::{HashError, LenientVersionSpecifiers};
use uv_redacted::DisplaySafeUrl;
/// A parsed structure from PyPI "HTML" index format for a single package. /// A parsed structure from PyPI "HTML" index format for a single package.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -27,7 +28,7 @@ impl SimpleHtml {
// 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( let base = BaseUrl::from(DisplaySafeUrl::from(
dom.nodes() dom.nodes()
.iter() .iter()
.filter_map(|node| node.as_tag()) .filter_map(|node| node.as_tag())
@ -37,7 +38,7 @@ impl SimpleHtml {
.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<File> = dom let mut files: Vec<File> = dom
@ -278,21 +279,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -335,21 +322,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -395,21 +368,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://index.python.org/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"index.python.org",
),
),
port: None,
path: "/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -452,21 +411,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -509,21 +454,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -566,21 +497,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -621,21 +538,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -673,28 +576,14 @@ mod tests {
"; ";
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleHtml::parse(text, &base).unwrap(); let result = SimpleHtml::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r###" insta::assert_debug_snapshot!(result, @r"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [], files: [],
} }
"###); ");
} }
#[test] #[test]
@ -711,28 +600,14 @@ mod tests {
"#; "#;
let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap();
let result = SimpleHtml::parse(text, &base).unwrap(); let result = SimpleHtml::parse(text, &base).unwrap();
insta::assert_debug_snapshot!(result, @r###" insta::assert_debug_snapshot!(result, @r"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [], files: [],
} }
"###); ");
} }
#[test] #[test]
@ -752,21 +627,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -807,21 +668,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -863,21 +710,7 @@ mod tests {
Ok( Ok(
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -920,21 +753,7 @@ mod tests {
Ok( Ok(
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -994,21 +813,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://storage.googleapis.com/jax-releases/jax_cuda_releases.html,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"storage.googleapis.com",
),
),
port: None,
path: "/jax-releases/jax_cuda_releases.html",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -1076,21 +881,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"account.d.codeartifact.us-west-2.amazonaws.com",
),
),
port: None,
path: "/pypi/shared-packages-pypi/simple/flask/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -1179,21 +970,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://download.pytorch.org/whl/jinja2/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/jinja2/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {
@ -1252,21 +1029,7 @@ mod tests {
insta::assert_debug_snapshot!(result, @r#" insta::assert_debug_snapshot!(result, @r#"
SimpleHtml { SimpleHtml {
base: BaseUrl( base: BaseUrl(
Url { https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"account.d.codeartifact.us-west-2.amazonaws.com",
),
),
port: None,
path: "/pypi/shared-packages-pypi/simple/flask/",
query: None,
fragment: None,
},
), ),
files: [ files: [
File { File {

View file

@ -1,19 +1,19 @@
use http::Extensions; use http::Extensions;
use std::fmt::Debug; use std::fmt::Debug;
use uv_redacted::DisplaySafeUrl;
use reqwest::{Request, Response}; use reqwest::{Request, Response};
use reqwest_middleware::{Middleware, Next}; use reqwest_middleware::{Middleware, Next};
use url::Url;
/// A custom error type for the offline middleware. /// A custom error type for the offline middleware.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OfflineError { pub(crate) struct OfflineError {
url: Url, url: DisplaySafeUrl,
} }
impl OfflineError { impl OfflineError {
/// Returns the URL that caused the error. /// Returns the URL that caused the error.
pub(crate) fn url(&self) -> &Url { pub(crate) fn url(&self) -> &DisplaySafeUrl {
&self.url &self.url
} }
} }
@ -43,7 +43,7 @@ impl Middleware for OfflineMiddleware {
) -> reqwest_middleware::Result<Response> { ) -> reqwest_middleware::Result<Response> {
Err(reqwest_middleware::Error::Middleware( Err(reqwest_middleware::Error::Middleware(
OfflineError { OfflineError {
url: req.url().clone(), url: DisplaySafeUrl::from(req.url().clone()),
} }
.into(), .into(),
)) ))

View file

@ -31,7 +31,7 @@ use uv_pep440::Version;
use uv_pep508::MarkerEnvironment; use uv_pep508::MarkerEnvironment;
use uv_platform_tags::Platform; use uv_platform_tags::Platform;
use uv_pypi_types::{ResolutionMetadata, SimpleJson}; use uv_pypi_types::{ResolutionMetadata, SimpleJson};
use uv_redacted::redacted_url; use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString; use uv_small_str::SmallString;
use uv_torch::TorchStrategy; use uv_torch::TorchStrategy;
@ -251,12 +251,12 @@ impl RegistryClient {
} }
/// Return the [`BaseClient`] used by this client. /// Return the [`BaseClient`] used by this client.
pub fn uncached_client(&self, url: &Url) -> &ClientWithMiddleware { pub fn uncached_client(&self, url: &DisplaySafeUrl) -> &ClientWithMiddleware {
self.client.uncached().for_host(url) self.client.uncached().for_host(url)
} }
/// Returns `true` if SSL verification is disabled for the given URL. /// Returns `true` if SSL verification is disabled for the given URL.
pub fn disable_ssl(&self, url: &Url) -> bool { pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool {
self.client.uncached().disable_ssl(url) self.client.uncached().disable_ssl(url)
} }
@ -485,10 +485,7 @@ impl RegistryClient {
// ref https://github.com/servo/rust-url/issues/333 // ref https://github.com/servo/rust-url/issues/333
.push(""); .push("");
trace!( trace!("Fetching metadata for {package_name} from {url}");
"Fetching metadata for {package_name} from {}",
redacted_url(&url)
);
let cache_entry = self.cache.entry( let cache_entry = self.cache.entry(
CacheBucket::Simple, CacheBucket::Simple,
@ -554,13 +551,13 @@ impl RegistryClient {
async fn fetch_remote_index( async fn fetch_remote_index(
&self, &self,
package_name: &PackageName, package_name: &PackageName,
url: &Url, url: &DisplaySafeUrl,
cache_entry: &CacheEntry, cache_entry: &CacheEntry,
cache_control: CacheControl, cache_control: CacheControl,
) -> Result<OwnedArchive<SimpleMetadata>, Error> { ) -> Result<OwnedArchive<SimpleMetadata>, Error> {
let simple_request = self let simple_request = self
.uncached_client(url) .uncached_client(url)
.get(url.clone()) .get(Url::from(url.clone()))
.header("Accept-Encoding", "gzip") .header("Accept-Encoding", "gzip")
.header("Accept", MediaType::accepts()) .header("Accept", MediaType::accepts())
.build() .build()
@ -569,7 +566,7 @@ impl RegistryClient {
async { async {
// Use the response URL, rather than the request URL, as the base for relative URLs. // Use the response URL, rather than the request URL, as the base for relative URLs.
// This ensures that we handle redirects and other URL transformations correctly. // This ensures that we handle redirects and other URL transformations correctly.
let url = response.url().clone(); let url = DisplaySafeUrl::from(response.url().clone());
let content_type = response let content_type = response
.headers() .headers()
@ -629,7 +626,7 @@ impl RegistryClient {
async fn fetch_local_index( async fn fetch_local_index(
&self, &self,
package_name: &PackageName, package_name: &PackageName,
url: &Url, url: &DisplaySafeUrl,
) -> Result<OwnedArchive<SimpleMetadata>, Error> { ) -> Result<OwnedArchive<SimpleMetadata>, Error> {
let path = url let path = url
.to_file_path() .to_file_path()
@ -669,7 +666,7 @@ impl RegistryClient {
/// A local file path. /// A local file path.
Path(PathBuf), Path(PathBuf),
/// A remote URL. /// A remote URL.
Url(Url), Url(DisplaySafeUrl),
} }
let wheel = wheels.best_wheel(); let wheel = wheels.best_wheel();
@ -770,14 +767,15 @@ impl RegistryClient {
&self, &self,
index: &IndexUrl, index: &IndexUrl,
file: &File, file: &File,
url: &Url, url: &DisplaySafeUrl,
capabilities: &IndexCapabilities, capabilities: &IndexCapabilities,
) -> Result<ResolutionMetadata, Error> { ) -> Result<ResolutionMetadata, Error> {
// If the metadata file is available at its own url (PEP 658), download it from there. // If the metadata file is available at its own url (PEP 658), download it from there.
let filename = WheelFilename::from_str(&file.filename).map_err(ErrorKind::WheelFilename)?; let filename = WheelFilename::from_str(&file.filename).map_err(ErrorKind::WheelFilename)?;
if file.dist_info_metadata { if file.dist_info_metadata {
let mut url = url.clone(); let mut url = url.clone();
url.set_path(&format!("{}.metadata", url.path())); let path = format!("{}.metadata", url.path());
url.set_path(&path);
let cache_entry = self.cache.entry( let cache_entry = self.cache.entry(
CacheBucket::Wheels, CacheBucket::Wheels,
@ -818,7 +816,7 @@ impl RegistryClient {
}; };
let req = self let req = self
.uncached_client(&url) .uncached_client(&url)
.get(url.clone()) .get(Url::from(url.clone()))
.build() .build()
.map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?;
Ok(self Ok(self
@ -844,7 +842,7 @@ impl RegistryClient {
async fn wheel_metadata_no_pep658<'data>( async fn wheel_metadata_no_pep658<'data>(
&self, &self,
filename: &'data WheelFilename, filename: &'data WheelFilename,
url: &'data Url, url: &'data DisplaySafeUrl,
index: Option<&'data IndexUrl>, index: Option<&'data IndexUrl>,
cache_shard: WheelCache<'data>, cache_shard: WheelCache<'data>,
capabilities: &'data IndexCapabilities, capabilities: &'data IndexCapabilities,
@ -874,7 +872,7 @@ impl RegistryClient {
if index.is_none_or(|index| capabilities.supports_range_requests(index)) { if index.is_none_or(|index| capabilities.supports_range_requests(index)) {
let req = self let req = self
.uncached_client(url) .uncached_client(url)
.head(url.clone()) .head(Url::from(url.clone()))
.header( .header(
"accept-encoding", "accept-encoding",
http::HeaderValue::from_static("identity"), http::HeaderValue::from_static("identity"),
@ -895,7 +893,7 @@ impl RegistryClient {
let mut reader = AsyncHttpRangeReader::from_head_response( let mut reader = AsyncHttpRangeReader::from_head_response(
self.uncached_client(url).clone(), self.uncached_client(url).clone(),
response, response,
url.clone(), Url::from(url.clone()),
headers.clone(), headers.clone(),
) )
.await .await
@ -949,7 +947,7 @@ impl RegistryClient {
// Create a request to stream the file. // Create a request to stream the file.
let req = self let req = self
.uncached_client(url) .uncached_client(url)
.get(url.clone()) .get(Url::from(url.clone()))
.header( .header(
// `reqwest` defaults to accepting compressed responses. // `reqwest` defaults to accepting compressed responses.
// Specify identity encoding to get consistent .whl downloading // Specify identity encoding to get consistent .whl downloading
@ -1141,7 +1139,11 @@ impl SimpleMetadata {
} }
/// Read the [`SimpleMetadata`] from an HTML index. /// Read the [`SimpleMetadata`] from an HTML index.
fn from_html(text: &str, package_name: &PackageName, url: &Url) -> Result<Self, Error> { fn from_html(
text: &str,
package_name: &PackageName,
url: &DisplaySafeUrl,
) -> Result<Self, Error> {
let SimpleHtml { base, files } = let SimpleHtml { base, files } =
SimpleHtml::parse(text, url).map_err(|err| Error::from_html_err(err, url.clone()))?; SimpleHtml::parse(text, url).map_err(|err| Error::from_html_err(err, url.clone()))?;
@ -1220,10 +1222,9 @@ impl Connectivity {
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;
use url::Url;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pypi_types::{JoinRelativeError, SimpleJson}; use uv_pypi_types::{JoinRelativeError, SimpleJson};
use uv_redacted::DisplaySafeUrl;
use crate::{SimpleMetadata, SimpleMetadatum, html::SimpleHtml}; use crate::{SimpleMetadata, SimpleMetadatum, html::SimpleHtml};
@ -1263,7 +1264,7 @@ mod tests {
} }
"#; "#;
let data: SimpleJson = serde_json::from_str(response).unwrap(); let data: SimpleJson = serde_json::from_str(response).unwrap();
let base = Url::parse("https://pypi.org/simple/pyflyby/").unwrap(); let base = DisplaySafeUrl::parse("https://pypi.org/simple/pyflyby/").unwrap();
let simple_metadata = SimpleMetadata::from_files( let simple_metadata = SimpleMetadata::from_files(
data.files, data.files,
&PackageName::from_str("pyflyby").unwrap(), &PackageName::from_str("pyflyby").unwrap(),
@ -1300,7 +1301,7 @@ mod tests {
"#; "#;
// Note the lack of a trailing `/` here is important for coverage of url-join behavior // Note the lack of a trailing `/` here is important for coverage of url-join behavior
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 SimpleHtml { base, files } = SimpleHtml::parse(text, &base).unwrap(); let SimpleHtml { base, files } = SimpleHtml::parse(text, &base).unwrap();
@ -1309,7 +1310,10 @@ mod tests {
.iter() .iter()
.map(|file| uv_pypi_types::base_url_join_relative(base.as_url().as_str(), &file.url)) .map(|file| uv_pypi_types::base_url_join_relative(base.as_url().as_str(), &file.url))
.collect::<Result<Vec<_>, JoinRelativeError>>()?; .collect::<Result<Vec<_>, JoinRelativeError>>()?;
let urls = urls.iter().map(Url::as_str).collect::<Vec<_>>(); let urls = urls
.iter()
.map(DisplaySafeUrl::to_string)
.collect::<Vec<_>>();
insta::assert_debug_snapshot!(urls, @r#" insta::assert_debug_snapshot!(urls, @r#"
[ [
"https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.1/Flask-0.1.tar.gz", "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.1/Flask-0.1.tar.gz",

View file

@ -1,13 +1,13 @@
use std::str::FromStr; use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use url::Url;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::RegistryClientBuilder; use uv_client::RegistryClientBuilder;
use uv_distribution_filename::WheelFilename; use uv_distribution_filename::WheelFilename;
use uv_distribution_types::{BuiltDist, DirectUrlBuiltDist, IndexCapabilities}; use uv_distribution_types::{BuiltDist, DirectUrlBuiltDist, IndexCapabilities};
use uv_pep508::VerbatimUrl; use uv_pep508::VerbatimUrl;
use uv_redacted::DisplaySafeUrl;
#[tokio::test] #[tokio::test]
async fn remote_metadata_with_and_without_cache() -> Result<()> { async fn remote_metadata_with_and_without_cache() -> Result<()> {
@ -21,7 +21,7 @@ async fn remote_metadata_with_and_without_cache() -> Result<()> {
let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?; let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?;
let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist { let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist {
filename, filename,
location: Box::new(Url::parse(url).unwrap()), location: Box::new(DisplaySafeUrl::parse(url).unwrap()),
url: VerbatimUrl::from_str(url).unwrap(), url: VerbatimUrl::from_str(url).unwrap(),
}); });
let capabilities = IndexCapabilities::default(); let capabilities = IndexCapabilities::default();

View file

@ -16,6 +16,7 @@ use uv_client::LineHaul;
use uv_client::RegistryClientBuilder; use uv_client::RegistryClientBuilder;
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
use uv_platform_tags::{Arch, Os, Platform}; use uv_platform_tags::{Arch, Os, Platform};
use uv_redacted::DisplaySafeUrl;
use uv_version::version; use uv_version::version;
#[tokio::test] #[tokio::test]
@ -54,12 +55,12 @@ async fn test_user_agent_has_version() -> Result<()> {
let client = RegistryClientBuilder::new(cache).build(); let client = RegistryClientBuilder::new(cache).build();
// Send request to our dummy server // Send request to our dummy server
let url = Url::from_str(&format!("http://{addr}"))?; let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?;
let res = client let res = client
.cached_client() .cached_client()
.uncached() .uncached()
.for_host(&url) .for_host(&url)
.get(url) .get(Url::from(url))
.send() .send()
.await?; .await?;
@ -151,12 +152,12 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
let client = builder.build(); let client = builder.build();
// Send request to our dummy server // Send request to our dummy server
let url = Url::from_str(&format!("http://{addr}"))?; let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?;
let res = client let res = client
.cached_client() .cached_client()
.uncached() .uncached()
.for_host(&url) .for_host(&url)
.get(url) .get(Url::from(url))
.send() .send()
.await?; .await?;

View file

@ -27,6 +27,7 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-platform-tags = { workspace = true } uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true } uv-small-str = { workspace = true }
arcstr = { workspace = true } arcstr = { workspace = true }
@ -50,3 +51,6 @@ version-ranges = { workspace = true }
[dev-dependencies] [dev-dependencies]
toml = { workspace = true } toml = { workspace = true }
[features]
schemars = ["dep:schemars", "uv-redacted/schemars"]

View file

@ -1,13 +1,13 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::path::Path; use std::path::Path;
use url::Url;
use uv_distribution_filename::SourceDistExtension; use uv_distribution_filename::SourceDistExtension;
use uv_git_types::GitUrl; use uv_git_types::GitUrl;
use uv_pep440::{Version, VersionSpecifiers}; use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::VerbatimUrl; use uv_pep508::VerbatimUrl;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl;
use crate::{DirectorySourceDist, GitSourceDist, Name, PathSourceDist, SourceDist}; use crate::{DirectorySourceDist, GitSourceDist, Name, PathSourceDist, SourceDist};
@ -102,8 +102,8 @@ pub enum SourceUrl<'a> {
} }
impl SourceUrl<'_> { impl SourceUrl<'_> {
/// Return the [`Url`] of the source. /// Return the [`DisplaySafeUrl`] of the source.
pub fn url(&self) -> &Url { pub fn url(&self) -> &DisplaySafeUrl {
match self { match self {
Self::Direct(dist) => dist.url, Self::Direct(dist) => dist.url,
Self::Git(dist) => dist.url, Self::Git(dist) => dist.url,
@ -147,7 +147,7 @@ impl std::fmt::Display for SourceUrl<'_> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DirectSourceUrl<'a> { pub struct DirectSourceUrl<'a> {
pub url: &'a Url, pub url: &'a DisplaySafeUrl,
pub subdirectory: Option<&'a Path>, pub subdirectory: Option<&'a Path>,
pub ext: SourceDistExtension, pub ext: SourceDistExtension,
} }
@ -185,7 +185,7 @@ impl<'a> From<&'a GitSourceDist> for GitSourceUrl<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PathSourceUrl<'a> { pub struct PathSourceUrl<'a> {
pub url: &'a Url, pub url: &'a DisplaySafeUrl,
pub path: Cow<'a, Path>, pub path: Cow<'a, Path>,
pub ext: SourceDistExtension, pub ext: SourceDistExtension,
} }
@ -208,7 +208,7 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DirectorySourceUrl<'a> { pub struct DirectorySourceUrl<'a> {
pub url: &'a Url, pub url: &'a DisplaySafeUrl,
pub install_path: Cow<'a, Path>, pub install_path: Cow<'a, Path>,
pub editable: bool, pub editable: bool,
} }

View file

@ -1,6 +1,5 @@
use url::Url;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
@ -17,7 +16,7 @@ pub enum Error {
MissingPathSegments(String), MissingPathSegments(String),
#[error("Distribution not found at: {0}")] #[error("Distribution not found at: {0}")]
NotFound(Url), NotFound(DisplaySafeUrl),
#[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")] #[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")]
PackageNameMismatch(PackageName, PackageName, String), PackageNameMismatch(PackageName, PackageName, String),

View file

@ -3,11 +3,11 @@ use std::str::FromStr;
use jiff::Timestamp; use jiff::Timestamp;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use uv_pep440::{VersionSpecifiers, VersionSpecifiersParseError}; use uv_pep440::{VersionSpecifiers, VersionSpecifiersParseError};
use uv_pep508::split_scheme; use uv_pep508::split_scheme;
use uv_pypi_types::{CoreMetadata, HashDigests, Yanked}; use uv_pypi_types::{CoreMetadata, HashDigests, Yanked};
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString; use uv_small_str::SmallString;
/// Error converting [`uv_pypi_types::File`] to [`distribution_type::File`]. /// Error converting [`uv_pypi_types::File`] to [`distribution_type::File`].
@ -87,13 +87,14 @@ impl FileLocation {
/// This returns an error if any of the URL parsing fails, or if, for /// This returns an error if any of the URL parsing fails, or if, for
/// example, the location is a path and the path isn't valid UTF-8. /// example, the location is a path and the path isn't valid UTF-8.
/// (Because URLs must be valid UTF-8.) /// (Because URLs must be valid UTF-8.)
pub fn to_url(&self) -> Result<Url, ToUrlError> { pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
match *self { match *self {
FileLocation::RelativeUrl(ref base, ref path) => { FileLocation::RelativeUrl(ref base, ref path) => {
let base_url = Url::parse(base).map_err(|err| ToUrlError::InvalidBase { let base_url =
base: base.to_string(), DisplaySafeUrl::parse(base).map_err(|err| ToUrlError::InvalidBase {
err, base: base.to_string(),
})?; err,
})?;
let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin { let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin {
base: base.to_string(), base: base.to_string(),
path: path.to_string(), path: path.to_string(),
@ -142,9 +143,9 @@ impl UrlString {
Self(url) Self(url)
} }
/// Converts a [`UrlString`] to a [`Url`]. /// Converts a [`UrlString`] to a [`DisplaySafeUrl`].
pub fn to_url(&self) -> Result<Url, ToUrlError> { pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
Url::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute { DisplaySafeUrl::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute {
absolute: self.0.to_string(), absolute: self.0.to_string(),
err, err,
}) })
@ -178,14 +179,14 @@ impl AsRef<str> for UrlString {
} }
} }
impl From<Url> for UrlString { impl From<DisplaySafeUrl> for UrlString {
fn from(value: Url) -> Self { fn from(value: DisplaySafeUrl) -> Self {
Self(value.as_str().into()) Self(value.as_str().into())
} }
} }
impl From<&Url> for UrlString { impl From<&DisplaySafeUrl> for UrlString {
fn from(value: &Url) -> Self { fn from(value: &DisplaySafeUrl) -> Self {
Self(value.as_str().into()) Self(value.as_str().into())
} }
} }

View file

@ -1,12 +1,12 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::path::PathBuf; use std::path::PathBuf;
use url::Url;
use uv_cache_key::{CanonicalUrl, RepositoryUrl}; use uv_cache_key::{CanonicalUrl, RepositoryUrl};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_pypi_types::HashDigest; use uv_pypi_types::HashDigest;
use uv_redacted::DisplaySafeUrl;
/// A unique identifier for a package. A package can either be identified by a name (e.g., `black`) /// A unique identifier for a package. A package can either be identified by a name (e.g., `black`)
/// or a URL (e.g., `git+https://github.com/psf/black`). /// or a URL (e.g., `git+https://github.com/psf/black`).
@ -25,7 +25,7 @@ impl PackageId {
} }
/// Create a new [`PackageId`] from a URL. /// Create a new [`PackageId`] from a URL.
pub fn from_url(url: &Url) -> Self { pub fn from_url(url: &DisplaySafeUrl) -> Self {
Self::Url(CanonicalUrl::new(url)) Self::Url(CanonicalUrl::new(url))
} }
} }
@ -55,7 +55,7 @@ impl VersionId {
} }
/// Create a new [`VersionId`] from a URL. /// Create a new [`VersionId`] from a URL.
pub fn from_url(url: &Url) -> Self { pub fn from_url(url: &DisplaySafeUrl) -> Self {
Self::Url(CanonicalUrl::new(url)) Self::Url(CanonicalUrl::new(url))
} }
} }

View file

@ -3,9 +3,9 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use url::Url;
use uv_auth::{AuthPolicy, Credentials}; use uv_auth::{AuthPolicy, Credentials};
use uv_redacted::DisplaySafeUrl;
use crate::index_name::{IndexName, IndexNameError}; use crate::index_name::{IndexName, IndexNameError};
use crate::origin::Origin; use crate::origin::Origin;
@ -82,7 +82,7 @@ pub struct Index {
/// url = "https://pypi.org/simple" /// url = "https://pypi.org/simple"
/// publish-url = "https://upload.pypi.org/legacy/" /// publish-url = "https://upload.pypi.org/legacy/"
/// ``` /// ```
pub publish_url: Option<Url>, pub publish_url: Option<DisplaySafeUrl>,
/// When uv should use authentication for requests to the index. /// When uv should use authentication for requests to the index.
/// ///
/// ```toml /// ```toml
@ -193,7 +193,7 @@ impl Index {
} }
/// Return the raw [`Url`] of the index. /// Return the raw [`Url`] of the index.
pub fn raw_url(&self) -> &Url { pub fn raw_url(&self) -> &DisplaySafeUrl {
self.url.url() self.url.url()
} }
@ -201,7 +201,7 @@ impl Index {
/// ///
/// For indexes with a `/simple` endpoint, this is simply the URL with the final segment /// For indexes with a `/simple` endpoint, this is simply the URL with the final segment
/// removed. This is useful, e.g., for credential propagation to other endpoints on the index. /// removed. This is useful, e.g., for credential propagation to other endpoints on the index.
pub fn root_url(&self) -> Option<Url> { pub fn root_url(&self) -> Option<DisplaySafeUrl> {
self.url.root() self.url.root()
} }

View file

@ -11,10 +11,12 @@ use thiserror::Error;
use url::{ParseError, Url}; use url::{ParseError, Url};
use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
use uv_redacted::DisplaySafeUrl;
use crate::{Index, IndexStatusCodeStrategy, Verbatim}; use crate::{Index, IndexStatusCodeStrategy, Verbatim};
static PYPI_URL: LazyLock<Url> = LazyLock::new(|| Url::parse("https://pypi.org/simple").unwrap()); static PYPI_URL: LazyLock<DisplaySafeUrl> =
LazyLock::new(|| DisplaySafeUrl::parse("https://pypi.org/simple").unwrap());
static DEFAULT_INDEX: LazyLock<Index> = LazyLock::new(|| { static DEFAULT_INDEX: LazyLock<Index> = LazyLock::new(|| {
Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url( Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url(
@ -69,7 +71,7 @@ impl IndexUrl {
/// ///
/// For indexes with a `/simple` endpoint, this is simply the URL with the final segment /// For indexes with a `/simple` endpoint, this is simply the URL with the final segment
/// removed. This is useful, e.g., for credential propagation to other endpoints on the index. /// removed. This is useful, e.g., for credential propagation to other endpoints on the index.
pub fn root(&self) -> Option<Url> { pub fn root(&self) -> Option<DisplaySafeUrl> {
let mut segments = self.url().path_segments()?; let mut segments = self.url().path_segments()?;
let last = match segments.next_back()? { let last = match segments.next_back()? {
// If the last segment is empty due to a trailing `/`, skip it (as in `pop_if_empty`) // If the last segment is empty due to a trailing `/`, skip it (as in `pop_if_empty`)
@ -108,7 +110,7 @@ impl schemars::JsonSchema for IndexUrl {
impl IndexUrl { impl IndexUrl {
/// Return the raw URL for the index. /// Return the raw URL for the index.
pub fn url(&self) -> &Url { pub fn url(&self) -> &DisplaySafeUrl {
match self { match self {
Self::Pypi(url) => url.raw(), Self::Pypi(url) => url.raw(),
Self::Url(url) => url.raw(), Self::Url(url) => url.raw(),
@ -116,8 +118,8 @@ impl IndexUrl {
} }
} }
/// Convert the index URL into a [`Url`]. /// Convert the index URL into a [`DisplaySafeUrl`].
pub fn into_url(self) -> Url { pub fn into_url(self) -> DisplaySafeUrl {
match self { match self {
Self::Pypi(url) => url.to_url(), Self::Pypi(url) => url.to_url(),
Self::Url(url) => url.to_url(), Self::Url(url) => url.to_url(),
@ -126,7 +128,7 @@ impl IndexUrl {
} }
/// Return the redacted URL for the index, omitting any sensitive credentials. /// Return the redacted URL for the index, omitting any sensitive credentials.
pub fn redacted(&self) -> Cow<'_, Url> { pub fn without_credentials(&self) -> Cow<'_, DisplaySafeUrl> {
let url = self.url(); let url = self.url();
if url.username().is_empty() && url.password().is_none() { if url.username().is_empty() && url.password().is_none() {
Cow::Borrowed(url) Cow::Borrowed(url)
@ -222,7 +224,7 @@ impl From<VerbatimUrl> for IndexUrl {
} }
} }
impl From<IndexUrl> for Url { impl From<IndexUrl> for DisplaySafeUrl {
fn from(index: IndexUrl) -> Self { fn from(index: IndexUrl) -> Self {
match index { match index {
IndexUrl::Pypi(url) => url.to_url(), IndexUrl::Pypi(url) => url.to_url(),

View file

@ -14,6 +14,7 @@ use uv_fs::Simplified;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_pypi_types::{DirectUrl, MetadataError}; use uv_pypi_types::{DirectUrl, MetadataError};
use uv_redacted::DisplaySafeUrl;
use crate::{DistributionMetadata, InstalledMetadata, InstalledVersion, Name, VersionOrUrlRef}; use crate::{DistributionMetadata, InstalledMetadata, InstalledVersion, Name, VersionOrUrlRef};
@ -86,7 +87,7 @@ pub struct InstalledDirectUrlDist {
pub name: PackageName, pub name: PackageName,
pub version: Version, pub version: Version,
pub direct_url: Box<DirectUrl>, pub direct_url: Box<DirectUrl>,
pub url: Url, pub url: DisplaySafeUrl,
pub editable: bool, pub editable: bool,
pub path: Box<Path>, pub path: Box<Path>,
pub cache_info: Option<CacheInfo>, pub cache_info: Option<CacheInfo>,
@ -112,7 +113,7 @@ pub struct InstalledLegacyEditable {
pub version: Version, pub version: Version,
pub egg_link: Box<Path>, pub egg_link: Box<Path>,
pub target: Box<Path>, pub target: Box<Path>,
pub target_url: Url, pub target_url: DisplaySafeUrl,
pub egg_info: Box<Path>, pub egg_info: Box<Path>,
} }
@ -144,7 +145,7 @@ impl InstalledDist {
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, url: DisplaySafeUrl::from(url),
path: path.to_path_buf().into_boxed_path(), path: path.to_path_buf().into_boxed_path(),
cache_info, cache_info,
}))), }))),
@ -272,7 +273,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: url, target_url: DisplaySafeUrl::from(url),
egg_info: egg_info.into_boxed_path(), egg_info: egg_info.into_boxed_path(),
}))); })));
} }

View file

@ -50,6 +50,7 @@ use uv_pep508::{Pep508Url, VerbatimUrl};
use uv_pypi_types::{ use uv_pypi_types::{
ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl,
}; };
use uv_redacted::DisplaySafeUrl;
pub use crate::annotation::*; pub use crate::annotation::*;
pub use crate::any::*; pub use crate::any::*;
@ -147,12 +148,12 @@ pub enum InstalledVersion<'a> {
Version(&'a Version), Version(&'a Version),
/// A URL, used to identify a distribution at an arbitrary location, along with the version /// A URL, used to identify a distribution at an arbitrary location, along with the version
/// specifier to which it resolved. /// specifier to which it resolved.
Url(&'a Url, &'a Version), Url(&'a DisplaySafeUrl, &'a Version),
} }
impl InstalledVersion<'_> { impl InstalledVersion<'_> {
/// If it is a URL, return its value. /// If it is a URL, return its value.
pub fn url(&self) -> Option<&Url> { pub fn url(&self) -> Option<&DisplaySafeUrl> {
match self { match self {
InstalledVersion::Version(_) => None, InstalledVersion::Version(_) => None,
InstalledVersion::Url(url, _) => Some(url), InstalledVersion::Url(url, _) => Some(url),
@ -258,7 +259,7 @@ pub struct DirectUrlBuiltDist {
/// `https://example.org/packages/flask-3.0.0-py3-none-any.whl` /// `https://example.org/packages/flask-3.0.0-py3-none-any.whl`
pub filename: WheelFilename, pub filename: WheelFilename,
/// The URL without the subdirectory fragment. /// The URL without the subdirectory fragment.
pub location: Box<Url>, pub location: Box<DisplaySafeUrl>,
/// The URL as it was provided by the user. /// The URL as it was provided by the user.
pub url: VerbatimUrl, pub url: VerbatimUrl,
} }
@ -299,7 +300,7 @@ pub struct DirectUrlSourceDist {
/// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip` /// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip`
pub name: PackageName, pub name: PackageName,
/// The URL without the subdirectory fragment. /// The URL without the subdirectory fragment.
pub location: Box<Url>, pub location: Box<DisplaySafeUrl>,
/// The subdirectory within the archive in which the source distribution is located. /// The subdirectory within the archive in which the source distribution is located.
pub subdirectory: Option<Box<Path>>, pub subdirectory: Option<Box<Path>>,
/// The file extension, e.g. `tar.gz`, `zip`, etc. /// The file extension, e.g. `tar.gz`, `zip`, etc.
@ -353,7 +354,7 @@ impl Dist {
pub fn from_http_url( pub fn from_http_url(
name: PackageName, name: PackageName,
url: VerbatimUrl, url: VerbatimUrl,
location: Url, location: DisplaySafeUrl,
subdirectory: Option<Box<Path>>, subdirectory: Option<Box<Path>>,
ext: DistExtension, ext: DistExtension,
) -> Result<Dist, Error> { ) -> Result<Dist, Error> {
@ -1168,7 +1169,7 @@ impl RemoteSource for Dist {
} }
} }
impl Identifier for Url { impl Identifier for DisplaySafeUrl {
fn distribution_id(&self) -> DistributionId { fn distribution_id(&self) -> DistributionId {
DistributionId::Url(uv_cache_key::CanonicalUrl::new(self)) DistributionId::Url(uv_cache_key::CanonicalUrl::new(self))
} }
@ -1461,7 +1462,7 @@ impl Identifier for BuildableSource<'_> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString}; use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString};
use url::Url; use uv_redacted::DisplaySafeUrl;
/// Ensure that we don't accidentally grow the `Dist` sizes. /// Ensure that we don't accidentally grow the `Dist` sizes.
#[test] #[test]
@ -1485,7 +1486,7 @@ mod test {
"https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment", "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment",
"https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3", "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3",
] { ] {
let url = Url::parse(url).unwrap(); let url = DisplaySafeUrl::parse(url).unwrap();
assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}"); assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
let url = UrlString::from(url.clone()); let url = UrlString::from(url.clone());
assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}"); assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");

View file

@ -4,7 +4,6 @@ use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error; use thiserror::Error;
use url::Url;
use uv_distribution_filename::DistExtension; use uv_distribution_filename::DistExtension;
use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to}; use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
@ -14,6 +13,7 @@ use uv_pep440::VersionSpecifiers;
use uv_pep508::{ use uv_pep508::{
MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker, MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker,
}; };
use uv_redacted::DisplaySafeUrl;
use crate::{IndexMetadata, IndexUrl}; use crate::{IndexMetadata, IndexUrl};
@ -391,7 +391,7 @@ pub enum RequirementSource {
/// e.g.`foo @ https://example.org/foo-1.0.zip`. /// e.g.`foo @ https://example.org/foo-1.0.zip`.
Url { Url {
/// The remote location of the archive file, without subdirectory fragment. /// The remote location of the archive file, without subdirectory fragment.
location: Url, location: DisplaySafeUrl,
/// For source distributions, the path to the distribution if it is not in the archive /// For source distributions, the path to the distribution if it is not in the archive
/// root. /// root.
subdirectory: Option<Box<Path>>, subdirectory: Option<Box<Path>>,
@ -682,7 +682,7 @@ enum RequirementSourceWire {
Git { git: String }, Git { git: String },
/// Ex) `source = { url = "<https://example.org/foo-1.0.zip>" }` /// Ex) `source = { url = "<https://example.org/foo-1.0.zip>" }`
Direct { Direct {
url: Url, url: DisplaySafeUrl,
subdirectory: Option<PortablePathBuf>, subdirectory: Option<PortablePathBuf>,
}, },
/// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }` /// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }`
@ -697,7 +697,7 @@ enum RequirementSourceWire {
Registry { Registry {
#[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
specifier: VersionSpecifiers, specifier: VersionSpecifiers,
index: Option<Url>, index: Option<DisplaySafeUrl>,
conflict: Option<ConflictItem>, conflict: Option<ConflictItem>,
}, },
} }
@ -711,7 +711,7 @@ impl From<RequirementSource> for RequirementSourceWire {
conflict, conflict,
} => { } => {
let index = index.map(|index| index.url.into_url()).map(|mut index| { let index = index.map(|index| index.url.into_url()).map(|mut index| {
redact_credentials(&mut index); index.remove_credentials();
index index
}); });
Self::Registry { Self::Registry {
@ -736,8 +736,8 @@ impl From<RequirementSource> for RequirementSourceWire {
} => { } => {
let mut url = git.repository().clone(); let mut url = git.repository().clone();
// Redact the credentials. // Remove the credentials.
redact_credentials(&mut url); url.remove_credentials();
// Clear out any existing state. // Clear out any existing state.
url.set_fragment(None); url.set_fragment(None);
@ -826,7 +826,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
conflict, conflict,
}), }),
RequirementSourceWire::Git { git } => { RequirementSourceWire::Git { git } => {
let mut repository = Url::parse(&git)?; let mut repository = DisplaySafeUrl::parse(&git)?;
let mut reference = GitReference::DefaultBranch; let mut reference = GitReference::DefaultBranch;
let mut subdirectory: Option<PortablePathBuf> = None; let mut subdirectory: Option<PortablePathBuf> = None;
@ -848,13 +848,14 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
repository.set_fragment(None); repository.set_fragment(None);
repository.set_query(None); repository.set_query(None);
// Redact the credentials. // Remove the credentials.
redact_credentials(&mut repository); repository.remove_credentials();
// Create a PEP 508-compatible URL. // Create a PEP 508-compatible URL.
let mut url = Url::parse(&format!("git+{repository}"))?; let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?;
if let Some(rev) = reference.as_str() { if let Some(rev) = reference.as_str() {
url.set_path(&format!("{}@{}", url.path(), rev)); let path = format!("{}@{}", url.path(), rev);
url.set_path(&path);
} }
if let Some(subdirectory) = subdirectory.as_ref() { if let Some(subdirectory) = subdirectory.as_ref() {
url.set_fragment(Some(&format!("subdirectory={subdirectory}"))); url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
@ -940,18 +941,6 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
} }
} }
/// Remove the credentials from a URL, allowing the generic `git` username (without a password)
/// in SSH URLs, as in, `ssh://git@github.com/...`.
pub fn redact_credentials(url: &mut Url) {
// For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the
// username.
if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() {
return;
}
let _ = url.set_password(None);
let _ = url.set_username("");
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -33,6 +33,7 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-platform-tags = { workspace = true } uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-workspace = { workspace = true } uv-workspace = { workspace = true }

View file

@ -27,6 +27,7 @@ use uv_extract::hash::Hasher;
use uv_fs::write_atomic; use uv_fs::write_atomic;
use uv_platform_tags::Tags; use uv_platform_tags::Tags;
use uv_pypi_types::{HashDigest, HashDigests}; use uv_pypi_types::{HashDigest, HashDigests};
use uv_redacted::DisplaySafeUrl;
use uv_types::{BuildContext, BuildStack}; use uv_types::{BuildContext, BuildStack};
use crate::archive::Archive; use crate::archive::Archive;
@ -529,7 +530,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
/// Stream a wheel from a URL, unzipping it into the cache as it's downloaded. /// Stream a wheel from a URL, unzipping it into the cache as it's downloaded.
async fn stream_wheel( async fn stream_wheel(
&self, &self,
url: Url, url: DisplaySafeUrl,
filename: &WheelFilename, filename: &WheelFilename,
size: Option<u64>, size: Option<u64>,
wheel_entry: &CacheEntry, wheel_entry: &CacheEntry,
@ -666,7 +667,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
/// Download a wheel from a URL, then unzip it into the cache. /// Download a wheel from a URL, then unzip it into the cache.
async fn download_wheel( async fn download_wheel(
&self, &self,
url: Url, url: DisplaySafeUrl,
filename: &WheelFilename, filename: &WheelFilename,
size: Option<u64>, size: Option<u64>,
wheel_entry: &CacheEntry, wheel_entry: &CacheEntry,
@ -980,11 +981,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
} }
/// Returns a GET [`reqwest::Request`] for the given URL. /// Returns a GET [`reqwest::Request`] for the given URL.
fn request(&self, url: Url) -> Result<reqwest::Request, reqwest::Error> { fn request(&self, url: DisplaySafeUrl) -> Result<reqwest::Request, reqwest::Error> {
self.client self.client
.unmanaged .unmanaged
.uncached_client(&url) .uncached_client(&url)
.get(url) .get(Url::from(url))
.header( .header(
// `reqwest` defaults to accepting compressed responses. // `reqwest` defaults to accepting compressed responses.
// Specify identity encoding to get consistent .whl downloading // Specify identity encoding to get consistent .whl downloading

View file

@ -2,7 +2,6 @@ use std::path::PathBuf;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tokio::task::JoinError; use tokio::task::JoinError;
use url::Url;
use zip::result::ZipError; use zip::result::ZipError;
use crate::metadata::MetadataError; use crate::metadata::MetadataError;
@ -13,6 +12,7 @@ use uv_fs::Simplified;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifiers}; use uv_pep440::{Version, VersionSpecifiers};
use uv_pypi_types::{HashAlgorithm, HashDigest}; use uv_pypi_types::{HashAlgorithm, HashDigest};
use uv_redacted::DisplaySafeUrl;
use uv_types::AnyErrorBuild; use uv_types::AnyErrorBuild;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -28,7 +28,7 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError), JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError),
#[error("Expected a file URL, but received: {0}")] #[error("Expected a file URL, but received: {0}")]
NonFileUrl(Url), NonFileUrl(DisplaySafeUrl),
#[error(transparent)] #[error(transparent)]
Git(#[from] uv_git::GitResolverError), Git(#[from] uv_git::GitResolverError),
#[error(transparent)] #[error(transparent)]
@ -89,7 +89,7 @@ pub enum Error {
#[error("The source distribution is missing a `PKG-INFO` file")] #[error("The source distribution is missing a `PKG-INFO` file")]
MissingPkgInfo, MissingPkgInfo,
#[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())] #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())]
MissingSubdirectory(Url, PathBuf), MissingSubdirectory(DisplaySafeUrl, PathBuf),
#[error("Failed to extract static metadata from `PKG-INFO`")] #[error("Failed to extract static metadata from `PKG-INFO`")]
PkgInfo(#[source] uv_pypi_types::MetadataError), PkgInfo(#[source] uv_pypi_types::MetadataError),
#[error("Failed to extract metadata from `requires.txt`")] #[error("Failed to extract metadata from `requires.txt`")]
@ -103,7 +103,7 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
MetadataLowering(#[from] MetadataError), MetadataLowering(#[from] MetadataError),
#[error("Distribution not found at: {0}")] #[error("Distribution not found at: {0}")]
NotFound(Url), NotFound(DisplaySafeUrl),
#[error("Attempted to re-extract the source distribution for `{}`, but the {} hash didn't match. Run `{}` to clear the cache.", _0, _1, "uv cache clean".green())] #[error("Attempted to re-extract the source distribution for `{}`, but the {} hash didn't match. Run `{}` to clear the cache.", _0, _1, "uv cache clean".green())]
CacheHeal(String, HashAlgorithm), CacheHeal(String, HashAlgorithm),
#[error("The source distribution requires Python {0}, but {1} is installed")] #[error("The source distribution requires Python {0}, but {1} is installed")]

View file

@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
use either::Either; use either::Either;
use thiserror::Error; use thiserror::Error;
use url::Url;
use uv_distribution_filename::DistExtension; use uv_distribution_filename::DistExtension;
use uv_distribution_types::{ use uv_distribution_types::{
@ -15,6 +14,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository}; use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository};
use uv_pypi_types::{ConflictItem, ParsedUrlError, VerbatimParsedUrl}; use uv_pypi_types::{ConflictItem, ParsedUrlError, VerbatimParsedUrl};
use uv_redacted::DisplaySafeUrl;
use uv_workspace::Workspace; use uv_workspace::Workspace;
use uv_workspace::pyproject::{PyProjectToml, Source, Sources}; use uv_workspace::pyproject::{PyProjectToml, Source, Sources};
@ -528,11 +528,11 @@ pub enum LoweringError {
#[error(transparent)] #[error(transparent)]
InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError), InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError),
#[error("Fragments are not allowed in URLs: `{0}`")] #[error("Fragments are not allowed in URLs: `{0}`")]
ForbiddenFragment(Url), ForbiddenFragment(DisplaySafeUrl),
#[error( #[error(
"`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)" "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)"
)] )]
MissingGitSource(PackageName, Url), MissingGitSource(PackageName, DisplaySafeUrl),
#[error("`workspace = false` is not yet supported")] #[error("`workspace = false` is not yet supported")]
WorkspaceFalse, WorkspaceFalse,
#[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")] #[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")]
@ -572,7 +572,7 @@ impl std::fmt::Display for SourceKind {
/// Convert a Git source into a [`RequirementSource`]. /// Convert a Git source into a [`RequirementSource`].
fn git_source( fn git_source(
git: &Url, git: &DisplaySafeUrl,
subdirectory: Option<Box<Path>>, subdirectory: Option<Box<Path>>,
rev: Option<String>, rev: Option<String>,
tag: Option<String>, tag: Option<String>,
@ -587,9 +587,10 @@ fn git_source(
}; };
// Create a PEP 508-compatible URL. // Create a PEP 508-compatible URL.
let mut url = Url::parse(&format!("git+{git}"))?; let mut url = DisplaySafeUrl::parse(&format!("git+{git}"))?;
if let Some(rev) = reference.as_str() { if let Some(rev) = reference.as_str() {
url.set_path(&format!("{}@{}", url.path(), rev)); let path = format!("{}@{}", url.path(), rev);
url.set_path(&path);
} }
if let Some(subdirectory) = subdirectory.as_ref() { if let Some(subdirectory) = subdirectory.as_ref() {
let subdirectory = subdirectory let subdirectory = subdirectory
@ -611,7 +612,7 @@ fn git_source(
/// Convert a URL source into a [`RequirementSource`]. /// Convert a URL source into a [`RequirementSource`].
fn url_source( fn url_source(
requirement: &uv_pep508::Requirement<VerbatimParsedUrl>, requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
url: Url, url: DisplaySafeUrl,
subdirectory: Option<Box<Path>>, subdirectory: Option<Box<Path>>,
) -> Result<RequirementSource, LoweringError> { ) -> Result<RequirementSource, LoweringError> {
let mut verbatim_url = url.clone(); let mut verbatim_url = url.clone();

View file

@ -1,9 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use url::Url;
use uv_distribution_types::BuildableSource; use uv_distribution_types::BuildableSource;
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_redacted::DisplaySafeUrl;
pub trait Reporter: Send + Sync { pub trait Reporter: Send + Sync {
/// Callback to invoke when a source distribution build is kicked off. /// Callback to invoke when a source distribution build is kicked off.
@ -13,10 +12,10 @@ pub trait Reporter: Send + Sync {
fn on_build_complete(&self, source: &BuildableSource, id: usize); fn on_build_complete(&self, source: &BuildableSource, id: usize);
/// Callback to invoke when a repository checkout begins. /// Callback to invoke when a repository checkout begins.
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
/// Callback to invoke when a repository checkout completes. /// Callback to invoke when a repository checkout completes.
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize); fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize);
/// Callback to invoke when a download is kicked off. /// Callback to invoke when a download is kicked off.
fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize; fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize;
@ -44,11 +43,11 @@ struct Facade {
} }
impl uv_git::Reporter for Facade { impl uv_git::Reporter for Facade {
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
self.reporter.on_checkout_start(url, rev) self.reporter.on_checkout_start(url, rev)
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) {
self.reporter.on_checkout_complete(url, rev, id); self.reporter.on_checkout_complete(url, rev, id);
} }
} }

View file

@ -20,6 +20,7 @@ use reqwest::{Response, StatusCode};
use tokio_util::compat::FuturesAsyncReadCompatExt; use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::{Instrument, debug, info_span, instrument, warn}; use tracing::{Instrument, debug, info_span, instrument, warn};
use url::Url; use url::Url;
use uv_redacted::DisplaySafeUrl;
use zip::ZipArchive; use zip::ZipArchive;
use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache}; use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache};
@ -386,7 +387,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
async fn url<'data>( async fn url<'data>(
&self, &self,
source: &BuildableSource<'data>, source: &BuildableSource<'data>,
url: &'data Url, url: &'data DisplaySafeUrl,
cache_shard: &CacheShard, cache_shard: &CacheShard,
subdirectory: Option<&'data Path>, subdirectory: Option<&'data Path>,
ext: SourceDistExtension, ext: SourceDistExtension,
@ -582,7 +583,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(
url.clone(), DisplaySafeUrl::from(url.clone()),
subdirectory.to_path_buf(), subdirectory.to_path_buf(),
)); ));
} }
@ -715,7 +716,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(url.clone(), client.unmanaged)?; let req = Self::request(DisplaySafeUrl::from(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(
@ -740,7 +741,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
client client
.cached_client() .cached_client()
.skip_cache_with_retry( .skip_cache_with_retry(
Self::request(url.clone(), client)?, Self::request(DisplaySafeUrl::from(url.clone()), client)?,
&cache_entry, &cache_entry,
download, download,
) )
@ -2077,7 +2078,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
client client
.cached_client() .cached_client()
.skip_cache_with_retry( .skip_cache_with_retry(
Self::request(url.clone(), client)?, Self::request(DisplaySafeUrl::from(url.clone()), client)?,
&cache_entry, &cache_entry,
download, download,
) )
@ -2402,10 +2403,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
} }
/// Returns a GET [`reqwest::Request`] for the given URL. /// Returns a GET [`reqwest::Request`] for the given URL.
fn request(url: Url, client: &RegistryClient) -> Result<reqwest::Request, reqwest::Error> { fn request(
url: DisplaySafeUrl,
client: &RegistryClient,
) -> Result<reqwest::Request, reqwest::Error> {
client client
.uncached_client(&url) .uncached_client(&url)
.get(url) .get(Url::from(url))
.header( .header(
// `reqwest` defaults to accepting compressed responses. // `reqwest` defaults to accepting compressed responses.
// Specify identity encoding to get consistent .whl downloading // Specify identity encoding to get consistent .whl downloading

View file

@ -16,9 +16,9 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
uv-redacted = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
url = { workspace = true } url = { workspace = true }
uv-redacted = { workspace = true }

View file

@ -3,9 +3,7 @@ pub use crate::oid::{GitOid, OidParseError};
pub use crate::reference::GitReference; pub use crate::reference::GitReference;
use thiserror::Error; use thiserror::Error;
use url::Url; use uv_redacted::DisplaySafeUrl;
use uv_redacted::redacted_url;
mod github; mod github;
mod oid; mod oid;
@ -16,7 +14,7 @@ pub enum GitUrlParseError {
#[error( #[error(
"Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)" "Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)"
)] )]
UnsupportedGitScheme(String, Url), UnsupportedGitScheme(String, DisplaySafeUrl),
} }
/// A URL reference to a Git repository. /// A URL reference to a Git repository.
@ -24,7 +22,7 @@ pub enum GitUrlParseError {
pub struct GitUrl { pub struct GitUrl {
/// The URL of the Git repository, with any query parameters, fragments, and leading `git+` /// The URL of the Git repository, with any query parameters, fragments, and leading `git+`
/// removed. /// removed.
repository: Url, repository: DisplaySafeUrl,
/// The reference to the commit to use, which could be a branch, tag or revision. /// The reference to the commit to use, which could be a branch, tag or revision.
reference: GitReference, reference: GitReference,
/// The precise commit to use, if known. /// The precise commit to use, if known.
@ -34,7 +32,7 @@ pub struct GitUrl {
impl GitUrl { impl GitUrl {
/// Create a new [`GitUrl`] from a repository URL and a reference. /// Create a new [`GitUrl`] from a repository URL and a reference.
pub fn from_reference( pub fn from_reference(
repository: Url, repository: DisplaySafeUrl,
reference: GitReference, reference: GitReference,
) -> Result<Self, GitUrlParseError> { ) -> Result<Self, GitUrlParseError> {
Self::from_fields(repository, reference, None) Self::from_fields(repository, reference, None)
@ -42,7 +40,7 @@ impl GitUrl {
/// Create a new [`GitUrl`] from a repository URL and a precise commit. /// Create a new [`GitUrl`] from a repository URL and a precise commit.
pub fn from_commit( pub fn from_commit(
repository: Url, repository: DisplaySafeUrl,
reference: GitReference, reference: GitReference,
precise: GitOid, precise: GitOid,
) -> Result<Self, GitUrlParseError> { ) -> Result<Self, GitUrlParseError> {
@ -51,7 +49,7 @@ impl GitUrl {
/// Create a new [`GitUrl`] from a repository URL and a precise commit, if known. /// Create a new [`GitUrl`] from a repository URL and a precise commit, if known.
pub fn from_fields( pub fn from_fields(
repository: Url, repository: DisplaySafeUrl,
reference: GitReference, reference: GitReference,
precise: Option<GitOid>, precise: Option<GitOid>,
) -> Result<Self, GitUrlParseError> { ) -> Result<Self, GitUrlParseError> {
@ -86,7 +84,7 @@ impl GitUrl {
} }
/// Return the [`Url`] of the Git repository. /// Return the [`Url`] of the Git repository.
pub fn repository(&self) -> &Url { pub fn repository(&self) -> &DisplaySafeUrl {
&self.repository &self.repository
} }
@ -101,11 +99,11 @@ impl GitUrl {
} }
} }
impl TryFrom<Url> for GitUrl { impl TryFrom<DisplaySafeUrl> for GitUrl {
type Error = GitUrlParseError; type Error = GitUrlParseError;
/// Initialize a [`GitUrl`] source from a URL. /// Initialize a [`GitUrl`] source from a URL.
fn try_from(mut url: Url) -> Result<Self, Self::Error> { fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
// Remove any query parameters and fragments. // Remove any query parameters and fragments.
url.set_fragment(None); url.set_fragment(None);
url.set_query(None); url.set_query(None);
@ -126,13 +124,14 @@ impl TryFrom<Url> for GitUrl {
} }
} }
impl From<GitUrl> for Url { impl From<GitUrl> for DisplaySafeUrl {
fn from(git: GitUrl) -> Self { fn from(git: GitUrl) -> Self {
let mut url = git.repository; let mut url = git.repository;
// If we have a precise commit, add `@` and the commit hash to the URL. // If we have a precise commit, add `@` and the commit hash to the URL.
if let Some(precise) = git.precise { if let Some(precise) = git.precise {
url.set_path(&format!("{}@{}", url.path(), precise)); let path = format!("{}@{}", url.path(), precise);
url.set_path(&path);
} else { } else {
// Otherwise, add the branch or tag name. // Otherwise, add the branch or tag name.
match git.reference { match git.reference {
@ -141,7 +140,8 @@ impl From<GitUrl> for Url {
| GitReference::BranchOrTag(rev) | GitReference::BranchOrTag(rev)
| GitReference::NamedRef(rev) | GitReference::NamedRef(rev)
| GitReference::BranchOrTagOrCommit(rev) => { | GitReference::BranchOrTagOrCommit(rev) => {
url.set_path(&format!("{}@{}", url.path(), rev)); let path = format!("{}@{}", url.path(), rev);
url.set_path(&path);
} }
GitReference::DefaultBranch => {} GitReference::DefaultBranch => {}
} }
@ -153,6 +153,6 @@ impl From<GitUrl> for Url {
impl std::fmt::Display for GitUrl { impl std::fmt::Display for GitUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", redacted_url(&self.repository)) write!(f, "{}", &self.repository)
} }
} }

View file

@ -1,10 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock}; use std::sync::{Arc, LazyLock, RwLock};
use tracing::trace; use tracing::trace;
use url::Url;
use uv_auth::Credentials; use uv_auth::Credentials;
use uv_cache_key::RepositoryUrl; use uv_cache_key::RepositoryUrl;
use uv_redacted::redacted_url; use uv_redacted::DisplaySafeUrl;
/// Global authentication cache for a uv invocation. /// Global authentication cache for a uv invocation.
/// ///
@ -30,9 +29,9 @@ impl GitStore {
/// Populate the global authentication store with credentials on a Git URL, if there are any. /// Populate the global authentication store with credentials on a Git URL, if there are any.
/// ///
/// Returns `true` if the store was updated. /// Returns `true` if the store was updated.
pub fn store_credentials_from_url(url: &Url) -> bool { pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool {
if let Some(credentials) = Credentials::from_url(url) { if let Some(credentials) = Credentials::from_url(url) {
trace!("Caching credentials for {}", redacted_url(url)); trace!("Caching credentials for {url}");
GIT_STORE.insert(RepositoryUrl::new(url), credentials); GIT_STORE.insert(RepositoryUrl::new(url), credentials);
true true
} else { } else {

View file

@ -16,6 +16,7 @@ use url::Url;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_git_types::{GitHubRepository, GitOid, GitReference}; use uv_git_types::{GitHubRepository, GitOid, GitReference};
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_version::version; use uv_version::version;
@ -132,7 +133,7 @@ impl Display for ReferenceOrOid<'_> {
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub(crate) struct GitRemote { pub(crate) struct GitRemote {
/// URL to a remote repository. /// URL to a remote repository.
url: Url, url: DisplaySafeUrl,
} }
/// A local clone of a remote repository's database. Multiple [`GitCheckout`]s /// A local clone of a remote repository's database. Multiple [`GitCheckout`]s
@ -205,12 +206,12 @@ impl GitRepository {
impl GitRemote { impl GitRemote {
/// Creates an instance for a remote repository URL. /// Creates an instance for a remote repository URL.
pub(crate) fn new(url: &Url) -> Self { pub(crate) fn new(url: &DisplaySafeUrl) -> Self {
Self { url: url.clone() } Self { url: url.clone() }
} }
/// Gets the remote repository URL. /// Gets the remote repository URL.
pub(crate) fn url(&self) -> &Url { pub(crate) fn url(&self) -> &DisplaySafeUrl {
&self.url &self.url
} }

View file

@ -9,11 +9,10 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use tracing::{debug, instrument}; use tracing::{debug, instrument};
use url::Url;
use uv_cache_key::{RepositoryUrl, cache_digest}; use uv_cache_key::{RepositoryUrl, cache_digest};
use uv_git_types::GitUrl; use uv_git_types::GitUrl;
use uv_redacted::redacted_url; use uv_redacted::DisplaySafeUrl;
use crate::GIT_STORE; use crate::GIT_STORE;
use crate::git::GitRemote; use crate::git::GitRemote;
@ -101,10 +100,7 @@ impl GitSource {
// situation that we have a locked revision but the database // situation that we have a locked revision but the database
// doesn't have it. // doesn't have it.
(locked_rev, db) => { (locked_rev, db) => {
debug!( debug!("Updating Git source `{}`", self.git.repository());
"Updating Git source `{}`",
redacted_url(self.git.repository())
);
// Report the checkout operation to the reporter. // Report the checkout operation to the reporter.
let task = self.reporter.as_ref().map(|reporter| { let task = self.reporter.as_ref().map(|reporter| {
@ -181,8 +177,8 @@ impl Fetch {
pub trait Reporter: Send + Sync { pub trait Reporter: Send + Sync {
/// Callback to invoke when a repository checkout begins. /// Callback to invoke when a repository checkout begins.
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
/// Callback to invoke when a repository checkout completes. /// Callback to invoke when a repository checkout completes.
fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize); fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize);
} }

View file

@ -31,6 +31,7 @@ uv-pep508 = { workspace = true }
uv-platform-tags = { workspace = true } uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-python = { workspace = true } uv-python = { workspace = true }
uv-redacted = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }

View file

@ -3,7 +3,6 @@ use std::sync::Arc;
use futures::{FutureExt, Stream, TryFutureExt, TryStreamExt, stream::FuturesUnordered}; use futures::{FutureExt, Stream, TryFutureExt, TryStreamExt, stream::FuturesUnordered};
use tracing::{debug, instrument}; use tracing::{debug, instrument};
use url::Url;
use uv_cache::Cache; use uv_cache::Cache;
use uv_configuration::BuildOptions; use uv_configuration::BuildOptions;
@ -14,6 +13,7 @@ use uv_distribution_types::{
}; };
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_platform_tags::Tags; use uv_platform_tags::Tags;
use uv_redacted::DisplaySafeUrl;
use uv_types::{BuildContext, HashStrategy, InFlight}; use uv_types::{BuildContext, HashStrategy, InFlight};
/// Prepare distributions for installation. /// Prepare distributions for installation.
@ -268,10 +268,10 @@ pub trait Reporter: Send + Sync {
fn on_build_complete(&self, source: &BuildableSource, id: usize); fn on_build_complete(&self, source: &BuildableSource, id: usize);
/// Callback to invoke when a repository checkout begins. /// Callback to invoke when a repository checkout begins.
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
/// Callback to invoke when a repository checkout completes. /// Callback to invoke when a repository checkout completes.
fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize); fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize);
} }
impl dyn Reporter { impl dyn Reporter {
@ -299,11 +299,11 @@ impl uv_distribution::Reporter for Facade {
self.reporter.on_build_complete(source, id); self.reporter.on_build_complete(source, id);
} }
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
self.reporter.on_checkout_start(url, rev) self.reporter.on_checkout_start(url, rev)
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize) {
self.reporter.on_checkout_complete(url, rev, index); self.reporter.on_checkout_complete(url, rev, index);
} }

View file

@ -6,7 +6,6 @@ use std::path::PathBuf;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use fs_err as fs; use fs_err as fs;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use url::Url;
use uv_distribution_types::{ use uv_distribution_types::{
Diagnostic, InstalledDist, Name, NameRequirementSpecification, Requirement, Diagnostic, InstalledDist, Name, NameRequirementSpecification, Requirement,
@ -18,6 +17,7 @@ use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::VersionOrUrl; use uv_pep508::VersionOrUrl;
use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl};
use uv_python::{Interpreter, PythonEnvironment}; use uv_python::{Interpreter, PythonEnvironment};
use uv_redacted::DisplaySafeUrl;
use uv_types::InstalledPackagesProvider; use uv_types::InstalledPackagesProvider;
use uv_warnings::warn_user; use uv_warnings::warn_user;
@ -38,7 +38,7 @@ pub struct SitePackages {
/// virtual environment, which we handle gracefully. /// virtual environment, which we handle gracefully.
by_name: FxHashMap<PackageName, Vec<usize>>, by_name: FxHashMap<PackageName, Vec<usize>>,
/// The installed editable distributions, keyed by URL. /// The installed editable distributions, keyed by URL.
by_url: FxHashMap<Url, Vec<usize>>, by_url: FxHashMap<DisplaySafeUrl, Vec<usize>>,
} }
impl SitePackages { impl SitePackages {
@ -174,7 +174,7 @@ impl SitePackages {
} }
/// Returns the distributions installed from the given URL, if any. /// Returns the distributions installed from the given URL, if any.
pub fn get_urls(&self, url: &Url) -> Vec<&InstalledDist> { pub fn get_urls(&self, url: &DisplaySafeUrl) -> Vec<&InstalledDist> {
let Some(indexes) = self.by_url.get(url) else { let Some(indexes) = self.by_url.get(url) else {
return Vec::new(); return Vec::new();
}; };

View file

@ -22,6 +22,7 @@ workspace = true
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-redacted = { workspace = true }
arcstr = { workspace = true} arcstr = { workspace = true}
boxcar = { workspace = true } boxcar = { workspace = true }

View file

@ -144,23 +144,50 @@ impl<T: Pep508Url> Requirement<T> {
self.version_or_url = None; self.version_or_url = None;
} }
} }
/// Returns a [`Display`] implementation that doesn't mask credentials.
pub fn displayable_with_credentials(&self) -> impl Display {
RequirementDisplay {
requirement: self,
display_credentials: true,
}
}
} }
impl<T: Pep508Url + Display> Display for Requirement<T> { impl<T: Pep508Url + Display> Display for Requirement<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?; RequirementDisplay {
if !self.extras.is_empty() { requirement: self,
display_credentials: false,
}
.fmt(f)
}
}
struct RequirementDisplay<'a, T>
where
T: Pep508Url + Display,
{
requirement: &'a Requirement<T>,
display_credentials: bool,
}
impl<T: Pep508Url + Display> Display for RequirementDisplay<'_, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.requirement.name)?;
if !self.requirement.extras.is_empty() {
write!( write!(
f, f,
"[{}]", "[{}]",
self.extras self.requirement
.extras
.iter() .iter()
.map(ToString::to_string) .map(ToString::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(",") .join(",")
)?; )?;
} }
if let Some(version_or_url) = &self.version_or_url { if let Some(version_or_url) = &self.requirement.version_or_url {
match version_or_url { match version_or_url {
VersionOrUrl::VersionSpecifier(version_specifier) => { VersionOrUrl::VersionSpecifier(version_specifier) => {
let version_specifier: Vec<String> = let version_specifier: Vec<String> =
@ -168,12 +195,17 @@ impl<T: Pep508Url + Display> Display for Requirement<T> {
write!(f, "{}", version_specifier.join(","))?; write!(f, "{}", version_specifier.join(","))?;
} }
VersionOrUrl::Url(url) => { VersionOrUrl::Url(url) => {
let url_string = if self.display_credentials {
url.displayable_with_credentials().to_string()
} else {
url.to_string()
};
// We add the space for markers later if necessary // We add the space for markers later if necessary
write!(f, " @ {url}")?; write!(f, " @ {url_string}")?;
} }
} }
} }
if let Some(marker) = self.marker.contents() { if let Some(marker) = self.requirement.marker.contents() {
write!(f, " ; {marker}")?; write!(f, " ; {marker}")?;
} }
Ok(()) Ok(())
@ -255,6 +287,9 @@ pub trait Pep508Url: Display + Debug + Sized {
/// Parse a url from `name @ <url>`. Defaults to [`Url::parse_url`]. /// Parse a url from `name @ <url>`. Defaults to [`Url::parse_url`].
fn parse_url(url: &str, working_dir: Option<&Path>) -> Result<Self, Self::Err>; fn parse_url(url: &str, working_dir: Option<&Path>) -> Result<Self, Self::Err>;
/// Returns a [`Display`] implementation that doesn't mask credentials.
fn displayable_with_credentials(&self) -> impl Display;
} }
impl Pep508Url for Url { impl Pep508Url for Url {
@ -263,6 +298,10 @@ impl Pep508Url for Url {
fn parse_url(url: &str, _working_dir: Option<&Path>) -> Result<Self, Self::Err> { fn parse_url(url: &str, _working_dir: Option<&Path>) -> Result<Self, Self::Err> {
Url::parse(url) Url::parse(url)
} }
fn displayable_with_credentials(&self) -> impl Display {
self
}
} }
/// A reporter for warnings that occur during marker parsing or evaluation. /// A reporter for warnings that occur during marker parsing or evaluation.

View file

@ -66,9 +66,9 @@ impl UnnamedRequirementUrl for VerbatimUrl {
/// dependencies. This isn't compliant with PEP 508, but is common in `requirements.txt`, which /// dependencies. This isn't compliant with PEP 508, but is common in `requirements.txt`, which
/// is implementation-defined. /// is implementation-defined.
#[derive(Hash, Debug, Clone, Eq, PartialEq)] #[derive(Hash, Debug, Clone, Eq, PartialEq)]
pub struct UnnamedRequirement<Url: UnnamedRequirementUrl = VerbatimUrl> { pub struct UnnamedRequirement<ReqUrl: UnnamedRequirementUrl = VerbatimUrl> {
/// The direct URL that defines the version specifier. /// The direct URL that defines the version specifier.
pub url: Url, pub url: ReqUrl,
/// The list of extras such as `security`, `tests` in /// The list of extras such as `security`, `tests` in
/// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`.
pub extras: Box<[ExtraName]>, pub extras: Box<[ExtraName]>,

View file

@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::Debug; use std::fmt::{Debug, Display};
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -13,6 +13,7 @@ use url::{ParseError, Url};
#[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_imports))] #[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_imports))]
use uv_fs::{normalize_absolute_path, normalize_url_path}; use uv_fs::{normalize_absolute_path, normalize_url_path};
use uv_redacted::DisplaySafeUrl;
use crate::Pep508Url; use crate::Pep508Url;
@ -20,7 +21,7 @@ use crate::Pep508Url;
#[derive(Debug, Clone, Eq)] #[derive(Debug, Clone, Eq)]
pub struct VerbatimUrl { pub struct VerbatimUrl {
/// The parsed URL. /// The parsed URL.
url: Url, url: DisplaySafeUrl,
/// The URL as it was provided by the user. /// The URL as it was provided by the user.
given: Option<ArcStr>, given: Option<ArcStr>,
} }
@ -39,14 +40,17 @@ impl PartialEq for VerbatimUrl {
impl VerbatimUrl { impl VerbatimUrl {
/// Create a [`VerbatimUrl`] from a [`Url`]. /// Create a [`VerbatimUrl`] from a [`Url`].
pub fn from_url(url: Url) -> Self { pub fn from_url(url: DisplaySafeUrl) -> Self {
Self { url, given: None } Self { url, given: None }
} }
/// 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 = Url::parse(given.as_ref())?;
Ok(Self { url, given: None }) Ok(Self {
url: DisplaySafeUrl::from(url),
given: None,
})
} }
/// Parse a URL from an absolute or relative path. /// Parse a URL from an absolute or relative path.
@ -72,8 +76,10 @@ 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 = Url::from_file_path(path.clone()) let mut url = DisplaySafeUrl::from(
.map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?; Url::from_file_path(path.clone())
.map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?,
);
// Set the fragment, if it exists. // Set the fragment, if it exists.
if let Some(fragment) = fragment { if let Some(fragment) = fragment {
@ -102,8 +108,10 @@ 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 = Url::from_file_path(path.clone()) let mut url = DisplaySafeUrl::from(
.unwrap_or_else(|()| panic!("path is absolute: {}", path.display())); Url::from_file_path(path.clone())
.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 {
@ -130,8 +138,10 @@ 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 = Url::from_file_path(path.clone()) let mut url = DisplaySafeUrl::from(
.unwrap_or_else(|()| panic!("path is absolute: {}", path.display())); Url::from_file_path(path.clone())
.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 {
@ -155,18 +165,18 @@ impl VerbatimUrl {
self.given.as_deref() self.given.as_deref()
} }
/// Return the underlying [`Url`]. /// Return the underlying [`DisplaySafeUrl`].
pub fn raw(&self) -> &Url { pub fn raw(&self) -> &DisplaySafeUrl {
&self.url &self.url
} }
/// Convert a [`VerbatimUrl`] into a [`Url`]. /// Convert a [`VerbatimUrl`] into a [`DisplaySafeUrl`].
pub fn to_url(&self) -> Url { pub fn to_url(&self) -> DisplaySafeUrl {
self.url.clone() self.url.clone()
} }
/// Convert a [`VerbatimUrl`] into a [`Url`]. /// Convert a [`VerbatimUrl`] into a [`DisplaySafeUrl`].
pub fn into_url(self) -> Url { pub fn into_url(self) -> DisplaySafeUrl {
self.url self.url
} }
@ -206,7 +216,7 @@ impl std::fmt::Display for VerbatimUrl {
} }
impl Deref for VerbatimUrl { impl Deref for VerbatimUrl {
type Target = Url; type Target = DisplaySafeUrl;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.url &self.url
@ -215,10 +225,22 @@ impl Deref for VerbatimUrl {
impl From<Url> for VerbatimUrl { impl From<Url> for VerbatimUrl {
fn from(url: Url) -> Self { fn from(url: Url) -> Self {
VerbatimUrl::from_url(DisplaySafeUrl::from(url))
}
}
impl From<DisplaySafeUrl> for VerbatimUrl {
fn from(url: DisplaySafeUrl) -> Self {
VerbatimUrl::from_url(url) VerbatimUrl::from_url(url)
} }
} }
impl From<VerbatimUrl> for Url {
fn from(url: VerbatimUrl) -> Self {
Url::from(url.url)
}
}
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
impl serde::Serialize for VerbatimUrl { impl serde::Serialize for VerbatimUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@ -235,7 +257,7 @@ impl<'de> serde::Deserialize<'de> for VerbatimUrl {
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let url = Url::deserialize(deserializer)?; let url = DisplaySafeUrl::deserialize(deserializer)?;
Ok(VerbatimUrl::from_url(url)) Ok(VerbatimUrl::from_url(url))
} }
} }
@ -314,6 +336,10 @@ impl Pep508Url for VerbatimUrl {
Err(Self::Err::NotAUrl(expanded.to_string())) Err(Self::Err::NotAUrl(expanded.to_string()))
} }
} }
fn displayable_with_credentials(&self) -> impl Display {
self.url.to_string_with_credentials()
}
} }
/// An error that can occur when parsing a [`VerbatimUrl`]. /// An error that can occur when parsing a [`VerbatimUrl`].

View file

@ -23,6 +23,7 @@ uv-extract = { workspace = true }
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-metadata = { workspace = true } uv-metadata = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }

View file

@ -38,6 +38,7 @@ use uv_extract::hash::{HashReader, Hasher};
use uv_fs::{ProgressReader, Simplified}; use uv_fs::{ProgressReader, Simplified};
use uv_metadata::read_metadata_async_seek; use uv_metadata::read_metadata_async_seek;
use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata23, MetadataError}; use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata23, MetadataError};
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
@ -59,7 +60,7 @@ pub enum PublishError {
#[error("Failed to publish: `{}`", _0.user_display())] #[error("Failed to publish: `{}`", _0.user_display())]
PublishPrepare(PathBuf, #[source] Box<PublishPrepareError>), PublishPrepare(PathBuf, #[source] Box<PublishPrepareError>),
#[error("Failed to publish `{}` to {}", _0.user_display(), _1)] #[error("Failed to publish `{}` to {}", _0.user_display(), _1)]
PublishSend(PathBuf, Url, #[source] PublishSendError), PublishSend(PathBuf, DisplaySafeUrl, #[source] PublishSendError),
#[error("Failed to obtain token for trusted publishing")] #[error("Failed to obtain token for trusted publishing")]
TrustedPublishing(#[from] TrustedPublishingError), TrustedPublishing(#[from] TrustedPublishingError),
#[error("{0} are not allowed when using trusted publishing")] #[error("{0} are not allowed when using trusted publishing")]
@ -308,7 +309,7 @@ pub async fn check_trusted_publishing(
password: Option<&str>, password: Option<&str>,
keyring_provider: KeyringProviderType, keyring_provider: KeyringProviderType,
trusted_publishing: TrustedPublishing, trusted_publishing: TrustedPublishing,
registry: &Url, registry: &DisplaySafeUrl,
client: &BaseClient, client: &BaseClient,
) -> Result<TrustedPublishResult, PublishError> { ) -> Result<TrustedPublishResult, PublishError> {
match trusted_publishing { match trusted_publishing {
@ -379,7 +380,7 @@ pub async fn upload(
file: &Path, file: &Path,
raw_filename: &str, raw_filename: &str,
filename: &DistFilename, filename: &DistFilename,
registry: &Url, registry: &DisplaySafeUrl,
client: &BaseClient, client: &BaseClient,
credentials: &Credentials, credentials: &Credentials,
check_url_client: Option<&CheckUrlClient<'_>>, check_url_client: Option<&CheckUrlClient<'_>>,
@ -751,7 +752,7 @@ async fn build_request(
file: &Path, file: &Path,
raw_filename: &str, raw_filename: &str,
filename: &DistFilename, filename: &DistFilename,
registry: &Url, registry: &DisplaySafeUrl,
client: &BaseClient, client: &BaseClient,
credentials: &Credentials, credentials: &Credentials,
form_metadata: &[(&'static str, String)], form_metadata: &[(&'static str, String)],
@ -790,7 +791,7 @@ async fn build_request(
let mut request = client let mut request = client
.for_host(&url) .for_host(&url)
.post(url) .post(Url::from(url))
.multipart(form) .multipart(form)
// Ask PyPI for a structured error messages instead of HTML-markup error messages. // Ask PyPI for a structured error messages instead of HTML-markup error messages.
// For other registries, we ask them to return plain text over HTML. See // For other registries, we ask them to return plain text over HTML. See
@ -889,10 +890,10 @@ mod tests {
use itertools::Itertools; use itertools::Itertools;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use url::Url;
use uv_auth::Credentials; use uv_auth::Credentials;
use uv_client::BaseClientBuilder; use uv_client::BaseClientBuilder;
use uv_distribution_filename::DistFilename; use uv_distribution_filename::DistFilename;
use uv_redacted::DisplaySafeUrl;
struct DummyReporter; struct DummyReporter;
@ -972,7 +973,7 @@ mod tests {
&file, &file,
raw_filename, raw_filename,
&filename, &filename,
&Url::parse("https://example.org/upload").unwrap(), &DisplaySafeUrl::parse("https://example.org/upload").unwrap(),
&BaseClientBuilder::new().build(), &BaseClientBuilder::new().build(),
&Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())),
&form_metadata, &form_metadata,
@ -1121,7 +1122,7 @@ mod tests {
&file, &file,
raw_filename, raw_filename,
&filename, &filename,
&Url::parse("https://example.org/upload").unwrap(), &DisplaySafeUrl::parse("https://example.org/upload").unwrap(),
&BaseClientBuilder::new().build(), &BaseClientBuilder::new().build(),
&Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())),
&form_metadata, &form_metadata,

View file

@ -12,6 +12,7 @@ use std::fmt::Display;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, trace}; use tracing::{debug, trace};
use url::Url; use url::Url;
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars; use uv_static::EnvVars;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -23,9 +24,9 @@ pub enum TrustedPublishingError {
#[error(transparent)] #[error(transparent)]
Url(#[from] url::ParseError), Url(#[from] url::ParseError),
#[error("Failed to fetch: `{0}`")] #[error("Failed to fetch: `{0}`")]
Reqwest(Url, #[source] reqwest::Error), Reqwest(DisplaySafeUrl, #[source] reqwest::Error),
#[error("Failed to fetch: `{0}`")] #[error("Failed to fetch: `{0}`")]
ReqwestMiddleware(Url, #[source] reqwest_middleware::Error), ReqwestMiddleware(DisplaySafeUrl, #[source] reqwest_middleware::Error),
#[error(transparent)] #[error(transparent)]
SerdeJson(#[from] serde_json::error::Error), SerdeJson(#[from] serde_json::error::Error),
#[error( #[error(
@ -94,7 +95,7 @@ pub struct OidcTokenClaims {
/// Returns the short-lived token to use for uploading. /// Returns the short-lived token to use for uploading.
pub(crate) async fn get_token( pub(crate) async fn get_token(
registry: &Url, registry: &DisplaySafeUrl,
client: &ClientWithMiddleware, client: &ClientWithMiddleware,
) -> Result<TrustedPublishingToken, TrustedPublishingError> { ) -> Result<TrustedPublishingToken, TrustedPublishingError> {
// If this fails, we can skip the audience request. // If this fails, we can skip the audience request.
@ -124,15 +125,16 @@ pub(crate) async fn get_token(
} }
async fn get_audience( async fn get_audience(
registry: &Url, registry: &DisplaySafeUrl,
client: &ClientWithMiddleware, client: &ClientWithMiddleware,
) -> Result<String, TrustedPublishingError> { ) -> Result<String, TrustedPublishingError> {
// `pypa/gh-action-pypi-publish` uses `netloc` (RFC 1808), which is deprecated for authority // `pypa/gh-action-pypi-publish` uses `netloc` (RFC 1808), which is deprecated for authority
// (RFC 3986). // (RFC 3986).
let audience_url = Url::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?; let audience_url =
DisplaySafeUrl::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?;
debug!("Querying the trusted publishing audience from {audience_url}"); debug!("Querying the trusted publishing audience from {audience_url}");
let response = client let response = client
.get(audience_url.clone()) .get(Url::from(audience_url.clone()))
.send() .send()
.await .await
.map_err(|err| TrustedPublishingError::ReqwestMiddleware(audience_url.clone(), err))?; .map_err(|err| TrustedPublishingError::ReqwestMiddleware(audience_url.clone(), err))?;
@ -154,14 +156,14 @@ async fn get_oidc_token(
let oidc_token_url = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL).map_err(|err| { let oidc_token_url = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL).map_err(|err| {
TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL, err) TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL, err)
})?; })?;
let mut oidc_token_url = Url::parse(&oidc_token_url)?; let mut oidc_token_url = DisplaySafeUrl::parse(&oidc_token_url)?;
oidc_token_url oidc_token_url
.query_pairs_mut() .query_pairs_mut()
.append_pair("audience", audience); .append_pair("audience", audience);
debug!("Querying the trusted publishing OIDC token from {oidc_token_url}"); debug!("Querying the trusted publishing OIDC token from {oidc_token_url}");
let authorization = format!("bearer {oidc_token_request_token}"); let authorization = format!("bearer {oidc_token_request_token}");
let response = client let response = client
.get(oidc_token_url.clone()) .get(Url::from(oidc_token_url.clone()))
.header(header::AUTHORIZATION, authorization) .header(header::AUTHORIZATION, authorization)
.send() .send()
.await .await
@ -188,11 +190,11 @@ fn decode_oidc_token(oidc_token: &str) -> Option<OidcTokenClaims> {
} }
async fn get_publish_token( async fn get_publish_token(
registry: &Url, registry: &DisplaySafeUrl,
oidc_token: &str, oidc_token: &str,
client: &ClientWithMiddleware, client: &ClientWithMiddleware,
) -> Result<TrustedPublishingToken, TrustedPublishingError> { ) -> Result<TrustedPublishingToken, TrustedPublishingError> {
let mint_token_url = Url::parse(&format!( let mint_token_url = DisplaySafeUrl::parse(&format!(
"https://{}/_/oidc/mint-token", "https://{}/_/oidc/mint-token",
registry.authority() registry.authority()
))?; ))?;
@ -201,7 +203,7 @@ async fn get_publish_token(
token: oidc_token.to_string(), token: oidc_token.to_string(),
}; };
let response = client let response = client
.post(mint_token_url.clone()) .post(Url::from(mint_token_url.clone()))
.body(serde_json::to_vec(&mint_token_payload)?) .body(serde_json::to_vec(&mint_token_payload)?)
.send() .send()
.await .await

View file

@ -21,6 +21,7 @@ uv-git-types = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true } uv-small-str = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }

View file

@ -1,9 +1,12 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use uv_redacted::DisplaySafeUrl;
/// Join a relative URL to a base URL. /// Join a relative URL to a base URL.
pub fn base_url_join_relative(base: &str, relative: &str) -> Result<Url, JoinRelativeError> { pub fn base_url_join_relative(
let base_url = Url::parse(base).map_err(|err| JoinRelativeError::ParseError { base: &str,
relative: &str,
) -> Result<DisplaySafeUrl, JoinRelativeError> {
let base_url = DisplaySafeUrl::parse(base).map_err(|err| JoinRelativeError::ParseError {
original: base.to_string(), original: base.to_string(),
source: err, source: err,
})?; })?;
@ -32,26 +35,26 @@ pub enum JoinRelativeError {
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct BaseUrl( pub struct BaseUrl(
#[serde( #[serde(
serialize_with = "Url::serialize_internal", serialize_with = "DisplaySafeUrl::serialize_internal",
deserialize_with = "Url::deserialize_internal" deserialize_with = "DisplaySafeUrl::deserialize_internal"
)] )]
Url, DisplaySafeUrl,
); );
impl BaseUrl { impl BaseUrl {
/// Return the underlying [`Url`]. /// Return the underlying [`DisplaySafeUrl`].
pub fn as_url(&self) -> &Url { pub fn as_url(&self) -> &DisplaySafeUrl {
&self.0 &self.0
} }
/// Return the underlying [`Url`] as a serialized string. /// Return the underlying [`DisplaySafeUrl`] as a serialized string.
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
self.0.as_str() self.0.as_str()
} }
} }
impl From<Url> for BaseUrl { impl From<DisplaySafeUrl> for BaseUrl {
fn from(url: Url) -> Self { fn from(url: DisplaySafeUrl) -> Self {
Self(url) Self(url)
} }
} }

View file

@ -9,6 +9,7 @@ use uv_git_types::{GitUrl, GitUrlParseError};
use uv_pep508::{ use uv_pep508::{
Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError, looks_like_git_repository, Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError, looks_like_git_repository,
}; };
use uv_redacted::DisplaySafeUrl;
use crate::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind}; use crate::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind};
@ -61,6 +62,10 @@ impl Pep508Url for VerbatimParsedUrl {
verbatim, verbatim,
}) })
} }
fn displayable_with_credentials(&self) -> impl Display {
self.verbatim.displayable_with_credentials()
}
} }
impl UnnamedRequirementUrl for VerbatimParsedUrl { impl UnnamedRequirementUrl for VerbatimParsedUrl {
@ -194,7 +199,7 @@ impl ParsedUrl {
/// * `file:///home/ferris/my_project/my_project-0.1.0-py3-none-any.whl` /// * `file:///home/ferris/my_project/my_project-0.1.0-py3-none-any.whl`
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)]
pub struct ParsedPathUrl { pub struct ParsedPathUrl {
pub url: Url, pub url: DisplaySafeUrl,
/// The absolute path to the distribution which we use for installing. /// The absolute path to the distribution which we use for installing.
pub install_path: Box<Path>, pub install_path: Box<Path>,
/// The file extension, e.g. `tar.gz`, `zip`, etc. /// The file extension, e.g. `tar.gz`, `zip`, etc.
@ -203,7 +208,7 @@ pub struct ParsedPathUrl {
impl ParsedPathUrl { impl ParsedPathUrl {
/// Construct a [`ParsedPathUrl`] from a path requirement source. /// Construct a [`ParsedPathUrl`] from a path requirement source.
pub fn from_source(install_path: Box<Path>, ext: DistExtension, url: Url) -> Self { pub fn from_source(install_path: Box<Path>, ext: DistExtension, url: DisplaySafeUrl) -> Self {
Self { Self {
url, url,
install_path, install_path,
@ -218,7 +223,7 @@ impl ParsedPathUrl {
/// * `file:///home/ferris/my_project` /// * `file:///home/ferris/my_project`
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)]
pub struct ParsedDirectoryUrl { pub struct ParsedDirectoryUrl {
pub url: Url, pub url: DisplaySafeUrl,
/// The absolute path to the distribution which we use for installing. /// The absolute path to the distribution which we use for installing.
pub install_path: Box<Path>, pub install_path: Box<Path>,
pub editable: bool, pub editable: bool,
@ -227,7 +232,12 @@ pub struct ParsedDirectoryUrl {
impl ParsedDirectoryUrl { impl ParsedDirectoryUrl {
/// Construct a [`ParsedDirectoryUrl`] from a path requirement source. /// Construct a [`ParsedDirectoryUrl`] from a path requirement source.
pub fn from_source(install_path: Box<Path>, editable: bool, r#virtual: bool, url: Url) -> Self { pub fn from_source(
install_path: Box<Path>,
editable: bool,
r#virtual: bool,
url: DisplaySafeUrl,
) -> Self {
Self { Self {
url, url,
install_path, install_path,
@ -255,21 +265,22 @@ impl ParsedGitUrl {
} }
} }
impl TryFrom<Url> for ParsedGitUrl { impl TryFrom<DisplaySafeUrl> for ParsedGitUrl {
type Error = ParsedUrlError; type Error = ParsedUrlError;
/// Supports URLs with and without the `git+` prefix. /// Supports URLs with and without the `git+` prefix.
/// ///
/// When the URL includes a prefix, it's presumed to come from a PEP 508 requirement; when it's /// When the URL includes a prefix, it's presumed to come from a PEP 508 requirement; when it's
/// excluded, it's presumed to come from `tool.uv.sources`. /// excluded, it's presumed to come from `tool.uv.sources`.
fn try_from(url_in: Url) -> Result<Self, Self::Error> { fn try_from(url_in: DisplaySafeUrl) -> Result<Self, Self::Error> {
let subdirectory = get_subdirectory(&url_in).map(PathBuf::into_boxed_path); let subdirectory = get_subdirectory(&url_in).map(PathBuf::into_boxed_path);
let url = url_in let url = url_in
.as_str() .as_str()
.strip_prefix("git+") .strip_prefix("git+")
.unwrap_or(url_in.as_str()); .unwrap_or(url_in.as_str());
let url = Url::parse(url).map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?; let url = DisplaySafeUrl::parse(url)
.map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?;
let url = GitUrl::try_from(url)?; let url = GitUrl::try_from(url)?;
Ok(Self { url, subdirectory }) Ok(Self { url, subdirectory })
} }
@ -283,14 +294,18 @@ impl TryFrom<Url> for ParsedGitUrl {
/// * A source dist with a recognizable extension but invalid name: `https://github.com/foo-labs/foo/archive/master.zip#egg=pkg&subdirectory=packages/bar` /// * A source dist with a recognizable extension but invalid name: `https://github.com/foo-labs/foo/archive/master.zip#egg=pkg&subdirectory=packages/bar`
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ParsedArchiveUrl { pub struct ParsedArchiveUrl {
pub url: Url, pub url: DisplaySafeUrl,
pub subdirectory: Option<Box<Path>>, pub subdirectory: Option<Box<Path>>,
pub ext: DistExtension, pub ext: DistExtension,
} }
impl ParsedArchiveUrl { impl ParsedArchiveUrl {
/// Construct a [`ParsedArchiveUrl`] from a URL requirement source. /// Construct a [`ParsedArchiveUrl`] from a URL requirement source.
pub fn from_source(location: Url, subdirectory: Option<Box<Path>>, ext: DistExtension) -> Self { pub fn from_source(
location: DisplaySafeUrl,
subdirectory: Option<Box<Path>>,
ext: DistExtension,
) -> Self {
Self { Self {
url: location, url: location,
subdirectory, subdirectory,
@ -299,10 +314,10 @@ impl ParsedArchiveUrl {
} }
} }
impl TryFrom<Url> for ParsedArchiveUrl { impl TryFrom<DisplaySafeUrl> for ParsedArchiveUrl {
type Error = ParsedUrlError; type Error = ParsedUrlError;
fn try_from(mut url: Url) -> Result<Self, Self::Error> { fn try_from(mut url: DisplaySafeUrl) -> Result<Self, Self::Error> {
// Extract the `#subdirectory` fragment, if present. // Extract the `#subdirectory` fragment, if present.
let subdirectory = get_subdirectory(&url).map(PathBuf::into_boxed_path); let subdirectory = get_subdirectory(&url).map(PathBuf::into_boxed_path);
url.set_fragment(None); url.set_fragment(None);
@ -338,10 +353,10 @@ fn get_subdirectory(url: &Url) -> Option<PathBuf> {
Some(PathBuf::from(subdirectory)) Some(PathBuf::from(subdirectory))
} }
impl TryFrom<Url> for ParsedUrl { impl TryFrom<DisplaySafeUrl> for ParsedUrl {
type Error = ParsedUrlError; type Error = ParsedUrlError;
fn try_from(url: Url) -> Result<Self, Self::Error> { fn try_from(url: DisplaySafeUrl) -> Result<Self, Self::Error> {
if let Some((prefix, ..)) = url.scheme().split_once('+') { if let Some((prefix, ..)) = url.scheme().split_once('+') {
match prefix { match prefix {
"git" => Ok(Self::Git(ParsedGitUrl::try_from(url)?)), "git" => Ok(Self::Git(ParsedGitUrl::try_from(url)?)),
@ -464,7 +479,7 @@ impl From<&ParsedGitUrl> for DirectUrl {
} }
} }
impl From<ParsedUrl> for Url { impl From<ParsedUrl> for DisplaySafeUrl {
fn from(value: ParsedUrl) -> Self { fn from(value: ParsedUrl) -> Self {
match value { match value {
ParsedUrl::Path(value) => value.into(), ParsedUrl::Path(value) => value.into(),
@ -475,19 +490,19 @@ impl From<ParsedUrl> for Url {
} }
} }
impl From<ParsedPathUrl> for Url { impl From<ParsedPathUrl> for DisplaySafeUrl {
fn from(value: ParsedPathUrl) -> Self { fn from(value: ParsedPathUrl) -> Self {
value.url value.url
} }
} }
impl From<ParsedDirectoryUrl> for Url { impl From<ParsedDirectoryUrl> for DisplaySafeUrl {
fn from(value: ParsedDirectoryUrl) -> Self { fn from(value: ParsedDirectoryUrl) -> Self {
value.url value.url
} }
} }
impl From<ParsedArchiveUrl> for Url { impl From<ParsedArchiveUrl> for DisplaySafeUrl {
fn from(value: ParsedArchiveUrl) -> Self { fn from(value: ParsedArchiveUrl) -> Self {
let mut url = value.url; let mut url = value.url;
if let Some(subdirectory) = value.subdirectory { if let Some(subdirectory) = value.subdirectory {
@ -497,7 +512,7 @@ impl From<ParsedArchiveUrl> for Url {
} }
} }
impl From<ParsedGitUrl> for Url { impl From<ParsedGitUrl> for DisplaySafeUrl {
fn from(value: ParsedGitUrl) -> Self { fn from(value: ParsedGitUrl) -> Self {
let mut url = Self::parse(&format!("{}{}", "git+", Self::from(value.url).as_str())) let mut url = Self::parse(&format!("{}{}", "git+", Self::from(value.url).as_str()))
.expect("Git URL is invalid"); .expect("Git URL is invalid");
@ -511,33 +526,36 @@ impl From<ParsedGitUrl> for Url {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::Result; use anyhow::Result;
use url::Url;
use crate::parsed_url::ParsedUrl; use crate::parsed_url::ParsedUrl;
use uv_redacted::DisplaySafeUrl;
#[test] #[test]
fn direct_url_from_url() -> Result<()> { fn direct_url_from_url() -> Result<()> {
let expected = Url::parse("git+https://github.com/pallets/flask.git")?; let expected = DisplaySafeUrl::parse("git+https://github.com/pallets/flask.git")?;
let actual = Url::from(ParsedUrl::try_from(expected.clone())?); let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual);
let expected = Url::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?;
let actual = Url::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual);
let expected = Url::parse("git+https://github.com/pallets/flask.git@2.0.0")?;
let actual = Url::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual); assert_eq!(expected, actual);
let expected = let expected =
Url::parse("git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir")?; DisplaySafeUrl::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?;
let actual = Url::from(ParsedUrl::try_from(expected.clone())?); let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual);
let expected = DisplaySafeUrl::parse("git+https://github.com/pallets/flask.git@2.0.0")?;
let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual);
let expected = DisplaySafeUrl::parse(
"git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir",
)?;
let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual); assert_eq!(expected, actual);
// TODO(charlie): Preserve other fragments. // TODO(charlie): Preserve other fragments.
let expected = let expected = DisplaySafeUrl::parse(
Url::parse("git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir")?; "git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir",
let actual = Url::from(ParsedUrl::try_from(expected.clone())?); )?;
let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?);
assert_ne!(expected, actual); assert_ne!(expected, actual);
Ok(()) Ok(())
@ -546,8 +564,8 @@ mod tests {
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn direct_url_from_url_absolute() -> Result<()> { fn direct_url_from_url_absolute() -> Result<()> {
let expected = Url::parse("file:///path/to/directory")?; let expected = DisplaySafeUrl::parse("file:///path/to/directory")?;
let actual = Url::from(ParsedUrl::try_from(expected.clone())?); let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?);
assert_eq!(expected, actual); assert_eq!(expected, actual);
Ok(()) Ok(())
} }

View file

@ -29,6 +29,7 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-platform-tags = { workspace = true } uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-state = { workspace = true } uv-state = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-trampoline-builder = { workspace = true } uv-trampoline-builder = { workspace = true }

View file

@ -26,6 +26,7 @@ use uv_distribution_filename::{ExtensionError, SourceDistExtension};
use uv_extract::hash::Hasher; use uv_extract::hash::Hasher;
use uv_fs::{Simplified, rename_with_retry}; use uv_fs::{Simplified, rename_with_retry};
use uv_pypi_types::{HashAlgorithm, HashDigest}; use uv_pypi_types::{HashAlgorithm, HashDigest};
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars; use uv_static::EnvVars;
use crate::PythonVariant; use crate::PythonVariant;
@ -51,9 +52,9 @@ pub enum Error {
#[error("Invalid request key (too many parts): {0}")] #[error("Invalid request key (too many parts): {0}")]
TooManyParts(String), TooManyParts(String),
#[error("Failed to download {0}")] #[error("Failed to download {0}")]
NetworkError(Url, #[source] WrappedReqwestError), NetworkError(DisplaySafeUrl, #[source] WrappedReqwestError),
#[error("Failed to download {0}")] #[error("Failed to download {0}")]
NetworkMiddlewareError(Url, #[source] anyhow::Error), NetworkMiddlewareError(DisplaySafeUrl, #[source] anyhow::Error),
#[error("Failed to extract archive: {0}")] #[error("Failed to extract archive: {0}")]
ExtractError(String, #[source] uv_extract::Error), ExtractError(String, #[source] uv_extract::Error),
#[error("Failed to hash installation")] #[error("Failed to hash installation")]
@ -1060,11 +1061,14 @@ fn parse_json_downloads(
} }
impl Error { impl Error {
pub(crate) fn from_reqwest(url: Url, err: reqwest::Error) -> Self { pub(crate) fn from_reqwest(url: DisplaySafeUrl, err: reqwest::Error) -> Self {
Self::NetworkError(url, WrappedReqwestError::from(err)) Self::NetworkError(url, WrappedReqwestError::from(err))
} }
pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { pub(crate) fn from_reqwest_middleware(
url: DisplaySafeUrl,
err: reqwest_middleware::Error,
) -> Self {
match err { match err {
reqwest_middleware::Error::Middleware(error) => { reqwest_middleware::Error::Middleware(error) => {
Self::NetworkMiddlewareError(url, error) Self::NetworkMiddlewareError(url, error)
@ -1155,6 +1159,7 @@ async fn read_url(
url: &Url, url: &Url,
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
@ -1167,8 +1172,8 @@ 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.clone()) .get(Url::from(url.clone()))
.send() .send()
.await .await
.map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?; .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?;
@ -1176,7 +1181,7 @@ async fn read_url(
// Ensure the request was successful. // Ensure the request was successful.
response response
.error_for_status_ref() .error_for_status_ref()
.map_err(|err| Error::from_reqwest(url.clone(), err))?; .map_err(|err| Error::from_reqwest(url, err))?;
let size = response.content_length(); let size = response.content_length();
let stream = response let stream = response

View file

@ -16,4 +16,9 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
schemars = { workspace = true, optional = true }
serde = { workspace = true }
url = { workspace = true } url = { workspace = true }
[features]
schemars = ["dep:schemars"]

View file

@ -1,21 +1,241 @@
use std::borrow::Cow; use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use url::Url; use url::Url;
/// Return a version of the URL with redacted credentials, allowing the generic `git` username (without a password) /// A [`Url`] wrapper that redacts credentials when displaying the URL.
/// in SSH URLs, as in, `ssh://git@github.com/...`. ///
pub fn redacted_url(url: &Url) -> Cow<'_, Url> { /// `DisplaySafeUrl` wraps the standard [`url::Url`] type, providing functionality to mask
if url.username().is_empty() && url.password().is_none() { /// secrets by default when the URL is displayed or logged. This helps prevent accidental
return Cow::Borrowed(url); /// exposure of sensitive information in logs and debug output.
} ///
if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() { /// # Examples
return Cow::Borrowed(url); ///
/// ```
/// use uv_redacted::DisplaySafeUrl;
/// use std::str::FromStr;
///
/// // Create a `DisplaySafeUrl` from a `&str`
/// let mut url = DisplaySafeUrl::parse("https://user:password@example.com").unwrap();
///
/// // Display will mask secrets
/// assert_eq!(url.to_string(), "https://user:****@example.com/");
///
/// // You can still access the username and password
/// assert_eq!(url.username(), "user");
/// assert_eq!(url.password(), Some("password"));
///
/// // And you can still update the username and password
/// let _ = url.set_username("new_user");
/// let _ = url.set_password(Some("new_password"));
/// assert_eq!(url.username(), "new_user");
/// assert_eq!(url.password(), Some("new_password"));
///
/// // It is also possible to remove the credentials entirely
/// url.remove_credentials();
/// assert_eq!(url.username(), "");
/// assert_eq!(url.password(), None);
/// ```
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(transparent))]
pub struct DisplaySafeUrl(Url);
impl DisplaySafeUrl {
#[inline]
pub fn parse(input: &str) -> Result<Self, url::ParseError> {
Ok(Self(Url::parse(input)?))
} }
let mut url = url.clone(); /// Parse a string as an URL, with this URL as the base URL.
let _ = url.set_username(""); #[inline]
let _ = url.set_password(None); pub fn join(&self, input: &str) -> Result<Self, url::ParseError> {
Cow::Owned(url) self.0.join(input).map(DisplaySafeUrl::from)
}
/// Serialize with Serde using the internal representation of the `Url` struct.
#[inline]
pub fn serialize_internal<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize_internal(serializer)
}
/// Serialize with Serde using the internal representation of the `Url` struct.
#[inline]
pub fn deserialize_internal<'de, D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Url::deserialize_internal(deserializer).map(DisplaySafeUrl::from)
}
#[allow(clippy::result_unit_err)]
pub fn from_file_path<P: AsRef<std::path::Path>>(path: P) -> Result<DisplaySafeUrl, ()> {
Url::from_file_path(path).map(DisplaySafeUrl::from)
}
/// Remove the credentials from a URL, allowing the generic `git` username (without a password)
/// in SSH URLs, as in, `ssh://git@github.com/...`.
#[inline]
pub fn remove_credentials(&mut self) {
// For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the
// username.
if self.0.scheme() == "ssh" && self.0.username() == "git" && self.0.password().is_none() {
return;
}
let _ = self.0.set_username("");
let _ = self.0.set_password(None);
}
/// Returns string representation without masking credentials.
#[inline]
pub fn to_string_with_credentials(&self) -> String {
self.0.to_string()
}
}
impl Deref for DisplaySafeUrl {
type Target = Url;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DisplaySafeUrl {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::fmt::Display for DisplaySafeUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt_with_obfuscated_credentials(&self.0, f)
}
}
impl std::fmt::Debug for DisplaySafeUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self}")
}
}
impl From<Url> for DisplaySafeUrl {
fn from(url: Url) -> Self {
DisplaySafeUrl(url)
}
}
impl From<DisplaySafeUrl> for Url {
fn from(url: DisplaySafeUrl) -> Self {
url.0
}
}
impl FromStr for DisplaySafeUrl {
type Err = url::ParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Ok(Self(Url::from_str(input)?))
}
}
fn fmt_with_obfuscated_credentials<W: Write>(url: &Url, mut f: W) -> std::fmt::Result {
if url.password().is_none() && url.username() == "" {
return write!(f, "{url}");
}
write!(f, "{}://", url.scheme())?;
if url.username() != "" && url.password().is_some() {
write!(f, "{}", url.username())?;
write!(f, ":****@")?;
} else if url.username() != "" {
write!(f, "****@")?;
} else if url.password().is_some() {
write!(f, ":****@")?;
}
write!(f, "{}", url.host_str().unwrap_or(""))?;
if let Some(port) = url.port() {
write!(f, ":{port}")?;
}
write!(f, "{}", url.path())?;
if let Some(query) = url.query() {
write!(f, "?{query}")?;
}
if let Some(fragment) = url.fragment() {
write!(f, "#{fragment}")?;
}
Ok(())
}
/// A wrapper around a [`url::Url`] ref that safely handles credentials for
/// logging purposes.
///
/// Uses the same underlying [`Display`] implementation as [`DisplaySafeUrl`].
///
/// # Examples
///
/// ```
/// use uv_redacted::DisplaySafeUrl;
/// use std::str::FromStr;
///
/// // Create from a `url::Url` ref
/// let url = Url::parse("https://user:password@example.com").unwrap();
/// let log_safe_url = DisplaySafeUrlRef::from(&url);
///
/// // Display will mask secrets
/// assert_eq!(url.to_string(), "https://user:****@example.com/");
///
/// // Since `DisplaySafeUrlRef` provides full access to the underlying `Url` through a
/// // `Deref` implementation, you can still access the username and password
/// assert_eq!(url.username(), "user");
/// assert_eq!(url.password(), Some("password"));
pub struct DisplaySafeUrlRef<'a>(&'a Url);
impl<'a> Deref for DisplaySafeUrlRef<'a> {
type Target = Url;
fn deref(&self) -> &'a Self::Target {
self.0
}
}
impl std::fmt::Display for DisplaySafeUrlRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt_with_obfuscated_credentials(self.0, f)
}
}
impl std::fmt::Debug for DisplaySafeUrlRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self}")
}
}
impl<'a> From<&'a Url> for DisplaySafeUrlRef<'a> {
fn from(url: &'a Url) -> Self {
DisplaySafeUrlRef(url)
}
}
impl<'a> From<&'a DisplaySafeUrl> for DisplaySafeUrlRef<'a> {
fn from(url: &'a DisplaySafeUrl) -> Self {
DisplaySafeUrlRef(url)
}
}
impl<'a> From<DisplaySafeUrlRef<'a>> for DisplaySafeUrl {
fn from(url: DisplaySafeUrlRef<'a>) -> Self {
DisplaySafeUrl(url.0.clone())
}
} }
#[cfg(test)] #[cfg(test)]
@ -24,49 +244,103 @@ mod tests {
#[test] #[test]
fn from_url_no_credentials() { fn from_url_no_credentials() {
let url = Url::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap(); let url_str = "https://pypi-proxy.fly.dev/basic-auth/simple";
let redacted = redacted_url(&url); let url = Url::parse(url_str).unwrap();
assert_eq!(redacted.username(), ""); let log_safe_url = DisplaySafeUrl::from(url);
assert!(redacted.password().is_none()); assert_eq!(log_safe_url.username(), "");
assert_eq!( assert!(log_safe_url.password().is_none());
format!("{redacted}"), assert_eq!(format!("{log_safe_url}"), url_str);
"https://pypi-proxy.fly.dev/basic-auth/simple"
);
} }
#[test] #[test]
fn from_url_username_and_password() { fn from_url_username_and_password() {
let url = Url::parse("https://user:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let redacted = redacted_url(&url); let url = Url::parse(url_str).unwrap();
assert_eq!(redacted.username(), ""); let log_safe_url = DisplaySafeUrl::from(url);
assert!(redacted.password().is_none()); assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!( assert_eq!(
format!("{redacted}"), format!("{log_safe_url}"),
"https://pypi-proxy.fly.dev/basic-auth/simple" "https://user:****@pypi-proxy.fly.dev/basic-auth/simple"
); );
} }
#[test] #[test]
fn from_url_just_password() { fn from_url_just_password() {
let url = Url::parse("https://:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); let url_str = "https://:pass@pypi-proxy.fly.dev/basic-auth/simple";
let redacted = redacted_url(&url); let url = Url::parse(url_str).unwrap();
assert_eq!(redacted.username(), ""); let log_safe_url = DisplaySafeUrl::from(url);
assert!(redacted.password().is_none()); assert_eq!(log_safe_url.username(), "");
assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!( assert_eq!(
format!("{redacted}"), format!("{log_safe_url}"),
"https://pypi-proxy.fly.dev/basic-auth/simple" "https://:****@pypi-proxy.fly.dev/basic-auth/simple"
); );
} }
#[test] #[test]
fn from_url_just_username() { fn from_url_just_username() {
let url = Url::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); let url_str = "https://user@pypi-proxy.fly.dev/basic-auth/simple";
let redacted = redacted_url(&url); let url = Url::parse(url_str).unwrap();
assert_eq!(redacted.username(), ""); let log_safe_url = DisplaySafeUrl::from(url);
assert!(redacted.password().is_none()); assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_none());
assert_eq!( assert_eq!(
format!("{redacted}"), format!("{log_safe_url}"),
"https://****@pypi-proxy.fly.dev/basic-auth/simple"
);
}
#[test]
fn parse_url_string() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let log_safe_url = DisplaySafeUrl::parse(url_str).unwrap();
assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!(
format!("{log_safe_url}"),
"https://user:****@pypi-proxy.fly.dev/basic-auth/simple"
);
}
#[test]
fn remove_credentials() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let mut log_safe_url = DisplaySafeUrl::parse(url_str).unwrap();
log_safe_url.remove_credentials();
assert_eq!(log_safe_url.username(), "");
assert!(log_safe_url.password().is_none());
assert_eq!(
format!("{log_safe_url}"),
"https://pypi-proxy.fly.dev/basic-auth/simple" "https://pypi-proxy.fly.dev/basic-auth/simple"
); );
} }
#[test]
fn to_string_with_credentials() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let log_safe_url = DisplaySafeUrl::parse(url_str).unwrap();
assert_eq!(&log_safe_url.to_string_with_credentials(), url_str);
}
#[test]
fn url_join() {
let url_str = "https://token@example.com/abc/";
let log_safe_url = DisplaySafeUrl::parse(url_str).unwrap();
let foo_url = log_safe_url.join("foo").unwrap();
assert_eq!(format!("{foo_url}"), "https://****@example.com/abc/foo");
}
#[test]
fn log_safe_url_ref() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrlRef::from(&url);
assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!(
format!("{log_safe_url}"),
"https://user:****@pypi-proxy.fly.dev/basic-auth/simple"
);
}
} }

View file

@ -23,6 +23,7 @@ uv-fs = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }

View file

@ -54,6 +54,8 @@ use uv_distribution_types::{
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_pep508::{Pep508Error, RequirementOrigin, VerbatimUrl, expand_env_vars}; use uv_pep508::{Pep508Error, RequirementOrigin, VerbatimUrl, expand_env_vars};
use uv_pypi_types::VerbatimParsedUrl; use uv_pypi_types::VerbatimParsedUrl;
#[cfg(feature = "http")]
use uv_redacted::DisplaySafeUrl;
use crate::requirement::EditableError; use crate::requirement::EditableError;
pub use crate::requirement::RequirementsTxtRequirement; pub use crate::requirement::RequirementsTxtRequirement;
@ -949,11 +951,11 @@ async fn read_url_to_string(
url: path.as_ref().to_owned(), url: path.as_ref().to_owned(),
})?; })?;
let url = Url::from_str(path_utf8) let url = DisplaySafeUrl::from_str(path_utf8)
.map_err(|err| RequirementsTxtParserError::InvalidUrl(path_utf8.to_string(), err))?; .map_err(|err| RequirementsTxtParserError::InvalidUrl(path_utf8.to_string(), err))?;
let response = client let response = client
.for_host(&url) .for_host(&url)
.get(url.clone()) .get(Url::from(url.clone()))
.send() .send()
.await .await
.map_err(|err| RequirementsTxtParserError::from_reqwest_middleware(url.clone(), err))?; .map_err(|err| RequirementsTxtParserError::from_reqwest_middleware(url.clone(), err))?;
@ -1047,7 +1049,7 @@ pub enum RequirementsTxtParserError {
url: PathBuf, url: PathBuf,
}, },
#[cfg(feature = "http")] #[cfg(feature = "http")]
Reqwest(Url, reqwest_middleware::Error), Reqwest(DisplaySafeUrl, reqwest_middleware::Error),
#[cfg(feature = "http")] #[cfg(feature = "http")]
InvalidUrl(String, url::ParseError), InvalidUrl(String, url::ParseError),
} }
@ -1301,11 +1303,11 @@ impl From<io::Error> for RequirementsTxtParserError {
#[cfg(feature = "http")] #[cfg(feature = "http")]
impl RequirementsTxtParserError { impl RequirementsTxtParserError {
fn from_reqwest(url: Url, err: reqwest::Error) -> Self { fn from_reqwest(url: DisplaySafeUrl, err: reqwest::Error) -> Self {
Self::Reqwest(url, reqwest_middleware::Error::Reqwest(err)) Self::Reqwest(url, reqwest_middleware::Error::Reqwest(err))
} }
fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { fn from_reqwest_middleware(url: DisplaySafeUrl, err: reqwest_middleware::Error) -> Self {
Self::Reqwest(url, err) Self::Reqwest(url, err)
} }
} }
@ -2039,7 +2041,7 @@ mod test {
insta::with_settings!({ insta::with_settings!({
filters => path_filters(&path_filter(temp_dir.path())), filters => path_filters(&path_filter(temp_dir.path())),
}, { }, {
insta::assert_debug_snapshot!(requirements, @r###" insta::assert_debug_snapshot!(requirements, @r#"
RequirementsTxt { RequirementsTxt {
requirements: [], requirements: [],
constraints: [], constraints: [],
@ -2050,34 +2052,14 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///foo/bar,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar",
query: None,
fragment: None,
},
install_path: "/foo/bar", install_path: "/foo/bar",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///foo/bar,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/foo/bar",
query: None,
fragment: None,
},
given: Some( given: Some(
"/foo/bar", "/foo/bar",
), ),
@ -2102,7 +2084,7 @@ mod test {
no_binary: None, no_binary: None,
only_binary: None, only_binary: None,
} }
"###); "#);
}); });
Ok(()) Ok(())
@ -2187,7 +2169,7 @@ mod test {
insta::with_settings!({ insta::with_settings!({
filters => path_filters(&path_filter(temp_dir.path())), filters => path_filters(&path_filter(temp_dir.path())),
}, { }, {
insta::assert_debug_snapshot!(requirements, @r###" insta::assert_debug_snapshot!(requirements, @r#"
RequirementsTxt { RequirementsTxt {
requirements: [ requirements: [
RequirementEntry { RequirementEntry {
@ -2333,21 +2315,7 @@ mod test {
editables: [], editables: [],
index_url: Some( index_url: Some(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://test.pypi.org/simple/,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"test.pypi.org",
),
),
port: None,
path: "/simple/",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://test.pypi.org/simple/", "https://test.pypi.org/simple/",
), ),
@ -2359,7 +2327,7 @@ mod test {
no_binary: All, no_binary: All,
only_binary: None, only_binary: None,
} }
"###); "#);
}); });
Ok(()) Ok(())
@ -2402,7 +2370,7 @@ mod test {
insta::with_settings!({ insta::with_settings!({
filters => path_filters(&path_filter(temp_dir.path())), filters => path_filters(&path_filter(temp_dir.path())),
}, { }, {
insta::assert_debug_snapshot!(requirements, @r###" insta::assert_debug_snapshot!(requirements, @r#"
RequirementsTxt { RequirementsTxt {
requirements: [ requirements: [
RequirementEntry { RequirementEntry {
@ -2411,33 +2379,13 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Path( parsed_url: Path(
ParsedPathUrl { ParsedPathUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.3.0-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.3.0-py3-none-any.whl",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.3.0-py3-none-any.whl", install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.3.0-py3-none-any.whl",
ext: Wheel, ext: Wheel,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.3.0-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.3.0-py3-none-any.whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"importlib_metadata-8.3.0-py3-none-any.whl", "importlib_metadata-8.3.0-py3-none-any.whl",
), ),
@ -2460,33 +2408,13 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Path( parsed_url: Path(
ParsedPathUrl { ParsedPathUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl", install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl",
ext: Wheel, ext: Wheel,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"importlib_metadata-8.2.0-py3-none-any.whl", "importlib_metadata-8.2.0-py3-none-any.whl",
), ),
@ -2509,33 +2437,13 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Path( parsed_url: Path(
ParsedPathUrl { ParsedPathUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl", install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl",
ext: Wheel, ext: Wheel,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0-py3-none-any.whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"importlib_metadata-8.2.0-py3-none-any.whl", "importlib_metadata-8.2.0-py3-none-any.whl",
), ),
@ -2562,33 +2470,13 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Path( parsed_url: Path(
ParsedPathUrl { ParsedPathUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl", install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
ext: Wheel, ext: Wheel,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"importlib_metadata-8.2.0+local-py3-none-any.whl", "importlib_metadata-8.2.0+local-py3-none-any.whl",
), ),
@ -2611,33 +2499,13 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Path( parsed_url: Path(
ParsedPathUrl { ParsedPathUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl", install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
ext: Wheel, ext: Wheel,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"importlib_metadata-8.2.0+local-py3-none-any.whl", "importlib_metadata-8.2.0+local-py3-none-any.whl",
), ),
@ -2660,33 +2528,13 @@ mod test {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Path( parsed_url: Path(
ParsedPathUrl { ParsedPathUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl", install_path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
ext: Wheel, ext: Wheel,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/importlib_metadata-8.2.0+local-py3-none-any.whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"importlib_metadata-8.2.0+local-py3-none-any.whl", "importlib_metadata-8.2.0+local-py3-none-any.whl",
), ),
@ -2717,7 +2565,7 @@ mod test {
no_binary: None, no_binary: None,
only_binary: None, only_binary: None,
} }
"###); "#);
}); });
Ok(()) Ok(())

View file

@ -39,21 +39,7 @@ RequirementsTxt {
parsed_url: Git( parsed_url: Git(
ParsedGitUrl { ParsedGitUrl {
url: GitUrl { url: GitUrl {
repository: Url { repository: https://github.com/pandas-dev/pandas.git,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"github.com",
),
),
port: None,
path: "/pandas-dev/pandas.git",
query: None,
fragment: None,
},
reference: DefaultBranch, reference: DefaultBranch,
precise: None, precise: None,
}, },
@ -61,21 +47,7 @@ RequirementsTxt {
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: git+https://github.com/pandas-dev/pandas.git,
scheme: "git+https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"github.com",
),
),
port: None,
path: "/pandas-dev/pandas.git",
query: None,
fragment: None,
},
given: Some( given: Some(
"git+https://github.com/pandas-dev/pandas.git", "git+https://github.com/pandas-dev/pandas.git",
), ),

View file

@ -10,34 +10,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black_editable", "./scripts/packages/black_editable",
), ),
@ -60,34 +40,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black_editable", "./scripts/packages/black_editable",
), ),
@ -114,34 +74,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/scripts/packages/black_editable",
query: None,
fragment: None,
},
install_path: "/scripts/packages/black_editable", install_path: "/scripts/packages/black_editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/scripts/packages/black_editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"file:///scripts/packages/black_editable", "file:///scripts/packages/black_editable",
), ),
@ -164,34 +104,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black editable", "./scripts/packages/black editable",
), ),
@ -214,34 +134,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black editable", "./scripts/packages/black editable",
), ),
@ -264,34 +164,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black editable", "./scripts/packages/black editable",
), ),

View file

@ -12,34 +12,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -69,34 +49,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -126,34 +86,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -183,34 +123,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -240,34 +160,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -290,34 +190,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable[d,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable[d",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable[d", install_path: "<REQUIREMENTS_DIR>/editable[d",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable[d,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable[d",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable[d", "./editable[d",
), ),
@ -340,34 +220,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -390,34 +250,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file://<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),

View file

@ -39,21 +39,7 @@ RequirementsTxt {
parsed_url: Git( parsed_url: Git(
ParsedGitUrl { ParsedGitUrl {
url: GitUrl { url: GitUrl {
repository: Url { repository: https://github.com/pandas-dev/pandas.git,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"github.com",
),
),
port: None,
path: "/pandas-dev/pandas.git",
query: None,
fragment: None,
},
reference: DefaultBranch, reference: DefaultBranch,
precise: None, precise: None,
}, },
@ -61,21 +47,7 @@ RequirementsTxt {
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: git+https://github.com/pandas-dev/pandas.git,
scheme: "git+https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"github.com",
),
),
port: None,
path: "/pandas-dev/pandas.git",
query: None,
fragment: None,
},
given: Some( given: Some(
"git+https://github.com/pandas-dev/pandas.git", "git+https://github.com/pandas-dev/pandas.git",
), ),

View file

@ -1,5 +1,5 @@
--- ---
source: crates/requirements-txt/src/lib.rs source: crates/uv-requirements-txt/src/lib.rs
expression: actual expression: actual
--- ---
RequirementsTxt { RequirementsTxt {
@ -10,34 +10,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black_editable", "./scripts/packages/black_editable",
), ),
@ -60,34 +40,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black_editable", "./scripts/packages/black_editable",
), ),
@ -114,34 +74,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black_editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black_editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black_editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"file:///scripts/packages/black_editable", "file:///scripts/packages/black_editable",
), ),
@ -164,34 +104,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black editable", "./scripts/packages/black editable",
), ),
@ -214,34 +134,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black editable", "./scripts/packages/black editable",
), ),
@ -264,34 +164,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable", install_path: "<REQUIREMENTS_DIR>/scripts/packages/black editable",
editable: false, editable: false,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/scripts/packages/black%20editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/scripts/packages/black%20editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./scripts/packages/black editable", "./scripts/packages/black editable",
), ),

View file

@ -12,34 +12,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -69,34 +49,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -126,34 +86,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -183,34 +123,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -240,34 +160,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -290,34 +190,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable[d,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable[d",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable[d", install_path: "<REQUIREMENTS_DIR>/editable[d",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable[d,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable[d",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable[d", "./editable[d",
), ),
@ -340,34 +220,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),
@ -390,34 +250,14 @@ RequirementsTxt {
url: VerbatimParsedUrl { url: VerbatimParsedUrl {
parsed_url: Directory( parsed_url: Directory(
ParsedDirectoryUrl { ParsedDirectoryUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
install_path: "<REQUIREMENTS_DIR>/editable", install_path: "<REQUIREMENTS_DIR>/editable",
editable: true, editable: true,
virtual: false, virtual: false,
}, },
), ),
verbatim: VerbatimUrl { verbatim: VerbatimUrl {
url: Url { url: file:///<REQUIREMENTS_DIR>/editable,
scheme: "file",
cannot_be_a_base: false,
username: "",
password: None,
host: None,
port: None,
path: "/<REQUIREMENTS_DIR>/editable",
query: None,
fragment: None,
},
given: Some( given: Some(
"./editable", "./editable",
), ),

View file

@ -28,6 +28,7 @@ uv-git = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-requirements-txt = { workspace = true, features = ["http"] } uv-requirements-txt = { workspace = true, features = ["http"] }
uv-resolver = { workspace = true, features = ["clap"] } uv-resolver = { workspace = true, features = ["clap"] }
uv-types = { workspace = true } uv-types = { workspace = true }

View file

@ -16,6 +16,7 @@ use uv_distribution_types::{
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
use uv_pep508::RequirementOrigin; use uv_pep508::RequirementOrigin;
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_types::{BuildContext, HashStrategy}; use uv_types::{BuildContext, HashStrategy};
@ -180,7 +181,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
return Ok(metadata); return Ok(metadata);
} }
let Ok(url) = Url::from_directory_path(source_tree) else { let Ok(url) = Url::from_directory_path(source_tree).map(DisplaySafeUrl::from) else {
return Err(anyhow::anyhow!("Failed to convert path to URL")); return Err(anyhow::anyhow!("Failed to convert path to URL"));
}; };
let source = SourceUrl::Directory(DirectorySourceUrl { let source = SourceUrl::Directory(DirectorySourceUrl {

View file

@ -33,6 +33,7 @@ uv-pep508 = { workspace = true }
uv-platform-tags = { workspace = true } uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-python = { workspace = true } uv-python = { workspace = true }
uv-redacted = { workspace = true }
uv-requirements-txt = { workspace = true } uv-requirements-txt = { workspace = true }
uv-small-str = { workspace = true } uv-small-str = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }

View file

@ -33,6 +33,7 @@ use uv_pep440::Version;
use uv_pep508::{MarkerEnvironment, MarkerTree, VerbatimUrl}; use uv_pep508::{MarkerEnvironment, MarkerTree, VerbatimUrl};
use uv_platform_tags::{TagCompatibility, TagPriority, Tags}; use uv_platform_tags::{TagCompatibility, TagPriority, Tags};
use uv_pypi_types::{HashDigests, Hashes, ParsedGitUrl, VcsKind}; use uv_pypi_types::{HashDigests, Hashes, ParsedGitUrl, VcsKind};
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString; use uv_small_str::SmallString;
use crate::lock::export::ExportableRequirements; use crate::lock::export::ExportableRequirements;
@ -93,7 +94,7 @@ pub enum PylockTomlErrorKind {
#[error("`packages.vcs` entry for `{0}` must have a `url` or `path`")] #[error("`packages.vcs` entry for `{0}` must have a `url` or `path`")]
VcsMissingPathUrl(PackageName), VcsMissingPathUrl(PackageName),
#[error("URL must end in a valid wheel filename: `{0}`")] #[error("URL must end in a valid wheel filename: `{0}`")]
UrlMissingFilename(Url), UrlMissingFilename(DisplaySafeUrl),
#[error("Path must end in a valid wheel filename: `{0}`")] #[error("Path must end in a valid wheel filename: `{0}`")]
PathMissingFilename(Box<Path>), PathMissingFilename(Box<Path>),
#[error("Failed to convert path to URL")] #[error("Failed to convert path to URL")]
@ -204,7 +205,7 @@ pub struct PylockTomlPackage {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<Version>, pub version: Option<Version>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<Url>, pub index: Option<DisplaySafeUrl>,
#[serde( #[serde(
skip_serializing_if = "uv_pep508::marker::ser::is_empty", skip_serializing_if = "uv_pep508::marker::ser::is_empty",
serialize_with = "uv_pep508::marker::ser::serialize", serialize_with = "uv_pep508::marker::ser::serialize",
@ -247,7 +248,7 @@ struct PylockTomlDirectory {
struct PylockTomlVcs { struct PylockTomlVcs {
r#type: VcsKind, r#type: VcsKind,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
url: Option<Url>, url: Option<DisplaySafeUrl>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
path: Option<PortablePathBuf>, path: Option<PortablePathBuf>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -261,7 +262,7 @@ struct PylockTomlVcs {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
struct PylockTomlArchive { struct PylockTomlArchive {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
url: Option<Url>, url: Option<DisplaySafeUrl>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
path: Option<PortablePathBuf>, path: Option<PortablePathBuf>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -284,7 +285,7 @@ struct PylockTomlSdist {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
name: Option<SmallString>, name: Option<SmallString>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
url: Option<Url>, url: Option<DisplaySafeUrl>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
path: Option<PortablePathBuf>, path: Option<PortablePathBuf>,
#[serde( #[serde(
@ -305,7 +306,7 @@ struct PylockTomlWheel {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
name: Option<WheelFilename>, name: Option<WheelFilename>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
url: Option<Url>, url: Option<DisplaySafeUrl>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
path: Option<PortablePathBuf>, path: Option<PortablePathBuf>,
#[serde( #[serde(
@ -1324,7 +1325,7 @@ impl PylockTomlWheel {
&self, &self,
install_path: &Path, install_path: &Path,
name: &PackageName, name: &PackageName,
index: Option<&Url>, index: Option<&DisplaySafeUrl>,
) -> Result<RegistryBuiltWheel, PylockTomlErrorKind> { ) -> Result<RegistryBuiltWheel, PylockTomlErrorKind> {
let filename = self.filename(name)?.into_owned(); let filename = self.filename(name)?.into_owned();
@ -1332,7 +1333,8 @@ impl PylockTomlWheel {
UrlString::from(url) UrlString::from(url)
} else if let Some(path) = self.path.as_ref() { } else if let Some(path) = self.path.as_ref() {
let path = install_path.join(path); let path = install_path.join(path);
let url = Url::from_file_path(path).map_err(|()| PylockTomlErrorKind::PathToUrl)?; let url = DisplaySafeUrl::from_file_path(path)
.map_err(|()| PylockTomlErrorKind::PathToUrl)?;
UrlString::from(url) UrlString::from(url)
} else { } else {
return Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone())); return Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone()));
@ -1408,8 +1410,10 @@ impl PylockTomlVcs {
let mut url = if let Some(url) = self.url.as_ref() { let mut url = if let Some(url) = self.url.as_ref() {
url.clone() url.clone()
} else if let Some(path) = self.path.as_ref() { } else if let Some(path) = self.path.as_ref() {
Url::from_directory_path(install_path.join(path)) DisplaySafeUrl::from(
.map_err(|()| PylockTomlErrorKind::PathToUrl)? Url::from_directory_path(install_path.join(path))
.map_err(|()| PylockTomlErrorKind::PathToUrl)?,
)
} else { } else {
return Err(PylockTomlErrorKind::VcsMissingPathUrl(name.clone())); return Err(PylockTomlErrorKind::VcsMissingPathUrl(name.clone()));
}; };
@ -1427,7 +1431,7 @@ impl PylockTomlVcs {
}; };
// Reconstruct the PEP 508-compatible URL from the `GitSource`. // Reconstruct the PEP 508-compatible URL from the `GitSource`.
let url = Url::from(ParsedGitUrl { let url = DisplaySafeUrl::from(ParsedGitUrl {
url: git_url.clone(), url: git_url.clone(),
subdirectory: subdirectory.clone(), subdirectory: subdirectory.clone(),
}); });
@ -1469,7 +1473,7 @@ impl PylockTomlSdist {
install_path: &Path, install_path: &Path,
name: &PackageName, name: &PackageName,
version: Option<&Version>, version: Option<&Version>,
index: Option<&Url>, index: Option<&DisplaySafeUrl>,
) -> Result<RegistrySourceDist, PylockTomlErrorKind> { ) -> Result<RegistrySourceDist, PylockTomlErrorKind> {
let filename = self.filename(name)?.into_owned(); let filename = self.filename(name)?.into_owned();
let ext = SourceDistExtension::from_path(filename.as_ref())?; let ext = SourceDistExtension::from_path(filename.as_ref())?;
@ -1485,7 +1489,8 @@ impl PylockTomlSdist {
UrlString::from(url) UrlString::from(url)
} else if let Some(path) = self.path.as_ref() { } else if let Some(path) = self.path.as_ref() {
let path = install_path.join(path); let path = install_path.join(path);
let url = Url::from_file_path(path).map_err(|()| PylockTomlErrorKind::PathToUrl)?; let url = DisplaySafeUrl::from_file_path(path)
.map_err(|()| PylockTomlErrorKind::PathToUrl)?;
UrlString::from(url) UrlString::from(url)
} else { } else {
return Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone())); return Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone()));

View file

@ -13,6 +13,7 @@ use uv_fs::Simplified;
use uv_git_types::GitReference; use uv_git_types::GitReference;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl}; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
use uv_redacted::DisplaySafeUrl;
use crate::lock::export::{ExportableRequirement, ExportableRequirements}; use crate::lock::export::{ExportableRequirement, ExportableRequirements};
use crate::lock::{Package, PackageId, Source}; use crate::lock::{Package, PackageId, Source};
@ -94,7 +95,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
.expect("Internal Git URLs must have supported schemes"); .expect("Internal Git URLs must have supported schemes");
// Reconstruct the PEP 508-compatible URL from the `GitSource`. // Reconstruct the PEP 508-compatible URL from the `GitSource`.
let url = Url::from(ParsedGitUrl { let url = DisplaySafeUrl::from(ParsedGitUrl {
url: git_url.clone(), url: git_url.clone(),
subdirectory: git.subdirectory.clone(), subdirectory: git.subdirectory.clone(),
}); });
@ -102,7 +103,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
write!(f, "{} @ {}", package.id.name, url)?; write!(f, "{} @ {}", package.id.name, url)?;
} }
Source::Direct(url, direct) => { Source::Direct(url, direct) => {
let url = Url::from(ParsedArchiveUrl { let url = DisplaySafeUrl::from(ParsedArchiveUrl {
url: url.to_url().map_err(|_| std::fmt::Error)?, url: url.to_url().map_err(|_| std::fmt::Error)?,
subdirectory: direct.subdirectory.clone(), subdirectory: direct.subdirectory.clone(),
ext: DistExtension::Source(SourceDistExtension::TarGz), ext: DistExtension::Source(SourceDistExtension::TarGz),

View file

@ -30,7 +30,7 @@ use uv_distribution_types::{
Dist, DistributionMetadata, FileLocation, GitSourceDist, IndexLocations, IndexMetadata, Dist, DistributionMetadata, FileLocation, GitSourceDist, IndexLocations, IndexMetadata,
IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel,
RegistrySourceDist, RemoteSource, Requirement, RequirementSource, ResolvedDist, StaticMetadata, RegistrySourceDist, RemoteSource, Requirement, RequirementSource, ResolvedDist, StaticMetadata,
ToUrlError, UrlString, redact_credentials, ToUrlError, UrlString,
}; };
use uv_fs::{PortablePath, PortablePathBuf, relative_to}; use uv_fs::{PortablePath, PortablePathBuf, relative_to};
use uv_git::{RepositoryReference, ResolvedRepositoryReference}; use uv_git::{RepositoryReference, ResolvedRepositoryReference};
@ -45,6 +45,7 @@ use uv_pypi_types::{
ConflictPackage, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes, ParsedArchiveUrl, ConflictPackage, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes, ParsedArchiveUrl,
ParsedGitUrl, ParsedGitUrl,
}; };
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString; use uv_small_str::SmallString;
use uv_types::{BuildContext, HashStrategy}; use uv_types::{BuildContext, HashStrategy};
use uv_workspace::WorkspaceMember; use uv_workspace::WorkspaceMember;
@ -1404,7 +1405,7 @@ impl Lock {
.into_iter() .into_iter()
.filter_map(|index| match index.url() { .filter_map(|index| match index.url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => { IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
Some(UrlString::from(index.url().redacted().as_ref())) Some(UrlString::from(index.url().without_credentials().as_ref()))
} }
IndexUrl::Path(_) => None, IndexUrl::Path(_) => None,
}) })
@ -2238,7 +2239,7 @@ impl Package {
Source::Direct(url, direct) => { Source::Direct(url, direct) => {
let filename: WheelFilename = let filename: WheelFilename =
self.wheels[best_wheel_index].filename.clone(); self.wheels[best_wheel_index].filename.clone();
let url = Url::from(ParsedArchiveUrl { let url = DisplaySafeUrl::from(ParsedArchiveUrl {
url: url.to_url().map_err(LockErrorKind::InvalidUrl)?, url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
subdirectory: direct.subdirectory.clone(), subdirectory: direct.subdirectory.clone(),
ext: DistExtension::Wheel, ext: DistExtension::Wheel,
@ -2400,7 +2401,7 @@ impl Package {
GitUrl::from_commit(url, GitReference::from(git.kind.clone()), git.precise)?; GitUrl::from_commit(url, GitReference::from(git.kind.clone()), git.precise)?;
// Reconstruct the PEP 508-compatible URL from the `GitSource`. // Reconstruct the PEP 508-compatible URL from the `GitSource`.
let url = Url::from(ParsedGitUrl { let url = DisplaySafeUrl::from(ParsedGitUrl {
url: git_url.clone(), url: git_url.clone(),
subdirectory: git.subdirectory.clone(), subdirectory: git.subdirectory.clone(),
}); });
@ -2419,7 +2420,7 @@ impl Package {
return Ok(None); return Ok(None);
}; };
let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?; let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
let url = Url::from(ParsedArchiveUrl { let url = DisplaySafeUrl::from(ParsedArchiveUrl {
url: location.clone(), url: location.clone(),
subdirectory: direct.subdirectory.clone(), subdirectory: direct.subdirectory.clone(),
ext: DistExtension::Source(ext), ext: DistExtension::Source(ext),
@ -2498,8 +2499,9 @@ impl Package {
name: name.clone(), name: name.clone(),
version: version.clone(), version: version.clone(),
})?; })?;
let file_url = Url::from_file_path(workspace_root.join(path).join(file_path)) let file_url =
.map_err(|()| LockErrorKind::PathToUrl)?; DisplaySafeUrl::from_file_path(workspace_root.join(path).join(file_path))
.map_err(|()| LockErrorKind::PathToUrl)?;
let filename = sdist let filename = sdist
.filename() .filename()
.ok_or_else(|| LockErrorKind::MissingFilename { .ok_or_else(|| LockErrorKind::MissingFilename {
@ -3192,7 +3194,7 @@ impl Source {
match index_url { match index_url {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => { IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
// Remove any sensitive credentials from the index URL. // Remove any sensitive credentials from the index URL.
let redacted = index_url.redacted(); let redacted = index_url.without_credentials();
let source = RegistrySource::Url(UrlString::from(redacted.as_ref())); let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
Ok(Source::Registry(source)) Ok(Source::Registry(source))
} }
@ -3405,7 +3407,7 @@ impl TryFrom<SourceWire> for Source {
match wire { match wire {
Registry { registry } => Ok(Source::Registry(registry.into())), Registry { registry } => Ok(Source::Registry(registry.into())),
Git { git } => { Git { git } => {
let url = Url::parse(&git) let url = DisplaySafeUrl::parse(&git)
.map_err(|err| SourceParseError::InvalidUrl { .map_err(|err| SourceParseError::InvalidUrl {
given: git.to_string(), given: git.to_string(),
err, err,
@ -3913,12 +3915,12 @@ impl From<GitSourceKind> for GitReference {
} }
} }
/// Construct the lockfile-compatible [`URL`] for a [`GitSourceDist`]. /// Construct the lockfile-compatible [`DisplaySafeUrl`] for a [`GitSourceDist`].
fn locked_git_url(git_dist: &GitSourceDist) -> Url { fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
let mut url = git_dist.git.repository().clone(); let mut url = git_dist.git.repository().clone();
// Redact the credentials. // Remove the credentials.
redact_credentials(&mut url); url.remove_credentials();
// Clear out any existing state. // Clear out any existing state.
url.set_fragment(None); url.set_fragment(None);
@ -4183,8 +4185,9 @@ impl Wheel {
.into()); .into());
} }
}; };
let file_url = Url::from_file_path(root.join(index_path).join(file_path)) let file_url =
.map_err(|()| LockErrorKind::PathToUrl)?; DisplaySafeUrl::from_file_path(root.join(index_path).join(file_path))
.map_err(|()| LockErrorKind::PathToUrl)?;
let file = Box::new(uv_distribution_types::File { let file = Box::new(uv_distribution_types::File {
dist_info_metadata: false, dist_info_metadata: false,
filename: SmallString::from(filename.to_string()), filename: SmallString::from(filename.to_string()),
@ -4571,8 +4574,8 @@ fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlEr
} }
} }
/// Convert a [`Url`] into a normalized [`UrlString`] by removing the fragment. /// Convert a [`DisplaySafeUrl`] into a normalized [`UrlString`] by removing the fragment.
fn normalize_url(mut url: Url) -> UrlString { fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
url.set_fragment(None); url.set_fragment(None);
UrlString::from(url) UrlString::from(url)
} }
@ -4606,8 +4609,8 @@ fn normalize_requirement(
let git = { let git = {
let mut repository = git.repository().clone(); let mut repository = git.repository().clone();
// Redact the credentials. // Remove the credentials.
redact_credentials(&mut repository); repository.remove_credentials();
// Remove the fragment and query from the URL; they're already present in the source. // Remove the fragment and query from the URL; they're already present in the source.
repository.set_fragment(None); repository.set_fragment(None);
@ -4617,7 +4620,7 @@ fn normalize_requirement(
}; };
// Reconstruct the PEP 508 URL from the underlying data. // Reconstruct the PEP 508 URL from the underlying data.
let url = Url::from(ParsedGitUrl { let url = DisplaySafeUrl::from(ParsedGitUrl {
url: git.clone(), url: git.clone(),
subdirectory: subdirectory.clone(), subdirectory: subdirectory.clone(),
}); });
@ -4692,7 +4695,7 @@ fn normalize_requirement(
let index = index let index = index
.map(|index| index.url.into_url()) .map(|index| index.url.into_url())
.map(|mut index| { .map(|mut index| {
redact_credentials(&mut index); index.remove_credentials();
index index
}) })
.map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))); .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
@ -4715,14 +4718,14 @@ fn normalize_requirement(
ext, ext,
url: _, url: _,
} => { } => {
// Redact the credentials. // Remove the credentials.
redact_credentials(&mut location); location.remove_credentials();
// Remove the fragment from the URL; it's already present in the source. // Remove the fragment from the URL; it's already present in the source.
location.set_fragment(None); location.set_fragment(None);
// Reconstruct the PEP 508 URL from the underlying data. // Reconstruct the PEP 508 URL from the underlying data.
let url = Url::from(ParsedArchiveUrl { let url = DisplaySafeUrl::from(ParsedArchiveUrl {
url: location.clone(), url: location.clone(),
subdirectory: subdirectory.clone(), subdirectory: subdirectory.clone(),
ext, ext,

View file

@ -1518,7 +1518,7 @@ impl std::fmt::Display for PubGrubHint {
"hint".bold().cyan(), "hint".bold().cyan(),
":".bold(), ":".bold(),
name.cyan(), name.cyan(),
found_index.redacted().cyan(), found_index.without_credentials().cyan(),
PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(), PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(),
next_index.cyan(), next_index.cyan(),
"--index-strategy unsafe-best-match".green(), "--index-strategy unsafe-best-match".green(),
@ -1530,7 +1530,7 @@ impl std::fmt::Display for PubGrubHint {
"{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).", "{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).",
"hint".bold().cyan(), "hint".bold().cyan(),
":".bold(), ":".bold(),
index.redacted().cyan(), index.without_credentials().cyan(),
"401 Unauthorized".red(), "401 Unauthorized".red(),
) )
} }
@ -1540,7 +1540,7 @@ impl std::fmt::Display for PubGrubHint {
"{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).", "{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).",
"hint".bold().cyan(), "hint".bold().cyan(),
":".bold(), ":".bold(),
index.redacted().cyan(), index.without_credentials().cyan(),
"403 Forbidden".red(), "403 Forbidden".red(),
) )
} }

View file

@ -1,8 +1,7 @@
use url::Url;
use uv_git::GitResolver; use uv_git::GitResolver;
use uv_pep508::VerbatimUrl; use uv_pep508::VerbatimUrl;
use uv_pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; use uv_pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl};
use uv_redacted::DisplaySafeUrl;
/// Map a URL to a precise URL, if possible. /// Map a URL to a precise URL, if possible.
pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> VerbatimParsedUrl { pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> VerbatimParsedUrl {
@ -26,7 +25,7 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba
url: new_git_url, url: new_git_url,
subdirectory: subdirectory.clone(), subdirectory: subdirectory.clone(),
}; };
let new_url = Url::from(new_parsed_url.clone()); let new_url = DisplaySafeUrl::from(new_parsed_url.clone());
let new_verbatim_url = apply_redirect(&url.verbatim, new_url); let new_verbatim_url = apply_redirect(&url.verbatim, new_url);
VerbatimParsedUrl { VerbatimParsedUrl {
parsed_url: ParsedUrl::Git(new_parsed_url), parsed_url: ParsedUrl::Git(new_parsed_url),
@ -36,7 +35,7 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba
/// Given a [`VerbatimUrl`] and a redirect, apply the redirect to the URL while preserving as much /// Given a [`VerbatimUrl`] and a redirect, apply the redirect to the URL while preserving as much
/// of the verbatim representation as possible. /// of the verbatim representation as possible.
fn apply_redirect(url: &VerbatimUrl, redirect: Url) -> VerbatimUrl { fn apply_redirect(url: &VerbatimUrl, redirect: DisplaySafeUrl) -> VerbatimUrl {
let redirect = VerbatimUrl::from_url(redirect); let redirect = VerbatimUrl::from_url(redirect);
// The redirect should be the "same" URL, but with a specific commit hash added after the `@`. // The redirect should be the "same" URL, but with a specific commit hash added after the `@`.
@ -85,9 +84,8 @@ fn apply_redirect(url: &VerbatimUrl, redirect: Url) -> VerbatimUrl {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use url::Url;
use uv_pep508::VerbatimUrl; use uv_pep508::VerbatimUrl;
use uv_redacted::DisplaySafeUrl;
use crate::redirect::apply_redirect; use crate::redirect::apply_redirect;
@ -97,8 +95,9 @@ mod tests {
// to the given representation. // to the given representation.
let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git")? let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git")?
.with_given("git+https://github.com/flask.git"); .with_given("git+https://github.com/flask.git");
let redirect = let redirect = DisplaySafeUrl::parse(
Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe",
)?;
let expected = VerbatimUrl::parse_url( let expected = VerbatimUrl::parse_url(
"https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe",
@ -111,8 +110,9 @@ mod tests {
// representation. // representation.
let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")?
.with_given("git+https://${DOMAIN}.com/flask.git@main"); .with_given("git+https://${DOMAIN}.com/flask.git@main");
let redirect = let redirect = DisplaySafeUrl::parse(
Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe",
)?;
let expected = VerbatimUrl::parse_url( let expected = VerbatimUrl::parse_url(
"https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe",
@ -123,8 +123,9 @@ mod tests {
// If there's a conflict after the `@`, discard the original representation. // If there's a conflict after the `@`, discard the original representation.
let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")?
.with_given("git+https://github.com/flask.git@${TAG}"); .with_given("git+https://github.com/flask.git@${TAG}");
let redirect = let redirect = DisplaySafeUrl::parse(
Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe",
)?;
let expected = VerbatimUrl::parse_url( let expected = VerbatimUrl::parse_url(
"https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe",
@ -134,7 +135,7 @@ mod tests {
// We should preserve subdirectory fragments. // We should preserve subdirectory fragments.
let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git#subdirectory=src")? let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git#subdirectory=src")?
.with_given("git+https://github.com/flask.git#subdirectory=src"); .with_given("git+https://github.com/flask.git#subdirectory=src");
let redirect = Url::parse( let redirect = DisplaySafeUrl::parse(
"https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src", "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src",
)?; )?;

View file

@ -290,7 +290,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
// `# from https://pypi.org/simple`). // `# from https://pypi.org/simple`).
if self.include_index_annotation { if self.include_index_annotation {
if let Some(index) = node.dist.index() { if let Some(index) = node.dist.index() {
let url = index.redacted(); let url = index.without_credentials();
writeln!(f, "{}", format!(" # from {url}").green())?; writeln!(f, "{}", format!(" # from {url}").green())?;
} }
} }

View file

@ -1,9 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use url::Url;
use uv_distribution_types::{BuildableSource, VersionOrUrlRef}; use uv_distribution_types::{BuildableSource, VersionOrUrlRef};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_redacted::DisplaySafeUrl;
pub type BuildId = usize; pub type BuildId = usize;
@ -31,10 +30,10 @@ pub trait Reporter: Send + Sync {
fn on_download_complete(&self, name: &PackageName, id: usize); fn on_download_complete(&self, name: &PackageName, id: usize);
/// Callback to invoke when a repository checkout begins. /// Callback to invoke when a repository checkout begins.
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
/// Callback to invoke when a repository checkout completes. /// Callback to invoke when a repository checkout completes.
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize); fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize);
} }
impl dyn Reporter { impl dyn Reporter {
@ -62,11 +61,11 @@ impl uv_distribution::Reporter for Facade {
self.reporter.on_build_complete(source, id); self.reporter.on_build_complete(source, id);
} }
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
self.reporter.on_checkout_start(url, rev) self.reporter.on_checkout_start(url, rev)
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) {
self.reporter.on_checkout_complete(url, rev, id); self.reporter.on_checkout_complete(url, rev, id);
} }

View file

@ -1,10 +1,10 @@
use std::str::FromStr; use std::str::FromStr;
use pubgrub::Ranges; use pubgrub::Ranges;
use url::Url;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_redacted::DisplaySafeUrl;
use uv_torch::TorchBackend; use uv_torch::TorchBackend;
use crate::pubgrub::{PubGrubDependency, PubGrubPackage, PubGrubPackageInner}; use crate::pubgrub::{PubGrubDependency, PubGrubPackage, PubGrubPackageInner};
@ -21,7 +21,7 @@ impl SystemDependency {
/// Extract a [`SystemDependency`] from an index URL. /// Extract a [`SystemDependency`] from an index URL.
/// ///
/// For example, given `https://download.pytorch.org/whl/cu124`, returns CUDA 12.4. /// For example, given `https://download.pytorch.org/whl/cu124`, returns CUDA 12.4.
pub(super) fn from_index(index: &Url) -> Option<Self> { pub(super) fn from_index(index: &DisplaySafeUrl) -> Option<Self> {
let backend = TorchBackend::from_index(index)?; let backend = TorchBackend::from_index(index)?;
let cuda_version = backend.cuda_version()?; let cuda_version = backend.cuda_version()?;
Some(Self { Some(Self {
@ -51,22 +51,21 @@ impl From<SystemDependency> for PubGrubDependency {
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;
use url::Url;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_redacted::DisplaySafeUrl;
use crate::resolver::system::SystemDependency; use crate::resolver::system::SystemDependency;
#[test] #[test]
fn pypi() { fn pypi() {
let url = Url::parse("https://pypi.org/simple").unwrap(); let url = DisplaySafeUrl::parse("https://pypi.org/simple").unwrap();
assert_eq!(SystemDependency::from_index(&url), None); assert_eq!(SystemDependency::from_index(&url), None);
} }
#[test] #[test]
fn pytorch_cuda_12_4() { fn pytorch_cuda_12_4() {
let url = Url::parse("https://download.pytorch.org/whl/cu124").unwrap(); let url = DisplaySafeUrl::parse("https://download.pytorch.org/whl/cu124").unwrap();
assert_eq!( assert_eq!(
SystemDependency::from_index(&url), SystemDependency::from_index(&url),
Some(SystemDependency { Some(SystemDependency {
@ -78,7 +77,7 @@ mod tests {
#[test] #[test]
fn pytorch_cpu() { fn pytorch_cpu() {
let url = Url::parse("https://download.pytorch.org/whl/cpu").unwrap(); let url = DisplaySafeUrl::parse("https://download.pytorch.org/whl/cpu").unwrap();
assert_eq!(SystemDependency::from_index(&url), None); assert_eq!(SystemDependency::from_index(&url), None);
} }
} }

View file

@ -14,6 +14,7 @@ workspace = true
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-settings = { workspace = true } uv-settings = { workspace = true }
uv-workspace = { workspace = true } uv-workspace = { workspace = true }

View file

@ -12,6 +12,7 @@ use url::Url;
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_pypi_types::VerbatimParsedUrl; use uv_pypi_types::VerbatimParsedUrl;
use uv_redacted::DisplaySafeUrl;
use uv_settings::{GlobalOptions, ResolverInstallerOptions}; use uv_settings::{GlobalOptions, ResolverInstallerOptions};
use uv_workspace::pyproject::Sources; use uv_workspace::pyproject::Sources;
@ -25,7 +26,7 @@ pub enum Pep723Item {
/// A PEP 723 script provided via `stdin`. /// A PEP 723 script provided via `stdin`.
Stdin(Pep723Metadata), Stdin(Pep723Metadata),
/// A PEP 723 script provided via a remote URL. /// A PEP 723 script provided via a remote URL.
Remote(Pep723Metadata, Url), Remote(Pep723Metadata, DisplaySafeUrl),
} }
impl Pep723Item { impl Pep723Item {

View file

@ -28,6 +28,7 @@ uv-options-metadata = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-python = { workspace = true, features = ["schemars", "clap"] } uv-python = { workspace = true, features = ["schemars", "clap"] }
uv-redacted = { workspace = true }
uv-resolver = { workspace = true, features = ["schemars", "clap"] } uv-resolver = { workspace = true, features = ["schemars", "clap"] }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-torch = { workspace = true, features = ["schemars", "clap"] } uv-torch = { workspace = true, features = ["schemars", "clap"] }

View file

@ -11,6 +11,7 @@ use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipInd
use uv_install_wheel::LinkMode; use uv_install_wheel::LinkMode;
use uv_pypi_types::{SchemaConflicts, SupportedEnvironments}; use uv_pypi_types::{SchemaConflicts, SupportedEnvironments};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
use uv_torch::TorchMode; use uv_torch::TorchMode;
@ -82,6 +83,7 @@ impl_combine_or!(IndexStrategy);
impl_combine_or!(IndexUrl); impl_combine_or!(IndexUrl);
impl_combine_or!(KeyringProviderType); impl_combine_or!(KeyringProviderType);
impl_combine_or!(LinkMode); impl_combine_or!(LinkMode);
impl_combine_or!(DisplaySafeUrl);
impl_combine_or!(NonZeroUsize); impl_combine_or!(NonZeroUsize);
impl_combine_or!(PathBuf); impl_combine_or!(PathBuf);
impl_combine_or!(PipExtraIndex); impl_combine_or!(PipExtraIndex);

View file

@ -1,7 +1,6 @@
use std::{fmt::Debug, num::NonZeroUsize, path::Path, path::PathBuf}; use std::{fmt::Debug, num::NonZeroUsize, path::Path, path::PathBuf};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use uv_cache_info::CacheKey; use uv_cache_info::CacheKey;
use uv_configuration::{ use uv_configuration::{
@ -17,6 +16,7 @@ use uv_normalize::{ExtraName, PackageName, PipGroupName};
use uv_pep508::Requirement; use uv_pep508::Requirement;
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_torch::TorchMode; use uv_torch::TorchMode;
@ -1837,7 +1837,7 @@ pub struct OptionsWire {
// #[serde(flatten)] // #[serde(flatten)]
// publish: PublishOptions // publish: PublishOptions
publish_url: Option<Url>, publish_url: Option<DisplaySafeUrl>,
trusted_publishing: Option<TrustedPublishing>, trusted_publishing: Option<TrustedPublishing>,
check_url: Option<IndexUrl>, check_url: Option<IndexUrl>,
@ -2019,7 +2019,7 @@ pub struct PublishOptions {
publish-url = "https://test.pypi.org/legacy/" publish-url = "https://test.pypi.org/legacy/"
"# "#
)] )]
pub publish_url: Option<Url>, pub publish_url: Option<DisplaySafeUrl>,
/// Configure trusted publishing via GitHub Actions. /// Configure trusted publishing via GitHub Actions.
/// ///

View file

@ -27,12 +27,12 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-python = { workspace = true } uv-python = { workspace = true }
uv-redacted = { workspace = true }
uv-workspace = { workspace = true } uv-workspace = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
url = { workspace = true }
[features] [features]
default = [] default = []

View file

@ -2,7 +2,6 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use url::Url;
use uv_configuration::HashCheckingMode; use uv_configuration::HashCheckingMode;
use uv_distribution_types::{ use uv_distribution_types::{
@ -12,6 +11,7 @@ use uv_distribution_types::{
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_pypi_types::{HashDigest, HashDigests, HashError, ResolverMarkerEnvironment}; use uv_pypi_types::{HashDigest, HashDigests, HashError, ResolverMarkerEnvironment};
use uv_redacted::DisplaySafeUrl;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub enum HashStrategy { pub enum HashStrategy {
@ -76,7 +76,7 @@ impl HashStrategy {
} }
/// Return the [`HashPolicy`] for the given direct URL package. /// Return the [`HashPolicy`] for the given direct URL package.
pub fn get_url(&self, url: &Url) -> HashPolicy { pub fn get_url(&self, url: &DisplaySafeUrl) -> HashPolicy {
match self { match self {
Self::None => HashPolicy::None, Self::None => HashPolicy::None,
Self::Generate(mode) => HashPolicy::Generate(*mode), Self::Generate(mode) => HashPolicy::Generate(*mode),
@ -109,7 +109,7 @@ impl HashStrategy {
} }
/// Returns `true` if the given direct URL package is allowed. /// Returns `true` if the given direct URL package is allowed.
pub fn allows_url(&self, url: &Url) -> bool { pub fn allows_url(&self, url: &DisplaySafeUrl) -> bool {
match self { match self {
Self::None => true, Self::None => true,
Self::Generate(_) => true, Self::Generate(_) => true,

View file

@ -27,6 +27,7 @@ uv-options-metadata = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
@ -42,7 +43,6 @@ tokio = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
toml_edit = { workspace = true } toml_edit = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
url = { workspace = true }
[dev-dependencies] [dev-dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
@ -52,7 +52,7 @@ regex = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
[features] [features]
schemars = ["dep:schemars", "uv-pypi-types/schemars"] schemars = ["dep:schemars", "uv-pypi-types/schemars", "uv-redacted/schemars"]
[package.metadata.cargo-shear] [package.metadata.cargo-shear]
ignored = ["uv-options-metadata"] ignored = ["uv-options-metadata"]

View file

@ -17,7 +17,6 @@ use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashSet}; use rustc_hash::{FxBuildHasher, FxHashSet};
use serde::{Deserialize, Deserializer, Serialize, de::IntoDeserializer, de::SeqAccess}; use serde::{Deserialize, Deserializer, Serialize, de::IntoDeserializer, de::SeqAccess};
use thiserror::Error; use thiserror::Error;
use url::Url;
use uv_build_backend::BuildBackendSettings; use uv_build_backend::BuildBackendSettings;
use uv_distribution_types::{Index, IndexName, RequirementSource}; use uv_distribution_types::{Index, IndexName, RequirementSource};
use uv_fs::{PortablePathBuf, relative_to}; use uv_fs::{PortablePathBuf, relative_to};
@ -29,6 +28,7 @@ use uv_pep508::MarkerTree;
use uv_pypi_types::{ use uv_pypi_types::{
Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl, Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
}; };
use uv_redacted::DisplaySafeUrl;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum PyprojectTomlError { pub enum PyprojectTomlError {
@ -891,7 +891,7 @@ pub enum Source {
/// ``` /// ```
Git { Git {
/// The repository URL (without the `git+` prefix). /// The repository URL (without the `git+` prefix).
git: Url, git: DisplaySafeUrl,
/// The path to the directory with the `pyproject.toml`, if it's not in the archive root. /// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
subdirectory: Option<PortablePathBuf>, subdirectory: Option<PortablePathBuf>,
// Only one of the three may be used; we'll validate this later and emit a custom error. // Only one of the three may be used; we'll validate this later and emit a custom error.
@ -915,7 +915,7 @@ pub enum Source {
/// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" } /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
/// ``` /// ```
Url { Url {
url: Url, url: DisplaySafeUrl,
/// For source distributions, the path to the directory with the `pyproject.toml`, if it's /// For source distributions, the path to the directory with the `pyproject.toml`, if it's
/// not in the archive root. /// not in the archive root.
subdirectory: Option<PortablePathBuf>, subdirectory: Option<PortablePathBuf>,
@ -989,12 +989,12 @@ impl<'de> Deserialize<'de> for Source {
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct CatchAll { struct CatchAll {
git: Option<Url>, git: Option<DisplaySafeUrl>,
subdirectory: Option<PortablePathBuf>, subdirectory: Option<PortablePathBuf>,
rev: Option<String>, rev: Option<String>,
tag: Option<String>, tag: Option<String>,
branch: Option<String>, branch: Option<String>,
url: Option<Url>, url: Option<DisplaySafeUrl>,
path: Option<PortablePathBuf>, path: Option<PortablePathBuf>,
editable: Option<bool>, editable: Option<bool>,
package: Option<bool>, package: Option<bool>,
@ -1083,7 +1083,7 @@ impl<'de> Deserialize<'de> for Source {
// If the user prefixed the URL with `git+`, strip it. // If the user prefixed the URL with `git+`, strip it.
let git = if let Some(git) = git.as_str().strip_prefix("git+") { let git = if let Some(git) = git.as_str().strip_prefix("git+") {
Url::parse(git).map_err(serde::de::Error::custom)? DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
} else { } else {
git git
}; };

View file

@ -7,7 +7,6 @@ use thiserror::Error;
use toml_edit::{ use toml_edit::{
Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value, Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value,
}; };
use url::Url;
use uv_cache_key::CanonicalUrl; use uv_cache_key::CanonicalUrl;
use uv_distribution_types::Index; use uv_distribution_types::Index;
@ -15,6 +14,7 @@ use uv_fs::PortablePath;
use uv_normalize::GroupName; use uv_normalize::GroupName;
use uv_pep440::{Version, VersionParseError, VersionSpecifier, VersionSpecifiers}; use uv_pep440::{Version, VersionParseError, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl}; use uv_pep508::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl};
use uv_redacted::DisplaySafeUrl;
use crate::pyproject::{DependencyType, Source}; use crate::pyproject::{DependencyType, Source};
@ -171,6 +171,7 @@ impl PyProjectTomlMut {
&mut self, &mut self,
req: &Requirement, req: &Requirement,
source: Option<&Source>, source: Option<&Source>,
raw: bool,
) -> Result<ArrayEdit, Error> { ) -> Result<ArrayEdit, Error> {
// Get or create `project.dependencies`. // Get or create `project.dependencies`.
let dependencies = self let dependencies = self
@ -180,7 +181,7 @@ impl PyProjectTomlMut {
.as_array_mut() .as_array_mut()
.ok_or(Error::MalformedDependencies)?; .ok_or(Error::MalformedDependencies)?;
let edit = add_dependency(req, dependencies, source.is_some())?; let edit = add_dependency(req, dependencies, source.is_some(), raw)?;
if let Some(source) = source { if let Some(source) = source {
self.add_source(&req.name, source)?; self.add_source(&req.name, source)?;
@ -196,6 +197,7 @@ impl PyProjectTomlMut {
&mut self, &mut self,
req: &Requirement, req: &Requirement,
source: Option<&Source>, source: Option<&Source>,
raw: bool,
) -> Result<ArrayEdit, Error> { ) -> Result<ArrayEdit, Error> {
// Get or create `tool.uv.dev-dependencies`. // Get or create `tool.uv.dev-dependencies`.
let dev_dependencies = self let dev_dependencies = self
@ -213,7 +215,7 @@ impl PyProjectTomlMut {
.as_array_mut() .as_array_mut()
.ok_or(Error::MalformedDependencies)?; .ok_or(Error::MalformedDependencies)?;
let edit = add_dependency(req, dev_dependencies, source.is_some())?; let edit = add_dependency(req, dev_dependencies, source.is_some(), raw)?;
if let Some(source) = source { if let Some(source) = source {
self.add_source(&req.name, source)?; self.add_source(&req.name, source)?;
@ -267,7 +269,7 @@ impl PyProjectTomlMut {
if table if table
.get("url") .get("url")
.and_then(|item| item.as_str()) .and_then(|item| item.as_str())
.and_then(|url| Url::parse(url).ok()) .and_then(|url| DisplaySafeUrl::parse(url).ok())
.is_some_and(|url| { .is_some_and(|url| {
CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url()) CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url())
}) })
@ -304,10 +306,10 @@ impl PyProjectTomlMut {
if table if table
.get("url") .get("url")
.and_then(|item| item.as_str()) .and_then(|item| item.as_str())
.and_then(|url| Url::parse(url).ok()) .and_then(|url| DisplaySafeUrl::parse(url).ok())
.is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url.url())) .is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url.url()))
{ {
let mut formatted = Formatted::new(index.url.redacted().to_string()); let mut formatted = Formatted::new(index.url.without_credentials().to_string());
if let Some(value) = table.get("url").and_then(Item::as_value) { if let Some(value) = table.get("url").and_then(Item::as_value) {
if let Some(prefix) = value.decor().prefix() { if let Some(prefix) = value.decor().prefix() {
formatted.decor_mut().set_prefix(prefix.clone()); formatted.decor_mut().set_prefix(prefix.clone());
@ -365,7 +367,7 @@ impl PyProjectTomlMut {
if table if table
.get("url") .get("url")
.and_then(|item| item.as_str()) .and_then(|item| item.as_str())
.and_then(|url| Url::parse(url).ok()) .and_then(|url| DisplaySafeUrl::parse(url).ok())
.is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url())) .is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url()))
{ {
return false; return false;
@ -400,6 +402,7 @@ impl PyProjectTomlMut {
group: &ExtraName, group: &ExtraName,
req: &Requirement, req: &Requirement,
source: Option<&Source>, source: Option<&Source>,
raw: bool,
) -> Result<ArrayEdit, Error> { ) -> Result<ArrayEdit, Error> {
// Get or create `project.optional-dependencies`. // Get or create `project.optional-dependencies`.
let optional_dependencies = self let optional_dependencies = self
@ -428,7 +431,7 @@ impl PyProjectTomlMut {
.as_array_mut() .as_array_mut()
.ok_or(Error::MalformedDependencies)?; .ok_or(Error::MalformedDependencies)?;
let added = add_dependency(req, group, source.is_some())?; let added = add_dependency(req, group, source.is_some(), raw)?;
// If `project.optional-dependencies` is an inline table, reformat it. // If `project.optional-dependencies` is an inline table, reformat it.
// //
@ -457,6 +460,7 @@ impl PyProjectTomlMut {
group: &GroupName, group: &GroupName,
req: &Requirement, req: &Requirement,
source: Option<&Source>, source: Option<&Source>,
raw: bool,
) -> Result<ArrayEdit, Error> { ) -> Result<ArrayEdit, Error> {
// Get or create `dependency-groups`. // Get or create `dependency-groups`.
let dependency_groups = self let dependency_groups = self
@ -492,7 +496,7 @@ impl PyProjectTomlMut {
.as_array_mut() .as_array_mut()
.ok_or(Error::MalformedDependencies)?; .ok_or(Error::MalformedDependencies)?;
let added = add_dependency(req, group, source.is_some())?; let added = add_dependency(req, group, source.is_some(), raw)?;
// To avoid churn in pyproject.toml, we only sort new group keys if the // To avoid churn in pyproject.toml, we only sort new group keys if the
// existing keys were sorted. // existing keys were sorted.
@ -999,6 +1003,7 @@ pub fn add_dependency(
req: &Requirement, req: &Requirement,
deps: &mut Array, deps: &mut Array,
has_source: bool, has_source: bool,
raw: bool,
) -> Result<ArrayEdit, Error> { ) -> Result<ArrayEdit, Error> {
let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps); let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps);
@ -1057,7 +1062,11 @@ pub fn add_dependency(
Sort::Unsorted Sort::Unsorted
}; };
let req_string = req.to_string(); let req_string = if raw {
req.displayable_with_credentials().to_string()
} else {
req.to_string()
};
let index = match sort { let index = match sort {
Sort::CaseInsensitive => deps.iter().position(|dep| { Sort::CaseInsensitive => deps.iter().position(|dep| {
dep.as_str().is_some_and(|dep| { dep.as_str().is_some_and(|dep| {

View file

@ -24,7 +24,7 @@ use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase; use uv_distribution::DistributionDatabase;
use uv_distribution_types::{ use uv_distribution_types::{
Index, IndexName, IndexUrls, NameRequirementSpecification, Requirement, RequirementSource, Index, IndexName, IndexUrls, NameRequirementSpecification, Requirement, RequirementSource,
UnresolvedRequirement, VersionId, redact_credentials, UnresolvedRequirement, VersionId,
}; };
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_git::GIT_STORE; use uv_git::GIT_STORE;
@ -33,6 +33,7 @@ use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, PackageName};
use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl}; use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl};
use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl}; use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl};
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::FlatIndex; use uv_resolver::FlatIndex;
use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script};
@ -627,7 +628,7 @@ fn edits(
} }
}; };
// Redact any credentials. By default, we avoid writing sensitive credentials to files that // Remove any credentials. By default, we avoid writing sensitive credentials to files that
// will be checked into version control (e.g., `pyproject.toml` and `uv.lock`). Instead, // will be checked into version control (e.g., `pyproject.toml` and `uv.lock`). Instead,
// we store the credentials in a global store, and reuse them during resolution. The // we store the credentials in a global store, and reuse them during resolution. The
// expectation is that subsequent resolutions steps will succeed by reading from (e.g.) the // expectation is that subsequent resolutions steps will succeed by reading from (e.g.) the
@ -649,7 +650,7 @@ fn edits(
GIT_STORE.insert(RepositoryUrl::new(&git), credentials); GIT_STORE.insert(RepositoryUrl::new(&git), credentials);
// Redact the credentials. // Redact the credentials.
redact_credentials(&mut git); git.remove_credentials();
} }
Some(Source::Git { Some(Source::Git {
git, git,
@ -705,13 +706,15 @@ fn edits(
// Update the `pyproject.toml`. // Update the `pyproject.toml`.
let edit = match &dependency_type { let edit = match &dependency_type {
DependencyType::Production => toml.add_dependency(&requirement, source.as_ref())?, DependencyType::Production => {
DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref())?, toml.add_dependency(&requirement, source.as_ref(), raw)?
}
DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref(), raw)?,
DependencyType::Optional(extra) => { DependencyType::Optional(extra) => {
toml.add_optional_dependency(extra, &requirement, source.as_ref())? toml.add_optional_dependency(extra, &requirement, source.as_ref(), raw)?
} }
DependencyType::Group(group) => { DependencyType::Group(group) => {
toml.add_dependency_group_requirement(group, &requirement, source.as_ref())? toml.add_dependency_group_requirement(group, &requirement, source.as_ref(), raw)?
} }
}; };
@ -863,6 +866,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 = Url::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);

View file

@ -30,6 +30,7 @@ use uv_python::{
PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
VersionFileDiscoveryOptions, VersionFileDiscoveryOptions,
}; };
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{Installable, Lock, Preference}; use uv_resolver::{Installable, Lock, Preference};
use uv_scripts::Pep723Item; use uv_scripts::Pep723Item;
@ -1198,7 +1199,7 @@ pub(crate) enum RunCommand {
/// Execute a `pythonw` script provided via `stdin`. /// Execute a `pythonw` script provided via `stdin`.
PythonGuiStdin(Vec<u8>, Vec<OsString>), PythonGuiStdin(Vec<u8>, Vec<OsString>),
/// Execute a Python script provided via a remote URL. /// Execute a Python script provided via a remote URL.
PythonRemote(Url, tempfile::NamedTempFile, Vec<OsString>), PythonRemote(DisplaySafeUrl, tempfile::NamedTempFile, Vec<OsString>),
/// Execute an external command. /// Execute an external command.
External(OsString, Vec<OsString>), External(OsString, Vec<OsString>),
/// Execute an empty command (in practice, `python` with no arguments). /// Execute an empty command (in practice, `python` with no arguments).
@ -1464,7 +1465,7 @@ impl RunCommand {
// We don't do this check on Windows since the file path would // We don't do this check on Windows since the file path would
// be invalid anyway, and thus couldn't refer to a local file. // be invalid anyway, and thus couldn't refer to a local file.
if !cfg!(unix) || matches!(target_path.try_exists(), Ok(false)) { if !cfg!(unix) || matches!(target_path.try_exists(), Ok(false)) {
let url = Url::parse(&target.to_string_lossy())?; let url = DisplaySafeUrl::parse(&target.to_string_lossy())?;
let file_stem = url let file_stem = url
.path_segments() .path_segments()
@ -1481,7 +1482,11 @@ impl RunCommand {
.native_tls(network_settings.native_tls) .native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone()) .allow_insecure_host(network_settings.allow_insecure_host.clone())
.build(); .build();
let response = client.for_host(&url).get(url.clone()).send().await?; let response = client
.for_host(&url)
.get(Url::from(url.clone()))
.send()
.await?;
// Stream the response to the file. // Stream the response to the file.
let mut writer = file.as_file(); let mut writer = file.as_file();

View file

@ -8,7 +8,6 @@ use console::Term;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tracing::{debug, info}; use tracing::{debug, info};
use url::Url;
use uv_auth::Credentials; use uv_auth::Credentials;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder}; use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder};
@ -17,6 +16,7 @@ use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl};
use uv_publish::{ use uv_publish::{
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload, CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
}; };
use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlRef};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::commands::reporters::PublishReporter; use crate::commands::reporters::PublishReporter;
@ -26,7 +26,7 @@ use crate::settings::NetworkSettings;
pub(crate) async fn publish( pub(crate) async fn publish(
paths: Vec<String>, paths: Vec<String>,
publish_url: Url, publish_url: DisplaySafeUrl,
trusted_publishing: TrustedPublishing, trusted_publishing: TrustedPublishing,
keyring_provider: KeyringProviderType, keyring_provider: KeyringProviderType,
network_settings: &NetworkSettings, network_settings: &NetworkSettings,
@ -196,7 +196,7 @@ enum Prompt {
/// ///
/// Returns the publish URL, the username and the password. /// Returns the publish URL, the username and the password.
async fn gather_credentials( async fn gather_credentials(
mut publish_url: Url, mut publish_url: DisplaySafeUrl,
mut username: Option<String>, mut username: Option<String>,
mut password: Option<String>, mut password: Option<String>,
trusted_publishing: TrustedPublishing, trusted_publishing: TrustedPublishing,
@ -205,7 +205,7 @@ async fn gather_credentials(
check_url: Option<&IndexUrl>, check_url: Option<&IndexUrl>,
prompt: Prompt, prompt: Prompt,
printer: Printer, printer: Printer,
) -> Result<(Url, Credentials)> { ) -> Result<(DisplaySafeUrl, Credentials)> {
// Support reading username and password from the URL, for symmetry with the index API. // Support reading username and password from the URL, for symmetry with the index API.
if let Some(url_password) = publish_url.password() { if let Some(url_password) = publish_url.password() {
if password.is_some_and(|password| password != url_password) { if password.is_some_and(|password| password != url_password) {
@ -296,7 +296,7 @@ async fn gather_credentials(
if let Some(username) = &username { if let Some(username) = &username {
debug!("Fetching password from keyring"); debug!("Fetching password from keyring");
if let Some(keyring_password) = keyring_provider if let Some(keyring_password) = keyring_provider
.fetch(&publish_url, Some(username)) .fetch(&DisplaySafeUrlRef::from(&publish_url), Some(username))
.await .await
.as_ref() .as_ref()
.and_then(|credentials| credentials.password()) .and_then(|credentials| credentials.password())
@ -342,13 +342,14 @@ mod tests {
use std::str::FromStr; use std::str::FromStr;
use insta::assert_snapshot; use insta::assert_snapshot;
use url::Url;
use uv_redacted::DisplaySafeUrl;
async fn get_credentials( async fn get_credentials(
url: Url, url: DisplaySafeUrl,
username: Option<String>, username: Option<String>,
password: Option<String>, password: Option<String>,
) -> Result<(Url, Credentials)> { ) -> Result<(DisplaySafeUrl, Credentials)> {
let client = BaseClientBuilder::new().build(); let client = BaseClientBuilder::new().build();
gather_credentials( gather_credentials(
url, url,
@ -366,10 +367,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn username_password_sources() { async fn username_password_sources() {
let example_url = Url::from_str("https://example.com").unwrap(); let example_url = DisplaySafeUrl::from_str("https://example.com").unwrap();
let example_url_username = Url::from_str("https://ferris@example.com").unwrap(); let example_url_username = DisplaySafeUrl::from_str("https://ferris@example.com").unwrap();
let example_url_username_password = let example_url_username_password =
Url::from_str("https://ferris:f3rr1s@example.com").unwrap(); DisplaySafeUrl::from_str("https://ferris:f3rr1s@example.com").unwrap();
let (publish_url, credentials) = get_credentials(example_url.clone(), None, None) let (publish_url, credentials) = get_credentials(example_url.clone(), None, None)
.await .await

View file

@ -8,8 +8,6 @@ use std::time::Duration;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use url::Url;
use uv_redacted::redacted_url;
use crate::commands::human_readable_bytes; use crate::commands::human_readable_bytes;
use crate::printer::Printer; use crate::printer::Printer;
@ -20,6 +18,7 @@ use uv_distribution_types::{
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
use uv_python::PythonInstallationKey; use uv_python::PythonInstallationKey;
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars; use uv_static::EnvVars;
/// Since downloads, fetches and builds run in parallel, their message output order is /// Since downloads, fetches and builds run in parallel, their message output order is
@ -359,8 +358,7 @@ impl ProgressReporter {
self.on_request_start(Direction::Upload, name, size) self.on_request_start(Direction::Upload, name, size)
} }
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
let url = redacted_url(url);
let ProgressMode::Multi { let ProgressMode::Multi {
multi_progress, multi_progress,
state, state,
@ -390,8 +388,7 @@ impl ProgressReporter {
id id
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) {
let url = redacted_url(url);
let ProgressMode::Multi { let ProgressMode::Multi {
state, state,
multi_progress, multi_progress,
@ -481,11 +478,11 @@ impl uv_installer::PrepareReporter for PrepareReporter {
self.reporter.on_download_complete(id); self.reporter.on_download_complete(id);
} }
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
self.reporter.on_checkout_start(url, rev) self.reporter.on_checkout_start(url, rev)
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) {
self.reporter.on_checkout_complete(url, rev, id); self.reporter.on_checkout_complete(url, rev, id);
} }
} }
@ -545,11 +542,11 @@ impl uv_resolver::ResolverReporter for ResolverReporter {
self.reporter.on_build_complete(source, id); self.reporter.on_build_complete(source, id);
} }
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
self.reporter.on_checkout_start(url, rev) self.reporter.on_checkout_start(url, rev)
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) {
self.reporter.on_checkout_complete(url, rev, id); self.reporter.on_checkout_complete(url, rev, id);
} }
@ -587,11 +584,11 @@ impl uv_distribution::Reporter for ResolverReporter {
self.reporter.on_download_complete(id); self.reporter.on_download_complete(id);
} }
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
self.reporter.on_checkout_start(url, rev) self.reporter.on_checkout_start(url, rev)
} }
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) {
self.reporter.on_checkout_complete(url, rev, id); self.reporter.on_checkout_complete(url, rev, id);
} }
} }

View file

@ -4,8 +4,6 @@ use std::path::PathBuf;
use std::process; use std::process;
use std::str::FromStr; use std::str::FromStr;
use url::Url;
use uv_cache::{CacheArgs, Refresh}; use uv_cache::{CacheArgs, Refresh};
use uv_cli::comma::CommaSeparatedRequirements; use uv_cli::comma::CommaSeparatedRequirements;
use uv_cli::{ use uv_cli::{
@ -35,6 +33,7 @@ use uv_normalize::{PackageName, PipGroupName};
use uv_pep508::{ExtraName, MarkerTree, RequirementOrigin}; use uv_pep508::{ExtraName, MarkerTree, RequirementOrigin};
use uv_pypi_types::SupportedEnvironments; use uv_pypi_types::SupportedEnvironments;
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
use uv_redacted::DisplaySafeUrl;
use uv_resolver::{ use uv_resolver::{
AnnotationStyle, DependencyMode, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode, AnnotationStyle, DependencyMode, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode,
}; };
@ -3162,7 +3161,7 @@ pub(crate) struct PublishSettings {
pub(crate) index: Option<String>, pub(crate) index: Option<String>,
// Both CLI and configuration. // Both CLI and configuration.
pub(crate) publish_url: Url, pub(crate) publish_url: DisplaySafeUrl,
pub(crate) trusted_publishing: TrustedPublishing, pub(crate) trusted_publishing: TrustedPublishing,
pub(crate) keyring_provider: KeyringProviderType, pub(crate) keyring_provider: KeyringProviderType,
pub(crate) check_url: Option<IndexUrl>, pub(crate) check_url: Option<IndexUrl>,
@ -3207,7 +3206,7 @@ impl PublishSettings {
publish_url: args publish_url: args
.publish_url .publish_url
.combine(publish_url) .combine(publish_url)
.unwrap_or_else(|| Url::parse(PYPI_PUBLISH_URL).unwrap()), .unwrap_or_else(|| DisplaySafeUrl::parse(PYPI_PUBLISH_URL).unwrap()),
trusted_publishing: trusted_publishing trusted_publishing: trusted_publishing
.combine(args.trusted_publishing) .combine(args.trusted_publishing)
.unwrap_or_default(), .unwrap_or_default(),

View file

@ -1622,9 +1622,13 @@ pub async fn download_to_disk(url: &str, path: &Path) {
let client = uv_client::BaseClientBuilder::new() let client = uv_client::BaseClientBuilder::new()
.allow_insecure_host(trusted_hosts) .allow_insecure_host(trusted_hosts)
.build(); .build();
let url: reqwest::Url = url.parse().unwrap(); let url = url.parse().unwrap();
let client = client.for_host(&url); let client = client.for_host(&url);
let response = client.request(http::Method::GET, url).send().await.unwrap(); let response = client
.request(http::Method::GET, reqwest::Url::from(url))
.send()
.await
.unwrap();
let mut file = tokio::fs::File::create(path).await.unwrap(); let mut file = tokio::fs::File::create(path).await.unwrap();
let mut stream = response.bytes_stream(); let mut stream = response.bytes_stream();

View file

@ -405,6 +405,8 @@ fn add_git_private_source() -> Result<()> {
fn add_git_private_raw() -> Result<()> { fn add_git_private_raw() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");
let token = decode_token(READ_ONLY_GITHUB_TOKEN); let token = decode_token(READ_ONLY_GITHUB_TOKEN);
let mut filters = context.filters();
filters.push((&token, "***"));
let pyproject_toml = context.temp_dir.child("pyproject.toml"); let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#" pyproject_toml.write_str(indoc! {r#"
@ -415,7 +417,7 @@ fn add_git_private_raw() -> Result<()> {
dependencies = [] dependencies = []
"#})?; "#})?;
uv_snapshot!(context.filters(), context.add().arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")).arg("--raw-sources"), @r" uv_snapshot!(filters, context.add().arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")).arg("--raw-sources"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -429,16 +431,11 @@ fn add_git_private_raw() -> Result<()> {
let pyproject_toml = context.read("pyproject.toml"); let pyproject_toml = context.read("pyproject.toml");
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
insta::with_settings!({ insta::with_settings!({
filters => filters filters => filters.clone()
}, { }, {
assert_snapshot!( assert_snapshot!(
pyproject_toml, @r###" pyproject_toml, @r#"
[project] [project]
name = "project" name = "project"
version = "0.1.0" version = "0.1.0"
@ -446,14 +443,14 @@ fn add_git_private_raw() -> Result<()> {
dependencies = [ dependencies = [
"uv-private-pypackage @ git+https://***@github.com/astral-test/uv-private-pypackage", "uv-private-pypackage @ git+https://***@github.com/astral-test/uv-private-pypackage",
] ]
"### "#
); );
}); });
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");
insta::with_settings!({ insta::with_settings!({
filters => context.filters(), filters => filters.clone(),
}, { }, {
assert_snapshot!( assert_snapshot!(
lock, @r#" lock, @r#"
@ -484,7 +481,7 @@ fn add_git_private_raw() -> Result<()> {
}); });
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" uv_snapshot!(filters, context.sync().arg("--frozen"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----

View file

@ -7983,11 +7983,6 @@ fn lock_redact_git_pep508() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let context = TestContext::new("3.12").with_filtered_link_mode_warning();
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let pyproject_toml = context.temp_dir.child("pyproject.toml"); let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { pyproject_toml.write_str(&formatdoc! {
r#" r#"
@ -8000,7 +7995,7 @@ fn lock_redact_git_pep508() -> Result<()> {
token = token, token = token,
})?; })?;
uv_snapshot!(&filters, context.lock(), @r###" uv_snapshot!(&context.filters(), context.lock(), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8012,7 +8007,7 @@ fn lock_redact_git_pep508() -> Result<()> {
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");
insta::with_settings!({ insta::with_settings!({
filters => filters.clone(), filters => context.filters(),
}, { }, {
assert_snapshot!( assert_snapshot!(
lock, @r#" lock, @r#"
@ -8043,7 +8038,7 @@ fn lock_redact_git_pep508() -> Result<()> {
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8053,7 +8048,7 @@ fn lock_redact_git_pep508() -> Result<()> {
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8073,11 +8068,6 @@ fn lock_redact_git_sources() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let context = TestContext::new("3.12").with_filtered_link_mode_warning();
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let pyproject_toml = context.temp_dir.child("pyproject.toml"); let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { pyproject_toml.write_str(&formatdoc! {
r#" r#"
@ -8093,7 +8083,7 @@ fn lock_redact_git_sources() -> Result<()> {
token = token, token = token,
})?; })?;
uv_snapshot!(&filters, context.lock(), @r###" uv_snapshot!(&context.filters(), context.lock(), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8105,7 +8095,7 @@ fn lock_redact_git_sources() -> Result<()> {
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");
insta::with_settings!({ insta::with_settings!({
filters => filters.clone(), filters => context.filters(),
}, { }, {
assert_snapshot!( assert_snapshot!(
lock, @r#" lock, @r#"
@ -8136,7 +8126,7 @@ fn lock_redact_git_sources() -> Result<()> {
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8146,7 +8136,7 @@ fn lock_redact_git_sources() -> Result<()> {
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8166,11 +8156,6 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let context = TestContext::new("3.12").with_filtered_link_mode_warning();
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let pyproject_toml = context.temp_dir.child("pyproject.toml"); let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { pyproject_toml.write_str(&formatdoc! {
r#" r#"
@ -8183,7 +8168,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
token = token, token = token,
})?; })?;
uv_snapshot!(&filters, context.lock(), @r###" uv_snapshot!(&context.filters(), context.lock(), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8196,7 +8181,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");
insta::with_settings!({ insta::with_settings!({
filters => filters.clone(), filters => context.filters(),
}, { }, {
assert_snapshot!( assert_snapshot!(
lock, @r#" lock, @r#"
@ -8221,7 +8206,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8232,7 +8217,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8249,12 +8234,6 @@ fn lock_redact_git_pep508_non_project() -> Result<()> {
#[test] #[test]
fn lock_redact_index_sources() -> Result<()> { fn lock_redact_index_sources() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let context = TestContext::new("3.12").with_filtered_link_mode_warning();
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let pyproject_toml = context.temp_dir.child("pyproject.toml"); let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str( pyproject_toml.write_str(
@ -8274,7 +8253,7 @@ fn lock_redact_index_sources() -> Result<()> {
"#, "#,
)?; )?;
uv_snapshot!(&filters, context.lock(), @r###" uv_snapshot!(&context.filters(), context.lock(), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8286,7 +8265,7 @@ fn lock_redact_index_sources() -> Result<()> {
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");
insta::with_settings!({ insta::with_settings!({
filters => filters.clone(), filters => context.filters(),
}, { }, {
assert_snapshot!( assert_snapshot!(
lock, @r#" lock, @r#"
@ -8321,7 +8300,7 @@ fn lock_redact_index_sources() -> Result<()> {
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8331,7 +8310,7 @@ fn lock_redact_index_sources() -> Result<()> {
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8348,12 +8327,6 @@ fn lock_redact_index_sources() -> Result<()> {
#[test] #[test]
fn lock_redact_url_sources() -> Result<()> { fn lock_redact_url_sources() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let context = TestContext::new("3.12").with_filtered_link_mode_warning();
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let pyproject_toml = context.temp_dir.child("pyproject.toml"); let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(r#" pyproject_toml.write_str(r#"
@ -8367,7 +8340,7 @@ fn lock_redact_url_sources() -> Result<()> {
iniconfig = { url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } iniconfig = { url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
"#)?; "#)?;
uv_snapshot!(&filters, context.lock(), @r###" uv_snapshot!(&context.filters(), context.lock(), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8379,7 +8352,7 @@ fn lock_redact_url_sources() -> Result<()> {
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");
insta::with_settings!({ insta::with_settings!({
filters => filters.clone(), filters => context.filters(),
}, { }, {
assert_snapshot!( assert_snapshot!(
lock, @r#" lock, @r#"
@ -8413,7 +8386,7 @@ fn lock_redact_url_sources() -> Result<()> {
}); });
// Re-run with `--locked`. // Re-run with `--locked`.
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8423,7 +8396,7 @@ fn lock_redact_url_sources() -> Result<()> {
"###); "###);
// Install from the lockfile. // Install from the lockfile.
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -8431,8 +8404,8 @@ fn lock_redact_url_sources() -> Result<()> {
----- stderr ----- ----- stderr -----
Prepared 1 package in [TIME] Prepared 1 package in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
+ iniconfig==2.0.0 (from https://public:heron@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) + iniconfig==2.0.0 (from https://public:****@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
"###); ");
Ok(()) Ok(())
} }
@ -15837,7 +15810,7 @@ fn lock_explicit_default_index() -> Result<()> {
DEBUG No workspace root found, using project root DEBUG No workspace root found, using project root
DEBUG Ignoring existing lockfile due to mismatched requirements for: `project==0.1.0` DEBUG Ignoring existing lockfile due to mismatched requirements for: `project==0.1.0`
Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }} Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }}
Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }} Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: https://test.pypi.org/simple, given: None }), format: Simple }), conflict: None }, origin: None }}
DEBUG Solving with installed Python version: 3.12.[X] DEBUG Solving with installed Python version: 3.12.[X]
DEBUG Solving with target Python version: >=3.12 DEBUG Solving with target Python version: >=3.12
DEBUG Adding direct dependency: project* DEBUG Adding direct dependency: project*

View file

@ -2119,18 +2119,12 @@ fn install_git_private_https_pat() {
let context = TestContext::new("3.8"); let context = TestContext::new("3.8");
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let package = format!( let package = format!(
"uv-private-pypackage@ git+https://{token}@github.com/astral-test/uv-private-pypackage" "uv-private-pypackage@ git+https://{token}@github.com/astral-test/uv-private-pypackage"
); );
uv_snapshot!(filters, context.pip_install().arg(package) uv_snapshot!(context.filters(), context.pip_install().arg(package)
, @r###" , @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2139,8 +2133,8 @@ fn install_git_private_https_pat() {
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
Prepared 1 package in [TIME] Prepared 1 package in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
+ uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
"###); ");
context.assert_installed("uv_private_pypackage", "0.1.0"); context.assert_installed("uv_private_pypackage", "0.1.0");
} }
@ -2153,16 +2147,11 @@ fn install_git_private_https_pat_mixed_with_public() {
let context = TestContext::new("3.8"); let context = TestContext::new("3.8");
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let package = format!( let package = format!(
"uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage" "uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage"
); );
uv_snapshot!(filters, context.pip_install().arg(package).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), uv_snapshot!(context.filters(), context.pip_install().arg(package).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"),
@r###" @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -2172,7 +2161,7 @@ fn install_git_private_https_pat_mixed_with_public() {
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389)
"###); "###);
@ -2187,11 +2176,6 @@ fn install_git_private_https_multiple_pat() {
let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2); let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2);
let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")]
.into_iter()
.chain(context.filters())
.collect();
let package_1 = format!( let package_1 = format!(
"uv-private-pypackage @ git+https://{token_1}@github.com/astral-test/uv-private-pypackage" "uv-private-pypackage @ git+https://{token_1}@github.com/astral-test/uv-private-pypackage"
); );
@ -2199,7 +2183,7 @@ fn install_git_private_https_multiple_pat() {
"uv-private-pypackage-2 @ git+https://{token_2}@github.com/astral-test/uv-private-pypackage-2" "uv-private-pypackage-2 @ git+https://{token_2}@github.com/astral-test/uv-private-pypackage-2"
); );
uv_snapshot!(filters, context.pip_install().arg(package_1).arg(package_2) uv_snapshot!(context.filters(), context.pip_install().arg(package_1).arg(package_2)
, @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -2209,8 +2193,8 @@ fn install_git_private_https_multiple_pat() {
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ uv-private-pypackage==0.1.0 (from git+https://***_1@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071)
+ uv-private-pypackage-2==0.1.0 (from git+https://***_2@github.com/astral-test/uv-private-pypackage-2@45c0bec7365710f09b1f4dbca61c86dde9537e4e) + uv-private-pypackage-2==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage-2@45c0bec7365710f09b1f4dbca61c86dde9537e4e)
"###); "###);
context.assert_installed("uv_private_pypackage", "0.1.0"); context.assert_installed("uv_private_pypackage", "0.1.0");
@ -2223,11 +2207,7 @@ fn install_git_private_https_pat_at_ref() {
let context = TestContext::new("3.8"); let context = TestContext::new("3.8");
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let mut filters: Vec<_> = [(token.as_str(), "***")] let mut filters = context.filters();
.into_iter()
.chain(context.filters())
.collect();
filters.push((r"git\+https://", "")); filters.push((r"git\+https://", ""));
// A user is _required_ on Windows // A user is _required_ on Windows
@ -2241,8 +2221,8 @@ fn install_git_private_https_pat_at_ref() {
let package = format!( let package = format!(
"uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac" "uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac"
); );
uv_snapshot!(filters, context.pip_install() uv_snapshot!(context.filters(), context.pip_install()
.arg(package), @r###" .arg(package), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2251,8 +2231,8 @@ fn install_git_private_https_pat_at_ref() {
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
Prepared 1 package in [TIME] Prepared 1 package in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
+ uv-private-pypackage==0.1.0 (from ***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac) + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
"###); ");
context.assert_installed("uv_private_pypackage", "0.1.0"); context.assert_installed("uv_private_pypackage", "0.1.0");
} }
@ -2270,12 +2250,7 @@ fn install_git_private_https_pat_and_username() {
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let user = "astral-test-bot"; let user = "astral-test-bot";
let filters: Vec<_> = [(token.as_str(), "***")] uv_snapshot!(context.filters(), context.pip_install().arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage"))
.into_iter()
.chain(context.filters())
.collect();
uv_snapshot!(filters, context.pip_install().arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage"))
, @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -2285,7 +2260,7 @@ fn install_git_private_https_pat_and_username() {
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
Prepared 1 package in [TIME] Prepared 1 package in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
+ uv-private-pypackage==0.1.0 (from git+https://astral-test-bot:***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac) + uv-private-pypackage==0.1.0 (from git+https://astral-test-bot:****@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac)
"###); "###);
context.assert_installed("uv_private_pypackage", "0.1.0"); context.assert_installed("uv_private_pypackage", "0.1.0");
@ -2301,7 +2276,10 @@ fn install_git_private_https_pat_not_authorized() {
let token = "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"; let token = "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";
let mut filters = context.filters(); let mut filters = context.filters();
filters.insert(0, (token, "***")); // TODO(john): We need this filter because we are displaying the token when
// an underlying process error message is being displayed. We should actually
// mask it.
filters.push((token, "***"));
filters.push(("`.*/git fetch (.*)`", "`git fetch $1`")); filters.push(("`.*/git fetch (.*)`", "`git fetch $1`"));
// We provide a username otherwise (since the token is invalid), the git cli will prompt for a password // We provide a username otherwise (since the token is invalid), the git cli will prompt for a password
@ -2314,7 +2292,7 @@ fn install_git_private_https_pat_not_authorized() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
× Failed to download and build `uv-private-pypackage @ git+https://git:***@github.com/astral-test/uv-private-pypackage` × Failed to download and build `uv-private-pypackage @ git+https://git:****@github.com/astral-test/uv-private-pypackage`
Git operation failed Git operation failed
failed to clone into: [CACHE_DIR]/git-v0/db/8401f5508e3e612d failed to clone into: [CACHE_DIR]/git-v0/db/8401f5508e3e612d
process didn't exit successfully: `git fetch --force --update-head-ok 'https://git:***@github.com/astral-test/uv-private-pypackage' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128) process didn't exit successfully: `git fetch --force --update-head-ok 'https://git:***@github.com/astral-test/uv-private-pypackage' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128)
@ -2334,17 +2312,12 @@ fn install_github_artifact_private_https_pat_mixed_with_public() {
let context = TestContext::new("3.8"); let context = TestContext::new("3.8");
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let filters: Vec<_> = [(token.as_str(), "***")]
.into_iter()
.chain(context.filters())
.collect();
let private_package = format!( let private_package = format!(
"uv-private-pypackage @ https://{token}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl" "uv-private-pypackage @ https://{token}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl"
); );
let public_package = "uv-public-pypackage @ https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl"; let public_package = "uv-public-pypackage @ https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl";
uv_snapshot!(filters, context.pip_install().arg(private_package).arg(public_package), uv_snapshot!(context.filters(), context.pip_install().arg(private_package).arg(public_package),
@r###" @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -2354,7 +2327,7 @@ fn install_github_artifact_private_https_pat_mixed_with_public() {
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ uv-private-pypackage==0.1.0 (from https://***@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl) + uv-private-pypackage==0.1.0 (from https://****@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl)
+ uv-public-pypackage==0.1.0 (from https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl) + uv-public-pypackage==0.1.0 (from https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl)
"###); "###);
@ -2370,11 +2343,6 @@ fn install_github_artifact_private_https_multiple_pat() {
let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN);
let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2); let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2);
let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")]
.into_iter()
.chain(context.filters())
.collect();
let package_1 = format!( let package_1 = format!(
"uv-private-pypackage @ https://astral-test-bot:{token_1}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl" "uv-private-pypackage @ https://astral-test-bot:{token_1}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl"
); );
@ -2382,7 +2350,7 @@ fn install_github_artifact_private_https_multiple_pat() {
"uv-private-pypackage-2 @ https://astral-test-bot:{token_2}@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl" "uv-private-pypackage-2 @ https://astral-test-bot:{token_2}@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl"
); );
uv_snapshot!(filters, context.pip_install().arg(package_1).arg(package_2) uv_snapshot!(context.filters(), context.pip_install().arg(package_1).arg(package_2)
, @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
@ -2392,8 +2360,8 @@ fn install_github_artifact_private_https_multiple_pat() {
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ uv-private-pypackage==0.1.0 (from https://astral-test-bot:***_1@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl) + uv-private-pypackage==0.1.0 (from https://astral-test-bot:****@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl)
+ uv-private-pypackage-2==0.1.0 (from https://astral-test-bot:***_2@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl) + uv-private-pypackage-2==0.1.0 (from https://astral-test-bot:****@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl)
"###); "###);
context.assert_installed("uv_private_pypackage", "0.1.0"); context.assert_installed("uv_private_pypackage", "0.1.0");

View file

@ -112,21 +112,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -293,21 +279,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -475,21 +447,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -689,21 +647,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -1032,21 +976,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -1240,21 +1170,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -1272,21 +1188,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://test.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"test.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://test.pypi.org/simple", "https://test.pypi.org/simple",
), ),
@ -1455,21 +1357,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://test.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"test.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://test.pypi.org/simple", "https://test.pypi.org/simple",
), ),
@ -1489,21 +1377,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -1521,21 +1395,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://test.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"test.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://test.pypi.org/simple", "https://test.pypi.org/simple",
), ),
@ -1728,21 +1588,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://download.pytorch.org/whl/torch_stable.html,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl/torch_stable.html",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://download.pytorch.org/whl/torch_stable.html", "https://download.pytorch.org/whl/torch_stable.html",
), ),
@ -2097,21 +1943,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://download.pytorch.org/whl,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://download.pytorch.org/whl", "https://download.pytorch.org/whl",
), ),
@ -2129,21 +1961,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://test.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"test.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://test.pypi.org/simple", "https://test.pypi.org/simple",
), ),
@ -2310,21 +2128,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://download.pytorch.org/whl,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"download.pytorch.org",
),
),
port: None,
path: "/whl",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://download.pytorch.org/whl", "https://download.pytorch.org/whl",
), ),
@ -2342,21 +2146,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://test.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"test.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://test.pypi.org/simple", "https://test.pypi.org/simple",
), ),
@ -3537,21 +3327,7 @@ fn resolve_both() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -3843,21 +3619,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
name: None, name: None,
url: Pypi( url: Pypi(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://pypi.org/simple", "https://pypi.org/simple",
), ),
@ -4629,21 +4391,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://cli.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"cli.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://cli.pypi.org/simple", "https://cli.pypi.org/simple",
), ),
@ -4663,21 +4411,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://file.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"file.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://file.pypi.org/simple", "https://file.pypi.org/simple",
), ),
@ -4844,21 +4578,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://cli.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"cli.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://cli.pypi.org/simple", "https://cli.pypi.org/simple",
), ),
@ -4878,21 +4598,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://file.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"file.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://file.pypi.org/simple", "https://file.pypi.org/simple",
), ),
@ -5065,21 +4771,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://cli.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"cli.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://cli.pypi.org/simple", "https://cli.pypi.org/simple",
), ),
@ -5099,21 +4791,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://file.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"file.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://file.pypi.org/simple", "https://file.pypi.org/simple",
), ),
@ -5281,21 +4959,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://cli.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"cli.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://cli.pypi.org/simple", "https://cli.pypi.org/simple",
), ),
@ -5315,21 +4979,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://file.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"file.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://file.pypi.org/simple", "https://file.pypi.org/simple",
), ),
@ -5504,21 +5154,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://cli.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"cli.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://cli.pypi.org/simple", "https://cli.pypi.org/simple",
), ),
@ -5538,21 +5174,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://file.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"file.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://file.pypi.org/simple", "https://file.pypi.org/simple",
), ),
@ -5720,21 +5342,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://cli.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"cli.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://cli.pypi.org/simple", "https://cli.pypi.org/simple",
), ),
@ -5754,21 +5362,7 @@ fn index_priority() -> anyhow::Result<()> {
name: None, name: None,
url: Url( url: Url(
VerbatimUrl { VerbatimUrl {
url: Url { url: https://file.pypi.org/simple,
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"file.pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some( given: Some(
"https://file.pypi.org/simple", "https://file.pypi.org/simple",
), ),