uv-configuration 0.0.90

This is an internal component crate of uv
Documentation
use std::str::FromStr;

use rustc_hash::{FxHashMap, FxHashSet};
use serde::de::Error;

use uv_distribution_types::ResolutionRecorder;
use uv_normalize::PackageName;
use uv_pep440::Version;

use crate::Overrides;

/// A set of exclusions that applies to the dependencies of a specific package version.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PackageExclusion {
    package: PackageExclusionTarget,
    dependencies: Box<[PackageName]>,
}

/// The package and optional version selected by a [`PackageExclusion`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct PackageExclusionTarget {
    name: PackageName,
    #[cfg_attr(
        feature = "schemars",
        schemars(
            with = "Option<String>",
            description = "PEP 440-style package version, e.g., `1.2.3`"
        )
    )]
    version: Option<Version>,
}

impl PackageExclusion {
    /// Return the parent package selected by this exclusion.
    pub fn package(&self) -> &PackageName {
        &self.package.name
    }
}

/// An exclusion, either global or scoped to a specific package version.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
#[serde(untagged)]
pub enum ExcludeDependency {
    Package(PackageExclusion),
    Dependency(PackageName),
}

impl<'de> serde::Deserialize<'de> for ExcludeDependency {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        serde_untagged::UntaggedEnumVisitor::new()
            .string(|string| {
                PackageName::from_str(string)
                    .map(Self::Dependency)
                    .map_err(Error::custom)
            })
            .map(|map| map.deserialize().map(Self::Package))
            .deserialize(deserializer)
    }
}

/// A set of packages to exclude from resolution.
#[derive(Debug, Default, Clone)]
pub struct Excludes {
    recorder: Option<ResolutionRecorder>,
    global: FxHashSet<PackageName>,
    scoped: FxHashMap<PackageName, Vec<ScopedExclusions>>,
}

#[derive(Debug, Clone)]
struct ScopedExclusions {
    version: Option<Version>,
    excludes: FxHashSet<PackageName>,
}

impl Excludes {
    /// Record which settings are consulted while resolving runtime dependencies.
    #[must_use]
    pub(crate) fn with_recorder(mut self, recorder: Option<ResolutionRecorder>) -> Self {
        self.recorder = recorder;
        self
    }

    /// Create an indexed set of exclusions.
    pub fn from_entries(entries: impl IntoIterator<Item = ExcludeDependency>) -> Self {
        let mut excludes = Self::default();
        for entry in entries {
            match entry {
                ExcludeDependency::Dependency(dependency) => {
                    excludes.global.insert(dependency);
                }
                ExcludeDependency::Package(package) => {
                    let packages = excludes.scoped.entry(package.package.name).or_default();
                    if let Some(entry) = packages
                        .iter_mut()
                        .find(|entry| entry.version == package.package.version)
                    {
                        entry.excludes.extend(package.dependencies);
                    } else {
                        packages.push(ScopedExclusions {
                            version: package.package.version,
                            excludes: package.dependencies.into_iter().collect(),
                        });
                    }
                }
            }
        }
        excludes
    }

    /// Return sorted declarations, combining duplicate exclusions within each package scope.
    ///
    /// Retain empty version-specific scopes: they shadow exclusions from a versionless scope.
    pub(crate) fn into_entries(self) -> Vec<ExcludeDependency> {
        let mut entries = self
            .global
            .into_iter()
            .map(ExcludeDependency::Dependency)
            .collect::<Vec<_>>();
        for (name, packages) in self.scoped {
            for package in packages {
                let mut dependencies = package.excludes.into_iter().collect::<Vec<_>>();
                dependencies.sort();
                entries.push(ExcludeDependency::Package(PackageExclusion {
                    package: PackageExclusionTarget {
                        name: name.clone(),
                        version: package.version,
                    },
                    dependencies: dependencies.into_boxed_slice(),
                }));
            }
        }
        entries.sort();
        entries
    }

    /// Check if a package is excluded.
    pub(crate) fn contains(&self, name: &PackageName) -> bool {
        if let Some(recorder) = &self.recorder {
            recorder.exclusion(name);
        }
        self.global.contains(name)
    }

    /// Return whether any exclusions are scoped to the given package.
    pub(crate) fn has_scoped_package(&self, package: &PackageName) -> bool {
        self.scoped.contains_key(package)
    }

    /// Check if a dependency is excluded from a specific package version.
    pub(crate) fn contains_for(
        &self,
        package: &PackageName,
        version: &Version,
        dependency: &PackageName,
    ) -> bool {
        self.contains_for_package(Some((package, version)), dependency)
    }

    /// Check if a dependency is always excluded from a package scope.
    ///
    /// A versionless scope remains eligible if any exact-version exclusion allows the dependency
    /// at a version where the override is not shadowed by an exact override scope.
    pub(crate) fn contains_for_scope(
        &self,
        overrides: &Overrides,
        package: &PackageName,
        version: Option<&Version>,
        dependency: &PackageName,
    ) -> bool {
        if let Some(version) = version {
            return self.contains_for(package, version, dependency);
        }
        if self.contains(dependency) {
            return true;
        }

        let Some(entries) = self.scoped.get(package) else {
            return false;
        };
        entries
            .iter()
            .find(|entry| entry.version.is_none())
            .is_some_and(|entry| entry.excludes.contains(dependency))
            && entries
                .iter()
                .filter(|entry| {
                    entry
                        .version
                        .as_ref()
                        .is_some_and(|version| !overrides.has_exact_scope(package, version))
                })
                .all(|entry| entry.excludes.contains(dependency))
    }

    /// Check if a dependency is excluded with optional package-version context.
    pub(crate) fn contains_for_package(
        &self,
        package: Option<(&PackageName, &Version)>,
        dependency: &PackageName,
    ) -> bool {
        self.contains(dependency)
            || package.is_some_and(|(package, version)| {
                if let Some(recorder) = &self.recorder {
                    recorder.scoped_exclusion(package);
                }
                self.scoped.get(package).is_some_and(|entries| {
                    entries
                        .iter()
                        .find(|entry| entry.version.as_ref() == Some(version))
                        .or_else(|| entries.iter().find(|entry| entry.version.is_none()))
                        .is_some_and(|entry| entry.excludes.contains(dependency))
                })
            })
    }
}

impl FromIterator<PackageName> for Excludes {
    fn from_iter<I: IntoIterator<Item = PackageName>>(iter: I) -> Self {
        Self::from_entries(iter.into_iter().map(ExcludeDependency::Dependency))
    }
}