From edd741bf131c23fd33cd5e6dba190b9002a99e96 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 Dec 2023 22:45:02 -0500 Subject: [PATCH] Add a diagnostic to detect invalid Python versions (#630) Related to: https://github.com/astral-sh/puffin/issues/410. --- crates/puffin-cli/tests/pip_install.rs | 2 +- crates/puffin-installer/src/site_packages.rs | 33 ++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/puffin-cli/tests/pip_install.rs b/crates/puffin-cli/tests/pip_install.rs index 83abd7e55..6b3fe3092 100644 --- a/crates/puffin-cli/tests/pip_install.rs +++ b/crates/puffin-cli/tests/pip_install.rs @@ -384,7 +384,7 @@ fn allow_incompatibilities() -> Result<()> { Installed 1 package in [TIME] - jinja2==3.1.2 + jinja2==2.11.3 - warning: The package `flask` requires `jinja2 >=3.1.2` but `2.11.3` is installed. + warning: The package `flask` requires `jinja2 >=3.1.2`, but `2.11.3` is installed. "###); }); diff --git a/crates/puffin-installer/src/site_packages.rs b/crates/puffin-installer/src/site_packages.rs index da89f90e6..d1c730522 100644 --- a/crates/puffin-installer/src/site_packages.rs +++ b/crates/puffin-installer/src/site_packages.rs @@ -6,7 +6,7 @@ use fs_err as fs; use rustc_hash::FxHashSet; use distribution_types::{InstalledDist, Metadata, VersionOrUrl}; -use pep440_rs::Version; +use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::Requirement; use puffin_interpreter::Virtualenv; use puffin_normalize::PackageName; @@ -95,6 +95,17 @@ impl<'a> SitePackages<'a> { // Determine the dependencies for the given package. let metadata = distribution.metadata()?; + // Verify that the package is compatible with the current Python version. + if let Some(requires_python) = metadata.requires_python.as_ref() { + if !requires_python.contains(self.venv.interpreter().version()) { + diagnostics.push(Diagnostic::IncompatiblePythonVersion { + package: package.clone(), + version: self.venv.interpreter().version().clone(), + requires_python: requires_python.clone(), + }); + } + } + // Verify that the dependencies are installed. for requirement in &metadata.requires_dist { if !requirement.evaluate_markers(self.venv.interpreter().markers(), &[]) { @@ -198,6 +209,14 @@ impl IntoIterator for SitePackages<'_> { #[derive(Debug)] pub enum Diagnostic { + IncompatiblePythonVersion { + /// The package that requires a different version of Python. + package: PackageName, + /// The version of Python that is installed. + version: Version, + /// The version of Python that is required. + requires_python: VersionSpecifiers, + }, MissingDependency { /// The package that is missing a dependency. package: PackageName, @@ -218,18 +237,25 @@ impl Diagnostic { /// Convert the diagnostic into a user-facing message. pub fn message(&self) -> String { match self { + Self::IncompatiblePythonVersion { + package, + version, + requires_python, + } => format!( + "The package `{package}` requires Python {requires_python}, but `{version}` is installed." + ), Self::MissingDependency { package, requirement, } => { - format!("The package `{package}` requires `{requirement}` but it is not installed.") + format!("The package `{package}` requires `{requirement}`, but it's not installed.") } Self::IncompatibleDependency { package, version, requirement, } => format!( - "The package `{package}` requires `{requirement}` but `{version}` is installed." + "The package `{package}` requires `{requirement}`, but `{version}` is installed." ), } } @@ -237,6 +263,7 @@ impl Diagnostic { /// Returns `true` if the [`PackageName`] is involved in this diagnostic. pub fn includes(&self, name: &PackageName) -> bool { match self { + Self::IncompatiblePythonVersion { package, .. } => name == package, Self::MissingDependency { package, .. } => name == package, Self::IncompatibleDependency { package,