diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index b4c816a9e..29659116c 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -170,6 +170,8 @@ pub enum PythonVariant { Debug, Freethreaded, FreethreadedDebug, + Gil, + GilDebug, } /// A Python discovery version request. @@ -1685,18 +1687,34 @@ impl PythonVariant { Self::Debug => interpreter.debug_enabled(), Self::Freethreaded => interpreter.gil_disabled(), 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`. /// /// Returns an empty string for the default Python variant. - pub fn suffix(self) -> &'static str { + pub fn executable_suffix(self) -> &'static str { match self { Self::Default => "", Self::Debug => "d", Self::Freethreaded => "t", 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`. pub fn lib_suffix(self) -> &'static str { match self { - Self::Default | Self::Debug => "", + Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "", Self::Freethreaded | Self::FreethreadedDebug => "t", } } pub fn is_freethreaded(self) -> bool { match self { - Self::Default | Self::Debug => false, + Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false, Self::Freethreaded | Self::FreethreadedDebug => true, } } pub fn is_debug(self) -> bool { match self { - Self::Default | Self::Freethreaded => false, - Self::Debug | Self::FreethreadedDebug => true, + Self::Default | Self::Freethreaded | Self::Gil => false, + Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true, } } } @@ -2450,7 +2468,7 @@ impl fmt::Display for ExecutableName { if let Some(prerelease) = &self.prerelease { write!(f, "{prerelease}")?; } - f.write_str(self.variant.suffix())?; + f.write_str(self.variant.executable_suffix())?; f.write_str(EXE_SUFFIX)?; Ok(()) } @@ -3067,6 +3085,8 @@ impl FromStr for PythonVariant { "t" | "freethreaded" => Ok(Self::Freethreaded), "d" | "debug" => Ok(Self::Debug), "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug), + "gil" => Ok(Self::Gil), + "gil+debug" => Ok(Self::GilDebug), "" => Ok(Self::Default), _ => Err(()), } @@ -3080,6 +3100,8 @@ impl fmt::Display for PythonVariant { Self::Debug => f.write_str("debug"), Self::Freethreaded => f.write_str("freethreaded"), 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 { Self::Any => f.write_str("any"), 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) => { - write!(f, "{major}.{minor}{}", variant.suffix()) + write!(f, "{major}.{minor}{}", variant.display_suffix()) } 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) => { - write!(f, "{major}.{minor}{prerelease}{}", variant.suffix()) + write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix()) } Self::Range(specifiers, _) => write!(f, "{specifiers}"), } diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 6422d16f6..cbe9144fc 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -460,7 +460,7 @@ impl PythonInstallationKey { "python{maj}.{min}{var}{exe}", maj = self.major, min = self.minor, - var = self.variant.suffix(), + var = self.variant.executable_suffix(), exe = std::env::consts::EXE_SUFFIX ) } @@ -470,7 +470,7 @@ impl PythonInstallationKey { format!( "python{maj}{var}{exe}", maj = self.major, - var = self.variant.suffix(), + var = self.variant.executable_suffix(), exe = std::env::consts::EXE_SUFFIX ) } @@ -479,7 +479,7 @@ impl PythonInstallationKey { pub fn executable_name(&self) -> String { format!( "python{var}{exe}", - var = self.variant.suffix(), + var = self.variant.executable_suffix(), exe = std::env::consts::EXE_SUFFIX ) } diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 644215f10..6bde8ddad 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -173,7 +173,7 @@ impl Interpreter { base_executable, self.python_major(), self.python_minor(), - self.variant().suffix(), + self.variant().executable_suffix(), ) { Ok(path) => path, Err(err) => { diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index b29cdb37b..d9ae5a336 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -392,7 +392,7 @@ impl ManagedPythonInstallation { let variant = if self.implementation() == ImplementationName::GraalPy { "" } else if cfg!(unix) { - self.key.variant.suffix() + self.key.variant.executable_suffix() } else if cfg!(windows) && windowed { // Use windowed Python that doesn't open a terminal. "w" @@ -626,7 +626,7 @@ impl ManagedPythonInstallation { "{}python{}{}{}", std::env::consts::DLL_PREFIX, self.key.version().python_version(), - self.key.variant().suffix(), + self.key.variant().executable_suffix(), std::env::consts::DLL_SUFFIX )); macos_dylib::patch_dylib_install_name(dylib_path)?; diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 145133142..11ccca2c2 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -746,7 +746,7 @@ pub(crate) fn report_interpreter( "Using {} {}{}", implementation.pretty(), interpreter.python_version(), - interpreter.variant().suffix(), + interpreter.variant().display_suffix(), ) .dimmed() )?; @@ -758,7 +758,7 @@ pub(crate) fn report_interpreter( "Using {} {}{} interpreter at: {}", implementation.pretty(), interpreter.python_version(), - interpreter.variant().suffix(), + interpreter.variant().display_suffix(), interpreter.sys_executable().user_display() ) .dimmed() @@ -771,7 +771,7 @@ pub(crate) fn report_interpreter( "Using {} {}{}", implementation.pretty(), interpreter.python_version().cyan(), - interpreter.variant().suffix().cyan() + interpreter.variant().display_suffix().cyan() )?; } else { writeln!( @@ -779,7 +779,7 @@ pub(crate) fn report_interpreter( "Using {} {}{} interpreter at: {}", implementation.pretty(), interpreter.python_version(), - interpreter.variant().suffix(), + interpreter.variant().display_suffix(), interpreter.sys_executable().user_display().cyan() )?; } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index cb6e730e8..5feddcf8f 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1011,7 +1011,7 @@ impl ProjectInterpreter { "Using {} {}{}", implementation.pretty(), interpreter.python_version().cyan(), - interpreter.variant().suffix().cyan(), + interpreter.variant().display_suffix().cyan(), )?; } else { writeln!( @@ -1019,7 +1019,7 @@ impl ProjectInterpreter { "Using {} {}{} interpreter at: {}", implementation.pretty(), interpreter.python_version(), - interpreter.variant().suffix(), + interpreter.variant().display_suffix(), interpreter.sys_executable().user_display().cyan() )?; } diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 2eff6e8f9..6344e87cc 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -859,7 +859,7 @@ fn create_bin_links( to.simplified_display(), installation.key().major(), installation.key().minor(), - installation.key().variant().suffix() + installation.key().variant().display_suffix() ); } else { errors.push(( diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 921b27c4e..75cb499e1 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -3968,7 +3968,7 @@ fn init_without_description() -> Result<()> { /// Run `uv init --python 3.13t` to create a pin to a freethreaded Python. #[test] -fn init_python_variant() -> Result<()> { +fn init_python_variant() { let context = TestContext::new("3.13"); uv_snapshot!(context.filters(), context.init().arg("foo").arg("--python").arg("3.13t"), @r###" success: true @@ -3979,10 +3979,8 @@ fn init_python_variant() -> Result<()> { Initialized project `foo` at `[TEMP_DIR]/foo` "###); - let python_version = fs_err::read_to_string(context.temp_dir.join("foo/.python-version"))?; - assert_eq!(python_version, "3.13t\n"); - - Ok(()) + let contents = context.read("foo/.python-version"); + assert_snapshot!(contents, @"3.13+freethreaded"); } /// Check how `uv init` reacts to working and broken git with different `--vcs` options. diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index efe52e509..544bb88a6 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -850,14 +850,14 @@ fn python_find_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 exit_code: 2 ----- stdout ----- ----- 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] @@ -1334,6 +1334,44 @@ fn python_find_freethreaded_314() { ----- 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] diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 83d0180fe..a8302e3e4 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -1130,7 +1130,7 @@ fn python_install_freethreaded() { ----- stdout ----- ----- stderr ----- - Using CPython 3.13.9t + Using CPython 3.13.9+freethreaded Creating virtual environment at: .venv Activate with: source .venv/[BIN]/activate "); @@ -1200,7 +1200,7 @@ fn python_install_freethreaded() { ----- stdout ----- ----- 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" diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index 1d980b37f..706480ec4 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -280,7 +280,7 @@ fn python_list_unsupported_version() { ----- stdout ----- ----- 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. "); } diff --git a/crates/uv/tests/it/python_pin.rs b/crates/uv/tests/it/python_pin.rs index b2c548b16..03606b20c 100644 --- a/crates/uv/tests/it/python_pin.rs +++ b/crates/uv/tests/it/python_pin.rs @@ -451,21 +451,21 @@ fn python_pin_compatible_with_requires_python() -> Result<()> { success: true exit_code: 0 ----- stdout ----- - Updated `.python-version` from `3.11` -> `3.13t` + Updated `.python-version` from `3.11` -> `3.13+freethreaded` ----- 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 - 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 exit_code: 0 ----- stdout ----- - Updated `.python-version` from `3.13t` -> `cpython@3.11` + Updated `.python-version` from `3.13+freethreaded` -> `cpython@3.11` ----- stderr ----- - "###); + "); let python_version = context.read(PYTHON_VERSION_FILENAME); insta::with_settings!({