From 38c7fc6eff798a7a75651f1d126267cd08a693d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 30 May 2023 20:15:27 +0200 Subject: [PATCH] Added support for leaxed pins (#255) --- CHANGELOG.md | 6 +++ docs/guide/toolchains/index.md | 14 ++++++ requirements-dev.lock | 1 + rye/src/cli/init.rs | 24 +++++----- rye/src/cli/pin.rs | 10 +++- rye/src/config.rs | 4 +- rye/src/platform.rs | 86 +++++++++++++++++----------------- rye/src/pyproject.rs | 41 +++++++++++++--- rye/src/sources.rs | 22 ++++++++- 9 files changed, 142 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e154ce18..62148fc6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ that were not yet released. _Unreleased_ +- It's now possible for `.python-version` to request partial Python versions + in which case the latest available is used. In particular this means that + a version like `3.10` can be written into `.python-version` rather than + `3.10.11`. This can be accomplished by invoking `pin` with the new + `--relaxed` flag. #255 + - Adding or removing dependencies with `add` or `remove` now reformats the `dependencies` array in the `pyproject.toml` file to multi-line with trailing commas. This should result in significantly better diff --git a/docs/guide/toolchains/index.md b/docs/guide/toolchains/index.md index 50d6e4a05a..dfc13bb4e9 100644 --- a/docs/guide/toolchains/index.md +++ b/docs/guide/toolchains/index.md @@ -22,6 +22,20 @@ rye pin cpython@3.11.4 ``` Pinning a downloadable version means that Rye will automatically fetch it when necessary. +By default toolchains are pinned to a precise version. This means that even if you +write `rye pin cpython@3.11`, a very specific version of cpython is written into the +`.python-version` file. With Rye 0.5.0 onwards it's possible to perform "relaxed" pins: + +``` +rye pin --relaxed cpython@3.11 +``` + +This will then persist `3.11` in the `.python-version` file and Rye will use the latest +available compatible version for the virtual environment. + ++/- 0.5.0 + + Relaxed pinning with `rye pin --relaxed` was added. ## Listing Toolchains diff --git a/requirements-dev.lock b/requirements-dev.lock index 343b161467..6d2381d268 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,6 +6,7 @@ # features: [] # all-features: false +-e file:. certifi==2023.5.7 charset-normalizer==3.1.0 click==8.1.3 diff --git a/rye/src/cli/init.rs b/rye/src/cli/init.rs index 9ac1047c4c..60f75f383c 100644 --- a/rye/src/cli/init.rs +++ b/rye/src/cli/init.rs @@ -11,9 +11,11 @@ use minijinja::{context, Environment}; use pep440_rs::VersionSpecifier; use crate::config::Config; -use crate::platform::{get_default_author, get_latest_cpython, get_python_version_from_pyenv_pin}; +use crate::platform::{ + get_default_author, get_latest_cpython_version, get_python_version_request_from_pyenv_pin, +}; use crate::pyproject::BuildSystem; -use crate::sources::PythonVersion; +use crate::sources::PythonVersionRequest; use crate::utils::is_inside_git_work_tree; /// Creates a new python project. @@ -146,17 +148,17 @@ pub fn execute(cmd: Args) -> Result<(), Error> { // Write pyproject.toml let mut requires_python = match cmd.min_py { Some(py) => format!(">= {}", py), - None => get_python_version_from_pyenv_pin() - .map(|x| format!(">= {}.{}", x.major, x.minor)) + None => get_python_version_request_from_pyenv_pin() + .map(|x| format!(">= {}.{}", x.major, x.minor.unwrap_or_default())) .unwrap_or_else(|| cfg.default_requires_python()), }; let py = match cmd.py { - Some(py) => { - PythonVersion::from_str(&py).map_err(|msg| anyhow!("invalid version: {}", msg))? - } - None => get_python_version_from_pyenv_pin() - .map(Ok) - .unwrap_or_else(get_latest_cpython)?, + Some(py) => PythonVersionRequest::from_str(&py) + .map_err(|msg| anyhow!("invalid version: {}", msg))?, + None => match get_python_version_request_from_pyenv_pin() { + Some(ver) => ver, + None => PythonVersionRequest::from(get_latest_cpython_version()?), + }, }; if !cmd.no_pin && !VersionSpecifier::from_str(&requires_python) @@ -167,7 +169,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { "{} conflicted python version with project's requires-python, will auto fix it.", style("warning:").red() ); - requires_python = format!(">= {}.{}", py.major, py.minor); + requires_python = format!(">= {}.{}", py.major, py.minor.unwrap_or_default()); } let name = slug::slugify( diff --git a/rye/src/cli/pin.rs b/rye/src/cli/pin.rs index 2c9447dcd0..01e317e439 100644 --- a/rye/src/cli/pin.rs +++ b/rye/src/cli/pin.rs @@ -19,14 +19,20 @@ use crate::sources::PythonVersionRequest; pub struct Args { /// The version of Python to pin. version: String, + /// Issue a relaxed pin + #[arg(long)] + relaxed: bool, /// Prevent updating requires-python in the pyproject.toml. #[arg(long)] no_update_requires_python: bool, } pub fn execute(cmd: Args) -> Result<(), Error> { - let req: PythonVersionRequest = cmd.version.parse()?; - let to_write = get_pinnable_version(&req) + let req: PythonVersionRequest = cmd + .version + .parse() + .with_context(|| format!("'{}' is not a valid version", cmd.version))?; + let to_write = get_pinnable_version(&req, cmd.relaxed) .ok_or_else(|| anyhow!("unsupported/unknown version for this platform"))?; let version_file = match PyProject::discover() { diff --git a/rye/src/config.rs b/rye/src/config.rs index a4afeade4b..037b3af519 100644 --- a/rye/src/config.rs +++ b/rye/src/config.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex}; use anyhow::{Context, Error}; use toml_edit::Document; -use crate::platform::{get_app_dir, get_latest_cpython}; +use crate::platform::{get_app_dir, get_latest_cpython_version}; use crate::pyproject::{BuildSystem, SourceRef, SourceRefType}; use crate::sources::PythonVersionRequest; @@ -84,7 +84,7 @@ impl Config { .and_then(|x| x.as_str()) { Some(ver) => ver.parse(), - None => get_latest_cpython().map(Into::into), + None => get_latest_cpython_version().map(Into::into), } .context("failed to get default toolchain") } diff --git a/rye/src/platform.rs b/rye/src/platform.rs index 4cf4a4e8ea..e238e5b1a8 100644 --- a/rye/src/platform.rs +++ b/rye/src/platform.rs @@ -1,4 +1,3 @@ -use std::env::consts::{ARCH, OS}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::Mutex; @@ -6,7 +5,8 @@ use std::{env, fs}; use anyhow::{anyhow, Context, Error}; -use crate::sources::{get_download_url, PythonVersion, PythonVersionRequest}; +use crate::pyproject::latest_available_python_version; +use crate::sources::{PythonVersion, PythonVersionRequest}; static APP_DIR: Mutex> = Mutex::new(None); @@ -106,38 +106,43 @@ pub fn get_toolchain_python_bin(version: &PythonVersion) -> Result Option { - let mut target_version = None; - - // If the version request points directly to a known version for which we - // have a known binary, we can use that. - if let Ok(ver) = PythonVersion::try_from(req.clone()) { - if let Ok(path) = get_toolchain_python_bin(&ver) { - if path.is_file() { - target_version = Some(ver); +pub fn get_pinnable_version(req: &PythonVersionRequest, relaxed: bool) -> Option { + let serialized = if relaxed { + req.to_string() + } else { + let mut target_version = None; + + // If the version request points directly to a known version for which we + // have a known binary, we can use that. + if let Ok(ver) = PythonVersion::try_from(req.clone()) { + if let Ok(path) = get_toolchain_python_bin(&ver) { + if path.is_file() { + target_version = Some(ver); + } } } - } - // otherwise, any version we can download is an acceptable version - if let Some((version, _, _)) = get_download_url(req, OS, ARCH) { - target_version = Some(version); - } + // otherwise, any version we can download is an acceptable version + if target_version.is_none() { + if let Some(version) = latest_available_python_version(req) { + target_version = Some(version); + } + } - // we return the stringified version of the version, but if always remove the - // cpython@ prefix to make it reusable with other toolchains such as pyenv. - if let Some(version) = target_version { - let serialized_version = version.to_string(); - Some( - if let Some(rest) = serialized_version.strip_prefix("cpython@") { - rest.to_string() - } else { - serialized_version - }, - ) + // we return the stringified version of the version, but if always remove the + // cpython@ prefix to make it reusable with other toolchains such as pyenv. + if let Some(version) = target_version { + version.to_string() + } else { + return None; + } + }; + + Some(if let Some(rest) = serialized.strip_prefix("cpython@") { + rest.to_string() } else { - None - } + serialized + }) } /// Returns a list of all registered toolchains. @@ -190,7 +195,7 @@ pub fn get_default_author() -> Option<(String, String)> { } /// Reads the current `.python-version` file. -pub fn get_python_version_from_pyenv_pin() -> Option { +pub fn get_python_version_request_from_pyenv_pin() -> Option { let mut here = env::current_dir().ok()?; loop { @@ -209,19 +214,14 @@ pub fn get_python_version_from_pyenv_pin() -> Option { } /// Returns the most recent cpython release. -pub fn get_latest_cpython() -> Result { - get_download_url( - &PythonVersionRequest { - kind: None, - major: 3, - minor: None, - patch: None, - suffix: None, - }, - OS, - ARCH, - ) - .map(|x| x.0) +pub fn get_latest_cpython_version() -> Result { + latest_available_python_version(&PythonVersionRequest { + kind: None, + major: 3, + minor: None, + patch: None, + suffix: None, + }) .context("unsupported platform") } diff --git a/rye/src/pyproject.rs b/rye/src/pyproject.rs index efaeb5d6ac..62181b7a0d 100644 --- a/rye/src/pyproject.rs +++ b/rye/src/pyproject.rs @@ -23,7 +23,7 @@ use url::Url; use crate::config::Config; use crate::consts::VENV_BIN; -use crate::platform::get_python_version_from_pyenv_pin; +use crate::platform::{get_python_version_request_from_pyenv_pin, list_known_toolchains}; use crate::sources::{get_download_url, PythonVersion, PythonVersionRequest}; use crate::sync::VenvMarker; use crate::utils::{ @@ -862,18 +862,45 @@ pub fn get_current_venv_python_version(venv_path: &Path) -> Option Option { + let mut all = if let Ok(available) = list_known_toolchains() { + available + .into_iter() + .filter_map(|(ver, _)| { + if Some(&ver.kind as &str) == requested_version.kind.as_deref() { + Some(ver) + } else { + None + } + }) + .collect() + } else { + Vec::new() + }; + + if let Some((latest, _, _)) = get_download_url(requested_version, OS, ARCH) { + all.push(latest); + }; + + all.sort(); + all.into_iter().rev().next() +} + fn resolve_target_python_version(doc: &Document, venv_path: &Path) -> Option { resolve_lower_bound_python_version(doc) .or_else(|| get_current_venv_python_version(venv_path).map(Into::into)) - .or_else(|| get_python_version_from_pyenv_pin().map(Into::into)) + .or_else(|| get_python_version_request_from_pyenv_pin().map(Into::into)) .or_else(|| Config::current().default_toolchain().ok()) } fn resolve_intended_venv_python_version(doc: &Document) -> Result { - if let Some(ver) = get_python_version_from_pyenv_pin() { - return Ok(ver); - } - let requested_version = resolve_lower_bound_python_version(doc) + let requested_version = get_python_version_request_from_pyenv_pin() + .or_else(|| resolve_lower_bound_python_version(doc)) .or_else(|| Config::current().default_toolchain().ok()) .ok_or_else(|| { anyhow!( @@ -886,7 +913,7 @@ fn resolve_intended_venv_python_version(doc: &Document) -> Result for Version { } } +impl From for Version { + fn from(value: PythonVersionRequest) -> Self { + Version { + epoch: 0, + release: vec![ + value.major as usize, + value.minor.unwrap_or_default() as usize, + value.patch.unwrap_or_default() as usize, + ], + pre: None, + post: None, + dev: None, + local: None, + } + } +} + /// Internal descriptor for a python version request. #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone)] pub struct PythonVersionRequest { @@ -169,10 +186,13 @@ impl FromStr for PythonVersionRequest { let major = iter .next() .and_then(|x| x.parse::().ok()) - .ok_or_else(|| anyhow!("invalid version"))?; + .ok_or_else(|| anyhow!("invalid syntax for version"))?; let minor = iter.next().and_then(|x| x.parse::().ok()); let patch = iter.next().and_then(|x| x.parse::().ok()); let suffix = iter.next().map(|x| Cow::Owned(x.to_string())); + if iter.next().is_some() { + return Err(anyhow!("unexpected garbage after version")); + } Ok(PythonVersionRequest { kind: kind.map(|x| x.to_string().into()),