Ignore pyproject index username in lockfile comparison (#16995)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Pyproject.toml index url may contain a username while lockfile doesn't.
Treat it as the same index to prevent unintended package updates

Fixes #16436

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
jkipper 2025-12-16 11:47:50 +01:00 committed by GitHub
parent b58f543e5e
commit af348c2a88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 1 deletions

View File

@ -163,7 +163,11 @@ impl PreferenceIndex {
match self { match self {
Self::Any => true, Self::Any => true,
Self::Implicit => false, Self::Implicit => false,
Self::Explicit(preference) => preference == index, Self::Explicit(preference) => {
// Preferences are stored in the lockfile without credentials, while the index URL
// in locations such as `pyproject.toml` may contain credentials.
*preference.url() == *index.without_credentials()
}
} }
} }
} }
@ -381,3 +385,30 @@ impl From<Version> for Pin {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
/// Test that [`PreferenceIndex::matches`] correctly ignores credentials when comparing URLs.
///
/// This is relevant for matching lockfile preferences (stored without credentials)
/// against index URLs from pyproject.toml (which may include usernames for auth).
#[test]
fn test_preference_index_matches_ignores_credentials() {
// URL without credentials (as stored in lockfile)
let index_without_creds = IndexUrl::from_str("https:/pypi_index.com/simple").unwrap();
// URL with username (as specified in pyproject.toml)
let index_with_username =
IndexUrl::from_str("https://username@pypi_index.com/simple").unwrap();
let preference = PreferenceIndex::Explicit(index_without_creds.clone());
assert!(
preference.matches(&index_with_username),
"PreferenceIndex should match URLs that differ only in username"
);
}
}

View File

@ -9600,6 +9600,84 @@ fn lock_redact_https() -> Result<()> {
Ok(()) Ok(())
} }
/// Test that packages aren't unnecessarily updated when an index URL contains a username.
#[test]
fn lock_index_url_username_change_no_update() -> Result<()> {
let context = TestContext::new("3.12");
// Create initial lockfile with exact version constraint
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==4.0.0"]
[[tool.uv.index]]
name = "test-index"
url = "https://fakeuser@pypi.org/simple"
[tool.uv.sources]
anyio = { index = "test-index" }
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
let lock = context.read("uv.lock");
// Verify anyio 4.0.0 is locked
assert!(lock.contains("name = \"anyio\""));
assert!(lock.contains("version = \"4.0.0\""));
// Update pyproject.toml to simulate availability of newer package with more open but still compatible constraint
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio>=4.0.0"]
[[tool.uv.index]]
name = "test-index"
url = "https://fakeuser@pypi.org/simple"
[tool.uv.sources]
anyio = { index = "test-index" }
"#,
)?;
// Run `uv lock` to update the lockfile
// The package should stay at 4.0.0
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
"###);
let lock_after = context.read("uv.lock");
assert!(
lock_after.contains("version = \"4.0.0\""),
"anyio should remain at version 4.0.0, not update despite >=4.0.0 constraint"
);
Ok(())
}
#[test] #[test]
#[cfg(feature = "git")] #[cfg(feature = "git")]
fn lock_redact_git_pep508() -> Result<()> { fn lock_redact_git_pep508() -> Result<()> {