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