Skip to content

Commit de6d549

Browse files
committed
Centralize installed dist satisfies requirement check (#3324)
Another split out from #3263. This abstracts the copy&pasted check whether an installed distribution satisfies a requirement used by both plan.rs and site_packages.rs into a shared module. It's less useful here than with the new requirement but helps with reducing #3263 diff size.
1 parent 66d750b commit de6d549

File tree

5 files changed

+103
-58
lines changed

5 files changed

+103
-58
lines changed

crates/pep508-rs/src/lib.rs

+12-1
Original file line numberDiff line numberDiff line change
@@ -463,14 +463,25 @@ pub enum VersionOrUrl {
463463
}
464464

465465
/// Unowned version specifier or URL to install.
466-
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
466+
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
467467
pub enum VersionOrUrlRef<'a> {
468468
/// A PEP 440 version specifier set
469469
VersionSpecifier(&'a VersionSpecifiers),
470470
/// A installable URL
471471
Url(&'a VerbatimUrl),
472472
}
473473

474+
impl<'a> From<&'a VersionOrUrl> for VersionOrUrlRef<'a> {
475+
fn from(value: &'a VersionOrUrl) -> Self {
476+
match value {
477+
VersionOrUrl::VersionSpecifier(version_specifier) => {
478+
VersionOrUrlRef::VersionSpecifier(version_specifier)
479+
}
480+
VersionOrUrl::Url(url) => VersionOrUrlRef::Url(url),
481+
}
482+
}
483+
}
484+
474485
/// A [`Cursor`] over a string.
475486
#[derive(Debug, Clone)]
476487
pub struct Cursor<'a> {

crates/uv-installer/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ mod downloader;
1111
mod editable;
1212
mod installer;
1313
mod plan;
14+
mod satisfies;
1415
mod site_packages;
1516
mod uninstall;

crates/uv-installer/src/plan.rs

+20-56
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ use distribution_types::{
1010
BuiltDist, CachedDirectUrlDist, CachedDist, Dist, IndexLocations, InstalledDist,
1111
InstalledMetadata, InstalledVersion, Name, SourceDist,
1212
};
13-
use pep508_rs::{Requirement, VersionOrUrl};
13+
use pep508_rs::{Requirement, VersionOrUrl, VersionOrUrlRef};
1414
use platform_tags::Tags;
15-
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, WheelCache};
15+
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache};
1616
use uv_configuration::{NoBinary, Reinstall};
1717
use uv_distribution::{
1818
BuiltWheelIndex, HttpArchivePointer, LocalArchivePointer, RegistryWheelIndex,
@@ -21,6 +21,7 @@ use uv_fs::Simplified;
2121
use uv_interpreter::PythonEnvironment;
2222
use uv_types::HashStrategy;
2323

24+
use crate::satisfies::RequirementSatisfaction;
2425
use crate::{ResolvedEditable, SitePackages};
2526

2627
/// A planner to generate an [`Plan`] based on a set of requirements.
@@ -182,10 +183,23 @@ impl<'a> Planner<'a> {
182183
match installed_dists.as_slice() {
183184
[] => {}
184185
[distribution] => {
185-
if installed_satisfies_requirement(distribution, requirement)? {
186-
debug!("Requirement already installed: {distribution}");
187-
installed.push(distribution.clone());
188-
continue;
186+
match RequirementSatisfaction::check(
187+
distribution,
188+
requirement
189+
.version_or_url
190+
.as_ref()
191+
.map(VersionOrUrlRef::from),
192+
requirement,
193+
)? {
194+
RequirementSatisfaction::Mismatch => {}
195+
RequirementSatisfaction::Satisfied => {
196+
debug!("Requirement already installed: {distribution}");
197+
installed.push(distribution.clone());
198+
continue;
199+
}
200+
RequirementSatisfaction::OutOfDate => {
201+
debug!("Requirement installed, but not fresh: {distribution}");
202+
}
189203
}
190204
reinstalls.push(distribution.clone());
191205
}
@@ -416,53 +430,3 @@ pub struct Plan {
416430
/// _not_ necessary to satisfy the requirements.
417431
pub extraneous: Vec<InstalledDist>,
418432
}
419-
420-
/// Returns true if a requirement is satisfied by an installed distribution.
421-
///
422-
/// Returns an error if IO fails during a freshness check for a local path.
423-
fn installed_satisfies_requirement(
424-
distribution: &InstalledDist,
425-
requirement: &Requirement,
426-
) -> Result<bool> {
427-
// Filter out already-installed packages.
428-
match requirement.version_or_url.as_ref() {
429-
// Accept any version of the package.
430-
None => return Ok(true),
431-
432-
// If the requirement comes from a registry, check by name.
433-
Some(VersionOrUrl::VersionSpecifier(version_specifier)) => {
434-
if version_specifier.contains(distribution.version()) {
435-
debug!("Requirement already satisfied: {distribution}");
436-
return Ok(true);
437-
}
438-
}
439-
440-
// If the requirement comes from a direct URL, check by URL.
441-
Some(VersionOrUrl::Url(url)) => {
442-
if let InstalledDist::Url(installed) = &distribution {
443-
if !installed.editable && &installed.url == url.raw() {
444-
// If the requirement came from a local path, check freshness.
445-
if let Some(archive) = (url.scheme() == "file")
446-
.then(|| url.to_file_path().ok())
447-
.flatten()
448-
{
449-
if ArchiveTimestamp::up_to_date_with(
450-
&archive,
451-
ArchiveTarget::Install(distribution),
452-
)? {
453-
debug!("Requirement already satisfied (and up-to-date): {installed}");
454-
return Ok(true);
455-
}
456-
debug!("Requirement already satisfied (but not up-to-date): {installed}");
457-
} else {
458-
// Otherwise, assume the requirement is up-to-date.
459-
debug!("Requirement already satisfied (assumed up-to-date): {installed}");
460-
return Ok(true);
461-
}
462-
}
463-
}
464-
}
465-
}
466-
467-
Ok(false)
468-
}

crates/uv-installer/src/satisfies.rs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use anyhow::Result;
2+
use std::fmt::Debug;
3+
use tracing::trace;
4+
5+
use distribution_types::InstalledDist;
6+
use pep508_rs::VersionOrUrlRef;
7+
8+
use uv_cache::{ArchiveTarget, ArchiveTimestamp};
9+
10+
#[derive(Debug, Copy, Clone)]
11+
pub(crate) enum RequirementSatisfaction {
12+
Mismatch,
13+
Satisfied,
14+
OutOfDate,
15+
}
16+
17+
impl RequirementSatisfaction {
18+
/// Returns true if a requirement is satisfied by an installed distribution.
19+
///
20+
/// Returns an error if IO fails during a freshness check for a local path.
21+
pub(crate) fn check(
22+
distribution: &InstalledDist,
23+
version_or_url: Option<VersionOrUrlRef>,
24+
requirement: impl Debug,
25+
) -> Result<Self> {
26+
trace!(
27+
"Comparing installed with requirement: {:?} {:?}",
28+
distribution,
29+
requirement
30+
);
31+
// Filter out already-installed packages.
32+
match version_or_url {
33+
// Accept any version of the package.
34+
None => return Ok(Self::Satisfied),
35+
36+
// If the requirement comes from a registry, check by name.
37+
Some(VersionOrUrlRef::VersionSpecifier(version_specifier)) => {
38+
if version_specifier.contains(distribution.version()) {
39+
return Ok(Self::Satisfied);
40+
}
41+
}
42+
43+
// If the requirement comes from a direct URL, check by URL.
44+
Some(VersionOrUrlRef::Url(url)) => {
45+
if let InstalledDist::Url(installed) = &distribution {
46+
if !installed.editable && &installed.url == url.raw() {
47+
// If the requirement came from a local path, check freshness.
48+
return if let Some(archive) = (url.scheme() == "file")
49+
.then(|| url.to_file_path().ok())
50+
.flatten()
51+
{
52+
if ArchiveTimestamp::up_to_date_with(
53+
&archive,
54+
ArchiveTarget::Install(distribution),
55+
)? {
56+
return Ok(Self::Satisfied);
57+
}
58+
Ok(Self::OutOfDate)
59+
} else {
60+
// Otherwise, assume the requirement is up-to-date.
61+
Ok(Self::Satisfied)
62+
};
63+
}
64+
}
65+
}
66+
}
67+
68+
Ok(Self::Mismatch)
69+
}
70+
}

crates/uv-installer/src/site_packages.rs

-1
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,6 @@ impl<'a> SitePackages<'a> {
421421
}
422422
}
423423
}
424-
425424
// Validate that the installed version satisfies the constraints.
426425
for constraint in constraints {
427426
if constraint.name != *distribution.name() {

0 commit comments

Comments
 (0)