ducktools-pythonfinder 0.8.6__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ducktools_pythonfinder-0.8.6/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.9.0}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/__main__.py +3 -2
- ducktools_pythonfinder-0.9.0/src/ducktools/pythonfinder/_version.py +2 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/details_script.py +17 -3
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/linux/__init__.py +12 -1
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/pythonorg_search.py +19 -13
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/shared.py +18 -4
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
- ducktools_pythonfinder-0.9.0/tests/sources/release.json +1 -0
- ducktools_pythonfinder-0.9.0/tests/sources/release_file.json +1 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_orgsearch.py +33 -30
- ducktools_pythonfinder-0.8.6/src/ducktools/pythonfinder/_version.py +0 -2
- ducktools_pythonfinder-0.8.6/tests/sources/release.json +0 -1
- ducktools_pythonfinder-0.8.6/tests/sources/release_file.json +0 -1
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/.gitignore +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/LICENSE +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/README.md +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/pyproject.toml +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/scripts/list_python_venvs.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/__init__.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/linux/pyenv_search.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/package_list_script.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/venv.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/win32/__init__.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/win32/pyenv_search.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/win32/registry_search.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/conftest.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_cache.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_foldersearch.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_pyenv.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_shared.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_uv_finder.py +0 -0
- {ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/tests/test_venv_finder.py +0 -0
{ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/__main__.py
RENAMED
|
@@ -35,6 +35,7 @@ _laz = LazyImporter(
|
|
|
35
35
|
ModuleImport("argparse"),
|
|
36
36
|
ModuleImport("csv"),
|
|
37
37
|
ModuleImport("subprocess"),
|
|
38
|
+
ModuleImport("sysconfig"),
|
|
38
39
|
ModuleImport("platform"),
|
|
39
40
|
FromImport(".pythonorg_search", "PythonOrgSearch"),
|
|
40
41
|
FromImport("packaging.specifiers", "SpecifierSet"),
|
|
@@ -207,7 +208,7 @@ def display_local_installs(
|
|
|
207
208
|
version_str = f"*{version_str}"
|
|
208
209
|
elif (
|
|
209
210
|
sys.prefix != sys.base_prefix
|
|
210
|
-
and
|
|
211
|
+
and install.paths.get("stdlib") == _laz.sysconfig.get_path("stdlib")
|
|
211
212
|
):
|
|
212
213
|
version_str = f"**{version_str}"
|
|
213
214
|
|
|
@@ -240,7 +241,7 @@ def display_local_installs(
|
|
|
240
241
|
if sys.platform != "win32":
|
|
241
242
|
print("[] - This Python install is shadowed by another on Path")
|
|
242
243
|
print("* - This is the active Python executable used to call this module")
|
|
243
|
-
print("** - This is
|
|
244
|
+
print("** - This is a parent Python executable of the venv used to call this module")
|
|
244
245
|
print()
|
|
245
246
|
|
|
246
247
|
headings_str = f"| {headings[0]:<{max_version_len}s} | {headings[1]:<{max_executable_len}s} |"
|
|
@@ -46,7 +46,7 @@ def version_str_to_tuple(version):
|
|
|
46
46
|
releaselevel = "alpha"
|
|
47
47
|
elif releaselevel == "b":
|
|
48
48
|
releaselevel = "beta"
|
|
49
|
-
elif releaselevel
|
|
49
|
+
elif releaselevel in {"c", "rc"}:
|
|
50
50
|
releaselevel = "candidate"
|
|
51
51
|
else:
|
|
52
52
|
releaselevel = "final"
|
|
@@ -80,9 +80,14 @@ def get_details():
|
|
|
80
80
|
"graalpy_version": version_str_to_tuple(ver)
|
|
81
81
|
}
|
|
82
82
|
except (NameError, ValueError):
|
|
83
|
-
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
83
|
+
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
84
84
|
elif implementation != "cpython": # pragma: no cover
|
|
85
|
-
|
|
85
|
+
if implementation == "micropython":
|
|
86
|
+
imp_ver = sys.implementation.version[:3]
|
|
87
|
+
else:
|
|
88
|
+
imp_ver = sys.implementation.version
|
|
89
|
+
|
|
90
|
+
metadata = {"{}_version".format(implementation): imp_ver}
|
|
86
91
|
else:
|
|
87
92
|
metadata = {}
|
|
88
93
|
if sys.version_info >= (3, 13):
|
|
@@ -90,12 +95,21 @@ def get_details():
|
|
|
90
95
|
freethreaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
|
|
91
96
|
metadata["freethreaded"] = freethreaded
|
|
92
97
|
|
|
98
|
+
# Attempt to get the paths to stdlib etc from sysconfig
|
|
99
|
+
try:
|
|
100
|
+
import sysconfig # Exists in 2.7 and 3.2 onwards, missing in earlier and micropython
|
|
101
|
+
except ImportError:
|
|
102
|
+
paths = {}
|
|
103
|
+
else:
|
|
104
|
+
paths = sysconfig.get_paths()
|
|
105
|
+
|
|
93
106
|
install = dict(
|
|
94
107
|
version=list(sys.version_info),
|
|
95
108
|
executable=sys.executable,
|
|
96
109
|
architecture="64bit" if (sys.maxsize > 2**32) else "32bit",
|
|
97
110
|
implementation=implementation,
|
|
98
111
|
metadata=metadata,
|
|
112
|
+
paths=paths,
|
|
99
113
|
)
|
|
100
114
|
|
|
101
115
|
return install
|
|
@@ -37,6 +37,14 @@ from ..shared import (
|
|
|
37
37
|
from .pyenv_search import get_pyenv_pythons, get_pyenv_root
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
KNOWN_MANAGED_PATHS = {
|
|
41
|
+
"/usr/bin": "OS",
|
|
42
|
+
"/bin": "OS",
|
|
43
|
+
"/usr/sbin": "OS",
|
|
44
|
+
"/sbin": "OS",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
40
48
|
def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonInstall]:
|
|
41
49
|
exe_names = set()
|
|
42
50
|
|
|
@@ -46,7 +54,7 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
|
|
|
46
54
|
|
|
47
55
|
excluded_folders = [pyenv_root, uv_root]
|
|
48
56
|
|
|
49
|
-
finder = DetailFinder if finder is None else finder
|
|
57
|
+
finder = DetailFinder() if finder is None else finder
|
|
50
58
|
|
|
51
59
|
for fld in path_folders:
|
|
52
60
|
# Don't retrieve pyenv installs
|
|
@@ -63,6 +71,9 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
|
|
|
63
71
|
continue
|
|
64
72
|
|
|
65
73
|
for install in get_folder_pythons(fld, finder=finder):
|
|
74
|
+
if manager := KNOWN_MANAGED_PATHS.get(os.path.dirname(install.executable)):
|
|
75
|
+
install.managed_by = manager
|
|
76
|
+
|
|
66
77
|
name = os.path.basename(install.executable)
|
|
67
78
|
if name in exe_names:
|
|
68
79
|
install.shadowed = True
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# ducktools-pythonfinder
|
|
2
2
|
# MIT License
|
|
3
|
-
#
|
|
3
|
+
#
|
|
4
4
|
# Copyright (c) 2023-2025 David C Ellis
|
|
5
|
-
#
|
|
5
|
+
#
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to deal
|
|
8
8
|
# in the Software without restriction, including without limitation the rights
|
|
9
9
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
10
|
# copies of the Software, and to permit persons to whom the Software is
|
|
11
11
|
# furnished to do so, subject to the following conditions:
|
|
12
|
-
#
|
|
12
|
+
#
|
|
13
13
|
# The above copyright notice and this permission notice shall be included in all
|
|
14
14
|
# copies or substantial portions of the Software.
|
|
15
|
-
#
|
|
15
|
+
#
|
|
16
16
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
17
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
18
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
@@ -31,6 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
import json
|
|
33
33
|
import platform
|
|
34
|
+
import re
|
|
34
35
|
|
|
35
36
|
from urllib.request import urlopen
|
|
36
37
|
|
|
@@ -44,6 +45,8 @@ from ducktools.pythonfinder.shared import version_str_to_tuple
|
|
|
44
45
|
RELEASE_PAGE = "https://www.python.org/api/v2/downloads/release/"
|
|
45
46
|
RELEASE_FILE_PAGE = "https://www.python.org/api/v2/downloads/release_file/"
|
|
46
47
|
|
|
48
|
+
PYTHON_RELEASE_RE = re.compile(R"Python \d+\.\d+\.\d+(?:(?:a|b|rc)\d)?")
|
|
49
|
+
|
|
47
50
|
|
|
48
51
|
class UnsupportedError(Exception):
|
|
49
52
|
pass
|
|
@@ -206,13 +209,16 @@ class PythonOrgSearch(Prefab):
|
|
|
206
209
|
def releases(self) -> list[PythonRelease]:
|
|
207
210
|
"""Get all releases from python.org/api/v2/downloads/release"""
|
|
208
211
|
if self._releases is None:
|
|
209
|
-
if self.release_page_cache:
|
|
210
|
-
data = json.loads(self.release_page_cache)
|
|
211
|
-
else: # pragma: nocover
|
|
212
|
+
if not self.release_page_cache:
|
|
212
213
|
with urlopen(self.release_page) as req:
|
|
213
|
-
|
|
214
|
+
self.release_page_cache = req.read().decode("utf8")
|
|
215
|
+
|
|
216
|
+
data = json.loads(self.release_page_cache)
|
|
214
217
|
|
|
215
|
-
self._releases = [
|
|
218
|
+
self._releases = [
|
|
219
|
+
PythonRelease.from_dict(release) for release in data
|
|
220
|
+
if PYTHON_RELEASE_RE.fullmatch(release["name"])
|
|
221
|
+
]
|
|
216
222
|
self._releases.sort(key=lambda ver: ver.version_tuple, reverse=True)
|
|
217
223
|
|
|
218
224
|
return self._releases
|
|
@@ -221,11 +227,11 @@ class PythonOrgSearch(Prefab):
|
|
|
221
227
|
def release_files(self) -> list[PythonReleaseFile]:
|
|
222
228
|
"""Get all release files from python.org/api/v2/downloads/release"""
|
|
223
229
|
if self._release_files is None:
|
|
224
|
-
if self.release_file_page_cache:
|
|
225
|
-
data = json.loads(self.release_file_page_cache)
|
|
226
|
-
else: # pragma: nocover
|
|
230
|
+
if not self.release_file_page_cache:
|
|
227
231
|
with urlopen(self.release_file_page) as req:
|
|
228
|
-
|
|
232
|
+
self.release_file_page_cache = req.read().decode("utf8")
|
|
233
|
+
|
|
234
|
+
data = json.loads(self.release_file_page_cache)
|
|
229
235
|
|
|
230
236
|
self._release_files = [PythonReleaseFile.from_dict(relfile) for relfile in data]
|
|
231
237
|
return self._release_files
|
{ducktools_pythonfinder-0.8.6 → ducktools_pythonfinder-0.9.0}/src/ducktools/pythonfinder/shared.py
RENAMED
|
@@ -80,7 +80,7 @@ else:
|
|
|
80
80
|
CACHE_FOLDER = os.path.join(USER_FOLDER, ".cache", "ducktools", "pythonfinder")
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
CACHE_VERSION =
|
|
83
|
+
CACHE_VERSION = 2
|
|
84
84
|
DETAILS_CACHE_PATH = os.path.join(CACHE_FOLDER, f"runtime_cache_v{CACHE_VERSION}.json")
|
|
85
85
|
INSTALLER_CACHE_PATH = os.path.join(CACHE_FOLDER, "installer_details.json")
|
|
86
86
|
|
|
@@ -321,6 +321,7 @@ class PythonInstall(Prefab):
|
|
|
321
321
|
implementation: str = "cpython"
|
|
322
322
|
managed_by: str | None = None
|
|
323
323
|
metadata: dict = attribute(default_factory=dict)
|
|
324
|
+
paths: dict[str, str] = attribute(default_factory=dict)
|
|
324
325
|
shadowed: bool = attribute(default=False, serialize=False)
|
|
325
326
|
_implementation_version: tuple[int, int, int, str, int] | None = attribute(default=None, private=True)
|
|
326
327
|
|
|
@@ -365,9 +366,13 @@ class PythonInstall(Prefab):
|
|
|
365
366
|
implementation: str = "cpython",
|
|
366
367
|
managed_by: str | None = None,
|
|
367
368
|
metadata: dict | None = None,
|
|
369
|
+
paths: dict | None = None,
|
|
368
370
|
):
|
|
369
371
|
version_tuple = version_str_to_tuple(version)
|
|
370
372
|
|
|
373
|
+
metadata = {} if metadata is None else metadata
|
|
374
|
+
paths = {} if paths is None else paths
|
|
375
|
+
|
|
371
376
|
# noinspection PyArgumentList
|
|
372
377
|
return cls(
|
|
373
378
|
version=version_tuple,
|
|
@@ -376,6 +381,7 @@ class PythonInstall(Prefab):
|
|
|
376
381
|
implementation=implementation,
|
|
377
382
|
managed_by=managed_by,
|
|
378
383
|
metadata=metadata,
|
|
384
|
+
paths=paths,
|
|
379
385
|
)
|
|
380
386
|
|
|
381
387
|
@classmethod
|
|
@@ -386,11 +392,14 @@ class PythonInstall(Prefab):
|
|
|
386
392
|
architecture,
|
|
387
393
|
implementation,
|
|
388
394
|
metadata,
|
|
395
|
+
paths=None,
|
|
389
396
|
managed_by=None,
|
|
390
397
|
):
|
|
391
398
|
if arch_ver := metadata.get(f"{implementation}_version"):
|
|
392
399
|
metadata[f"{implementation}_version"] = tuple(arch_ver)
|
|
393
400
|
|
|
401
|
+
paths = {} if paths is None else paths
|
|
402
|
+
|
|
394
403
|
# noinspection PyArgumentList
|
|
395
404
|
return cls(
|
|
396
405
|
version=tuple(version), # type: ignore
|
|
@@ -399,6 +408,7 @@ class PythonInstall(Prefab):
|
|
|
399
408
|
implementation=implementation,
|
|
400
409
|
managed_by=managed_by,
|
|
401
410
|
metadata=metadata,
|
|
411
|
+
paths=paths,
|
|
402
412
|
)
|
|
403
413
|
|
|
404
414
|
def get_pip_version(self) -> str | None:
|
|
@@ -430,7 +440,7 @@ def _python_exe_regex(basename: str = "python"):
|
|
|
430
440
|
|
|
431
441
|
def get_folder_pythons(
|
|
432
442
|
base_folder: str | os.PathLike,
|
|
433
|
-
basenames: tuple[str] = ("python", "pypy"),
|
|
443
|
+
basenames: tuple[str, ...] = ("python", "pypy", "micropython"),
|
|
434
444
|
finder: DetailFinder | None = None,
|
|
435
445
|
managed_by: str | None = None,
|
|
436
446
|
):
|
|
@@ -438,6 +448,8 @@ def get_folder_pythons(
|
|
|
438
448
|
|
|
439
449
|
finder = DetailFinder() if finder is None else finder
|
|
440
450
|
|
|
451
|
+
base_folder = str(base_folder)
|
|
452
|
+
|
|
441
453
|
with finder, os.scandir(base_folder) as fld:
|
|
442
454
|
for file_path in fld:
|
|
443
455
|
try:
|
|
@@ -452,11 +464,13 @@ def get_folder_pythons(
|
|
|
452
464
|
p = file_path.path
|
|
453
465
|
if file_path.is_symlink():
|
|
454
466
|
# Might be a venv - look for pyvenv.cfg in parent
|
|
455
|
-
|
|
456
|
-
|
|
467
|
+
dirname = os.path.dirname(p)
|
|
468
|
+
|
|
469
|
+
if os.path.exists(os.path.join(dirname, "../pyvenv.cfg")):
|
|
457
470
|
continue
|
|
458
471
|
else:
|
|
459
472
|
p = file_path.path
|
|
473
|
+
|
|
460
474
|
install = finder.get_install_details(p, managed_by=managed_by)
|
|
461
475
|
if install:
|
|
462
476
|
yield install
|