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