mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 12:59:45 +00:00
sort dependencies in pyproject.toml (#6388)
## Summary resolves https://github.com/astral-sh/uv/issues/6203 ## Test Plan added a test fixing the bug described in the issue. --------- Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
parent
f956ab8fae
commit
4f5356ed55
4 changed files with 171 additions and 18 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5281,6 +5281,7 @@ dependencies = [
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"glob",
|
"glob",
|
||||||
"insta",
|
"insta",
|
||||||
|
"itertools 0.13.0",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ toml = { workspace = true }
|
||||||
toml_edit = { workspace = true }
|
toml_edit = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.39.0", features = ["filters", "json", "redactions"] }
|
insta = { version = "1.39.0", features = ["filters", "json", "redactions"] }
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
|
use itertools::Itertools;
|
||||||
|
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
||||||
|
use pep508_rs::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fmt, mem};
|
use std::{fmt, mem};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
||||||
|
|
||||||
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
|
||||||
use pep508_rs::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl};
|
|
||||||
use uv_fs::PortablePath;
|
use uv_fs::PortablePath;
|
||||||
|
|
||||||
use crate::pyproject::{DependencyType, Source};
|
use crate::pyproject::{DependencyType, Source};
|
||||||
|
|
@ -522,13 +521,53 @@ pub fn add_dependency(
|
||||||
deps: &mut Array,
|
deps: &mut Array,
|
||||||
has_source: bool,
|
has_source: bool,
|
||||||
) -> Result<ArrayEdit, Error> {
|
) -> Result<ArrayEdit, Error> {
|
||||||
// Find matching dependencies.
|
|
||||||
let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps);
|
let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps);
|
||||||
|
|
||||||
match to_replace.as_slice() {
|
match to_replace.as_slice() {
|
||||||
[] => {
|
[] => {
|
||||||
deps.push(req.to_string());
|
// Determine if the dependency list is sorted prior to
|
||||||
|
// adding the new dependency; the new dependency list
|
||||||
|
// will be sorted only when the original list is sorted
|
||||||
|
// so that user's custom dependency ordering is preserved.
|
||||||
|
// Additionally, if the table is invalid (i.e. contains non-string values)
|
||||||
|
// we still treat it as unsorted for the sake of simplicity.
|
||||||
|
let sorted = deps.iter().all(toml_edit::Value::is_str)
|
||||||
|
&& deps
|
||||||
|
.iter()
|
||||||
|
.tuple_windows()
|
||||||
|
.all(|(a, b)| a.as_str() <= b.as_str());
|
||||||
|
|
||||||
|
let req_string = req.to_string();
|
||||||
|
let index = if sorted {
|
||||||
|
deps.iter()
|
||||||
|
.position(|d: &Value| d.as_str() > Some(req_string.as_str()))
|
||||||
|
.unwrap_or(deps.len())
|
||||||
|
} else {
|
||||||
|
deps.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
deps.insert(index, req_string);
|
||||||
|
// `reformat_array_multiline` uses the indentation of the first dependency entry.
|
||||||
|
// Therefore, we retrieve the indentation of the first dependency entry and apply it to
|
||||||
|
// the new entry. Note that it is only necessary if the newly added dependency is going
|
||||||
|
// to be the first in the list _and_ the dependency list was not empty prior to adding
|
||||||
|
// the new dependency.
|
||||||
|
if deps.len() > 1 && index == 0 {
|
||||||
|
let prefix = deps
|
||||||
|
.clone()
|
||||||
|
.get(index + 1)
|
||||||
|
.unwrap()
|
||||||
|
.decor()
|
||||||
|
.prefix()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
deps.get_mut(index).unwrap().decor_mut().set_prefix(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
reformat_array_multiline(deps);
|
reformat_array_multiline(deps);
|
||||||
Ok(ArrayEdit::Add(deps.len() - 1))
|
|
||||||
|
Ok(ArrayEdit::Add(index))
|
||||||
}
|
}
|
||||||
[_] => {
|
[_] => {
|
||||||
let (i, mut old_req) = to_replace.remove(0);
|
let (i, mut old_req) = to_replace.remove(0);
|
||||||
|
|
|
||||||
|
|
@ -2310,8 +2310,8 @@ fn add_update_marker() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests>=2.30; python_version >= '3.11'",
|
|
||||||
"requests>=2.0,<2.29 ; python_full_version < '3.11'",
|
"requests>=2.0,<2.29 ; python_full_version < '3.11'",
|
||||||
|
"requests>=2.30; python_version >= '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
@ -2348,8 +2348,8 @@ fn add_update_marker() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests>=2.30; python_version >= '3.11'",
|
|
||||||
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
||||||
|
"requests>=2.30; python_version >= '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
@ -2390,8 +2390,8 @@ fn add_update_marker() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests>=2.30; python_version >= '3.11'",
|
|
||||||
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
||||||
|
"requests>=2.30; python_version >= '3.11'",
|
||||||
"requests>=2.31 ; python_full_version >= '3.12' and sys_platform == 'win32'",
|
"requests>=2.31 ; python_full_version >= '3.12' and sys_platform == 'win32'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2430,10 +2430,10 @@ fn add_update_marker() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests>=2.30; python_version >= '3.11'",
|
|
||||||
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
||||||
"requests>=2.31 ; python_full_version >= '3.12' and sys_platform == 'win32'",
|
|
||||||
"requests>=2.10 ; sys_platform == 'win32'",
|
"requests>=2.10 ; sys_platform == 'win32'",
|
||||||
|
"requests>=2.30; python_version >= '3.11'",
|
||||||
|
"requests>=2.31 ; python_full_version >= '3.12' and sys_platform == 'win32'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
@ -3848,8 +3848,8 @@ fn add_requirements_file() -> Result<()> {
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flask==2.3.2",
|
|
||||||
"anyio",
|
"anyio",
|
||||||
|
"flask==2.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
@ -3932,9 +3932,9 @@ fn add_script() -> Result<()> {
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.11"
|
# requires-python = ">=3.11"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
|
# "anyio",
|
||||||
# "requests<3",
|
# "requests<3",
|
||||||
# "rich",
|
# "rich",
|
||||||
# "anyio",
|
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
|
||||||
|
|
@ -3984,8 +3984,8 @@ fn add_script_without_metadata_table() -> Result<()> {
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.12"
|
# requires-python = ">=3.12"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "rich",
|
|
||||||
# "requests<3",
|
# "requests<3",
|
||||||
|
# "rich",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -4036,8 +4036,8 @@ fn add_script_without_metadata_table_with_shebang() -> Result<()> {
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.12"
|
# requires-python = ">=3.12"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "rich",
|
|
||||||
# "requests<3",
|
# "requests<3",
|
||||||
|
# "rich",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -4092,8 +4092,8 @@ fn add_script_with_metadata_table_and_shebang() -> Result<()> {
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.12"
|
# requires-python = ">=3.12"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "rich",
|
|
||||||
# "requests<3",
|
# "requests<3",
|
||||||
|
# "rich",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -4143,8 +4143,8 @@ fn add_script_without_metadata_table_with_docstring() -> Result<()> {
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.12"
|
# requires-python = ">=3.12"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "rich",
|
|
||||||
# "requests<3",
|
# "requests<3",
|
||||||
|
# "rich",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
"""This is a script."""
|
"""This is a script."""
|
||||||
|
|
@ -4410,3 +4410,115 @@ fn fail_to_add_revert_project() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure that the added dependencies are sorted
|
||||||
|
/// if the dependency list was already sorted prior to adding the new one.
|
||||||
|
#[test]
|
||||||
|
fn sorted_dependencies() -> 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"
|
||||||
|
description = "Add your description here"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"CacheControl[filecache]>=0.14,<0.15",
|
||||||
|
"mwparserfromhell",
|
||||||
|
"pywikibot",
|
||||||
|
"sentry-sdk",
|
||||||
|
"yarl",
|
||||||
|
]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&["pydantic"]).arg("--frozen"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r###"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"CacheControl[filecache]>=0.14,<0.15",
|
||||||
|
"mwparserfromhell",
|
||||||
|
"pydantic",
|
||||||
|
"pywikibot",
|
||||||
|
"sentry-sdk",
|
||||||
|
"yarl",
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that the custom ordering of the dependencies is preserved
|
||||||
|
/// after adding a package.
|
||||||
|
#[test]
|
||||||
|
fn custom_dependencies() -> 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"
|
||||||
|
description = "Add your description here"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"yarl",
|
||||||
|
"CacheControl[filecache]>=0.14,<0.15",
|
||||||
|
"mwparserfromhell",
|
||||||
|
"pywikibot",
|
||||||
|
"sentry-sdk",
|
||||||
|
]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&["pydantic"]).arg("--frozen"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(
|
||||||
|
pyproject_toml, @r###"
|
||||||
|
[project]
|
||||||
|
name = "project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"yarl",
|
||||||
|
"CacheControl[filecache]>=0.14,<0.15",
|
||||||
|
"mwparserfromhell",
|
||||||
|
"pywikibot",
|
||||||
|
"sentry-sdk",
|
||||||
|
"pydantic",
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue