From edf39b3809b7579ac34d3f590b4c50138bac4162 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 23 Nov 2025 15:57:23 -0500 Subject: [PATCH 1/2] Add requires python --- .../src/requires_python.rs | 220 ++++++++++++++++++ .../src/supported_environments.rs | 10 + .../uv-resolver/src/pubgrub/dependencies.rs | 39 +++- crates/uv-resolver/src/resolver/mod.rs | 58 ++++- crates/uv/tests/it/lock.rs | 133 +++++++++++ foo/.python-version | 1 + foo/README.md | 0 foo/main.py | 6 + foo/pyproject.toml | 12 + foo/uv.lock | 11 + 10 files changed, 476 insertions(+), 14 deletions(-) create mode 100644 foo/.python-version create mode 100644 foo/README.md create mode 100644 foo/main.py create mode 100644 foo/pyproject.toml create mode 100644 foo/uv.lock diff --git a/crates/uv-distribution-types/src/requires_python.rs b/crates/uv-distribution-types/src/requires_python.rs index eb46021a3d961..ee389a27be799 100644 --- a/crates/uv-distribution-types/src/requires_python.rs +++ b/crates/uv-distribution-types/src/requires_python.rs @@ -371,6 +371,180 @@ impl RequiresPython { marker.complexify_python_versions(lower.as_ref(), upper.as_ref()) } + /// Extract the Python version requirement from a wheel's tags. + /// + /// Returns a [`VersionSpecifiers`] indicating the required Python version range based on the + /// wheel's tags. Returns `None` if no Python version requirement can be determined or if the + /// wheel uses Python 2. + /// + /// This is similar to the logic in `implied_python_markers` (from `prioritized_distribution`) + /// but returns version specifiers instead of a marker tree. + /// + /// # Examples + /// + /// ``` + /// use std::str::FromStr; + /// use uv_distribution_filename::WheelFilename; + /// use uv_distribution_types::RequiresPython; + /// + /// // Implementation-specific tags indicate exact Python versions + /// let wheel = WheelFilename::from_str("black-24.4.2-cp310-cp310-win_amd64.whl").unwrap(); + /// let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + /// assert_eq!(specifiers.to_string(), "==3.10.*"); + /// + /// // Generic Python tags indicate lower bounds + /// let wheel = WheelFilename::from_str("package-1.0-py3-none-any.whl").unwrap(); + /// let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + /// assert_eq!(specifiers.to_string(), ">=3"); + /// + /// // Python 2 wheels return None + /// let wheel = WheelFilename::from_str("old_package-1.0-py27-none-any.whl").unwrap(); + /// assert_eq!(RequiresPython::from_wheel(&wheel), None); + /// ``` + pub fn from_wheel(wheel: &WheelFilename) -> Option { + let mut lower_bound: Option = None; + let mut upper_bound: Option = None; + + for python_tag in wheel.python_tags() { + match python_tag { + // Reject Python 2 wheels + LanguageTag::Python { major: 2, .. } + | LanguageTag::CPython { + python_version: (2, ..), + } + | LanguageTag::PyPy { + python_version: (2, ..), + } + | LanguageTag::GraalPy { + python_version: (2, ..), + } + | LanguageTag::Pyston { + python_version: (2, ..), + } => { + return None; + } + // Generic Python tag with just major version (e.g., py3) + LanguageTag::Python { + major: 3, + minor: None, + } => { + // py3 means >=3.0 + lower_bound = Some(Version::new([3])); + } + // Generic Python tag with major.minor (e.g., py310) + // These represent lower bounds: py310 means >=3.10 + LanguageTag::Python { + major: 3, + minor: Some(minor), + } => { + let version = Version::new([3, u64::from(*minor)]); + lower_bound = Some(match &lower_bound { + Some(existing) if existing < &version => existing.clone(), + _ => version, + }); + } + // Implementation-specific tags (e.g., cp310, pp39) + // These represent exact minor versions: cp310 means ==3.10.* + LanguageTag::CPython { + python_version: (3, minor), + } + | LanguageTag::PyPy { + python_version: (3, minor), + } + | LanguageTag::GraalPy { + python_version: (3, minor), + } + | LanguageTag::Pyston { + python_version: (3, minor), + } => { + let version = Version::new([3, u64::from(*minor)]); + lower_bound = Some(match &lower_bound { + Some(existing) if existing < &version => existing.clone(), + _ => version.clone(), + }); + upper_bound = Some(match &upper_bound { + Some(existing) if existing > &version => existing.clone(), + _ => version, + }); + } + _ => { + // Unknown or None tags + } + } + } + + // Also check ABI tags for version information + for abi_tag in wheel.abi_tags() { + match abi_tag { + // Reject Python 2 ABI tags + AbiTag::CPython { + python_version: (2, ..), + .. + } + | AbiTag::PyPy { + python_version: Some((2, ..)), + .. + } + | AbiTag::GraalPy { + python_version: (2, ..), + .. + } => { + return None; + } + // Python 3 ABI tags indicate exact versions + AbiTag::CPython { + python_version: (3, minor), + .. + } + | AbiTag::PyPy { + python_version: Some((3, minor)), + .. + } + | AbiTag::GraalPy { + python_version: (3, minor), + .. + } => { + let version = Version::new([3, u64::from(*minor)]); + lower_bound = Some(match &lower_bound { + Some(existing) if existing < &version => existing.clone(), + _ => version.clone(), + }); + upper_bound = Some(match &upper_bound { + Some(existing) if existing > &version => existing.clone(), + _ => version, + }); + } + _ => { + // abi3, None, or other tags don't provide version constraints + } + } + } + + // Build the version specifiers based on the bounds we found + match (lower_bound, upper_bound) { + (Some(lower), Some(upper)) if lower == upper => { + // Exact version match: ==3.X.* + Some(VersionSpecifiers::from( + VersionSpecifier::equals_star_version(lower), + )) + } + (Some(lower), Some(upper)) if lower < upper => { + // Multiple exact versions: >=3.X, <=3.Y + Some(VersionSpecifiers::from_iter([ + VersionSpecifier::greater_than_equal_version(lower), + VersionSpecifier::less_than_equal_version(upper), + ])) + } + (Some(lower), None) => { + // Just a lower bound: >=3.X + Some(VersionSpecifiers::from( + VersionSpecifier::greater_than_equal_version(lower), + )) + } + _ => None, + } + } + /// Returns `false` if the wheel's tags state it can't be used in the given Python version /// range. /// @@ -774,6 +948,52 @@ mod tests { } } + #[test] + fn from_wheel_tags() { + // Test implementation-specific tags (exact versions) + let wheel = WheelFilename::from_str("black-24.4.2-cp310-cp310-win_amd64.whl").unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), "==3.10.*"); + + let wheel = WheelFilename::from_str("dearpygui-1.11.1-cp312-cp312-win_amd64.whl").unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), "==3.12.*"); + + // Test generic Python tags (lower bounds) + let wheel = WheelFilename::from_str("cbor2-5.6.4-py3-none-any.whl").unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), ">=3"); + + let wheel = WheelFilename::from_str("example-1.0-py310-none-any.whl").unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), ">=3.10"); + + // Test abi3 wheels + let wheel = + WheelFilename::from_str("bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl").unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), "==3.7.*"); + + // Test PyPy tags + let wheel = + WheelFilename::from_str("watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl") + .unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), "==3.10.*"); + + // Test multiple python tags (should create a range) + let wheel = WheelFilename::from_str("example-1.0-cp310.cp311-none-any.whl").unwrap(); + let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); + assert_eq!(specifiers.to_string(), ">=3.10, <=3.11"); + + // Test Python 2 wheels (should return None) + let wheel = WheelFilename::from_str("PySocks-1.7.1-py27-none-any.whl").unwrap(); + assert_eq!(RequiresPython::from_wheel(&wheel), None); + + let wheel = WheelFilename::from_str("psutil-6.0.0-cp27-none-win32.whl").unwrap(); + assert_eq!(RequiresPython::from_wheel(&wheel), None); + } + #[test] fn split_version() { // Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`. diff --git a/crates/uv-pypi-types/src/supported_environments.rs b/crates/uv-pypi-types/src/supported_environments.rs index 59cbbf23be8e4..8a1299dac027f 100644 --- a/crates/uv-pypi-types/src/supported_environments.rs +++ b/crates/uv-pypi-types/src/supported_environments.rs @@ -28,6 +28,16 @@ impl SupportedEnvironments { pub fn iter(&self) -> std::slice::Iter<'_, MarkerTree> { self.0.iter() } + + /// Returns `true` if there are no supported environments. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the number of supported environments. + pub fn len(&self) -> usize { + self.0.len() + } } impl<'a> IntoIterator for &'a SupportedEnvironments { diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 7ef848ab26a23..e1301207cdeea 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -243,18 +243,37 @@ impl PubGrubRequirement { } }; + let package = PubGrubPackage::from_package( + requirement.name.clone(), + extra, + group, + requirement.marker, + ); + let url = VerbatimParsedUrl { + parsed_url, + verbatim: verbatim_url.clone(), + }; + + // In theory this can help a little bit because... we know the exact version, so if we visit + // some other distribution earlier, we have this information earlier to narrow our choices. + // TODO(charlie): We can just do this with the filename, though -- we don't need to do `Dist::from_url`. + // TODO(charlie): Does this mean that we'd then have a problem if the filename and the + // metadata don't match? + // let version = if let Ok(Dist::Built(dist)) = Dist::from_url(requirement.name.clone(), url.clone()) { + // let version = match dist { + // BuiltDist::DirectUrl(dist) => dist.filename.version, + // BuiltDist::Path(dist) => dist.filename.version, + // BuiltDist::Registry(..) => unreachable!() + // }; + // Ranges::singleton(version) + // } else { + // Ranges::full() + // }; + Self { - package: PubGrubPackage::from_package( - requirement.name.clone(), - extra, - group, - requirement.marker, - ), + package, version: Ranges::full(), - url: Some(VerbatimParsedUrl { - parsed_url, - verbatim: verbatim_url.clone(), - }), + url: Some(url), } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index e893a82b059b7..bb75f5b00bd27 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -26,7 +26,7 @@ use uv_distribution_types::{ BuiltDist, CompatibleDist, DerivationChain, Dist, DistErrorKind, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations, IndexMetadata, IndexUrl, InstalledDist, Name, PythonRequirementKind, RemoteSource, Requirement, - ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, + ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, implied_markers, }; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -34,7 +34,7 @@ use uv_pep440::{MIN_VERSION, Version, VersionSpecifiers, release_specifiers_to_r use uv_pep508::{ MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString, }; -use uv_platform_tags::Tags; +use uv_platform_tags::{IncompatibleTag, Tags}; use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictKindRef, Conflicts, VerbatimParsedUrl}; use uv_torch::TorchStrategy; use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider}; @@ -1109,7 +1109,7 @@ impl ResolverState { if let Some(url) = package.name().and_then(|name| fork_urls.get(name)) { - self.choose_version_url(name, range, url, python_requirement) + self.choose_version_url(id, name, range, url, env, python_requirement, pubgrub) } else { self.choose_version_registry( package, @@ -1134,10 +1134,13 @@ impl ResolverState, name: &PackageName, range: &Range, url: &VerbatimParsedUrl, + env: &ResolverEnvironment, python_requirement: &PythonRequirement, + pubgrub: &State, ) -> Result, ResolveError> { debug!( "Searching for a compatible version of {name} @ {} ({range})", @@ -1178,8 +1181,53 @@ impl ResolverState &dist.best_wheel().filename, + BuiltDist::DirectUrl(dist) => &dist.filename, + BuiltDist::Path(dist) => &dist.filename, + }; + + // If the wheel does _not_ cover a required platform, it's incompatible. + if env.marker_environment().is_none() && !self.options.required_environments.is_empty() + { + let wheel_marker = implied_markers(filename); + // If the user explicitly marked a platform as required, ensure it has coverage. + for environment_marker in self.options.required_environments.iter().copied() { + // If the platform is part of the current environment... + if env.included_by_marker(environment_marker) + && !find_environments(id, pubgrub).is_disjoint(environment_marker) + { + // ...but the wheel doesn't support it, it's incompatible. + if wheel_marker.is_disjoint(environment_marker) { + return Ok(Some(ResolverVersion::Unavailable( + version.clone(), + UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel( + IncompatibleWheel::MissingPlatform(environment_marker), + )), + ))); + } + } + } + } + + // If the wheel's Python tag doesn't match the target Python, it's incompatible. + if !python_requirement.target().matches_wheel_tag(filename) { + return Ok(Some(ResolverVersion::Unavailable( + filename.version.clone(), + UnavailableVersion::IncompatibleDist(IncompatibleDist::Wheel( + IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion), + )), + ))); + } + } + + // The version is incompatible due to its `Requires-Python` requirement. if let Some(requires_python) = metadata.requires_python.as_ref() { + // TODO(charlie): We only care about this for source distributions. if !python_requirement .installed() .is_contained_by(requires_python) @@ -1207,6 +1255,8 @@ impl ResolverState Result<()> { Ok(()) } + +#[test] +fn lock_unsupported_wheel_url_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["numpy @ https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only numpy==2.3.5 is available and numpy==2.3.5 has no wheels with a matching Python version tag (e.g., `cp312`), we can conclude that all versions of numpy cannot be used. + And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable. + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "numpy" + version = "2.3.5" + source = { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" } + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "numpy" }, + ] + + [package.metadata] + requires-dist = [{ name = "numpy", url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" }] + "# + ); + }); + + Ok(()) +} + +#[test] +fn lock_unsupported_wheel_url_required_platform() -> Result<()> { + let context = TestContext::new("3.11"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = ["numpy @ https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl"] + + [tool.uv] + required-environments = ["sys_platform == 'win32'"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only numpy==2.3.5 is available and numpy==2.3.5 has no Windows-compatible wheels, we can conclude that all versions of numpy cannot be used. + And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable. + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.11" + required-markers = [ + "sys_platform == 'win32'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "numpy" + version = "2.3.5" + source = { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "numpy" }, + ] + + [package.metadata] + requires-dist = [{ name = "numpy", url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" }] + "# + ); + }); + + Ok(()) +} diff --git a/foo/.python-version b/foo/.python-version new file mode 100644 index 0000000000000..f982feb41bd00 --- /dev/null +++ b/foo/.python-version @@ -0,0 +1 @@ +3.14.0 diff --git a/foo/README.md b/foo/README.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/foo/main.py b/foo/main.py new file mode 100644 index 0000000000000..f8a203dd42159 --- /dev/null +++ b/foo/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from foo!") + + +if __name__ == "__main__": + main() diff --git a/foo/pyproject.toml b/foo/pyproject.toml new file mode 100644 index 0000000000000..0ba7344d5e3bd --- /dev/null +++ b/foo/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "foo" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14.0" +dependencies = [ + "triton", +] + +[tool.uv.sources] +triton = { path = "../../../Downloads/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" } diff --git a/foo/uv.lock b/foo/uv.lock new file mode 100644 index 0000000000000..a6606aa317240 --- /dev/null +++ b/foo/uv.lock @@ -0,0 +1,11 @@ +version = 1 +revision = 3 +requires-python = ">=3.14.0" + +[[package]] +name = "foo" +version = "0.1.0" +source = { virtual = "." } + +[package.metadata] +requires-dist = [{ name = "triton", path = "../../../Downloads/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" }] From 3542bfb05e7c6a177d660f79314af36caa1c2039 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 23 Nov 2025 16:06:39 -0500 Subject: [PATCH 2/2] Reverts --- .../src/requires_python.rs | 220 ------------------ .../uv-resolver/src/pubgrub/dependencies.rs | 39 +--- crates/uv/tests/it/lock.rs | 72 ------ foo/.python-version | 1 - foo/README.md | 0 foo/main.py | 6 - foo/pyproject.toml | 12 - foo/uv.lock | 11 - 8 files changed, 10 insertions(+), 351 deletions(-) delete mode 100644 foo/.python-version delete mode 100644 foo/README.md delete mode 100644 foo/main.py delete mode 100644 foo/pyproject.toml delete mode 100644 foo/uv.lock diff --git a/crates/uv-distribution-types/src/requires_python.rs b/crates/uv-distribution-types/src/requires_python.rs index ee389a27be799..eb46021a3d961 100644 --- a/crates/uv-distribution-types/src/requires_python.rs +++ b/crates/uv-distribution-types/src/requires_python.rs @@ -371,180 +371,6 @@ impl RequiresPython { marker.complexify_python_versions(lower.as_ref(), upper.as_ref()) } - /// Extract the Python version requirement from a wheel's tags. - /// - /// Returns a [`VersionSpecifiers`] indicating the required Python version range based on the - /// wheel's tags. Returns `None` if no Python version requirement can be determined or if the - /// wheel uses Python 2. - /// - /// This is similar to the logic in `implied_python_markers` (from `prioritized_distribution`) - /// but returns version specifiers instead of a marker tree. - /// - /// # Examples - /// - /// ``` - /// use std::str::FromStr; - /// use uv_distribution_filename::WheelFilename; - /// use uv_distribution_types::RequiresPython; - /// - /// // Implementation-specific tags indicate exact Python versions - /// let wheel = WheelFilename::from_str("black-24.4.2-cp310-cp310-win_amd64.whl").unwrap(); - /// let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - /// assert_eq!(specifiers.to_string(), "==3.10.*"); - /// - /// // Generic Python tags indicate lower bounds - /// let wheel = WheelFilename::from_str("package-1.0-py3-none-any.whl").unwrap(); - /// let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - /// assert_eq!(specifiers.to_string(), ">=3"); - /// - /// // Python 2 wheels return None - /// let wheel = WheelFilename::from_str("old_package-1.0-py27-none-any.whl").unwrap(); - /// assert_eq!(RequiresPython::from_wheel(&wheel), None); - /// ``` - pub fn from_wheel(wheel: &WheelFilename) -> Option { - let mut lower_bound: Option = None; - let mut upper_bound: Option = None; - - for python_tag in wheel.python_tags() { - match python_tag { - // Reject Python 2 wheels - LanguageTag::Python { major: 2, .. } - | LanguageTag::CPython { - python_version: (2, ..), - } - | LanguageTag::PyPy { - python_version: (2, ..), - } - | LanguageTag::GraalPy { - python_version: (2, ..), - } - | LanguageTag::Pyston { - python_version: (2, ..), - } => { - return None; - } - // Generic Python tag with just major version (e.g., py3) - LanguageTag::Python { - major: 3, - minor: None, - } => { - // py3 means >=3.0 - lower_bound = Some(Version::new([3])); - } - // Generic Python tag with major.minor (e.g., py310) - // These represent lower bounds: py310 means >=3.10 - LanguageTag::Python { - major: 3, - minor: Some(minor), - } => { - let version = Version::new([3, u64::from(*minor)]); - lower_bound = Some(match &lower_bound { - Some(existing) if existing < &version => existing.clone(), - _ => version, - }); - } - // Implementation-specific tags (e.g., cp310, pp39) - // These represent exact minor versions: cp310 means ==3.10.* - LanguageTag::CPython { - python_version: (3, minor), - } - | LanguageTag::PyPy { - python_version: (3, minor), - } - | LanguageTag::GraalPy { - python_version: (3, minor), - } - | LanguageTag::Pyston { - python_version: (3, minor), - } => { - let version = Version::new([3, u64::from(*minor)]); - lower_bound = Some(match &lower_bound { - Some(existing) if existing < &version => existing.clone(), - _ => version.clone(), - }); - upper_bound = Some(match &upper_bound { - Some(existing) if existing > &version => existing.clone(), - _ => version, - }); - } - _ => { - // Unknown or None tags - } - } - } - - // Also check ABI tags for version information - for abi_tag in wheel.abi_tags() { - match abi_tag { - // Reject Python 2 ABI tags - AbiTag::CPython { - python_version: (2, ..), - .. - } - | AbiTag::PyPy { - python_version: Some((2, ..)), - .. - } - | AbiTag::GraalPy { - python_version: (2, ..), - .. - } => { - return None; - } - // Python 3 ABI tags indicate exact versions - AbiTag::CPython { - python_version: (3, minor), - .. - } - | AbiTag::PyPy { - python_version: Some((3, minor)), - .. - } - | AbiTag::GraalPy { - python_version: (3, minor), - .. - } => { - let version = Version::new([3, u64::from(*minor)]); - lower_bound = Some(match &lower_bound { - Some(existing) if existing < &version => existing.clone(), - _ => version.clone(), - }); - upper_bound = Some(match &upper_bound { - Some(existing) if existing > &version => existing.clone(), - _ => version, - }); - } - _ => { - // abi3, None, or other tags don't provide version constraints - } - } - } - - // Build the version specifiers based on the bounds we found - match (lower_bound, upper_bound) { - (Some(lower), Some(upper)) if lower == upper => { - // Exact version match: ==3.X.* - Some(VersionSpecifiers::from( - VersionSpecifier::equals_star_version(lower), - )) - } - (Some(lower), Some(upper)) if lower < upper => { - // Multiple exact versions: >=3.X, <=3.Y - Some(VersionSpecifiers::from_iter([ - VersionSpecifier::greater_than_equal_version(lower), - VersionSpecifier::less_than_equal_version(upper), - ])) - } - (Some(lower), None) => { - // Just a lower bound: >=3.X - Some(VersionSpecifiers::from( - VersionSpecifier::greater_than_equal_version(lower), - )) - } - _ => None, - } - } - /// Returns `false` if the wheel's tags state it can't be used in the given Python version /// range. /// @@ -948,52 +774,6 @@ mod tests { } } - #[test] - fn from_wheel_tags() { - // Test implementation-specific tags (exact versions) - let wheel = WheelFilename::from_str("black-24.4.2-cp310-cp310-win_amd64.whl").unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), "==3.10.*"); - - let wheel = WheelFilename::from_str("dearpygui-1.11.1-cp312-cp312-win_amd64.whl").unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), "==3.12.*"); - - // Test generic Python tags (lower bounds) - let wheel = WheelFilename::from_str("cbor2-5.6.4-py3-none-any.whl").unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), ">=3"); - - let wheel = WheelFilename::from_str("example-1.0-py310-none-any.whl").unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), ">=3.10"); - - // Test abi3 wheels - let wheel = - WheelFilename::from_str("bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl").unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), "==3.7.*"); - - // Test PyPy tags - let wheel = - WheelFilename::from_str("watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl") - .unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), "==3.10.*"); - - // Test multiple python tags (should create a range) - let wheel = WheelFilename::from_str("example-1.0-cp310.cp311-none-any.whl").unwrap(); - let specifiers = RequiresPython::from_wheel(&wheel).unwrap(); - assert_eq!(specifiers.to_string(), ">=3.10, <=3.11"); - - // Test Python 2 wheels (should return None) - let wheel = WheelFilename::from_str("PySocks-1.7.1-py27-none-any.whl").unwrap(); - assert_eq!(RequiresPython::from_wheel(&wheel), None); - - let wheel = WheelFilename::from_str("psutil-6.0.0-cp27-none-win32.whl").unwrap(); - assert_eq!(RequiresPython::from_wheel(&wheel), None); - } - #[test] fn split_version() { // Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`. diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index e1301207cdeea..7ef848ab26a23 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -243,37 +243,18 @@ impl PubGrubRequirement { } }; - let package = PubGrubPackage::from_package( - requirement.name.clone(), - extra, - group, - requirement.marker, - ); - let url = VerbatimParsedUrl { - parsed_url, - verbatim: verbatim_url.clone(), - }; - - // In theory this can help a little bit because... we know the exact version, so if we visit - // some other distribution earlier, we have this information earlier to narrow our choices. - // TODO(charlie): We can just do this with the filename, though -- we don't need to do `Dist::from_url`. - // TODO(charlie): Does this mean that we'd then have a problem if the filename and the - // metadata don't match? - // let version = if let Ok(Dist::Built(dist)) = Dist::from_url(requirement.name.clone(), url.clone()) { - // let version = match dist { - // BuiltDist::DirectUrl(dist) => dist.filename.version, - // BuiltDist::Path(dist) => dist.filename.version, - // BuiltDist::Registry(..) => unreachable!() - // }; - // Ranges::singleton(version) - // } else { - // Ranges::full() - // }; - Self { - package, + package: PubGrubPackage::from_package( + requirement.name.clone(), + extra, + group, + requirement.marker, + ), version: Ranges::full(), - url: Some(url), + url: Some(VerbatimParsedUrl { + parsed_url, + verbatim: verbatim_url.clone(), + }), } } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 26897d5c38621..1aff6c3e7d5b1 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -32247,39 +32247,6 @@ fn lock_unsupported_wheel_url_requires_python() -> Result<()> { And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable. "); - let lock = context.read("uv.lock"); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r#" - version = 1 - revision = 3 - requires-python = ">=3.12" - - [options] - exclude-newer = "2024-03-25T00:00:00Z" - - [[package]] - name = "numpy" - version = "2.3.5" - source = { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" } - - [[package]] - name = "project" - version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "numpy" }, - ] - - [package.metadata] - requires-dist = [{ name = "numpy", url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" }] - "# - ); - }); - Ok(()) } @@ -32312,44 +32279,5 @@ fn lock_unsupported_wheel_url_required_platform() -> Result<()> { And because your project depends on numpy, we can conclude that your project's requirements are unsatisfiable. "); - let lock = context.read("uv.lock"); - - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r#" - version = 1 - revision = 3 - requires-python = ">=3.11" - required-markers = [ - "sys_platform == 'win32'", - ] - - [options] - exclude-newer = "2024-03-25T00:00:00Z" - - [[package]] - name = "numpy" - version = "2.3.5" - source = { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" } - wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d" }, - ] - - [[package]] - name = "project" - version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "numpy" }, - ] - - [package.metadata] - requires-dist = [{ name = "numpy", url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl" }] - "# - ); - }); - Ok(()) } diff --git a/foo/.python-version b/foo/.python-version deleted file mode 100644 index f982feb41bd00..0000000000000 --- a/foo/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.14.0 diff --git a/foo/README.md b/foo/README.md deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/foo/main.py b/foo/main.py deleted file mode 100644 index f8a203dd42159..0000000000000 --- a/foo/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from foo!") - - -if __name__ == "__main__": - main() diff --git a/foo/pyproject.toml b/foo/pyproject.toml deleted file mode 100644 index 0ba7344d5e3bd..0000000000000 --- a/foo/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -[project] -name = "foo" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -requires-python = ">=3.14.0" -dependencies = [ - "triton", -] - -[tool.uv.sources] -triton = { path = "../../../Downloads/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" } diff --git a/foo/uv.lock b/foo/uv.lock deleted file mode 100644 index a6606aa317240..0000000000000 --- a/foo/uv.lock +++ /dev/null @@ -1,11 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.14.0" - -[[package]] -name = "foo" -version = "0.1.0" -source = { virtual = "." } - -[package.metadata] -requires-dist = [{ name = "triton", path = "../../../Downloads/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" }]