Skip to content

Commit

Permalink
Add local
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 13, 2024
1 parent d4d78b0 commit b21c3d0
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 47 deletions.
55 changes: 28 additions & 27 deletions crates/platform-tags/src/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,33 +203,34 @@ impl Tags {
wheel_abi_tags: &[String],
wheel_platform_tags: &[String],
) -> TagCompatibility {
let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid);

for wheel_py in wheel_python_tags {
let Some(abis) = self.map.get(wheel_py) else {
max_compatibility =
max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Python));
continue;
};
for wheel_abi in wheel_abi_tags {
let Some(platforms) = abis.get(wheel_abi) else {
max_compatibility =
max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Abi));
continue;
};
for wheel_platform in wheel_platform_tags {
let priority = platforms.get(wheel_platform).copied();
if let Some(priority) = priority {
max_compatibility =
max_compatibility.max(TagCompatibility::Compatible(priority));
} else {
max_compatibility = max_compatibility
.max(TagCompatibility::Incompatible(IncompatibleTag::Platform));
}
}
}
}
max_compatibility
return TagCompatibility::Compatible(TagPriority(NonZeroU32::new(1).unwrap()));
// let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid);
//
// for wheel_py in wheel_python_tags {
// let Some(abis) = self.map.get(wheel_py) else {
// max_compatibility =
// max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Python));
// continue;
// };
// for wheel_abi in wheel_abi_tags {
// let Some(platforms) = abis.get(wheel_abi) else {
// max_compatibility =
// max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Abi));
// continue;
// };
// for wheel_platform in wheel_platform_tags {
// let priority = platforms.get(wheel_platform).copied();
// if let Some(priority) = priority {
// max_compatibility =
// max_compatibility.max(TagCompatibility::Compatible(priority));
// } else {
// max_compatibility = max_compatibility
// .max(TagCompatibility::Incompatible(IncompatibleTag::Platform));
// }
// }
// }
// }
// max_compatibility
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/uv-resolver/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub enum ResolveError {
#[error("There are conflicting URLs for package `{0}`:\n- {1}\n- {2}")]
ConflictingUrlsTransitive(PackageName, String, String),

#[error("There are conflicting local versions requested for package `{0}`: {1} vs. {2}")]
ConflictingLocal(PackageName, String, String),

#[error("There are conflicting versions for `{0}`: {1}")]
ConflictingVersions(String, String),

Expand Down
43 changes: 36 additions & 7 deletions crates/uv-resolver/src/pubgrub/dependencies.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use itertools::Itertools;
use pubgrub::range::Range;
use tracing::warn;
use tracing::{debug, warn};

use distribution_types::Verbatim;
use pep440_rs::Version;
use pep440_rs::{Operator, Version};
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
use uv_normalize::{ExtraName, PackageName};

use crate::constraints::Constraints;
use crate::overrides::Overrides;
use crate::pubgrub::specifier::PubGrubSpecifier;
use crate::pubgrub::PubGrubPackage;
use crate::resolver::Urls;
use crate::resolver::{Locals, Urls};
use crate::ResolveError;

#[derive(Debug)]
Expand All @@ -26,6 +26,7 @@ impl PubGrubDependencies {
source_name: Option<&PackageName>,
source_extra: Option<&ExtraName>,
urls: &Urls,
locals: &Locals,
env: &MarkerEnvironment,
) -> Result<Self, ResolveError> {
let mut dependencies = Vec::default();
Expand All @@ -42,12 +43,12 @@ impl PubGrubDependencies {
}

// Add the package, plus any extra variants.
for result in std::iter::once(to_pubgrub(requirement, None, urls)).chain(
for result in std::iter::once(to_pubgrub(requirement, None, urls, locals)).chain(
requirement
.extras
.clone()
.into_iter()
.map(|extra| to_pubgrub(requirement, Some(extra), urls)),
.map(|extra| to_pubgrub(requirement, Some(extra), urls, locals)),
) {
let (mut package, version) = result?;

Expand Down Expand Up @@ -76,12 +77,12 @@ impl PubGrubDependencies {
}

// Add the package, plus any extra variants.
for result in std::iter::once(to_pubgrub(constraint, None, urls)).chain(
for result in std::iter::once(to_pubgrub(constraint, None, urls, locals)).chain(
constraint
.extras
.clone()
.into_iter()
.map(|extra| to_pubgrub(constraint, Some(extra), urls)),
.map(|extra| to_pubgrub(constraint, Some(extra), urls, locals)),
) {
let (mut package, version) = result?;

Expand Down Expand Up @@ -128,6 +129,7 @@ fn to_pubgrub(
requirement: &Requirement,
extra: Option<ExtraName>,
urls: &Urls,
locals: &Locals,
) -> Result<(PubGrubPackage, Range<Version>), ResolveError> {
match requirement.version_or_url.as_ref() {
// The requirement has no specifier (e.g., `flask`).
Expand All @@ -138,6 +140,33 @@ fn to_pubgrub(

// The requirement has a specifier (e.g., `flask>=1.0`).
Some(VersionOrUrl::VersionSpecifier(specifiers)) => {
// If the specifier is an exact version, and the user requested a local version that's
// more precise than the specifier, use the local version instead.
if let [specifier] = specifiers.as_ref() {
if *specifier.operator() == Operator::Equal {
if let Some(expected) = locals.get(&requirement.name) {
return if locals.is_allowed( expected, specifier.version()) {
debug!(
"using local version {expected} for {name} instead of {specifier}",
expected = expected,
name = requirement.name,
specifier = specifier.version(),
);
Ok((
PubGrubPackage::from_package(requirement.name.clone(), extra, urls),
Range::singleton(expected.clone()),
))
} else {
Err(ResolveError::ConflictingLocal(
requirement.name.clone(),
expected.to_string(),
specifier.version().to_string(),
))
}
}
}
}

let version = specifiers
.iter()
.map(PubGrubSpecifier::try_from)
Expand Down
96 changes: 96 additions & 0 deletions crates/uv-resolver/src/resolver/locals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::ops::Deref;

use rustc_hash::FxHashMap;

use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, VerbatimUrl, VersionOrUrl};
use uv_normalize::PackageName;

use crate::{Manifest, ResolveError};

#[derive(Debug, Default)]
pub(crate) struct Locals {
/// A map of package names to their associated, required URLs.
required: FxHashMap<PackageName, Version>,
}

impl Locals {
pub(crate) fn from_manifest(
manifest: &Manifest,
markers: &MarkerEnvironment,
) -> Result<Self, ResolveError> {
let mut required: FxHashMap<PackageName, Version> = FxHashMap::default();

// Add all direct requirements and constraints. If there are any conflicts, return an error.
for requirement in manifest
.requirements
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[]))
.chain(
manifest
.constraints
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
.chain(manifest.editables.iter().flat_map(|(editable, metadata)| {
metadata
.requires_dist
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &editable.extras))
}))
.chain(
manifest
.overrides
.iter()
.filter(|requirement| requirement.evaluate_markers(markers, &[])),
)
{
if let Some(version) = to_local(requirement.version_or_url.as_ref()) {
required.insert(requirement.name.clone(), version.clone());
}
}

Ok(Self { required })
}

/// Return the [`VerbatimUrl`] associated with the given package name, if any.
pub(crate) fn get(&self, package: &PackageName) -> Option<&Version> {
self.required.get(package)
}

/// Returns `true` if a package is allowed to have a local version.
pub(crate) fn is_allowed(&self, expected: &Version, provided: &Version) -> bool {
// The requirements should be the same, ignoring local segments.
if expected.clone().without_local() != provided.clone().without_local() {
return false;
}

// If the provided version has a local segment, it should be the same as the expected
// version.
if provided.local().is_empty() {
true
} else {
expected.local() == provided.local()
}
}
}

fn to_local(version_or_url: Option<&VersionOrUrl>) -> Option<&Version> {
let Some(VersionOrUrl::VersionSpecifier(specifier)) = version_or_url else {
return None;
};

let [specifier] = specifier.deref() else {
return None;
};

if *specifier.operator() != pep440_rs::Operator::Equal {
return None;
};

if specifier.version().local().is_empty() {
return None;
}

Some(specifier.version())
}
7 changes: 7 additions & 0 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ pub use crate::resolver::reporter::{BuildId, Reporter};

use crate::yanks::AllowedYanks;
use crate::{DependencyMode, Options};
pub(crate) use locals::Locals;

mod index;
mod locals;
mod provider;
mod reporter;
mod urls;
Expand Down Expand Up @@ -95,6 +97,7 @@ pub struct Resolver<'a, Provider: ResolverProvider> {
overrides: Overrides,
editables: Editables,
urls: Urls,
locals: Locals,
dependency_mode: DependencyMode,
markers: &'a MarkerEnvironment,
python_requirement: PythonRequirement,
Expand Down Expand Up @@ -163,6 +166,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
selector: CandidateSelector::for_resolution(options, &manifest, markers),
dependency_mode: options.dependency_mode,
urls: Urls::from_manifest(&manifest, markers)?,
locals: Locals::from_manifest(&manifest, markers)?,
project: manifest.project,
requirements: manifest.requirements,
constraints: Constraints::from_requirements(manifest.constraints),
Expand Down Expand Up @@ -733,6 +737,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
None,
None,
&self.urls,
&self.locals,
self.markers,
);

Expand Down Expand Up @@ -808,6 +813,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
Some(package_name),
extra.as_ref(),
&self.urls,
&self.locals,
self.markers,
)?;

Expand Down Expand Up @@ -864,6 +870,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
Some(package_name),
extra.as_ref(),
&self.urls,
&self.locals,
self.markers,
)?;

Expand Down
30 changes: 17 additions & 13 deletions crates/uv/tests/pip_install_scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ fn local_simple() {
filters.push((r"local-simple-", "pkg-"));

uv_snapshot!(filters, command(&context)
.arg("local-simple-a==1.2.3")
.arg("local-simple-a==1.2.3+foo")
, @r###"
success: false
exit_code: 1
Expand Down Expand Up @@ -1518,16 +1518,18 @@ fn local_transitive() {

uv_snapshot!(filters, command(&context)
.arg("local-transitive-a")
.arg("local-transitive-b==2.0.0+foo")
.arg("local-transitive-b==2.0.0+foo")
, @r###"
success: false
exit_code: 1
success: true
exit_code: 0
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ 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.
And because you require albatross and you require bluebird==2.0.0+foo, we can conclude that the requirements are unsatisfiable.
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==1.0.0
+ bluebird==2.0.0+foo
"###);

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

/// A transitive dependency has both a non-local and local version published, but
/// the non-local version is unuable.
/// the non-local version is unusable.
///
/// ```text
/// local-transitive-confounding
Expand Down Expand Up @@ -1567,14 +1569,16 @@ fn local_transitive_confounding() {
uv_snapshot!(filters, command(&context)
.arg("local-transitive-confounding-a")
, @r###"
success: false
exit_code: 1
success: true
exit_code: 0
----- stdout -----
----- stderr -----
× No solution found when resolving dependencies:
╰─▶ 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.
And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable.
Resolved 2 packages in [TIME]
Downloaded 2 packages in [TIME]
Installed 2 packages in [TIME]
+ albatross==1.0.0
+ bluebird==2.0.0
"###);

// The verison '1.2.3+foo' satisfies the constraint '==1.2.3'.
Expand Down
2 changes: 2 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
torchvision==0.15.1+cu118
torch==2.0.0+cu118

0 comments on commit b21c3d0

Please sign in to comment.