Allow creating venv with free-threaded python builds (#7431)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

closes #4828

First iteration for an implementation. I need to add more tests but
wanted your opinion on the implementation first.

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
Currently tested using the following command but will add tests shortly:

```console
D:\repo\uv> cargo run venv -p 3.13t && .venv\Scripts\python.exe
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target\debug\uv.exe venv -p 3.13t`
Using Python 3.13.0rc1 interpreter at: C:\Users\bschoen\AppData\Local\Programs\Python\Python313\python3.13t.exe
Creating virtualenv at: .venv
Activate with: .venv\Scripts\activate
Python 3.13.0rc1 experimental free-threading build (tags/v3.13.0rc1:e4a3e78, Jul 31 2024, 21:06:58) [MSC v.1940 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 
```

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Bas Schoenmaeckers 2024-09-24 00:36:16 +02:00 committed by GitHub
parent 0dea932d83
commit 77c2496f47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 423 additions and 152 deletions

View file

@ -201,6 +201,7 @@ mod tests {
version: &PythonVersion,
implementation: ImplementationName,
system: bool,
free_threaded: bool,
) -> Result<()> {
let json = indoc! {r##"
{
@ -251,7 +252,7 @@ mod tests {
"scripts": "bin"
},
"pointer_size": "64",
"gil_disabled": true
"gil_disabled": {FREE_THREADED}
}
"##};
@ -268,6 +269,7 @@ mod tests {
)
.replace("{FULL_VERSION}", &version.to_string())
.replace("{VERSION}", &version.without_patch().to_string())
.replace("{FREE_THREADED}", &free_threaded.to_string())
.replace("{IMPLEMENTATION}", (&implementation).into());
fs_err::create_dir_all(path.parent().unwrap())?;
@ -327,6 +329,7 @@ mod tests {
&PythonVersion::from_str(version).expect("Test uses valid version"),
ImplementationName::default(),
true,
false,
)
}
@ -366,6 +369,7 @@ mod tests {
&PythonVersion::from_str(version).unwrap(),
*implementation,
*system,
false,
)?;
}
Ok(())
@ -385,6 +389,7 @@ mod tests {
.expect("A valid Python version is used for tests"),
ImplementationName::default(),
false,
false,
)?;
ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
Ok(())
@ -406,6 +411,7 @@ mod tests {
.expect("A valid Python version is used for tests"),
ImplementationName::default(),
true,
false,
)?;
ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
Ok(())
@ -537,6 +543,7 @@ mod tests {
&PythonVersion::from_str("3.12.1").unwrap(),
ImplementationName::default(),
true,
false,
)?;
let python = context.run(|| {
@ -604,6 +611,7 @@ mod tests {
&PythonVersion::from_str("3.12.1").unwrap(),
ImplementationName::default(),
true,
false,
)?;
let python = context.run(|| {
@ -1169,6 +1177,7 @@ mod tests {
ImplementationName::CPython,
// Note we mark this as a system interpreter instead of a virtual environment
true,
false,
)?;
let python = context.run_with_vars(
@ -1289,6 +1298,7 @@ mod tests {
ImplementationName::CPython,
// Note we mark this as a system interpreter instead of a virtual environment
true,
false,
)?;
let python = context.run_with_vars(
@ -1461,6 +1471,7 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::default(),
true,
false,
)?;
let python = context.run(|| {
@ -1504,6 +1515,7 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::default(),
true,
false,
)?;
let python = context.run(|| {
@ -1609,6 +1621,7 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::default(),
true,
false,
)?;
let python = context.run(|| {
find_python_installation(
@ -1701,6 +1714,7 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::default(),
true,
false,
)?;
context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
@ -1740,6 +1754,7 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::default(),
false, // Not a system interpreter
false,
)?;
context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
@ -2095,12 +2110,14 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::GraalPy,
true,
false,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("graalpy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::GraalPy,
true,
false,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
@ -2166,12 +2183,14 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
false,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
false,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
@ -2195,12 +2214,14 @@ mod tests {
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
false,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
false,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
@ -2220,4 +2241,108 @@ mod tests {
Ok(())
}
#[test]
fn find_python_version_free_threaded() -> Result<()> {
let mut context = TestContext::new()?;
TestContext::create_mock_interpreter(
&context.tempdir.join("python"),
&PythonVersion::from_str("3.13.1").unwrap(),
ImplementationName::CPython,
true,
false,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("python3.13t"),
&PythonVersion::from_str("3.13.0").unwrap(),
ImplementationName::CPython,
true,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("3.13t"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert!(
matches!(
python,
PythonInstallation {
source: PythonSource::SearchPath,
interpreter: _
}
),
"We should find a python; got {python:?}"
);
assert_eq!(
&python.interpreter().python_full_version().to_string(),
"3.13.0",
"We should find the correct interpreter for the request"
);
assert!(
&python.interpreter().gil_disabled(),
"We should find a python without the GIL"
);
Ok(())
}
#[test]
fn find_python_version_prefer_non_free_threaded() -> Result<()> {
let mut context = TestContext::new()?;
TestContext::create_mock_interpreter(
&context.tempdir.join("python"),
&PythonVersion::from_str("3.13.0").unwrap(),
ImplementationName::CPython,
true,
false,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("python3.13t"),
&PythonVersion::from_str("3.13.0").unwrap(),
ImplementationName::CPython,
true,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("3.13"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert!(
matches!(
python,
PythonInstallation {
source: PythonSource::SearchPath,
interpreter: _
}
),
"We should find a python; got {python:?}"
);
assert_eq!(
&python.interpreter().python_full_version().to_string(),
"3.13.0",
"We should find the correct interpreter for the request"
);
assert!(
!&python.interpreter().gil_disabled(),
"We should prefer a python with the GIL"
);
Ok(())
}
}