Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/80.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``debug_build`` attribute to :class:`PythonInfo` exposing whether the interpreter is a debug build
(``Py_DEBUG``) - by :user:`gaborbernat`.
18 changes: 9 additions & 9 deletions src/python_discovery/_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sysconfig
import warnings
from collections import OrderedDict
from itertools import product
from string import digits
from typing import TYPE_CHECKING, ClassVar, Final, NamedTuple

Expand Down Expand Up @@ -73,6 +74,7 @@ def _init_identity(self) -> None:
self.version = sys.version
self.os = os.name
self.free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
self.debug_build = bool(sysconfig.get_config_var("Py_DEBUG"))

def _init_prefixes(self) -> None:
def abs_path(value: str | None) -> str | None:
Expand Down Expand Up @@ -404,10 +406,11 @@ def machine(self) -> str:
@property
def spec(self) -> str:
"""A specification string identifying this interpreter (e.g. ``CPython3.13.2-64-arm64``)."""
return "{}{}{}-{}-{}".format(
return "{}{}{}{}-{}-{}".format(
self.implementation,
".".join(str(i) for i in self.version_info),
"t" if self.free_threaded else "",
"d" if self.debug_build else "",
self.architecture,
self.machine,
)
Expand Down Expand Up @@ -731,17 +734,14 @@ def _find_possible_folders(self, inside_folder: str) -> list[str]:

def _find_possible_exe_names(self) -> list[str]:
name_candidate = OrderedDict()
mods = ["", "t"] if self.free_threaded else [""]
debug_suffixes = ["_d", ""] if self.debug_build else [""]
archs = [f"-{self.architecture}", ""]
for name in self._possible_base():
for at in (3, 2, 1, 0):
version = ".".join(str(i) for i in self.version_info[:at])
mods = [""]
if self.free_threaded:
mods.append("t")
for mod in mods:
for arch in [f"-{self.architecture}", ""]:
for ext in EXTENSIONS:
candidate = f"{name}{version}{mod}{arch}{ext}"
name_candidate[candidate] = None
for mod, debug, arch, ext in product(mods, debug_suffixes, archs, EXTENSIONS):
name_candidate[f"{name}{version}{mod}{debug}{arch}{ext}"] = None
return list(name_candidate.keys())

def _possible_base(self) -> Generator[str, None, None]:
Expand Down
23 changes: 23 additions & 0 deletions tests/py_info/test_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def test_current_as_json() -> None:
"serial": serial,
}
assert parsed["free_threaded"] is free_threaded
assert parsed["debug_build"] is bool(sysconfig.get_config_var("Py_DEBUG"))


def test_bad_exe_py_info_raise(tmp_path: Path, session_cache: DiskCache) -> None:
Expand Down Expand Up @@ -418,6 +419,28 @@ def test_py_info_satisfies_machine_cross_os_normalization(platform: str, spec_ma
assert info.satisfies(spec, impl_must_match=True) is True


@pytest.mark.parametrize("debug", [True, False], ids=["debug", "release"])
def test_py_info_debug_build_in_spec(*, debug: bool) -> None:
info = copy.deepcopy(CURRENT)
info.debug_build = debug
assert ("d-" in info.spec) is debug


@pytest.mark.parametrize("debug", [True, False], ids=["debug", "release"])
def test_py_info_debug_build_exe_names(*, debug: bool) -> None:
info = copy.deepcopy(CURRENT)
info.debug_build = debug
names = info._find_possible_exe_names()
assert any("_d" in n for n in names) is debug


def test_py_info_debug_build_json_round_trip() -> None:
info = copy.deepcopy(CURRENT)
info.debug_build = True
restored = PythonInfo.from_json(info.to_json())
assert restored.debug_build is True


def test_py_info_to_dict_includes_sysconfig_platform() -> None:
data = CURRENT.to_dict()
assert "sysconfig_platform" in data
Expand Down