mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-14 17:59: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`.
|
/// Add an [`Index`] to `tool.uv.index`.
|
||||||
pub fn add_index(&mut self, index: &Index) -> Result<(), Error> {
|
pub fn add_index(&mut self, index: &Index) -> Result<(), Error> {
|
||||||
|
let size = self.doc.len();
|
||||||
let existing = self
|
let existing = self
|
||||||
.doc
|
.doc
|
||||||
.entry("tool")
|
.entry("tool")
|
||||||
|
|
@ -472,8 +473,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| DisplaySafeUrl::parse(url).ok())
|
.is_none_or(|url| url != index.url.without_credentials().as_str())
|
||||||
.is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url.url()))
|
|
||||||
{
|
{
|
||||||
let mut formatted = Formatted::new(index.url.without_credentials().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) {
|
||||||
|
|
@ -552,6 +552,9 @@ impl PyProjectTomlMut {
|
||||||
table.set_position(position + 1);
|
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.
|
// 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.
|
// Add any indexes that were provided on the command-line, in priority order.
|
||||||
if !raw {
|
if !raw {
|
||||||
let urls = IndexUrls::from_indexes(indexes);
|
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)?;
|
toml.add_index(index)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11307,6 +11307,115 @@ fn remove_all_with_comments() -> Result<()> {
|
||||||
Ok(())
|
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
|
/// If an index is repeated by the CLI and an environment variable, the CLI value should take
|
||||||
/// precedence.
|
/// precedence.
|
||||||
///
|
///
|
||||||
|
|
@ -11418,7 +11527,7 @@ fn repeated_index_cli_environment_variable() -> Result<()> {
|
||||||
Ok(())
|
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.
|
/// 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
|
/// 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(())
|
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
|
/// The index that appears in the `pyproject.toml` should also be consistent with the index that
|
||||||
/// appears in the `uv.lock`.
|
/// appears in the `uv.lock`.
|
||||||
|
|
@ -11634,7 +11743,7 @@ fn repeated_index_cli() -> Result<()> {
|
||||||
Ok(())
|
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
|
/// The index that appears in the `pyproject.toml` should also be consistent with the index that
|
||||||
/// appears in the `uv.lock`.
|
/// 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 context = TestContext::new("3.12");
|
||||||
|
|
||||||
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(indoc! {r#"
|
||||||
r#"
|
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -28824,8 +28823,7 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
|
||||||
[[tool.uv.index]]
|
[[tool.uv.index]]
|
||||||
name = "pypi-proxy"
|
name = "pypi-proxy"
|
||||||
url = "https://pypi-proxy.fly.dev/simple/"
|
url = "https://pypi-proxy.fly.dev/simple/"
|
||||||
"#,
|
"#})?;
|
||||||
)?;
|
|
||||||
|
|
||||||
let no_trailing_slash_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
|
+ 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");
|
let lock = context.read("uv.lock");
|
||||||
|
|
||||||
insta::with_settings!({
|
insta::with_settings!({
|
||||||
|
|
@ -28904,13 +28924,12 @@ fn lock_trailing_slash_index_url_in_pyproject_not_index_argument() -> Result<()>
|
||||||
|
|
||||||
// Re-run with `--locked`.
|
// Re-run with `--locked`.
|
||||||
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
|
||||||
success: false
|
success: true
|
||||||
exit_code: 1
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 4 packages in [TIME]
|
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(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue