diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index d8692e38f..b02f3eb91 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1668,9 +1668,19 @@ fn is_windows_store_shim(_path: &Path) -> bool { impl PythonVariant { fn matches_interpreter(self, interpreter: &Interpreter) -> bool { match self { - // TODO(zanieb): Right now, we allow debug interpreters to be selected by default for - // backwards compatibility, but we may want to change this in the future. - Self::Default => !interpreter.gil_disabled(), + Self::Default => { + // TODO(zanieb): Right now, we allow debug interpreters to be selected by default for + // backwards compatibility, but we may want to change this in the future. + if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) { + // For Python 3.14+, the free-threaded build is not considered experimental + // and can satisfy the default variant without opt-in + true + } else { + // In Python 3.13 and earlier, the free-threaded build is considered + // experimental and requires explicit opt-in + !interpreter.gil_disabled() + } + } Self::Debug => interpreter.debug_enabled(), Self::Freethreaded => interpreter.gil_disabled(), Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(), diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index dfd797d5c..9f5872e4e 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -1,3 +1,4 @@ +use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::{FileTouch, PathChild}; use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir}; use indoc::indoc; @@ -1256,3 +1257,81 @@ fn python_find_path() { error: No interpreter found at path `foobar` "); } + +#[test] +fn python_find_freethreaded_313() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_python_sources() + .with_managed_python_dirs() + .with_python_download_cache() + .with_filtered_python_install_bin() + .with_filtered_python_names() + .with_filtered_exe_suffix(); + + context + .python_install() + .arg("--preview") + .arg("3.13t") + .assert() + .success(); + + // Request Python 3.13 (without opt-in) + uv_snapshot!(context.filters(), context.python_find().arg("3.13"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No interpreter found for Python 3.13 in [PYTHON SOURCES] + "); + + // Request Python 3.13t (with explicit opt-in) + uv_snapshot!(context.filters(), context.python_find().arg("3.13t"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.13+freethreaded-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); +} + +#[test] +fn python_find_freethreaded_314() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_python_sources() + .with_managed_python_dirs() + .with_python_download_cache() + .with_filtered_python_install_bin() + .with_filtered_python_names() + .with_filtered_exe_suffix(); + + context + .python_install() + .arg("--preview") + .arg("3.14t") + .assert() + .success(); + + // Request Python 3.14 (without opt-in) + uv_snapshot!(context.filters(), context.python_find().arg("3.14"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14+freethreaded-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); + + // Request Python 3.14t (with explicit opt-in) + uv_snapshot!(context.filters(), context.python_find().arg("3.14t"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [TEMP_DIR]/managed/cpython-3.14+freethreaded-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + + ----- stderr ----- + "); +}