diff --git a/crates/platform-tags/src/tags.rs b/crates/platform-tags/src/tags.rs index b4db57da9d8a..e753c6c5ca2b 100644 --- a/crates/platform-tags/src/tags.rs +++ b/crates/platform-tags/src/tags.rs @@ -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 } } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index d83ab963cc48..86a12c4dc425 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -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), diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index af105522e598..416dfc2c2932 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -1,9 +1,9 @@ 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}; @@ -11,7 +11,7 @@ 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)] @@ -26,6 +26,7 @@ impl PubGrubDependencies { source_name: Option<&PackageName>, source_extra: Option<&ExtraName>, urls: &Urls, + locals: &Locals, env: &MarkerEnvironment, ) -> Result { let mut dependencies = Vec::default(); @@ -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?; @@ -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?; @@ -128,6 +129,7 @@ fn to_pubgrub( requirement: &Requirement, extra: Option, urls: &Urls, + locals: &Locals, ) -> Result<(PubGrubPackage, Range), ResolveError> { match requirement.version_or_url.as_ref() { // The requirement has no specifier (e.g., `flask`). @@ -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) diff --git a/crates/uv-resolver/src/resolver/locals.rs b/crates/uv-resolver/src/resolver/locals.rs new file mode 100644 index 000000000000..4bd363a05a81 --- /dev/null +++ b/crates/uv-resolver/src/resolver/locals.rs @@ -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, +} + +impl Locals { + pub(crate) fn from_manifest( + manifest: &Manifest, + markers: &MarkerEnvironment, + ) -> Result { + let mut required: FxHashMap = 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()) +} diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index a5ad108c206a..31ec19d03ca6 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -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; @@ -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, @@ -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), @@ -733,6 +737,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { None, None, &self.urls, + &self.locals, self.markers, ); @@ -808,6 +813,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { Some(package_name), extra.as_ref(), &self.urls, + &self.locals, self.markers, )?; @@ -864,6 +870,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { Some(package_name), extra.as_ref(), &self.urls, + &self.locals, self.markers, )?; diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 85075a38ae9b..874d561b4951 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -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 @@ -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. @@ -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 @@ -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'. diff --git a/requirements.in b/requirements.in new file mode 100644 index 000000000000..a29dbf99c037 --- /dev/null +++ b/requirements.in @@ -0,0 +1,2 @@ +torchvision==0.15.1+cu118 +torch==2.0.0+cu118