ducktools-pythonfinder 0.5.3__tar.gz → 0.5.5__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.5.3/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.5.5}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/__init__.py +3 -2
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/__main__.py +64 -27
- ducktools_pythonfinder-0.5.5/src/ducktools/pythonfinder/_version.py +2 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/details_script.py +40 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/linux/__init__.py +9 -6
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/linux/pyenv_search.py +6 -11
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/shared.py +77 -35
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/win32/__init__.py +3 -3
- ducktools_pythonfinder-0.5.5/src/ducktools/pythonfinder/win32/pyenv_search.py +87 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/conftest.py +7 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/test_foldersearch.py +1 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/test_pyenv.py +13 -8
- ducktools_pythonfinder-0.5.3/src/ducktools/pythonfinder/_version.py +0 -2
- ducktools_pythonfinder-0.5.3/src/ducktools/pythonfinder/win32/pyenv_search.py +0 -81
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/.gitignore +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/LICENSE +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/README.md +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/pyproject.toml +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/win32/registry_search.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/sources/release.json +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/sources/release_file.json +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/test_orgsearch.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/test_shared.py +0 -0
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/__init__.py
RENAMED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
# Find platform python versions
|
|
25
25
|
|
|
26
26
|
__all__ = [
|
|
27
|
+
"__version__",
|
|
27
28
|
"get_python_installs",
|
|
28
29
|
"list_python_installs",
|
|
29
30
|
"PythonInstall",
|
|
@@ -42,9 +43,9 @@ else:
|
|
|
42
43
|
from .linux import get_python_installs
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
def list_python_installs():
|
|
46
|
+
def list_python_installs(*, query_executables=True):
|
|
46
47
|
return sorted(
|
|
47
|
-
get_python_installs(),
|
|
48
|
+
get_python_installs(query_executables=query_executables),
|
|
48
49
|
reverse=True,
|
|
49
50
|
key=lambda x: (x.version[3], *x.version[:3], x.version[4])
|
|
50
51
|
)
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/__main__.py
RENAMED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
21
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
# SOFTWARE.
|
|
23
|
+
from __future__ import annotations
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
import sys
|
|
@@ -79,6 +80,11 @@ def parse_args(args):
|
|
|
79
80
|
description="Discover base Python installs",
|
|
80
81
|
)
|
|
81
82
|
parser.add_argument("-V", "--version", action="version", version=__version__)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--fast",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Skip Python installs that need to be launched to obtain metadata"
|
|
87
|
+
)
|
|
82
88
|
|
|
83
89
|
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
84
90
|
|
|
@@ -121,7 +127,12 @@ def parse_args(args):
|
|
|
121
127
|
return vals
|
|
122
128
|
|
|
123
129
|
|
|
124
|
-
def display_local_installs(
|
|
130
|
+
def display_local_installs(
|
|
131
|
+
min_ver=None,
|
|
132
|
+
max_ver=None,
|
|
133
|
+
compatible=None,
|
|
134
|
+
query_executables=True,
|
|
135
|
+
):
|
|
125
136
|
if min_ver:
|
|
126
137
|
min_ver = tuple(int(i) for i in min_ver.split("."))
|
|
127
138
|
if max_ver:
|
|
@@ -129,24 +140,17 @@ def display_local_installs(min_ver=None, max_ver=None, compatible=None):
|
|
|
129
140
|
if compatible:
|
|
130
141
|
compatible = tuple(int(i) for i in compatible.split("."))
|
|
131
142
|
|
|
132
|
-
installs = list_python_installs()
|
|
133
|
-
headings = ["Python Version", "Executable Location"]
|
|
134
|
-
max_executable_len = max(
|
|
135
|
-
len(headings[1]), max(len(inst.executable) for inst in installs)
|
|
136
|
-
)
|
|
137
|
-
headings_str = f"| {headings[0]} | {headings[1]:<{max_executable_len}s} |"
|
|
143
|
+
installs = list_python_installs(query_executables=query_executables)
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
print(headings_str)
|
|
149
|
-
print(f"| {'-' * len(headings[0])} | {'-' * max_executable_len} |")
|
|
145
|
+
headings = ["Version", "Executable Location"]
|
|
146
|
+
|
|
147
|
+
install_collection: list[tuple[str, str]] = []
|
|
148
|
+
max_version_len = len(headings[0])
|
|
149
|
+
max_executable_len = len(headings[1])
|
|
150
|
+
|
|
151
|
+
alternate_implementations = False
|
|
152
|
+
|
|
153
|
+
# First collect the strings
|
|
150
154
|
for install in installs:
|
|
151
155
|
if min_ver and install.version < min_ver:
|
|
152
156
|
continue
|
|
@@ -180,17 +184,45 @@ def display_local_installs(min_ver=None, max_ver=None, compatible=None):
|
|
|
180
184
|
if install.shadowed:
|
|
181
185
|
version_str = f"[{version_str}]"
|
|
182
186
|
|
|
183
|
-
|
|
187
|
+
if install.implementation != "cpython":
|
|
188
|
+
alternate_implementations = True
|
|
189
|
+
version_str = f"({install.implementation_version_str}) {version_str}"
|
|
190
|
+
|
|
191
|
+
max_version_len = max(max_version_len, len(version_str))
|
|
192
|
+
max_executable_len = max(max_executable_len, len(install.executable))
|
|
193
|
+
|
|
194
|
+
install_collection.append((version_str, install.executable))
|
|
195
|
+
|
|
196
|
+
print("Discoverable Python Installs")
|
|
197
|
+
print()
|
|
198
|
+
if alternate_implementations:
|
|
199
|
+
print("Alternate implementation versions are listed in parentheses")
|
|
200
|
+
|
|
201
|
+
if sys.platform == "win32":
|
|
202
|
+
print("+ - Listed in the Windows Registry ")
|
|
203
|
+
print("^ - This is a 32-bit Python install")
|
|
204
|
+
if sys.platform != "win32":
|
|
205
|
+
print("[] - This Python install is shadowed by another on Path")
|
|
206
|
+
print("* - This is the active Python executable used to call this module")
|
|
207
|
+
print("** - This is the parent Python executable of the venv used to call this module")
|
|
208
|
+
print()
|
|
209
|
+
|
|
210
|
+
headings_str = f"| {headings[0]:<{max_version_len}s} | {headings[1]:<{max_executable_len}s} |"
|
|
211
|
+
print(headings_str)
|
|
212
|
+
print(f"| {'-' * max_version_len} | {'-' * max_executable_len} |")
|
|
213
|
+
|
|
214
|
+
for version_str, executable in install_collection:
|
|
215
|
+
print(f"| {version_str:>{max_version_len}s} | {executable:<{max_executable_len}s} |")
|
|
184
216
|
|
|
185
217
|
|
|
186
218
|
def display_remote_binaries(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
219
|
+
min_ver,
|
|
220
|
+
max_ver,
|
|
221
|
+
compatible,
|
|
222
|
+
all_binaries,
|
|
223
|
+
system,
|
|
224
|
+
machine,
|
|
225
|
+
prerelease
|
|
194
226
|
):
|
|
195
227
|
specs = []
|
|
196
228
|
if min_ver:
|
|
@@ -252,7 +284,12 @@ def main():
|
|
|
252
284
|
except _laz.URLError:
|
|
253
285
|
print("Could not connect to python.org")
|
|
254
286
|
else:
|
|
255
|
-
display_local_installs(
|
|
287
|
+
display_local_installs(
|
|
288
|
+
min_ver=vals.min,
|
|
289
|
+
max_ver=vals.max,
|
|
290
|
+
compatible=vals.compatible,
|
|
291
|
+
query_executables=not vals.fast,
|
|
292
|
+
)
|
|
256
293
|
else:
|
|
257
294
|
# No arguments to parse
|
|
258
295
|
display_local_installs()
|
|
@@ -26,6 +26,35 @@ Get the details from a python install as JSON
|
|
|
26
26
|
"""
|
|
27
27
|
import sys
|
|
28
28
|
|
|
29
|
+
FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>[a-zA-Z]*)(?P<serial>\d*)"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def version_str_to_tuple(version):
|
|
33
|
+
# Needed to parse GraalPy versions only available as strings
|
|
34
|
+
import re
|
|
35
|
+
|
|
36
|
+
parsed_version = re.fullmatch(FULL_PY_VER_RE, version)
|
|
37
|
+
|
|
38
|
+
major, minor, micro, releaselevel, serial = parsed_version.groups()
|
|
39
|
+
|
|
40
|
+
if releaselevel in {"a", "dev"}:
|
|
41
|
+
releaselevel = "alpha"
|
|
42
|
+
elif releaselevel == "b":
|
|
43
|
+
releaselevel = "beta"
|
|
44
|
+
elif releaselevel == "rc":
|
|
45
|
+
releaselevel = "candidate"
|
|
46
|
+
else:
|
|
47
|
+
releaselevel = "final"
|
|
48
|
+
|
|
49
|
+
version_tuple = (
|
|
50
|
+
int(major),
|
|
51
|
+
int(minor),
|
|
52
|
+
int(micro) if micro else 0,
|
|
53
|
+
releaselevel,
|
|
54
|
+
int(serial if serial != "" else 0),
|
|
55
|
+
)
|
|
56
|
+
return version_tuple
|
|
57
|
+
|
|
29
58
|
|
|
30
59
|
def get_details():
|
|
31
60
|
try:
|
|
@@ -37,7 +66,17 @@ def get_details():
|
|
|
37
66
|
implementation = platform.python_implementation().lower()
|
|
38
67
|
metadata = {}
|
|
39
68
|
else:
|
|
40
|
-
if implementation
|
|
69
|
+
if implementation == "graalpy":
|
|
70
|
+
# Special case GraalPy as it erroneously reports the CPython target
|
|
71
|
+
# instead of the Graal versiion
|
|
72
|
+
try:
|
|
73
|
+
ver = __graalpython__.get_graalvm_version()
|
|
74
|
+
metadata = {
|
|
75
|
+
"graalpy_version": version_str_to_tuple(ver)
|
|
76
|
+
}
|
|
77
|
+
except NameError:
|
|
78
|
+
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
79
|
+
elif implementation != "cpython": # pragma: no cover
|
|
41
80
|
metadata = {"{}_version".format(implementation): sys.implementation.version}
|
|
42
81
|
else:
|
|
43
82
|
metadata = {}
|
|
@@ -63,14 +63,17 @@ def get_path_pythons() -> Iterator[PythonInstall]:
|
|
|
63
63
|
yield install
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def get_python_installs():
|
|
66
|
+
def get_python_installs(*, query_executables=True):
|
|
67
67
|
listed_pythons = set()
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
get_pyenv_pythons(),
|
|
71
|
-
get_uv_pythons(),
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
chain_commands = [
|
|
70
|
+
get_pyenv_pythons(query_executables=query_executables),
|
|
71
|
+
get_uv_pythons(query_executables=query_executables),
|
|
72
|
+
]
|
|
73
|
+
if query_executables:
|
|
74
|
+
chain_commands.append(get_path_pythons())
|
|
75
|
+
|
|
76
|
+
for py in itertools.chain.from_iterable(chain_commands):
|
|
74
77
|
if py.executable not in listed_pythons:
|
|
75
78
|
yield py
|
|
76
79
|
listed_pythons.add(py.executable)
|
|
@@ -45,24 +45,20 @@ _laz = LazyImporter(
|
|
|
45
45
|
PYTHON_VER_RE = r"\d{1,2}\.\d{1,2}\.\d+[a-z]*\d*"
|
|
46
46
|
PYPY_VER_RE = r"^pypy(?P<pyversion>\d{1,2}\.\d+)-(?P<pypyversion>[\d\.]*)$"
|
|
47
47
|
|
|
48
|
-
# 'pypy -V' output matcher
|
|
49
|
-
PYPY_V_OUTPUT = (
|
|
50
|
-
r"(?is)python (?P<python_version>\d+\.\d+\.\d+[a-z]*\d*).*?"
|
|
51
|
-
r"pypy (?P<pypy_version>\d+\.\d+\.\d+[a-z]*\d*).*"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
48
|
PYENV_VERSIONS_FOLDER = os.path.join(os.environ.get("PYENV_ROOT", ""), "versions")
|
|
55
49
|
|
|
56
50
|
|
|
57
51
|
def get_pyenv_pythons(
|
|
58
52
|
versions_folder: str | os.PathLike = PYENV_VERSIONS_FOLDER,
|
|
53
|
+
*,
|
|
54
|
+
query_executables: bool = True,
|
|
59
55
|
) -> Iterator[PythonInstall]:
|
|
60
56
|
if not os.path.exists(versions_folder):
|
|
61
57
|
return
|
|
62
58
|
|
|
63
|
-
# Sorting puts standard python versions before
|
|
59
|
+
# Sorting puts standard python versions before alternate implementations
|
|
64
60
|
# This can lead to much faster returns by potentially yielding
|
|
65
|
-
# the required python version before checking pypy
|
|
61
|
+
# the required python version before checking pypy/graalpy/micropython
|
|
66
62
|
|
|
67
63
|
for p in sorted(os.scandir(versions_folder), key=lambda x: x.path):
|
|
68
64
|
executable = os.path.join(p.path, "bin/python")
|
|
@@ -70,6 +66,5 @@ def get_pyenv_pythons(
|
|
|
70
66
|
if os.path.exists(executable):
|
|
71
67
|
if _laz.re.fullmatch(PYTHON_VER_RE, p.name):
|
|
72
68
|
yield PythonInstall.from_str(p.name, executable)
|
|
73
|
-
elif
|
|
74
|
-
|
|
75
|
-
yield install
|
|
69
|
+
elif query_executables and (install := get_install_details(executable)):
|
|
70
|
+
yield install
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/src/ducktools/pythonfinder/shared.py
RENAMED
|
@@ -28,24 +28,25 @@ import os.path
|
|
|
28
28
|
|
|
29
29
|
from _collections_abc import Iterator
|
|
30
30
|
|
|
31
|
-
from ducktools.classbuilder import
|
|
31
|
+
from ducktools.classbuilder.prefab import Prefab, attribute
|
|
32
32
|
from ducktools.lazyimporter import LazyImporter, ModuleImport, FromImport
|
|
33
33
|
|
|
34
34
|
from . import details_script
|
|
35
35
|
|
|
36
36
|
_laz = LazyImporter(
|
|
37
37
|
[
|
|
38
|
-
ModuleImport("re"),
|
|
39
|
-
ModuleImport("subprocess"),
|
|
40
|
-
ModuleImport("platform"),
|
|
41
38
|
FromImport("glob", "glob"),
|
|
42
39
|
ModuleImport("json"),
|
|
40
|
+
ModuleImport("platform"),
|
|
41
|
+
ModuleImport("re"),
|
|
42
|
+
ModuleImport("subprocess"),
|
|
43
|
+
ModuleImport("tempfile"),
|
|
43
44
|
ModuleImport("zipfile"),
|
|
44
45
|
]
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)(?P<releaselevel>[a-zA-Z]*)(?P<serial>\d*)"
|
|
49
|
+
FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>[a-zA-Z]*)(?P<serial>\d*)"
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def version_str_to_tuple(version):
|
|
@@ -56,7 +57,7 @@ def version_str_to_tuple(version):
|
|
|
56
57
|
|
|
57
58
|
major, minor, micro, releaselevel, serial = parsed_version.groups()
|
|
58
59
|
|
|
59
|
-
if releaselevel
|
|
60
|
+
if releaselevel in {"a", "dev"}:
|
|
60
61
|
releaselevel = "alpha"
|
|
61
62
|
elif releaselevel == "b":
|
|
62
63
|
releaselevel = "beta"
|
|
@@ -95,17 +96,12 @@ def version_tuple_to_str(version_tuple):
|
|
|
95
96
|
return f"{major}.{minor}.{micro}{releaselevel}{serial}"
|
|
96
97
|
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
class DetailsScript:
|
|
99
|
+
class DetailsScript(Prefab):
|
|
100
100
|
"""
|
|
101
101
|
Class to obtain and cache the source code of details_script.py
|
|
102
102
|
to use on external Pythons.
|
|
103
103
|
"""
|
|
104
|
-
|
|
105
|
-
_source_code=Field(default=None)
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
_source_code: str | None
|
|
104
|
+
_source_code: str | None = attribute(default=None, private=True)
|
|
109
105
|
|
|
110
106
|
def get_source_code(self):
|
|
111
107
|
if self._source_code is None:
|
|
@@ -128,27 +124,46 @@ class DetailsScript:
|
|
|
128
124
|
details = DetailsScript()
|
|
129
125
|
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
class PythonInstall:
|
|
133
|
-
__slots__ = SlotFields(
|
|
134
|
-
version=Field(),
|
|
135
|
-
executable=Field(),
|
|
136
|
-
architecture="64bit",
|
|
137
|
-
implementation="cpython",
|
|
138
|
-
metadata=Field(default_factory=dict),
|
|
139
|
-
shadowed=False,
|
|
140
|
-
)
|
|
127
|
+
class PythonInstall(Prefab):
|
|
141
128
|
version: tuple[int, int, int, str, int]
|
|
142
129
|
executable: str
|
|
143
|
-
architecture: str
|
|
144
|
-
implementation: str
|
|
145
|
-
metadata: dict
|
|
146
|
-
shadowed: bool
|
|
130
|
+
architecture: str = "64bit"
|
|
131
|
+
implementation: str = "cpython"
|
|
132
|
+
metadata: dict = attribute(default_factory=dict)
|
|
133
|
+
shadowed: bool = False
|
|
134
|
+
_implementation_version: tuple[int, int, int, str, int] | None = attribute(default=None, private=True)
|
|
135
|
+
|
|
136
|
+
def __prefab_post_init__(
|
|
137
|
+
self,
|
|
138
|
+
version: tuple[int, int, int] | tuple[int, int, int, str, int]
|
|
139
|
+
):
|
|
140
|
+
if len(version) == 3:
|
|
141
|
+
# Micropython gives an invalid 3 part version here
|
|
142
|
+
# Add the extras to avoid breaking
|
|
143
|
+
self.version = tuple([*version, "final", 0]) # type: ignore
|
|
144
|
+
else:
|
|
145
|
+
self.version = version
|
|
147
146
|
|
|
148
147
|
@property
|
|
149
148
|
def version_str(self) -> str:
|
|
150
149
|
return version_tuple_to_str(self.version)
|
|
151
150
|
|
|
151
|
+
@property
|
|
152
|
+
def implementation_version(self) -> tuple[int, int, int, str, int] | None:
|
|
153
|
+
if self._implementation_version is None:
|
|
154
|
+
if self.implementation == "cpython":
|
|
155
|
+
self._implementation_version = self.version
|
|
156
|
+
elif implementation_ver := self.metadata.get(f"{self.implementation}_version"):
|
|
157
|
+
if len(implementation_ver) == 3:
|
|
158
|
+
self._implementation_version = tuple([*implementation_ver, "final", 0]) # type: ignore
|
|
159
|
+
else:
|
|
160
|
+
self._implementation_version = implementation_ver
|
|
161
|
+
return self._implementation_version
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def implementation_version_str(self) -> str:
|
|
165
|
+
return version_tuple_to_str(self.implementation_version)
|
|
166
|
+
|
|
152
167
|
@classmethod
|
|
153
168
|
def from_str(
|
|
154
169
|
cls,
|
|
@@ -219,12 +234,31 @@ def get_install_details(executable: str) -> PythonInstall | None:
|
|
|
219
234
|
text=True,
|
|
220
235
|
check=True,
|
|
221
236
|
).stdout
|
|
222
|
-
except
|
|
237
|
+
except OSError:
|
|
238
|
+
# Something else has gone wrong
|
|
223
239
|
return None
|
|
240
|
+
except (_laz.subprocess.CalledProcessError, FileNotFoundError):
|
|
241
|
+
# Potentially this is micropython which does not support
|
|
242
|
+
# piping from stdin. Try using a file in a temporary folder.
|
|
243
|
+
# Python 3.12 has delete_on_close that would make TemporaryFile
|
|
244
|
+
# Usable on windows but for now use a directory
|
|
245
|
+
with _laz.tempfile.TemporaryDirectory() as tempdir:
|
|
246
|
+
temp_script = os.path.join(tempdir, "details_script.py")
|
|
247
|
+
with open(temp_script, "w") as f:
|
|
248
|
+
f.write(source)
|
|
249
|
+
try:
|
|
250
|
+
detail_output = _laz.subprocess.run(
|
|
251
|
+
[executable, temp_script],
|
|
252
|
+
capture_output=True,
|
|
253
|
+
text=True,
|
|
254
|
+
check=True,
|
|
255
|
+
).stdout
|
|
256
|
+
except (_laz.subprocess.CalledProcessError, FileNotFoundError):
|
|
257
|
+
return None
|
|
224
258
|
|
|
225
259
|
try:
|
|
226
260
|
output = _laz.json.loads(detail_output)
|
|
227
|
-
except _laz.json.JSONDecodeError:
|
|
261
|
+
except _laz.json.JSONDecodeError as e:
|
|
228
262
|
return None
|
|
229
263
|
|
|
230
264
|
return PythonInstall.from_json(**output)
|
|
@@ -266,7 +300,10 @@ def get_uv_python_path() -> str | None:
|
|
|
266
300
|
return uv_python_dir
|
|
267
301
|
|
|
268
302
|
|
|
269
|
-
def _implementation_from_uv_dir(
|
|
303
|
+
def _implementation_from_uv_dir(
|
|
304
|
+
direntry: os.DirEntry,
|
|
305
|
+
query_executables: bool = True,
|
|
306
|
+
) -> PythonInstall | None:
|
|
270
307
|
python_exe = "python.exe" if sys.platform == "win32" else "bin/python"
|
|
271
308
|
python_path = os.path.join(direntry, python_exe)
|
|
272
309
|
|
|
@@ -278,9 +315,10 @@ def _implementation_from_uv_dir(direntry: os.DirEntry) -> PythonInstall | None:
|
|
|
278
315
|
except ValueError:
|
|
279
316
|
# Directory name format has changed
|
|
280
317
|
# Slow backup - ask python itself
|
|
281
|
-
|
|
318
|
+
if query_executables:
|
|
319
|
+
install = get_install_details(python_path)
|
|
282
320
|
else:
|
|
283
|
-
if implementation
|
|
321
|
+
if implementation in {"cpython"}:
|
|
284
322
|
install = PythonInstall.from_str(
|
|
285
323
|
version=version,
|
|
286
324
|
executable=python_path,
|
|
@@ -289,16 +327,20 @@ def _implementation_from_uv_dir(direntry: os.DirEntry) -> PythonInstall | None:
|
|
|
289
327
|
)
|
|
290
328
|
else:
|
|
291
329
|
# Get additional alternate implementation details
|
|
292
|
-
|
|
330
|
+
if query_executables:
|
|
331
|
+
install = get_install_details(python_path)
|
|
293
332
|
|
|
294
333
|
return install
|
|
295
334
|
|
|
296
335
|
|
|
297
|
-
def get_uv_pythons() -> Iterator[PythonInstall]:
|
|
336
|
+
def get_uv_pythons(query_executables=True) -> Iterator[PythonInstall]:
|
|
298
337
|
# This takes some shortcuts over the regular pythonfinder
|
|
299
338
|
# As the UV folders give the python version and the implementation
|
|
300
339
|
if uv_python_path := get_uv_python_path():
|
|
301
340
|
with os.scandir(uv_python_path) as fld:
|
|
302
341
|
for f in fld:
|
|
303
|
-
if
|
|
342
|
+
if (
|
|
343
|
+
f.is_dir()
|
|
344
|
+
and (install := _implementation_from_uv_dir(f, query_executables))
|
|
345
|
+
):
|
|
304
346
|
yield install
|
|
@@ -30,12 +30,12 @@ from .pyenv_search import get_pyenv_pythons
|
|
|
30
30
|
from .registry_search import get_registered_pythons
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def get_python_installs() -> Iterator[PythonInstall]:
|
|
33
|
+
def get_python_installs(*, query_executables: bool = True) -> Iterator[PythonInstall]:
|
|
34
34
|
listed_installs = set()
|
|
35
35
|
for py in itertools.chain(
|
|
36
36
|
get_registered_pythons(),
|
|
37
|
-
get_pyenv_pythons(),
|
|
38
|
-
get_uv_pythons(),
|
|
37
|
+
get_pyenv_pythons(query_executables=query_executables),
|
|
38
|
+
get_uv_pythons(query_executables=query_executables),
|
|
39
39
|
):
|
|
40
40
|
if py.executable not in listed_installs:
|
|
41
41
|
yield py
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# MIT License
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2023-2024 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
|
+
import os
|
|
26
|
+
import os.path
|
|
27
|
+
from _collections_abc import Iterator
|
|
28
|
+
|
|
29
|
+
from ..shared import PythonInstall, get_install_details
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
PYENV_VERSIONS_FOLDER = os.path.join(os.environ.get("PYENV_ROOT", ""), "versions")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_pyenv_pythons(
|
|
36
|
+
versions_folder: str | os.PathLike = PYENV_VERSIONS_FOLDER,
|
|
37
|
+
*,
|
|
38
|
+
query_executables: bool = True,
|
|
39
|
+
) -> Iterator[PythonInstall]:
|
|
40
|
+
if not os.path.exists(versions_folder):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
for p in os.scandir(versions_folder):
|
|
44
|
+
path_base = os.path.basename(p.path)
|
|
45
|
+
|
|
46
|
+
if query_executables:
|
|
47
|
+
# Check for pypy/graalpy
|
|
48
|
+
if path_base.startswith("pypy"):
|
|
49
|
+
executable = os.path.join(p.path, "pypy.exe")
|
|
50
|
+
if os.path.exists(executable):
|
|
51
|
+
yield get_install_details(executable)
|
|
52
|
+
continue
|
|
53
|
+
elif path_base.startswith("graalpy"):
|
|
54
|
+
# Graalpy exe in bin subfolder
|
|
55
|
+
executable = os.path.join(p.path, "bin", "graalpy.exe")
|
|
56
|
+
if os.path.exists(executable):
|
|
57
|
+
yield get_install_details(executable)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Regular CPython
|
|
61
|
+
executable = os.path.join(p.path, "python.exe")
|
|
62
|
+
|
|
63
|
+
if os.path.exists(executable):
|
|
64
|
+
split_version = p.name.split("-")
|
|
65
|
+
|
|
66
|
+
# If there are 1 or 2 arguments this is a recognised version
|
|
67
|
+
# Otherwise it is unrecognised
|
|
68
|
+
if len(split_version) == 2:
|
|
69
|
+
version, arch = split_version
|
|
70
|
+
|
|
71
|
+
# win32 in pyenv name means 32 bit python install
|
|
72
|
+
# 'arm' is the only alternative which will be 64bit
|
|
73
|
+
arch = "32bit" if arch == "win32" else "64bit"
|
|
74
|
+
try:
|
|
75
|
+
yield PythonInstall.from_str(
|
|
76
|
+
version, executable, architecture=arch
|
|
77
|
+
)
|
|
78
|
+
except ValueError:
|
|
79
|
+
pass
|
|
80
|
+
elif len(split_version) == 1:
|
|
81
|
+
version = split_version[0]
|
|
82
|
+
try:
|
|
83
|
+
yield PythonInstall.from_str(
|
|
84
|
+
version, executable, architecture="64bit"
|
|
85
|
+
)
|
|
86
|
+
except ValueError:
|
|
87
|
+
pass
|
|
@@ -24,7 +24,14 @@ from pathlib import Path
|
|
|
24
24
|
|
|
25
25
|
import pytest
|
|
26
26
|
|
|
27
|
+
from ducktools.pythonfinder import details_script
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
@pytest.fixture(scope="session")
|
|
29
31
|
def sources_folder():
|
|
30
32
|
return Path(__file__).parent / "sources"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def uses_details_script(fs):
|
|
37
|
+
fs.add_real_file(details_script.__file__)
|
|
@@ -89,7 +89,7 @@ def test_get_install_details_error():
|
|
|
89
89
|
) as run_mock:
|
|
90
90
|
details = get_install_details(fake_details_out.executable)
|
|
91
91
|
|
|
92
|
-
run_mock.
|
|
92
|
+
run_mock.assert_any_call(
|
|
93
93
|
[fake_details_out.executable, "-"],
|
|
94
94
|
input=details_text,
|
|
95
95
|
capture_output=True,
|
|
@@ -110,30 +110,32 @@ def test_32bit_version(fs):
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
@pytest.mark.skipif(sys.platform != "win32", reason="Test for Windows only")
|
|
113
|
-
def test_invalid_ver_win(fs):
|
|
113
|
+
def test_invalid_ver_win(fs, uses_details_script):
|
|
114
114
|
# Ignore non-standard versions
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
pyenv_fld = "C:\\Fake_Folder"
|
|
117
117
|
|
|
118
|
-
py_folder = os.path.join(
|
|
118
|
+
py_folder = os.path.join(pyenv_fld, "external-python3.12.1")
|
|
119
119
|
py_exe = os.path.join(py_folder, "python.exe")
|
|
120
120
|
|
|
121
121
|
fs.create_dir(py_folder)
|
|
122
122
|
fs.create_file(py_exe)
|
|
123
123
|
|
|
124
|
-
py2_folder = os.path.join(
|
|
124
|
+
py2_folder = os.path.join(pyenv_fld, "ext3.13.0")
|
|
125
125
|
py2_exe = os.path.join(py2_folder, "python.exe")
|
|
126
126
|
|
|
127
127
|
fs.create_dir(py2_folder)
|
|
128
128
|
fs.create_file(py2_exe)
|
|
129
129
|
|
|
130
|
-
py3_folder = os.path.join(
|
|
130
|
+
py3_folder = os.path.join(pyenv_fld, "invalid-version-3.12.1")
|
|
131
131
|
py3_exe = os.path.join(py3_folder, "python.exe")
|
|
132
132
|
|
|
133
133
|
fs.create_dir(py3_folder)
|
|
134
134
|
fs.create_file(py3_exe)
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
with patch("subprocess.run") as run_mock:
|
|
137
|
+
versions = list(get_pyenv_pythons(pyenv_fld))
|
|
138
|
+
run_mock.assert_not_called()
|
|
137
139
|
|
|
138
140
|
assert versions == []
|
|
139
141
|
|
|
@@ -157,7 +159,7 @@ def test_fs_versions_nix(fs):
|
|
|
157
159
|
|
|
158
160
|
|
|
159
161
|
@pytest.mark.skipif(sys.platform == "win32", reason="Test for non-Windows only")
|
|
160
|
-
def test_invalid_ver_nix(fs):
|
|
162
|
+
def test_invalid_ver_nix(fs, uses_details_script):
|
|
161
163
|
# Test folders in fake file system
|
|
162
164
|
|
|
163
165
|
tmpdir = "~/.pyenv/versions"
|
|
@@ -183,7 +185,10 @@ def test_invalid_ver_nix(fs):
|
|
|
183
185
|
fs.create_dir(os.path.join(py3_folder, "bin"))
|
|
184
186
|
fs.create_file(py3_exe)
|
|
185
187
|
|
|
186
|
-
|
|
188
|
+
with patch("subprocess.run") as run_mock:
|
|
189
|
+
run_mock.side_effect = OSError("Failure")
|
|
190
|
+
versions = list(get_pyenv_pythons(tmpdir))
|
|
191
|
+
assert run_mock.call_count == 3
|
|
187
192
|
|
|
188
193
|
assert versions == []
|
|
189
194
|
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# ducktools-pythonfinder
|
|
2
|
-
# MIT License
|
|
3
|
-
#
|
|
4
|
-
# Copyright (c) 2023-2024 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
|
-
import os
|
|
26
|
-
import os.path
|
|
27
|
-
from _collections_abc import Iterator
|
|
28
|
-
|
|
29
|
-
from ..shared import PythonInstall, get_install_details
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
PYENV_VERSIONS_FOLDER = os.path.join(os.environ.get("PYENV_ROOT", ""), "versions")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_pyenv_pythons(
|
|
36
|
-
versions_folder: str | os.PathLike = PYENV_VERSIONS_FOLDER,
|
|
37
|
-
) -> Iterator[PythonInstall]:
|
|
38
|
-
if not os.path.exists(versions_folder):
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
for p in os.scandir(versions_folder):
|
|
42
|
-
path_base = os.path.basename(p.path)
|
|
43
|
-
|
|
44
|
-
if path_base.startswith("pypy"):
|
|
45
|
-
executable = os.path.join(p.path, "pypy.exe")
|
|
46
|
-
if os.path.exists(executable):
|
|
47
|
-
yield get_install_details(executable)
|
|
48
|
-
elif path_base.startswith("graalpy"):
|
|
49
|
-
# Graalpy exe in bin subfolder
|
|
50
|
-
executable = os.path.join(p.path, "bin", "graalpy.exe")
|
|
51
|
-
if os.path.exists(executable):
|
|
52
|
-
yield get_install_details(executable)
|
|
53
|
-
else:
|
|
54
|
-
# Regular CPython
|
|
55
|
-
executable = os.path.join(p.path, "python.exe")
|
|
56
|
-
|
|
57
|
-
if os.path.exists(executable):
|
|
58
|
-
split_version = p.name.split("-")
|
|
59
|
-
|
|
60
|
-
# If there are 1 or 2 arguments this is a recognised version
|
|
61
|
-
# Otherwise it is unrecognised
|
|
62
|
-
if len(split_version) == 2:
|
|
63
|
-
version, arch = split_version
|
|
64
|
-
|
|
65
|
-
# win32 in pyenv name means 32 bit python install
|
|
66
|
-
# 'arm' is the only alternative which will be 64bit
|
|
67
|
-
arch = "32bit" if arch == "win32" else "64bit"
|
|
68
|
-
try:
|
|
69
|
-
yield PythonInstall.from_str(
|
|
70
|
-
version, executable, architecture=arch
|
|
71
|
-
)
|
|
72
|
-
except ValueError:
|
|
73
|
-
pass
|
|
74
|
-
elif len(split_version) == 1:
|
|
75
|
-
version = split_version[0]
|
|
76
|
-
try:
|
|
77
|
-
yield PythonInstall.from_str(
|
|
78
|
-
version, executable, architecture="64bit"
|
|
79
|
-
)
|
|
80
|
-
except ValueError:
|
|
81
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/scripts/print_python_versions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/sources/python_versions.txt
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.5}/tests/sources/release_file.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|