diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index ba80736a0..492942f1b 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -1,7 +1,8 @@ use anstream::eprintln; use uv_cache::Refresh; -use uv_distribution_types::{ConfigSettings, PackageConfigSettings}; +use uv_configuration::UpgradeSelection; +use uv_distribution_types::{ConfigSettings, PackageConfigSettings, Requirement}; use uv_resolver::{ExcludeNewer, ExcludeNewerPackage, PrereleaseMode}; use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions}; use uv_warnings::owo_colors::OwoColorize; @@ -333,8 +334,10 @@ pub fn resolver_options( .filter_map(Maybe::into_option) .collect() }), - upgrade: flag(upgrade, no_upgrade, "no-upgrade"), - upgrade_package: Some(upgrade_package), + upgrade: UpgradeSelection::from_args( + flag(upgrade, no_upgrade, "no-upgrade"), + upgrade_package.into_iter().map(Requirement::from).collect(), + ), index_strategy, keyring_provider, resolution, @@ -442,12 +445,10 @@ pub fn resolver_installer_options( .filter_map(Maybe::into_option) .collect() }), - upgrade: flag(upgrade, no_upgrade, "upgrade"), - upgrade_package: if upgrade_package.is_empty() { - None - } else { - Some(upgrade_package) - }, + upgrade: UpgradeSelection::from_args( + flag(upgrade, no_upgrade, "upgrade"), + upgrade_package.into_iter().map(Requirement::from).collect(), + ), reinstall: flag(reinstall, no_reinstall, "reinstall"), reinstall_package: if reinstall_package.is_empty() { None diff --git a/crates/uv-configuration/src/package_options.rs b/crates/uv-configuration/src/package_options.rs index a8654089e..f4b04a245 100644 --- a/crates/uv-configuration/src/package_options.rs +++ b/crates/uv-configuration/src/package_options.rs @@ -134,9 +134,57 @@ impl From for Refresh { } } +/// An upgrade selection as specified by a user on the command line or in a configuration file. +#[derive(Debug, Default, Clone)] +pub enum UpgradeSelection { + /// Prefer pinned versions from the existing lockfile, if possible. + #[default] + None, + + /// Allow package upgrades for all packages, ignoring the existing lockfile. + All, + + /// Allow package upgrades, but only for the specified packages. + Packages(Vec), +} + +impl UpgradeSelection { + /// Determine the upgrade selection strategy from the command-line arguments. + pub fn from_args(upgrade: Option, upgrade_package: Vec) -> Option { + match upgrade { + Some(true) => Some(Self::All), + // TODO(charlie): `--no-upgrade` with `--upgrade-package` should allow the specified + // packages to be upgraded. Right now, `--upgrade-package` is silently ignored. + Some(false) => Some(Self::None), + None if upgrade_package.is_empty() => None, + None => Some(Self::Packages(upgrade_package)), + } + } + + /// Combine a set of [`UpgradeSelection`] values. + #[must_use] + pub fn combine(self, other: Self) -> Self { + match self { + // Setting `--upgrade` or `--no-upgrade` should clear previous `--upgrade-package` selections. + Self::All | Self::None => self, + Self::Packages(self_packages) => match other { + // If `--upgrade` was enabled previously, `--upgrade-package` is subsumed by upgrading all packages. + Self::All => other, + // If `--no-upgrade` was enabled previously, then `--upgrade-package` enables an explicit upgrade of those packages. + Self::None => Self::Packages(self_packages), + // If `--upgrade-package` was included twice, combine the requirements. + Self::Packages(other_packages) => { + let mut combined = self_packages; + combined.extend(other_packages); + Self::Packages(combined) + } + }, + } + } +} + /// Whether to allow package upgrades. -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[derive(Debug, Default, Clone)] pub enum Upgrade { /// Prefer pinned versions from the existing lockfile, if possible. #[default] @@ -149,30 +197,28 @@ pub enum Upgrade { Packages(FxHashMap>), } -impl Upgrade { - /// Determine the [`Upgrade`] strategy from the command-line arguments. - pub fn from_args(upgrade: Option, upgrade_package: Vec) -> Self { - match upgrade { - Some(true) => Self::All, - Some(false) => Self::None, - None => { - if upgrade_package.is_empty() { - Self::None - } else { - Self::Packages(upgrade_package.into_iter().fold( - FxHashMap::default(), - |mut map, requirement| { - map.entry(requirement.name.clone()) - .or_default() - .push(requirement); - map - }, - )) - } - } +/// Determine the [`Upgrade`] strategy from the command-line arguments. +impl From> for Upgrade { + fn from(value: Option) -> Self { + match value { + None => Self::None, + Some(UpgradeSelection::None) => Self::None, + Some(UpgradeSelection::All) => Self::All, + Some(UpgradeSelection::Packages(requirements)) => Self::Packages( + requirements + .into_iter() + .fold(FxHashMap::default(), |mut map, requirement| { + map.entry(requirement.name.clone()) + .or_default() + .push(requirement); + map + }), + ), } } +} +impl Upgrade { /// Create an [`Upgrade`] strategy to upgrade a single package. pub fn package(package_name: PackageName) -> Self { Self::Packages({ diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index c70c2a354..0fac60d08 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -14,7 +14,7 @@ use uv_pep440::VersionSpecifiers; use uv_pep508::PackageName; use uv_pypi_types::VerbatimParsedUrl; use uv_redacted::DisplaySafeUrl; -use uv_settings::{GlobalOptions, ResolverInstallerOptions}; +use uv_settings::{GlobalOptions, ResolverInstallerSchema}; use uv_warnings::warn_user; use uv_workspace::pyproject::{ExtraBuildDependency, Sources}; @@ -424,7 +424,7 @@ pub struct ToolUv { #[serde(flatten)] pub globals: GlobalOptions, #[serde(flatten)] - pub top_level: ResolverInstallerOptions, + pub top_level: ResolverInstallerSchema, pub override_dependencies: Option>>, pub constraint_dependencies: Option>>, pub build_constraint_dependencies: Option>>, diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 3a297276f..02be876ec 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -5,7 +5,7 @@ use url::Url; use uv_configuration::{ ExportFormat, IndexStrategy, KeyringProviderType, RequiredVersion, TargetTriple, - TrustedPublishing, + TrustedPublishing, UpgradeSelection, }; use uv_distribution_types::{ ConfigSettings, ExtraBuildVariables, Index, IndexUrl, PackageConfigSettings, PipExtraIndex, @@ -181,6 +181,15 @@ impl Combine for Option { } } +impl Combine for Option { + fn combine(self, other: Self) -> Self { + match (self, other) { + (Some(a), Some(b)) => Some(a.combine(b)), + (a, b) => a.or(b), + } + } +} + impl Combine for serde::de::IgnoredAny { fn combine(self, _other: Self) -> Self { self diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 4eda772ad..df15d60bf 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -301,7 +301,7 @@ fn warn_uv_toml_masked_fields(options: &Options) { allow_insecure_host, }, top_level: - ResolverInstallerOptions { + ResolverInstallerSchema { index, index_url, extra_index_url, diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 31218f2a5..96509f524 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use uv_cache_info::CacheKey; use uv_configuration::{ IndexStrategy, KeyringProviderType, PackageNameSpecifier, RequiredVersion, TargetTriple, - TrustedHost, TrustedPublishing, + TrustedHost, TrustedPublishing, UpgradeSelection, }; use uv_distribution_types::{ ConfigSettings, ExtraBuildVariables, Index, IndexUrl, IndexUrlError, PackageConfigSettings, @@ -52,7 +52,7 @@ pub struct Options { pub globals: GlobalOptions, #[serde(flatten)] - pub top_level: ResolverInstallerOptions, + pub top_level: ResolverInstallerSchema, #[serde(flatten)] pub install_mirrors: PythonInstallMirrors, @@ -161,7 +161,7 @@ pub struct Options { impl Options { /// Construct an [`Options`] with the given global and top-level settings. - pub fn simple(globals: GlobalOptions, top_level: ResolverInstallerOptions) -> Self { + pub fn simple(globals: GlobalOptions, top_level: ResolverInstallerSchema) -> Self { Self { globals, top_level, @@ -369,8 +369,7 @@ pub struct ResolverOptions { pub config_settings_package: Option, pub exclude_newer: ExcludeNewer, pub link_mode: Option, - pub upgrade: Option, - pub upgrade_package: Option>>, + pub upgrade: Option, pub no_build: Option, pub no_build_package: Option>, pub no_binary: Option, @@ -384,10 +383,159 @@ pub struct ResolverOptions { /// Shared settings, relevant to all operations that must resolve and install dependencies. The /// union of [`InstallerOptions`] and [`ResolverOptions`]. +#[derive(Debug, Clone, Default, CombineOptions)] +pub struct ResolverInstallerOptions { + pub index: Option>, + pub index_url: Option, + pub extra_index_url: Option>, + pub no_index: Option, + pub find_links: Option>, + pub index_strategy: Option, + pub keyring_provider: Option, + pub resolution: Option, + pub prerelease: Option, + pub fork_strategy: Option, + pub dependency_metadata: Option>, + pub config_settings: Option, + pub config_settings_package: Option, + pub no_build_isolation: Option, + pub no_build_isolation_package: Option>, + pub extra_build_dependencies: Option, + pub extra_build_variables: Option, + pub exclude_newer: Option, + pub exclude_newer_package: Option, + pub link_mode: Option, + pub compile_bytecode: Option, + pub no_sources: Option, + pub upgrade: Option, + pub reinstall: Option, + pub reinstall_package: Option>, + pub no_build: Option, + pub no_build_package: Option>, + pub no_binary: Option, + pub no_binary_package: Option>, +} + +impl From for ResolverInstallerOptions { + fn from(value: ResolverInstallerSchema) -> Self { + let ResolverInstallerSchema { + index, + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + resolution, + prerelease, + fork_strategy, + dependency_metadata, + config_settings, + config_settings_package, + no_build_isolation, + no_build_isolation_package, + extra_build_dependencies, + extra_build_variables, + exclude_newer, + exclude_newer_package, + link_mode, + compile_bytecode, + no_sources, + upgrade, + upgrade_package, + reinstall, + reinstall_package, + no_build, + no_build_package, + no_binary, + no_binary_package, + } = value; + Self { + index, + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + resolution, + prerelease, + fork_strategy, + dependency_metadata, + config_settings, + config_settings_package, + no_build_isolation, + no_build_isolation_package, + extra_build_dependencies, + extra_build_variables, + exclude_newer, + exclude_newer_package, + link_mode, + compile_bytecode, + no_sources, + upgrade: UpgradeSelection::from_args( + upgrade, + upgrade_package + .into_iter() + .flatten() + .map(Into::into) + .collect(), + ), + reinstall, + reinstall_package, + no_build, + no_build_package, + no_binary, + no_binary_package, + } + } +} + +impl ResolverInstallerSchema { + /// Resolve the [`ResolverInstallerSchema`] relative to the given root directory. + pub fn relative_to(self, root_dir: &Path) -> Result { + Ok(Self { + index: self + .index + .map(|index| { + index + .into_iter() + .map(|index| index.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?, + index_url: self + .index_url + .map(|index_url| index_url.relative_to(root_dir)) + .transpose()?, + extra_index_url: self + .extra_index_url + .map(|extra_index_url| { + extra_index_url + .into_iter() + .map(|extra_index_url| extra_index_url.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?, + find_links: self + .find_links + .map(|find_links| { + find_links + .into_iter() + .map(|find_link| find_link.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?, + ..self + }) + } +} + +/// The JSON schema for the `[tool.uv]` section of a `pyproject.toml` file. #[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, CombineOptions, OptionsMetadata)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ResolverInstallerOptions { +pub struct ResolverInstallerSchema { /// The package indexes to use when resolving dependencies. /// /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) @@ -814,46 +962,6 @@ pub struct ResolverInstallerOptions { pub no_binary_package: Option>, } -impl ResolverInstallerOptions { - /// Resolve the [`ResolverInstallerOptions`] relative to the given root directory. - pub fn relative_to(self, root_dir: &Path) -> Result { - Ok(Self { - index: self - .index - .map(|index| { - index - .into_iter() - .map(|index| index.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - index_url: self - .index_url - .map(|index_url| index_url.relative_to(root_dir)) - .transpose()?, - extra_index_url: self - .extra_index_url - .map(|extra_index_url| { - extra_index_url - .into_iter() - .map(|extra_index_url| extra_index_url.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - find_links: self - .find_links - .map(|find_links| { - find_links - .into_iter() - .map(|find_link| find_link.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - ..self - }) - } -} - /// Shared settings, relevant to all operations that might create managed python installations. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, CombineOptions, OptionsMetadata)] #[serde(rename_all = "kebab-case")] @@ -1752,8 +1860,8 @@ impl PipOptions { } } -impl From for ResolverOptions { - fn from(value: ResolverInstallerOptions) -> Self { +impl From for ResolverOptions { + fn from(value: ResolverInstallerSchema) -> Self { Self { index: value.index, index_url: value.index_url, @@ -1778,8 +1886,15 @@ impl From for ResolverOptions { .collect(), ), link_mode: value.link_mode, - upgrade: value.upgrade, - upgrade_package: value.upgrade_package, + upgrade: UpgradeSelection::from_args( + value.upgrade, + value + .upgrade_package + .into_iter() + .flatten() + .map(Into::into) + .collect(), + ), no_build: value.no_build, no_build_package: value.no_build_package, no_binary: value.no_binary, @@ -1793,8 +1908,8 @@ impl From for ResolverOptions { } } -impl From for InstallerOptions { - fn from(value: ResolverInstallerOptions) -> Self { +impl From for InstallerOptions { + fn from(value: ResolverInstallerSchema) -> Self { Self { index: value.index, index_url: value.index_url, @@ -1830,7 +1945,7 @@ impl From for InstallerOptions { /// The options persisted alongside an installed tool. /// -/// A mirror of [`ResolverInstallerOptions`], without upgrades and reinstalls, which shouldn't be +/// A mirror of [`ResolverInstallerSchema`], without upgrades and reinstalls, which shouldn't be /// persisted in a tool receipt. #[derive( Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, CombineOptions, OptionsMetadata, @@ -1925,7 +2040,6 @@ impl From for ResolverInstallerOptions { compile_bytecode: value.compile_bytecode, no_sources: value.no_sources, upgrade: None, - upgrade_package: None, reinstall: None, reinstall_package: None, no_build: value.no_build, @@ -2120,7 +2234,7 @@ impl From for Options { // Used twice for backwards compatibility allow_insecure_host: allow_insecure_host.clone(), }, - top_level: ResolverInstallerOptions { + top_level: ResolverInstallerSchema { index, index_url, extra_index_url, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index f56a02e82..27793c60d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -24,7 +24,7 @@ use uv_configuration::{ BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, Preview, ProjectBuildBackend, Reinstall, RequiredVersion, SourceStrategy, - TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + TargetTriple, TrustedHost, TrustedPublishing, Upgrade, UpgradeSelection, VersionControlSystem, }; use uv_distribution_types::{ ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations, IndexUrl, @@ -42,7 +42,7 @@ use uv_resolver::{ }; use uv_settings::{ Combine, EnvironmentOptions, FilesystemOptions, Options, PipOptions, PublishOptions, - PythonInstallMirrors, ResolverInstallerOptions, ResolverOptions, + PythonInstallMirrors, ResolverInstallerOptions, ResolverInstallerSchema, ResolverOptions, }; use uv_static::EnvVars; use uv_torch::TorchMode; @@ -538,13 +538,14 @@ impl ToolRunSettings { } } - let options = resolver_installer_options(installer, build).combine( - filesystem - .clone() - .map(FilesystemOptions::into_options) - .map(|options| options.top_level) - .unwrap_or_default(), - ); + let options = + resolver_installer_options(installer, build).combine(ResolverInstallerOptions::from( + filesystem + .clone() + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) + .unwrap_or_default(), + )); let install_mirrors = filesystem .map(FilesystemOptions::into_options) @@ -636,13 +637,14 @@ impl ToolInstallSettings { python, } = args; - let options = resolver_installer_options(installer, build).combine( - filesystem - .clone() - .map(FilesystemOptions::into_options) - .map(|options| options.top_level) - .unwrap_or_default(), - ); + let options = + resolver_installer_options(installer, build).combine(ResolverInstallerOptions::from( + filesystem + .clone() + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) + .unwrap_or_default(), + )); let install_mirrors = filesystem .map(FilesystemOptions::into_options) @@ -777,9 +779,11 @@ impl ToolUpgradeSettings { .clone() .map(|options| options.install_mirrors) .unwrap_or_default(); - let top_level = filesystem - .map(|options| options.top_level) - .unwrap_or_default(); + let top_level = ResolverInstallerOptions::from( + filesystem + .map(|options| options.top_level) + .unwrap_or_default(), + ); Self { names: if all { vec![] } else { name }, @@ -2798,6 +2802,8 @@ pub(crate) struct ResolverSettings { impl ResolverSettings { /// Resolve the [`ResolverSettings`] from the CLI and filesystem configuration. pub(crate) fn combine(args: ResolverOptions, filesystem: Option) -> Self { + // The problem is that for `upgrade`... we want to combine the two `Upgrade` structs, + // not the individual fields. let options = args.combine(ResolverOptions::from( filesystem .map(FilesystemOptions::into_options) @@ -2846,15 +2852,7 @@ impl From for ResolverSettings { exclude_newer: value.exclude_newer, link_mode: value.link_mode.unwrap_or_default(), sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()), - upgrade: Upgrade::from_args( - value.upgrade, - value - .upgrade_package - .into_iter() - .flatten() - .map(Requirement::from) - .collect(), - ), + upgrade: Upgrade::from(value.upgrade), build_options: BuildOptions::new( NoBinary::from_args(value.no_binary, value.no_binary_package.unwrap_or_default()), NoBuild::from_args(value.no_build, value.no_build_package.unwrap_or_default()), @@ -2881,12 +2879,12 @@ impl ResolverInstallerSettings { args: ResolverInstallerOptions, filesystem: Option, ) -> Self { - let options = args.combine( + let options = args.combine(ResolverInstallerOptions::from( filesystem .map(FilesystemOptions::into_options) .map(|options| options.top_level) .unwrap_or_default(), - ); + )); Self::from(options) } @@ -2945,15 +2943,7 @@ impl From for ResolverInstallerSettings { prerelease: value.prerelease.unwrap_or_default(), resolution: value.resolution.unwrap_or_default(), sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()), - upgrade: Upgrade::from_args( - value.upgrade, - value - .upgrade_package - .into_iter() - .flatten() - .map(Requirement::from) - .collect(), - ), + upgrade: Upgrade::from(value.upgrade), }, compile_bytecode: value.compile_bytecode.unwrap_or_default(), reinstall: Reinstall::from_args( @@ -3098,7 +3088,7 @@ impl PipSettings { exclude_newer_package, } = pip.unwrap_or_default(); - let ResolverInstallerOptions { + let ResolverInstallerSchema { index: top_level_index, index_url: top_level_index_url, extra_index_url: top_level_extra_index_url, @@ -3327,14 +3317,23 @@ impl PipSettings { args.no_sources.combine(no_sources).unwrap_or_default(), ), strict: args.strict.combine(strict).unwrap_or_default(), - upgrade: Upgrade::from_args( - args.upgrade.combine(upgrade), - args.upgrade_package - .combine(upgrade_package) - .into_iter() - .flatten() - .map(Requirement::from) - .collect(), + upgrade: Upgrade::from( + UpgradeSelection::from_args( + args.upgrade, + args.upgrade_package + .into_iter() + .flatten() + .map(Requirement::from) + .collect(), + ) + .combine(UpgradeSelection::from_args( + upgrade, + upgrade_package + .into_iter() + .flatten() + .map(Requirement::from) + .collect(), + )), ), reinstall: Reinstall::from_args( args.reinstall.combine(reinstall), @@ -3418,7 +3417,7 @@ impl PublishSettings { trusted_publishing, check_url, } = publish; - let ResolverInstallerOptions { + let ResolverInstallerSchema { keyring_provider, index, extra_index_url, diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index faae87628..c7018a6c6 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3514,7 +3514,6 @@ fn resolve_tool() -> anyhow::Result<()> { compile_bytecode: None, no_sources: None, upgrade: None, - upgrade_package: None, reinstall: None, reinstall_package: None, no_build: None, @@ -8569,7 +8568,6 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { "})?; // Despite `upgrade = false` in the configuration file, we should mark `idna` for upgrade. - // TODO(charlie): This doesn't mark `idna` for upgrade; it just disables upgrades. uv_snapshot!(context.filters(), add_shared_args(context.pip_compile(), context.temp_dir.path()) .arg("--upgrade-package") .arg("idna") @@ -8727,7 +8725,30 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { hash_checking: Some( Verify, ), - upgrade: None, + upgrade: Packages( + { + PackageName( + "idna", + ): [ + Requirement { + name: PackageName( + "idna", + ), + extras: [], + groups: [], + marker: true, + source: Registry { + specifier: VersionSpecifiers( + [], + ), + index: None, + conflict: None, + }, + origin: None, + }, + ], + }, + ), reinstall: None, }, } @@ -9600,7 +9621,6 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { "#})?; // Despite `upgrade = false` in the configuration file, we should mark `idna` for upgrade. - // TODO(charlie): This doesn't mark `idna` for upgrade; it just disables upgrades. uv_snapshot!(context.filters(), add_shared_args(context.lock(), context.temp_dir.path()) .arg("--upgrade-package") .arg("idna") @@ -9699,7 +9719,30 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { prerelease: IfNecessaryOrExplicit, resolution: Highest, sources: Enabled, - upgrade: None, + upgrade: Packages( + { + PackageName( + "idna", + ): [ + Requirement { + name: PackageName( + "idna", + ), + extras: [], + groups: [], + marker: true, + source: Registry { + specifier: VersionSpecifiers( + [], + ), + index: None, + conflict: None, + }, + origin: None, + }, + ], + }, + ), }, }