Skip to content

Commit 71343e3

Browse files
authored
Merge pull request #46 from DavidCEllis/cache_runs
Cache details of Python installs that are queried
2 parents 8db7f6f + 99b808f commit 71343e3

File tree

15 files changed

+660
-208
lines changed

15 files changed

+660
-208
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ celerybeat.pid
117117
# Environments
118118
.env
119119
.venv
120+
.venv_*/
120121
env/
121122
venv/
122123
ENV/

src/ducktools/pythonfinder/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
import sys
3434
from ._version import __version__
35-
from .shared import PythonInstall
35+
from .shared import PythonInstall, DetailFinder
3636

3737

3838
if sys.platform == "win32":
@@ -43,9 +43,10 @@
4343
from .linux import get_python_installs
4444

4545

46-
def list_python_installs(*, query_executables=True):
46+
def list_python_installs(*, query_executables: bool = True, finder: "DetailFinder | None" = None):
47+
finder = DetailFinder() if finder is None else finder
4748
return sorted(
48-
get_python_installs(query_executables=query_executables),
49+
get_python_installs(query_executables=query_executables, finder=finder),
4950
reverse=True,
5051
key=lambda x: (x.version[3], *x.version[:3], x.version[4])
5152
)

src/ducktools/pythonfinder/details_script.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>a|b|c|rc)?(?P<serial>\d*)?"
3030

3131

32-
def version_str_to_tuple(version):
32+
def version_str_to_tuple(version: str):
3333
# Needed to parse GraalPy versions only available as strings
3434
import re
3535

3636
parsed_version = re.fullmatch(FULL_PY_VER_RE, version)
37+
if parsed_version is None:
38+
raise ValueError(f"'version' must be a valid Python version string, not {version!r}")
3739

3840
major, minor, micro, releaselevel, serial = parsed_version.groups()
3941

@@ -70,11 +72,11 @@ def get_details():
7072
# Special case GraalPy as it erroneously reports the CPython target
7173
# instead of the Graal versiion
7274
try:
73-
ver = __graalpython__.get_graalvm_version()
75+
ver = __graalpython__.get_graalvm_version() # type: ignore
7476
metadata = {
7577
"graalpy_version": version_str_to_tuple(ver)
7678
}
77-
except NameError:
79+
except (NameError, ValueError):
7880
metadata = {"{}_version".format(implementation): sys.implementation.version}
7981
elif implementation != "cpython": # pragma: no cover
8082
metadata = {"{}_version".format(implementation): sys.implementation.version}

src/ducktools/pythonfinder/linux/__init__.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,17 @@
2727
import itertools
2828
from _collections_abc import Iterator
2929

30-
from ..shared import PythonInstall, get_folder_pythons, get_uv_pythons, get_uv_python_path
30+
from ..shared import (
31+
DetailFinder,
32+
PythonInstall,
33+
get_folder_pythons,
34+
get_uv_pythons,
35+
get_uv_python_path
36+
)
3137
from .pyenv_search import get_pyenv_pythons, get_pyenv_root
3238

3339

34-
def get_path_pythons() -> Iterator[PythonInstall]:
40+
def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonInstall]:
3541
exe_names = set()
3642

3743
path_folders = os.environ.get("PATH", "").split(":")
@@ -40,6 +46,8 @@ def get_path_pythons() -> Iterator[PythonInstall]:
4046

4147
excluded_folders = [pyenv_root, uv_root]
4248

49+
finder = DetailFinder if finder is None else finder
50+
4351
for fld in path_folders:
4452
# Don't retrieve pyenv installs
4553
skip_folder = False
@@ -54,7 +62,7 @@ def get_path_pythons() -> Iterator[PythonInstall]:
5462
if not os.path.exists(fld):
5563
continue
5664

57-
for install in get_folder_pythons(fld):
65+
for install in get_folder_pythons(fld, finder=finder):
5866
name = os.path.basename(install.executable)
5967
if name in exe_names:
6068
install.shadowed = True
@@ -63,17 +71,24 @@ def get_path_pythons() -> Iterator[PythonInstall]:
6371
yield install
6472

6573

66-
def get_python_installs(*, query_executables=True):
74+
def get_python_installs(
75+
*,
76+
query_executables: bool = True,
77+
finder: DetailFinder | None = None,
78+
) -> Iterator[PythonInstall]:
6779
listed_pythons = set()
6880

81+
finder = DetailFinder() if finder is None else finder
82+
6983
chain_commands = [
70-
get_pyenv_pythons(query_executables=query_executables),
71-
get_uv_pythons(query_executables=query_executables),
84+
get_pyenv_pythons(query_executables=query_executables, finder=finder),
85+
get_uv_pythons(query_executables=query_executables, finder=finder),
7286
]
7387
if query_executables:
74-
chain_commands.append(get_path_pythons())
88+
chain_commands.append(get_path_pythons(finder=finder))
7589

