Skip to content

Commit e8aeb19

Browse files
committed
Update tests
1 parent 96c094e commit e8aeb19

File tree

8 files changed

+1159
-170
lines changed

8 files changed

+1159
-170
lines changed

crates/pep440-rs/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ pub use {
4141
LocalSegment, Operator, OperatorParseError, PreRelease, PreReleaseKind, Version,
4242
VersionParseError, VersionPattern, VersionPatternParseError, MIN_VERSION,
4343
},
44-
version_specifier::{VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError},
44+
version_specifier::{
45+
VersionSpecifier, VersionSpecifierBuildError, VersionSpecifiers,
46+
VersionSpecifiersParseError,
47+
},
4548
};
4649

4750
mod version;

crates/pep440-rs/src/version_specifier.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -340,15 +340,6 @@ impl Serialize for VersionSpecifier {
340340
}
341341

342342
impl VersionSpecifier {
343-
/// Create a new version specifier from an operator and a version.
344-
///
345-
/// Warning: This function is not recommended for general use. It is intended for use in
346-
/// situations where the operator and version are known to be valid. For general use, prefer
347-
/// [`VersionSpecifier::from_pattern`].
348-
pub fn new(operator: Operator, version: Version) -> Self {
349-
Self { operator, version }
350-
}
351-
352343
/// Build from parts, validating that the operator is allowed with that version. The last
353344
/// parameter indicates a trailing `.*`, to differentiate between `1.1.*` and `1.1`
354345
pub fn from_pattern(
@@ -357,10 +348,6 @@ impl VersionSpecifier {
357348
) -> Result<Self, VersionSpecifierBuildError> {
358349
let star = version_pattern.is_wildcard();
359350
let version = version_pattern.into_version();
360-
// "Local version identifiers are NOT permitted in this version specifier."
361-
if version.is_local() && !operator.is_local_compatible() {
362-
return Err(BuildErrorKind::OperatorLocalCombo { operator, version }.into());
363-
}
364351

365352
// Check if there are star versions and if so, switch operator to star version
366353
let operator = if star {
@@ -374,6 +361,19 @@ impl VersionSpecifier {
374361
operator
375362
};
376363

364+
Self::from_version(operator, version)
365+
}
366+
367+
/// Create a new version specifier from an operator and a version.
368+
pub fn from_version(
369+
operator: Operator,
370+
version: Version,
371+
) -> Result<Self, VersionSpecifierBuildError> {
372+
// "Local version identifiers are NOT permitted in this version specifier."
373+
if version.is_local() && !operator.is_local_compatible() {
374+
return Err(BuildErrorKind::OperatorLocalCombo { operator, version }.into());
375+
}
376+
377377
if operator == Operator::TildeEqual && version.release().len() < 2 {
378378
return Err(BuildErrorKind::CompatibleRelease.into());
379379
}

crates/uv-resolver/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ pub enum ResolveError {
8484
version: Box<Version>,
8585
},
8686

87+
#[error("Attempted to construct an invalid version specifier")]
88+
InvalidVersion(#[from] pep440_rs::VersionSpecifierBuildError),
89+
8790
/// Something unexpected happened.
8891
#[error("{0}")]
8992
Failure(String),

crates/uv-resolver/src/pubgrub/dependencies.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,11 @@ fn to_pubgrub(
146146
let version = if let Some(expected) = locals.get(&requirement.name) {
147147
specifiers
148148
.iter()
149-
.map(|specifier| Locals::map(expected, specifier))
150-
.map(|specifier| PubGrubSpecifier::try_from(&specifier))
149+
.map(|specifier| {
150+
Locals::map(expected, specifier)
151+
.map_err(ResolveError::InvalidVersion)
152+
.and_then(|specifier| PubGrubSpecifier::try_from(&specifier))
153+
})
151154
.fold_ok(Range::full(), |range, specifier| {
152155
range.intersection(&specifier.into())
153156
})?

crates/uv-resolver/src/resolver/locals.rs

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use rustc_hash::FxHashMap;
22

3-
use pep440_rs::{Operator, Version, VersionSpecifier};
3+
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError};
44
use pep508_rs::{MarkerEnvironment, VersionOrUrl};
55
use uv_normalize::PackageName;
66

@@ -63,63 +63,71 @@ impl Locals {
6363

6464
/// Given a specifier that may include the version _without_ a local segment, return a specifier
6565
/// 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> {
6770
match specifier.operator() {
6871
Operator::Equal | Operator::EqualStar => {
6972
// 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`.
7176
if is_compatible(local, specifier.version()) {
72-
VersionSpecifier::new(Operator::Equal, local.clone())
77+
VersionSpecifier::from_version(Operator::Equal, local.clone())
7378
} else {
74-
specifier.clone()
79+
Ok(specifier.clone())
7580
}
7681
}
7782
Operator::NotEqual | Operator::NotEqualStar => {
7883
// 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.
8290
if is_compatible(local, specifier.version()) {
83-
VersionSpecifier::new(Operator::NotEqual, local.clone())
91+
VersionSpecifier::from_version(Operator::NotEqual, local.clone())
8492
} else {
85-
specifier.clone()
93+
Ok(specifier.clone())
8694
}
8795
}
8896
Operator::LessThanEqual => {
8997
// 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.
93106
if is_compatible(local, specifier.version()) {
94-
VersionSpecifier::new(Operator::LessThanEqual, local.clone())
107+
VersionSpecifier::from_version(Operator::Equal, local.clone())
95108
} else {
96-
specifier.clone()
109+
Ok(specifier.clone())
97110
}
98111
}
99112
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())
107115
}
108116
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())
111119
}
112120
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())
115123
}
116124
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())
119127
}
120128
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())
123131
}
124132
}
125133
}

