Allow selection of debug build interpreters (#11520)
Some checks are pending
CI / typos (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
zizmor / Run zizmor (push) Waiting to run

Extends the `PythonVariant` logic to support interpreters with the debug
flag enabled.
This commit is contained in:
Zanie Blue 2025-09-12 08:32:22 -05:00 committed by GitHub
parent 8917b00fd9
commit 8f3583a6e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 8015 additions and 109 deletions

View file

@ -6,13 +6,6 @@ fn process_json(data: &serde_json::Value) -> serde_json::Value {
if let Some(obj) = data.as_object() { if let Some(obj) = data.as_object() {
for (key, value) in obj { for (key, value) in obj {
if let Some(variant) = value.get("variant") {
// Exclude debug variants for now, we don't support them
if variant == "debug" {
continue;
}
}
out_data.insert(key.clone(), value.clone()); out_data.insert(key.clone(), value.clone());
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -142,6 +142,20 @@ class ImplementationName(StrEnum):
class Variant(StrEnum): class Variant(StrEnum):
FREETHREADED = "freethreaded" FREETHREADED = "freethreaded"
DEBUG = "debug" DEBUG = "debug"
FREETHREADED_DEBUG = "freethreaded+debug"
@classmethod
def from_build_options(
cls: type["Variant"], build_options: list[str]
) -> "Variant" | None:
if "debug" in build_options and "freethreaded" in build_options:
return cls.FREETHREADED_DEBUG
elif "debug" in build_options:
return cls.DEBUG
elif "freethreaded" in build_options:
return cls.FREETHREADED
else:
return None
@dataclass @dataclass
@ -208,7 +222,10 @@ class CPythonFinder(Finder):
cpython- cpython-
(?P<ver>\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?)(?:\+\d+)?\+ (?P<ver>\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?)(?:\+\d+)?\+
(?P<date>\d+)- (?P<date>\d+)-
(?P<triple>[a-z\d_]+-[a-z\d]+(?>-[a-z\d]+)?-[a-z\d]+)- # Note we lookahead to avoid matching "debug" as a triple as we'd
# prefer it matches as a build option; we could enumerate all known
# build options instead but this is the easy path forward
(?P<triple>[a-z\d_]+-[a-z\d]+(?>-[a-z\d]+)?-(?!debug(?:-|$))[a-z\d_]+)-
(?:(?P<build_options>.+)-)? (?:(?P<build_options>.+)-)?
(?P<flavor>[a-z_]+)? (?P<flavor>[a-z_]+)?
\.tar\.(?:gz|zst) \.tar\.(?:gz|zst)
@ -377,13 +394,7 @@ class CPythonFinder(Finder):
flavor = groups.get("flavor", "full") flavor = groups.get("flavor", "full")
build_options = build_options.split("+") if build_options else [] build_options = build_options.split("+") if build_options else []
variant: Variant | None variant = Variant.from_build_options(build_options)
for variant in Variant:
if variant in build_options:
break
else:
variant = None
version = Version.from_str(version) version = Version.from_str(version)
triple = self._normalize_triple(triple) triple = self._normalize_triple(triple)
if triple is None: if triple is None:
@ -766,8 +777,10 @@ def render(downloads: list[PythonDownload]) -> None:
match variant: match variant:
case Variant.FREETHREADED: case Variant.FREETHREADED:
return 1 return 1
case Variant.DEBUG: case Variant.FREETHREADED_DEBUG:
return 2 return 2
case Variant.DEBUG:
return 3
raise ValueError(f"Missing sort key implementation for variant: {variant}") raise ValueError(f"Missing sort key implementation for variant: {variant}")
def sort_key(download: PythonDownload) -> tuple: def sort_key(download: PythonDownload) -> tuple:

View file

@ -654,6 +654,8 @@ def main() -> None:
# The `t` abiflag for freethreading Python. # The `t` abiflag for freethreading Python.
# https://peps.python.org/pep-0703/#build-configuration-changes # https://peps.python.org/pep-0703/#build-configuration-changes
"gil_disabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")), "gil_disabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")),
# https://docs.python.org/3/using/configure.html#debug-build
"debug_enabled": bool(sysconfig.get_config_var("Py_DEBUG")),
# Determine if the interpreter is 32-bit or 64-bit. # Determine if the interpreter is 32-bit or 64-bit.
# https://github.com/python/cpython/blob/b228655c227b2ca298a8ffac44d14ce3d22f6faa/Lib/venv/__init__.py#L136 # https://github.com/python/cpython/blob/b228655c227b2ca298a8ffac44d14ce3d22f6faa/Lib/venv/__init__.py#L136
"pointer_size": "64" if sys.maxsize > 2**32 else "32", "pointer_size": "64" if sys.maxsize > 2**32 else "32",

View file

@ -166,7 +166,9 @@ pub(crate) struct DiscoveryPreferences {
pub enum PythonVariant { pub enum PythonVariant {
#[default] #[default]
Default, Default,
Debug,
Freethreaded, Freethreaded,
FreethreadedDebug,
} }
/// A Python discovery version request. /// A Python discovery version request.
@ -1296,6 +1298,7 @@ pub(crate) fn find_python_installation(
let installations = let installations =
find_python_installations(request, environments, preference, cache, preview); find_python_installations(request, environments, preference, cache, preview);
let mut first_prerelease = None; let mut first_prerelease = None;
let mut first_debug = None;
let mut first_managed = None; let mut first_managed = None;
let mut first_error = None; let mut first_error = None;
for result in installations { for result in installations {
@ -1340,6 +1343,20 @@ pub(crate) fn find_python_installation(
continue; continue;
} }
// If it's a debug build and debug builds aren't allowed, skip it — but store it for later
// since we'll use a debug build if no other versions are available.
if installation.key().variant().is_debug()
&& !request.allows_debug()
&& !installation.source.allows_debug()
&& !has_default_executable_name
{
debug!("Skipping debug installation {}", installation.key());
if first_debug.is_none() {
first_debug = Some(installation.clone());
}
continue;
}
// If it's an alternative implementation and alternative implementations aren't allowed, // If it's an alternative implementation and alternative implementations aren't allowed,
// skip it. Note we avoid querying these interpreters at all if they're on the search path // skip it. Note we avoid querying these interpreters at all if they're on the search path
// and are not requested, but other sources such as the managed installations can include // and are not requested, but other sources such as the managed installations can include
@ -1382,6 +1399,16 @@ pub(crate) fn find_python_installation(
return Ok(Ok(installation)); return Ok(Ok(installation));
} }
// If we only found debug installations, they're implicitly allowed and we should return the
// first one.
if let Some(installation) = first_debug {
debug!(
"Allowing debug installation {}: no non-debug installations",
installation.key()
);
return Ok(Ok(installation));
}
// If we only found pre-releases, they're implicitly allowed and we should return the first one. // If we only found pre-releases, they're implicitly allowed and we should return the first one.
if let Some(installation) = first_prerelease { if let Some(installation) = first_prerelease {
debug!( debug!(
@ -1641,18 +1668,47 @@ fn is_windows_store_shim(_path: &Path) -> bool {
impl PythonVariant { impl PythonVariant {
fn matches_interpreter(self, interpreter: &Interpreter) -> bool { fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
match self { 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 => !interpreter.gil_disabled(),
Self::Debug => interpreter.debug_enabled(),
Self::Freethreaded => interpreter.gil_disabled(), Self::Freethreaded => interpreter.gil_disabled(),
Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
} }
} }
/// Return the lib or 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 suffix(self) -> &'static str {
match self { match self {
Self::Default => "", Self::Default => "",
Self::Debug => "d",
Self::Freethreaded => "t", Self::Freethreaded => "t",
Self::FreethreadedDebug => "td",
}
}
/// Return the lib suffix for the variant, e.g., `t` for `python3.13t` but an empty string for
/// `python3.13d` or `python3.13`.
pub fn lib_suffix(self) -> &'static str {
match self {
Self::Default | Self::Debug => "",
Self::Freethreaded | Self::FreethreadedDebug => "t",
}
}
pub fn is_freethreaded(self) -> bool {
match self {
Self::Default | Self::Debug => false,
Self::Freethreaded | Self::FreethreadedDebug => true,
}
}
pub fn is_debug(self) -> bool {
match self {
Self::Default | Self::Freethreaded => false,
Self::Debug | Self::FreethreadedDebug => true,
} }
} }
} }
@ -1984,6 +2040,19 @@ impl PythonRequest {
} }
} }
/// Whether this request opts-in to a debug Python version.
pub(crate) fn allows_debug(&self) -> bool {
match self {
Self::Default => false,
Self::Any => true,
Self::Version(version) => version.is_debug(),
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
Self::Implementation(_) => false,
Self::ImplementationVersion(_, _) => true,
Self::Key(request) => request.allows_debug(),
}
}
/// Whether this request opts-in to an alternative Python implementation, e.g., PyPy. /// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
pub(crate) fn allows_alternative_implementations(&self) -> bool { pub(crate) fn allows_alternative_implementations(&self) -> bool {
match self { match self {
@ -2043,6 +2112,21 @@ impl PythonSource {
} }
} }
/// Whether a debug Python installation from this source can be used without opt-in.
pub(crate) fn allows_debug(self) -> bool {
match self {
Self::Managed | Self::Registry | Self::MicrosoftStore => false,
Self::SearchPath
| Self::SearchPathFirst
| Self::CondaPrefix
| Self::BaseCondaPrefix
| Self::ProvidedPath
| Self::ParentInterpreter
| Self::ActiveEnvironment
| Self::DiscoveredEnvironment => true,
}
}
/// Whether an alternative Python implementation from this source can be used without opt-in. /// Whether an alternative Python implementation from this source can be used without opt-in.
pub(crate) fn allows_alternative_implementations(self) -> bool { pub(crate) fn allows_alternative_implementations(self) -> bool {
match self { match self {
@ -2423,10 +2507,12 @@ impl VersionRequest {
} }
// Include free-threaded variants // Include free-threaded variants
if self.is_freethreaded() { if let Some(variant) = self.variant() {
for i in 0..names.len() { if variant != PythonVariant::Default {
let name = names[i].with_variant(PythonVariant::Freethreaded); for i in 0..names.len() {
names.push(name); let name = names[i].with_variant(variant);
names.push(name);
}
} }
} }
@ -2725,6 +2811,18 @@ impl VersionRequest {
} }
} }
/// Whether this request is for a debug Python variant.
pub(crate) fn is_debug(&self) -> bool {
match self {
Self::Any | Self::Default => false,
Self::Major(_, variant)
| Self::MajorMinor(_, _, variant)
| Self::MajorMinorPatch(_, _, _, variant)
| Self::MajorMinorPrerelease(_, _, _, variant)
| Self::Range(_, variant) => variant.is_debug(),
}
}
/// Whether this request is for a free-threaded Python variant. /// Whether this request is for a free-threaded Python variant.
pub(crate) fn is_freethreaded(&self) -> bool { pub(crate) fn is_freethreaded(&self) -> bool {
match self { match self {
@ -2733,7 +2831,7 @@ impl VersionRequest {
| Self::MajorMinor(_, _, variant) | Self::MajorMinor(_, _, variant)
| Self::MajorMinorPatch(_, _, _, variant) | Self::MajorMinorPatch(_, _, _, variant)
| Self::MajorMinorPrerelease(_, _, _, variant) | Self::MajorMinorPrerelease(_, _, _, variant)
| Self::Range(_, variant) => variant == &PythonVariant::Freethreaded, | Self::Range(_, variant) => variant.is_freethreaded(),
} }
} }
@ -2778,24 +2876,43 @@ impl FromStr for VersionRequest {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
// Stripping the 't' suffix produces awkward error messages if the user tries a version /// Extract the variant from the end of a version request string, returning the prefix and
// like "latest". HACK: If the version is all letters, don't even try to parse it further. /// the variant type.
if s.chars().all(char::is_alphabetic) { fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
return Err(Error::InvalidVersionRequest(s.to_string())); // This cannot be a valid version, just error immediately
} if s.chars().all(char::is_alphabetic) {
return Err(Error::InvalidVersionRequest(s.to_string()));
// Check if the version request is for a free-threaded Python version }
let (s, variant) = s
.strip_suffix('t') let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
.map_or((s, PythonVariant::Default), |s| { return Ok((s, PythonVariant::Default));
(s, PythonVariant::Freethreaded) };
});
// Advance past the first digit
if variant == PythonVariant::Freethreaded && s.ends_with('t') { start += 1;
// More than one trailing "t" is not allowed
return Err(Error::InvalidVersionRequest(format!("{s}t"))); // Ensure we're not out of bounds
if start + 1 > s.len() {
return Ok((s, PythonVariant::Default));
}
let variant = &s[start..];
let prefix = &s[..start];
// Strip a leading `+` if present
let variant = variant.strip_prefix('+').unwrap_or(variant);
// TODO(zanieb): Special-case error for use of `dt` instead of `td`
// If there's not a valid variant, fallback to failure in [`Version::from_str`]
let Ok(variant) = PythonVariant::from_str(variant) else {
return Ok((s, PythonVariant::Default));
};
Ok((prefix, variant))
} }
let (s, variant) = parse_variant(s)?;
let Ok(version) = Version::from_str(s) else { let Ok(version) = Version::from_str(s) else {
return parse_version_specifiers_request(s, variant); return parse_version_specifiers_request(s, variant);
}; };
@ -2808,26 +2925,11 @@ impl FromStr for VersionRequest {
return Err(Error::InvalidVersionRequest(s.to_string())); return Err(Error::InvalidVersionRequest(s.to_string()));
} }
// Check if the local version includes a variant // We don't allow local version suffixes unless they're variants, in which case they'd
let variant = if version.local().is_empty() { // already be stripped.
variant if !version.local().is_empty() {
} else { return Err(Error::InvalidVersionRequest(s.to_string()));
// If we already have a variant, do not allow another to be requested }
if variant != PythonVariant::Default {
return Err(Error::InvalidVersionRequest(s.to_string()));
}
let uv_pep440::LocalVersionSlice::Segments([uv_pep440::LocalSegment::String(local)]) =
version.local()
else {
return Err(Error::InvalidVersionRequest(s.to_string()));
};
match local.as_str() {
"freethreaded" => PythonVariant::Freethreaded,
_ => return Err(Error::InvalidVersionRequest(s.to_string())),
}
};
// Cast the release components into u8s since that's what we use in `VersionRequest` // Cast the release components into u8s since that's what we use in `VersionRequest`
let Ok(release) = try_into_u8_slice(&version.release()) else { let Ok(release) = try_into_u8_slice(&version.release()) else {
@ -2879,6 +2981,8 @@ impl FromStr for PythonVariant {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"t" | "freethreaded" => Ok(Self::Freethreaded), "t" | "freethreaded" => Ok(Self::Freethreaded),
"d" | "debug" => Ok(Self::Debug),
"td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
"" => Ok(Self::Default), "" => Ok(Self::Default),
_ => Err(()), _ => Err(()),
} }
@ -2889,7 +2993,9 @@ impl fmt::Display for PythonVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Default => f.write_str("default"), Self::Default => f.write_str("default"),
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"),
} }
} }
} }
@ -2919,23 +3025,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, PythonVariant::Default) => write!(f, "{major}"), Self::Major(major, variant) => write!(f, "{major}{}", variant.suffix()),
Self::Major(major, PythonVariant::Freethreaded) => write!(f, "{major}t"), Self::MajorMinor(major, minor, variant) => {
Self::MajorMinor(major, minor, PythonVariant::Default) => write!(f, "{major}.{minor}"), write!(f, "{major}.{minor}{}", variant.suffix())
Self::MajorMinor(major, minor, PythonVariant::Freethreaded) => {
write!(f, "{major}.{minor}t")
} }
Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default) => { Self::MajorMinorPatch(major, minor, patch, variant) => {
write!(f, "{major}.{minor}.{patch}") write!(f, "{major}.{minor}.{patch}{}", variant.suffix())
} }
Self::MajorMinorPatch(major, minor, patch, PythonVariant::Freethreaded) => { Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
write!(f, "{major}.{minor}.{patch}t") write!(f, "{major}.{minor}{prerelease}{}", variant.suffix())
}
Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default) => {
write!(f, "{major}.{minor}{prerelease}")
}
Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Freethreaded) => {
write!(f, "{major}.{minor}{prerelease}t")
} }
Self::Range(specifiers, _) => write!(f, "{specifiers}"), Self::Range(specifiers, _) => write!(f, "{specifiers}"),
} }

