Skip to content

Commit

Permalink
Added support for leaxed pins (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko authored May 30, 2023
1 parent e4a80a6 commit 38c7fc6
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 66 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/toolchains/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ rye pin [email protected]
```

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 [email protected]`, 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 [email protected]
```

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

Expand Down
1 change: 1 addition & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# features: []
# all-features: false

-e file:.
certifi==2023.5.7
charset-normalizer==3.1.0
click==8.1.3
Expand Down
24 changes: 13 additions & 11 deletions rye/src/cli/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
10 changes: 8 additions & 2 deletions rye/src/cli/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions rye/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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")
}
Expand Down
86 changes: 43 additions & 43 deletions rye/src/platform.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::env::consts::{ARCH, OS};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Mutex;
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<Option<&'static PathBuf>> = Mutex::new(None);

Expand Down Expand Up @@ -106,38 +106,43 @@ pub fn get_toolchain_python_bin(version: &PythonVersion) -> Result<PathBuf, Erro
/// Returns a pinnable version for this version request.
///
/// This is the version number that will be written into `.python-version`
pub fn get_pinnable_version(req: &PythonVersionRequest) -> Option<String> {
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<String> {
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.
Expand Down Expand Up @@ -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<PythonVersion> {
pub fn get_python_version_request_from_pyenv_pin() -> Option<PythonVersionRequest> {
let mut here = env::current_dir().ok()?;

loop {
Expand All @@ -209,19 +214,14 @@ pub fn get_python_version_from_pyenv_pin() -> Option<PythonVersion> {
}

/// Returns the most recent cpython release.
pub fn get_latest_cpython() -> Result<PythonVersion, Error> {
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<PythonVersion, Error> {
latest_available_python_version(&PythonVersionRequest {
kind: None,
major: 3,
minor: None,
patch: None,
suffix: None,
})
.context("unsupported platform")
}

Expand Down
41 changes: 34 additions & 7 deletions rye/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -862,18 +862,45 @@ pub fn get_current_venv_python_version(venv_path: &Path) -> Option<PythonVersion
Some(marker.python)
}

/// Give a given python version request, returns the latest available version.
///
/// This can return a version that requires downloading.
pub fn latest_available_python_version(
requested_version: &PythonVersionRequest,
) -> Option<PythonVersion> {
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<PythonVersionRequest> {
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<PythonVersion, Error> {
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!(
Expand All @@ -886,7 +913,7 @@ fn resolve_intended_venv_python_version(doc: &Document) -> Result<PythonVersion,
return Ok(ver);
}

if let Some((latest, _, _)) = get_download_url(&requested_version, OS, ARCH) {
if let Some(latest) = latest_available_python_version(&requested_version) {
Ok(latest)
} else {
Err(anyhow!(
Expand Down
22 changes: 21 additions & 1 deletion rye/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ impl From<PythonVersion> for Version {
}
}

impl From<PythonVersionRequest> 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 {
Expand Down Expand Up @@ -169,10 +186,13 @@ impl FromStr for PythonVersionRequest {
let major = iter
.next()
.and_then(|x| x.parse::<u8>().ok())
.ok_or_else(|| anyhow!("invalid version"))?;
.ok_or_else(|| anyhow!("invalid syntax for version"))?;
let minor = iter.next().and_then(|x| x.parse::<u8>().ok());
let patch = iter.next().and_then(|x| x.parse::<u8>().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()),
Expand Down

0 comments on commit 38c7fc6

Please sign in to comment.