Allow Python requests to include `+gil` to require a GIL-enabled interpreter (#16537)

Addresses
https://github.com/astral-sh/uv/pull/16142#issuecomment-3390586957

Nobody seems to have a good idea about how to spell this. "not
free-threaded" would be the most technically correct, but I think "gil"
will be more intuitive.
This commit is contained in:
Zanie Blue 2025-11-03 13:35:52 -06:00 committed by GitHub
parent a87a3bbae4
commit 60a811e715
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 97 additions and 39 deletions

View File

@ -170,6 +170,8 @@ pub enum PythonVariant {
Debug, Debug,
Freethreaded, Freethreaded,
FreethreadedDebug, FreethreadedDebug,
Gil,
GilDebug,
} }
/// A Python discovery version request. /// A Python discovery version request.
@ -1685,18 +1687,34 @@ impl PythonVariant {
Self::Debug => interpreter.debug_enabled(), Self::Debug => interpreter.debug_enabled(),
Self::Freethreaded => interpreter.gil_disabled(), Self::Freethreaded => interpreter.gil_disabled(),
Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(), Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
Self::Gil => !interpreter.gil_disabled(),
Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
} }
} }
/// Return the executable suffix for the variant, e.g., `t` for `python3.13t`. /// Return the executable suffix for the variant, e.g., `t` for `python3.13t`.
/// ///
/// Returns an empty string for the default Python variant. /// Returns an empty string for the default Python variant.
pub fn suffix(self) -> &'static str { pub fn executable_suffix(self) -> &'static str {
match self { match self {
Self::Default => "", Self::Default => "",
Self::Debug => "d", Self::Debug => "d",
Self::Freethreaded => "t", Self::Freethreaded => "t",
Self::FreethreadedDebug => "td", Self::FreethreadedDebug => "td",
Self::Gil => "",
Self::GilDebug => "d",
}
}
/// Return the suffix for display purposes, e.g., `+gil`.
pub fn display_suffix(self) -> &'static str {
match self {
Self::Default => "",
Self::Debug => "+debug",
Self::Freethreaded => "+freethreaded",
Self::FreethreadedDebug => "+freethreaded+debug",
Self::Gil => "+gil",
Self::GilDebug => "+gil+debug",
} }
} }
@ -1704,22 +1722,22 @@ impl PythonVariant {
/// `python3.13d` or `python3.13`. /// `python3.13d` or `python3.13`.
pub fn lib_suffix(self) -> &'static str { pub fn lib_suffix(self) -> &'static str {
match self { match self {
Self::Default | Self::Debug => "", Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
Self::Freethreaded | Self::FreethreadedDebug => "t", Self::Freethreaded | Self::FreethreadedDebug => "t",
} }
} }
pub fn is_freethreaded(self) -> bool { pub fn is_freethreaded(self) -> bool {
match self { match self {
Self::Default | Self::Debug => false, Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
Self::Freethreaded | Self::FreethreadedDebug => true, Self::Freethreaded | Self::FreethreadedDebug => true,
} }
} }
pub fn is_debug(self) -> bool { pub fn is_debug(self) -> bool {
match self { match self {
Self::Default | Self::Freethreaded => false, Self::Default | Self::Freethreaded | Self::Gil => false,
Self::Debug | Self::FreethreadedDebug => true, Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
} }
} }
} }
@ -2450,7 +2468,7 @@ impl fmt::Display for ExecutableName {
if let Some(prerelease) = &self.prerelease { if let Some(prerelease) = &self.prerelease {
write!(f, "{prerelease}")?; write!(f, "{prerelease}")?;
} }
f.write_str(self.variant.suffix())?; f.write_str(self.variant.executable_suffix())?;
f.write_str(EXE_SUFFIX)?; f.write_str(EXE_SUFFIX)?;
Ok(()) Ok(())
} }
@ -3067,6 +3085,8 @@ impl FromStr for PythonVariant {
"t" | "freethreaded" => Ok(Self::Freethreaded), "t" | "freethreaded" => Ok(Self::Freethreaded),
"d" | "debug" => Ok(Self::Debug), "d" | "debug" => Ok(Self::Debug),
"td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug), "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
"gil" => Ok(Self::Gil),
"gil+debug" => Ok(Self::GilDebug),
"" => Ok(Self::Default), "" => Ok(Self::Default),
_ => Err(()), _ => Err(()),
} }
@ -3080,6 +3100,8 @@ impl fmt::Display for PythonVariant {
Self::Debug => f.write_str("debug"), Self::Debug => f.write_str("debug"),
Self::Freethreaded => f.write_str("freethreaded"), Self::Freethreaded => f.write_str("freethreaded"),
Self::FreethreadedDebug => f.write_str("freethreaded+debug"), Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
Self::Gil => f.write_str("gil"),
Self::GilDebug => f.write_str("gil+debug"),
} }
} }
} }
@ -3109,15 +3131,15 @@ impl fmt::Display for VersionRequest {
match self { match self {
Self::Any => f.write_str("any"), Self::Any => f.write_str("any"),
Self::Default => f.write_str("default"), Self::Default => f.write_str("default"),
Self::Major(major, variant) => write!(f, "{major}{}", variant.suffix()), Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
Self::MajorMinor(major, minor, variant) => { Self::MajorMinor(major, minor, variant) => {
write!(f, "{major}.{minor}{}", variant.suffix()) write!(f, "{major}.{minor}{}", variant.display_suffix())
} }
Self::MajorMinorPatch(major, minor, patch, variant) => { Self::MajorMinorPatch(major, minor, patch, variant) => {
write!(f, "{major}.{minor}.{patch}{}", variant.suffix()) write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
} }
Self::MajorMinorPrerelease(major, minor, prerelease, variant) => { Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
write!(f, "{major}.{minor}{prerelease}{}", variant.suffix()) write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
} }
Self::Range(specifiers, _) => write!(f, "{specifiers}"), Self::Range(specifiers, _) => write!(f, "{specifiers}"),
} }

