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 /// The order of the initial forks is significant, although we don't
152 /// guarantee any specific treatment (similar to, at time of writing, how
153 /// the order of dependencies specified is also significant but has no
154 /// specific guarantees around it). Changing the ordering can help when our
155 /// custom fork prioritization fails.
156 pub fn universal(initial_forks: Vec<MarkerTree>) -> Self {
157 let kind = Kind::Universal {
158 initial_forks: initial_forks.into(),
159 markers: MarkerTree::TRUE,
160 include: Arc::new(crate::FxHashbrownSet::default()),
161 exclude: Arc::new(crate::FxHashbrownSet::default()),
162 };
163 Self { kind }
164 }
165
166 /// Returns the marker environment corresponding to this resolver
167 /// environment.
168 ///
169 /// This only returns a marker environment when resolving for a specific
170 /// marker environment. i.e., A non-universal or "pip"-style resolution.
171 pub fn marker_environment(&self) -> Option<&MarkerEnvironment> {
172 match self.kind {
173 Kind::Specific { ref marker_env } => Some(marker_env),
174 Kind::Universal { .. } => None,
175 }
176 }
177
178 /// Returns `false` only when this environment is a fork and it is disjoint
179 /// with the given marker.
180 pub(crate) fn included_by_marker(&self, marker: MarkerTree) -> bool {
181 match self.kind {
182 Kind::Specific { .. } => true,
183 Kind::Universal { ref markers, .. } => !markers.is_disjoint(marker),
184 }
185 }
186
187 /// Returns true if the dependency represented by this forker may be
188 /// included in the given resolver environment.
189 pub(crate) fn included_by_group(&self, group: ConflictItemRef<'_>) -> bool {
190 match self.kind {
191 Kind::Specific { .. } => true,
192 Kind::Universal {
193 ref include,
194 ref exclude,
195 ..
196 } => {
197 if exclude.contains(&group) {
198 return false;
199 }
200 // When a project-level conflict item is excluded, the
201 // project's extras should be excluded too (unless they
202 // are explicitly included). This is because extras
203 // transitively depend on the base package, so leaving
204 // them in a fork that excludes the project would pull
205 // the project's dependencies back in.
206 //
207 // Groups, on the other hand, do NOT depend on the base
208 // package — they are independent dependency sets — so
209 // they can safely remain active even when the project
210 // itself is excluded.
211 if matches!(group.kind(), ConflictKindRef::Extra(_)) {
212 if exclude.contains(&ConflictItemRef::from(group.package())) {
213 // But if this specific extra is explicitly
214 // included (e.g., in a conflict between a project
215 // and one of its own extras), respect the inclusion.
216 return include.contains(&group);
217 }
218 }
219 true
220 }
221 }
222 }
223
224 /// Returns the bounding Python versions that can satisfy this
225 /// resolver environment's marker, if it's constrained.
226 pub(crate) fn requires_python(&self) -> Option<RequiresPythonRange> {
227 let Kind::Universal {
228 markers: pep508_marker,
229 ..
230 } = self.kind
231 else {
232 return None;
233 };
234 crate::marker::requires_python(pep508_marker)
235 }
236
237 /// For a universal resolution, return the markers of the current fork.
238 pub(crate) fn fork_markers(&self) -> Option<MarkerTree> {
239 match self.kind {
240 Kind::Specific { .. } => None,
241 Kind::Universal { markers, .. } => Some(markers),
242 }
243 }
244
245 /// Narrow this environment given the forking markers.
246 ///
247 /// This effectively intersects any markers in this environment with the
248 /// markers given, and returns the new resulting environment.
249 ///
250 /// This is also useful in tests to generate a "forked" marker environment.
251 ///
252 /// # Panics
253 ///
254 /// This panics if the resolver environment corresponds to one and only one
255 /// specific marker environment. i.e., "pip"-style resolution.
256 fn narrow_environment(&self, rhs: MarkerTree) -> Self {
257 match self.kind {
258 Kind::Specific { .. } => {
259 unreachable!("environment narrowing only happens in universal resolution")
260 }
261 Kind::Universal {
262 ref initial_forks,
263 markers: ref lhs,
264 ref include,
265 ref exclude,
266 } => {
267 let mut markers = *lhs;
268 markers.and(rhs);
269 let kind = Kind::Universal {
270 initial_forks: Arc::clone(initial_forks),
271 markers,
272 include: Arc::clone(include),
273 exclude: Arc::clone(exclude),
274 };
275 Self { kind }
276 }
277 }
278 }
279
280 /// Returns a new resolver environment with the given groups included or
281 /// excluded from it. An `Ok` variant indicates an include rule while an
282 /// `Err` variant indicates en exclude rule.
283 ///
284 /// When a group is excluded from a resolver environment,
285 /// `ResolverEnvironment::included_by_group` will return false. The idea
286 /// is that a dependency with a corresponding group should be excluded by
287 /// forks in the resolver with this environment. (Include rules also
288 /// affect `included_by_group`: when a project-level exclusion exists,
289 /// an explicit inclusion for a specific extra overrides it.)
290 ///
291 /// If calling this routine results in the same conflict item being both
292 /// included and excluded, then this returns `None` (since it would
293 /// otherwise result in a fork that can never be satisfied).
294 ///
295 /// # Panics
296 ///
297 /// This panics if the resolver environment corresponds to one and only one
298 /// specific marker environment. i.e., "pip"-style resolution.
299 pub(crate) fn filter_by_group(
300 &self,
301 rules: impl IntoIterator<Item = Result<ConflictItem, ConflictItem>>,
302 ) -> Option<Self> {
303 match self.kind {
304 Kind::Specific { .. } => {
305 unreachable!("environment narrowing only happens in universal resolution")
306 }
307 Kind::Universal {
308 ref initial_forks,
309 ref markers,
310 ref include,
311 ref exclude,
312 } => {
313 let mut include: crate::FxHashbrownSet<_> = (**include).clone();
314 let mut exclude: crate::FxHashbrownSet<_> = (**exclude).clone();
315 for rule in rules {
316 match rule {
317 Ok(item) => {
318 if exclude.contains(&item) {
319 return None;
320 }
321 include.insert(item);
322 }
323 Err(item) => {
324 if include.contains(&item) {
325 return None;
326 }
327 exclude.insert(item);
328 }
329 }
330 }
331 let kind = Kind::Universal {
332 initial_forks: Arc::clone(initial_forks),
333 markers: *markers,
334 include: Arc::new(include),
335 exclude: Arc::new(exclude),
336 };
337 Some(Self { kind })
338 }
339 }
340 }
341
342 /// Create an initial set of forked states based on this resolver
343 /// environment configuration.
344 ///
345 /// In the "clean" universal case, this just returns a singleton `Vec` with
346 /// the given fork state. But when the resolver is configured to start
347 /// with an initial set of forked resolver states (e.g., those present in
348 /// a lock file), then this creates the initial set of forks from that
349 /// configuration.
350 pub(crate) fn initial_forked_states(
351 &self,
352 init: ForkState,
353 ) -> Result<Vec<ForkState>, ResolveError> {
354 let Kind::Universal {
355 ref initial_forks,
356 markers: ref _markers,
357 include: ref _include,
358 exclude: ref _exclude,
359 } = self.kind
360 else {
361 return Ok(vec![init]);
362 };
363 if initial_forks.is_empty() {
364 return Ok(vec![init]);
365 }
366 initial_forks
367 .iter()
368 .rev()
369 .filter_map(|&initial_fork| {
370 let combined = UniversalMarker::from_combined(initial_fork);
371 let (include, exclude) = match combined.conflict().filter_rules() {
372 Ok(rules) => rules,
373 Err(err) => return Some(Err(err)),
374 };
375 let mut env = self.filter_by_group(
376 include
377 .into_iter()
378 .map(Ok)
379 .chain(exclude.into_iter().map(Err)),
380 )?;
381 env = env.narrow_environment(combined.pep508());
382 Some(Ok(init.clone().with_env(env)))
383 })
384 .collect()
385 }
386
387 /// Narrow the [`PythonRequirement`] if this resolver environment
388 /// corresponds to a more constraining fork.
389 ///
390 /// For example, if this is a fork where `python_version >= '3.12'` is
391 /// always true, and if the given python requirement (perhaps derived from
392 /// `Requires-Python`) is `>=3.10`, then this will "narrow" the requirement
393 /// to `>=3.12`, corresponding to the marker expression describing this
394 /// fork.
395 ///
396 /// If this environment is not a fork, then this returns `None`.
397 pub(crate) fn narrow_python_requirement(
398 &self,
399 python_requirement: &PythonRequirement,
400 ) -> Option<PythonRequirement> {
401 python_requirement.narrow(&self.requires_python()?)
402 }
403
404 /// Returns a message formatted for end users representing a fork in the
405 /// resolver.
406 ///
407 /// If this resolver environment does not correspond to a particular fork,
408 /// then `None` is returned.
409 ///
410 /// This is useful in contexts where one wants to display a message
411 /// relating to a particular fork, but either no message or an entirely
412 /// different message when this isn't a fork.
413 pub(crate) fn end_user_fork_display(&self) -> Option<String> {
414 match &self.kind {
415 Kind::Specific { .. } => None,
416 Kind::Universal {
417 initial_forks: _,
418 markers,
419 include,
420 exclude,
421 } => {
422 let format_conflict_item = |conflict_item: &ConflictItem| {
423 format!(
424 "{}{}",
425 conflict_item.package(),
426 match conflict_item.kind() {
427 ConflictKind::Extra(extra) => format!("[{extra}]"),
428 ConflictKind::Group(group) => {
429 format!("[group:{group}]")
430 }
431 ConflictKind::Project => String::new(),
432 }
433 )
434 };
435
436 if markers.is_true() && include.is_empty() && exclude.is_empty() {
437 return None;
438 }
439
440 let mut descriptors = Vec::new();
441 if !markers.is_true() {
442 descriptors.push(format!("markers: {markers:?}"));
443 }
444 if !include.is_empty() {
445 descriptors.push(format!(
446 "included: {}",
447 // Sort to ensure stable error messages
448 include
449 .iter()
450 .map(format_conflict_item)
451 .collect::<BTreeSet<_>>()
452 .into_iter()
453 .join(", "),
454 ));
455 }
456 if !exclude.is_empty() {
457 descriptors.push(format!(
458 "excluded: {}",
459 // Sort to ensure stable error messages
460 exclude
461 .iter()
462 .map(format_conflict_item)
463 .collect::<BTreeSet<_>>()
464 .into_iter()
465 .join(", "),
466 ));
467 }
468
469 Some(format!("split ({})", descriptors.join("; ")))
470 }
471 }
472 }
473
474 /// Creates a universal marker expression corresponding to the fork that is
475 /// represented by this resolver environment. A universal marker includes
476 /// not just the standard PEP 508 marker, but also a marker based on
477 /// conflicting extras/groups.
478 ///
479 /// This returns `None` when this does not correspond to a fork.
480 pub(crate) fn try_universal_markers(&self) -> Option<UniversalMarker> {
481 match self.kind {
482 Kind::Specific { .. } => None,
483 Kind::Universal {
484 ref markers,
485 ref include,
486 ref exclude,
487 ..
488 } => {
489 let mut conflict_marker = ConflictMarker::TRUE;
490 for item in exclude.iter() {
491 conflict_marker =
492 conflict_marker.and(ConflictMarker::from_conflict_item(item).negate());
493 }
494 for item in include.iter() {
495 conflict_marker = conflict_marker.and(ConflictMarker::from_conflict_item(item));
496 }
497 Some(UniversalMarker::new(*markers, conflict_marker))
498 }
499 }
500 }
501}
502
503/// A user visible representation of a resolver environment.
504///
505/// This is most useful in error and log messages.
506impl std::fmt::Display for ResolverEnvironment {
507 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
508 match self.kind {
509 Kind::Specific { .. } => write!(f, "marker environment"),
510 Kind::Universal { ref markers, .. } => {
511 if markers.is_true() {
512 write!(f, "all marker environments")
513 } else {
514 write!(f, "split `{markers:?}`")
515 }
516 }
517 }
518 }
519}
520
521/// The different forking possibilities.
522///
523/// Upon seeing a dependency, when determining whether to fork, three
524/// different cases are possible:
525///
526/// 1. Forking cannot be ruled out.
527/// 2. The dependency is excluded by the "parent" fork.
528/// 3. The dependency is unconditional and thus cannot provoke new forks.
529///
530/// This enum encapsulates those possibilities. In the first case, a helper is
531/// returned to help management the nuts and bolts of forking.
532#[derive(Debug)]
533pub(crate) enum ForkingPossibility<'d> {
534 Possible(Forker<'d>),
535 DependencyAlwaysExcluded,
536 NoForkingPossible,
537}
538
539impl<'d> ForkingPossibility<'d> {
540 pub(crate) fn new(env: &ResolverEnvironment, dep: &'d PubGrubDependency) -> Self {
541 let marker = dep.package.marker();
542 if !env.included_by_marker(marker) {
543 ForkingPossibility::DependencyAlwaysExcluded
544 } else if marker.is_true() {
545 ForkingPossibility::NoForkingPossible
546 } else {
547 let forker = Forker {
548 package: &dep.package,
549 marker,
550 };
551 ForkingPossibility::Possible(forker)
552 }
553 }
554}
555
556/// An encapsulation of forking based on a single dependency.
557#[derive(Debug)]
558pub(crate) struct Forker<'d> {
559 package: &'d PubGrubPackage,
560 marker: MarkerTree,
561}
562
563impl Forker<'_> {
564 /// Attempt a fork based on the given resolver environment.
565 ///
566 /// If a fork is possible, then a new forker and at least one new
567 /// resolver environment is returned. In some cases, it is possible for
568 /// more resolver environments to be returned. (For example, when the
569 /// negation of this forker's markers has overlap with the given resolver
570 /// environment.)
571 pub(crate) fn fork(
572 &self,
573 env: &ResolverEnvironment,
574 ) -> Option<(Self, Vec<ResolverEnvironment>)> {
575 if !env.included_by_marker(self.marker) {
576 return None;
577 }
578
579 let Kind::Universal {
580 markers: ref env_marker,
581 ..
582 } = env.kind
583 else {
584 panic!("resolver must be in universal mode for forking")
585 };
586
587 let mut envs = vec![];
588 {
589 let not_marker = self.marker.negate();
590 if !env_marker.is_disjoint(not_marker) {
591 envs.push(env.narrow_environment(not_marker));
592 }
593 }
594 // Note also that we push this one last for historical reasons.
595 // Changing the order of forks can change the output in some
596 // ways. While it's probably fine, we try to avoid changing the
597 // output.
598 envs.push(env.narrow_environment(self.marker));
599
600 let mut remaining_marker = self.marker;
601 remaining_marker.and(env_marker.negate());
602 let remaining_forker = Forker {
603 package: self.package,
604 marker: remaining_marker,
605 };
606 Some((remaining_forker, envs))
607 }
608
609 /// Returns true if the dependency represented by this forker may be
610 /// included in the given resolver environment.
611 pub(crate) fn included(&self, env: &ResolverEnvironment) -> bool {
612 let marker = self.package.marker();
613 env.included_by_marker(marker)
614 }
615}
616
617/// Fork the resolver based on a `Requires-Python` specifier.
618pub(crate) fn fork_version_by_python_requirement(
619 requires_python: &VersionSpecifiers,
620 python_requirement: &PythonRequirement,
621 env: &ResolverEnvironment,
622) -> Vec<ResolverEnvironment> {
623 let requires_python = RequiresPython::from_specifiers(requires_python);
624 let lower = requires_python.range().lower().clone();
625
626 // Attempt to split the current Python requirement based on the `requires-python` specifier.
627 //
628 // For example, if the current requirement is `>=3.10`, and the split point is `>=3.11`, then
629 // the result will be `>=3.10 and <3.11` and `>=3.11`.
630 //
631 // However, if the current requirement is `>=3.10`, and the split point is `>=3.9`, then the
632 // lower segment will be empty, so we should return an empty list.
633 let Some((lower, upper)) = python_requirement.split(lower.into()) else {
634 trace!(
635 "Unable to split Python requirement `{}` via `Requires-Python` specifier `{}`",
636 python_requirement.target(),
637 requires_python,
638 );
639 return vec![];
640 };
641
642 let Kind::Universal {
643 markers: ref env_marker,
644 ..
645 } = env.kind
646 else {
647 panic!("resolver must be in universal mode for forking")
648 };
649
650 let mut envs = vec![];
651 if !env_marker.is_disjoint(lower.to_marker_tree()) {
652 envs.push(env.narrow_environment(lower.to_marker_tree()));
653 }
654 if !env_marker.is_disjoint(upper.to_marker_tree()) {
655 envs.push(env.narrow_environment(upper.to_marker_tree()));
656 }
657 debug_assert!(!envs.is_empty(), "at least one fork should be produced");
658 envs
659}
660
661/// Fork the resolver based on a marker.
662pub(crate) fn fork_version_by_marker(
663 env: &ResolverEnvironment,
664 marker: MarkerTree,
665) -> Option<(ResolverEnvironment, ResolverEnvironment)> {
666 let Kind::Universal {
667 markers: ref env_marker,
668 ..
669 } = env.kind
670 else {
671 panic!("resolver must be in universal mode for forking")
672 };
673
674 // Attempt to split based on the marker.
675 //
676 // For example, given `python_version >= '3.10'` and the split marker `sys_platform == 'linux'`,
677 // the result will be:
678 //
679 // `python_version >= '3.10' and sys_platform == 'linux'`
680 // `python_version >= '3.10' and sys_platform != 'linux'`
681 //
682 // If the marker is disjoint with the current environment, then we should return an empty list.
683 // If the marker complement is disjoint with the current environment, then we should also return
684 // an empty list.
685 //
686 // For example, given `python_version >= '3.10' and sys_platform == 'linux'` and the split marker
687 // `sys_platform == 'win32'`, return an empty list, since the following isn't satisfiable:
688 //
689 // python_version >= '3.10' and sys_platform == 'linux' and sys_platform == 'win32'
690 if env_marker.is_disjoint(marker) {
691 return None;
692 }
693 let with_marker = env.narrow_environment(marker);
694
695 let complement = marker.negate();
696 if env_marker.is_disjoint(complement) {
697 return None;
698 }
699 let without_marker = env.narrow_environment(complement);
700
701 Some((with_marker, without_marker))
702}
703
704#[cfg(test)]
705mod tests {
706 use std::ops::Bound;
707 use std::sync::LazyLock;
708
709 use uv_pep440::{LowerBound, UpperBound, Version};
710 use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
711
712 use uv_distribution_types::{RequiresPython, RequiresPythonRange};
713
714 use super::*;
715
716 /// A dummy marker environment used in tests below.
717 ///
718 /// It doesn't matter too much what we use here, and indeed, this one was
719 /// copied from our uv microbenchmarks.
720 static MARKER_ENV: LazyLock<MarkerEnvironment> = LazyLock::new(|| {
721 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
722 implementation_name: "cpython",
723 implementation_version: "3.11.5",
724 os_name: "posix",
725 platform_machine: "arm64",
726 platform_python_implementation: "CPython",
727 platform_release: "21.6.0",
728 platform_system: "Darwin",
729 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",
730 python_full_version: "3.11.5",
731 python_version: "3.11",
732 sys_platform: "darwin",
733 }).unwrap()
734 });
735
736 fn requires_python_lower(lower_version_bound: &str) -> RequiresPython {
737 RequiresPython::greater_than_equal_version(&version(lower_version_bound))
738 }
739
740 fn requires_python_range_lower(lower_version_bound: &str) -> RequiresPythonRange {
741 let lower = LowerBound::new(Bound::Included(version(lower_version_bound)));
742 RequiresPythonRange::new(lower, UpperBound::default())
743 }
744
745 fn marker(marker: &str) -> MarkerTree {
746 marker
747 .parse::<MarkerTree>()
748 .expect("valid pep508 marker expression")
749 }
750
751 fn version(v: &str) -> Version {
752 v.parse().expect("valid pep440 version string")
753 }
754
755 fn python_requirement(python_version_greater_than_equal: &str) -> PythonRequirement {
756 let requires_python = requires_python_lower(python_version_greater_than_equal);
757 PythonRequirement::from_marker_environment(&MARKER_ENV, requires_python)
758 }
759
760 /// Tests that narrowing a Python requirement when resolving for a
761 /// specific marker environment never produces a more constrained Python
762 /// requirement.
763 #[test]
764 fn narrow_python_requirement_specific() {
765 let resolver_marker_env = ResolverMarkerEnvironment::from(MARKER_ENV.clone());
766 let resolver_env = ResolverEnvironment::specific(resolver_marker_env);
767
768 let pyreq = python_requirement("3.10");
769 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
770
771 let pyreq = python_requirement("3.11");
772 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
773
774 let pyreq = python_requirement("3.12");
775 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
776 }
777
778 /// Tests that narrowing a Python requirement during a universal resolution
779 /// *without* any forks will never produce a more constrained Python
780 /// requirement.
781 #[test]
782 fn narrow_python_requirement_universal() {
783 let resolver_env = ResolverEnvironment::universal(vec![]);
784
785 let pyreq = python_requirement("3.10");
786 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
787
788 let pyreq = python_requirement("3.11");
789 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
790
791 let pyreq = python_requirement("3.12");
792 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
793 }
794
795 /// Inside a fork whose marker's Python requirement is equal
796 /// to our Requires-Python means that narrowing does not produce
797 /// a result.
798 #[test]
799 fn narrow_python_requirement_forking_no_op() {
800 let pyreq = python_requirement("3.10");
801 let resolver_env = ResolverEnvironment::universal(vec![])
802 .narrow_environment(marker("python_version >= '3.10'"));
803 assert_eq!(resolver_env.narrow_python_requirement(&pyreq), None);
804 }
805
806 /// In this test, we narrow a more relaxed requirement compared to the
807 /// marker for the current fork. This in turn results in a stricter
808 /// requirement corresponding to what's specified in the fork.
809 #[test]
810 fn narrow_python_requirement_forking_stricter() {
811 let pyreq = python_requirement("3.10");
812 let resolver_env = ResolverEnvironment::universal(vec![])
813 .narrow_environment(marker("python_version >= '3.11'"));
814 let expected = {
815 let range = requires_python_range_lower("3.11");
816 let requires_python = requires_python_lower("3.10").narrow(&range).unwrap();
817 PythonRequirement::from_marker_environment(&MARKER_ENV, requires_python)
818 };
819 assert_eq!(
820 resolver_env.narrow_python_requirement(&pyreq),
821 Some(expected)
822 );
823 }
824
825 /// In this test, we narrow a stricter requirement compared to the marker
826 /// for the current fork. This in turn results in a requirement that
827 /// remains unchanged.
828 #[test]
829 fn narrow_python_requirement_forking_relaxed() {
830 let pyreq = python_requirement("3.11");
831 let resolver_env = ResolverEnvironment::universal(vec![])
832 .narrow_environment(marker("python_version >= '3.10'"));
833 assert_eq!(
834 resolver_env.narrow_python_requirement(&pyreq),
835 Some(python_requirement("3.11")),
836 );
837 }
838}