Skip to main content

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}