mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 04:48:18 +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",
|
||||
"glob",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"pypi-types",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ toml = { workspace = true }
|
|||
toml_edit = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
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::str::FromStr;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use thiserror::Error;
|
||||
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 crate::pyproject::{DependencyType, Source};
|
||||
|
|
@ -522,13 +521,53 @@ pub fn add_dependency(
|
|||
deps: &mut Array,
|
||||
has_source: bool,
|
||||
) -> Result<ArrayEdit, Error> {
|
||||
// Find matching dependencies.
|
||||
let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps);
|
||||
|
||||
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);
|
||||
Ok(ArrayEdit::Add(deps.len() - 1))
|
||||
|
||||
Ok(ArrayEdit::Add(index))
|
||||
}
|
||||
[_] => {
|
||||
let (i, mut old_req) = to_replace.remove(0);
|
||||
|
|
|
|||
|
|
@ -2310,8 +2310,8 @@ fn add_update_marker() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"requests>=2.30; python_version >= '3.11'",
|
||||
"requests>=2.0,<2.29 ; python_full_version < '3.11'",
|
||||
"requests>=2.30; python_version >= '3.11'",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
@ -2348,8 +2348,8 @@ fn add_update_marker() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"requests>=2.30; python_version >= '3.11'",
|
||||
"requests>=2.0,<2.20 ; python_full_version < '3.11'",
|
||||
"requests>=2.30; python_version >= '3.11'",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
@ -2390,8 +2390,8 @@ fn add_update_marker() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"requests>=2.30; python_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'",
|
||||
]
|
||||
|
||||
|
|
@ -2430,10 +2430,10 @@ fn add_update_marker() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"requests>=2.30; python_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.30; python_version >= '3.11'",
|
||||
"requests>=2.31 ; python_full_version >= '3.12' and sys_platform == 'win32'",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
@ -3848,8 +3848,8 @@ fn add_requirements_file() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"flask==2.3.2",
|
||||
"anyio",
|
||||
"flask==2.3.2",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
@ -3932,9 +3932,9 @@ fn add_script() -> Result<()> {
|
|||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# "requests<3",
|
||||
# "rich",
|
||||
# "anyio",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
|
|
@ -3984,8 +3984,8 @@ fn add_script_without_metadata_table() -> Result<()> {
|
|||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "rich",
|
||||
# "requests<3",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
import requests
|
||||
|
|
@ -4036,8 +4036,8 @@ fn add_script_without_metadata_table_with_shebang() -> Result<()> {
|
|||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "rich",
|
||||
# "requests<3",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
import requests
|
||||
|
|
@ -4092,8 +4092,8 @@ fn add_script_with_metadata_table_and_shebang() -> Result<()> {
|
|||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "rich",
|
||||
# "requests<3",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
import requests
|
||||
|
|
@ -4143,8 +4143,8 @@ fn add_script_without_metadata_table_with_docstring() -> Result<()> {
|
|||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "rich",
|
||||
# "requests<3",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
"""This is a script."""
|
||||
|
|
@ -4410,3 +4410,115 @@ fn fail_to_add_revert_project() -> Result<()> {
|
|||
|
||||
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