crates/uv/tests/pip_compile.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,22 +1501,11 @@ fn disallowed_transitive_url_dependency() -> Result<()> {
15011501
.arg("requirements.in")
15021502
.env("HATCHLING", hatchling_path.as_os_str()), @r###"
15031503
success: false
1504-
exit_code: 1
1504+
exit_code: 2
15051505
----- stdout -----
15061506
15071507
----- stderr -----
1508-
× No solution found when resolving dependencies:
1509-
╰─▶ Because only hatchling-editable==0.1.0 is available and
1510-
hatchling-editable==0.1.0 is unusable because its dependencies are
1511-
unusable because package `iniconfig` attempted to resolve via URL:
1512-
git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4.
1513-
URL dependencies must be expressed as direct
1514-
requirements or constraints. Consider adding `iniconfig @
1515-
git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4`
1516-
to your dependencies or constraints file., we can conclude that all
1517-
versions of hatchling-editable cannot be used.
1518-
And because you require hatchling-editable, we can conclude that the
1519-
requirements are unsatisfiable.
1508+
error: Package `iniconfig` attempted to resolve via URL: git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4. URL dependencies must be expressed as direct requirements or constraints. Consider adding `iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4` to your dependencies or constraints file.
15201509
"###
15211510
);
15221511

crates/uv/tests/pip_compile_scenarios.rs

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,18 @@ fn incompatible_python_compatible_override() -> Result<()> {
7575

7676
let output = uv_snapshot!(filters, command(&context, python_versions)
7777
.arg("--python-version=3.11")
78-
, @r###"<snapshot>
79-
"###
78+
, @r###"
79+
success: true
80+
exit_code: 0
81+
----- stdout -----
82+
# This file was autogenerated by uv via the following command:
83+
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
84+
package-a==1.0.0
85+
86+
----- stderr -----
87+
warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead.
88+
Resolved 1 package in [TIME]
89+
"###
8090
);
8191

8292
output.assert().success().stdout(predicate::str::contains(
@@ -114,8 +124,17 @@ fn compatible_python_incompatible_override() -> Result<()> {
114124

115125
let output = uv_snapshot!(filters, command(&context, python_versions)
116126
.arg("--python-version=3.9")
117-
, @r###"<snapshot>
118-
"###
127+
, @r###"
128+
success: false
129+
exit_code: 1
130+
----- stdout -----
131+
132+
----- stderr -----
133+
warning: The requested Python version 3.9 is not available; 3.11.7 will be used to build dependencies instead.
134+
× No solution found when resolving dependencies:
135+
╰─▶ Because the requested Python version (3.9) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
136+
And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable.
137+
"###
119138
);
120139

121140
output.assert().failure();
@@ -159,8 +178,17 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()>
159178
// dependencies.
160179
let output = uv_snapshot!(filters, command(&context, python_versions)
161180
.arg("--python-version=3.11")
162-
, @r###"<snapshot>
163-
"###
181+
, @r###"
182+
success: false
183+
exit_code: 1
184+
----- stdout -----
185+
186+
----- stderr -----
187+
warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead.
188+
× No solution found when resolving dependencies:
189+
╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
190+
And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable.
191+
"###
164192
);
165193

166194
output.assert().failure();
@@ -205,8 +233,17 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> {
205233
// used to build the source distributions.
206234
let output = uv_snapshot!(filters, command(&context, python_versions)
207235
.arg("--python-version=3.11")
208-
, @r###"<snapshot>
209-
"###
236+
, @r###"
237+
success: true
238+
exit_code: 0
239+
----- stdout -----
240+
# This file was autogenerated by uv via the following command:
241+
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11
242+
package-a==1.0.0
243+
244+
----- stderr -----
245+
Resolved 1 package in [TIME]
246+
"###
210247
);
211248

212249
output.assert().success().stdout(predicate::str::contains(
@@ -252,8 +289,17 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()>
252289
// determine its dependencies.
253290
let output = uv_snapshot!(filters, command(&context, python_versions)
254291
.arg("--python-version=3.11")
255-
, @r###"<snapshot>
256-
"###
292+
, @r###"
293+
success: false
294+
exit_code: 1
295+
----- stdout -----
296+
297+
----- stderr -----
298+
warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead.
299+
× No solution found when resolving dependencies:
300+
╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
301+
And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable.
302+
"###
257303
);
258304

259305
output.assert().failure();
@@ -301,8 +347,24 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> {
301347
// available, but is not compatible with the target version and cannot be used.
302348
let output = uv_snapshot!(filters, command(&context, python_versions)
303349
.arg("--python-version=3.11")
304-
, @r###"<snapshot>
305-
"###
350+
, @r###"
351+
success: false
352+
exit_code: 1
353+
----- stdout -----
354+
355+
----- stderr -----
356+
warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead.
357+
× No solution found when resolving dependencies:
358+
╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used.
359+
And because only the following versions of package-a are available:
360+
package-a==1.0.0
361+
package-a==2.0.0
362+
we can conclude that package-a<2.0.0 cannot be used. (1)
363+
364+
Because the requested Python version (3.11) does not satisfy Python>=3.12 and package-a==2.0.0 depends on Python>=3.12, we can conclude that package-a==2.0.0 cannot be used.
365+
And because we know from (1) that package-a<2.0.0 cannot be used, we can conclude that all versions of package-a cannot be used.
366+
And because you require package-a, we can conclude that the requirements are unsatisfiable.
367+
"###
306368
);
307369

308370
output.assert().failure();
@@ -340,8 +402,16 @@ fn python_patch_override_no_patch() -> Result<()> {
340402
// requirement is treated as 3.8.0.
341403
let output = uv_snapshot!(filters, command(&context, python_versions)
342404
.arg("--python-version=3.8")
343-
, @r###"<snapshot>
344-
"###
405+
, @r###"
406+
success: false
407+
exit_code: 1
408+
----- stdout -----
409+
410+
----- stderr -----
411+
× No solution found when resolving dependencies:
412+
╰─▶ Because the requested Python version (3.8) does not satisfy Python>=3.8.4 and package-a==1.0.0 depends on Python>=3.8.4, we can conclude that package-a==1.0.0 cannot be used.
413+
And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable.
414+
"###
345415
);
346416

347417
output.assert().failure();
@@ -377,8 +447,18 @@ fn python_patch_override_patch_compatible() -> Result<()> {
377447

378448
let output = uv_snapshot!(filters, command(&context, python_versions)
379449
.arg("--python-version=3.8.0")
380-
, @r###"<snapshot>
381-
"###
450+
, @r###"
451+
success: true
452+
exit_code: 0
453+
----- stdout -----
454+
# This file was autogenerated by uv via the following command:
455+
# uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.8.0
456+
package-a==1.0.0
457+
458+
----- stderr -----
459+
warning: The requested Python version 3.8.0 is not available; 3.8.18 will be used to build dependencies instead.
460+
Resolved 1 package in [TIME]
461+
"###
382462
);
383463

384464
output.assert().success().stdout(predicate::str::contains(

0 commit comments

Comments
 (0)