|
1 | 1 | use rustc_hash::FxHashMap; |
2 | 2 |
|
3 | | -use pep440_rs::{Operator, Version, VersionSpecifier}; |
| 3 | +use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError}; |
4 | 4 | use pep508_rs::{MarkerEnvironment, VersionOrUrl}; |
5 | 5 | use uv_normalize::PackageName; |
6 | 6 |
|
@@ -63,63 +63,71 @@ impl Locals { |
63 | 63 |
|
64 | 64 | /// Given a specifier that may include the version _without_ a local segment, return a specifier |
65 | 65 | /// that includes the local segment from the expected version. |
66 | | - pub(crate) fn map(local: &Version, specifier: &VersionSpecifier) -> VersionSpecifier { |
| 66 | + pub(crate) fn map( |
| 67 | + local: &Version, |
| 68 | + specifier: &VersionSpecifier, |
| 69 | + ) -> Result<VersionSpecifier, VersionSpecifierBuildError> { |
67 | 70 | match specifier.operator() { |
68 | 71 | Operator::Equal | Operator::EqualStar => { |
69 | 72 | // Given `foo==1.0.0`, if the local version is `1.0.0+local`, map to |
70 | | - // `foo==1.0.0+local`. This has the intended effect of allowing `1.0.0+local`. |
| 73 | + // `foo==1.0.0+local`. |
| 74 | + // |
| 75 | + // This has the intended effect of allowing `1.0.0+local`. |
71 | 76 | if is_compatible(local, specifier.version()) { |
72 | | - VersionSpecifier::new(Operator::Equal, local.clone()) |
| 77 | + VersionSpecifier::from_version(Operator::Equal, local.clone()) |
73 | 78 | } else { |
74 | | - specifier.clone() |
| 79 | + Ok(specifier.clone()) |
75 | 80 | } |
76 | 81 | } |
77 | 82 | Operator::NotEqual | Operator::NotEqualStar => { |
78 | 83 | // Given `foo!=1.0.0`, if the local version is `1.0.0+local`, map to |
79 | | - // `foo!=1.0.0+local`. This has the intended effect of disallowing `1.0.0+local`. |
80 | | - // There's no risk of including `foo @ 1.0.0` in the resolution, since we _know_ |
81 | | - // `foo @ 1.0.0+local` is required and would conflict. |
| 84 | + // `foo!=1.0.0+local`. |
| 85 | + // |
| 86 | + // This has the intended effect of disallowing `1.0.0+local`. |
| 87 | + // |
| 88 | + // There's no risk of accidentally including `foo @ 1.0.0` in the resolution, since |
| 89 | + // we _know_ `foo @ 1.0.0+local` is required and would therefore conflict. |
82 | 90 | if is_compatible(local, specifier.version()) { |
83 | | - VersionSpecifier::new(Operator::NotEqual, local.clone()) |
| 91 | + VersionSpecifier::from_version(Operator::NotEqual, local.clone()) |
84 | 92 | } else { |
85 | | - specifier.clone() |
| 93 | + Ok(specifier.clone()) |
86 | 94 | } |
87 | 95 | } |
88 | 96 | Operator::LessThanEqual => { |
89 | 97 | // Given `foo<=1.0.0`, if the local version is `1.0.0+local`, map to |
90 | | - // `foo<=1.0.0+local`. This has the intended effect of allowing `1.0.0+local`. |
91 | | - // There's no risk of including `foo @ 1.0.0.post1` in the resolution, since we |
92 | | - // _know_ `foo @ 1.0.0+local` is required and would conflict. |
| 98 | + // `foo==1.0.0+local`. |
| 99 | + // |
| 100 | + // This has the intended effect of allowing `1.0.0+local`. |
| 101 | + // |
| 102 | + // Since `foo==1.0.0+local` is already required, we know that to satisfy |
| 103 | + // `foo<=1.0.0`, we _must_ satisfy `foo==1.0.0+local`. We _could_ map to |
| 104 | + // `foo<=1.0.0+local`, but local versions are _not_ allowed in exclusive ordered |
| 105 | + // specifiers, so introducing `foo<=1.0.0+local` would risk breaking invariants. |
93 | 106 | if is_compatible(local, specifier.version()) { |
94 | | - VersionSpecifier::new(Operator::LessThanEqual, local.clone()) |
| 107 | + VersionSpecifier::from_version(Operator::Equal, local.clone()) |
95 | 108 | } else { |
96 | | - specifier.clone() |
| 109 | + Ok(specifier.clone()) |
97 | 110 | } |
98 | 111 | } |
99 | 112 | Operator::GreaterThan => { |
100 | | - // Given `foo>1.0.0`, if the local version is `1.0.0+local`, map to |
101 | | - // `foo>1.0.0+local`. This has the intended effect of disallowing `1.0.0+local`. |
102 | | - if is_compatible(local, specifier.version()) { |
103 | | - VersionSpecifier::new(Operator::GreaterThan, local.clone()) |
104 | | - } else { |
105 | | - specifier.clone() |
106 | | - } |
| 113 | + // Given `foo>1.0.0`, `foo @ 1.0.0+local` is already (correctly) disallowed. |
| 114 | + Ok(specifier.clone()) |
107 | 115 | } |
108 | 116 | Operator::ExactEqual => { |
109 | | - // Given `foo===1.0.0`, `1.0.0+local` is already disallowed. |
110 | | - specifier.clone() |
| 117 | + // Given `foo===1.0.0`, `1.0.0+local` is already (correctly) disallowed. |
| 118 | + Ok(specifier.clone()) |
111 | 119 | } |
112 | 120 | Operator::TildeEqual => { |
113 | | - // Given `foo~=1.0.0`, `foo~=1.0.0+local` is already allowed. |
114 | | - specifier.clone() |
| 121 | + // Given `foo~=1.0.0`, `foo~=1.0.0+local` is already (correctly) allowed. |
| 122 | + Ok(specifier.clone()) |
115 | 123 | } |
116 | 124 | Operator::LessThan => { |
117 | | - // Given `foo<1.0.0`, `1.0.0+local` is already disallowed. |
118 | | - specifier.clone() |
| 125 | + // Given `foo<1.0.0`, `1.0.0+local` is already (correctly) disallowed. |
| 126 | + Ok(specifier.clone()) |
119 | 127 | } |
120 | 128 | Operator::GreaterThanEqual => { |
121 | | - // Given `foo>=1.0.0`, `foo>1.0.0+local` is already allowed. |
122 | | - specifier.clone() |
| 129 | + // Given `foo>=1.0.0`, `foo @ 1.0.0+local` is already (correctly) allowed. |
| 130 | + Ok(specifier.clone()) |
123 | 131 | } |
124 | 132 | } |
125 | 133 | } |
@@ -163,3 +171,82 @@ fn to_local(specifier: &VersionSpecifier) -> Option<&Version> { |
163 | 171 |
|
164 | 172 | Some(specifier.version()) |
165 | 173 | } |
| 174 | + |
| 175 | +#[cfg(test)] |
| 176 | +mod tests { |
| 177 | + use std::str::FromStr; |
| 178 | + |
| 179 | + use anyhow::Result; |
| 180 | + |
| 181 | + use pep440_rs::{Operator, Version, VersionSpecifier}; |
| 182 | + |
| 183 | + use super::Locals; |
| 184 | + |
| 185 | + #[test] |
| 186 | + fn map_version() -> Result<()> { |
| 187 | + // Given `==1.0.0`, if the local version is `1.0.0+local`, map to `==1.0.0+local`. |
| 188 | + let local = Version::from_str("1.0.0+local")?; |
| 189 | + let specifier = |
| 190 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0")?)?; |
| 191 | + assert_eq!( |
| 192 | + Locals::map(&local, &specifier)?, |
| 193 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? |
| 194 | + ); |
| 195 | + |
| 196 | + // Given `!=1.0.0`, if the local version is `1.0.0+local`, map to `!=1.0.0+local`. |
| 197 | + let local = Version::from_str("1.0.0+local")?; |
| 198 | + let specifier = |
| 199 | + VersionSpecifier::from_version(Operator::NotEqual, Version::from_str("1.0.0")?)?; |
| 200 | + assert_eq!( |
| 201 | + Locals::map(&local, &specifier)?, |
| 202 | + VersionSpecifier::from_version(Operator::NotEqual, Version::from_str("1.0.0+local")?)? |
| 203 | + ); |
| 204 | + |
| 205 | + // Given `<=1.0.0`, if the local version is `1.0.0+local`, map to `==1.0.0+local`. |
| 206 | + let local = Version::from_str("1.0.0+local")?; |
| 207 | + let specifier = |
| 208 | + VersionSpecifier::from_version(Operator::LessThanEqual, Version::from_str("1.0.0")?)?; |
| 209 | + assert_eq!( |
| 210 | + Locals::map(&local, &specifier)?, |
| 211 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? |
| 212 | + ); |
| 213 | + |
| 214 | + // Given `>1.0.0`, `1.0.0+local` is already (correctly) disallowed. |
| 215 | + let local = Version::from_str("1.0.0+local")?; |
| 216 | + let specifier = |
| 217 | + VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)?; |
| 218 | + assert_eq!( |
| 219 | + Locals::map(&local, &specifier)?, |
| 220 | + VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)? |
| 221 | + ); |
| 222 | + |
| 223 | + // Given `===1.0.0`, `1.0.0+local` is already (correctly) disallowed. |
| 224 | + let local = Version::from_str("1.0.0+local")?; |
| 225 | + let specifier = |
| 226 | + VersionSpecifier::from_version(Operator::ExactEqual, Version::from_str("1.0.0")?)?; |
| 227 | + assert_eq!( |
| 228 | + Locals::map(&local, &specifier)?, |
| 229 | + VersionSpecifier::from_version(Operator::ExactEqual, Version::from_str("1.0.0")?)? |
| 230 | + ); |
| 231 | + |
| 232 | + // Given `==1.0.0+local`, `1.0.0+local` is already (correctly) allowed. |
| 233 | + let local = Version::from_str("1.0.0+local")?; |
| 234 | + let specifier = |
| 235 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)?; |
| 236 | + assert_eq!( |
| 237 | + Locals::map(&local, &specifier)?, |
| 238 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? |
| 239 | + ); |
| 240 | + |
| 241 | + // Given `==1.0.0+other`, `1.0.0+local` is already (correctly) disallowed. |
| 242 | + let local = Version::from_str("1.0.0+local")?; |
| 243 | + let specifier = |
| 244 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+other")?)?; |
| 245 | + assert_eq!( |
| 246 | + Locals::map(&local, &specifier)?, |
| 247 | + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+other")?)? |
| 248 | + ); |
| 249 | + |
| 250 | + Ok(()) |
| 251 | + } |
| 252 | +} |
0 commit comments