View File

@ -460,7 +460,7 @@ impl PythonInstallationKey {
"python{maj}.{min}{var}{exe}", "python{maj}.{min}{var}{exe}",
maj = self.major, maj = self.major,
min = self.minor, min = self.minor,
var = self.variant.suffix(), var = self.variant.executable_suffix(),
exe = std::env::consts::EXE_SUFFIX exe = std::env::consts::EXE_SUFFIX
) )
} }
@ -470,7 +470,7 @@ impl PythonInstallationKey {
format!( format!(
"python{maj}{var}{exe}", "python{maj}{var}{exe}",
maj = self.major, maj = self.major,
var = self.variant.suffix(), var = self.variant.executable_suffix(),
exe = std::env::consts::EXE_SUFFIX exe = std::env::consts::EXE_SUFFIX
) )
} }
@ -479,7 +479,7 @@ impl PythonInstallationKey {
pub fn executable_name(&self) -> String { pub fn executable_name(&self) -> String {
format!( format!(
"python{var}{exe}", "python{var}{exe}",
var = self.variant.suffix(), var = self.variant.executable_suffix(),
exe = std::env::consts::EXE_SUFFIX exe = std::env::consts::EXE_SUFFIX
) )
} }

View File

@ -173,7 +173,7 @@ impl Interpreter {
base_executable, base_executable,
self.python_major(), self.python_major(),
self.python_minor(), self.python_minor(),
self.variant().suffix(), self.variant().executable_suffix(),
) { ) {
Ok(path) => path, Ok(path) => path,
Err(err) => { Err(err) => {

View File

@ -392,7 +392,7 @@ impl ManagedPythonInstallation {
let variant = if self.implementation() == ImplementationName::GraalPy { let variant = if self.implementation() == ImplementationName::GraalPy {
"" ""
} else if cfg!(unix) { } else if cfg!(unix) {
self.key.variant.suffix() self.key.variant.executable_suffix()
} else if cfg!(windows) && windowed { } else if cfg!(windows) && windowed {
// Use windowed Python that doesn't open a terminal. // Use windowed Python that doesn't open a terminal.
"w" "w"
@ -626,7 +626,7 @@ impl ManagedPythonInstallation {
"{}python{}{}{}", "{}python{}{}{}",
std::env::consts::DLL_PREFIX, std::env::consts::DLL_PREFIX,
self.key.version().python_version(), self.key.version().python_version(),
self.key.variant().suffix(), self.key.variant().executable_suffix(),
std::env::consts::DLL_SUFFIX std::env::consts::DLL_SUFFIX
)); ));
macos_dylib::patch_dylib_install_name(dylib_path)?; macos_dylib::patch_dylib_install_name(dylib_path)?;

View File

@ -746,7 +746,7 @@ pub(crate) fn report_interpreter(
"Using {} {}{}", "Using {} {}{}",
implementation.pretty(), implementation.pretty(),
interpreter.python_version(), interpreter.python_version(),
interpreter.variant().suffix(), interpreter.variant().display_suffix(),
) )
.dimmed() .dimmed()
)?; )?;
@ -758,7 +758,7 @@ pub(crate) fn report_interpreter(
"Using {} {}{} interpreter at: {}", "Using {} {}{} interpreter at: {}",
implementation.pretty(), implementation.pretty(),
interpreter.python_version(), interpreter.python_version(),
interpreter.variant().suffix(), interpreter.variant().display_suffix(),
interpreter.sys_executable().user_display() interpreter.sys_executable().user_display()
) )
.dimmed() .dimmed()
@ -771,7 +771,7 @@ pub(crate) fn report_interpreter(
"Using {} {}{}", "Using {} {}{}",
implementation.pretty(), implementation.pretty(),
interpreter.python_version().cyan(), interpreter.python_version().cyan(),
interpreter.variant().suffix().cyan() interpreter.variant().display_suffix().cyan()
)?; )?;
} else { } else {
writeln!( writeln!(
@ -779,7 +779,7 @@ pub(crate) fn report_interpreter(
"Using {} {}{} interpreter at: {}", "Using {} {}{} interpreter at: {}",
implementation.pretty(), implementation.pretty(),
interpreter.python_version(), interpreter.python_version(),
interpreter.variant().suffix(), interpreter.variant().display_suffix(),
interpreter.sys_executable().user_display().cyan() interpreter.sys_executable().user_display().cyan()
)?; )?;
} }

