mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-28 21:14:47 +00:00
Support granular target Python versions (#534)
## Summary Allows, e.g., `--python-version 3.7` or `--python-version 3.7.9`. This was also feedback I received in the original PR. Closes https://github.com/astral-sh/puffin/issues/533.
This commit is contained in:
parent
06ee321e9c
commit
c3a917bbf6
6 changed files with 160 additions and 57 deletions
|
@ -59,6 +59,7 @@ struct Cli {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Compile a `requirements.in` file to a `requirements.txt` file.
|
/// Compile a `requirements.in` file to a `requirements.txt` file.
|
||||||
PipCompile(PipCompileArgs),
|
PipCompile(PipCompileArgs),
|
||||||
|
@ -148,7 +149,11 @@ struct PipCompileArgs {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
no_build: bool,
|
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)]
|
#[arg(long, short, value_enum)]
|
||||||
python_version: Option<PythonVersion>,
|
python_version: Option<PythonVersion>,
|
||||||
|
|
||||||
|
|
|
@ -1,54 +1,65 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use pep440_rs::Version;
|
||||||
use pep508_rs::{MarkerEnvironment, StringVersion};
|
use pep508_rs::{MarkerEnvironment, StringVersion};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) enum PythonVersion {
|
pub(crate) struct PythonVersion(StringVersion);
|
||||||
Py37,
|
|
||||||
Py38,
|
impl FromStr for PythonVersion {
|
||||||
Py39,
|
type Err = String;
|
||||||
Py310,
|
|
||||||
Py311,
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Py312,
|
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 {
|
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
|
/// Return a [`MarkerEnvironment`] compatible with the given [`PythonVersion`], based on
|
||||||
/// a base [`MarkerEnvironment`].
|
/// a base [`MarkerEnvironment`].
|
||||||
///
|
///
|
||||||
|
@ -56,15 +67,18 @@ impl PythonVersion {
|
||||||
/// but override its Python version markers.
|
/// but override its Python version markers.
|
||||||
pub(crate) fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment {
|
pub(crate) fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment {
|
||||||
let mut markers = base.clone();
|
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"`
|
// Ex) `implementation_version == "3.12.0"`
|
||||||
if markers.implementation_name == "cpython" {
|
if markers.implementation_name == "cpython" {
|
||||||
markers.implementation_version =
|
markers.implementation_version = self.0.clone();
|
||||||
StringVersion::from_str(self.implementation_version()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
markers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -473,7 +473,7 @@ fn compile_python_312() -> Result<()> {
|
||||||
.arg("pip-compile")
|
.arg("pip-compile")
|
||||||
.arg("requirements.in")
|
.arg("requirements.in")
|
||||||
.arg("--python-version")
|
.arg("--python-version")
|
||||||
.arg("py312")
|
.arg("3.12")
|
||||||
.arg("--cache-dir")
|
.arg("--cache-dir")
|
||||||
.arg(cache_dir.path())
|
.arg(cache_dir.path())
|
||||||
.arg("--exclude-newer")
|
.arg("--exclude-newer")
|
||||||
|
@ -502,7 +502,7 @@ fn compile_python_37() -> Result<()> {
|
||||||
.arg("pip-compile")
|
.arg("pip-compile")
|
||||||
.arg("requirements.in")
|
.arg("requirements.in")
|
||||||
.arg("--python-version")
|
.arg("--python-version")
|
||||||
.arg("py37")
|
.arg("3.7")
|
||||||
.arg("--cache-dir")
|
.arg("--cache-dir")
|
||||||
.arg(cache_dir.path())
|
.arg(cache_dir.path())
|
||||||
.arg("--exclude-newer")
|
.arg("--exclude-newer")
|
||||||
|
@ -514,6 +514,50 @@ fn compile_python_37() -> Result<()> {
|
||||||
Ok(())
|
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
|
/// Test that we select the last 3.8 compatible numpy version instead of trying to compile an
|
||||||
/// incompatible sdist <https://github.com/astral-sh/puffin/issues/388>
|
/// incompatible sdist <https://github.com/astral-sh/puffin/issues/388>
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -6,17 +6,19 @@ info:
|
||||||
- pip-compile
|
- pip-compile
|
||||||
- requirements.in
|
- requirements.in
|
||||||
- "--python-version"
|
- "--python-version"
|
||||||
- py312
|
- "3.12"
|
||||||
- "--cache-dir"
|
- "--cache-dir"
|
||||||
- /tmp/.tmp4asOTQ
|
- /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpBGkaCR
|
||||||
|
- "--exclude-newer"
|
||||||
|
- "2023-11-18T12:00:00Z"
|
||||||
env:
|
env:
|
||||||
VIRTUAL_ENV: /tmp/.tmpjRCdtR/.venv
|
VIRTUAL_ENV: /var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/.tmpfxl60Y/.venv
|
||||||
---
|
---
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by Puffin v0.0.1 via the following command:
|
# 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
|
black==23.10.1
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via black
|
# via black
|
||||||
|
|
|
@ -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>': Python version 3.7-dev is a development release
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
|
|
@ -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 <PYTHON_VERSION>': Version `3.7.x` doesn't match PEP 440 rules
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue