ducktools-pythonfinder 0.9.1__tar.gz → 0.9.3__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.9.1/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.9.3}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/__init__.py +1 -1
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/__main__.py +36 -28
- ducktools_pythonfinder-0.9.3/src/ducktools/pythonfinder/_version.py +2 -0
- ducktools_pythonfinder-0.9.3/src/ducktools/pythonfinder/darwin/__init__.py +74 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/details_script.py +9 -1
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/linux/__init__.py +16 -4
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/linux/pyenv_search.py +9 -5
- ducktools_pythonfinder-0.9.3/src/ducktools/pythonfinder/py.typed +1 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/pythonorg_search.py +4 -1
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/shared.py +20 -14
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/venv.py +10 -5
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/win32/__init__.py +5 -1
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/win32/pyenv_search.py +6 -2
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/win32/registry_search.py +6 -2
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +1 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_uv_finder.py +6 -6
- ducktools_pythonfinder-0.9.1/src/ducktools/pythonfinder/_version.py +0 -2
- ducktools_pythonfinder-0.9.1/src/ducktools/pythonfinder/darwin/__init__.py +0 -26
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/.gitignore +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/LICENSE +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/README.md +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/pyproject.toml +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/scripts/list_python_venvs.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/package_list_script.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/conftest.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/sources/release.json +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/sources/release_file.json +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_cache.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_foldersearch.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_orgsearch.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_pyenv.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_shared.py +0 -0
- {ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/test_venv_finder.py +0 -0
{ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/__init__.py
RENAMED
|
@@ -43,7 +43,7 @@ else:
|
|
|
43
43
|
from .linux import get_python_installs
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def list_python_installs(*, finder: DetailFinder | None = None):
|
|
46
|
+
def list_python_installs(*, finder: DetailFinder | None = None) -> list[PythonInstall]:
|
|
47
47
|
finder = DetailFinder() if finder is None else finder
|
|
48
48
|
return sorted(
|
|
49
49
|
get_python_installs(finder=finder),
|
{ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/__main__.py
RENAMED
|
@@ -22,13 +22,19 @@
|
|
|
22
22
|
# SOFTWARE.
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
-
|
|
26
25
|
import sys
|
|
27
26
|
import os
|
|
28
27
|
|
|
29
28
|
from ducktools.lazyimporter import LazyImporter, ModuleImport, FromImport
|
|
30
|
-
|
|
31
|
-
from
|
|
29
|
+
|
|
30
|
+
from . import list_python_installs, __version__
|
|
31
|
+
from .shared import purge_caches
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
TYPE_CHECKING = False
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
import argparse
|
|
37
|
+
|
|
32
38
|
|
|
33
39
|
_laz = LazyImporter(
|
|
34
40
|
[
|
|
@@ -49,7 +55,7 @@ class UnsupportedPythonError(Exception):
|
|
|
49
55
|
pass
|
|
50
56
|
|
|
51
57
|
|
|
52
|
-
def stop_autoclose():
|
|
58
|
+
def stop_autoclose() -> None:
|
|
53
59
|
"""
|
|
54
60
|
Checks if it thinks windows will auto close the window after running
|
|
55
61
|
|
|
@@ -76,11 +82,11 @@ def stop_autoclose():
|
|
|
76
82
|
_laz.subprocess.run("pause", shell=True)
|
|
77
83
|
|
|
78
84
|
|
|
79
|
-
def _get_parser_class():
|
|
85
|
+
def _get_parser_class() -> type[argparse.ArgumentParser]:
|
|
80
86
|
# This class is deferred to avoid the argparse import
|
|
81
87
|
# if there are no arguments to parse
|
|
82
88
|
|
|
83
|
-
class FixedArgumentParser(_laz.argparse.ArgumentParser):
|
|
89
|
+
class FixedArgumentParser(_laz.argparse.ArgumentParser): # type: ignore
|
|
84
90
|
"""
|
|
85
91
|
The builtin argument parser uses shutil to figure out the terminal width
|
|
86
92
|
to display help info. This one replaces the function that calls help info
|
|
@@ -108,7 +114,7 @@ def _get_parser_class():
|
|
|
108
114
|
return FixedArgumentParser
|
|
109
115
|
|
|
110
116
|
|
|
111
|
-
def get_parser():
|
|
117
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
112
118
|
FixedArgumentParser = _get_parser_class() # noqa
|
|
113
119
|
|
|
114
120
|
parser = FixedArgumentParser(
|
|
@@ -162,16 +168,16 @@ def get_parser():
|
|
|
162
168
|
|
|
163
169
|
|
|
164
170
|
def display_local_installs(
|
|
165
|
-
min_ver=None,
|
|
166
|
-
max_ver=None,
|
|
167
|
-
compatible=None,
|
|
168
|
-
):
|
|
171
|
+
min_ver: str | None = None,
|
|
172
|
+
max_ver: str | None = None,
|
|
173
|
+
compatible: str | None = None,
|
|
174
|
+
) -> None:
|
|
169
175
|
if min_ver:
|
|
170
|
-
|
|
176
|
+
min_ver_tuple = tuple(int(i) for i in min_ver.split("."))
|
|
171
177
|
if max_ver:
|
|
172
|
-
|
|
178
|
+
max_ver_tuple = tuple(int(i) for i in max_ver.split("."))
|
|
173
179
|
if compatible:
|
|
174
|
-
|
|
180
|
+
compatible_tuple = tuple(int(i) for i in compatible.split("."))
|
|
175
181
|
|
|
176
182
|
installs = list_python_installs()
|
|
177
183
|
|
|
@@ -185,15 +191,15 @@ def display_local_installs(
|
|
|
185
191
|
|
|
186
192
|
# First collect the strings
|
|
187
193
|
for install in installs:
|
|
188
|
-
if min_ver and install.version <
|
|
194
|
+
if min_ver and install.version < min_ver_tuple:
|
|
189
195
|
continue
|
|
190
|
-
elif max_ver and install.version >
|
|
196
|
+
elif max_ver and install.version > max_ver_tuple:
|
|
191
197
|
continue
|
|
192
198
|
elif compatible:
|
|
193
|
-
if install.version <
|
|
199
|
+
if install.version < compatible_tuple:
|
|
194
200
|
continue
|
|
195
201
|
version_parts = len(compatible) - 1
|
|
196
|
-
if install.version[:version_parts] >
|
|
202
|
+
if install.version[:version_parts] > compatible_tuple[:-1]:
|
|
197
203
|
continue
|
|
198
204
|
|
|
199
205
|
version_str = install.version_str
|
|
@@ -253,14 +259,14 @@ def display_local_installs(
|
|
|
253
259
|
|
|
254
260
|
|
|
255
261
|
def display_remote_binaries(
|
|
256
|
-
min_ver,
|
|
257
|
-
max_ver,
|
|
258
|
-
compatible,
|
|
259
|
-
all_binaries,
|
|
260
|
-
system,
|
|
261
|
-
machine,
|
|
262
|
-
prerelease
|
|
263
|
-
):
|
|
262
|
+
min_ver: str,
|
|
263
|
+
max_ver: str,
|
|
264
|
+
compatible: str,
|
|
265
|
+
all_binaries: bool,
|
|
266
|
+
system: str,
|
|
267
|
+
machine: str,
|
|
268
|
+
prerelease: bool,
|
|
269
|
+
) -> None:
|
|
264
270
|
specs = []
|
|
265
271
|
if min_ver:
|
|
266
272
|
specs.append(f">={min_ver}")
|
|
@@ -294,7 +300,7 @@ def display_remote_binaries(
|
|
|
294
300
|
print("No Python releases found matching specification")
|
|
295
301
|
|
|
296
302
|
|
|
297
|
-
def main():
|
|
303
|
+
def main() -> int:
|
|
298
304
|
if sys.version_info < (3, 8):
|
|
299
305
|
v = sys.version_info
|
|
300
306
|
raise UnsupportedPythonError(
|
|
@@ -334,7 +340,9 @@ def main():
|
|
|
334
340
|
display_local_installs()
|
|
335
341
|
|
|
336
342
|
stop_autoclose()
|
|
343
|
+
|
|
344
|
+
return 0
|
|
337
345
|
|
|
338
346
|
|
|
339
347
|
if __name__ == "__main__":
|
|
340
|
-
main()
|
|
348
|
+
sys.exit(main())
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import itertools
|
|
26
|
+
try:
|
|
27
|
+
from _collections_abc import Iterator
|
|
28
|
+
except ImportError:
|
|
29
|
+
from collections.abc import Iterator
|
|
30
|
+
|
|
31
|
+
from .. import linux
|
|
32
|
+
from ..shared import get_uv_pythons, DetailFinder,PythonInstall
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# This is the difference from the linux methods
|
|
36
|
+
KNOWN_MANAGED_PATHS = {
|
|
37
|
+
**linux.KNOWN_MANAGED_PATHS,
|
|
38
|
+
"/opt/homebrew": "Homebrew", # ARM Apple
|
|
39
|
+
"/usr/local/opt": "Homebrew", # x86_64 Apple
|
|
40
|
+
"/Applications/Xcode.app": "Xcode",
|
|
41
|
+
"/Library/Developer/CommandLineTools": "Xcode", # Xcode commandline tools
|
|
42
|
+
"/Library/Frameworks/Python.framework": "python.org",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_path_pythons(
|
|
47
|
+
*,
|
|
48
|
+
finder: DetailFinder | None = None,
|
|
49
|
+
known_paths: dict[str, str] | None = None,
|
|
50
|
+
) -> Iterator[PythonInstall]:
|
|
51
|
+
|
|
52
|
+
known_paths = KNOWN_MANAGED_PATHS if known_paths is None else known_paths
|
|
53
|
+
|
|
54
|
+
return linux.get_path_pythons(finder=finder, known_paths=known_paths)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_python_installs(
|
|
58
|
+
*,
|
|
59
|
+
finder: DetailFinder | None = None,
|
|
60
|
+
) -> Iterator[PythonInstall]:
|
|
61
|
+
listed_pythons = set()
|
|
62
|
+
|
|
63
|
+
finder = DetailFinder() if finder is None else finder
|
|
64
|
+
|
|
65
|
+
chain_commands = [
|
|
66
|
+
linux.get_pyenv_pythons(finder=finder),
|
|
67
|
+
get_uv_pythons(finder=finder),
|
|
68
|
+
get_path_pythons(finder=finder),
|
|
69
|
+
]
|
|
70
|
+
with finder:
|
|
71
|
+
for py in itertools.chain.from_iterable(chain_commands):
|
|
72
|
+
if py.executable not in listed_pythons:
|
|
73
|
+
yield py
|
|
74
|
+
listed_pythons.add(py.executable)
|
|
@@ -103,10 +103,18 @@ def get_details():
|
|
|
103
103
|
else:
|
|
104
104
|
paths = sysconfig.get_paths()
|
|
105
105
|
|
|
106
|
+
# Use struct to correctly identify GraalPy as 64 bit
|
|
107
|
+
try:
|
|
108
|
+
import struct
|
|
109
|
+
except ImportError:
|
|
110
|
+
architecture = "64bit" if (sys.maxsize > 2**32) else "32bit"
|
|
111
|
+
else:
|
|
112
|
+
architecture = "32bit" if (struct.calcsize("P") == 4) else "64bit"
|
|
113
|
+
|
|
106
114
|
install = dict(
|
|
107
115
|
version=list(sys.version_info),
|
|
108
116
|
executable=sys.executable,
|
|
109
|
-
architecture=
|
|
117
|
+
architecture=architecture,
|
|
110
118
|
implementation=implementation,
|
|
111
119
|
metadata=metadata,
|
|
112
120
|
paths=paths,
|
|
@@ -25,7 +25,11 @@ from __future__ import annotations
|
|
|
25
25
|
import os
|
|
26
26
|
import os.path
|
|
27
27
|
import itertools
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from _collections_abc import Iterator
|
|
31
|
+
except ImportError:
|
|
32
|
+
from collections.abc import Iterator
|
|
29
33
|
|
|
30
34
|
from ..shared import (
|
|
31
35
|
DetailFinder,
|
|
@@ -45,7 +49,12 @@ KNOWN_MANAGED_PATHS = {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
|
|
48
|
-
def get_path_pythons(
|
|
52
|
+
def get_path_pythons(
|
|
53
|
+
*,
|
|
54
|
+
finder: DetailFinder | None = None,
|
|
55
|
+
known_paths: dict[str, str] | None = None,
|
|
56
|
+
) -> Iterator[PythonInstall]:
|
|
57
|
+
|
|
49
58
|
exe_names = set()
|
|
50
59
|
|
|
51
60
|
path_folders = os.environ.get("PATH", "").split(":")
|
|
@@ -55,6 +64,7 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
|
|
|
55
64
|
excluded_folders = [pyenv_root, uv_root]
|
|
56
65
|
|
|
57
66
|
finder = DetailFinder() if finder is None else finder
|
|
67
|
+
known_paths = KNOWN_MANAGED_PATHS if known_paths is None else known_paths
|
|
58
68
|
|
|
59
69
|
for fld in path_folders:
|
|
60
70
|
# Don't retrieve pyenv installs
|
|
@@ -71,8 +81,10 @@ def get_path_pythons(*, finder: DetailFinder | None = None) -> Iterator[PythonIn
|
|
|
71
81
|
continue
|
|
72
82
|
|
|
73
83
|
for install in get_folder_pythons(fld, finder=finder):
|
|
74
|
-
|
|
75
|
-
install.
|
|
84
|
+
for path, manager in known_paths.items():
|
|
85
|
+
if os.path.commonpath((path, install.executable)) == path:
|
|
86
|
+
install.managed_by = manager
|
|
87
|
+
break
|
|
76
88
|
|
|
77
89
|
name = os.path.basename(install.executable)
|
|
78
90
|
if name in exe_names:
|
|
@@ -20,15 +20,19 @@
|
|
|
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
|
|
24
|
-
|
|
25
23
|
"""
|
|
26
24
|
Discover python installs that have been created with pyenv
|
|
27
25
|
"""
|
|
28
26
|
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
29
|
import os
|
|
30
30
|
import os.path
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from _collections_abc import Iterator
|
|
34
|
+
except ImportError:
|
|
35
|
+
from collections.abc import Iterator
|
|
32
36
|
|
|
33
37
|
from ducktools.lazyimporter import LazyImporter, FromImport, ModuleImport
|
|
34
38
|
|
|
@@ -74,7 +78,7 @@ def get_pyenv_root() -> str | None:
|
|
|
74
78
|
def get_pyenv_pythons(
|
|
75
79
|
versions_folder: str | os.PathLike | None = None,
|
|
76
80
|
*,
|
|
77
|
-
finder: DetailFinder = None,
|
|
81
|
+
finder: DetailFinder | None = None,
|
|
78
82
|
) -> Iterator[PythonInstall]:
|
|
79
83
|
if versions_folder is None:
|
|
80
84
|
if pyenv_root := get_pyenv_root():
|
|
@@ -90,7 +94,7 @@ def get_pyenv_pythons(
|
|
|
90
94
|
finder = DetailFinder() if finder is None else finder
|
|
91
95
|
|
|
92
96
|
with finder:
|
|
93
|
-
for p in sorted(os.scandir(versions_folder), key=lambda x: x.path):
|
|
97
|
+
for p in sorted(os.scandir(str(versions_folder)), key=lambda x: x.path):
|
|
94
98
|
# Don't include folders that are venvs
|
|
95
99
|
venv_indicator = os.path.join(p.path, "pyvenv.cfg")
|
|
96
100
|
if os.path.exists(venv_indicator):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
partial
|
|
@@ -39,7 +39,8 @@ from packaging.specifiers import SpecifierSet
|
|
|
39
39
|
from packaging.version import Version
|
|
40
40
|
|
|
41
41
|
from ducktools.classbuilder.prefab import Prefab, attribute, get_attributes
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
from .shared import version_str_to_tuple
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
RELEASE_PAGE = "https://www.python.org/api/v2/downloads/release/"
|
|
@@ -341,6 +342,7 @@ class PythonOrgSearch(Prefab):
|
|
|
341
342
|
for download in self.matching_downloads(specifier, prereleases):
|
|
342
343
|
if download.url.endswith(tag):
|
|
343
344
|
return download
|
|
345
|
+
return None
|
|
344
346
|
|
|
345
347
|
def latest_python_download(self, prereleases=False) -> PythonDownload | None:
|
|
346
348
|
"""
|
|
@@ -369,3 +371,4 @@ class PythonOrgSearch(Prefab):
|
|
|
369
371
|
md5_sum=release_file.md5_sum,
|
|
370
372
|
)
|
|
371
373
|
return download
|
|
374
|
+
return None
|
{ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/shared.py
RENAMED
|
@@ -26,7 +26,10 @@ import sys
|
|
|
26
26
|
import os
|
|
27
27
|
import os.path
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
try:
|
|
30
|
+
from _collections_abc import Iterator
|
|
31
|
+
except ImportError:
|
|
32
|
+
from collections.abc import Iterator
|
|
30
33
|
|
|
31
34
|
from ducktools.classbuilder.prefab import Prefab, attribute, as_dict
|
|
32
35
|
from ducktools.lazyimporter import LazyImporter, ModuleImport, FromImport
|
|
@@ -190,7 +193,7 @@ class DetailFinder(Prefab):
|
|
|
190
193
|
self.save()
|
|
191
194
|
|
|
192
195
|
@property
|
|
193
|
-
def raw_cache(self):
|
|
196
|
+
def raw_cache(self) -> dict:
|
|
194
197
|
if self._raw_cache is None:
|
|
195
198
|
try:
|
|
196
199
|
with open(self.cache_path) as f:
|
|
@@ -199,14 +202,14 @@ class DetailFinder(Prefab):
|
|
|
199
202
|
self._raw_cache = {}
|
|
200
203
|
return self._raw_cache
|
|
201
204
|
|
|
202
|
-
def save(self):
|
|
205
|
+
def save(self) -> None:
|
|
203
206
|
os.makedirs(os.path.dirname(self.cache_path), exist_ok=True)
|
|
204
207
|
with open(self.cache_path, 'w') as f:
|
|
205
208
|
_laz.json.dump(self.raw_cache, f, indent=4)
|
|
206
209
|
|
|
207
210
|
self._dirty_cache = False
|
|
208
211
|
|
|
209
|
-
def clear_invalid_runtimes(self):
|
|
212
|
+
def clear_invalid_runtimes(self) -> None:
|
|
210
213
|
"""
|
|
211
214
|
Remove cache entries where the python.exe no longer exists
|
|
212
215
|
"""
|
|
@@ -218,7 +221,7 @@ class DetailFinder(Prefab):
|
|
|
218
221
|
if removed_runtimes:
|
|
219
222
|
self._dirty_cache = True
|
|
220
223
|
|
|
221
|
-
def clear_cache(self):
|
|
224
|
+
def clear_cache(self) -> None:
|
|
222
225
|
"""
|
|
223
226
|
Completely empty the cache
|
|
224
227
|
"""
|
|
@@ -357,6 +360,8 @@ class PythonInstall(Prefab):
|
|
|
357
360
|
def implementation_version_str(self) -> str:
|
|
358
361
|
return version_tuple_to_str(self.implementation_version)
|
|
359
362
|
|
|
363
|
+
# Typing these classmethods would require an import
|
|
364
|
+
# This is not acceptable for performance reasons
|
|
360
365
|
@classmethod
|
|
361
366
|
def from_str(
|
|
362
367
|
cls,
|
|
@@ -387,13 +392,13 @@ class PythonInstall(Prefab):
|
|
|
387
392
|
@classmethod
|
|
388
393
|
def from_json(
|
|
389
394
|
cls,
|
|
390
|
-
version,
|
|
391
|
-
executable,
|
|
392
|
-
architecture,
|
|
393
|
-
implementation,
|
|
394
|
-
metadata,
|
|
395
|
-
paths=None,
|
|
396
|
-
managed_by=None,
|
|
395
|
+
version: str,
|
|
396
|
+
executable: str,
|
|
397
|
+
architecture: str,
|
|
398
|
+
implementation: str,
|
|
399
|
+
metadata: dict,
|
|
400
|
+
paths: dict | None = None,
|
|
401
|
+
managed_by: str | None = None,
|
|
397
402
|
):
|
|
398
403
|
if arch_ver := metadata.get(f"{implementation}_version"):
|
|
399
404
|
metadata[f"{implementation}_version"] = tuple(arch_ver)
|
|
@@ -431,6 +436,7 @@ class PythonInstall(Prefab):
|
|
|
431
436
|
return pip_call.stdout
|
|
432
437
|
|
|
433
438
|
|
|
439
|
+
# Return type missing due to import requirements
|
|
434
440
|
def _python_exe_regex(basename: str = "python"):
|
|
435
441
|
if sys.platform == "win32":
|
|
436
442
|
return _laz.re.compile(rf"{basename}\d?\.?\d*\.exe")
|
|
@@ -443,7 +449,7 @@ def get_folder_pythons(
|
|
|
443
449
|
basenames: tuple[str, ...] = ("python", "pypy", "micropython"),
|
|
444
450
|
finder: DetailFinder | None = None,
|
|
445
451
|
managed_by: str | None = None,
|
|
446
|
-
):
|
|
452
|
+
) -> Iterator[PythonInstall]:
|
|
447
453
|
regexes = [_python_exe_regex(name) for name in basenames]
|
|
448
454
|
|
|
449
455
|
finder = DetailFinder() if finder is None else finder
|
|
@@ -531,7 +537,7 @@ def _implementation_from_uv_dir(
|
|
|
531
537
|
|
|
532
538
|
for pth in python_paths:
|
|
533
539
|
if os.path.exists(pth):
|
|
534
|
-
install = finder.get_install_details(pth, managed_by="Astral
|
|
540
|
+
install = finder.get_install_details(pth, managed_by="Astral")
|
|
535
541
|
break
|
|
536
542
|
|
|
537
543
|
return install
|
{ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/src/ducktools/pythonfinder/venv.py
RENAMED
|
@@ -30,7 +30,6 @@ except ImportError:
|
|
|
30
30
|
import os
|
|
31
31
|
import sys
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
from ducktools.classbuilder.prefab import Prefab, attribute
|
|
35
34
|
from ducktools.lazyimporter import LazyImporter, FromImport, ModuleImport
|
|
36
35
|
|
|
@@ -84,7 +83,7 @@ class PythonVEnv(Prefab):
|
|
|
84
83
|
return version_tuple_to_str(self.version)
|
|
85
84
|
|
|
86
85
|
@property
|
|
87
|
-
def parent_executable(self) -> str:
|
|
86
|
+
def parent_executable(self) -> str | None:
|
|
88
87
|
if self._parent_executable is None:
|
|
89
88
|
# Guess the parent executable file
|
|
90
89
|
parent_exe = None
|
|
@@ -122,7 +121,9 @@ class PythonVEnv(Prefab):
|
|
|
122
121
|
|
|
123
122
|
@property
|
|
124
123
|
def parent_exists(self) -> bool:
|
|
125
|
-
|
|
124
|
+
if self.parent_executable and os.path.exists(self.parent_executable):
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
126
127
|
|
|
127
128
|
def get_parent_install(
|
|
128
129
|
self,
|
|
@@ -135,6 +136,9 @@ class PythonVEnv(Prefab):
|
|
|
135
136
|
finder = DetailFinder() if finder is None else finder
|
|
136
137
|
|
|
137
138
|
if self.parent_exists:
|
|
139
|
+
# parent_exists forces this check
|
|
140
|
+
assert self.parent_executable is not None
|
|
141
|
+
|
|
138
142
|
exe = self.parent_executable
|
|
139
143
|
|
|
140
144
|
# Python installs may be cached, can skip querying exe.
|
|
@@ -253,6 +257,7 @@ def get_python_venvs(
|
|
|
253
257
|
:param search_parent_folders: Also search parent folders
|
|
254
258
|
:yield: PythonVEnv details.
|
|
255
259
|
"""
|
|
260
|
+
# This converts base_dir to a Path, but mypy doesn't know that
|
|
256
261
|
base_dir = _laz.Path.cwd() if base_dir is None else _laz.Path(base_dir)
|
|
257
262
|
|
|
258
263
|
cwd_pattern = pattern = f"*/{VENV_CONFIG_NAME}"
|
|
@@ -261,7 +266,7 @@ def get_python_venvs(
|
|
|
261
266
|
# Only search cwd recursively, parents are searched non-recursively
|
|
262
267
|
cwd_pattern = "*" + pattern
|
|
263
268
|
|
|
264
|
-
for conf in base_dir.glob(cwd_pattern):
|
|
269
|
+
for conf in base_dir.glob(cwd_pattern): # type: ignore
|
|
265
270
|
try:
|
|
266
271
|
env = PythonVEnv.from_cfg(conf)
|
|
267
272
|
except InvalidVEnvError:
|
|
@@ -270,7 +275,7 @@ def get_python_venvs(
|
|
|
270
275
|
|
|
271
276
|
if search_parent_folders:
|
|
272
277
|
# Search parent folders
|
|
273
|
-
for fld in base_dir.parents:
|
|
278
|
+
for fld in base_dir.parents: # type: ignore
|
|
274
279
|
try:
|
|
275
280
|
for conf in fld.glob(pattern):
|
|
276
281
|
try:
|
|
@@ -22,9 +22,13 @@
|
|
|
22
22
|
# SOFTWARE.
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
-
from _collections_abc import Iterator
|
|
26
25
|
import itertools
|
|
27
26
|
|
|
27
|
+
try:
|
|
28
|
+
from _collections_abc import Iterator
|
|
29
|
+
except ImportError:
|
|
30
|
+
from collections.abc import Iterator
|
|
31
|
+
|
|
28
32
|
from ..shared import PythonInstall, get_uv_pythons, DetailFinder
|
|
29
33
|
from .pyenv_search import get_pyenv_pythons
|
|
30
34
|
from .registry_search import get_registered_pythons
|
|
@@ -24,7 +24,11 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
26
|
import os.path
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from _collections_abc import Iterator
|
|
30
|
+
except ImportError:
|
|
31
|
+
from collections.abc import Iterator
|
|
28
32
|
|
|
29
33
|
from ..shared import PythonInstall, DetailFinder
|
|
30
34
|
|
|
@@ -52,7 +56,7 @@ def get_pyenv_pythons(
|
|
|
52
56
|
finder = DetailFinder() if finder is None else finder
|
|
53
57
|
|
|
54
58
|
with finder:
|
|
55
|
-
for p in os.scandir(versions_folder):
|
|
59
|
+
for p in os.scandir(str(versions_folder)):
|
|
56
60
|
# On windows, venv folders usually have the python.exe in \Scripts\
|
|
57
61
|
# while runtimes have it in the base folder so venvs shouldn't be disovered
|
|
58
62
|
# but exclude them early anyway
|
|
@@ -20,7 +20,9 @@
|
|
|
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
|
-
|
|
23
|
+
|
|
24
|
+
# This is an overly broad ignore as linux mypy errors
|
|
25
|
+
# mypy: disable-error-code="attr-defined"
|
|
24
26
|
|
|
25
27
|
"""
|
|
26
28
|
Search the Windows registry to find python installs
|
|
@@ -28,8 +30,10 @@ Search the Windows registry to find python installs
|
|
|
28
30
|
Based on PEP 514 registry entries.
|
|
29
31
|
"""
|
|
30
32
|
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
31
35
|
import os.path
|
|
32
|
-
import winreg
|
|
36
|
+
import winreg
|
|
33
37
|
from _collections_abc import Iterator
|
|
34
38
|
|
|
35
39
|
from ..shared import DetailFinder, PythonInstall, version_str_to_tuple
|
|
@@ -12,6 +12,7 @@ src/ducktools/pythonfinder/__main__.py
|
|
|
12
12
|
src/ducktools/pythonfinder/_version.py
|
|
13
13
|
src/ducktools/pythonfinder/details_script.py
|
|
14
14
|
src/ducktools/pythonfinder/package_list_script.py
|
|
15
|
+
src/ducktools/pythonfinder/py.typed
|
|
15
16
|
src/ducktools/pythonfinder/pythonorg_search.py
|
|
16
17
|
src/ducktools/pythonfinder/shared.py
|
|
17
18
|
src/ducktools/pythonfinder/venv.py
|
|
@@ -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
|
|
@@ -138,7 +138,7 @@ class TestUVReal:
|
|
|
138
138
|
assert len(pythons) == 1
|
|
139
139
|
assert pythons[0].version_str == "3.12.6"
|
|
140
140
|
assert pythons[0].implementation == "cpython"
|
|
141
|
-
assert pythons[0].managed_by == "Astral
|
|
141
|
+
assert pythons[0].managed_by == "Astral"
|
|
142
142
|
|
|
143
143
|
@pytest.mark.uv_python
|
|
144
144
|
def test_finds_installed_pypy(self, uv_pythondir, temp_finder):
|
|
@@ -152,7 +152,7 @@ class TestUVReal:
|
|
|
152
152
|
assert pythons[0].version >= (3, 10, 14)
|
|
153
153
|
assert pythons[0].implementation == "pypy"
|
|
154
154
|
assert pythons[0].implementation_version >= (7, 3, 17)
|
|
155
|
-
assert pythons[0].managed_by == "Astral
|
|
155
|
+
assert pythons[0].managed_by == "Astral"
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def test_regex_matches():
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
|
|
24
|
-
"""Currently just copied from linux"""
|
|
25
|
-
|
|
26
|
-
from ..linux import get_python_installs, get_path_pythons, get_pyenv_pythons
|
|
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.9.1 → ducktools_pythonfinder-0.9.3}/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
|
{ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/sources/python_versions.txt
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.9.1 → ducktools_pythonfinder-0.9.3}/tests/sources/release_file.json
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
|