diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index 1e26a3475..d7d431abe 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -59,6 +59,7 @@ struct Cli { } #[derive(Subcommand)] +#[allow(clippy::large_enum_variant)] enum Commands { /// Compile a `requirements.in` file to a `requirements.txt` file. PipCompile(PipCompileArgs), @@ -148,7 +149,11 @@ struct PipCompileArgs { #[clap(long)] no_build: bool, - /// The minimum Python version that should be supported. + /// The minimum Python version that should be supported by the compiled requirements (e.g., + /// `3.7` or `3.7.9`). + /// + /// If a patch version is omitted, the most recent known patch version for that minor version + /// is assumed. For example, `3.7` is mapped to `3.7.17`. #[arg(long, short, value_enum)] python_version: Option, diff --git a/crates/puffin-cli/src/python_version.rs b/crates/puffin-cli/src/python_version.rs index 722288360..b6c16ad25 100644 --- a/crates/puffin-cli/src/python_version.rs +++ b/crates/puffin-cli/src/python_version.rs @@ -1,54 +1,65 @@ use std::str::FromStr; +use tracing::debug; +use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, StringVersion}; -#[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub(crate) enum PythonVersion { - Py37, - Py38, - Py39, - Py310, - Py311, - Py312, +#[derive(Debug, Clone)] +pub(crate) struct PythonVersion(StringVersion); + +impl FromStr for PythonVersion { + type Err = String; + + fn from_str(s: &str) -> Result { + let version = StringVersion::from_str(s)?; + if version.is_dev() { + return Err(format!("Python version {s} is a development release")); + } + if version.is_local() { + return Err(format!("Python version {s} is a local version")); + } + if version.epoch != 0 { + return Err(format!("Python version {s} has a non-zero epoch")); + } + if version.version < Version::from_release(vec![3, 7]) { + return Err(format!("Python version {s} must be >= 3.7")); + } + if version.version >= Version::from_release(vec![4, 0]) { + return Err(format!("Python version {s} must be < 4.0")); + } + + // If the version lacks a patch, assume the most recent known patch for that minor version. + match version.release.as_slice() { + [3, 7] => { + debug!("Assuming Python 3.7.17"); + Ok(Self(StringVersion::from_str("3.7.17")?)) + } + [3, 8] => { + debug!("Assuming Python 3.8.18"); + Ok(Self(StringVersion::from_str("3.8.18")?)) + } + [3, 9] => { + debug!("Assuming Python 3.9.18"); + Ok(Self(StringVersion::from_str("3.9.18")?)) + } + [3, 10] => { + debug!("Assuming Python 3.10.13"); + Ok(Self(StringVersion::from_str("3.10.13")?)) + } + [3, 11] => { + debug!("Assuming Python 3.11.6"); + Ok(Self(StringVersion::from_str("3.11.6")?)) + } + [3, 12] => { + debug!("Assuming Python 3.12.0"); + Ok(Self(StringVersion::from_str("3.12.0")?)) + } + _ => Ok(Self(version)), + } + } } 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`]. /// @@ -56,15 +67,18 @@ impl PythonVersion { /// 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.implementation_version = self.0.clone(); } + + // Ex) `python_full_version == "3.12.0"` + markers.python_full_version = self.0.clone(); + + // Ex) `python_version == "3.12"` + markers.python_version = self.0; + markers } } diff --git a/crates/puffin-cli/tests/pip_compile.rs b/crates/puffin-cli/tests/pip_compile.rs index b946c9801..1ca043b0a 100644 --- a/crates/puffin-cli/tests/pip_compile.rs +++ b/crates/puffin-cli/tests/pip_compile.rs @@ -473,7 +473,7 @@ fn compile_python_312() -> Result<()> { .arg("pip-compile") .arg("requirements.in") .arg("--python-version") - .arg("py312") + .arg("3.12") .arg("--cache-dir") .arg(cache_dir.path()) .arg("--exclude-newer") @@ -502,7 +502,7 @@ fn compile_python_37() -> Result<()> { .arg("pip-compile") .arg("requirements.in") .arg("--python-version") - .arg("py37") + .arg("3.7") .arg("--cache-dir") .arg(cache_dir.path()) .arg("--exclude-newer") @@ -514,6 +514,50 @@ fn compile_python_37() -> Result<()> { Ok(()) } +/// Resolve a specific version of Black against an invalid Python version. +#[test] +fn compile_python_invalid_version() -> Result<()> { + let temp_dir = TempDir::new()?; + + let requirements_in = temp_dir.child("requirements.in"); + 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("3.7.x") + .current_dir(&temp_dir)); + }); + + Ok(()) +} + +/// Resolve a specific version of Black against an invalid Python version. +#[test] +fn compile_python_dev_version() -> Result<()> { + let temp_dir = TempDir::new()?; + + let requirements_in = temp_dir.child("requirements.in"); + 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("3.7-dev") + .current_dir(&temp_dir)); + }); + + Ok(()) +} + /// Test that we select the last 3.8 compatible numpy version instead of trying to compile an /// incompatible sdist #[test] 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 index b44d02638..79fe847d8 100644 --- a/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_312.snap +++ b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_312.snap @@ -6,17 +6,19 @@ info: - pip-compile - requirements.in - "--python-version" - - py312 + - "3.12" - "--cache-dir" - - /tmp/.tmp4asOTQ + - /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpBGkaCR + - "--exclude-newer" + - "2023-11-18T12:00:00Z" env: - VIRTUAL_ENV: /tmp/.tmpjRCdtR/.venv + VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpfxl60Y/.venv --- success: true exit_code: 0 ----- stdout ----- # This file was autogenerated by Puffin v0.0.1 via the following command: -# puffin pip-compile requirements.in --python-version py312 --cache-dir [CACHE_DIR] +# puffin pip-compile requirements.in --python-version 3.12 --cache-dir [CACHE_DIR] black==23.10.1 click==8.1.7 # via black diff --git a/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_dev_version.snap b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_dev_version.snap new file mode 100644 index 000000000..ff7e9c002 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_dev_version.snap @@ -0,0 +1,19 @@ +--- +source: crates/puffin-cli/tests/pip_compile.rs +info: + program: puffin + args: + - pip-compile + - requirements.in + - "--python-version" + - 3.7-dev +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: invalid value '3.7-dev' for '--python-version ': Python version 3.7-dev is a development release + +For more information, try '--help'. + diff --git a/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_invalid_version.snap b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_invalid_version.snap new file mode 100644 index 000000000..3bf6e2444 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_compile__compile_python_invalid_version.snap @@ -0,0 +1,19 @@ +--- +source: crates/puffin-cli/tests/pip_compile.rs +info: + program: puffin + args: + - pip-compile + - requirements.in + - "--python-version" + - 3.7.x +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: invalid value '3.7.x' for '--python-version ': Version `3.7.x` doesn't match PEP 440 rules + +For more information, try '--help'. +