Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 15 additions & 9 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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]>;
Expand Down Expand Up @@ -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())),
)
}
};
Expand All @@ -238,15 +237,15 @@ 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],
installed_packages: &'a InstalledPackages,
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;
Expand Down Expand Up @@ -290,9 +289,16 @@ 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 is "global", i.e., it comes from a solve without a fork,
// or is provided via a lockfile, accept it unless pre-releases are completely
// banned.
AllowPrerelease::IfNecessary => match source {
PreferenceSource::Fork => false,
PreferenceSource::Lock
| PreferenceSource::Environment
| PreferenceSource::Resolve
| PreferenceSource::RequirementsTxt => true,
},
Comment on lines -293 to +299

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the crux of the change. I believe relying on marker.is_true() to determine if we should respect a preference was fallible for cases where the preference came an existing fork in the lockfile with markers attatched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think I remember / understand the "rather than a fork" / "comes from a solve without a fork" part, can you help me with that? In other words, why do we have both fork and resolve?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Previously we used marker.is_true() to capture the case where resolution.env.try_universal_markers() returns None (which indicates we're not in a fork) and is unwrapped to UniversalMarker::TRUE and propagated here. Now, we just move that up to https://github.com/astral-sh/uv/pull/14498/files#diff-6be6d80fe4821c47b70a372260f55e73b8da8182b8dcad7525d5cd3eb584532bR448-R452.

I don't actually know if we should do something different with PreferenceSource::Resolve, but this is intended to match the existing behavior in case it was important — I don't actually know the original motivation either. I could do some digging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like it's been around for a long time: #5736. I guess this was a proxy for detecting whether the pre-release came from a lock file? I suspect you could collapse the fork and resolve cases, and make them both false...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm down for trying it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The PR suggests we have scenarios to test this already, so worth trying I think.

};
if !allow {
continue;
Expand Down
30 changes: 30 additions & 0 deletions crates/uv-resolver/src/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -73,6 +75,7 @@ impl Preference {
.map(String::as_str)
.map(HashDigest::from_str)
.collect::<Result<_, _>>()?,
source: PreferenceSource::RequirementsTxt,
}))
}

Expand All @@ -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,
}))
}

Expand All @@ -112,6 +116,7 @@ impl Preference {
// `pylock.toml` doesn't have fork annotations.
fork_markers: vec![],
hashes: HashDigests::empty(),
source: PreferenceSource::Lock,
}))
}

Expand All @@ -127,6 +132,7 @@ impl Preference {
index: PreferenceIndex::Any,
fork_markers: vec![],
hashes: HashDigests::empty(),
source: PreferenceSource::Environment,
})
}

Expand Down Expand Up @@ -171,11 +177,26 @@ 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, without a fork.
Resolve,
/// The preference is from a fork in the current solve.
Fork,
}
Comment on lines +180 to +190

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A little annoyed by the number of source variants, but propagating them from the original location seemed better than assuming one variant during Preferences::from_iter and another during Preferences::insert (which was my first implementation)


#[derive(Debug, Clone)]
pub(crate) struct Entry {
marker: UniversalMarker,
index: PreferenceIndex,
pin: Pin,
source: PreferenceSource,
}

impl Entry {
Expand All @@ -193,6 +214,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.
Expand Down Expand Up @@ -245,6 +271,7 @@ impl Preferences {
version: preference.version,
hashes: preference.hashes,
},
source: preference.source,
});
} else {
for fork_marker in preference.fork_markers {
Expand All @@ -255,6 +282,7 @@ impl Preferences {
version: preference.version.clone(),
hashes: preference.hashes.clone(),
},
source: preference.source,
});
}
}
Expand All @@ -270,11 +298,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,
});
}

Expand Down
13 changes: 8 additions & 5 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -438,15 +438,18 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
self.options.resolution_mode,
ResolutionMode::Lowest | ResolutionMode::Highest
) {
let markers = resolution.env.try_universal_markers();
for (package, version) in &resolution.nodes {
preferences.insert(
package.name.clone(),
package.index.clone(),
resolution
.env
.try_universal_markers()
.unwrap_or(UniversalMarker::TRUE),
markers.unwrap_or(UniversalMarker::TRUE),
version.clone(),
if markers.is_none() {
PreferenceSource::Resolve
} else {
PreferenceSource::Fork
},
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
12 changes: 6 additions & 6 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1730,7 +1730,7 @@ pub(crate) async fn resolve_names(
}

#[derive(Debug, Clone)]
pub(crate) enum PreferenceSource<'lock> {
pub(crate) enum PreferenceLocation<'lock> {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This renamed to disambiguate from Source

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah that's good.

/// The preferences should be extracted from a lockfile.
Lock {
lock: &'lock Lock,
Expand All @@ -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<'_> {
Expand All @@ -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
Expand Down Expand Up @@ -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)?;

Expand All @@ -1881,7 +1881,7 @@ pub(crate) async fn resolve_environment(

preferences
}
Some(PreferenceSource::Entries(entries)) => entries,
Some(PreferenceLocation::Entries(entries)) => entries,
None => vec![],
};

Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading