mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 05:03:46 +00:00
Preserve index URL priority order when writing to pyproject.toml (#14831)
## Summary A little nuanced, but... When you add multiple `--index` URLs on the CLI (e.g., in `uv pip install`), we check the first-provided index, then the second index, etc. However, when we _write_ those URLs to the `pyproject.toml` in `uv add`, we were adding them in reverse-order. We now add them in a way that preserves the priority order. Closes https://github.com/astral-sh/uv/issues/14817.
This commit is contained in:
parent
3d1fec2732
commit
27ade0676f
4 changed files with 146 additions and 13 deletions
|
|
@ -392,6 +392,7 @@ impl PyProjectTomlMut {
|
|||
|
||||
/// Add an [`Index`] to `tool.uv.index`.
|
||||
pub fn add_index(&mut self, index: &Index) -> Result<(), Error> {
|
||||
let size = self.doc.len();
|
||||
let existing = self
|
||||
.doc
|
||||
.entry("tool")
|
||||
|
|
@ -472,8 +473,7 @@ impl PyProjectTomlMut {
|
|||
if table
|
||||
.get("url")
|
||||
.and_then(|item| item.as_str())
|
||||
.and_then(|url| DisplaySafeUrl::parse(url).ok())
|
||||
.is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url.url()))
|
||||
.is_none_or(|url| url != index.url.without_credentials().as_str())
|
||||
{
|
||||
let mut formatted = Formatted::new(index.url.without_credentials().to_string());
|
||||
if let Some(value) = table.get("url").and_then(Item::as_value) {
|
||||
|
|
@ -552,6 +552,9 @@ impl PyProjectTomlMut {
|
|||
table.set_position(position + 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let position = isize::try_from(size).expect("TOML table size fits in `isize`");
|
||||
table.set_position(position);
|
||||
}
|
||||
|
||||
// Push the item to the table.
|
||||
|
|
|
|||
|
|
@ -644,7 +644,9 @@ pub(crate) async fn add(
|
|||
// Add any indexes that were provided on the command-line, in priority order.
|
||||
if !raw {
|
||||
let urls = IndexUrls::from_indexes(indexes);
|
||||
for index in urls.defined_indexes() {
|
||||
let mut indexes = urls.defined_indexes().collect::<Vec<_>>();
|
||||
indexes.reverse();
|
||||
for index in indexes {
|
||||
toml.add_index(index)?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11307,6 +11307,115 @@ fn remove_all_with_comments() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// If multiple indexes are provided on the CLI, the first-provided index should take precedence
|
||||
/// during resolution, and should appear first in the `pyproject.toml` file.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/uv/issues/14817>
|
||||
#[test]
|
||||
fn multiple_index_cli() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
.add()
|
||||
.arg("requests")
|
||||
.arg("--index")
|
||||
.arg("https://test.pypi.org/simple")
|
||||
.arg("--index")
|
||||
.arg("https://pypi.org/simple"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ requests==2.5.4.1
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"requests>=2.5.4.1",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://test.pypi.org/simple"
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.org/simple"
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "requests", specifier = ">=2.5.4.1" }]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.5.4.1"
|
||||
source = { registry = "https://test.pypi.org/simple" }
|
||||
sdist = { url = "https://test-files.pythonhosted.org/packages/6e/93/638dbb5f2c1f4120edaad4f3d45ffb1718e463733ad07d68f59e042901d6/requests-2.5.4.1.tar.gz", hash = "sha256:b19df51fa3e52a2bd7fc80a1ac11fb6b2f51a7c0bf31ba9ff6b5d11ea8605ae9", size = 448691, upload-time = "2015-03-13T21:30:03.228Z" }
|
||||
wheels = [
|
||||
{ url = "https://test-files.pythonhosted.org/packages/6d/00/8ed1b6ea43b10bfe28d08e6af29fd6aa5d8dab5e45ead9394a6268a2d2ec/requests-2.5.4.1-py2.py3-none-any.whl", hash = "sha256:0a2c98e46121e7507afb0edc89d342641a1fb9e8d56f7d592d4975ee6b685f9a", size = 468942, upload-time = "2015-03-13T21:29:55.769Z" },
|
||||
]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If an index is repeated by the CLI and an environment variable, the CLI value should take
|
||||
/// precedence.
|
||||
///
|
||||
|
|
@ -11418,7 +11527,7 @@ fn repeated_index_cli_environment_variable() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// If an index is repeated on the CLI, the last-provided index should take precedence.
|
||||
/// If an index is repeated on the CLI, the first-provided index should take precedence.
|
||||
/// Newlines in `UV_INDEX` should be treated as separators.
|
||||
///
|
||||
/// The index that appears in the `pyproject.toml` should also be consistent with the index that
|
||||
|
|
@ -11524,7 +11633,7 @@ fn repeated_index_cli_environment_variable_newline() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// If an index is repeated on the CLI, the last-provided index should take precedence.
|
||||
/// If an index is repeated on the CLI, the first-provided index should take precedence.
|
||||
///
|
||||
/// The index that appears in the `pyproject.toml` should also be consistent with the index that
|
||||
/// appears in the `uv.lock`.
|
||||
|
|
@ -11634,7 +11743,7 @@ fn repeated_index_cli() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// If an index is repeated on the CLI, the last-provided index should take precedence.
|
||||
/// If an index is repeated on the CLI, the first-provided index should take precedence.
|
||||
///
|
||||
/// The index that appears in the `pyproject.toml` should also be consistent with the index that
|
||||
/// appears in the `uv.lock`.
|
||||
|
|
|
|||
|
|
@ -28813,8 +28813,7 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
|
|||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
|
|
@ -28824,8 +28823,7 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
|
|||
[[tool.uv.index]]
|
||||
name = "pypi-proxy"
|
||||
url = "https://pypi-proxy.fly.dev/simple/"
|
||||
"#,
|
||||
)?;
|
||||
"#})?;
|
||||
|
||||
let no_trailing_slash_url = "https://pypi-proxy.fly.dev/simple";
|
||||
|
||||
|
|
@ -28843,6 +28841,28 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
|
|||
+ sniffio==1.3.1
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio>=4.3.0",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pypi-proxy"
|
||||
url = "https://pypi-proxy.fly.dev/simple"
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
|
|
@ -28904,13 +28924,12 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
|
|||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue