diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs index 4b8349caa2d9d..e952dfe17cbe1 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 4ad72d623f36d..2edfefcc98d79 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -123,7 +123,7 @@ impl Display for Pep508Error { } /// We need this to allow anyhow's `.context()` and `AsDynError`. -impl> std::error::Error for Pep508Error {} +impl> std::error::Error for Pep508Error {} #[cfg(feature = "pyo3")] create_exception!( @@ -484,7 +484,7 @@ impl schemars::JsonSchema for Requirement { })), ..schemars::schema::SchemaObject::default() } - .into() + .into() } } @@ -1131,8 +1131,8 @@ mod tests { &mut Cursor::new("file:///C:/Users/ferris/wheel-0.42.0.tar.gz"), None, ) - .unwrap() - .to_file_path(); + .unwrap() + .to_file_path(); let expected = PathBuf::from(r"C:\Users\ferris\wheel-0.42.0.tar.gz"); assert_eq!(actual, Ok(expected)); } @@ -1172,7 +1172,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 { @@ -1183,19 +1183,19 @@ mod tests { ], version_or_url: Some(VersionOrUrl::VersionSpecifier( [ - VersionSpecifier::from_pattern( - Operator::GreaterThanEqual, - VersionPattern::verbatim(Version::new([2, 8, 1])), - ) - .unwrap(), VersionSpecifier::from_pattern( Operator::Equal, VersionPattern::wildcard(Version::new([2, 8])), ) - .unwrap(), + .unwrap(), + VersionSpecifier::from_pattern( + Operator::GreaterThanEqual, + VersionPattern::verbatim(Version::new([2, 8, 1])), + ) + .unwrap(), ] - .into_iter() - .collect(), + .into_iter() + .collect(), )), marker: MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, @@ -1203,7 +1203,7 @@ mod tests { pep440_rs::Operator::LessThan, "2.7".parse().unwrap(), ) - .unwrap(), + .unwrap(), }), origin: None, }; @@ -1248,7 +1248,7 @@ mod tests { let numpy = crate::UnnamedRequirement::::from_str( "/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]", ) - .unwrap(); + .unwrap(); assert_eq!( numpy.url.to_string(), "file:///path/to/numpy-1.26.4-cp312-cp312-win32.whl" @@ -1262,7 +1262,7 @@ mod tests { let numpy = crate::UnnamedRequirement::::from_str( "C:\\path\\to\\numpy-1.26.4-cp312-cp312-win32.whl[dev]", ) - .unwrap(); + .unwrap(); assert_eq!( numpy.url.to_string(), "file:///C:/path/to/numpy-1.26.4-cp312-cp312-win32.whl" @@ -1444,8 +1444,8 @@ mod tests { &mut Cursor::new(marker), &mut TracingReporter, ) - .unwrap() - .unwrap(); + .unwrap() + .unwrap(); let mut a = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonVersion, @@ -1453,7 +1453,7 @@ mod tests { pep440_rs::Operator::Equal, "2.7".parse().unwrap(), ) - .unwrap(), + .unwrap(), }); let mut b = MarkerTree::expression(MarkerExpression::String { key: MarkerValueString::SysPlatform, @@ -1849,11 +1849,11 @@ mod tests { let requirement = Requirement::::from_str( "pytest; '4.0' >= python_version or sys_platform == 'win32'", ) - .unwrap(); + .unwrap(); let expected = Requirement::from_str( "pytest; ('4.0' >= python_version or sys_platform == 'win32') and extra == 'dotenv'", ) - .unwrap(); + .unwrap(); let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); assert_eq!(actual, expected); diff --git a/crates/uv/tests/pip_check.rs b/crates/uv/tests/pip_check.rs index 5d7e26498e279..835d3ea5774bd 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 731c8e6ad0aac..d1d3675f27cc5 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 ----- @@ -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 e37286321b318..197286702d337 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" },