View file

@ -497,6 +497,11 @@ impl PythonDownloadRequest {
}) })
} }
/// Whether this download request opts-in to a debug Python version.
pub fn allows_debug(&self) -> bool {
self.version.as_ref().is_some_and(VersionRequest::is_debug)
}
/// Whether this download request opts-in to alternative Python implementations. /// Whether this download request opts-in to alternative Python implementations.
pub fn allows_alternative_implementations(&self) -> bool { pub fn allows_alternative_implementations(&self) -> bool {
self.implementation self.implementation

View file

@ -491,7 +491,7 @@ impl fmt::Display for PythonInstallationKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let variant = match self.variant { let variant = match self.variant {
PythonVariant::Default => String::new(), PythonVariant::Default => String::new(),
PythonVariant::Freethreaded => format!("+{}", self.variant), _ => format!("+{}", self.variant),
}; };
write!( write!(
f, f,
@ -632,7 +632,7 @@ impl fmt::Display for PythonInstallationMinorVersionKey {
// and prerelease (with special formatting for the variant). // and prerelease (with special formatting for the variant).
let variant = match self.0.variant { let variant = match self.0.variant {
PythonVariant::Default => String::new(), PythonVariant::Default => String::new(),
PythonVariant::Freethreaded => format!("+{}", self.0.variant), _ => format!("+{}", self.0.variant),
}; };
write!( write!(
f, f,

View file

@ -37,6 +37,7 @@ use crate::{
use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR}; use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
/// A Python executable and its associated platform markers. /// A Python executable and its associated platform markers.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Interpreter { pub struct Interpreter {
platform: Platform, platform: Platform,
@ -59,6 +60,7 @@ pub struct Interpreter {
pointer_size: PointerSize, pointer_size: PointerSize,
gil_disabled: bool, gil_disabled: bool,
real_executable: PathBuf, real_executable: PathBuf,
debug_enabled: bool,
} }
impl Interpreter { impl Interpreter {
@ -82,6 +84,7 @@ impl Interpreter {
sys_base_exec_prefix: info.sys_base_exec_prefix, sys_base_exec_prefix: info.sys_base_exec_prefix,
pointer_size: info.pointer_size, pointer_size: info.pointer_size,
gil_disabled: info.gil_disabled, gil_disabled: info.gil_disabled,
debug_enabled: info.debug_enabled,
sys_base_prefix: info.sys_base_prefix, sys_base_prefix: info.sys_base_prefix,
sys_base_executable: info.sys_base_executable, sys_base_executable: info.sys_base_executable,
sys_executable: info.sys_executable, sys_executable: info.sys_executable,
@ -212,7 +215,13 @@ impl Interpreter {
pub fn variant(&self) -> PythonVariant { pub fn variant(&self) -> PythonVariant {
if self.gil_disabled() { if self.gil_disabled() {
PythonVariant::Freethreaded if self.debug_enabled() {
PythonVariant::FreethreadedDebug
} else {
PythonVariant::Freethreaded
}
} else if self.debug_enabled() {
PythonVariant::Debug
} else { } else {
PythonVariant::default() PythonVariant::default()
} }
@ -508,6 +517,12 @@ impl Interpreter {
self.gil_disabled self.gil_disabled
} }
/// Return whether this is a debug build of Python, as specified by the sysconfig var
/// `Py_DEBUG`.
pub fn debug_enabled(&self) -> bool {
self.debug_enabled
}
/// Return the `--target` directory for this interpreter, if any. /// Return the `--target` directory for this interpreter, if any.
pub fn target(&self) -> Option<&Target> { pub fn target(&self) -> Option<&Target> {
self.target.as_ref() self.target.as_ref()
@ -877,6 +892,7 @@ pub enum InterpreterInfoError {
EmscriptenNotPyodide, EmscriptenNotPyodide,
} }
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct InterpreterInfo { struct InterpreterInfo {
platform: Platform, platform: Platform,
@ -895,6 +911,7 @@ struct InterpreterInfo {
standalone: bool, standalone: bool,
pointer_size: PointerSize, pointer_size: PointerSize,
gil_disabled: bool, gil_disabled: bool,
debug_enabled: bool,
} }
impl InterpreterInfo { impl InterpreterInfo {
@ -1299,7 +1316,8 @@ mod tests {
"scripts": "bin" "scripts": "bin"
}, },
"pointer_size": "64", "pointer_size": "64",
"gil_disabled": true "gil_disabled": true,
"debug_enabled": false
} }
"##}; "##};

View file

@ -294,7 +294,8 @@ mod tests {
"scripts": "bin" "scripts": "bin"
}, },
"pointer_size": "64", "pointer_size": "64",
"gil_disabled": {FREE_THREADED} "gil_disabled": {FREE_THREADED},
"debug_enabled": false
} }
"##}; "##};
@ -385,7 +386,8 @@ mod tests {
"data": "" "data": ""
}, },
"pointer_size": "32", "pointer_size": "32",
"gil_disabled": false "gil_disabled": false,
"debug_enabled": false
} }
"##}; "##};

View file

@ -574,7 +574,7 @@ impl ManagedPythonInstallation {
let stdlib = if self.key.os().is_windows() { let stdlib = if self.key.os().is_windows() {
self.python_dir().join("Lib") self.python_dir().join("Lib")
} else { } else {
let lib_suffix = self.key.variant.suffix(); let lib_suffix = self.key.variant.lib_suffix();
let python = if matches!( let python = if matches!(
self.key.implementation, self.key.implementation,
LenientImplementationName::Known(ImplementationName::PyPy) LenientImplementationName::Known(ImplementationName::PyPy)
@ -605,7 +605,7 @@ impl ManagedPythonInstallation {
self.path(), self.path(),
self.key.major, self.key.major,
self.key.minor, self.key.minor,
self.key.variant.suffix(), self.key.variant.lib_suffix(),
)?; )?;
} }
} }

View file

@ -106,7 +106,7 @@ fn find_sysconfigdata(
.join("lib") .join("lib")
.join(format!("python{major}.{minor}{suffix}")); .join(format!("python{major}.{minor}{suffix}"));
if !lib.exists() { if !lib.exists() {
return Err(Error::MissingLib); return Err(Error::MissingLib(lib));
} }
// Probe the `lib` directory for `_sysconfigdata_`. // Probe the `lib` directory for `_sysconfigdata_`.
@ -270,8 +270,8 @@ fn patch_pkgconfig(contents: &str) -> Option<String> {
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Python installation is missing a `lib` directory")] #[error("Python installation is missing a `lib` directory at: {0}")]
MissingLib, MissingLib(PathBuf),
#[error("Python installation is missing a `_sysconfigdata_` file")] #[error("Python installation is missing a `_sysconfigdata_` file")]
MissingSysconfigdata, MissingSysconfigdata,
#[error(transparent)] #[error(transparent)]

View file

@ -109,7 +109,9 @@ pub(crate) async fn list(
.map(|a| PythonDownloadRequest::iter_downloads(a, python_downloads_json_url.as_deref())) .map(|a| PythonDownloadRequest::iter_downloads(a, python_downloads_json_url.as_deref()))
.transpose()? .transpose()?
.into_iter() .into_iter()
.flatten(); .flatten()
// TODO(zanieb): Add a way to show debug downloads, we just hide them for now
.filter(|download| download.key().variant() != &uv_python::PythonVariant::Debug);
for download in downloads { for download in downloads {
output.insert(( output.insert((

View file

@ -233,10 +233,10 @@ impl TestContext {
// On Unix, we'll strip version numbers // On Unix, we'll strip version numbers
if name == "python" { if name == "python" {
// We can't require them in this case since `/python` is common // We can't require them in this case since `/python` is common
r"(\d\.\d+|\d)?".to_string() r"(\d\.\d+|\d)?(t|d|td)?".to_string()
} else { } else {
// However, for other names we'll require them to avoid over-matching // However, for other names we'll require them to avoid over-matching
r"(\d\.\d+|\d)".to_string() r"(\d\.\d+|\d)(t|d|td)?".to_string()
} }
}; };
@ -412,7 +412,7 @@ impl TestContext {
)? # (we allow the patch version to be missing entirely, e.g., in a request) )? # (we allow the patch version to be missing entirely, e.g., in a request)
(?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2` (?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2`
(?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug) (?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug)
(?:\+[a-z]+)? # A long variant, such as `+free-threaded` (?:(\+[a-z]+)+)? # A long variant, such as `+freethreaded` or `+freethreaded+debug`
) )
- -
[a-z0-9]+ # Operating system (e.g., 'macos') [a-z0-9]+ # Operating system (e.g., 'macos')

View file

@ -2439,7 +2439,7 @@ fn init_requires_python_specifiers() -> Result<()> {
})?; })?;
let child = context.temp_dir.join("foo"); let child = context.temp_dir.join("foo");
uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child).arg("--python").arg("==3.9.*"), @r###" uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg(&child).arg("--python").arg("==3.9.*"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2447,7 +2447,7 @@ fn init_requires_python_specifiers() -> Result<()> {
----- stderr ----- ----- stderr -----
Adding `foo` as member of workspace `[TEMP_DIR]/` Adding `foo` as member of workspace `[TEMP_DIR]/`
Initialized project `foo` at `[TEMP_DIR]/foo` Initialized project `foo` at `[TEMP_DIR]/foo`
"###); ");
let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?; let pyproject_toml = fs_err::read_to_string(child.join("pyproject.toml"))?;
insta::with_settings!({ insta::with_settings!({

View file

@ -11836,14 +11836,14 @@ fn lock_local_index() -> Result<()> {
Url::from_file_path(&root).unwrap().as_str() Url::from_file_path(&root).unwrap().as_str()
})?; })?;
uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 2 packages in [TIME] Resolved 2 packages in [TIME]
"###); ");
let lock = context.read("uv.lock"); let lock = context.read("uv.lock");

View file

@ -42,6 +42,16 @@ fn python_find() {
----- stderr ----- ----- stderr -----
"###); "###);
// Request Python 3.12
uv_snapshot!(context.filters(), context.python_find().arg("==3.12.*"), @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-3.12]
----- stderr -----
"###);
// Request Python 3.11 // Request Python 3.11
uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###" uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###"
success: true success: true

View file

@ -1060,9 +1060,11 @@ fn python_install_preview_upgrade() {
fn python_install_freethreaded() { fn python_install_freethreaded() {
let context: TestContext = TestContext::new_with_versions(&[]) let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys() .with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs() .with_managed_python_dirs()
.with_python_download_cache(); .with_python_download_cache()
.with_filtered_python_install_bin()
.with_filtered_python_names()
.with_filtered_exe_suffix();
// Install the latest version // Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13t"), @r" uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13t"), @r"
@ -1097,6 +1099,26 @@ fn python_install_freethreaded() {
----- stderr ----- ----- stderr -----
"###); "###);
// We should find it with 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 -----
");
// We should be able to select it with `+freethreaded`
uv_snapshot!(context.filters(), context.python_find().arg("3.13+freethreaded"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+freethreaded-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
----- stderr -----
");
// Create a virtual environment with the freethreaded Python // Create a virtual environment with the freethreaded Python
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.13t"), @r" uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.13t"), @r"
success: true success: true
@ -1190,6 +1212,284 @@ fn python_install_freethreaded() {
"); ");
} }
// We only support debug builds on Unix
#[cfg(unix)]
#[test]
fn python_install_debug() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13+debug"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.7 in [TIME]
+ cpython-3.13.7+debug-[PLATFORM] (python3.13d)
");
let bin_python = context
.bin_dir
.child(format!("python3.13d{}", std::env::consts::EXE_SUFFIX));
// The executable should be installed in the bin directory
bin_python.assert(predicate::path::exists());
// On Unix, it should be a link
#[cfg(unix)]
bin_python.assert(predicate::path::is_symlink());
// The executable should "work"
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
success: true
exit_code: 0
----- stdout -----
hello world
----- stderr -----
"###);
// We should find it with opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13d"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+debug-[PLATFORM]/bin/python3.13d
----- stderr -----
");
// We should find it without opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+debug-[PLATFORM]/bin/python3.13d
----- stderr -----
");
// Should be distinct from 3.13
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.7 in [TIME]
+ cpython-3.13.7-[PLATFORM] (python3.13)
");
// Now we should prefer the non-debug version without opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13.7-[PLATFORM]/bin/python3.13
----- stderr -----
");
// But still select it with opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13d"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+debug-[PLATFORM]/bin/python3.13d
----- stderr -----
");
// We should allow selection with `+debug`
uv_snapshot!(context.filters(), context.python_find().arg("3.13+debug"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+debug-[PLATFORM]/bin/python3.13d
----- stderr -----
");
// Should work with older Python versions too
uv_snapshot!(context.filters(), context.python_install().arg("3.12d"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.12.11 in [TIME]
+ cpython-3.12.11+debug-[PLATFORM] (python3.12d)
");
uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python installations
Uninstalled 3 versions in [TIME]
- cpython-3.12.11+debug-[PLATFORM] (python3.12d)
- cpython-3.13.7+debug-[PLATFORM] (python3.13d)
- cpython-3.13.7-[PLATFORM] (python3.13)
");
}
// We only support debug builds on Unix
#[cfg(unix)]
#[test]
fn python_install_debug_freethreaded() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13td"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.7 in [TIME]
+ cpython-3.13.7+freethreaded+debug-[PLATFORM] (python3.13td)
");
let bin_python = context
.bin_dir
.child(format!("python3.13td{}", std::env::consts::EXE_SUFFIX));
// The executable should be installed in the bin directory
bin_python.assert(predicate::path::exists());
// On Unix, it should be a link
#[cfg(unix)]
bin_python.assert(predicate::path::is_symlink());
// The executable should "work"
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
success: true
exit_code: 0
----- stdout -----
hello world
----- stderr -----
"###);
// We should find it with opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13td"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+freethreaded+debug-[PLATFORM]/bin/python3.13td
----- stderr -----
");
// We should not find it 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 virtual environments, managed installations, or search path
");
// We should allow selection with `+freethread+debug`
// TODO(zanieb): We don't support this yet
uv_snapshot!(context.filters(), context.python_find().arg("3.13+freethreaded+debug"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+freethreaded+debug-[PLATFORM]/bin/python3.13td
----- stderr -----
");
// Should be distinct from 3.13
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.7 in [TIME]
+ cpython-3.13.7-[PLATFORM] (python3.13)
");
// Should be distinct from 3.13t
uv_snapshot!(context.filters(), context.python_install().arg("3.13t"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.7 in [TIME]
+ cpython-3.13.7+freethreaded-[PLATFORM] (python3.13t)
");
// Should be distinct from 3.13d
uv_snapshot!(context.filters(), context.python_install().arg("3.13d"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.7 in [TIME]
+ cpython-3.13.7+debug-[PLATFORM] (python3.13d)
");
// Now we should prefer the non-debug version without opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13.7-[PLATFORM]/bin/python3.13
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_find().arg("3.13t"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13.7+freethreaded-[PLATFORM]/bin/python3.13t
----- stderr -----
");
// But still select it with opt-in
uv_snapshot!(context.filters(), context.python_find().arg("3.13td"), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13+freethreaded+debug-[PLATFORM]/bin/python3.13td
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python installations
Uninstalled 4 versions in [TIME]
- cpython-3.13.7+freethreaded+debug-[PLATFORM] (python3.13td)
- cpython-3.13.7+freethreaded-[PLATFORM] (python3.13t)
- cpython-3.13.7+debug-[PLATFORM] (python3.13d)
- cpython-3.13.7-[PLATFORM] (python3.13)
");
}
#[test] #[test]
fn python_install_invalid_request() { fn python_install_invalid_request() {
let context: TestContext = TestContext::new_with_versions(&[]) let context: TestContext = TestContext::new_with_versions(&[])