diff --git a/Cargo.lock b/Cargo.lock index e5e4c63..57f826c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,8 +69,6 @@ name = "common" version = "1.0.0" dependencies = [ "indoc", - "lexical-sort", - "pep440_rs", "pep508_rs", "rstest", "taplo", @@ -91,17 +89,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "either" version = "1.13.0" @@ -185,7 +172,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -324,9 +311,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "log" @@ -386,22 +373,21 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pep440_rs" -version = "0.6.6" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "466eada3179c2e069ca897b99006cbb33f816290eaeec62464eea907e22ae385" +checksum = "7c8ee724d21f351f9d47276614ac9710975db827ba9fe2ca5a517ba648193307" dependencies = [ - "once_cell", + "serde", "unicode-width", "unscanny", ] [[package]] name = "pep508_rs" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8877489a99ccc80012333123e434f84e645fe1ede3b30e9d3b815887a12979" +checksum = "e30eadafcb06bf6c81392fa6fb2e8f7961e75a89856854d732604bce9313dc73" dependencies = [ - "derivative", "once_cell", "pep440_rs", "regex", @@ -419,9 +405,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -452,9 +438,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -506,7 +492,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -519,7 +505,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -528,6 +514,7 @@ version = "2.4.3" dependencies = [ "common", "indoc", + "lexical-sort", "pyo3", "regex", "rstest", @@ -544,9 +531,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -621,7 +608,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.79", + "syn 2.0.85", "unicode-ident", ] @@ -654,29 +641,29 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -706,9 +693,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -750,22 +737,22 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -850,7 +837,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -885,9 +872,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unindent" @@ -956,5 +943,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] diff --git a/common/Cargo.toml b/common/Cargo.toml index 1ac34de..5ac37c0 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -9,9 +9,7 @@ edition = "2021" [dependencies] taplo = { version = "0.13.2" } # formatter -pep508_rs = { version = "0.6.1" } -pep440_rs = { version = "0.6.5" } # align up with pep508_rs for now https://github.com/konstin/pep508_rs/issues/19 -lexical-sort = { version = "0.3.1" } +pep508_rs = { version = "0.8.1" } [dev-dependencies] rstest = { version = "0.23.0" } # parametrized tests diff --git a/common/src/array.rs b/common/src/array.rs index 8b6f3ad..acfa502 100644 --- a/common/src/array.rs +++ b/common/src/array.rs @@ -1,143 +1,152 @@ use std::cell::RefCell; +use std::cmp::Ordering; use std::collections::HashMap; - -use lexical_sort::{natural_lexical_cmp, StringSort}; +use std::hash::Hash; use taplo::syntax::SyntaxKind::{ARRAY, COMMA, NEWLINE, STRING, VALUE, WHITESPACE}; use taplo::syntax::{SyntaxElement, SyntaxKind, SyntaxNode}; use crate::create::{make_comma, make_newline}; use crate::string::{load_text, update_content}; +use crate::util::{find_first, iter}; pub fn transform(node: &SyntaxNode, transform: &F) where F: Fn(&str) -> String, { - for array in node.children_with_tokens() { - if array.kind() == ARRAY { - for array_entry in array.as_node().unwrap().children_with_tokens() { - if array_entry.kind() == VALUE { - update_content(array_entry.as_node().unwrap(), transform); - } - } - } - } + iter(node, [ARRAY, VALUE].as_ref(), &|array_entry| { + update_content(array_entry, transform); + }); } #[allow(clippy::range_plus_one, clippy::too_many_lines)] -pub fn sort(node: &SyntaxNode, transform: F) +pub fn sort(node: &SyntaxNode, to_key: K, cmp: &C) where - F: Fn(&str) -> String, + K: Fn(&SyntaxNode) -> Option, + C: Fn(&T, &T) -> Ordering, + T: Clone + Eq + Hash, { - for array in node.children_with_tokens() { - if array.kind() == ARRAY { - let array_node = array.as_node().unwrap(); - let has_trailing_comma = array_node - .children_with_tokens() - .map(|x| x.kind()) - .filter(|x| *x == COMMA || *x == VALUE) - .last() - == Some(COMMA); - let multiline = array_node.children_with_tokens().any(|e| e.kind() == NEWLINE); - let mut value_set = Vec::>::new(); - let entry_set = RefCell::new(Vec::::new()); - let mut key_to_pos = HashMap::::new(); + iter(node, [ARRAY].as_ref(), &|array| { + let has_trailing_comma = array + .children_with_tokens() + .map(|x| x.kind()) + .filter(|x| *x == COMMA || *x == VALUE) + .last() + == Some(COMMA); + let multiline = array.children_with_tokens().any(|e| e.kind() == NEWLINE); - let mut add_to_value_set = |entry: String| { - let mut entry_set_borrow = entry_set.borrow_mut(); - if !entry_set_borrow.is_empty() { - key_to_pos.insert(entry, value_set.len()); - value_set.push(entry_set_borrow.clone()); - entry_set_borrow.clear(); - } - }; - let mut entries = Vec::::new(); - let mut has_value = false; - let mut previous_is_bracket_open = false; - let mut entry_value = String::new(); - let mut count = 0; + let mut entries = Vec::::new(); + let mut order_sets = Vec::>::new(); + let mut key_to_order_set = HashMap::::new(); + let current_set = RefCell::new(Vec::::new()); + let mut current_set_value: Option = None; + let mut previous_is_bracket_open = false; + + let mut add_to_order_sets = |entry: T| { + let mut entry_set_borrow = current_set.borrow_mut(); + if !entry_set_borrow.is_empty() { + key_to_order_set.insert(entry, order_sets.len()); + order_sets.push(entry_set_borrow.clone()); + entry_set_borrow.clear(); + } + }; + + let mut count = 0; - for entry in array_node.children_with_tokens() { - count += 1; - if previous_is_bracket_open { - // make sure ends with trailing comma - if entry.kind() == NEWLINE || entry.kind() == WHITESPACE { - continue; + // collect elements to order into to_order_sets, the rest goes into entries + for entry in array.children_with_tokens() { + count += 1; + if previous_is_bracket_open { + // make sure ends with trailing comma + if entry.kind() == NEWLINE || entry.kind() == WHITESPACE { + continue; + } + previous_is_bracket_open = false; + } + match &entry.kind() { + SyntaxKind::BRACKET_START => { + entries.push(entry); + if multiline { + entries.push(make_newline()); } - previous_is_bracket_open = false; + previous_is_bracket_open = true; } - match &entry.kind() { - SyntaxKind::BRACKET_START => { - entries.push(entry); - if multiline { - entries.push(make_newline()); + SyntaxKind::BRACKET_END => { + match current_set_value.take() { + None => { + entries.extend(current_set.borrow_mut().clone()); } - previous_is_bracket_open = true; - } - SyntaxKind::BRACKET_END => { - if has_value { - add_to_value_set(entry_value.clone()); - } else { - entries.extend(entry_set.borrow_mut().clone()); + Some(val) => { + add_to_order_sets(val); } - entries.push(entry); } - VALUE => { - if has_value { + entries.push(entry); + } + VALUE => { + match current_set_value.take() { + None => {} + Some(val) => { if multiline { - entry_set.borrow_mut().push(make_newline()); - } - add_to_value_set(entry_value.clone()); - } - has_value = true; - let value_node = entry.as_node().unwrap(); - let mut found_string = false; - for child in value_node.children_with_tokens() { - let kind = child.kind(); - if kind == STRING { - entry_value = transform(load_text(child.as_token().unwrap().text(), STRING).as_str()); - found_string = true; - break; + current_set.borrow_mut().push(make_newline()); } + add_to_order_sets(val); } - if !found_string { - // abort if not correct types - return; - } - entry_set.borrow_mut().push(entry); - entry_set.borrow_mut().push(make_comma()); } - NEWLINE => { - entry_set.borrow_mut().push(entry); - if has_value { - add_to_value_set(entry_value.clone()); - has_value = false; - } - } - COMMA => {} - _ => { - entry_set.borrow_mut().push(entry); + let value_node = entry.as_node().unwrap(); + current_set_value = to_key(value_node); + + current_set.borrow_mut().push(entry); + current_set.borrow_mut().push(make_comma()); + } + NEWLINE => { + current_set.borrow_mut().push(entry); + if current_set_value.is_some() { + add_to_order_sets(current_set_value.unwrap()); + current_set_value = None; } } + COMMA => {} + _ => { + current_set.borrow_mut().push(entry); + } } + } - let mut order: Vec = key_to_pos.clone().into_keys().collect(); - order.string_sort_unstable(natural_lexical_cmp); - let end = entries.split_off(if multiline { 2 } else { 1 }); - for key in order { - entries.extend(value_set[key_to_pos[&key]].clone()); - } - entries.extend(end); - array_node.splice_children(0..count, entries); - if !has_trailing_comma { - if let Some((i, _)) = array_node - .children_with_tokens() - .enumerate() - .filter(|(_, x)| x.kind() == COMMA) - .last() - { - array_node.splice_children(i..i + 1, vec![]); - } + let trailing_content = entries.split_off(if multiline { 2 } else { 1 }); + let mut order: Vec = key_to_order_set.keys().cloned().collect(); + order.sort_by(&cmp); + for key in order { + entries.extend(order_sets[key_to_order_set[&key]].clone()); + } + entries.extend(trailing_content); + array.splice_children(0..count, entries); + + if !has_trailing_comma { + if let Some((i, _)) = array + .children_with_tokens() + .enumerate() + .filter(|(_, x)| x.kind() == COMMA) + .last() + { + array.splice_children(i..i + 1, vec![]); } } - } + }); +} + +#[allow(clippy::range_plus_one, clippy::too_many_lines)] +pub fn sort_strings(node: &SyntaxNode, to_key: K, cmp: &C) +where + K: Fn(String) -> String, + C: Fn(&String, &String) -> Ordering, + T: Clone + Eq + Hash, +{ + sort( + node, + |e| -> Option { + find_first(e, &[STRING], &|s| -> String { + to_key(load_text(s.as_token().unwrap().text(), STRING)) + }) + }, + cmp, + ); } diff --git a/common/src/lib.rs b/common/src/lib.rs index abb7c00..ed68a4a 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,8 +3,7 @@ pub mod create; pub mod pep508; pub mod string; pub mod table; - +pub mod util; pub use taplo; - #[cfg(test)] mod tests; diff --git a/common/src/pep508.rs b/common/src/pep508.rs index 633cc56..203cfb9 100644 --- a/common/src/pep508.rs +++ b/common/src/pep508.rs @@ -1,6 +1,7 @@ use std::fmt::Write; use std::str::FromStr; +use pep508_rs::pep440_rs::Operator::TildeEqual; use pep508_rs::{MarkerTree, Requirement, VersionOrUrl}; pub fn format_requirement(value: &str, keep_full_version: bool) -> String { @@ -23,7 +24,7 @@ pub fn format_requirement(value: &str, keep_full_version: bool) -> String { let extra_count = v.len() - 1; for (at, spec) in v.iter().enumerate() { let mut spec_repr = format!("{spec}"); - if !keep_full_version && spec.operator() != &pep440_rs::Operator::TildeEqual { + if !keep_full_version && spec.operator() != &TildeEqual { loop { let propose = spec_repr.strip_suffix(".0"); if propose.is_none() { diff --git a/common/src/table.rs b/common/src/table.rs index cb83532..f526e85 100644 --- a/common/src/table.rs +++ b/common/src/table.rs @@ -256,6 +256,22 @@ where } } +pub fn find_key(table: &SyntaxNode, key: &str) -> Option { + let mut current_key = String::new(); + for table_entry in table.children_with_tokens() { + if table_entry.kind() == ENTRY { + for entry in table_entry.as_node().unwrap().children_with_tokens() { + if entry.kind() == KEY { + current_key = entry.as_node().unwrap().text().to_string().trim().to_string(); + } else if entry.kind() == VALUE && current_key == key { + return Some(entry.as_node().unwrap().clone()); + } + } + } + } + None +} + pub fn collapse_sub_tables(tables: &mut Tables, name: &str) { let h2p = tables.header_to_pos.clone(); let sub_name_prefix = format!("{name}."); diff --git a/common/src/tests/array_tests.rs b/common/src/tests/array_tests.rs index 4439e50..cd85683 100644 --- a/common/src/tests/array_tests.rs +++ b/common/src/tests/array_tests.rs @@ -4,7 +4,7 @@ use taplo::formatter::{format_syntax, Options}; use taplo::parser::parse; use taplo::syntax::SyntaxKind::{ENTRY, VALUE}; -use crate::array::{sort, transform}; +use crate::array::{sort_strings, transform}; use crate::pep508::format_requirement; #[rstest] @@ -148,7 +148,9 @@ fn test_order_array(#[case] start: &str, #[case] expected: &str) { if children.kind() == ENTRY { for entry in children.as_node().unwrap().children_with_tokens() { if entry.kind() == VALUE { - sort(entry.as_node().unwrap(), str::to_lowercase); + sort_strings::(entry.as_node().unwrap(), |s| s.to_lowercase(), &|lhs, rhs| { + lhs.cmp(rhs) + }); } } } @@ -172,7 +174,9 @@ fn test_reorder_no_trailing_comma(#[case] start: &str, #[case] expected: &str) { if children.kind() == ENTRY { for entry in children.as_node().unwrap().children_with_tokens() { if entry.kind() == VALUE { - sort(entry.as_node().unwrap(), str::to_lowercase); + sort_strings::(entry.as_node().unwrap(), |s| s.to_lowercase(), &|lhs, rhs| { + lhs.cmp(rhs) + }); } } } diff --git a/common/src/util.rs b/common/src/util.rs new file mode 100644 index 0000000..f309335 --- /dev/null +++ b/common/src/util.rs @@ -0,0 +1,33 @@ +use taplo::syntax::{SyntaxElement, SyntaxKind, SyntaxNode}; + +pub fn iter(node: &SyntaxNode, paths: &[SyntaxKind], handle: &F) +where + F: Fn(&SyntaxNode), +{ + for entry in node.children_with_tokens() { + if entry.kind() == paths[0] { + let found = entry.as_node().unwrap(); + if paths.len() == 1 { + handle(found); + } else { + iter(found, &paths[1..], handle); + } + } + } +} + +pub fn find_first(node: &SyntaxNode, paths: &[SyntaxKind], extract: &F) -> Option +where + F: Fn(SyntaxElement) -> T, +{ + for entry in node.children_with_tokens() { + if entry.kind() == paths[0] { + if paths.len() == 1 { + return Some(extract(entry)); + } else { + find_first(entry.as_node().unwrap(), &paths[1..], extract); + } + } + } + None +} diff --git a/pyproject-fmt/Cargo.toml b/pyproject-fmt/Cargo.toml index eb171fb..48b1ae4 100644 --- a/pyproject-fmt/Cargo.toml +++ b/pyproject-fmt/Cargo.toml @@ -14,8 +14,9 @@ crate-type = ["cdylib"] [dependencies] common = {path = "../common" } -regex = { version = "1.11.0" } +regex = { version = "1.11.1" } pyo3 = { version = "0.22.5", features = ["abi3-py38"] } # integration with Python +lexical-sort = { version = "0.3.1" } [features] extension-module = ["pyo3/extension-module"] diff --git a/pyproject-fmt/rust/src/build_system.rs b/pyproject-fmt/rust/src/build_system.rs index 74708ec..89d743b 100644 --- a/pyproject-fmt/rust/src/build_system.rs +++ b/pyproject-fmt/rust/src/build_system.rs @@ -1,6 +1,7 @@ -use common::array::{sort, transform}; +use common::array::{sort_strings, transform}; use common::pep508::{format_requirement, get_canonic_requirement_name}; use common::table::{for_entries, reorder_table_keys, Tables}; +use lexical_sort::{lexical_cmp, natural_lexical_cmp}; pub fn fix(tables: &Tables, keep_full_version: bool) { let table_element = tables.get("build-system"); @@ -11,10 +12,14 @@ pub fn fix(tables: &Tables, keep_full_version: bool) { for_entries(table, &mut |key, entry| match key.as_str() { "requires" => { transform(entry, &|s| format_requirement(s, keep_full_version)); - sort(entry, |e| get_canonic_requirement_name(e).to_lowercase()); + sort_strings::( + entry, + |s| get_canonic_requirement_name(s.as_str()).to_lowercase(), + &|lhs, rhs| natural_lexical_cmp(lhs, rhs), + ); } "backend-path" => { - sort(entry, str::to_lowercase); + sort_strings::(entry, |s| s.to_lowercase(), &|lhs, rhs| lexical_cmp(lhs, rhs)); } _ => {} }); diff --git a/pyproject-fmt/rust/src/dependency_groups.rs b/pyproject-fmt/rust/src/dependency_groups.rs new file mode 100644 index 0000000..c757aac --- /dev/null +++ b/pyproject-fmt/rust/src/dependency_groups.rs @@ -0,0 +1,69 @@ +use common::array::{sort, transform}; +use common::pep508::{format_requirement, get_canonic_requirement_name}; +use common::string::{load_text, update_content}; +use common::table::{collapse_sub_tables, find_key, for_entries, reorder_table_keys, Tables}; +use common::taplo::syntax::SyntaxKind::{ARRAY, ENTRY, INLINE_TABLE, STRING, VALUE}; +use common::util::iter; +use lexical_sort::natural_lexical_cmp; +use std::cmp::Ordering; + +pub fn fix(tables: &mut Tables, keep_full_version: bool) { + collapse_sub_tables(tables, "dependency-groups"); + let table_element = tables.get("dependency-groups"); + if table_element.is_none() { + return; + } + + let table = &mut table_element.unwrap().first().unwrap().borrow_mut(); + for_entries(table, &mut |_key, entry| { + // format dependency specifications + transform(entry, &|s| format_requirement(s, keep_full_version)); + + // update inline table values to double-quoted string, e.g. include-group + iter(entry, [ARRAY, VALUE, INLINE_TABLE, ENTRY, VALUE].as_ref(), &|node| { + update_content(node, |s| String::from(s)); + }); + + // sort array elements + sort::<(u8, String, String), _, _>( + entry, + |node| { + for child in node.children_with_tokens() { + match child.kind() { + STRING => { + let val = load_text(child.as_token().unwrap().text(), STRING); + let package_name = get_canonic_requirement_name(val.as_str()).to_lowercase(); + return Some((0, package_name, val)); + } + INLINE_TABLE => { + match find_key(child.as_node().unwrap(), "include-group") { + None => {} + Some(n) => { + return Some(( + 1, + load_text(n.first_token().unwrap().text(), STRING), + String::from(""), + )); + } + }; + } + _ => {} + } + } + None + }, + &|lhs, rhs| { + let mut res = lhs.0.cmp(&rhs.0); + if res == Ordering::Equal { + res = natural_lexical_cmp(lhs.1.as_str(), rhs.1.as_str()); + if res == Ordering::Equal { + res = natural_lexical_cmp(lhs.2.as_str(), rhs.2.as_str()); + } + } + res + }, + ); + }); + + reorder_table_keys(table, &["", "dev", "test", "type", "docs"]); +} diff --git a/pyproject-fmt/rust/src/global.rs b/pyproject-fmt/rust/src/global.rs index 8ffa1e4..e73041c 100644 --- a/pyproject-fmt/rust/src/global.rs +++ b/pyproject-fmt/rust/src/global.rs @@ -10,6 +10,7 @@ pub fn reorder_tables(root_ast: &SyntaxNode, tables: &Tables) { "", "build-system", "project", + "dependency-groups", // Build backends "tool.poetry", "tool.poetry-dynamic-versioning", diff --git a/pyproject-fmt/rust/src/main.rs b/pyproject-fmt/rust/src/main.rs index 6bdd4a9..1871cd3 100644 --- a/pyproject-fmt/rust/src/main.rs +++ b/pyproject-fmt/rust/src/main.rs @@ -9,6 +9,7 @@ use crate::global::reorder_tables; use common::table::Tables; mod build_system; +mod dependency_groups; mod project; mod global; @@ -60,6 +61,7 @@ pub fn format_toml(content: &str, opt: &Settings) -> String { opt.max_supported_python, opt.min_supported_python, ); + dependency_groups::fix(&mut tables, opt.keep_full_version); ruff::fix(&mut tables); reorder_tables(&root_ast, &tables); diff --git a/pyproject-fmt/rust/src/project.rs b/pyproject-fmt/rust/src/project.rs index 0a27aba..3130617 100644 --- a/pyproject-fmt/rust/src/project.rs +++ b/pyproject-fmt/rust/src/project.rs @@ -1,18 +1,18 @@ -use std::cell::RefMut; - +use common::array::{sort, sort_strings, transform}; +use common::create::{make_array, make_array_entry, make_comma, make_entry_of_string, make_newline}; +use common::pep508::{format_requirement, get_canonic_requirement_name}; +use common::string::{load_text, update_content}; +use common::table::{collapse_sub_tables, for_entries, reorder_table_keys, Tables}; use common::taplo::syntax::SyntaxKind::{ ARRAY, BRACKET_END, BRACKET_START, COMMA, ENTRY, IDENT, INLINE_TABLE, KEY, NEWLINE, STRING, VALUE, }; use common::taplo::syntax::{SyntaxElement, SyntaxNode}; use common::taplo::util::StrExt; use common::taplo::HashSet; +use lexical_sort::natural_lexical_cmp; use regex::Regex; - -use common::array::{sort, transform}; -use common::create::{make_array, make_array_entry, make_comma, make_entry_of_string, make_newline}; -use common::pep508::{format_requirement, get_canonic_requirement_name}; -use common::string::{load_text, update_content}; -use common::table::{collapse_sub_tables, for_entries, reorder_table_keys, Tables}; +use std::cell::RefMut; +use std::cmp::Ordering; pub fn fix( tables: &mut Tables, @@ -55,17 +55,34 @@ pub fn fix( } "dependencies" | "optional-dependencies" => { transform(entry, &|s| format_requirement(s, keep_full_version)); - sort(entry, |e| { - get_canonic_requirement_name(e).to_lowercase() + " " + &format_requirement(e, keep_full_version) - }); + sort::<(String, String), _, _>( + entry, + |node| { + for child in node.children_with_tokens() { + if let STRING = child.kind() { + let val = load_text(child.as_token().unwrap().text(), STRING); + let package_name = get_canonic_requirement_name(val.as_str()).to_lowercase(); + return Some((package_name, val)); + } + } + None + }, + &|lhs, rhs| { + let mut res = natural_lexical_cmp(lhs.0.as_str(), rhs.0.as_str()); + if res == Ordering::Equal { + res = natural_lexical_cmp(lhs.1.as_str(), rhs.1.as_str()); + } + res + }, + ); } "dynamic" | "keywords" => { transform(entry, &|s| String::from(s)); - sort(entry, str::to_lowercase); + sort_strings::(entry, |s| s.to_lowercase(), &|lhs, rhs| natural_lexical_cmp(lhs, rhs)); } "classifiers" => { transform(entry, &|s| String::from(s)); - sort(entry, str::to_lowercase); + sort_strings::(entry, |s| s.to_lowercase(), &|lhs, rhs| natural_lexical_cmp(lhs, rhs)); } _ => {} }); @@ -73,7 +90,7 @@ pub fn fix( generate_classifiers(table, max_supported_python, min_supported_python); for_entries(table, &mut |key, entry| { if key.as_str() == "classifiers" { - sort(entry, str::to_lowercase); + sort_strings::(entry, |s| s.to_lowercase(), &|lhs, rhs| natural_lexical_cmp(lhs, rhs)); } }); reorder_table_keys( diff --git a/pyproject-fmt/rust/src/ruff.rs b/pyproject-fmt/rust/src/ruff.rs index d4da1a5..8f35c8d 100644 --- a/pyproject-fmt/rust/src/ruff.rs +++ b/pyproject-fmt/rust/src/ruff.rs @@ -1,6 +1,7 @@ -use common::array::{sort, transform}; +use common::array::{sort_strings, transform}; use common::string::update_content; use common::table::{collapse_sub_tables, for_entries, reorder_table_keys, Tables}; +use lexical_sort::natural_lexical_cmp; #[allow(clippy::too_many_lines)] pub fn fix(tables: &mut Tables) { @@ -92,7 +93,7 @@ pub fn fix(tables: &mut Tables) { | "lint.pylint.allow-dunder-method-names" | "lint.pylint.allow-magic-value-types" => { transform(entry, &|s| String::from(s)); - sort(entry, str::to_lowercase); + sort_strings::(entry, |s| s.to_lowercase(), &|lhs, rhs| natural_lexical_cmp(lhs, rhs)); } "lint.isort.section-order" => { transform(entry, &|s| String::from(s)); @@ -100,7 +101,7 @@ pub fn fix(tables: &mut Tables) { _ => { if key.starts_with("lint.extend-per-file-ignores.") || key.starts_with("lint.per-file-ignores.") { transform(entry, &|s| String::from(s)); - sort(entry, str::to_lowercase); + sort_strings::(entry, |s| s.to_lowercase(), &|lhs, rhs| natural_lexical_cmp(lhs, rhs)); } } }); diff --git a/pyproject-fmt/rust/src/tests/dependency_groups_tests.rs b/pyproject-fmt/rust/src/tests/dependency_groups_tests.rs new file mode 100644 index 0000000..3bb7c46 --- /dev/null +++ b/pyproject-fmt/rust/src/tests/dependency_groups_tests.rs @@ -0,0 +1,143 @@ +use common::taplo::formatter::{format_syntax, Options}; +use common::taplo::parser::parse; +use common::taplo::syntax::SyntaxElement; +use indoc::indoc; +use rstest::rstest; + +use crate::dependency_groups::fix; +use common::table::Tables; + +fn evaluate(start: &str, keep_full_version: bool) -> String { + let root_ast = parse(start).into_syntax().clone_for_update(); + let count = root_ast.children_with_tokens().count(); + let mut tables = Tables::from_ast(&root_ast); + fix(&mut tables, keep_full_version); + let entries = tables + .table_set + .iter() + .flat_map(|e| e.borrow().clone()) + .collect::>(); + root_ast.splice_children(0..count, entries); + let opt = Options { + column_width: 1, + ..Options::default() + }; + format_syntax(root_ast, opt) +} + +#[rstest] +#[case::no_groups( + indoc ! {r""}, + "\n", + false, +)] +#[case::single_group_single_dep( + indoc ! {r#" + [dependency-groups] + test=["a>1.0.0"] + "#}, + indoc ! {r#" + [dependency-groups] + test = [ + "a>1", + ] + "#}, + false, +)] +#[case::single_group_single_dep_full_version( + indoc ! {r#" + [dependency-groups] + test=["a>1.0.0"] + "#}, + indoc ! {r#" + [dependency-groups] + test = [ + "a>1.0.0", + ] + "#}, + true, +)] +#[case::single_group_multiple_deps( + indoc ! {r#" + [dependency-groups] + test=["b==2.0.*", "a>1"] + "#}, + indoc ! {r#" + [dependency-groups] + test = [ + "a>1", + "b==2.0.*", + ] + "#}, + false, +)] +#[case::multiple_groups( + indoc ! {r#" + [dependency-groups] + example=["c<1"] + docs=["b==1"] + test=["a>1"] + dev=["d>=2"] + "#}, + indoc ! {r#" + [dependency-groups] + dev = [ + "d>=2", + ] + test = [ + "a>1", + ] + docs = [ + "b==1", + ] + example = [ + "c<1", + ] + "#}, + false, +)] +#[case::include_single_group( + indoc ! {r#" + [dependency-groups] + docs=["b==1"] + test=["a>1",{include-group="docs"}] + "#}, + indoc ! {r#" + [dependency-groups] + test = [ + "a>1", + { include-group = "docs" }, + ] + docs = [ + "b==1", + ] + "#}, + false, +)] +#[case::include_many_groups( + indoc ! {r#" + [dependency-groups] + all=['c<1', {include-group='test'}, {include-group='docs'}, 'd>1'] + docs = ['b==1'] + test = ['a>1'] + "#}, + indoc ! {r#" + [dependency-groups] + test = [ + "a>1", + ] + docs = [ + "b==1", + ] + all = [ + "c<1", + "d>1", + { include-group = "docs" }, + { include-group = "test" }, + ] + "#}, + false, +)] +fn test_format_dependency_groups(#[case] start: &str, #[case] expected: &str, #[case] keep_full_version: bool) { + assert_eq!(evaluate(start, keep_full_version), expected); +} diff --git a/pyproject-fmt/rust/src/tests/global_tests.rs b/pyproject-fmt/rust/src/tests/global_tests.rs index f9a84ca..4a8ee6c 100644 --- a/pyproject-fmt/rust/src/tests/global_tests.rs +++ b/pyproject-fmt/rust/src/tests/global_tests.rs @@ -17,6 +17,9 @@ use common::table::Tables; [build-system] build-backend="backend" requires=["c", "d"] + [dependency-groups] + docs=["s"] + test=["p", "q"] [tool.mypy] mk="mv" [tool.ruff.test] @@ -57,6 +60,15 @@ use common::table::Tables; "e", ] + [dependency-groups] + docs = [ + "s", + ] + test = [ + "p", + "q", + ] + [tool.ruff] mr = "vr" [tool.ruff.test] diff --git a/pyproject-fmt/rust/src/tests/main_tests.rs b/pyproject-fmt/rust/src/tests/main_tests.rs index 4f8b82c..5e89b55 100644 --- a/pyproject-fmt/rust/src/tests/main_tests.rs +++ b/pyproject-fmt/rust/src/tests/main_tests.rs @@ -17,6 +17,8 @@ use crate::{format_toml, Settings}; [build-system] build-backend="backend" requires=[" c >= 1.5.0", "d == 2.0.0"] + [dependency-groups] + test=["p>1.0.0"] [tool.mypy] mk="mv" "#}, @@ -45,6 +47,11 @@ use crate::{format_toml, Settings}; "e>=1.5", ] + [dependency-groups] + test = [ + "p>1", + ] + [tool.mypy] mk = "mv" "#}, diff --git a/pyproject-fmt/rust/src/tests/mod.rs b/pyproject-fmt/rust/src/tests/mod.rs index 11e29e4..55a18bc 100644 --- a/pyproject-fmt/rust/src/tests/mod.rs +++ b/pyproject-fmt/rust/src/tests/mod.rs @@ -1,4 +1,5 @@ mod build_systems_tests; +mod dependency_groups_tests; mod global_tests; mod main_tests; mod project_tests;