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
87 changes: 70 additions & 17 deletions crates/uv-distribution-types/src/requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use uv_distribution_filename::DistExtension;
use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::VersionSpecifiers;
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{
MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker,
};
Expand Down Expand Up @@ -201,9 +201,9 @@ impl From<Requirement> for uv_pep508::Requirement<VerbatimUrl> {
marker: requirement.marker,
origin: requirement.origin,
version_or_url: match requirement.source {
RequirementSource::Registry { specifier, .. } => {
Some(VersionOrUrl::VersionSpecifier(specifier))
}
RequirementSource::Registry { specifier, .. } => Some(
VersionOrUrl::VersionSpecifier(specifier.to_version_specifiers()),
),
RequirementSource::Url { url, .. }
| RequirementSource::Git { url, .. }
| RequirementSource::Path { url, .. }
Expand All @@ -222,9 +222,9 @@ impl From<Requirement> for uv_pep508::Requirement<VerbatimParsedUrl> {
marker: requirement.marker,
origin: requirement.origin,
version_or_url: match requirement.source {
RequirementSource::Registry { specifier, .. } => {
Some(VersionOrUrl::VersionSpecifier(specifier))
}
RequirementSource::Registry { specifier, .. } => Some(
VersionOrUrl::VersionSpecifier(specifier.to_version_specifiers()),
),
RequirementSource::Url {
location,
subdirectory,
Expand Down Expand Up @@ -285,13 +285,13 @@ impl From<uv_pep508::Requirement<VerbatimParsedUrl>> for Requirement {
fn from(requirement: uv_pep508::Requirement<VerbatimParsedUrl>) -> Self {
let source = match requirement.version_or_url {
None => RequirementSource::Registry {
specifier: VersionSpecifiers::empty(),
specifier: VersionSpecifiersOrExact::VersionSpecifiers(VersionSpecifiers::empty()),
index: None,
conflict: None,
},
// The most popular case: just a name, a version range and maybe extras.
Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry {
specifier,
specifier: VersionSpecifiersOrExact::VersionSpecifiers(specifier),
index: None,
conflict: None,
},
Expand Down Expand Up @@ -393,6 +393,9 @@ impl CacheKey for Requirement {
conflict: _,
} => {
0u8.cache_key(state);
// TODO(konsti): We should use the information whether this is an exact specifier
// or not here, but this changes the cache.
let specifier = specifier.to_version_specifiers();
specifier.len().cache_key(state);
for spec in specifier.iter() {
spec.operator().as_str().cache_key(state);
Expand Down Expand Up @@ -466,6 +469,54 @@ impl CacheKey for Requirement {
}
}

/// A local version aware version specifier.
///
/// For checking installed requirements against a lockfile, we want to check that the local version
/// matches, too. When checking whether the specifier `=={version}` contains `{version}+{local}`,
/// the check will pass, which we avoid by doing an exact version comparison instead. An example
/// is torch `2.9.0` in the lockfile vs. `2.9.0+cu128` installed.
#[derive(Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum VersionSpecifiersOrExact {
VersionSpecifiers(VersionSpecifiers),
Exact(Version),
}

impl VersionSpecifiersOrExact {
/// Note that this conversion is lossy wrt to local version matching.
pub fn to_version_specifiers(&self) -> VersionSpecifiers {
match self {
Self::VersionSpecifiers(specifier) => specifier.clone(),
Self::Exact(version) => {
VersionSpecifiers::from(VersionSpecifier::equals_version(version.clone()))
}
}
}

pub fn is_empty(&self) -> bool {
match self {
Self::VersionSpecifiers(specifier) => specifier.is_empty(),
Self::Exact(_) => false,
}
}

pub fn contains(&self, other: &Version) -> bool {
match self {
Self::VersionSpecifiers(specifiers) => specifiers.contains(other),
// Compare including the version specifier
Self::Exact(version) => version == other,
}
}
}

impl Display for VersionSpecifiersOrExact {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::VersionSpecifiers(specifiers) => Display::fmt(specifiers, f),
Self::Exact(version) => write!(f, "=={version}"),
}
}
}

/// The different locations with can install a distribution from: Version specifier (from an index),
/// HTTP(S) URL, git repository, and path.
///
Expand All @@ -479,7 +530,7 @@ impl CacheKey for Requirement {
pub enum RequirementSource {
/// The requirement has a version specifier, such as `foo >1,<2`.
Registry {
specifier: VersionSpecifiers,
specifier: VersionSpecifiersOrExact,
/// Choose a version from the index at the given URL.
index: Option<IndexMetadata>,
/// The conflict item associated with the source, if any.
Expand Down Expand Up @@ -635,7 +686,9 @@ impl RequirementSource {
if specifier.is_empty() {
None
} else {
Some(VersionOrUrl::VersionSpecifier(specifier.clone()))
Some(VersionOrUrl::VersionSpecifier(
specifier.to_version_specifiers(),
))
}
}
Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
Expand Down Expand Up @@ -666,9 +719,9 @@ impl RequirementSource {
}

/// If the source is the registry, return the version specifiers
pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
pub fn version_specifiers(&self) -> Option<VersionSpecifiers> {
match self {
Self::Registry { specifier, .. } => Some(specifier),
Self::Registry { specifier, .. } => Some(specifier.to_version_specifiers()),
Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
None
}
Expand Down Expand Up @@ -817,7 +870,7 @@ impl From<RequirementSource> for RequirementSourceWire {
index
});
Self::Registry {
specifier,
specifier: specifier.to_version_specifiers(),
index,
conflict,
}
Expand Down Expand Up @@ -922,7 +975,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
index,
conflict,
} => Ok(Self::Registry {
specifier,
specifier: VersionSpecifiersOrExact::VersionSpecifiers(specifier),
index: index
.map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))),
conflict,
Expand Down Expand Up @@ -1049,7 +1102,7 @@ mod tests {

use uv_pep508::{MarkerTree, VerbatimUrl};

use crate::{Requirement, RequirementSource};
use crate::{Requirement, RequirementSource, VersionSpecifiersOrExact};

#[test]
fn roundtrip() {
Expand All @@ -1059,7 +1112,7 @@ mod tests {
groups: Box::new([]),
marker: MarkerTree::TRUE,
source: RequirementSource::Registry {
specifier: ">1,<2".parse().unwrap(),
specifier: VersionSpecifiersOrExact::VersionSpecifiers(">1,<2".parse().unwrap()),
index: None,
conflict: None,
},
Expand Down
15 changes: 4 additions & 11 deletions crates/uv-distribution-types/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use uv_pypi_types::{HashDigest, HashDigests};

use crate::{
BuiltDist, Diagnostic, Dist, IndexMetadata, Name, RequirementSource, ResolvedDist, SourceDist,
VersionSpecifiersOrExact,
};

/// A set of packages pinned at specific versions.
Expand Down Expand Up @@ -216,11 +217,7 @@ impl From<&ResolvedDist> for RequirementSource {
Dist::Built(BuiltDist::Registry(wheels)) => {
let wheel = wheels.best_wheel();
Self::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(
wheel.filename.version.clone(),
),
),
specifier: VersionSpecifiersOrExact::Exact(wheel.filename.version.clone()),
index: Some(IndexMetadata::from(wheel.index.clone())),
conflict: None,
}
Expand All @@ -241,9 +238,7 @@ impl From<&ResolvedDist> for RequirementSource {
ext: DistExtension::Wheel,
},
Dist::Source(SourceDist::Registry(sdist)) => Self::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()),
),
specifier: VersionSpecifiersOrExact::Exact(sdist.version.clone()),
index: Some(IndexMetadata::from(sdist.index.clone())),
conflict: None,
},
Expand Down Expand Up @@ -275,9 +270,7 @@ impl From<&ResolvedDist> for RequirementSource {
},
},
ResolvedDist::Installed { dist } => Self::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(dist.version().clone()),
),
specifier: VersionSpecifiersOrExact::Exact(dist.version().clone()),
index: None,
conflict: None,
},
Expand Down
7 changes: 4 additions & 3 deletions crates/uv-distribution/src/metadata/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use thiserror::Error;
use uv_distribution_filename::DistExtension;
use uv_distribution_types::{
Index, IndexLocations, IndexMetadata, IndexName, Origin, Requirement, RequirementSource,
VersionSpecifiersOrExact,
};
use uv_git_types::{GitReference, GitUrl, GitUrlParseError};
use uv_normalize::{ExtraName, GroupName, PackageName};
Expand Down Expand Up @@ -659,17 +660,17 @@ fn registry_source(
) -> RequirementSource {
match &requirement.version_or_url {
None => RequirementSource::Registry {
specifier: VersionSpecifiers::empty(),
specifier: VersionSpecifiersOrExact::VersionSpecifiers(VersionSpecifiers::empty()),
index: Some(index),
conflict,
},
Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
specifier: version.clone(),
specifier: VersionSpecifiersOrExact::VersionSpecifiers(version.clone()),
index: Some(index),
conflict,
},
Some(VersionOrUrl::Url(_)) => RequirementSource::Registry {
specifier: VersionSpecifiers::empty(),
specifier: VersionSpecifiersOrExact::VersionSpecifiers(VersionSpecifiers::empty()),
index: Some(index),
conflict,
},
Expand Down
1 change: 1 addition & 0 deletions crates/uv-installer/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ impl<'a> Planner<'a> {
[] => {}
[installed] => {
let source = RequirementSource::from(dist);

match RequirementSatisfaction::check(
dist.name(),
installed,
Expand Down
1 change: 1 addition & 0 deletions crates/uv-resolver/src/prerelease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl PrereleaseStrategy {
};

if specifier
.to_version_specifiers()
.iter()
.filter(|spec| {
!matches!(spec.operator(), Operator::NotEqual | Operator::NotEqualStar)
Expand Down
11 changes: 8 additions & 3 deletions crates/uv-resolver/src/pubgrub/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,12 @@ impl PubGrubRequirement {
) -> Self {
let (verbatim_url, parsed_url) = match &requirement.source {
RequirementSource::Registry { specifier, .. } => {
return Self::from_registry_requirement(specifier, extra, group, requirement);
return Self::from_registry_requirement(
specifier.to_version_specifiers(),
extra,
group,
requirement,
);
}
RequirementSource::Url {
subdirectory,
Expand Down Expand Up @@ -259,7 +264,7 @@ impl PubGrubRequirement {
}

fn from_registry_requirement(
specifier: &VersionSpecifiers,
specifier: VersionSpecifiers,
extra: Option<ExtraName>,
group: Option<GroupName>,
requirement: &Requirement,
Expand All @@ -272,7 +277,7 @@ impl PubGrubRequirement {
requirement.marker,
),
url: None,
version: Ranges::from(specifier.clone()),
version: Ranges::from(specifier),
}
}
}
1 change: 1 addition & 0 deletions crates/uv-resolver/src/yanks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl AllowedYanks {
let RequirementSource::Registry { specifier, .. } = &requirement.source else {
continue;
};
let specifier = specifier.to_version_specifiers();
let [specifier] = specifier.as_ref() else {
continue;
};
Expand Down
32 changes: 20 additions & 12 deletions crates/uv-types/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
use uv_configuration::HashCheckingMode;
use uv_distribution_types::{
DistributionMetadata, HashGeneration, HashPolicy, Name, Requirement, RequirementSource,
Resolution, UnresolvedRequirement, VersionId,
Resolution, UnresolvedRequirement, VersionId, VersionSpecifiersOrExact,
};
use uv_normalize::PackageName;
use uv_pep440::Version;
Expand Down Expand Up @@ -301,19 +301,27 @@ impl HashStrategy {
match &requirement.source {
RequirementSource::Registry { specifier, .. } => {
// Must be a single specifier.
let [specifier] = specifier.as_ref() else {
return None;
};
match specifier {
VersionSpecifiersOrExact::VersionSpecifiers(specifier) => {
let [specifier] = specifier.as_ref() else {
return None;
};

// Must be pinned to a specific version.
if *specifier.operator() != uv_pep440::Operator::Equal {
return None;
}
// Must be pinned to a specific version.
if *specifier.operator() != uv_pep440::Operator::Equal {
return None;
}

Some(VersionId::from_registry(
requirement.name.clone(),
specifier.version().clone(),
))
Some(VersionId::from_registry(
requirement.name.clone(),
specifier.version().clone(),
))
}
VersionSpecifiersOrExact::Exact(version) => Some(VersionId::from_registry(
requirement.name.clone(),
version.clone(),
)),
}
}
RequirementSource::Url { url, .. }
| RequirementSource::Git { url, .. }
Expand Down
12 changes: 6 additions & 6 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ use uv_configuration::{Concurrency, Constraints, DryRun, Reinstall, TargetTriple
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{
ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource,
UnresolvedRequirementSpecification,
UnresolvedRequirementSpecification, VersionSpecifiersOrExact,
};
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
use uv_pep440::VersionSpecifiers;
use uv_pep508::MarkerTree;
use uv_preview::Preview;
use uv_python::{
Expand Down Expand Up @@ -180,9 +180,7 @@ pub(crate) async fn install(
groups: Box::new([]),
marker: MarkerTree::default(),
source: RequirementSource::Registry {
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
version.clone(),
)),
specifier: VersionSpecifiersOrExact::Exact(version.clone()),
index: None,
conflict: None,
},
Expand All @@ -204,7 +202,9 @@ pub(crate) async fn install(
groups: Box::new([]),
marker: MarkerTree::default(),
source: RequirementSource::Registry {
specifier: VersionSpecifiers::empty(),
specifier: VersionSpecifiersOrExact::VersionSpecifiers(
VersionSpecifiers::empty(),
),
index: None,
conflict: None,
},
Expand Down
Loading
Loading