Fix handling of pre-releases in preferences (#14498)
Some checks are pending
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions

Closes https://github.com/astral-sh/uv/issues/14485

I tested this using the reproduction in the issue. It'd be nice to add
test coverage though.
This commit is contained in:
Zanie Blue 2025-07-07 20:10:35 -05:00 committed by GitHub
parent e31f556205
commit 7e48292fac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 53 additions and 20 deletions

View file

@ -13,10 +13,9 @@ use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_types::InstalledPackagesProvider;
use crate::preferences::{Entry, Preferences};
use crate::preferences::{Entry, PreferenceSource, Preferences};
use crate::prerelease::{AllowPrerelease, PrereleaseStrategy};
use crate::resolution_mode::ResolutionStrategy;
use crate::universal_marker::UniversalMarker;
use crate::version_map::{VersionMap, VersionMapDistHandle};
use crate::{Exclusions, Manifest, Options, ResolverEnvironment};
@ -188,7 +187,7 @@ impl CandidateSelector {
if index.is_some_and(|index| !entry.index().matches(index)) {
return None;
}
Either::Left(std::iter::once((entry.marker(), entry.pin().version())))
Either::Left(std::iter::once((entry.pin().version(), entry.source())))
}
[..] => {
type Entries<'a> = SmallVec<[&'a Entry; 3]>;
@ -219,7 +218,7 @@ impl CandidateSelector {
Either::Right(
preferences
.into_iter()
.map(|entry| (entry.marker(), entry.pin().version())),
.map(|entry| (entry.pin().version(), entry.source())),
)
}
};
@ -238,7 +237,7 @@ impl CandidateSelector {
/// Return the first preference that satisfies the current range and is allowed.
fn get_preferred_from_iter<'a, InstalledPackages: InstalledPackagesProvider>(
&'a self,
preferences: impl Iterator<Item = (&'a UniversalMarker, &'a Version)>,
preferences: impl Iterator<Item = (&'a Version, PreferenceSource)>,
package_name: &'a PackageName,
range: &Range<Version>,
version_maps: &'a [VersionMap],
@ -246,7 +245,7 @@ impl CandidateSelector {
reinstall: bool,
env: &ResolverEnvironment,
) -> Option<Candidate<'a>> {
for (marker, version) in preferences {
for (version, source) in preferences {
// Respect the version range for this requirement.
if !range.contains(version) {
continue;
@ -290,9 +289,14 @@ impl CandidateSelector {
let allow = match self.prerelease_strategy.allows(package_name, env) {
AllowPrerelease::Yes => true,
AllowPrerelease::No => false,
// If the pre-release is "global" (i.e., provided via a lockfile, rather than
// a fork), accept it unless pre-releases are completely banned.
AllowPrerelease::IfNecessary => marker.is_true(),
// If the pre-release was provided via an existing file, rather than from the
// current solve, accept it unless pre-releases are completely banned.
AllowPrerelease::IfNecessary => match source {
PreferenceSource::Resolver => false,
PreferenceSource::Lock
| PreferenceSource::Environment
| PreferenceSource::RequirementsTxt => true,
},
};
if !allow {
continue;

View file

@ -34,6 +34,8 @@ pub struct Preference {
/// is part of, otherwise `None`.
fork_markers: Vec<UniversalMarker>,
hashes: HashDigests,
/// The source of the preference.
source: PreferenceSource,
}
impl Preference {
@ -73,6 +75,7 @@ impl Preference {
.map(String::as_str)
.map(HashDigest::from_str)
.collect::<Result<_, _>>()?,
source: PreferenceSource::RequirementsTxt,
}))
}
@ -91,6 +94,7 @@ impl Preference {
index: PreferenceIndex::from(package.index(install_path)?),
fork_markers: package.fork_markers().to_vec(),
hashes: HashDigests::empty(),
source: PreferenceSource::Lock,
}))
}
@ -112,6 +116,7 @@ impl Preference {
// `pylock.toml` doesn't have fork annotations.
fork_markers: vec![],
hashes: HashDigests::empty(),
source: PreferenceSource::Lock,
}))
}
@ -127,6 +132,7 @@ impl Preference {
index: PreferenceIndex::Any,
fork_markers: vec![],
hashes: HashDigests::empty(),
source: PreferenceSource::Environment,
})
}
@ -171,11 +177,24 @@ impl From<Option<IndexUrl>> for PreferenceIndex {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PreferenceSource {
/// The preference is from an installed package in the environment.
Environment,
/// The preference is from a `uv.ock` file.
Lock,
/// The preference is from a `requirements.txt` file.
RequirementsTxt,
/// The preference is from the current solve.
Resolver,
}
#[derive(Debug, Clone)]
pub(crate) struct Entry {
marker: UniversalMarker,
index: PreferenceIndex,
pin: Pin,
source: PreferenceSource,
}
impl Entry {
@ -193,6 +212,11 @@ impl Entry {
pub(crate) fn pin(&self) -> &Pin {
&self.pin
}
/// Return the source of the entry.
pub(crate) fn source(&self) -> PreferenceSource {
self.source
}
}
/// A set of pinned packages that should be preserved during resolution, if possible.
@ -245,6 +269,7 @@ impl Preferences {
version: preference.version,
hashes: preference.hashes,
},
source: preference.source,
});
} else {
for fork_marker in preference.fork_markers {
@ -255,6 +280,7 @@ impl Preferences {
version: preference.version.clone(),
hashes: preference.hashes.clone(),
},
source: preference.source,
});
}
}
@ -270,11 +296,13 @@ impl Preferences {
index: Option<IndexUrl>,
markers: UniversalMarker,
pin: impl Into<Pin>,
source: PreferenceSource,
) {
self.0.entry(package_name).or_default().push(Entry {
marker: markers,
index: PreferenceIndex::from(index),
pin: pin.into(),
source,
});
}

