Skip to content

Commit b2459e6

Browse files
Introduce a --fork-strategy preference mode (#9868)
## Summary This PR makes the behavior in #9827 the default: we try to select the latest supported package version for each supported Python version, but we still optimize for choosing fewer versions when stratifying by platform. However, you can opt out with `--fork-strategy fewest`. Closes #7190.
1 parent 0ee2114 commit b2459e6

35 files changed

+699
-48
lines changed

crates/uv-cli/src/lib.rs

+55-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
1919
use uv_pep508::Requirement;
2020
use uv_pypi_types::VerbatimParsedUrl;
2121
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
22-
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
22+
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
2323
use uv_static::EnvVars;
2424

2525
pub mod comma;
@@ -4045,6 +4045,24 @@ pub struct ToolUpgradeArgs {
40454045
#[arg(long, hide = true)]
40464046
pub pre: bool,
40474047

4048+
/// The strategy to use when selecting multiple versions of a given package across Python
4049+
/// versions and platforms.
4050+
///
4051+
/// By default, uv will optimize for selecting the latest version of each package for each
4052+
/// supported Python version (`requires-python`), while minimizing the number of selected
4053+
/// versions across platforms.
4054+
///
4055+
/// Under `fewest`, uv will minimize the number of
4056+
/// selected versions for each package, preferring older versions that are compatible with a
4057+
/// wider range of supported Python versions or platforms.
4058+
#[arg(
4059+
long,
4060+
value_enum,
4061+
env = EnvVars::UV_FORK_STRATEGY,
4062+
help_heading = "Resolver options"
4063+
)]
4064+
pub fork_strategy: Option<ForkStrategy>,
4065+
40484066
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
40494067
#[arg(
40504068
long,
@@ -4834,6 +4852,24 @@ pub struct ResolverArgs {
48344852
#[arg(long, hide = true, help_heading = "Resolver options")]
48354853
pub pre: bool,
48364854

4855+
/// The strategy to use when selecting multiple versions of a given package across Python
4856+
/// versions and platforms.
4857+
///
4858+
/// By default, uv will optimize for selecting the latest version of each package for each
4859+
/// supported Python version (`requires-python`), while minimizing the number of selected
4860+
/// versions across platforms.
4861+
///
4862+
/// Under `fewest`, uv will minimize the number of
4863+
/// selected versions for each package, preferring older versions that are compatible with a
4864+
/// wider range of supported Python versions or platforms.
4865+
#[arg(
4866+
long,
4867+
value_enum,
4868+
env = EnvVars::UV_FORK_STRATEGY,
4869+
help_heading = "Resolver options"
4870+
)]
4871+
pub fork_strategy: Option<ForkStrategy>,
4872+
48374873
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
48384874
#[arg(
48394875
long,
@@ -5006,6 +5042,24 @@ pub struct ResolverInstallerArgs {
50065042
#[arg(long, hide = true)]
50075043
pub pre: bool,
50085044

5045+
/// The strategy to use when selecting multiple versions of a given package across Python
5046+
/// versions and platforms.
5047+
///
5048+
/// By default, uv will optimize for selecting the latest version of each package for each
5049+
/// supported Python version (`requires-python`), while minimizing the number of selected
5050+
/// versions across platforms.
5051+
///
5052+
/// Under `fewest`, uv will minimize the number of
5053+
/// selected versions for each package, preferring older versions that are compatible with a
5054+
/// wider range of supported Python versions or platforms.
5055+
#[arg(
5056+
long,
5057+
value_enum,
5058+
env = EnvVars::UV_FORK_STRATEGY,
5059+
help_heading = "Resolver options"
5060+
)]
5061+
pub fork_strategy: Option<ForkStrategy>,
5062+
50095063
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
50105064
#[arg(
50115065
long,

crates/uv-cli/src/options.rs

