Skip to content

Commit bcac8f8

Browse files
authored
Merge pull request #50 from DavidCEllis/expand_cache_usage
Don't rely on uv/pyenv/windows registry for runtime details - use the script and cache.
2 parents 5973e05 + 8b64b52 commit bcac8f8

File tree

12 files changed

+135
-261
lines changed

12 files changed

+135
-261
lines changed

src/ducktools/pythonfinder/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2121
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
# SOFTWARE.
23-
23+
from __future__ import annotations
2424
# Find platform python versions
2525

2626
__all__ = [
@@ -43,10 +43,10 @@
4343
from .linux import get_python_installs
4444

4545

46-
def list_python_installs(*, query_executables: bool = True, finder: "DetailFinder | None" = None):
46+
def list_python_installs(*, finder: DetailFinder | None = None):
4747
finder = DetailFinder() if finder is None else finder
4848
return sorted(
49-
get_python_installs(query_executables=query_executables, finder=finder),
49+
get_python_installs(finder=finder),
5050
reverse=True,
5151
key=lambda x: (x.version[3], *x.version[:3], x.version[4])
5252
)

src/ducktools/pythonfinder/__main__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,6 @@ def get_parser():
115115
description="Discover base Python installs",
116116
)
117117
parser.add_argument("-V", "--version", action="version", version=__version__)
118-
parser.add_argument(
119-
"--fast",
120-
action="store_true",
121-
help="Skip Python installs that need to be launched to obtain metadata"
122-
)
123118

124119
subparsers = parser.add_subparsers(dest="command", required=False)
125120

@@ -169,7 +164,6 @@ def display_local_installs(
169164
min_ver=None,
170165
max_ver=None,
171166
compatible=None,
172-
query_executables=True,
173167
):
174168
if min_ver:
175169
min_ver = tuple(int(i) for i in min_ver.split("."))
@@ -178,7 +172,7 @@ def display_local_installs(
178172
if compatible:
179173
compatible = tuple(int(i) for i in compatible.split("."))
180174

181-
installs = list_python_installs(query_executables=query_executables)
175+
installs = list_python_installs()
182176

183177
headings = ["Version", "Executable Location"]
184178

@@ -333,7 +327,6 @@ def main():
333327
min_ver=vals.min,
334328
max_ver=vals.max,
335329
compatible=vals.compatible,
336-
query_executables=not vals.fast,
337330
)
338331
else:
339332
# No arguments to parse

src/ducktools/pythonfinder/details_script.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ def get_details():
9090
freethreaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
9191
metadata["freethreaded"] = freethreaded
9292