76-
for py in itertools.chain.from_iterable(chain_commands):
77-
if py.executable not in listed_pythons:
78-
yield py
79-
listed_pythons.add(py.executable)
90+
with finder:
91+
for py in itertools.chain.from_iterable(chain_commands):
92+
if py.executable not in listed_pythons:
93+
yield py
94+
listed_pythons.add(py.executable)

src/ducktools/pythonfinder/linux/pyenv_search.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
# SOFTWARE.
2323
from __future__ import annotations
2424

25-
2625
"""
2726
Discover python installs that have been created with pyenv
2827
"""
@@ -33,10 +32,11 @@
3332

3433
from ducktools.lazyimporter import LazyImporter, FromImport, ModuleImport
3534

36-
from ..shared import PythonInstall, get_install_details, FULL_PY_VER_RE, version_str_to_tuple
35+
from ..shared import PythonInstall, DetailFinder, FULL_PY_VER_RE, INSTALLER_CACHE_PATH, version_str_to_tuple
3736

3837
_laz = LazyImporter(
3938
[
39+
ModuleImport("json"),
4040
ModuleImport("re"),
4141
FromImport("subprocess", "run"),
4242
]
@@ -52,11 +52,24 @@ def get_pyenv_root() -> str | None:
5252
pyenv_root = os.environ.get("PYENV_ROOT")
5353
if not pyenv_root:
5454
try:
55-
output = _laz.run(["pyenv", "root"], capture_output=True, text=True)
56-
except FileNotFoundError:
57-
return None
55+
with open(INSTALLER_CACHE_PATH) as f:
56+
installer_cache = _laz.json.load(f)
57+
except (FileNotFoundError, _laz.json.JSONDecodeError):
58+
installer_cache = {}
59+
60+
pyenv_root = installer_cache.get("pyenv")
61+
if pyenv_root is None or not os.path.exists(pyenv_root):
62+
try:
63+
output = _laz.run(["pyenv", "root"], capture_output=True, text=True)
64+
except FileNotFoundError:
65+
return None
66+
67+
pyenv_root = output.stdout.strip()
5868

59-
pyenv_root = output.stdout.strip()
69+
installer_cache["pyenv"] = pyenv_root
70+
os.makedirs(os.path.dirname(INSTALLER_CACHE_PATH), exist_ok=True)
71+
with open(INSTALLER_CACHE_PATH, 'w') as f:
72+
_laz.json.dump(installer_cache, f)
6073

6174
return pyenv_root
6275

@@ -65,6 +78,7 @@ def get_pyenv_pythons(
6578
versions_folder: str | os.PathLike | None = None,
6679
*,
6780
query_executables: bool = True,
81+
finder: DetailFinder = None,
6882
) -> Iterator[PythonInstall]:
6983
if versions_folder is None:
7084
if pyenv_root := get_pyenv_root():
@@ -77,26 +91,32 @@ def get_pyenv_pythons(
7791
# This can lead to much faster returns by potentially yielding
7892
# the required python version before checking pypy/graalpy/micropython
7993

80-
for p in sorted(os.scandir(versions_folder), key=lambda x: x.path):
81-
executable = os.path.join(p.path, "bin/python")
82-
83-
if os.path.exists(executable):
84-
if p.name.endswith("t"):
85-
freethreaded = True
86-
version = p.name[:-1]
87-
else:
88-
freethreaded = False
89-
version = p.name
90-
if _laz.re.fullmatch(FULL_PY_VER_RE, version):
91-
version_tuple = version_str_to_tuple(version)
92-
metadata = {}
93-
if version_tuple >= (3, 13):
94-
metadata["freethreaded"] = freethreaded
95-
yield PythonInstall(
96-
version=version_tuple,
97-
executable=executable,
98-
metadata=metadata,
99-
managed_by="pyenv",
100-
)
101-
elif query_executables and (install := get_install_details(executable, managed_by="pyenv")):
102-
yield install
94+
finder = DetailFinder() if finder is None else finder
95+
96+
with finder:
97+
for p in sorted(os.scandir(versions_folder), key=lambda x: x.path):
98+
executable = os.path.join(p.path, "bin/python")
99+
100+
if os.path.exists(executable):
101+
if p.name.endswith("t"):
102+
freethreaded = True
103+
version = p.name[:-1]
104+
else:
105+
freethreaded = False
106+
version = p.name
107+
if _laz.re.fullmatch(FULL_PY_VER_RE, version):
108+
version_tuple = version_str_to_tuple(version)
109+
metadata = {}
110+
if version_tuple >= (3, 13):
111+
metadata["freethreaded"] = freethreaded
112+
yield PythonInstall(
113+
version=version_tuple,
114+
executable=executable,
115+
metadata=metadata,
116+
managed_by="pyenv",
117+
)
118+
elif (
119+
query_executables
120+
and (install := finder.get_install_details(executable, managed_by="pyenv"))
121+
):
122+
yield install

0 commit comments

Comments
 (0)