+8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ impl From<ResolverArgs> for PipOptions {
4343
resolution,
4444
prerelease,
4545
pre,
46+
fork_strategy,
4647
config_setting,
4748
no_build_isolation,
4849
no_build_isolation_package,
@@ -58,6 +59,7 @@ impl From<ResolverArgs> for PipOptions {
5859
index_strategy,
5960
keyring_provider,
6061
resolution,
62+
fork_strategy,
6163
prerelease: if pre {
6264
Some(PrereleaseMode::Allow)
6365
} else {
@@ -126,6 +128,7 @@ impl From<ResolverInstallerArgs> for PipOptions {
126128
resolution,
127129
prerelease,
128130
pre,
131+
fork_strategy,
129132
config_setting,
130133
no_build_isolation,
131134
no_build_isolation_package,
@@ -150,6 +153,7 @@ impl From<ResolverInstallerArgs> for PipOptions {
150153
} else {
151154
prerelease
152155
},
156+
fork_strategy,
153157
config_settings: config_setting
154158
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
155159
no_build_isolation: flag(no_build_isolation, build_isolation),
@@ -235,6 +239,7 @@ pub fn resolver_options(
235239
resolution,
236240
prerelease,
237241
pre,
242+
fork_strategy,
238243
config_setting,
239244
no_build_isolation,
240245
no_build_isolation_package,
@@ -291,6 +296,7 @@ pub fn resolver_options(
291296
} else {
292297
prerelease
293298
},
299+
fork_strategy,
294300
dependency_metadata: None,
295301
config_settings: config_setting
296302
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
@@ -324,6 +330,7 @@ pub fn resolver_installer_options(
324330
resolution,
325331
prerelease,
326332
pre,
333+
fork_strategy,
327334
config_setting,
328335
no_build_isolation,
329336
no_build_isolation_package,
@@ -392,6 +399,7 @@ pub fn resolver_installer_options(
392399
} else {
393400
prerelease
394401
},
402+
fork_strategy,
395403
dependency_metadata: None,
396404
config_settings: config_setting
397405
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),

crates/uv-python/src/python_version.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ impl schemars::JsonSchema for PythonVersion {
5050
..schemars::schema::StringValidation::default()
5151
})),
5252
metadata: Some(Box::new(schemars::schema::Metadata {
53-
description: Some("A Python version specifier, e.g. `3.7` or `3.8.0`.".to_string()),
53+
description: Some(
54+
"A Python version specifier, e.g. `3.11` or `3.12.4`.".to_string(),
55+
),
5456
..schemars::schema::Metadata::default()
5557
})),
5658
..schemars::schema::SchemaObject::default()
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
2+
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
3+
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
4+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
5+
pub enum ForkStrategy {
6+
/// Optimize for selecting the fewest number of versions for each package. Older versions may
7+
/// be preferred if they are compatible with a wider range of supported Python versions or
8+
/// platforms.
9+
Fewest,
10+
/// Optimize for selecting latest supported version of each package, for each supported Python
11+
/// version.
12+
#[default]
13+
RequiresPython,
14+
}
15+
16+
impl std::fmt::Display for ForkStrategy {
17+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18+
match self {
19+
Self::Fewest => write!(f, "fewest"),
20+
Self::RequiresPython => write!(f, "requires-python"),
21+
}
22+
}
23+
}

crates/uv-resolver/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub use error::{NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange};
33
pub use exclude_newer::ExcludeNewer;
44
pub use exclusions::Exclusions;
55
pub use flat_index::{FlatDistributions, FlatIndex};
6+
pub use fork_strategy::ForkStrategy;
67
pub use lock::{
78
InstallTarget, Lock, LockError, LockVersion, PackageMap, RequirementsTxtExport,
89
ResolverManifest, SatisfiesResult, TreeDisplay, VERSION,
@@ -41,6 +42,7 @@ mod exclude_newer;
4142
mod exclusions;
4243
mod flat_index;
4344
mod fork_indexes;
45+
mod fork_strategy;
4446
mod fork_urls;
4547
mod graph_ops;
4648
mod lock;

crates/uv-resolver/src/lock/mod.rs

+16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::sync::{Arc, LazyLock};
1313
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
1414
use url::Url;
1515

16+
use crate::fork_strategy::ForkStrategy;
1617
pub use crate::lock::map::PackageMap;
1718
pub use crate::lock::requirements_txt::RequirementsTxtExport;
1819
pub use crate::lock::target::InstallTarget;
@@ -239,6 +240,7 @@ impl Lock {
239240
let options = ResolverOptions {
240241
resolution_mode: resolution.options.resolution_mode,
241242
prerelease_mode: resolution.options.prerelease_mode,
243+
fork_strategy: resolution.options.fork_strategy,
242244
exclude_newer: resolution.options.exclude_newer,
243245
};
244246
let lock = Self::new(
@@ -548,6 +550,11 @@ impl Lock {
548550
self.options.prerelease_mode
549551
}
550552

553+
/// Returns the multi-version mode used to generate this lock.
554+
pub fn fork_strategy(&self) -> ForkStrategy {
555+
self.options.fork_strategy
556+
}
557+
551558
/// Returns the exclude newer setting used to generate this lock.
552559
pub fn exclude_newer(&self) -> Option<ExcludeNewer> {
553560
self.options.exclude_newer
@@ -675,6 +682,12 @@ impl Lock {
675682
value(self.options.prerelease_mode.to_string()),
676683
);
677684
}
685+
if self.options.fork_strategy != ForkStrategy::default() {
686+
options_table.insert(
687+
"fork-strategy",
688+
value(self.options.fork_strategy.to_string()),
689+
);
690+
}
678691
if let Some(exclude_newer) = self.options.exclude_newer {
679692
options_table.insert("exclude-newer", value(exclude_newer.to_string()));
680693
}
@@ -1317,6 +1330,9 @@ struct ResolverOptions {
13171330
/// The [`PrereleaseMode`] used to generate this lock.
13181331
#[serde(default)]
13191332
prerelease_mode: PrereleaseMode,
1333+
/// The [`ForkStrategy`] used to generate this lock.
1334+
#[serde(default)]
1335+
fork_strategy: ForkStrategy,
13201336
/// The [`ExcludeNewer`] used to generate this lock.
13211337
exclude_newer: Option<ExcludeNewer>,
13221338
}

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
source: crates/uv-resolver/src/lock/tests.rs
2+
source: crates/uv-resolver/src/lock/mod.rs
33
expression: result
44
---
55
Ok(
@@ -33,6 +33,7 @@ Ok(
3333
options: ResolverOptions {
3434
resolution_mode: Highest,
3535
prerelease_mode: IfNecessaryOrExplicit,
36+
fork_strategy: RequiresPython,
3637
exclude_newer: None,
3738
},
3839
packages: [

0 commit comments

Comments
 (0)