View file

@ -47,7 +47,7 @@ use crate::fork_strategy::ForkStrategy;
use crate::fork_urls::ForkUrls;
use crate::manifest::Manifest;
use crate::pins::FilePins;
use crate::preferences::Preferences;
use crate::preferences::{PreferenceSource, Preferences};
use crate::pubgrub::{
PubGrubDependency, PubGrubDistribution, PubGrubPackage, PubGrubPackageInner, PubGrubPriorities,
PubGrubPython,
@ -447,6 +447,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
.try_universal_markers()
.unwrap_or(UniversalMarker::TRUE),
version.clone(),
PreferenceSource::Resolver,
);
}
}

View file

@ -933,7 +933,7 @@ impl ValidatedLock {
lock.prerelease_mode().cyan(),
options.prerelease_mode.cyan()
);
return Ok(Self::Unusable(lock));
return Ok(Self::Preferable(lock));
}
if lock.fork_strategy() != options.fork_strategy {
let _ = writeln!(

View file

@ -1730,7 +1730,7 @@ pub(crate) async fn resolve_names(
}
#[derive(Debug, Clone)]
pub(crate) enum PreferenceSource<'lock> {
pub(crate) enum PreferenceLocation<'lock> {
/// The preferences should be extracted from a lockfile.
Lock {
lock: &'lock Lock,
@ -1745,7 +1745,7 @@ pub(crate) struct EnvironmentSpecification<'lock> {
/// The requirements to include in the environment.
requirements: RequirementsSpecification,
/// The preferences to respect when resolving.
preferences: Option<PreferenceSource<'lock>>,
preferences: Option<PreferenceLocation<'lock>>,
}
impl From<RequirementsSpecification> for EnvironmentSpecification<'_> {
@ -1758,9 +1758,9 @@ impl From<RequirementsSpecification> for EnvironmentSpecification<'_> {
}
impl<'lock> EnvironmentSpecification<'lock> {
/// Set the [`PreferenceSource`] for the specification.
/// Set the [`PreferenceLocation`] for the specification.
#[must_use]
pub(crate) fn with_preferences(self, preferences: PreferenceSource<'lock>) -> Self {
pub(crate) fn with_preferences(self, preferences: PreferenceLocation<'lock>) -> Self {
Self {
preferences: Some(preferences),
..self
@ -1869,7 +1869,7 @@ pub(crate) async fn resolve_environment(
// If an existing lockfile exists, build up a set of preferences.
let preferences = match spec.preferences {
Some(PreferenceSource::Lock { lock, install_path }) => {
Some(PreferenceLocation::Lock { lock, install_path }) => {
let LockedRequirements { preferences, git } =
read_lock_requirements(lock, install_path, &upgrade)?;
@ -1881,7 +1881,7 @@ pub(crate) async fn resolve_environment(
preferences
}
Some(PreferenceSource::Entries(entries)) => entries,
Some(PreferenceLocation::Entries(entries)) => entries,
None => vec![],
};

View file

@ -49,7 +49,7 @@ use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::LockMode;
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
EnvironmentSpecification, PreferenceSource, ProjectEnvironment, ProjectError,
EnvironmentSpecification, PreferenceLocation, ProjectEnvironment, ProjectError,
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
default_dependency_groups, script_specification, update_environment,
validate_project_requires_python,
@ -958,10 +958,10 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
let spec = EnvironmentSpecification::from(spec).with_preferences(
if let Some((lock, install_path)) = base_lock.as_ref() {
// If we have a lockfile, use the locked versions as preferences.
PreferenceSource::Lock { lock, install_path }
PreferenceLocation::Lock { lock, install_path }
} else {
// Otherwise, extract preferences from the base environment.
PreferenceSource::Entries(
PreferenceLocation::Entries(
base_site_packages
.iter()
.filter_map(Preference::from_installed)