ducktools-pythonfinder 0.7.7__tar.gz → 0.8.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.7.7 → ducktools_pythonfinder-0.8.0}/.gitignore +1 -0
- {ducktools_pythonfinder-0.7.7/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.8.0}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/__init__.py +4 -3
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/__main__.py +9 -1
- ducktools_pythonfinder-0.8.0/src/ducktools/pythonfinder/_version.py +2 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/details_script.py +5 -3
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/linux/__init__.py +28 -13
- ducktools_pythonfinder-0.8.0/src/ducktools/pythonfinder/linux/pyenv_search.py +122 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/shared.py +214 -60
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/venv.py +10 -3
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/win32/__init__.py +18 -10
- ducktools_pythonfinder-0.8.0/src/ducktools/pythonfinder/win32/pyenv_search.py +106 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +1 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/conftest.py +16 -7
- ducktools_pythonfinder-0.8.0/tests/test_cache.py +227 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_foldersearch.py +11 -9
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_pyenv.py +48 -23
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_uv_finder.py +12 -6
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_venv_finder.py +2 -2
- ducktools_pythonfinder-0.7.7/src/ducktools/pythonfinder/_version.py +0 -2
- ducktools_pythonfinder-0.7.7/src/ducktools/pythonfinder/linux/pyenv_search.py +0 -84
- ducktools_pythonfinder-0.7.7/src/ducktools/pythonfinder/win32/pyenv_search.py +0 -93
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/LICENSE +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/README.md +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/pyproject.toml +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/scripts/list_python_venvs.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/package_list_script.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/win32/registry_search.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/sources/release.json +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/sources/release_file.json +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_orgsearch.py +0 -0
- {ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/tests/test_shared.py +0 -0
{ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/__init__.py
RENAMED
|
@@ -32,7 +32,7 @@ __all__ = [
|
|
|
32
32
|
|
|
33
33
|
import sys
|
|
34
34
|
from ._version import __version__
|
|
35
|
-
from .shared import PythonInstall
|
|
35
|
+
from .shared import PythonInstall, DetailFinder
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
if sys.platform == "win32":
|
|
@@ -43,9 +43,10 @@ else:
|
|
|
43
43
|
from .linux import get_python_installs
|
|
44
44
|
|
|
45
45
|
|
|
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
|
|
47
48
|
return sorted(
|
|
48
|
-
get_python_installs(query_executables=query_executables),
|
|
49
|
+
get_python_installs(query_executables=query_executables, finder=finder),
|
|
49
50
|
reverse=True,
|
|
50
51
|
key=lambda x: (x.version[3], *x.version[:3], x.version[4])
|
|
51
52
|
)
|
{ducktools_pythonfinder-0.7.7 → ducktools_pythonfinder-0.8.0}/src/ducktools/pythonfinder/__main__.py
RENAMED
|
@@ -28,6 +28,7 @@ import os
|
|
|
28
28
|
|
|
29
29
|
from ducktools.lazyimporter import LazyImporter, ModuleImport, FromImport
|
|
30
30
|
from ducktools.pythonfinder import list_python_installs, __version__
|
|
31
|
+
from ducktools.pythonfinder.shared import purge_caches
|
|
31
32
|
|
|
32
33
|
_laz = LazyImporter(
|
|
33
34
|
[
|
|
@@ -122,6 +123,11 @@ def get_parser():
|
|
|
122
123
|
|
|
123
124
|
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
124
125
|
|
|
126
|
+
clear_cache = subparsers.add_parser(
|
|
127
|
+
"clear-cache",
|
|
128
|
+
help="Clear the cache of Python install details"
|
|
129
|
+
)
|
|
130
|
+
|
|
125
131
|
online = subparsers.add_parser(
|
|
126
132
|
"online",
|
|
127
133
|
help="Get links to binaries from python.org"
|
|
@@ -305,7 +311,9 @@ def main():
|
|
|
305
311
|
parser = get_parser()
|
|
306
312
|
vals = parser.parse_args(sys.argv[1:])
|
|
307
313
|
|
|
308
|
-
if vals.command == "
|
|
314
|
+
if vals.command == "clear-cache":
|
|
315
|
+
purge_caches()
|
|
316
|
+
elif vals.command == "online":
|
|
309
317
|
system = vals.system if vals.system else _laz.platform.system()
|
|
310
318
|
machine = vals.machine if vals.machine else _laz.platform.machine()
|
|
311
319
|
try:
|
|
@@ -29,11 +29,13 @@ import sys
|
|
|
29
29
|
FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>a|b|c|rc)?(?P<serial>\d*)?"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def version_str_to_tuple(version):
|
|
32
|
+
def version_str_to_tuple(version: str):
|
|
33
33
|
# Needed to parse GraalPy versions only available as strings
|
|
34
34
|
import re
|
|
35
35
|
|
|
36
36
|
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}")
|
|
37
39
|
|
|
38
40
|
major, minor, micro, releaselevel, serial = parsed_version.groups()
|
|
39
41
|
|
|
@@ -70,11 +72,11 @@ def get_details():
|
|
|
70
72
|
# Special case GraalPy as it erroneously reports the CPython target
|
|
71
73
|
# instead of the Graal versiion
|
|
72
74
|
try:
|
|
73
|
-
ver = __graalpython__.get_graalvm_version()
|
|
75
|
+
ver = __graalpython__.get_graalvm_version() # type: ignore
|
|
74
76
|
metadata = {
|
|
75
77
|
"graalpy_version": version_str_to_tuple(ver)
|
|
76
78
|
}
|
|
77
|
-
except NameError:
|
|
79
|
+
except (NameError, ValueError):
|
|
78
80
|
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
79
81
|
elif implementation != "cpython": # pragma: no cover
|
|
80
82
|
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
@@ -27,19 +27,27 @@ import os.path
|
|
|
27
27
|
import itertools
|
|
28
28
|
from _collections_abc import Iterator
|
|
29
29
|
|
|
30
|
-
from ..shared import
|
|
31
|
-
|
|
30
|
+
from ..shared import (
|
|
31
|
+
DetailFinder,
|
|
32
|
+
PythonInstall,
|
|
33
|
+
get_folder_pythons,
|
|
34
|
+
get_uv_pythons,
|
|
35
|
+
get_uv_python_path
|
|
36
|
+
)
|
|
37
|
+
from .pyenv_search import get_pyenv_pythons, get_pyenv_root
|
|
32
38
|
|
|
33
39
|
|
|
34
|
-
def get_path_pythons() -> Iterator[PythonInstall]:
|
|
40
|
+
def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonInstall]:
|
|
35
41
|
exe_names = set()
|
|
36
42
|
|
|
37
43
|
path_folders = os.environ.get("PATH", "").split(":")
|
|
38
|
-
pyenv_root =
|
|
44
|
+
pyenv_root = get_pyenv_root()
|
|
39
45
|
uv_root = get_uv_python_path()
|
|
40
46
|
|
|
41
47
|
excluded_folders = [pyenv_root, uv_root]
|
|
42
48
|
|
|
49
|
+
finder = DetailFinder if finder is None else finder
|
|
50
|
+
|
|
43
51
|
for fld in path_folders:
|
|
44
52
|
# Don't retrieve pyenv installs
|
|
45
53
|
skip_folder = False
|
|
@@ -54,7 +62,7 @@ def get_path_pythons() -> Iterator[PythonInstall]:
|
|
|
54
62
|
if not os.path.exists(fld):
|
|
55
63
|
continue
|
|
56
64
|
|
|
57
|
-
for install in get_folder_pythons(fld):
|
|
65
|
+
for install in get_folder_pythons(fld, finder=finder):
|
|
58
66
|
name = os.path.basename(install.executable)
|
|
59
67
|
if name in exe_names:
|
|
60
68
|
install.shadowed = True
|
|
@@ -63,17 +71,24 @@ def get_path_pythons() -> Iterator[PythonInstall]:
|
|
|
63
71
|
yield install
|
|
64
72
|
|
|
65
73
|
|
|
66
|
-
def get_python_installs(
|
|
74
|
+
def get_python_installs(
|
|
75
|
+
*,
|
|
76
|
+
query_executables: bool = True,
|
|
77
|
+
finder: DetailFinder | None = None,
|
|
78
|
+
) -> Iterator[PythonInstall]:
|
|
67
79
|
listed_pythons = set()
|
|
68
80
|
|
|
81
|
+
finder = DetailFinder() if finder is None else finder
|
|
82
|
+
|
|
69
83
|
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),
|
|
72
86
|
]
|
|
73
87
|
if query_executables:
|
|
74
|
-
chain_commands.append(get_path_pythons())
|
|
88
|
+
chain_commands.append(get_path_pythons(finder=finder))
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# MIT License
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2023-2025 David C Ellis
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
# furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
# copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
# SOFTWARE.
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
Discover python installs that have been created with pyenv
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import os.path
|
|
31
|
+
from _collections_abc import Iterator
|
|
32
|
+
|
|
33
|
+
from ducktools.lazyimporter import LazyImporter, FromImport, ModuleImport
|
|
34
|
+
|
|
35
|
+
from ..shared import PythonInstall, DetailFinder, FULL_PY_VER_RE, INSTALLER_CACHE_PATH, version_str_to_tuple
|
|
36
|
+
|
|
37
|
+
_laz = LazyImporter(
|
|
38
|
+
[
|
|
39
|
+
ModuleImport("json"),
|
|
40
|
+
ModuleImport("re"),
|
|
41
|
+
FromImport("subprocess", "run"),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# pyenv folder name for pypy
|
|
46
|
+
PYPY_VER_RE = r"^pypy(?P<pyversion>\d{1,2}\.\d+)-(?P<pypyversion>[\d\.]*)$"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_pyenv_root() -> str | None:
|
|
50
|
+
# Check if the environment variable exists, if so use that
|
|
51
|
+
# As a backup try to run pyenv to obtain the root folder
|
|
52
|
+
pyenv_root = os.environ.get("PYENV_ROOT")
|
|
53
|
+
if not pyenv_root:
|
|
54
|
+
try:
|
|
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()
|
|
68
|
+
|
|
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)
|
|
73
|
+
|
|
74
|
+
return pyenv_root
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_pyenv_pythons(
|
|
78
|
+
versions_folder: str | os.PathLike | None = None,
|
|
79
|
+
*,
|
|
80
|
+
query_executables: bool = True,
|
|
81
|
+
finder: DetailFinder = None,
|
|
82
|
+
) -> Iterator[PythonInstall]:
|
|
83
|
+
if versions_folder is None:
|
|
84
|
+
if pyenv_root := get_pyenv_root():
|
|
85
|
+
versions_folder = os.path.join(pyenv_root, "versions")
|
|
86
|
+
|
|
87
|
+
if versions_folder is None or not os.path.exists(versions_folder):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Sorting puts standard python versions before alternate implementations
|
|
91
|
+
# This can lead to much faster returns by potentially yielding
|
|
92
|
+
# the required python version before checking pypy/graalpy/micropython
|
|
93
|
+
|
|
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
|