mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
use requires-python of tool.uv.dependency-groups
This commit is contained in:
parent
eee3540558
commit
0a037894c8
5 changed files with 331 additions and 147 deletions
|
@ -1,11 +1,11 @@
|
|||
use std::collections::Bound;
|
||||
|
||||
use std::str::FromStr;
|
||||
use version_ranges::Ranges;
|
||||
|
||||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_pep440::{
|
||||
LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
|
||||
release_specifiers_to_ranges,
|
||||
VersionSpecifiersParseError, release_specifiers_to_ranges,
|
||||
};
|
||||
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||
use uv_platform_tags::{AbiTag, LanguageTag};
|
||||
|
@ -502,6 +502,19 @@ impl RequiresPython {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove trailing zeroes from all specifiers
|
||||
pub fn remove_zeroes(&self) -> String {
|
||||
self.specifiers().remove_zeroes().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RequiresPython {
|
||||
type Err = VersionSpecifiersParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
VersionSpecifiers::from_str(s).map(|v| Self::from_specifiers(&v))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequiresPython {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use std::cmp::min;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::num::NonZero;
|
||||
use std::ops::Deref;
|
||||
|
@ -771,7 +772,8 @@ impl Version {
|
|||
&& (operator == Operator::GreaterThan
|
||||
|| operator == Operator::GreaterThanEqual
|
||||
|| operator == Operator::Equal
|
||||
|| operator == Operator::EqualStar);
|
||||
|| operator == Operator::EqualStar)
|
||||
|| ordering == Ordering::Equal && operator == Operator::GreaterThan;
|
||||
|
||||
let new = &*latest.release();
|
||||
let old = &*self.release();
|
||||
|
@ -794,7 +796,7 @@ impl Version {
|
|||
let addsub = |v: u64| if subtract { v - delta } else { v + delta };
|
||||
if downgrade || orle {
|
||||
let minor = oplt || ople && zero;
|
||||
let upgrade = match old.len() {
|
||||
let upgrade = match min(old.len(), new.len()) {
|
||||
// <1 to <=1.0.0 : <2
|
||||
1 => Version::new([addsub(new[0])]),
|
||||
// <1.2 to <=1.2.3 : <1.3
|
||||
|
@ -814,6 +816,21 @@ impl Version {
|
|||
}
|
||||
(false, false)
|
||||
}
|
||||
|
||||
/// Remove trailing zeroes from the release
|
||||
#[must_use]
|
||||
pub fn remove_zeroes(&self) -> Self {
|
||||
let mut r = vec![];
|
||||
let mut found = false;
|
||||
for d in self.release().to_vec().iter().rev() {
|
||||
if !found && *d == 0 {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
r.push(*d);
|
||||
}
|
||||
Self::new(r.iter().rev())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Version {
|
||||
|
@ -2646,14 +2663,15 @@ pub struct VersionDigit {
|
|||
|
||||
impl VersionDigit {
|
||||
/// Increase a digit
|
||||
pub fn add(&mut self, digit: usize) {
|
||||
pub fn add(&mut self, digit: usize) -> bool {
|
||||
match digit {
|
||||
1 => self.major += 1,
|
||||
2 => self.minor += 1,
|
||||
3 => self.patch += 1,
|
||||
4 => self.build += 1,
|
||||
_ => {}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Increase all digits from `other`
|
||||
|
|
|
@ -136,7 +136,7 @@ impl VersionSpecifiers {
|
|||
latest: &Version,
|
||||
allow: &[usize],
|
||||
skipped: &mut VersionDigit,
|
||||
) -> (bool, bool, Option<usize>) {
|
||||
) -> (bool, bool, Option<usize>, bool) {
|
||||
match self.last_mut().filter(|last| !last.contains(latest)) {
|
||||
Some(last) => {
|
||||
let last_copy = last.clone();
|
||||
|
@ -153,19 +153,33 @@ impl VersionSpecifiers {
|
|||
let mut semver = bumped
|
||||
.then(|| old.semver_change(&new, last_new.operator))
|
||||
.flatten();
|
||||
let mut skip = false;
|
||||
if matches!(semver, Some(i) if i > 0 && allow.contains(&i)) {
|
||||
*last = last_new;
|
||||
} else if bumped {
|
||||
bumped = false;
|
||||
upgraded = false;
|
||||
skipped.add(semver.unwrap_or(0)); // Skip forbidden digits
|
||||
skip = skipped.add(semver.unwrap_or(0)); // Skip forbidden digits
|
||||
semver = None;
|
||||
}
|
||||
(bumped, upgraded, semver)
|
||||
(bumped, upgraded, semver, skip)
|
||||
}
|
||||
_ => (false, false, None),
|
||||
_ => (false, false, None, false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove trailing zeroes from all specifiers
|
||||
#[must_use]
|
||||
pub fn remove_zeroes(&self) -> Self {
|
||||
Self::from_unsorted(
|
||||
self.iter()
|
||||
.map(|v| {
|
||||
VersionSpecifier::from_version(*v.operator(), v.version().remove_zeroes())
|
||||
.unwrap()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<VersionSpecifier> for VersionSpecifiers {
|
||||
|
@ -2102,6 +2116,8 @@ Failed to parse version: Unexpected end of version specifier, expected operator.
|
|||
(0, "1.2.3", "==1.2.3", "", 0),
|
||||
(0, "1.2.3.4", "==1.2.3.*", "", 0),
|
||||
(0, "1.2.3.4", "==1.2.3.4", "", 0),
|
||||
(2, "0.1", "<0.1.0", "<0.2", 1), // irregular versions (i.e. pydantic 0.1)
|
||||
(2, "0.1", ">0.1", ">=0.1", 2), // irregular versions (i.e. pydantic 0.1)
|
||||
];
|
||||
for (i, tuple) in success.iter().enumerate() {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
|
@ -2113,7 +2129,7 @@ Failed to parse version: Unexpected end of version specifier, expected operator.
|
|||
let new = VersionSpecifiers::from_str(new).unwrap();
|
||||
let latest = Version::from_str(latest).unwrap();
|
||||
let mut skipped = VersionDigit::default();
|
||||
let (bumped, upgraded, semver_change) =
|
||||
let (bumped, upgraded, semver_change, _skip) =
|
||||
modified.bump_last(&latest, &[1, 2, 3, 4], &mut skipped);
|
||||
let should_bump = !new.to_string().is_empty();
|
||||
let not = if should_bump { "not " } else { "" };
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
use itertools::Itertools;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, iter, mem};
|
||||
use thiserror::Error;
|
||||
use toml_edit::{
|
||||
Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value,
|
||||
Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TableLike, TomlError,
|
||||
Value,
|
||||
};
|
||||
use uv_cache_key::CanonicalUrl;
|
||||
use uv_distribution_types::Index;
|
||||
use uv_distribution_types::{Index, RequiresPython};
|
||||
use uv_fs::PortablePath;
|
||||
use uv_normalize::GroupName;
|
||||
use uv_pep440::{Version, VersionDigit, VersionParseError, VersionSpecifier, VersionSpecifiers};
|
||||
|
@ -267,8 +269,30 @@ type UpgradeResult = (
|
|||
bool,
|
||||
DependencyType,
|
||||
Option<usize>,
|
||||
Option<RequiresPython>,
|
||||
);
|
||||
|
||||
type VersionedPackages = FxHashMap<Option<RequiresPython>, FxHashSet<PackageName>>;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct PackageVersions {
|
||||
versions: HashMap<PackageName, HashMap<Option<RequiresPython>, Version>>,
|
||||
}
|
||||
|
||||
impl PackageVersions {
|
||||
pub fn insert(&mut self, name: PackageName, version: Version, python: Option<RequiresPython>) {
|
||||
self.versions
|
||||
.entry(name)
|
||||
.or_default()
|
||||
.insert(python, version);
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
fn find(&self, name: &PackageName, python: &Option<RequiresPython>) -> Option<&Version> {
|
||||
self.versions.get(name).and_then(|v| v.get(python))
|
||||
}
|
||||
}
|
||||
|
||||
impl PyProjectTomlMut {
|
||||
/// Initialize a [`PyProjectTomlMut`] from a [`str`].
|
||||
pub fn from_toml(raw: &str, target: DependencyTarget) -> Result<Self, Error> {
|
||||
|
@ -1123,17 +1147,17 @@ impl PyProjectTomlMut {
|
|||
types
|
||||
}
|
||||
|
||||
/// Returns package names of all dependencies with version constraints.
|
||||
/// Returns package names of all dependencies with version constraints, including requires-python.
|
||||
///
|
||||
/// This method searches `project.dependencies`, `project.optional-dependencies`,
|
||||
/// `dependency-groups` and `tool.uv.dev-dependencies`.
|
||||
pub fn find_versioned_dependencies(&self) -> FxHashSet<PackageName> {
|
||||
let mut versioned_dependencies = FxHashSet::default();
|
||||
pub fn find_versioned_dependencies(&self) -> VersionedPackages {
|
||||
let mut versioned_dependencies = FxHashMap::default();
|
||||
|
||||
if let Some(project) = self.doc.get("project").and_then(Item::as_table) {
|
||||
// Check `project.dependencies`.
|
||||
if let Some(dependencies) = project.get("dependencies").and_then(Item::as_array) {
|
||||
Self::extract_names_if_versioned(&mut versioned_dependencies, dependencies);
|
||||
Self::extract_names_if_versioned(&mut versioned_dependencies, dependencies, None);
|
||||
}
|
||||
|
||||
// Check `project.optional-dependencies`.
|
||||
|
@ -1148,13 +1172,18 @@ impl PyProjectTomlMut {
|
|||
let Ok(_extra) = ExtraName::from_str(extra) else {
|
||||
continue;
|
||||
};
|
||||
Self::extract_names_if_versioned(&mut versioned_dependencies, dependencies);
|
||||
Self::extract_names_if_versioned(
|
||||
&mut versioned_dependencies,
|
||||
dependencies,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check `dependency-groups`.
|
||||
if let Some(groups) = self.doc.get("dependency-groups").and_then(Item::as_table) {
|
||||
let group_requires_python = self.get_uv_tool_dep_groups_requires_python();
|
||||
for (group, dependencies) in groups {
|
||||
let Ok(_group) = GroupName::from_str(group) else {
|
||||
continue;
|
||||
|
@ -1162,7 +1191,12 @@ impl PyProjectTomlMut {
|
|||
let Some(dependencies) = dependencies.as_array() else {
|
||||
continue;
|
||||
};
|
||||
Self::extract_names_if_versioned(&mut versioned_dependencies, dependencies);
|
||||
let requires_python = group_requires_python.get(&_group.to_string());
|
||||
Self::extract_names_if_versioned(
|
||||
&mut versioned_dependencies,
|
||||
dependencies,
|
||||
requires_python,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1176,25 +1210,33 @@ impl PyProjectTomlMut {
|
|||
.and_then(|uv| uv.get("dev-dependencies"))
|
||||
.and_then(Item::as_array)
|
||||
{
|
||||
Self::extract_names_if_versioned(&mut versioned_dependencies, dependencies);
|
||||
Self::extract_names_if_versioned(&mut versioned_dependencies, dependencies, None);
|
||||
}
|
||||
|
||||
versioned_dependencies
|
||||
}
|
||||
|
||||
fn extract_names_if_versioned(types: &mut FxHashSet<PackageName>, dependencies: &Array) {
|
||||
fn extract_names_if_versioned(
|
||||
packages: &mut VersionedPackages,
|
||||
dependencies: &Array,
|
||||
group_requires: Option<&RequiresPython>,
|
||||
) {
|
||||
let mut found = FxHashMap::default();
|
||||
for dep in dependencies {
|
||||
let Some(req) = dep.as_str().and_then(try_parse_requirement) else {
|
||||
continue;
|
||||
};
|
||||
let name = req.name;
|
||||
if types.contains(&name) {
|
||||
// Skip requirements without version constraints
|
||||
if req.version_or_url.is_none() {
|
||||
continue;
|
||||
}
|
||||
// Skip requirements without version constraints
|
||||
if let Some(VersionOrUrl::VersionSpecifier(_)) = req.version_or_url {
|
||||
types.insert(name);
|
||||
}
|
||||
found
|
||||
.entry(group_requires)
|
||||
.or_insert_with(FxHashSet::default)
|
||||
.insert(req.name);
|
||||
}
|
||||
for (requires, names) in found {
|
||||
packages.entry(requires.cloned()).or_default().extend(names);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1204,13 +1246,15 @@ impl PyProjectTomlMut {
|
|||
/// `dependency-groups` and `tool.uv.dev-dependencies`.
|
||||
pub fn upgrade_all_dependencies(
|
||||
&mut self,
|
||||
latest_versions: &FxHashMap<&PackageName, Version>,
|
||||
latest_versions: &PackageVersions,
|
||||
tables: &[DependencyType],
|
||||
allow: &[usize],
|
||||
skipped: &mut VersionDigit,
|
||||
) -> (usize, Vec<UpgradeResult>) {
|
||||
requires_python: &Option<RequiresPython>,
|
||||
) -> (Vec<UpgradeResult>, usize, usize) {
|
||||
let mut all_upgrades = Vec::new();
|
||||
let mut found = 0;
|
||||
let mut all_found = 0;
|
||||
let mut all_skipped = 0;
|
||||
|
||||
// Check `project.dependencies`
|
||||
if let Some(item) = tables
|
||||
|
@ -1223,15 +1267,17 @@ impl PyProjectTomlMut {
|
|||
})
|
||||
.flatten()
|
||||
{
|
||||
found += item.as_array().map_or(0, Array::len);
|
||||
Self::replace_dependencies(
|
||||
let (found, count_skipped) = Self::replace_dependencies(
|
||||
latest_versions,
|
||||
&mut all_upgrades,
|
||||
item,
|
||||
&DependencyType::Production,
|
||||
allow,
|
||||
skipped,
|
||||
requires_python,
|
||||
);
|
||||
all_found += found;
|
||||
all_skipped += count_skipped;
|
||||
}
|
||||
|
||||
// Check `project.optional-dependencies`
|
||||
|
@ -1251,43 +1297,49 @@ impl PyProjectTomlMut {
|
|||
.map(|(key, value)| (ExtraName::from_str(key.get()).ok(), value))
|
||||
{
|
||||
if let Some(_extra) = extra {
|
||||
found += item.as_array().map_or(0, Array::len);
|
||||
Self::replace_dependencies(
|
||||
let (found, count_skipped) = Self::replace_dependencies(
|
||||
latest_versions,
|
||||
&mut all_upgrades,
|
||||
item,
|
||||
&DependencyType::Optional(_extra),
|
||||
allow,
|
||||
skipped,
|
||||
requires_python,
|
||||
);
|
||||
all_found += found;
|
||||
all_skipped += count_skipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check `dependency-groups`.
|
||||
if let Some(groups) = tables
|
||||
.iter()
|
||||
.find(|t| matches!(t, DependencyType::Group(_)))
|
||||
.and_then(|_| {
|
||||
self.doc
|
||||
.get_mut("dependency-groups")
|
||||
.and_then(Item::as_table_like_mut)
|
||||
})
|
||||
{
|
||||
for (group, item) in groups
|
||||
.iter_mut()
|
||||
.map(|(key, value)| (GroupName::from_str(key.get()).ok(), value))
|
||||
{
|
||||
if let Some(_group) = group {
|
||||
found += item.as_array().map_or(0, Array::len);
|
||||
Self::replace_dependencies(
|
||||
latest_versions,
|
||||
&mut all_upgrades,
|
||||
item,
|
||||
&DependencyType::Group(_group),
|
||||
allow,
|
||||
skipped,
|
||||
);
|
||||
if tables.iter().any(|t| matches!(t, DependencyType::Group(_))) {
|
||||
let group_requires_python = self.get_uv_tool_dep_groups_requires_python();
|
||||
let dep_groups = self
|
||||
.doc
|
||||
.get_mut("dependency-groups")
|
||||
.and_then(Item::as_table_like_mut);
|
||||
if let Some(groups) = dep_groups {
|
||||
for (group, item) in groups
|
||||
.iter_mut()
|
||||
.map(|(key, value)| (GroupName::from_str(key.get()).ok(), value))
|
||||
{
|
||||
if let Some(_group) = group {
|
||||
let python = group_requires_python
|
||||
.get(&_group.to_string())
|
||||
.or(requires_python.as_ref());
|
||||
let (found, count_skipped) = Self::replace_dependencies(
|
||||
latest_versions,
|
||||
&mut all_upgrades,
|
||||
item,
|
||||
&DependencyType::Group(_group),
|
||||
allow,
|
||||
skipped,
|
||||
&python.cloned(),
|
||||
);
|
||||
all_found += found;
|
||||
all_skipped += count_skipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1305,60 +1357,104 @@ impl PyProjectTomlMut {
|
|||
})
|
||||
.flatten()
|
||||
{
|
||||
found += item.as_array().map_or(0, Array::len);
|
||||
Self::replace_dependencies(
|
||||
let (found, count_skipped) = Self::replace_dependencies(
|
||||
latest_versions,
|
||||
&mut all_upgrades,
|
||||
item,
|
||||
&DependencyType::Dev,
|
||||
allow,
|
||||
skipped,
|
||||
requires_python,
|
||||
);
|
||||
all_found += found;
|
||||
all_skipped += count_skipped;
|
||||
}
|
||||
|
||||
(found, all_upgrades)
|
||||
(all_upgrades, all_found, all_skipped)
|
||||
}
|
||||
|
||||
fn get_uv_tool_dep_groups_requires_python(&self) -> FxHashMap<String, RequiresPython> {
|
||||
self.doc
|
||||
.get("tool")
|
||||
.and_then(Item::as_table_like)
|
||||
.and_then(|tool| tool.get("uv").and_then(Item::as_table_like))
|
||||
.and_then(|uv| uv.get("dependency-groups").and_then(Item::as_table_like))
|
||||
.map(Self::map_requires_python)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn map_requires_python(groups: &dyn TableLike) -> FxHashMap<String, RequiresPython> {
|
||||
groups
|
||||
.get_values()
|
||||
.iter()
|
||||
.filter_map(|(keys, value)| {
|
||||
value
|
||||
.as_inline_table()
|
||||
.and_then(|i| i.get("requires-python"))
|
||||
.and_then(|requires| {
|
||||
requires
|
||||
.as_str()
|
||||
.and_then(|v| VersionSpecifiers::from_str(v).ok())
|
||||
})
|
||||
.map(|specifiers| {
|
||||
(
|
||||
keys.iter().join("."),
|
||||
RequiresPython::from_specifiers(&specifiers),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
fn replace_dependencies(
|
||||
latest_versions: &FxHashMap<&PackageName, Version>,
|
||||
latest_versions: &PackageVersions,
|
||||
all_upgrades: &mut Vec<UpgradeResult>,
|
||||
item: &mut Item,
|
||||
dependency_type: &DependencyType,
|
||||
allow: &[usize],
|
||||
skipped: &mut VersionDigit,
|
||||
) {
|
||||
requires_python: &Option<RequiresPython>,
|
||||
) -> (usize, usize) {
|
||||
if let Some(dependencies) = item.as_array_mut().filter(|d| !d.is_empty()) {
|
||||
Self::replace_upgrades(
|
||||
return Self::replace_upgrades(
|
||||
latest_versions,
|
||||
all_upgrades,
|
||||
dependencies,
|
||||
dependency_type,
|
||||
allow,
|
||||
skipped,
|
||||
requires_python,
|
||||
);
|
||||
}
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
fn find_upgrades(
|
||||
latest_versions: &FxHashMap<&PackageName, Version>,
|
||||
latest_versions: &PackageVersions,
|
||||
dependencies: &mut Array,
|
||||
dependency_type: &DependencyType,
|
||||
allow: &[usize],
|
||||
skipped: &mut VersionDigit,
|
||||
) -> Vec<UpgradeResult> {
|
||||
requires_python: &Option<RequiresPython>,
|
||||
) -> (Vec<UpgradeResult>, usize, usize) {
|
||||
let mut upgrades = Vec::new();
|
||||
let mut count_skipped = 0;
|
||||
let mut found = 0;
|
||||
for (i, dep) in dependencies.iter().enumerate() {
|
||||
let Some(mut req) = dep.as_str().and_then(try_parse_requirement) else {
|
||||
continue;
|
||||
};
|
||||
found += 1;
|
||||
let old = req.clone();
|
||||
// Skip requirements without version constraints
|
||||
let Some(VersionOrUrl::VersionSpecifier(mut version_specifiers)) = req.version_or_url
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(upgrade) = latest_versions.get(&old.name) {
|
||||
let (bumped, upgraded, semver) =
|
||||
if let Some(upgrade) = latest_versions.find(&old.name, requires_python) {
|
||||
let (bumped, upgraded, semver, skip) =
|
||||
version_specifiers.bump_last(upgrade, allow, skipped);
|
||||
if bumped {
|
||||
req.version_or_url = Some(VersionOrUrl::VersionSpecifier(version_specifiers));
|
||||
|
@ -1371,33 +1467,42 @@ impl PyProjectTomlMut {
|
|||
upgraded,
|
||||
dependency_type.clone(),
|
||||
semver,
|
||||
requires_python.clone(),
|
||||
));
|
||||
} else if skip {
|
||||
count_skipped += 1;
|
||||
}
|
||||
} else {
|
||||
panic!("Error: No latest found for {}", old.name)
|
||||
}
|
||||
}
|
||||
upgrades
|
||||
(upgrades, found, count_skipped)
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
fn replace_upgrades(
|
||||
latest_versions: &FxHashMap<&PackageName, Version>,
|
||||
latest_versions: &PackageVersions,
|
||||
all_upgrades: &mut Vec<UpgradeResult>,
|
||||
dependencies: &mut Array,
|
||||
dependency_type: &DependencyType,
|
||||
allow: &[usize],
|
||||
skipped: &mut VersionDigit,
|
||||
) {
|
||||
let upgrades = Self::find_upgrades(
|
||||
requires_python: &Option<RequiresPython>,
|
||||
) -> (usize, usize) {
|
||||
let (upgrades, found, count_skipped) = Self::find_upgrades(
|
||||
latest_versions,
|
||||
dependencies,
|
||||
dependency_type,
|
||||
allow,
|
||||
skipped,
|
||||
requires_python,
|
||||
);
|
||||
for (i, _dep, _old, new, _upgrade, _upgraded, _, _) in &upgrades {
|
||||
for (i, _dep, _old, new, _upgrade, _upgraded, _, _, _) in &upgrades {
|
||||
let string = new.to_string();
|
||||
dependencies.replace(*i, toml_edit::Value::from(string));
|
||||
}
|
||||
all_upgrades.extend(upgrades);
|
||||
(found, count_skipped)
|
||||
}
|
||||
|
||||
pub fn version(&mut self) -> Result<Version, Error> {
|
||||
|
|
|
@ -8,25 +8,27 @@ use owo_colors::OwoColorize;
|
|||
use prettytable::format::FormatBuilder;
|
||||
use prettytable::row;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::cmp::min;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Write;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use tokio::sync::Semaphore;
|
||||
use uv_cache::Cache;
|
||||
use uv_cli::{Maybe, UpgradeProjectArgs};
|
||||
use uv_client::{BaseClientBuilder, RegistryClient, RegistryClientBuilder};
|
||||
use uv_configuration::Concurrency;
|
||||
use uv_distribution_filename::DistFilename;
|
||||
use uv_distribution_types::{IndexCapabilities, IndexLocations};
|
||||
use uv_pep440::{Version, VersionDigit, VersionSpecifiers};
|
||||
use uv_distribution_types::{IndexCapabilities, IndexLocations, RequiresPython};
|
||||
use uv_pep440::{Version, VersionDigit};
|
||||
use uv_pep508::{PackageName, Requirement};
|
||||
use uv_resolver::{PrereleaseMode, RequiresPython};
|
||||
use uv_resolver::PrereleaseMode;
|
||||
use uv_warnings::warn_user;
|
||||
use uv_workspace::pyproject::DependencyType;
|
||||
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
|
||||
use uv_workspace::pyproject_mut::{DependencyTarget, PackageVersions, PyProjectTomlMut};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Upgrade all dependencies in the project requirements (pyproject.toml).
|
||||
|
@ -65,6 +67,7 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
|
||||
let printer = Printer::Default;
|
||||
let info = format!("{}{}", "info".cyan().bold(), ":".bold());
|
||||
let uv_sync = format!("{}", "`uv sync -U`".green().bold());
|
||||
|
||||
let capabilities = IndexCapabilities::default();
|
||||
let client_builder = BaseClientBuilder::new();
|
||||
|
@ -76,14 +79,21 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
.build();
|
||||
let concurrency = Concurrency::default();
|
||||
|
||||
let (mut item_written, mut all_found, mut all_bumped, mut all_skipped) =
|
||||
(false, 0, 0, VersionDigit::default());
|
||||
let mut item_written = false;
|
||||
let mut all_found = 0;
|
||||
let mut all_bumped = 0;
|
||||
let mut files_bumped = 0;
|
||||
let mut all_count_skipped = 0;
|
||||
let mut all_skipped = VersionDigit::default();
|
||||
|
||||
let mut all_latest_versions = FxHashMap::default();
|
||||
// 1. args (override) 2. group (tool.uv.dependency-groups) 3. toml (project.requires-python)
|
||||
let python_args = args
|
||||
.python
|
||||
.clone()
|
||||
.and_then(Maybe::into_option)
|
||||
.and_then(|v| RequiresPython::from_str(&v).ok());
|
||||
let mut all_versioned = FxHashMap::default();
|
||||
let mut required_python_downloaded = FxHashSet::default();
|
||||
let mut toml_contents = BTreeMap::default();
|
||||
|
||||
for toml_dir in &tomls {
|
||||
let pyproject_toml = Path::new(toml_dir).join("pyproject.toml");
|
||||
let toml = match read_pyproject_toml(&pyproject_toml).await {
|
||||
|
@ -94,35 +104,35 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
if versioned.is_empty() {
|
||||
continue; // Skip pyproject.toml without versioned dependencies
|
||||
}
|
||||
let python = get_python(&args, &toml);
|
||||
let requires_python = create_requires_python(python);
|
||||
all_versioned
|
||||
.entry(requires_python.to_string())
|
||||
.or_insert_with(FxHashSet::default)
|
||||
.extend(versioned);
|
||||
let python_toml = get_requires_python(&toml);
|
||||
for (python_group, packages) in versioned {
|
||||
let python = python_args.clone().or(python_group).or(python_toml.clone());
|
||||
all_versioned
|
||||
.entry(python)
|
||||
.or_insert_with(FxHashSet::default)
|
||||
.extend(packages);
|
||||
}
|
||||
toml_contents.insert(toml_dir, toml);
|
||||
}
|
||||
|
||||
let mut package_versions = PackageVersions::default();
|
||||
for (requires_python, packages) in all_versioned {
|
||||
let latest_versions = find_latest(
|
||||
&client,
|
||||
&capabilities,
|
||||
requires_python.clone(),
|
||||
&packages,
|
||||
concurrency.downloads,
|
||||
)
|
||||
.await;
|
||||
// A package can be downloaded multiple times (one time per requires_python)
|
||||
for (name, version) in latest_versions {
|
||||
package_versions.insert(name.clone(), version, requires_python.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (toml_dir, toml) in &mut toml_contents {
|
||||
let pyproject_toml = Path::new(*toml_dir).join("pyproject.toml");
|
||||
let python = get_python(&args, toml);
|
||||
let requires_python = create_requires_python(python);
|
||||
let requires_python_str = requires_python.to_string();
|
||||
|
||||
if !required_python_downloaded.contains(&requires_python_str) {
|
||||
let query_versions = &all_versioned[&requires_python_str];
|
||||
let latest_versions = find_latest(
|
||||
&client,
|
||||
&capabilities,
|
||||
&requires_python,
|
||||
query_versions,
|
||||
concurrency.downloads,
|
||||
)
|
||||
.await;
|
||||
all_latest_versions.extend(latest_versions);
|
||||
required_python_downloaded.insert(requires_python_str);
|
||||
}
|
||||
|
||||
let relative = if *toml_dir == "." {
|
||||
String::new()
|
||||
} else {
|
||||
|
@ -131,16 +141,28 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
let subpath = format!("{relative}pyproject.toml");
|
||||
let mut skipped = VersionDigit::default();
|
||||
|
||||
let (found, upgrades) =
|
||||
toml.upgrade_all_dependencies(&all_latest_versions, &tables, &allow, &mut skipped);
|
||||
let python_toml = get_requires_python(toml);
|
||||
let requires_python = python_args.clone().or(python_toml);
|
||||
let (upgrades, found, count_skipped) = toml.upgrade_all_dependencies(
|
||||
&package_versions,
|
||||
&tables,
|
||||
&allow,
|
||||
&mut skipped,
|
||||
&requires_python,
|
||||
);
|
||||
all_skipped.add_other(&skipped);
|
||||
all_count_skipped += count_skipped;
|
||||
let bumped = upgrades.len();
|
||||
all_found += found;
|
||||
all_bumped += bumped;
|
||||
files_bumped += min(bumped, 1);
|
||||
if upgrades.is_empty() {
|
||||
if args.recursive && bumped == 0 {
|
||||
if !skipped.is_empty() {
|
||||
writeln!(printer.stderr(), "{info} Skipped {skipped} in {subpath}")?;
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} Skipped {skipped} ({count_skipped} upgrades) of {found} dependencies in {subpath}"
|
||||
)?;
|
||||
}
|
||||
continue; // Skip intermediate messages if nothing was changed
|
||||
}
|
||||
|
@ -152,8 +174,8 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} No upgrades found in {subpath}, check manually if not committed yet{}",
|
||||
skipped.format(" (skipped ", ")")
|
||||
"{info} No upgrades found for {found} dependencies in {subpath}, check manually if not committed yet{}",
|
||||
skipped.format(" (skipped ", &format!(" of {count_skipped} upgrades)"))
|
||||
)?;
|
||||
}
|
||||
continue;
|
||||
|
@ -169,7 +191,7 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
if args.dry_run { "dry-run" } else { "upgraded" }
|
||||
);
|
||||
table.add_row(
|
||||
row![r->"#", rb->"name", Fr->"-old", bFg->"+new", "latest", "S", "type", dry_run],
|
||||
row![r->"#", rb->"name", Fr->"-old", bFg->"+new", "latest", "S", "type", "py", dry_run],
|
||||
); // diff-like
|
||||
let remove_spaces = |v: &Requirement| {
|
||||
v.clone()
|
||||
|
@ -181,7 +203,7 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
upgrades
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, (_, _dep, old, new, version, upgraded, dependency_type, semver_change))| {
|
||||
.for_each(|(i, (_, _dep, old, new, version, upgraded, dependency_type, semver_change, python))| {
|
||||
let from = remove_spaces(old);
|
||||
let to = remove_spaces(new);
|
||||
let upordown = if *upgraded { "✅ up" } else { "❌ down" };
|
||||
|
@ -192,8 +214,9 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
DependencyType::Group(group) => format!("{group} [group]"),
|
||||
};
|
||||
let semver = semver_change.map_or(String::new(), |s| s.to_string());
|
||||
let _python = format_requires_python(python.clone());
|
||||
table.add_row(
|
||||
row![r->i + 1, rb->old.name, Fr->from, bFg->to, version.to_string(), semver, _type, upordown],
|
||||
row![r->i + 1, rb->old.name, Fr->from, bFg->to, version.to_string(), semver, _type, _python, upordown],
|
||||
);
|
||||
});
|
||||
table.printstd();
|
||||
|
@ -203,12 +226,14 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
}
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} Upgraded {subpath} 🚀 Check manually, update {} and run tests{}",
|
||||
"`uv sync -U`".green().bold(),
|
||||
skipped.format(" (skipped ", ")")
|
||||
"{info} Upgraded {bumped}/{found} in {subpath} 🚀 Check manually, update {uv_sync} and run tests{}",
|
||||
skipped.format(" (skipped ", &format!(" of {count_skipped} upgrades)"))
|
||||
)?;
|
||||
} else if !skipped.is_empty() {
|
||||
writeln!(printer.stderr(), "{info} Skipped {skipped} in {subpath}")?;
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} Skipped {skipped} ({count_skipped} upgrades), upgraded {bumped} of {found} dependencies in {subpath}"
|
||||
)?;
|
||||
}
|
||||
if !item_written {
|
||||
item_written = true;
|
||||
|
@ -219,7 +244,7 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
tomls.len(),
|
||||
if tomls.len() == 1 { "" } else { "s" }
|
||||
);
|
||||
if args.recursive {
|
||||
if args.recursive && files_bumped != 1 {
|
||||
if tomls.is_empty() {
|
||||
warn_user!("No pyproject.toml files found recursively");
|
||||
return Ok(ExitStatus::Error);
|
||||
|
@ -229,30 +254,29 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
printer.stderr(),
|
||||
"{info} No dependencies in {files} found recursively"
|
||||
)?;
|
||||
} else if !all_skipped.is_empty() {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} Skipped {all_skipped} ({all_count_skipped} upgrades), {all_found} dependencies in {files} not upgraded for --allow={}",
|
||||
format_allow(&allow)
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} No upgrades in {all_found} dependencies and {files} found, check manually if not committed yet{}",
|
||||
all_skipped.format(" (skipped ", ")")
|
||||
"{info} No upgrades in {all_found} dependencies and {files} found, check manually if not committed yet"
|
||||
)?;
|
||||
}
|
||||
} else if !all_skipped.is_empty() {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} Skipped {all_skipped} in {all_bumped} upgrades for --allow={}",
|
||||
allow
|
||||
.iter()
|
||||
.sorted()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
"{info} Total: Skipped {all_skipped} ({all_count_skipped} upgrades), upgraded {all_bumped} of {all_found} dependencies for --allow={}",
|
||||
format_allow(&allow)
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{info} Upgraded {all_bumped} dependencies in {files} 🚀 Check manually, update {} and run tests{}",
|
||||
"`uv sync -U`".green().bold(),
|
||||
all_skipped.format(" (skipped ", ")")
|
||||
"{info} Upgraded {all_bumped}/{all_found} dependencies in {files} 🚀 Check manually, update {uv_sync} and run tests{}",
|
||||
all_skipped.format(" (skipped ", &format!(" of {all_count_skipped} upgrades)"))
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
@ -260,23 +284,29 @@ pub(crate) async fn upgrade_project_dependencies(
|
|||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn create_requires_python(python: Option<String>) -> RequiresPython {
|
||||
let version_specifiers = python.and_then(|s| VersionSpecifiers::from_str(&s).ok());
|
||||
version_specifiers
|
||||
.map(|v| RequiresPython::from_specifiers(&v))
|
||||
.unwrap_or_else(|| RequiresPython::greater_than_equal_version(&Version::new([4]))) // allow any by default
|
||||
fn get_requires_python(toml: &PyProjectTomlMut) -> Option<RequiresPython> {
|
||||
toml.get_requires_python()
|
||||
.map(RequiresPython::from_str)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn get_python(args: &UpgradeProjectArgs, toml: &PyProjectTomlMut) -> Option<String> {
|
||||
let python = args
|
||||
.python
|
||||
.clone()
|
||||
.and_then(Maybe::into_option)
|
||||
.or_else(|| {
|
||||
toml.get_requires_python()
|
||||
.map(std::string::ToString::to_string)
|
||||
});
|
||||
python
|
||||
fn format_requires_python(python: Option<RequiresPython>) -> String {
|
||||
match python.map(|r| r.remove_zeroes()) {
|
||||
Some(s) if s == ">4" => String::new(), // hide default value
|
||||
Some(s) => s,
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_allow(allow: &[usize]) -> String {
|
||||
allow
|
||||
.iter()
|
||||
.sorted()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
|
||||
async fn read_pyproject_toml(
|
||||
|
@ -305,17 +335,19 @@ async fn read_pyproject_toml(
|
|||
async fn find_latest<'a>(
|
||||
client: &RegistryClient,
|
||||
capabilities: &IndexCapabilities,
|
||||
requires_python: &RequiresPython,
|
||||
requires_python: Option<RequiresPython>,
|
||||
names: &'a FxHashSet<PackageName>,
|
||||
downloads: usize,
|
||||
) -> FxHashMap<&'a PackageName, Version> {
|
||||
static DEFAULT_PYTHON: LazyLock<RequiresPython> =
|
||||
LazyLock::new(|| RequiresPython::from_str(">4").ok().unwrap());
|
||||
let latest_client = LatestClient {
|
||||
client,
|
||||
capabilities,
|
||||
prerelease: PrereleaseMode::Disallow,
|
||||
exclude_newer: None,
|
||||
tags: None,
|
||||
requires_python,
|
||||
requires_python: requires_python.as_ref().unwrap_or_else(|| &*DEFAULT_PYTHON),
|
||||
};
|
||||
|
||||
let download_concurrency = Semaphore::new(downloads);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue