ducktools-pythonfinder 0.5.3__tar.gz → 0.5.4__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.4}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/__init__.py +3 -2
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/__main__.py +25 -10
- ducktools_pythonfinder-0.5.4/src/ducktools/pythonfinder/_version.py +2 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/linux/__init__.py +9 -6
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/linux/pyenv_search.py +6 -11
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/shared.py +59 -33
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/win32/__init__.py +3 -3
- ducktools_pythonfinder-0.5.4/src/ducktools/pythonfinder/win32/pyenv_search.py +87 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/conftest.py +7 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/test_foldersearch.py +1 -1
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/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.4}/.gitignore +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/LICENSE +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/README.md +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/pyproject.toml +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/details_script.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools/pythonfinder/win32/registry_search.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/sources/release.json +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/sources/release_file.json +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/test_orgsearch.py +0 -0
- {ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/test_shared.py +0 -0
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/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.4}/src/ducktools/pythonfinder/__main__.py
RENAMED
|
@@ -79,6 +79,11 @@ def parse_args(args):
|
|
|
79
79
|
description="Discover base Python installs",
|
|
80
80
|
)
|
|
81
81
|
parser.add_argument("-V", "--version", action="version", version=__version__)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--fast",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Skip Python installs that need to be launched to obtain metadata"
|
|
86
|
+
)
|
|
82
87
|
|
|
83
88
|
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
84
89
|
|
|
@@ -121,7 +126,12 @@ def parse_args(args):
|
|
|
121
126
|
return vals
|
|
122
127
|
|
|
123
128
|
|
|
124
|
-
def display_local_installs(
|
|
129
|
+
def display_local_installs(
|
|
130
|
+
min_ver=None,
|
|
131
|
+
max_ver=None,
|
|
132
|
+
compatible=None,
|
|
133
|
+
query_executables=True,
|
|
134
|
+
):
|
|
125
135
|
if min_ver:
|
|
126
136
|
min_ver = tuple(int(i) for i in min_ver.split("."))
|
|
127
137
|
if max_ver:
|
|
@@ -129,7 +139,7 @@ def display_local_installs(min_ver=None, max_ver=None, compatible=None):
|
|
|
129
139
|
if compatible:
|
|
130
140
|
compatible = tuple(int(i) for i in compatible.split("."))
|
|
131
141
|
|
|
132
|
-
installs = list_python_installs()
|
|
142
|
+
installs = list_python_installs(query_executables=query_executables)
|
|
133
143
|
headings = ["Python Version", "Executable Location"]
|
|
134
144
|
max_executable_len = max(
|
|
135
145
|
len(headings[1]), max(len(inst.executable) for inst in installs)
|
|
@@ -184,13 +194,13 @@ def display_local_installs(min_ver=None, max_ver=None, compatible=None):
|
|
|
184
194
|
|
|
185
195
|
|
|
186
196
|
def display_remote_binaries(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
min_ver,
|
|
198
|
+
max_ver,
|
|
199
|
+
compatible,
|
|
200
|
+
all_binaries,
|
|
201
|
+
system,
|
|
202
|
+
machine,
|
|
203
|
+
prerelease
|
|
194
204
|
):
|
|
195
205
|
specs = []
|
|
196
206
|
if min_ver:
|
|
@@ -252,7 +262,12 @@ def main():
|
|
|
252
262
|
except _laz.URLError:
|
|
253
263
|
print("Could not connect to python.org")
|
|
254
264
|
else:
|
|
255
|
-
display_local_installs(
|
|
265
|
+
display_local_installs(
|
|
266
|
+
min_ver=vals.min,
|
|
267
|
+
max_ver=vals.max,
|
|
268
|
+
compatible=vals.compatible,
|
|
269
|
+
query_executables=not vals.fast,
|
|
270
|
+
)
|
|
256
271
|
else:
|
|
257
272
|
# No arguments to parse
|
|
258
273
|
display_local_installs()
|
|
@@ -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.4}/src/ducktools/pythonfinder/shared.py
RENAMED
|
@@ -28,18 +28,19 @@ 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
|
)
|
|
@@ -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,22 +124,24 @@ 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
|
+
|
|
135
|
+
def __prefab_post_init__(
|
|
136
|
+
self,
|
|
137
|
+
version: tuple[int, int, int] | tuple[int, int, int, str, int]
|
|
138
|
+
):
|
|
139
|
+
if len(version) == 3:
|
|
140
|
+
# Micropython gives an invalid 3 part version here
|
|
141
|
+
# Add the extras to avoid breaking
|
|
142
|
+
self.version = tuple([*version, "final", 0]) # type: ignore
|
|
143
|
+
else:
|
|
144
|
+
self.version = version
|
|
147
145
|
|
|
148
146
|
@property
|
|
149
147
|
def version_str(self) -> str:
|
|
@@ -219,12 +217,32 @@ def get_install_details(executable: str) -> PythonInstall | None:
|
|
|
219
217
|
text=True,
|
|
220
218
|
check=True,
|
|
221
219
|
).stdout
|
|
222
|
-
except
|
|
220
|
+
except OSError:
|
|
221
|
+
# Something else has gone wrong
|
|
223
222
|
return None
|
|
223
|
+
except (_laz.subprocess.CalledProcessError, FileNotFoundError):
|
|
224
|
+
# Potentially this is micropython which does not support
|
|
225
|
+
# piping from stdin. Try using a file in a temporary folder.
|
|
226
|
+
# Python 3.12 has delete_on_close that would make TemporaryFile
|
|
227
|
+
# Usable on windows but for now use a directory
|
|
228
|
+
with _laz.tempfile.TemporaryDirectory() as tempdir:
|
|
229
|
+
temp_script = os.path.join(tempdir, "details_script.py")
|
|
230
|
+
with open(temp_script, "w") as f:
|
|
231
|
+
f.write(source)
|
|
232
|
+
try:
|
|
233
|
+
detail_output = _laz.subprocess.run(
|
|
234
|
+
[executable, temp_script],
|
|
235
|
+
capture_output=True,
|
|
236
|
+
text=True,
|
|
237
|
+
check=True,
|
|
238
|
+
).stdout
|
|
239
|
+
except (_laz.subprocess.CalledProcessError, FileNotFoundError):
|
|
240
|
+
return None
|
|
224
241
|
|
|
225
242
|
try:
|
|
226
243
|
output = _laz.json.loads(detail_output)
|
|
227
|
-
except _laz.json.JSONDecodeError:
|
|
244
|
+
except _laz.json.JSONDecodeError as e:
|
|
245
|
+
print(e)
|
|
228
246
|
return None
|
|
229
247
|
|
|
230
248
|
return PythonInstall.from_json(**output)
|
|
@@ -266,7 +284,10 @@ def get_uv_python_path() -> str | None:
|
|
|
266
284
|
return uv_python_dir
|
|
267
285
|
|
|
268
286
|
|
|
269
|
-
def _implementation_from_uv_dir(
|
|
287
|
+
def _implementation_from_uv_dir(
|
|
288
|
+
direntry: os.DirEntry,
|
|
289
|
+
query_executables: bool = True,
|
|
290
|
+
) -> PythonInstall | None:
|
|
270
291
|
python_exe = "python.exe" if sys.platform == "win32" else "bin/python"
|
|
271
292
|
python_path = os.path.join(direntry, python_exe)
|
|
272
293
|
|
|
@@ -278,9 +299,10 @@ def _implementation_from_uv_dir(direntry: os.DirEntry) -> PythonInstall | None:
|
|
|
278
299
|
except ValueError:
|
|
279
300
|
# Directory name format has changed
|
|
280
301
|
# Slow backup - ask python itself
|
|
281
|
-
|
|
302
|
+
if query_executables:
|
|
303
|
+
install = get_install_details(python_path)
|
|
282
304
|
else:
|
|
283
|
-
if implementation
|
|
305
|
+
if implementation in {"cpython", "pypy"}:
|
|
284
306
|
install = PythonInstall.from_str(
|
|
285
307
|
version=version,
|
|
286
308
|
executable=python_path,
|
|
@@ -289,16 +311,20 @@ def _implementation_from_uv_dir(direntry: os.DirEntry) -> PythonInstall | None:
|
|
|
289
311
|
)
|
|
290
312
|
else:
|
|
291
313
|
# Get additional alternate implementation details
|
|
292
|
-
|
|
314
|
+
if query_executables:
|
|
315
|
+
install = get_install_details(python_path)
|
|
293
316
|
|
|
294
317
|
return install
|
|
295
318
|
|
|
296
319
|
|
|
297
|
-
def get_uv_pythons() -> Iterator[PythonInstall]:
|
|
320
|
+
def get_uv_pythons(query_executables=True) -> Iterator[PythonInstall]:
|
|
298
321
|
# This takes some shortcuts over the regular pythonfinder
|
|
299
322
|
# As the UV folders give the python version and the implementation
|
|
300
323
|
if uv_python_path := get_uv_python_path():
|
|
301
324
|
with os.scandir(uv_python_path) as fld:
|
|
302
325
|
for f in fld:
|
|
303
|
-
if
|
|
326
|
+
if (
|
|
327
|
+
f.is_dir()
|
|
328
|
+
and (install := _implementation_from_uv_dir(f, query_executables))
|
|
329
|
+
):
|
|
304
330
|
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.4}/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
|
|
File without changes
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/sources/python_versions.txt
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.5.3 → ducktools_pythonfinder-0.5.4}/tests/sources/release_file.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|