Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5717,8 +5717,9 @@ pub struct ToolUpgradeArgs {
#[arg(long)]
pub python_platform: Option<TargetTriple>,

// The following is equivalent to flattening `ResolverInstallerArgs`, with the `--upgrade`, and
// `--upgrade-package` options hidden, and the `--no-upgrade` option removed.
// The following is equivalent to flattening `ResolverInstallerArgs`, with the `--upgrade`,
// `--upgrade-package`, and `--upgrade-group` options hidden, and the `--no-upgrade` option
// removed.
/// Allow package upgrades, ignoring pinned versions in any existing output file. Implies
/// `--refresh`.
#[arg(hide = true, long, short = 'U', help_heading = "Resolver options")]
Expand All @@ -5729,6 +5730,11 @@ pub struct ToolUpgradeArgs {
#[arg(hide = true, long, short = 'P', help_heading = "Resolver options")]
pub upgrade_package: Vec<Requirement<VerbatimParsedUrl>>,

/// Allow upgrades for all packages in a dependency group, ignoring pinned versions in any
/// existing output file.
#[arg(hide = true, long, help_heading = "Resolver options")]
pub upgrade_group: Vec<GroupName>,

#[command(flatten)]
pub index_args: IndexArgs,

Expand Down Expand Up @@ -7048,6 +7054,11 @@ pub struct ResolverArgs {
#[arg(long, short = 'P', help_heading = "Resolver options")]
pub upgrade_package: Vec<Requirement<VerbatimParsedUrl>>,

/// Allow upgrades for all packages in a dependency group, ignoring pinned versions in any
/// existing output file.
#[arg(long, help_heading = "Resolver options")]
pub upgrade_group: Vec<GroupName>,

/// The strategy to use when resolving against multiple index URLs.
///
/// By default, uv will stop at the first index on which a given package is available, and limit
Expand Down Expand Up @@ -7259,6 +7270,11 @@ pub struct ResolverInstallerArgs {
#[arg(long, short = 'P', help_heading = "Resolver options", value_hint = ValueHint::Other)]
pub upgrade_package: Vec<Requirement<VerbatimParsedUrl>>,

/// Allow upgrades for all packages in a dependency group, ignoring pinned versions in any
/// existing output file.
#[arg(long, help_heading = "Resolver options")]
pub upgrade_group: Vec<GroupName>,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we support package:group for workspace member group selection? We support that syntax elsewhere right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not supported in the project interface right now, deferring.


/// Reinstall all packages, regardless of whether they're already installed. Implies
/// `--refresh`.
#[arg(
Expand Down
26 changes: 26 additions & 0 deletions crates/uv-cli/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ impl From<ResolverArgs> for PipOptions {
upgrade,
no_upgrade,
upgrade_package,
upgrade_group,
index_strategy,
keyring_provider,
resolution,
Expand All @@ -220,6 +221,16 @@ impl From<ResolverArgs> for PipOptions {
exclude_newer_package,
} = args;

if !upgrade_group.is_empty() {
eprintln!(
"{}{} `{}` is not supported in `uv pip` commands",
"error".bold().red(),
":".bold(),
"--upgrade-group".green(),
);
std::process::exit(2);
}

Self {
upgrade: flag(upgrade, no_upgrade, "no-upgrade"),
upgrade_package: Some(upgrade_package),
Expand Down Expand Up @@ -304,6 +315,7 @@ impl From<ResolverInstallerArgs> for PipOptions {
upgrade,
no_upgrade,
upgrade_package,
upgrade_group,
reinstall,
no_reinstall,
reinstall_package,
Expand All @@ -327,6 +339,16 @@ impl From<ResolverInstallerArgs> for PipOptions {
exclude_newer_package,
} = args;

if !upgrade_group.is_empty() {
eprintln!(
"{}{} `{}` is not supported in `uv pip` commands",
"error".bold().red(),
":".bold(),
"--upgrade-group".green(),
);
std::process::exit(2);
}

Self {
upgrade: flag(upgrade, no_upgrade, "upgrade"),
upgrade_package: Some(upgrade_package),
Expand Down Expand Up @@ -430,6 +452,7 @@ pub fn resolver_options(
upgrade,
no_upgrade,
upgrade_package,
upgrade_group,
index_strategy,
keyring_provider,
resolution,
Expand Down Expand Up @@ -490,6 +513,7 @@ pub fn resolver_options(
upgrade: Upgrade::from_args(
flag(upgrade, no_upgrade, "no-upgrade"),
upgrade_package.into_iter().map(Requirement::from).collect(),
upgrade_group,
),
index_strategy,
keyring_provider,
Expand Down Expand Up @@ -539,6 +563,7 @@ pub fn resolver_installer_options(
upgrade,
no_upgrade,
upgrade_package,
upgrade_group,
reinstall,
no_reinstall,
reinstall_package,
Expand Down Expand Up @@ -606,6 +631,7 @@ pub fn resolver_installer_options(
upgrade: Upgrade::from_args(
flag(upgrade, no_upgrade, "upgrade"),
upgrade_package.into_iter().map(Requirement::from).collect(),
upgrade_group,
),
reinstall: Reinstall::from_args(
flag(reinstall, no_reinstall, "reinstall"),
Expand Down
51 changes: 34 additions & 17 deletions crates/uv-configuration/src/package_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use uv_cache::Refresh;
use uv_cache_info::Timestamp;
use uv_distribution_types::{Requirement, RequirementSource};
use uv_normalize::PackageName;
use uv_normalize::{GroupName, PackageName};

/// Whether to reinstall packages.
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -141,8 +141,8 @@ pub enum UpgradeStrategy {
/// Allow package upgrades for all packages, ignoring the existing lockfile.
All,

/// Allow package upgrades, but only for the specified packages.
Packages(FxHashSet<PackageName>),
/// Allow package upgrades, but only for the specified packages and/or dependency groups.
Some(FxHashSet<PackageName>, FxHashSet<GroupName>),
}

/// Whether to allow package upgrades.
Expand Down Expand Up @@ -173,23 +173,30 @@ impl Upgrade {
}

/// Determine the upgrade selection strategy from the command-line arguments.
pub fn from_args(upgrade: Option<bool>, upgrade_package: Vec<Requirement>) -> Option<Self> {
pub fn from_args(
upgrade: Option<bool>,
upgrade_package: Vec<Requirement>,
upgrade_group: Vec<GroupName>,
) -> Option<Self> {
let groups: FxHashSet<GroupName> = upgrade_group.into_iter().collect();

let strategy = match upgrade {
Some(true) => UpgradeStrategy::All,
Some(false) => {
if upgrade_package.is_empty() {
if upgrade_package.is_empty() && groups.is_empty() {
return Some(Self::none());
}
// `--no-upgrade` with `--upgrade-package` allows selecting the specified packages for upgrade.
// `--no-upgrade` with `--upgrade-package` allows selecting the specified packages
// for upgrade.
let packages = upgrade_package.iter().map(|req| req.name.clone()).collect();
UpgradeStrategy::Packages(packages)
UpgradeStrategy::Some(packages, groups)
}
None => {
if upgrade_package.is_empty() {
if upgrade_package.is_empty() && groups.is_empty() {
return None;
}
let packages = upgrade_package.iter().map(|req| req.name.clone()).collect();
UpgradeStrategy::Packages(packages)
UpgradeStrategy::Some(packages, groups)
}
};

Expand Down Expand Up @@ -218,7 +225,7 @@ impl Upgrade {
let mut packages = FxHashSet::default();
packages.insert(package_name);
Self {
strategy: UpgradeStrategy::Packages(packages),
strategy: UpgradeStrategy::Some(packages, FxHashSet::default()),
constraints: FxHashMap::default(),
}
}
Expand All @@ -238,7 +245,7 @@ impl Upgrade {
match &self.strategy {
UpgradeStrategy::None => false,
UpgradeStrategy::All => true,
UpgradeStrategy::Packages(packages) => packages.contains(package_name),
UpgradeStrategy::Some(packages, _) => packages.contains(package_name),
}
}

Expand All @@ -251,21 +258,31 @@ impl Upgrade {
.flat_map(|requirements| requirements.iter())
}

/// Returns the set of dependency groups whose packages should be upgraded.
pub fn groups(&self) -> Option<&FxHashSet<GroupName>> {
match &self.strategy {
UpgradeStrategy::Some(_, groups) if !groups.is_empty() => Some(groups),
_ => None,
}
}

/// Combine a set of [`Upgrade`] values.
#[must_use]
pub fn combine(self, other: Self) -> Self {
// For `strategy`: `other` takes precedence for an explicit `All` or `None`; otherwise, merge.
// For `strategy`: `other` takes precedence for an explicit `All` or `None`; otherwise,
// merge.
let strategy = match (self.strategy, other.strategy) {
(_, UpgradeStrategy::All) => UpgradeStrategy::All,
(_, UpgradeStrategy::None) => UpgradeStrategy::None,
(
UpgradeStrategy::Packages(mut self_packages),
UpgradeStrategy::Packages(other_packages),
UpgradeStrategy::Some(mut self_packages, mut self_groups),
UpgradeStrategy::Some(other_packages, other_groups),
) => {
self_packages.extend(other_packages);
UpgradeStrategy::Packages(self_packages)
self_groups.extend(other_groups);
UpgradeStrategy::Some(self_packages, self_groups)
}
(_, UpgradeStrategy::Packages(packages)) => UpgradeStrategy::Packages(packages),
(_, UpgradeStrategy::Some(packages, groups)) => UpgradeStrategy::Some(packages, groups),
};

// For `constraints`: always merge the constraints of `self` and `other`.
Expand All @@ -290,7 +307,7 @@ impl From<Upgrade> for Refresh {
match value.strategy {
UpgradeStrategy::None => Self::None(Timestamp::now()),
UpgradeStrategy::All => Self::All(Timestamp::now()),
UpgradeStrategy::Packages(packages) => Self::Packages(
UpgradeStrategy::Some(packages, _) => Self::Packages(
packages.into_iter().collect::<Vec<_>>(),
Vec::new(),
Timestamp::now(),
Expand Down
45 changes: 43 additions & 2 deletions crates/uv-requirements/src/upgrade.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::path::Path;

use anyhow::Result;
use rustc_hash::FxHashSet;

use uv_configuration::Upgrade;
use uv_fs::CWD;
use uv_git::ResolvedRepositoryReference;
use uv_normalize::PackageName;
use uv_requirements_txt::RequirementsTxt;
use uv_resolver::{Lock, LockError, Preference, PreferenceError, PylockToml, PylockTomlErrorKind};

Expand Down Expand Up @@ -71,12 +73,17 @@ pub fn read_lock_requirements(
return Ok(LockedRequirements::default());
}

// Resolve `--upgrade-group` to a set of package names by collecting all dependencies from
// the specified groups across all workspace member packages in the lock.
let group_packages = resolve_group_packages(lock, upgrade);

let mut preferences = Vec::new();
let mut git = Vec::new();

for package in lock.packages() {
// Skip the distribution if it's not included in the upgrade strategy.
if upgrade.contains(package.name()) {
// Skip the distribution if it's included in the upgrade strategy or belongs to an
// upgraded group.
if upgrade.contains(package.name()) || group_packages.contains(package.name()) {
continue;
}

Expand All @@ -94,6 +101,40 @@ pub fn read_lock_requirements(
Ok(LockedRequirements { preferences, git })
}

/// Resolve the `--upgrade-group` group names to a set of package names by looking at the
/// dependency groups defined on packages in the lockfile.
fn resolve_group_packages(lock: &Lock, upgrade: &Upgrade) -> FxHashSet<PackageName> {
let Some(groups) = upgrade.groups() else {
return FxHashSet::default();
};

let mut packages = FxHashSet::default();

// Check package-level dependency groups (the standard case for projects with a `[project]`
// table).
for package in lock.packages() {
for (group_name, dependencies) in package.resolved_dependency_groups() {
if groups.contains(group_name) {
for dependency in dependencies {
packages.insert(dependency.package_name().clone());
}
}
}
}

// Check manifest-level dependency groups, which cover projects without a `[project]` table
// (e.g., virtual workspace roots or PEP 723 scripts).
for (group_name, requirements) in lock.dependency_groups() {
if groups.contains(group_name) {
for requirement in requirements {
packages.insert(requirement.name.clone());
}
}
}

packages
}

/// Load the preferred requirements from an existing `pylock.toml` file, applying the upgrade strategy.
pub async fn read_pylock_toml_requirements(
output_file: &Path,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ impl From<ResolverInstallerSchema> for ResolverInstallerOptions {
.flatten()
.map(Into::into)
.collect(),
Vec::new(),
),
reinstall: Reinstall::from_args(reinstall, reinstall_package.unwrap_or_default()),
no_build,
Expand Down Expand Up @@ -2006,6 +2007,7 @@ impl From<ResolverInstallerSchema> for ResolverOptions {
.flatten()
.map(Into::into)
.collect(),
Vec::new(),
),
no_build: value.no_build,
no_build_package: value.no_build_package,
Expand Down
13 changes: 13 additions & 0 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,19 @@ fn cmd() -> String {
return Some(None);
}

// Always skip the `--upgrade-group` and mark the next item to be skipped
if arg == "--upgrade-group" {
*skip_next = Some(true);
return Some(None);
}

// Skip only this argument if option and value are together
if arg.starts_with("--upgrade-group=") {
// Reset state; skip this iteration.
*skip_next = None;
return Some(None);
}

// Always skip the `--quiet` flag.
if arg == "--quiet" || arg == "-q" {
*skip_next = None;
Expand Down
8 changes: 5 additions & 3 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,10 +1193,12 @@ impl ValidatedLock {
return Ok(Self::Preferable(lock));
}

// If the user specified `--upgrade-package`, then at best we can prefer some of
// the existing versions.
// If the user specified `--upgrade-package` or `--upgrade-group`, then at best we can
// prefer some of the existing versions.
if !(upgrade.is_none() || upgrade.is_all()) {
debug!("Resolving despite existing lockfile due to `--upgrade-package`");
debug!(
"Resolving despite existing lockfile due to `--upgrade-package` or `--upgrade-group`"
);
return Ok(Self::Preferable(lock));
}

Expand Down
Loading