Allow pip-compile without a venv (#494)

The semantics are a bit unintuitive because `--python-version` is a
preference when looking for a python version without a venv, but if we
don't find that exact version we'll take `python3` and patch the
markers. This will make more sense once we start provisioning python
builds.

We can now resolve black with both python 3.8 and 3.12, with or without
that python version being in scope. In the example below,
`PATH=$HOME/.cargo/bin:/usr/bin` removes the pyenv builds and leaves
only `python3`, which is python 3.11.

```console
$ RUST_LOG=puffin::commands=debug cargo run --bin puffin -q -- pip-compile -v scripts/benchmarks/requirements/black.in --python-version py38
    0.004108s DEBUG puffin::commands::pip_compile Using Python 3.8 at /home/konsti/.local/bin/python3.8
Resolved 8 packages in 44ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    puffin pip-compile -v scripts/benchmarks/requirements/black.in --python-version py38
black==23.11.0
[...]
platformdirs==4.0.0
    # via black
tomli==2.0.1
    # via black
typing-extensions==4.8.0
    # via black
$ PATH=$HOME/.cargo/bin:/usr/bin RUST_LOG=puffin::commands=debug cargo run --bin puffin -q -- pip-compile -v scripts/benchmarks/requirements/black.in --python-version py38
    0.004315s DEBUG puffin::commands::pip_compile Using Python 3.11 at /usr/bin/python3
Resolved 8 packages in 43ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    puffin pip-compile -v scripts/benchmarks/requirements/black.in --python-version py38
black==23.11.0
[...]
platformdirs==4.0.0
    # via black
tomli==2.0.1
    # via black
typing-extensions==4.8.0
    # via black
```

```console
$ RUST_LOG=puffin::commands=debug cargo run --bin puffin -q -- pip-compile -v scripts/benchmarks/requirements/black.in --python-version py312
    0.004216s DEBUG puffin::commands::pip_compile Using Python 3.12 at /home/konsti/.local/bin/python3.12
Resolved 6 packages in 37ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    puffin pip-compile -v scripts/benchmarks/requirements/black.in --python-version py312
black==23.11.0
[...]
platformdirs==4.0.0
    # via black
$ PATH=$HOME/.cargo/bin:/usr/bin RUST_LOG=puffin::commands=debug cargo run --bin puffin -q -- pip-compile -v scripts/benchmarks/requirements/black.in --python-version py312
    0.004190s DEBUG puffin::commands::pip_compile Using Python 3.11 at /usr/bin/python3
Resolved 6 packages in 39ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    puffin pip-compile -v scripts/benchmarks/requirements/black.in --python-version py312
black==23.11.0
[...]
platformdirs==4.0.0
    # via black
```

Fixes #235.

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
konsti
2024-01-05 16:01:06 +01:00
committed by GitHub
parent 76064cdec2
commit 673bece595
8 changed files with 110 additions and 21 deletions
@@ -0,0 +1,95 @@
use std::str::FromStr;
use tracing::debug;
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, StringVersion};
#[derive(Debug, Clone)]
pub struct PythonVersion(StringVersion);
impl FromStr for PythonVersion {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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::new([3, 7]) {
return Err(format!("Python version {s} must be >= 3.7"));
}
if version.version >= Version::new([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() {
[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 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 fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment {
let mut markers = base.clone();
// Ex) `implementation_version == "3.12.0"`
if markers.implementation_name == "cpython" {
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
}
/// Return the major version of this Python version.
pub fn major(&self) -> u64 {
self.0.release()[0]
}
/// Return the minor version of this Python version.
pub fn minor(&self) -> u64 {
self.0.release()[1]
}
}