use requires-python of tool.uv.dependency-groups

This commit is contained in:
Rene Leonhardt 2025-06-14 18:32:35 +02:00
parent eee3540558
commit 0a037894c8
No known key found for this signature in database
GPG key ID: 8C95C84F75AB1E8E
5 changed files with 331 additions and 147 deletions

View file

@ -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 {

View file

@ -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`

View file

@ -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 { "" };

View file

@ -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> {

View file

@ -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);