93-
9493
install = dict(
9594
version=list(sys.version_info),
9695
executable=sys.executable,

src/ducktools/pythonfinder/linux/__init__.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,17 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
7373

7474
def get_python_installs(
7575
*,
76-
query_executables: bool = True,
7776
finder: DetailFinder | None = None,
7877
) -> Iterator[PythonInstall]:
7978
listed_pythons = set()
8079

8180
finder = DetailFinder() if finder is None else finder
8281

8382
chain_commands = [
84-
get_pyenv_pythons(query_executables=query_executables, finder=finder),
85-
get_uv_pythons(query_executables=query_executables, finder=finder),
83+
get_pyenv_pythons(finder=finder),
84+
get_uv_pythons(finder=finder),
85+
get_path_pythons(finder=finder),
8686
]
87-
if query_executables:
88-
chain_commands.append(get_path_pythons(finder=finder))
89-
9087
with finder:
9188
for py in itertools.chain.from_iterable(chain_commands):
9289
if py.executable not in listed_pythons:

src/ducktools/pythonfinder/linux/pyenv_search.py

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

3333
from ducktools.lazyimporter import LazyImporter, FromImport, ModuleImport
3434

35-
from ..shared import PythonInstall, DetailFinder, FULL_PY_VER_RE, INSTALLER_CACHE_PATH, version_str_to_tuple
35+
from ..shared import PythonInstall, DetailFinder, INSTALLER_CACHE_PATH
3636

3737
_laz = LazyImporter(
3838
[
@@ -42,9 +42,6 @@
4242
]
4343
)
4444

45-
# pyenv folder name for pypy
46-
PYPY_VER_RE = r"^pypy(?P<pyversion>\d{1,2}\.\d+)-(?P<pypyversion>[\d\.]*)$"
47-
4845

4946
def get_pyenv_root() -> str | None:
5047
# Check if the environment variable exists, if so use that
@@ -77,7 +74,6 @@ def get_pyenv_root() -> str | None:
7774
def get_pyenv_pythons(
7875
versions_folder: str | os.PathLike | None = None,
7976
*,
80-
query_executables: bool = True,
8177
finder: DetailFinder = None,
8278
) -> Iterator[PythonInstall]:
8379
if versions_folder is None:
@@ -96,27 +92,7 @@ def get_pyenv_pythons(
9692
with finder:
9793
for p in sorted(os.scandir(versions_folder), key=lambda x: x.path):
9894
executable = os.path.join(p.path, "bin/python")
99-
10095
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-
):
96+
install = finder.get_install_details(executable, managed_by="pyenv")
97+
if install:
12298
yield install

src/ducktools/pythonfinder/shared.py

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,18 @@ def clear_cache(self):
225225
self._raw_cache = {}
226226
self._dirty_cache = True
227227

228-
def query_install(self, exe_path: str, managed_by: str | None = None) -> PythonInstall | None:
228+
def query_install(
229+
self,
230+
exe_path: str,
231+
managed_by: str | None = None,
232+
metadata: dict | None = None,
233+
) -> PythonInstall | None:
229234
"""
230235
Query the details of a Python install directly
231236
232237
:param exe_path: Path to the runtime .exe
233238
:param managed_by: Which tool manages this install (if any)
239+
:param metadata: Dictionary of install metadata
234240
:return: a PythonInstall if one exists at the exe Path
235241
"""
236242
try:
@@ -273,9 +279,19 @@ def query_install(self, exe_path: str, managed_by: str | None = None) -> PythonI
273279
except _laz.json.JSONDecodeError:
274280
return None
275281

276-
return PythonInstall.from_json(**output, managed_by=managed_by)
282+
if metadata:
283+
output["metadata"].update(metadata)
284+
285+
install = PythonInstall.from_json(**output, managed_by=managed_by)
277286

278-
def get_install_details(self, exe_path: str, managed_by=None) -> PythonInstall | None:
287+
return install
288+
289+
def get_install_details(
290+
self,
291+
exe_path: str,
292+
managed_by: str | None = None,
293+
metadata: dict | None = None,
294+
) -> PythonInstall | None:
279295
exe_path = os.path.abspath(exe_path)
280296
mtime = os.stat(exe_path).st_mtime
281297

@@ -287,7 +303,7 @@ def get_install_details(self, exe_path: str, managed_by=None) -> PythonInstall |
287303
self.raw_cache.pop(exe_path)
288304

289305
if install is None:
290-
install = self.query_install(exe_path, managed_by)
306+
install = self.query_install(exe_path, managed_by, metadata)
291307
if install:
292308
self.raw_cache[exe_path] = {
293309
"mtime": mtime,
@@ -416,6 +432,7 @@ def get_folder_pythons(
416432
base_folder: str | os.PathLike,
417433
basenames: tuple[str] = ("python", "pypy"),
418434
finder: DetailFinder | None = None,
435+
managed_by: str | None = None,
419436
):
420437
regexes = [_python_exe_regex(name) for name in basenames]
421438

@@ -440,7 +457,7 @@ def get_folder_pythons(
440457
continue
441458
else:
442459
p = file_path.path
443-
install = finder.get_install_details(p)
460+
install = finder.get_install_details(p, managed_by=managed_by)
444461
if install:
445462
yield install
446463

@@ -483,7 +500,6 @@ def get_uv_python_path() -> str | None:
483500

484501
def _implementation_from_uv_dir(
485502
direntry: os.DirEntry,
486-
query_executables: bool = True,
487503
finder: DetailFinder | None = None,
488504
) -> PythonInstall | None:
489505
python_exe = "python.exe" if sys.platform == "win32" else "bin/python"
@@ -493,35 +509,12 @@ def _implementation_from_uv_dir(
493509
finder = DetailFinder() if finder is None else finder
494510

495511
if os.path.exists(python_path):
496-
if match := _laz.re.fullmatch(UV_PYTHON_RE, direntry.name):
497-
implementation, version, extra, platform, arch = match.groups()
498-
metadata = {
499-
"freethreaded": "freethreaded" in extra,
500-
}
501-
502-
try:
503-
if implementation in {"cpython"}:
504-
install = PythonInstall.from_str(
505-
version=version,
506-
executable=python_path,
507-
architecture="32bit" if arch in {"i686", "armv7"} else "64bit",
508-
implementation=implementation,
509-
metadata=metadata,
510-
managed_by="Astral",
511-
)
512-
except ValueError:
513-
pass
514-
515-
if install is None:
516-
# Directory name format has changed or this is an alternate implementation
517-
# Slow backup - ask python itself
518-
if query_executables:
519-
install = finder.get_install_details(python_path, managed_by="Astral")
512+
install = finder.get_install_details(python_path, managed_by="Astral")
520513

521514
return install
522515

523516

524-
def get_uv_pythons(query_executables=True, finder=None) -> Iterator[PythonInstall]:
517+
def get_uv_pythons(finder=None) -> Iterator[PythonInstall]:
525518
# This takes some shortcuts over the regular pythonfinder
526519
# As the UV folders give the python version and the implementation
527520
if uv_python_path := get_uv_python_path():
@@ -532,6 +525,6 @@ def get_uv_pythons(query_executables=True, finder=None) -> Iterator[PythonInstal
532525
for f in fld:
533526
if (
534527
f.is_dir()
535-
and (install := _implementation_from_uv_dir(f, query_executables, finder=finder))
528+
and (install := _implementation_from_uv_dir(f, finder=finder))
536529
):
537530
yield install

src/ducktools/pythonfinder/win32/__init__.py

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

3333
def get_python_installs(
3434
*,
35-
query_executables: bool = True,
3635
finder: DetailFinder | None = None
3736
) -> Iterator[PythonInstall]:
3837
listed_installs = set()
@@ -42,8 +41,8 @@ def get_python_installs(
4241
with finder:
4342
for py in itertools.chain(
4443
get_registered_pythons(),
45-
get_pyenv_pythons(query_executables=query_executables, finder=finder),
46-
get_uv_pythons(query_executables=query_executables, finder=finder),
44+
get_pyenv_pythons(finder=finder),
45+
get_uv_pythons(finder=finder),
4746
):
4847
if py.executable not in listed_installs:
4948
yield py

src/ducktools/pythonfinder/win32/pyenv_search.py

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def get_pyenv_root() -> str | None:
3939
def get_pyenv_pythons(
4040
versions_folder: str | os.PathLike | None = None,
4141
*,
42-
query_executables: bool = True,
4342
finder: DetailFinder | None = None,
4443
) -> Iterator[PythonInstall]:
4544

@@ -56,51 +55,14 @@ def get_pyenv_pythons(
5655
for p in os.scandir(versions_folder):
5756
path_base = os.path.basename(p.path)
5857

59-
if query_executables:
60-
# Check for pypy/graalpy
61-
if path_base.startswith("pypy"):
62-
executable = os.path.join(p.path, "pypy.exe")
63-
if os.path.exists(executable):
64-
yield finder.get_install_details(executable, managed_by="pyenv")
65-
continue
66-
elif path_base.startswith("graalpy"):
67-
# Graalpy exe in bin subfolder
68-
executable = os.path.join(p.path, "bin", "graalpy.exe")
69-
if os.path.exists(executable):
70-
yield finder.get_install_details(executable, managed_by="pyenv")
71-
continue
72-
73-
# Regular CPython
74-
executable = os.path.join(p.path, "python.exe")
58+
if path_base.startswith("pypy"):
59+
executable = os.path.join(p.path, "pypy.exe")
60+
elif path_base.startswith("graalpy"):
61+
# Graalpy exe in bin subfolder
62+
executable = os.path.join(p.path, "bin", "graalpy.exe")
63+
else:
64+
# Try python.exe
65+
executable = os.path.join(p.path, "python.exe")
7566

7667
if os.path.exists(executable):
77-
split_version = p.name.split("-")
78-
79-
# If there are 1 or 2 arguments this is a recognised version
80-
# Otherwise it is unrecognised
81-
if len(split_version) == 2:
82-
version, arch = split_version
83-
84-
# win32 in pyenv name means 32 bit python install
85-
# 'arm' is the only alternative which will be 64bit
86-
arch = "32bit" if arch == "win32" else "64bit"
87-
try:
88-
yield PythonInstall.from_str(
89-
version=version,
90-
executable=executable,
91-
architecture=arch,
92-
managed_by="pyenv",
93-
)
94-
except ValueError:
95-
pass
96-
elif len(split_version) == 1:
97-
version = split_version[0]
98-
try:
99-
yield PythonInstall.from_str(
100-
version=version,
101-
executable=executable,
102-
architecture="64bit",
103-
managed_by="pyenv",
104-
)
105-
except ValueError:
106-
pass
68+
yield finder.get_install_details(executable, managed_by="pyenv")

0 commit comments

Comments
 (0)