Skip to content

Commit b21c3d0

Browse files
committed
Add local
1 parent d4d78b0 commit b21c3d0

File tree

7 files changed

+189
-47
lines changed

7 files changed

+189
-47
lines changed

crates/platform-tags/src/tags.rs

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -203,33 +203,34 @@ impl Tags {
203203
wheel_abi_tags: &[String],
204204
wheel_platform_tags: &[String],
205205
) -> TagCompatibility {
206-
let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid);
207-
208-
for wheel_py in wheel_python_tags {
209-
let Some(abis) = self.map.get(wheel_py) else {
210-
max_compatibility =
211-
max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Python));
212-
continue;
213-
};
214-
for wheel_abi in wheel_abi_tags {
215-
let Some(platforms) = abis.get(wheel_abi) else {
216-
max_compatibility =
217-
max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Abi));
218-
continue;
219-
};
220-
for wheel_platform in wheel_platform_tags {
221-
let priority = platforms.get(wheel_platform).copied();
222-
if let Some(priority) = priority {
223-
max_compatibility =
224-
max_compatibility.max(TagCompatibility::Compatible(priority));
225-
} else {
226-
max_compatibility = max_compatibility
227-
.max(TagCompatibility::Incompatible(IncompatibleTag::Platform));
228-
}
229-
}
230-
}
231-
}
232-
max_compatibility
206+
return TagCompatibility::Compatible(TagPriority(NonZeroU32::new(1).unwrap()));
207+
// let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid);
208+
//
209+
// for wheel_py in wheel_python_tags {
210+
// let Some(abis) = self.map.get(wheel_py) else {
211+
// max_compatibility =
212+
// max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Python));
213+
// continue;
214+
// };
215+
// for wheel_abi in wheel_abi_tags {
216+
// let Some(platforms) = abis.get(wheel_abi) else {
217+
// max_compatibility =
218+
// max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Abi));
219+
// continue;
220+
// };
221+
// for wheel_platform in wheel_platform_tags {
222+
// let priority = platforms.get(wheel_platform).copied();
223+
// if let Some(priority) = priority {
224+
// max_compatibility =
225+
// max_compatibility.max(TagCompatibility::Compatible(priority));
226+
// } else {
227+
// max_compatibility = max_compatibility
228+
// .max(TagCompatibility::Incompatible(IncompatibleTag::Platform));
229+
// }
230+
// }
231+
// }
232+
// }
233+
// max_compatibility
233234
}
234235
}
235236

