diff --git a/crates/puffin-cli/src/commands/pip_compile.rs b/crates/puffin-cli/src/commands/pip_compile.rs index 1d67d16b1..de674e261 100644 --- a/crates/puffin-cli/src/commands/pip_compile.rs +++ b/crates/puffin-cli/src/commands/pip_compile.rs @@ -1,13 +1,14 @@ +use std::borrow::Cow; use std::fmt::Write; use std::io::{stdout, BufWriter}; use std::path::Path; +use std::str::FromStr; use std::{env, fs}; use anyhow::{anyhow, Result}; use colored::Colorize; use fs_err::File; use itertools::Itertools; - use tracing::debug; use pep508_rs::Requirement; @@ -18,12 +19,12 @@ use puffin_dispatch::BuildDispatch; use puffin_interpreter::Virtualenv; use puffin_normalize::ExtraName; use puffin_resolver::{Manifest, PreReleaseMode, ResolutionMode}; -use std::str::FromStr; use crate::commands::reporters::ResolverReporter; use crate::commands::{elapsed, ExitStatus}; use crate::index_urls::IndexUrls; use crate::printer::Printer; +use crate::python_version::PythonVersion; use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsSpecification}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -40,6 +41,7 @@ pub(crate) async fn pip_compile( upgrade_mode: UpgradeMode, index_urls: Option, no_build: bool, + python_version: Option, cache: &Path, mut printer: Printer, ) -> Result { @@ -118,6 +120,12 @@ pub(crate) async fn pip_compile( venv.interpreter_info().simple_version(), )?; + // Determine the markers to use for resolution. + let markers = python_version.map_or_else( + || Cow::Borrowed(venv.interpreter_info().markers()), + |python_version| Cow::Owned(python_version.markers(venv.interpreter_info().markers())), + ); + // Instantiate a client. let client = { let mut builder = RegistryClientBuilder::default(); @@ -142,14 +150,9 @@ pub(crate) async fn pip_compile( ); // Resolve the dependencies. - let resolver = puffin_resolver::Resolver::new( - manifest, - venv.interpreter_info().markers(), - &tags, - &client, - &build_dispatch, - ) - .with_reporter(ResolverReporter::from(printer)); + let resolver = + puffin_resolver::Resolver::new(manifest, &markers, &tags, &client, &build_dispatch) + .with_reporter(ResolverReporter::from(printer)); let resolution = match resolver.resolve().await { Err(puffin_resolver::ResolveError::PubGrub(err)) => { #[allow(clippy::print_stderr)] diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index 31bfce715..b2833bb85 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -15,12 +15,14 @@ use requirements::ExtrasSpecification; use crate::commands::{extra_name_with_clap_error, ExitStatus}; use crate::index_urls::IndexUrls; +use crate::python_version::PythonVersion; use crate::requirements::RequirementsSource; mod commands; mod index_urls; mod logging; mod printer; +mod python_version; mod requirements; #[derive(Parser)] @@ -116,6 +118,10 @@ struct PipCompileArgs { /// cached wheels of already built source distributions will be reused. #[clap(long)] no_build: bool, + + /// The minimum Python version that should be supported. + #[arg(long, short, value_enum)] + python_version: Option, } #[derive(Args)] @@ -249,6 +255,7 @@ async fn inner() -> Result { args.upgrade.into(), index_urls, args.no_build, + args.python_version, &cache_dir, printer, ) diff --git a/crates/puffin-cli/src/python_version.rs b/crates/puffin-cli/src/python_version.rs new file mode 100644 index 000000000..722288360 --- /dev/null +++ b/crates/puffin-cli/src/python_version.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; + +use pep508_rs::{MarkerEnvironment, StringVersion}; + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +pub(crate) enum PythonVersion { + Py37, + Py38, + Py39, + Py310, + Py311, + Py312, +} + +impl PythonVersion { + /// Return the `python_version` marker for a [`PythonVersion`]. + fn python_version(self) -> &'static str { + match self { + Self::Py37 => "3.7", + Self::Py38 => "3.8", + Self::Py39 => "3.9", + Self::Py310 => "3.10", + Self::Py311 => "3.11", + Self::Py312 => "3.12", + } + } + + /// Return the `python_full_version` marker for a [`PythonVersion`]. + fn python_full_version(self) -> &'static str { + match self { + Self::Py37 => "3.7.0", + Self::Py38 => "3.8.0", + Self::Py39 => "3.9.0", + Self::Py310 => "3.10.0", + Self::Py311 => "3.11.0", + Self::Py312 => "3.12.0", + } + } + + /// Return the `implementation_version` marker for a [`PythonVersion`]. + fn implementation_version(self) -> &'static str { + match self { + Self::Py37 => "3.7.0", + Self::Py38 => "3.8.0", + Self::Py39 => "3.9.0", + Self::Py310 => "3.10.0", + Self::Py311 => "3.11.0", + Self::Py312 => "3.12.0", + } + } + + /// Return a [`MarkerEnvironment`] compatible with the given [`PythonVersion`], based on + /// a base [`MarkerEnvironment`]. + /// + /// The returned [`MarkerEnvironment`] will preserve the base environment's platform markers, + /// but override its Python version markers. + pub(crate) fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment { + let mut markers = base.clone(); + // Ex) `python_version == "3.12"` + markers.python_version = StringVersion::from_str(self.python_version()).unwrap(); + // Ex) `python_full_version == "3.12.0"` + markers.python_full_version = StringVersion::from_str(self.python_full_version()).unwrap(); + // Ex) `implementation_version == "3.12.0"` + if markers.implementation_name == "cpython" { + markers.implementation_version = + StringVersion::from_str(self.implementation_version()).unwrap(); + } + markers + } +} diff --git a/crates/puffin-cli/tests/pip_compile.rs b/crates/puffin-cli/tests/pip_compile.rs index 22e861b27..170e20fe7 100644 --- a/crates/puffin-cli/tests/pip_compile.rs +++ b/crates/puffin-cli/tests/pip_compile.rs @@ -554,6 +554,82 @@ optional-dependencies.foo = [ Ok(()) } +/// Resolve a specific version of Black at Python 3.12. +#[test] +fn compile_python_312() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + let venv = temp_dir.child(".venv"); + + Command::new(get_cargo_bin(BIN_NAME)) + .arg("venv") + .arg(venv.as_os_str()) + .arg("--cache-dir") + .arg(cache_dir.path()) + .current_dir(&temp_dir) + .assert() + .success(); + venv.assert(predicates::path::is_dir()); + + let requirements_in = temp_dir.child("requirements.in"); + requirements_in.touch()?; + requirements_in.write_str("black==23.10.1")?; + + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-compile") + .arg("requirements.in") + .arg("--python-version") + .arg("py312") + .arg("--cache-dir") + .arg(cache_dir.path()) + .env("VIRTUAL_ENV", venv.as_os_str()) + .current_dir(&temp_dir)); + }); + + Ok(()) +} + +/// Resolve a specific version of Black at Python 3.7. +#[test] +fn compile_python_37() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + let venv = temp_dir.child(".venv"); + + Command::new(get_cargo_bin(BIN_NAME)) + .arg("venv") + .arg(venv.as_os_str()) + .arg("--cache-dir") + .arg(cache_dir.path()) + .current_dir(&temp_dir) + .assert() + .success(); + venv.assert(predicates::path::is_dir()); + + let requirements_in = temp_dir.child("requirements.in"); + requirements_in.touch()?; + requirements_in.write_str("black==23.10.1")?; + + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-compile") + .arg("requirements.in") + .arg("--python-version") + .arg("py37") + .arg("--cache-dir") + .arg(cache_dir.path()) + .env("VIRTUAL_ENV", venv.as_os_str()) + .current_dir(&temp_dir)); + }); + + Ok(()) +} + /// Resolve a specific Flask wheel via a URL dependency. #[test] fn compile_wheel_url_dependency() -> Result<()> { diff --git a/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_312.snap b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_312.snap new file mode 100644 index 000000000..22ce3d471 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_312.snap @@ -0,0 +1,34 @@ +--- +source: crates/puffin-cli/tests/pip_compile.rs +info: + program: puffin + args: + - pip-compile + - requirements.in + - "--python-version" + - py312 + - "--cache-dir" + - /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpbKzceW + env: + VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpZkKRNz/.venv +--- +success: true +exit_code: 0 +----- stdout ----- +# This file was autogenerated by Puffin v0.0.1 via the following command: +# [BIN_PATH] pip-compile requirements.in --python-version py312 --cache-dir [CACHE_DIR] +black==23.10.1 +click==8.1.7 + # via black +mypy-extensions==1.0.0 + # via black +packaging==23.2 + # via black +pathspec==0.11.2 + # via black +platformdirs==3.11.0 + # via black + +----- stderr ----- +Resolved 6 packages in [TIME] + diff --git a/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_37.snap b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_37.snap new file mode 100644 index 000000000..54dc89187 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_37.snap @@ -0,0 +1,45 @@ +--- +source: crates/puffin-cli/tests/pip_compile.rs +info: + program: puffin + args: + - pip-compile + - requirements.in + - "--python-version" + - py37 + - "--cache-dir" + - /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpQwHoBA + env: + VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmp1TmUIW/.venv +--- +success: true +exit_code: 0 +----- stdout ----- +# This file was autogenerated by Puffin v0.0.1 via the following command: +# [BIN_PATH] pip-compile requirements.in --python-version py37 --cache-dir [CACHE_DIR] +black==23.10.1 +click==8.1.7 + # via black +importlib-metadata==6.8.0 + # via click +mypy-extensions==1.0.0 + # via black +packaging==23.2 + # via black +pathspec==0.11.2 + # via black +platformdirs==3.11.0 + # via black +tomli==2.0.1 + # via black +typing-extensions==4.8.0 + # via + # black + # importlib-metadata + # platformdirs +zipp==3.17.0 + # via importlib-metadata + +----- stderr ----- +Resolved 10 packages in [TIME] +