View File

@ -1011,7 +1011,7 @@ impl ProjectInterpreter {
"Using {} {}{}", "Using {} {}{}",
implementation.pretty(), implementation.pretty(),
interpreter.python_version().cyan(), interpreter.python_version().cyan(),
interpreter.variant().suffix().cyan(), interpreter.variant().display_suffix().cyan(),
)?; )?;
} else { } else {
writeln!( writeln!(
@ -1019,7 +1019,7 @@ impl ProjectInterpreter {
"Using {} {}{} interpreter at: {}", "Using {} {}{} interpreter at: {}",
implementation.pretty(), implementation.pretty(),
interpreter.python_version(), interpreter.python_version(),
interpreter.variant().suffix(), interpreter.variant().display_suffix(),
interpreter.sys_executable().user_display().cyan() interpreter.sys_executable().user_display().cyan()
)?; )?;
} }

View File

@ -859,7 +859,7 @@ fn create_bin_links(
to.simplified_display(), to.simplified_display(),
installation.key().major(), installation.key().major(),
installation.key().minor(), installation.key().minor(),
installation.key().variant().suffix() installation.key().variant().display_suffix()
); );
} else { } else {
errors.push(( errors.push((

View File

@ -3968,7 +3968,7 @@ fn init_without_description() -> Result<()> {
/// Run `uv init --python 3.13t` to create a pin to a freethreaded Python. /// Run `uv init --python 3.13t` to create a pin to a freethreaded Python.
#[test] #[test]
fn init_python_variant() -> Result<()> { fn init_python_variant() {
let context = TestContext::new("3.13"); let context = TestContext::new("3.13");
uv_snapshot!(context.filters(), context.init().arg("foo").arg("--python").arg("3.13t"), @r###" uv_snapshot!(context.filters(), context.init().arg("foo").arg("--python").arg("3.13t"), @r###"
success: true success: true
@ -3979,10 +3979,8 @@ fn init_python_variant() -> Result<()> {
Initialized project `foo` at `[TEMP_DIR]/foo` Initialized project `foo` at `[TEMP_DIR]/foo`
"###); "###);
let python_version = fs_err::read_to_string(context.temp_dir.join("foo/.python-version"))?; let contents = context.read("foo/.python-version");
assert_eq!(python_version, "3.13t\n"); assert_snapshot!(contents, @"3.13+freethreaded");
Ok(())
} }
/// Check how `uv init` reacts to working and broken git with different `--vcs` options. /// Check how `uv init` reacts to working and broken git with different `--vcs` options.

View File

@ -850,14 +850,14 @@ fn python_find_unsupported_version() {
"###); "###);
// Request free-threaded Python on unsupported version // Request free-threaded Python on unsupported version
uv_snapshot!(context.filters(), context.python_find().arg("3.12t"), @r###" uv_snapshot!(context.filters(), context.python_find().arg("3.12t"), @r"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: Invalid version request: Python <3.13 does not support free-threading but 3.12t was requested. error: Invalid version request: Python <3.13 does not support free-threading but 3.12+freethreaded was requested.
"###); ");
} }
#[test] #[test]
@ -1334,6 +1334,44 @@ fn python_find_freethreaded_314() {
----- stderr ----- ----- stderr -----
"); ");
// Request Python 3.14+gil
uv_snapshot!(context.filters(), context.python_find().arg("3.14+gil"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for Python 3.14+gil in [PYTHON SOURCES]
");
// Install the non-freethreaded version
context
.python_install()
.arg("--preview")
.arg("3.14")
.assert()
.success();
// Request Python 3.14
uv_snapshot!(context.filters(), context.python_find().arg("3.14"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
----- stderr -----
");
// Request Python 3.14+gil
uv_snapshot!(context.filters(), context.python_find().arg("3.14+gil"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
----- stderr -----
");
} }
#[test] #[test]

View File

@ -1130,7 +1130,7 @@ fn python_install_freethreaded() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Using CPython 3.13.9t Using CPython 3.13.9+freethreaded
Creating virtual environment at: .venv Creating virtual environment at: .venv
Activate with: source .venv/[BIN]/activate Activate with: source .venv/[BIN]/activate
"); ");
@ -1200,7 +1200,7 @@ fn python_install_freethreaded() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: No download found for request: cpython-3.12t-[PLATFORM] error: No download found for request: cpython-3.12+freethreaded-[PLATFORM]
"); ");
uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r" uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r"

View File

@ -280,7 +280,7 @@ fn python_list_unsupported_version() {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: Invalid version request: Python <3.13 does not support free-threading but 3.12t was requested. error: Invalid version request: Python <3.13 does not support free-threading but 3.12+freethreaded was requested.
"); ");
} }

View File

@ -451,21 +451,21 @@ fn python_pin_compatible_with_requires_python() -> Result<()> {
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
Updated `.python-version` from `3.11` -> `3.13t` Updated `.python-version` from `3.11` -> `3.13+freethreaded`
----- stderr ----- ----- stderr -----
warning: No interpreter found for Python 3.13t in [PYTHON SOURCES] warning: No interpreter found for Python 3.13+freethreaded in [PYTHON SOURCES]
"); ");
// Request a implementation version that is compatible // Request a implementation version that is compatible
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.11"), @r###" uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.11"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
Updated `.python-version` from `3.13t` -> `cpython@3.11` Updated `.python-version` from `3.13+freethreaded` -> `cpython@3.11`
----- stderr ----- ----- stderr -----
"###); ");
let python_version = context.read(PYTHON_VERSION_FILENAME); let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({ insta::with_settings!({