crates/uv-resolver/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ pub enum ResolveError {
5252
#[error("There are conflicting URLs for package `{0}`:\n- {1}\n- {2}")]
5353
ConflictingUrlsTransitive(PackageName, String, String),
5454

55+
#[error("There are conflicting local versions requested for package `{0}`: {1} vs. {2}")]
56+
ConflictingLocal(PackageName, String, String),
57+
5558
#[error("There are conflicting versions for `{0}`: {1}")]
5659
ConflictingVersions(String, String),
5760

crates/uv-resolver/src/pubgrub/dependencies.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use itertools::Itertools;
22
use pubgrub::range::Range;
3-
use tracing::warn;
3+
use tracing::{debug, warn};
44

55
use distribution_types::Verbatim;
6-
use pep440_rs::Version;
6+
use pep440_rs::{Operator, Version};
77
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
88
use uv_normalize::{ExtraName, PackageName};
99

1010
use crate::constraints::Constraints;
1111
use crate::overrides::Overrides;
1212
use crate::pubgrub::specifier::PubGrubSpecifier;
1313
use crate::pubgrub::PubGrubPackage;
14-
use crate::resolver::Urls;
14+
use crate::resolver::{Locals, Urls};
1515
use crate::ResolveError;
1616

1717
#[derive(Debug)]
@@ -26,6 +26,7 @@ impl PubGrubDependencies {
2626
source_name: Option<&PackageName>,
2727
source_extra: Option<&ExtraName>,
2828
urls: &Urls,
29+
locals: &Locals,
2930
env: &MarkerEnvironment,
3031
) -> Result<Self, ResolveError> {
3132
let mut dependencies = Vec::default();
@@ -42,12 +43,12 @@ impl PubGrubDependencies {
4243
}
4344

4445
// Add the package, plus any extra variants.
45-
for result in std::iter::once(to_pubgrub(requirement, None, urls)).chain(
46+
for result in std::iter::once(to_pubgrub(requirement, None, urls, locals)).chain(
4647
requirement
4748
.extras
4849
.clone()
4950
.into_iter()
50-
.map(|extra| to_pubgrub(requirement, Some(extra), urls)),
51+
.map(|extra| to_pubgrub(requirement, Some(extra), urls, locals)),
5152
) {
5253
let (mut package, version) = result?;
5354

@@ -76,12 +77,12 @@ impl PubGrubDependencies {
7677
}
7778

7879
// Add the package, plus any extra variants.
79-
for result in std::iter::once(to_pubgrub(constraint, None, urls)).chain(
80+
for result in std::iter::once(to_pubgrub(constraint, None, urls, locals)).chain(
8081
constraint
8182
.extras
8283
.clone()
8384
.into_iter()
84-
.map(|extra| to_pubgrub(constraint, Some(extra), urls)),
85+
.map(|extra| to_pubgrub(constraint, Some(extra), urls, locals)),
8586
) {
8687
let (mut package, version) = result?;
8788

@@ -128,6 +129,7 @@ fn to_pubgrub(
128129
requirement: &Requirement,
129130
extra: Option<ExtraName>,
130131
urls: &Urls,
132+
locals: &Locals,
131133
) -> Result<(PubGrubPackage, Range<Version>), ResolveError> {
132134
match requirement.version_or_url.as_ref() {
133135
// The requirement has no specifier (e.g., `flask`).
@@ -138,6 +140,33 @@ fn to_pubgrub(
138140

139141
// The requirement has a specifier (e.g., `flask>=1.0`).
140142
Some(VersionOrUrl::VersionSpecifier(specifiers)) => {
143+
// If the specifier is an exact version, and the user requested a local version that's
144+
// more precise than the specifier, use the local version instead.
145+
if let [specifier] = specifiers.as_ref() {
146+
if *specifier.operator() == Operator::Equal {
147+
if let Some(expected) = locals.get(&requirement.name) {
148+
return if locals.is_allowed( expected, specifier.version()) {
149+
debug!(
150+
"using local version {expected} for {name} instead of {specifier}",
151+
expected = expected,
152+
name = requirement.name,
153+
specifier = specifier.version(),
154+
);
155+
Ok((
156+
PubGrubPackage::from_package(requirement.name.clone(), extra, urls),
157+
Range::singleton(expected.clone()),
158+
))
159+
} else {
160+
Err(ResolveError::ConflictingLocal(
161+
requirement.name.clone(),
162+
expected.to_string(),
163+
specifier.version().to_string(),
164+
))
165+
}
166+
}
167+
}
168+
}
169+
141170
let version = specifiers
142171
.iter()
143172
.map(PubGrubSpecifier::try_from)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use std::ops::Deref;
2+
3+
use rustc_hash::FxHashMap;
4+
5+
use pep440_rs::Version;
6+
use pep508_rs::{MarkerEnvironment, VerbatimUrl, VersionOrUrl};
7+
use uv_normalize::PackageName;
8+
9+
use crate::{Manifest, ResolveError};
10+
11+
#[derive(Debug, Default)]
12+
pub(crate) struct Locals {
13+
/// A map of package names to their associated, required URLs.
14+
required: FxHashMap<PackageName, Version>,
15+
}
16+
17+
impl Locals {
18+
pub(crate) fn from_manifest(
19+
manifest: &Manifest,
20+
markers: &MarkerEnvironment,
21+
) -> Result<Self, ResolveError> {
22+
let mut required: FxHashMap<PackageName, Version> = FxHashMap::default();
23+
24+
// Add all direct requirements and constraints. If there are any conflicts, return an error.
25+
for requirement in manifest
26+
.requirements
27+
.iter()
28+
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
29+
.chain(
30+
manifest
31+
.constraints
32+
.iter()
33+
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
34+
)
35+
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
36+
metadata
37+
.requires_dist
38+
.iter()
39+
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
40+
}))
41+
.chain(
42+
manifest
43+
.overrides
44+
.iter()
45+
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
46+
)
47+
{
48+
if let Some(version) = to_local(requirement.version_or_url.as_ref()) {
49+
required.insert(requirement.name.clone(), version.clone());
50+
}
51+
}
52+
53+
Ok(Self { required })
54+
}
55+
56+
/// Return the [`VerbatimUrl`] associated with the given package name, if any.
57+
pub(crate) fn get(&self, package: &PackageName) -> Option<&Version> {
58+
self.required.get(package)
59+
}
60+
61+
/// Returns `true` if a package is allowed to have a local version.
62+
pub(crate) fn is_allowed(&self, expected: &Version, provided: &Version) -> bool {
63+
// The requirements should be the same, ignoring local segments.
64+
if expected.clone().without_local() != provided.clone().without_local() {
65+
return false;
66+
}
67+
68+
// If the provided version has a local segment, it should be the same as the expected
69+
// version.
70+
if provided.local().is_empty() {
71+
true
72+
} else {
73+
expected.local() == provided.local()
74+
}
75+
}
76+
}
77+
78+
fn to_local(version_or_url: Option<&VersionOrUrl>) -> Option<&Version> {
79+
let Some(VersionOrUrl::VersionSpecifier(specifier)) = version_or_url else {
80+
return None;
81+
};
82+
83+
let [specifier] = specifier.deref() else {
84+
return None;
85+
};
86+
87+
if *specifier.operator() != pep440_rs::Operator::Equal {
88+
return None;
89+
};
90+
91+
if specifier.version().local().is_empty() {
92+
return None;
93+
}
94+
95+
Some(specifier.version())
96+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ pub use crate::resolver::reporter::{BuildId, Reporter};
5656

5757
use crate::yanks::AllowedYanks;
5858
use crate::{DependencyMode, Options};
59+
pub(crate) use locals::Locals;
5960

6061
mod index;
62+
mod locals;
6163
mod provider;
6264
mod reporter;
6365
mod urls;
@@ -95,6 +97,7 @@ pub struct Resolver<'a, Provider: ResolverProvider> {
9597
overrides: Overrides,
9698
editables: Editables,
9799
urls: Urls,
100+
locals: Locals,
98101
dependency_mode: DependencyMode,
99102
markers: &'a MarkerEnvironment,
100103
python_requirement: PythonRequirement,
@@ -163,6 +166,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
163166
selector: CandidateSelector::for_resolution(options, &manifest, markers),
164167
dependency_mode: options.dependency_mode,
165168
urls: Urls::from_manifest(&manifest, markers)?,
169+
locals: Locals::from_manifest(&manifest, markers)?,
166170
project: manifest.project,
167171
requirements: manifest.requirements,
168172
constraints: Constraints::from_requirements(manifest.constraints),
@@ -733,6 +737,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
733737
None,
734738
None,
735739
&self.urls,
740+
&self.locals,
736741
self.markers,
737742
);
738743

@@ -808,6 +813,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
808813
Some(package_name),
809814
extra.as_ref(),
810815
&self.urls,
816+
&self.locals,
811817
self.markers,
812818
)?;
813819

@@ -864,6 +870,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
864870
Some(package_name),
865871
extra.as_ref(),
866872
&self.urls,
873+
&self.locals,
867874
self.markers,
868875
)?;
869876

crates/uv/tests/pip_install_scenarios.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,7 @@ fn local_simple() {
13331333
filters.push((r"local-simple-", "pkg-"));
13341334

13351335
uv_snapshot!(filters, command(&context)
1336-
.arg("local-simple-a==1.2.3")
1336+
.arg("local-simple-a==1.2.3+foo")
13371337
, @r###"
13381338
success: false
13391339
exit_code: 1
@@ -1518,16 +1518,18 @@ fn local_transitive() {
15181518

15191519
uv_snapshot!(filters, command(&context)
15201520
.arg("local-transitive-a")
1521-
.arg("local-transitive-b==2.0.0+foo")
1521+
.arg("local-transitive-b==2.0.0+foo")
15221522
, @r###"
1523-
success: false
1524-
exit_code: 1
1523+
success: true
1524+
exit_code: 0
15251525
----- stdout -----
15261526
15271527
----- stderr -----
1528-
× No solution found when resolving dependencies:
1529-
╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on bluebird==2.0.0, we can conclude that all versions of albatross depend on bluebird==2.0.0.
1530-
And because you require albatross and you require bluebird==2.0.0+foo, we can conclude that the requirements are unsatisfiable.
1528+
Resolved 2 packages in [TIME]
1529+
Downloaded 2 packages in [TIME]
1530+
Installed 2 packages in [TIME]
1531+
+ albatross==1.0.0
1532+
+ bluebird==2.0.0+foo
15311533
"###);
15321534

15331535
// The verison '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo.
@@ -1536,7 +1538,7 @@ fn local_transitive() {
15361538
}
15371539

15381540
/// A transitive dependency has both a non-local and local version published, but
1539-
/// the non-local version is unuable.
1541+
/// the non-local version is unusable.
15401542
///
15411543
/// ```text
15421544
/// local-transitive-confounding
@@ -1567,14 +1569,16 @@ fn local_transitive_confounding() {
15671569
uv_snapshot!(filters, command(&context)
15681570
.arg("local-transitive-confounding-a")
15691571
, @r###"
1570-
success: false
1571-
exit_code: 1
1572+
success: true
1573+
exit_code: 0
15721574
----- stdout -----
15731575
15741576
----- stderr -----
1575-
× No solution found when resolving dependencies:
1576-
╰─▶ Because bluebird==2.0.0 is unusable because no wheels are available with a matching Python ABI and albatross==1.0.0 depends on bluebird==2.0.0, we can conclude that albatross==1.0.0 cannot be used.
1577-
And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable.
1577+
Resolved 2 packages in [TIME]
1578+
Downloaded 2 packages in [TIME]
1579+
Installed 2 packages in [TIME]
1580+
+ albatross==1.0.0
1581+
+ bluebird==2.0.0
15781582
"###);
15791583

15801584
// The verison '1.2.3+foo' satisfies the constraint '==1.2.3'.

requirements.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
torchvision==0.15.1+cu118
2+
torch==2.0.0+cu118

0 commit comments

Comments
 (0)