uv_resolver/resolver/environment.rs
1use std::collections::BTreeSet;
2use std::sync::Arc;
3
4use itertools::Itertools;
5use tracing::trace;
6
7use uv_distribution_types::{RequiresPython, RequiresPythonRange};
8use uv_pep440::VersionSpecifiers;
9use uv_pep508::{MarkerEnvironment, MarkerTree};
10use uv_pypi_types::{
11 ConflictItem, ConflictItemRef, ConflictKind, ConflictKindRef, ResolverMarkerEnvironment,
12};
13
14use crate::pubgrub::{PubGrubDependency, PubGrubPackage};
15use crate::resolver::ForkState;
16use crate::universal_marker::{ConflictMarker, UniversalMarker};
17use crate::{PythonRequirement, ResolveError};
18
19/// Represents one or more marker environments for a resolution.
20///
21/// Dependencies outside of the marker environments represented by this value
22/// are ignored for that particular resolution.
23///
24/// In normal "pip"-style resolution, one resolver environment corresponds to
25/// precisely one marker environment. In universal resolution, multiple marker
26/// environments may be specified via a PEP 508 marker expression. In either
27/// case, as mentioned above, dependencies not in these marker environments are
28/// ignored for the corresponding resolution.
29///
30/// Callers must provide this to the resolver to indicate, broadly, what kind
31/// of resolution it will produce. Generally speaking, callers should provide
32/// a specific marker environment for `uv pip`-style resolutions and ask for a
33/// universal resolution for uv's project based commands like `uv lock`.
34///
35/// Callers can rely on this type being reasonably cheap to clone.
36///
37/// # Internals
38///
39/// Inside the resolver, when doing a universal resolution, it may create
40/// many "forking" states to deal with the fact that there may be multiple
41/// incompatible dependency specifications. Specifically, in the Python world,
42/// the main constraint is that for any one *specific* marker environment,
43/// there must be only one version of a package in a corresponding resolution.
44/// But when doing a universal resolution, we want to support many marker
45/// environments, and in this context, the "universal" resolution may contain
46/// multiple versions of the same package. This is allowed so long as, for
47/// any marker environment supported by this resolution, an installation will
48/// select at most one version of any given package.
49///
50/// During resolution, a `ResolverEnvironment` is attached to each internal
51/// fork. For non-universal or "specific" resolution, there is only ever one
52/// fork because a `ResolverEnvironment` corresponds to one and exactly one
53/// marker environment. For universal resolution, the resolver may choose
54/// to split its execution into multiple branches. Each of those branches
55/// (also called "forks" or "splits") will get its own marker expression that
56/// represents a set of marker environments that is guaranteed to be disjoint
57/// with the marker environments described by the marker expressions of all
58/// other branches.
59///
60/// Whether it's universal resolution or not, and whether it's one of many
61/// forks or one fork, this type represents the set of possible dependency
62/// specifications allowed in the resolution produced by a single fork.
63///
64/// An exception to this is `requires-python`. That is handled separately and
65/// explicitly by the resolver. (Perhaps a future refactor can incorporate
66/// `requires-python` into this type as well, but it's not totally clear at
67/// time of writing if that's a good idea or not.)
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ResolverEnvironment {
70 kind: Kind,
71}
72
73/// The specific kind of resolver environment.
74///
75/// Note that it is explicitly intended that this type remain unexported from
76/// this module. The motivation for this design is to discourage repeated case
77/// analysis on this type, and instead try to encapsulate the case analysis via
78/// higher level routines on `ResolverEnvironment` itself. (This goal may prove
79/// intractable, so don't treat it like gospel.)
80#[derive(Clone, Debug, Eq, PartialEq)]
81enum Kind {
82 /// We're solving for one specific marker environment only.
83 ///
84 /// Generally, this is what's done for `uv pip`. For the project based
85 /// commands, like `uv lock`, we do universal resolution.
86 Specific {
87 /// The marker environment being resolved for.
88 ///
89 /// Any dependency specification that isn't satisfied by this marker
90 /// environment is ignored.
91 marker_env: ResolverMarkerEnvironment,
92 },
93 /// We're solving for all possible marker environments.
94 Universal {
95 /// The initial set of "fork preferences." These will come from the
96 /// lock file when available, or the list of supported environments
97 /// explicitly written into the `pyproject.toml`.
98 ///
99 /// Note that this may be empty, which means resolution should begin
100 /// with no forks. Or equivalently, a single fork whose marker
101 /// expression matches all marker environments.
102 initial_forks: Arc<[MarkerTree]>,
103 /// The markers associated with this resolver fork.
104 markers: MarkerTree,
105 /// Conflicting group inclusions.
106 ///
107 /// Inclusions are checked in `included_by_group` only when
108 /// a project-level exclusion exists for the same package:
109 /// an explicit inclusion overrides the project-level
110 /// exclusion, allowing a specific extra/group to remain
111 /// active even when the project as a whole is excluded.
112 ///
113 /// We also record inclusions because if we somehow wind up
114 /// with an inclusion and exclusion rule for the same conflict
115 /// item, then we treat the resulting fork as impossible.
116 /// (You cannot require that an extra is both included and
117 /// excluded. Such a rule can never be satisfied.) Finally,
118 /// we use the inclusion rules to write conflict markers
119 /// after resolution is finished.
120 include: Arc<crate::FxHashbrownSet<ConflictItem>>,
121 /// Conflicting group exclusions.
122 exclude: Arc<crate::FxHashbrownSet<ConflictItem>>,
123 },
124}
125
126impl ResolverEnvironment {
127 /// Create a resolver environment that is fixed to one and only one marker
128 /// environment.
129 ///
130 /// This enables `uv pip`-style resolutions. That is, the resolution
131 /// returned is only guaranteed to be installable for this specific marker
132 /// environment.
133 pub fn specific(marker_env: ResolverMarkerEnvironment) -> Self {
134 let kind = Kind::Specific { marker_env };
135 Self { kind }
136 }
137
138 /// Create a resolver environment for producing a multi-platform
139 /// resolution.
140 ///
141 /// The set of marker expressions given corresponds to an initial
142 /// seeded set of resolver branches. This might come from a lock file
143 /// corresponding to the set of forks produced by a previous resolution, or
144 /// it might come from a human crafted set of marker expressions.
145 ///
146 /// The "normal" case is that the initial forks are empty. When empty,
147 /// resolution will create forks as needed to deal with potentially
148 /// conflicting dependency specifications across distinct marker
149 /// environments.
150 ///
151 /// Initial forks with distinct lower Python bounds are ordered by the fork
152 /// strategy and resolution mode, the same way forks created during
153 /// resolution are. The given order still decides between forks that tie,
154 /// although we don't guarantee any specific treatment (similar to, at time
155 /// of writing, how the order of dependencies specified is also significant
156 /// but has no specific guarantees around it).
157 pub fn universal(initial_forks: Vec<MarkerTree>) -> Self {
158 let kind = Kind::Universal {
159 initial_forks: initial_forks.into(),
160 markers: MarkerTree::TRUE,
161 include: Arc::new(crate::FxHashbrownSet::default()),
162 exclude: Arc::new(crate::FxHashbrownSet::default()),
163 };
164 Self { kind }
165 }
166
167 /// Returns the marker environment corresponding to this resolver
168 /// environment.
169 ///
170 /// This only returns a marker environment when resolving for a specific
171 /// marker environment. i.e., A non-universal or "pip"-style resolution.
172 pub fn marker_environment(&self) -> Option<&MarkerEnvironment> {
173 match self.kind {
174 Kind::Specific { ref marker_env } => Some(marker_env),
175 Kind::Universal { .. } => None,
176 }
177 }
178
179 /// Returns `false` only when this environment is a fork and it is disjoint
180 /// with the given marker.
181 pub(crate) fn included_by_marker(&self, marker: MarkerTree) -> bool {
182 match self.kind {
183 Kind::Specific { .. } => true,
184 Kind::Universal { ref markers, .. } => !markers.is_disjoint(marker),
185 }
186 }
187
188 /// Returns true if the dependency represented by this forker may be
189 /// included in the given resolver environment.
190 pub(crate) fn included_by_group(&self, group: ConflictItemRef<'_>) -> bool {
191 match self.kind {
192 Kind::Specific { .. } => true,
193 Kind::Universal {
194 ref include,
195 ref exclude,
196 ..
197 } => {
198 if exclude.contains(&group) {
199 return false;
200 }
201 // When a project-level conflict item is excluded, the
202 // project's extras should be excluded too (unless they
203 // are explicitly included). This is because extras
204 // transitively depend on the base package, so leaving
205 // them in a fork that excludes the project would pull
206 // the project's dependencies back in.
207 //
208 // Groups, on the other hand, do NOT depend on the base
209 // package — they are independent dependency sets — so
210 // they can safely remain active even when the project
211 // itself is excluded.
212 if matches!(group.kind(), ConflictKindRef::Extra(_)) {
213 if exclude.contains(&ConflictItemRef::from(group.package())) {
214 // But if this specific extra is explicitly
215 // included (e.g., in a conflict between a project
216 // and one of its own extras), respect the inclusion.
217 return include.contains(&group);
218 }
219 }
220 true
221 }
222 }
223 }
224
225 /// Returns the bounding Python versions that can satisfy this
226 /// resolver environment's marker, if it's constrained.
227 pub(crate) fn requires_python(&self) -> Option<RequiresPythonRange> {
228 let Kind::Universal {
229 markers: pep508_marker,
230 ..
231 } = self.kind
232 else {
233 return None;
234 };
235 crate::marker::requires_python(pep508_marker)
236 }
237
238 /// For a universal resolution, return the markers of the current fork.
239 pub(crate) fn fork_markers(&self) -> Option<MarkerTree> {
240 match self.kind {
241 Kind::Specific { .. } => None,
242 Kind::Universal { markers, .. } => Some(markers),
243 }
244 }
245
246 /// Narrow this environment given the forking markers.
247 ///
248 /// This effectively intersects any markers in this environment with the
249 /// markers given, and returns the new resulting environment.
250 ///
251 /// This is also useful in tests to generate a "forked" marker environment.
252 ///
253 /// # Panics
254 ///
255 /// This panics if the resolver environment corresponds to one and only one
256 /// specific marker environment. i.e., "pip"-style resolution.
257 fn narrow_environment(&self, rhs: MarkerTree) -> Self {
258 match self.kind {
259 Kind::Specific { .. } => {
260 unreachable!("environment narrowing only happens in universal resolution")
261 }
262 Kind::Universal {
263 ref initial_forks,
264 markers: ref lhs,
265 ref include,
266 ref exclude,
267 } => {
268 let mut markers = *lhs;
269 markers = markers.and(rhs);
270 let kind = Kind::Universal {
271 initial_forks: Arc::clone(initial_forks),
272 markers,
273 include: Arc::clone(include),
274 exclude: Arc::clone(exclude),
275 };
276 Self { kind }
277 }
278 }
279 }
280
281 /// Returns a new resolver environment with the given groups included or
282 /// excluded from it. An `Ok` variant indicates an include rule while an
283 /// `Err` variant indicates en exclude rule.
284 ///
285 /// When a group is excluded from a resolver environment,
286 /// `ResolverEnvironment::included_by_group` will return false. The idea
287 /// is that a dependency with a corresponding group should be excluded by
288 /// forks in the resolver with this environment. (Include rules also
289 /// affect `included_by_group`: when a project-level exclusion exists,
290 /// an explicit inclusion for a specific extra overrides it.)
291 ///
292 /// If calling this routine results in the same conflict item being both
293 /// included and excluded, then this returns `None` (since it would
294 /// otherwise result in a fork that can never be satisfied).
295 ///
296 /// # Panics
297 ///
298 /// This panics if the resolver environment corresponds to one and only one
299 /// specific marker environment. i.e., "pip"-style resolution.
300 pub(crate) fn filter_by_group(
301 &self,
302 rules: impl IntoIterator<Item = Result<ConflictItem, ConflictItem>>,
303 ) -> Option<Self> {
304 match self.kind {
305 Kind::Specific { .. } => {
306 unreachable!("environment narrowing only happens in universal resolution")
307 }
308 Kind::Universal {
309 ref initial_forks,
310 ref markers,
311 ref include,
312 ref exclude,
313 } => {
314 let mut include: crate::FxHashbrownSet<_> = (**include).clone();
315 let mut exclude: crate::FxHashbrownSet<_> = (**exclude).clone();
316 for rule in rules {
317 match rule {
318 Ok(item) => {
319 if exclude.contains(&item) {
320 return None;
321 }
322 include.insert(item);
323 }
324 Err(item) => {
325 if include.contains(&item) {
326 return None;
327 }
328 exclude.insert(item);
329 }
330 }
331 }
332 let kind = Kind::Universal {
333 initial_forks: Arc::clone(initial_forks),
334 markers: *markers,
335 include: Arc::new(include),
336 exclude: Arc::new(exclude),
337 };
338 Some(Self { kind })
339 }
340 }
341 }
342
343 /// Create an initial set of forked states based on this resolver
344 /// environment configuration.
345 ///
346 /// In the "clean" universal case, this just returns a singleton `Vec` with
347 /// the given fork state. But when the resolver is configured to start
348 /// with an initial set of forked resolver states (e.g., those present in
349 /// a lock file), then this creates the initial set of forks from that
350 /// configuration.
351 pub(crate) fn initial_forked_states(
352 &self,
353 init: ForkState,
354 ) -> Result<Vec<ForkState>, ResolveError> {
355 let Kind::Universal {
356 ref initial_forks,
357 markers: ref _markers,
358 include: ref _include,
359 exclude: ref _exclude,
360 } = self.kind
361 else {
362 return Ok(vec![init]);
363 };
364 if initial_forks.is_empty() {
365 return Ok(vec![init]);
366 }
367 initial_forks
368 .iter()
369 .rev()
370 .filter_map(|&initial_fork| {
371 let combined = UniversalMarker::from_combined(initial_fork);
372 let (include, exclude) = match combined.conflict().filter_rules() {
373 Ok(rules) => rules,
374 Err(err) => return Some(Err(err)),
375 };
376 let mut env = self.filter_by_group(
377 include
378 .into_iter()
379 .map(Ok)
380 .chain(exclude.into_iter().map(Err)),
381 )?;
382 env = env.narrow_environment(combined.pep508());
383 Some(Ok(init.clone().with_env(env)))
384 })
385 .collect()
386 }
387
388 /// Narrow the [`PythonRequirement`] if this resolver environment
389 /// corresponds to a more constraining fork.
390 ///
391 /// For example, if this is a fork where `python_version >= '3.12'` is
392 /// always true, and if the given python requirement (perhaps derived from
393 /// `Requires-Python`) is `>=3.10`, then this will "narrow" the requirement
394 /// to `>=3.12`, corresponding to the marker expression describing this
395 /// fork.
396 ///
397 /// If this environment is not a fork, then this returns `None`.
398 pub(crate) fn narrow_python_requirement(
399 &self,
400 python_requirement: &PythonRequirement,
401 ) -> Option<PythonRequirement> {
402 python_requirement.narrow(&self.requires_python()?)
403 }
404
405 /// Returns a message formatted for end users representing a fork in the
406 /// resolver.
407 ///
408 /// If this resolver environment does not correspond to a particular fork,
409 /// then `None` is returned.
410 ///
411 /// This is useful in contexts where one wants to display a message
412 /// relating to a particular fork, but either no message or an entirely
413 /// different message when this isn't a fork.
414 pub(crate) fn end_user_fork_display(&self) -> Option<String> {
415 match &self.kind {
416 Kind::Specific { .. } => None,
417 Kind::Universal {
418 initial_forks: _,
419 markers,
420 include,
421 exclude,
422 } => {
423 let format_conflict_item = |conflict_item: &ConflictItem| {
424 format!(
425 "{}{}",
426 conflict_item.package(),
427 match conflict_item.kind() {
428 ConflictKind::Extra(extra) => format!("[{extra}]"),
429 ConflictKind::Group(group) => {
430 format!("[group:{group}]")
431 }
432 ConflictKind::Project => String::new(),
433 }
434 )
435 };
436
437 if markers.is_true() && include.is_empty() && exclude.is_empty() {
438 return None;
439 }
440
441 let mut descriptors = Vec::new();
442 if !markers.is_true() {
443 descriptors.push(format!("markers: {markers:?}"));
444 }
445 if !include.is_empty() {
446 descriptors.push(format!(
447 "included: {}",
448 // Sort to ensure stable error messages
449 include
450 .iter()
451 .map(format_conflict_item)
452 .collect::<BTreeSet<_>>()
453 .into_iter()
454 .join(", "),
455 ));
456 }
457 if !exclude.is_empty() {
458 descriptors.push(format!(
459 "excluded: {}",
460 // Sort to ensure stable error messages
461 exclude
462 .iter()
463 .map(format_conflict_item)
464 .collect::<BTreeSet<_>>()
465 .into_iter()
466 .join(", "),
467 ));
468 }
469
470 Some(format!("split ({})", descriptors.join("; ")))
471 }
472 }
473 }
474
475 /// Creates a universal marker expression corresponding to the fork that is
476 /// represented by this resolver environment. A universal marker includes
477 /// not just the standard PEP 508 marker, but also a marker based on
478 /// conflicting extras/groups.
479 ///
480 /// This returns `None` when this does not correspond to a fork.
481 pub(crate) fn try_universal_markers(&self) -> Option<UniversalMarker> {
482 match self.kind {
483 Kind::Specific { .. } => None,
484 Kind::Universal {
485 ref markers,
486 ref include,
487 ref exclude,
488 ..
489 } => {
490 let mut conflict_marker = ConflictMarker::TRUE;
491 for item in exclude.iter() {
492 conflict_marker =
493 conflict_marker.and(ConflictMarker::from_conflict_item(item).negate());
494 }
495 for item in include.iter() {
496 conflict_marker = conflict_marker.and(ConflictMarker::from_conflict_item(item));
497 }
498 Some(UniversalMarker::new(*markers, conflict_marker))
499 }
500 }
501 }
502}
503
504/// A user visible representation of a resolver environment.
505///
506/// This is most useful in error and log messages.
507impl std::fmt::Display for ResolverEnvironment {
508 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
509 match self.kind {
510 Kind::Specific { .. } => write!(f, "marker environment"),
511 Kind::Universal { ref markers, .. } => {
512 if markers.is_true() {
513 write!(f, "all marker environments")
514 } else {
515 write!(f, "split `{markers:?}`")
516 }
517 }
518 }
519 }
520}
521
522/// The different forking possibilities.
523///
524/// Upon seeing a dependency, when determining whether to fork, three
525/// different cases are possible:
526///
527/// 1. Forking cannot be ruled out.
528/// 2. The dependency is excluded by the "parent" fork.
529/// 3. The dependency is unconditional and thus cannot provoke new forks.
530///
531/// This enum encapsulates those possibilities. In the first case, a helper is
532/// returned to help management the nuts and bolts of forking.
533#[derive(Debug)]
534pub(crate) enum ForkingPossibility<'d> {
535 Possible(Forker<'d>),
536 DependencyAlwaysExcluded,
537 NoForkingPossible,
538}
539
540impl<'d> ForkingPossibility<'d> {
541 pub(crate) fn new(env: &ResolverEnvironment, dep: &'d PubGrubDependency) -> Self {
542 let marker = dep.package.marker();
543 if !env.included_by_marker(marker) {
544 ForkingPossibility::DependencyAlwaysExcluded
545 } else if marker.is_true() {
546 ForkingPossibility::NoForkingPossible
547 } else {
548 let forker = Forker {
549 package: &dep.package,
550 marker,
551 };
552 ForkingPossibility::Possible(forker)
553 }
554 }
555}
556
557/// An encapsulation of forking based on a single dependency.
558#[derive(Debug)]
559pub(crate) struct Forker<'d> {
560 package: &'d PubGrubPackage,
561 marker: MarkerTree,
562}
563
564impl Forker<'_> {
565 /// Attempt a fork based on the given resolver environment.
566 ///
567 /// If a fork is possible, then a new forker and at least one new
568 /// resolver environment is returned. In some cases, it is possible for
569 /// more resolver environments to be returned. (For example, when the
570 /// negation of this forker's markers has overlap with the given resolver
571 /// environment.)
572 pub(crate) fn fork(
573 &self,
574 env: &ResolverEnvironment,
575 ) -> Option<(Self, Vec<ResolverEnvironment>)> {
576 if !env.included_by_marker(self.marker) {
577 return None;
578 }
579
580 let Kind::Universal {
581 markers: ref env_marker,
582 ..
583 } = env.kind
584 else {
585 panic!("resolver must be in universal mode for forking")
586 };
587
588 let mut envs = vec![];
589 {
590 let not_marker = self.marker.negate();
591 if !env_marker.is_disjoint(not_marker) {
592 envs.push(env.narrow_environment(not_marker));
593 }
594 }
595 // Note also that we push this one last for historical reasons.
596 // Changing the order of forks can change the output in some
597 // ways. While it's probably fine, we try to avoid changing the
598 // output.
599 envs.push(env.narrow_environment(self.marker));
600
601 let mut remaining_marker = self.marker;
602 remaining_marker = remaining_marker.and(env_marker.negate());
603 let remaining_forker = Forker {
604 package: self.package,
605 marker: remaining_marker,
606 };
607 Some((remaining_forker, envs))
608 }
609
610 /// Returns true if the dependency represented by this forker may be
611 /// included in the given resolver environment.
612 pub(crate) fn included(&self, env: &ResolverEnvironment) -> bool {
613 let marker = self.package.marker();
614 env.included_by_marker(marker)
615 }
616}
617
618/// Fork the resolver based on a `Requires-Python` specifier.
619pub(crate) fn fork_version_by_python_requirement(
620 requires_python: &VersionSpecifiers,
621 python_requirement: &PythonRequirement,
622 env: &ResolverEnvironment,
623) -> Vec<ResolverEnvironment> {
624 let requires_python = RequiresPython::from_specifiers(requires_python.clone());
625 let lower = requires_python.range().lower().clone();
626
627 // Attempt to split the current Python requirement based on the `requires-python` specifier.
628 //
629 // For example, if the current requirement is `>=3.10`, and the split point is `>=3.11`, then
630 // the result will be `>=3.10 and <3.11` and `>=3.11`.
631 //
632 // However, if the current requirement is `>=3.10`, and the split point is `>=3.9`, then the
633 // lower segment will be empty, so we should return an empty list.
634 let Some((lower, upper)) = python_requirement.split(lower.into()) else {
635 trace!(
636 "Unable to split Python requirement `{}` via `Requires-Python` specifier `{}`",
637 python_requirement.target(),
638 requires_python,
639 );
640 return vec![];
641 };
642
643 let Kind::Universal {
644 markers: ref env_marker,
645 ..
646 } = env.kind
647 else {
648 panic!("resolver must be in universal mode for forking")
649 };
650
651 let mut envs = vec![];
652 if !env_marker.is_disjoint(lower.to_marker_tree()) {
653 envs.push(env.narrow_environment(lower.to_marker_tree()));
654 }
655 if !env_marker.is_disjoint(upper.to_marker_tree()) {
656 envs.push(env.narrow_environment(upper.to_marker_tree()));
657 }
658 debug_assert!(!envs.is_empty(), "at least one fork should be produced");
659 envs
660}
661
662/// Fork the resolver based on a marker.
663pub(crate) fn fork_version_by_marker(
664 env: &ResolverEnvironment,
665 marker: MarkerTree,
666) -> Option<(ResolverEnvironment, ResolverEnvironment)> {
667 let Kind::Universal {
668 markers: ref env_marker,
669 ..
670 } = env.kind
671 else {
672 panic!("resolver must be in universal mode for forking")
673 };
674
675 // Attempt to split based on the marker.
676 //
677 // For example, given `python_version >= '3.10'` and the split marker `sys_platform == 'linux'`,
678 // the result will be:
679 //
680 // `python_version >= '3.10' and sys_platform == 'linux'`
681 // `python_version >= '3.10' and sys_platform != 'linux'`
682 //
683 // If the marker is disjoint with the current environment, then we should return an empty list.
684 // If the marker complement is disjoint with the current environment, then we should also return
685 // an empty list.
686 //
687 // For example, given `python_version >= '3.10' and sys_platform == 'linux'` and the split marker
688 // `sys_platform == 'win32'`, return an empty list, since the following isn't satisfiable:
689 //
690 // python_version >= '3.10' and sys_platform == 'linux' and sys_platform == 'win32'
691 if env_marker.is_disjoint(marker) {
692 return None;
693 }
694 let with_marker = env.narrow_environment(marker);
695
696 let complement = marker.negate();
697 if env_marker.is_disjoint(complement) {
698 return None;
699 }
700 let without_marker = env.narrow_environment(complement);
701
702 Some((with_marker, without_marker))
703}
704
705#[cfg(test)]
706mod tests {
707 use std::ops::Bound;
708 use std::sync::LazyLock;
709
710 use uv_pep440::{LowerBound, UpperBound, Version};
711 use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
712
713 use uv_distribution_types::{RequiresPython, RequiresPythonRange};
714
715 use super::*;
716
717 /// A dummy marker environment used in tests below.
718 ///
719 /// It doesn't matter too much what we use here, and indeed, this one was
720 /// copied from our uv microbenchmarks.
721 static MARKER_ENV: LazyLock<MarkerEnvironment> = LazyLock::new(|| {
722 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
723 implementation_name: "cpython",
724 implementation_version: "3.11.5",
725 os_name: "posix",
726 platform_machine: "arm64",
727 platform_python_implementation: "CPython",
728 platform_release: "21.6.0",
729 platform_system: "Darwin",
730 platform_version: "Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000",
731 python_full_version: "3.11.5",
732 python_version: "3.11",
733 sys_platform: "darwin",
734 }).unwrap()
735 });
736
737 fn requires_python_lower(lower_version_bound: &str) -> RequiresPython {
738 RequiresPython::greater_than_equal_version(&version(lower_version_bound))
739 }
740
741 fn requires_python_range_lower(lower_version_bound: &str) -> RequiresPythonRange {
742 let lower = LowerBound::new(Bound::Included(version(lower_version_bound)));
743 RequiresPythonRange::new(lower, UpperBound::default())
744 }
745
746 fn marker(marker: &str) -> MarkerTree {
747 marker
748 .parse::<MarkerTree>()
749 .expect("valid pep508 marker expression")
750 }
751
752 fn version(v: &str) -> Version {
753 v.parse().expect("valid pep440 version string")
754 }
755
756 fn python_requirement(python_version_greater_than_equal: &str) -> PythonRequirement {
757 let requires_python = requires_python_lower(python_version_greater_than_equal);
758 PythonRequirement::from_marker_environment(&MARKER_ENV, requires_python)
759 }
760
761 /// Tests that narrowing a Python requirement when resolving for a
762 /// specific marker environment never produces a more constrained Python
763 /// requirement.
764 #[test]
765 fn narrow_python_requirement_specific() {
766 let resolver_marker_env = ResolverMarkerEnvironment::from(MARKER_ENV.clone());
767 let resolver_env = ResolverEnvironment::specific(resolver_marker_env);
768
769 let pyreq = python_requirement("3.10");
770 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
771
772 let pyreq = python_requirement("3.11");
773 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
774
775 let pyreq = python_requirement("3.12");
776 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
777 }
778
779 /// Tests that narrowing a Python requirement during a universal resolution
780 /// *without* any forks will never produce a more constrained Python
781 /// requirement.
782 #[test]
783 fn narrow_python_requirement_universal() {
784 let resolver_env = ResolverEnvironment::universal(vec![]);
785
786 let pyreq = python_requirement("3.10");
787 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
788
789 let pyreq = python_requirement("3.11");
790 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
791
792 let pyreq = python_requirement("3.12");
793 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
794 }
795
796 /// Inside a fork whose marker's Python requirement is equal
797 /// to our Requires-Python means that narrowing does not produce
798 /// a result.
799 #[test]
800 fn narrow_python_requirement_forking_no_op() {
801 let pyreq = python_requirement("3.10");
802 let resolver_env = ResolverEnvironment::universal(vec![])
803 .narrow_environment(marker("python_version >= '3.10'"));
804 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
805 }
806
807 /// In this test, we narrow a more relaxed requirement compared to the
808 /// marker for the current fork. This in turn results in a stricter
809 /// requirement corresponding to what's specified in the fork.
810 #[test]
811 fn narrow_python_requirement_forking_stricter() {
812 let pyreq = python_requirement("3.10");
813 let resolver_env = ResolverEnvironment::universal(vec![])
814 .narrow_environment(marker("python_version >= '3.11'"));
815 let expected = {
816 let range = requires_python_range_lower("3.11");
817 let requires_python = requires_python_lower("3.10").narrow(&range).unwrap();
818 PythonRequirement::from_marker_environment(&MARKER_ENV, requires_python)
819 };
820 assert_eq!(
821 resolver_env.narrow_python_requirement(&pyreq),
822 Some(expected)
823 );
824 }
825
826 /// In this test, we narrow a stricter requirement compared to the marker
827 /// for the current fork. This in turn results in a requirement that
828 /// remains unchanged.
829 #[test]
830 fn narrow_python_requirement_forking_relaxed() {
831 let pyreq = python_requirement("3.11");
832 let resolver_env = ResolverEnvironment::universal(vec![])
833 .narrow_environment(marker("python_version >= '3.10'"));
834 assert_eq!(
835 resolver_env.narrow_python_requirement(&pyreq),
836 Some(python_requirement("3.11")),
837 );
838 }
839}