diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs index 4b8349caa2d9..e952dfe17cbe 100644 --- a/crates/pep440-rs/src/version_specifier.rs +++ b/crates/pep440-rs/src/version_specifier.rs @@ -20,13 +20,11 @@ use crate::{ version, Operator, OperatorParseError, Version, VersionPattern, VersionPatternParseError, }; -/// A thin wrapper around `Vec` with a serde implementation +/// Sorted version specifiers, such as `>=2.1,<3`. /// /// Python requirements can contain multiple version specifier so we need to store them in a list, /// such as `>1.2,<2.0` being `[">1.2", "<2.0"]`. /// -/// You can use the serde implementation to e.g. parse `requires-python` from pyproject.toml -/// /// ```rust /// # use std::str::FromStr; /// # use pep440_rs::{VersionSpecifiers, Version, Operator}; @@ -77,11 +75,19 @@ impl VersionSpecifiers { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + /// Sort the specifiers. + fn from_unsorted(mut specifiers: Vec) -> Self { + // TODO(konsti): This seems better than sorting on insert and not getting the size hint, + // but i haven't measured it. + specifiers.sort_by(|a, b| a.version().cmp(b.version())); + Self(specifiers) + } } impl FromIterator for VersionSpecifiers { fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) + Self::from_unsorted(iter.into_iter().collect()) } } @@ -89,7 +95,7 @@ impl FromStr for VersionSpecifiers { type Err = VersionSpecifiersParseError; fn from_str(s: &str) -> Result { - parse_version_specifiers(s).map(Self) + parse_version_specifiers(s).map(Self::from_unsorted) } } @@ -1742,7 +1748,7 @@ mod tests { VersionSpecifiers::from_str(">=3.7, < 4.0, != 3.9.0") .unwrap() .to_string(), - ">=3.7, <4.0, !=3.9.0" + ">=3.7, !=3.9.0, <4.0" ); } diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 5946a1b06853..86a9cecf3467 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -1173,7 +1173,7 @@ mod tests { #[test] fn basic_examples() { - let input = r"requests[security,tests]>=2.8.1,==2.8.* ; python_full_version < '2.7'"; + let input = r"requests[security,tests]==2.8.*,>=2.8.1 ; python_full_version < '2.7'"; let requests = Requirement::::from_str(input).unwrap(); assert_eq!(input, requests.to_string()); let expected = Requirement { @@ -1185,13 +1185,13 @@ mod tests { version_or_url: Some(VersionOrUrl::VersionSpecifier( [ VersionSpecifier::from_pattern( - Operator::GreaterThanEqual, - VersionPattern::verbatim(Version::new([2, 8, 1])), + Operator::Equal, + VersionPattern::wildcard(Version::new([2, 8])), ) .unwrap(), VersionSpecifier::from_pattern( - Operator::Equal, - VersionPattern::wildcard(Version::new([2, 8])), + Operator::GreaterThanEqual, + VersionPattern::verbatim(Version::new([2, 8, 1])), ) .unwrap(), ] diff --git a/crates/uv/tests/pip_check.rs b/crates/uv/tests/pip_check.rs index 5d7e26498e27..835d3ea5774b 100644 --- a/crates/uv/tests/pip_check.rs +++ b/crates/uv/tests/pip_check.rs @@ -109,7 +109,7 @@ fn check_incompatible_packages() -> Result<()> { Installed 1 package in [TIME] - idna==3.6 + idna==2.4 - warning: The package `requests` requires `idna<4,>=2.5`, but `2.4` is installed + warning: The package `requests` requires `idna>=2.5,<4`, but `2.4` is installed "### ); @@ -121,7 +121,7 @@ fn check_incompatible_packages() -> Result<()> { ----- stderr ----- Checked 5 packages in [TIME] Found 1 incompatibility - The package `requests` requires `idna<4,>=2.5`, but `2.4` is installed + The package `requests` requires `idna>=2.5,<4`, but `2.4` is installed "### ); @@ -180,8 +180,8 @@ fn check_multiple_incompatible_packages() -> Result<()> { + idna==2.4 - urllib3==2.2.1 + urllib3==1.20 - warning: The package `requests` requires `idna<4,>=2.5`, but `2.4` is installed - warning: The package `requests` requires `urllib3<3,>=1.21.1`, but `1.20` is installed + warning: The package `requests` requires `idna>=2.5,<4`, but `2.4` is installed + warning: The package `requests` requires `urllib3>=1.21.1,<3`, but `1.20` is installed "### ); @@ -193,8 +193,8 @@ fn check_multiple_incompatible_packages() -> Result<()> { ----- stderr ----- Checked 5 packages in [TIME] Found 2 incompatibilities - The package `requests` requires `idna<4,>=2.5`, but `2.4` is installed - The package `requests` requires `urllib3<3,>=1.21.1`, but `1.20` is installed + The package `requests` requires `idna>=2.5,<4`, but `2.4` is installed + The package `requests` requires `urllib3>=1.21.1,<3`, but `1.20` is installed "### ); diff --git a/crates/uv/tests/pip_tree.rs b/crates/uv/tests/pip_tree.rs index 731c8e6ad0aa..ef96dfb651ae 100644 --- a/crates/uv/tests/pip_tree.rs +++ b/crates/uv/tests/pip_tree.rs @@ -1534,9 +1534,9 @@ fn show_version_specifiers_simple() { exit_code: 0 ----- stdout ----- requests v2.31.0 - ├── charset-normalizer v3.3.2 [required: <4, >=2] - ├── idna v3.6 [required: <4, >=2.5] - ├── urllib3 v2.2.1 [required: <3, >=1.21.1] + ├── charset-normalizer v3.3.2 [required: >=2, <4] + ├── idna v3.6 [required: >=2.5, <4] + ├── urllib3 v2.2.1 [required: >=1.21.1, <3] └── certifi v2024.2.2 [required: >=2017.4.17] ----- stderr ----- @@ -1620,12 +1620,12 @@ fn show_version_specifiers_complex() { │ ├── docutils v0.20.1 [required: >=0.13.1] │ └── pygments v2.17.2 [required: >=2.5.1] ├── requests v2.31.0 [required: >=2.20] - │ ├── charset-normalizer v3.3.2 [required: <4, >=2] - │ ├── idna v3.6 [required: <4, >=2.5] - │ ├── urllib3 v2.2.1 [required: <3, >=1.21.1] + │ ├── charset-normalizer v3.3.2 [required: >=2, <4] + │ ├── idna v3.6 [required: >=2.5, <4] + │ ├── urllib3 v2.2.1 [required: >=1.21.1, <3] │ └── certifi v2024.2.2 [required: >=2017.4.17] - ├── requests-toolbelt v1.0.0 [required: !=0.9.0, >=0.8.0] - │ └── requests v2.31.0 [required: <3.0.0, >=2.0.1] (*) + ├── requests-toolbelt v1.0.0 [required: >=0.8.0, !=0.9.0] + │ └── requests v2.31.0 [required: >=2.0.1, <3.0.0] (*) ├── urllib3 v2.2.1 [required: >=1.26.0] ├── importlib-metadata v7.1.0 [required: >=3.6] │ └── zipp v3.18.1 [required: >=0.5] @@ -1688,8 +1688,8 @@ fn show_version_specifiers_with_invert() { joblib v1.3.2 └── scikit-learn v1.4.1.post1 [requires: joblib >=1.2.0] numpy v1.26.4 - ├── scikit-learn v1.4.1.post1 [requires: numpy <2.0, >=1.19.5] - └── scipy v1.12.0 [requires: numpy <1.29.0, >=1.22.4] + ├── scikit-learn v1.4.1.post1 [requires: numpy >=1.19.5, <2.0] + └── scipy v1.12.0 [requires: numpy >=1.22.4, <1.29.0] └── scikit-learn v1.4.1.post1 [requires: scipy >=1.6.0] threadpoolctl v3.4.0 └── scikit-learn v1.4.1.post1 [requires: threadpoolctl >=2.0.0] @@ -1739,7 +1739,7 @@ fn show_version_specifiers_with_package() { exit_code: 0 ----- stdout ----- scipy v1.12.0 - └── numpy v1.26.4 [required: <1.29.0, >=1.22.4] + └── numpy v1.26.4 [required: >=1.22.4, <1.29.0] ----- stderr ----- "### diff --git a/crates/uv/tests/snapshots/ecosystem__black-lock-file.snap b/crates/uv/tests/snapshots/ecosystem__black-lock-file.snap index e37286321b31..197286702d33 100644 --- a/crates/uv/tests/snapshots/ecosystem__black-lock-file.snap +++ b/crates/uv/tests/snapshots/ecosystem__black-lock-file.snap @@ -204,7 +204,7 @@ uvloop = [ [package.metadata] requires-dist = [ - { name = "aiohttp", marker = "implementation_name == 'pypy' and sys_platform == 'win32' and extra == 'd'", specifier = "!=3.9.0,>=3.7.4" }, + { name = "aiohttp", marker = "implementation_name == 'pypy' and sys_platform == 'win32' and extra == 'd'", specifier = ">=3.7.4,!=3.9.0" }, { name = "aiohttp", marker = "(implementation_name != 'pypy' and extra == 'd') or (sys_platform != 'win32' and extra == 'd')", specifier = ">=3.7.4" }, { name = "click", specifier = ">=8.0.0" }, { name = "colorama", marker = "extra == 'colorama'", specifier = ">=0.4.3" },