ducktools-pythonfinder 0.9.2__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ducktools_pythonfinder-0.9.2/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.10.0}/PKG-INFO +5 -11
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/pyproject.toml +12 -9
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/__init__.py +1 -1
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/__main__.py +38 -30
- ducktools_pythonfinder-0.10.0/src/ducktools/pythonfinder/_version.py +2 -0
- ducktools_pythonfinder-0.10.0/src/ducktools/pythonfinder/darwin/__init__.py +74 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/linux/__init__.py +16 -4
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/linux/pyenv_search.py +9 -5
- ducktools_pythonfinder-0.10.0/src/ducktools/pythonfinder/py.typed +1 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/pythonorg_search.py +4 -1
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/shared.py +19 -15
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/venv.py +24 -13
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/win32/__init__.py +5 -1
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/win32/pyenv_search.py +6 -2
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/win32/registry_search.py +6 -2
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0/src/ducktools_pythonfinder.egg-info}/PKG-INFO +5 -11
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +2 -0
- ducktools_pythonfinder-0.10.0/src/ducktools_pythonfinder.egg-info/requires.txt +3 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/conftest.py +7 -9
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_orgsearch.py +4 -1
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_venv_finder.py +20 -7
- ducktools_pythonfinder-0.10.0/uv.lock +325 -0
- ducktools_pythonfinder-0.9.2/src/ducktools/pythonfinder/_version.py +0 -2
- ducktools_pythonfinder-0.9.2/src/ducktools/pythonfinder/darwin/__init__.py +0 -26
- ducktools_pythonfinder-0.9.2/src/ducktools_pythonfinder.egg-info/requires.txt +0 -8
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/.gitignore +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/LICENSE +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/README.md +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/scripts/list_python_venvs.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/details_script.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/package_list_script.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/sources/release.json +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/sources/release_file.json +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_cache.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_foldersearch.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_pyenv.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_shared.py +0 -0
- {ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/tests/test_uv_finder.py +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ducktools-pythonfinder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Cross platform tool to find available python installations
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
Project-URL: Homepage, https://github.com/davidcellis/ducktools-pythonfinder
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
8
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -15,16 +13,12 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
15
13
|
Classifier: Operating System :: MacOS
|
|
16
14
|
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
17
15
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.10
|
|
19
17
|
Description-Content-Type: text/markdown
|
|
20
18
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: ducktools-lazyimporter
|
|
22
|
-
Requires-Dist: ducktools-classbuilder>=0.
|
|
23
|
-
Requires-Dist: packaging>=
|
|
24
|
-
Provides-Extra: testing
|
|
25
|
-
Requires-Dist: pytest; extra == "testing"
|
|
26
|
-
Requires-Dist: pytest-cov; extra == "testing"
|
|
27
|
-
Requires-Dist: pyfakefs; extra == "testing"
|
|
19
|
+
Requires-Dist: ducktools-lazyimporter>=0.7.3
|
|
20
|
+
Requires-Dist: ducktools-classbuilder>=0.9.1
|
|
21
|
+
Requires-Dist: packaging>=24.2
|
|
28
22
|
Dynamic: license-file
|
|
29
23
|
|
|
30
24
|
# ducktools: pythonfinder #
|
|
@@ -12,16 +12,14 @@ authors = [
|
|
|
12
12
|
{ name = "David C Ellis" },
|
|
13
13
|
]
|
|
14
14
|
readme = "README.md"
|
|
15
|
-
requires-python = ">=3.
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
16
|
dependencies = [
|
|
17
|
-
"ducktools-lazyimporter",
|
|
18
|
-
"ducktools-classbuilder>=0.
|
|
19
|
-
"packaging>=
|
|
17
|
+
"ducktools-lazyimporter>=0.7.3",
|
|
18
|
+
"ducktools-classbuilder>=0.9.1",
|
|
19
|
+
"packaging>=24.2",
|
|
20
20
|
]
|
|
21
21
|
classifiers = [
|
|
22
22
|
"Development Status :: 3 - Alpha",
|
|
23
|
-
"Programming Language :: Python :: 3.8",
|
|
24
|
-
"Programming Language :: Python :: 3.9",
|
|
25
23
|
"Programming Language :: Python :: 3.10",
|
|
26
24
|
"Programming Language :: Python :: 3.11",
|
|
27
25
|
"Programming Language :: Python :: 3.12",
|
|
@@ -36,12 +34,17 @@ dynamic = ['version']
|
|
|
36
34
|
[project.urls]
|
|
37
35
|
"Homepage" = "https://github.com/davidcellis/ducktools-pythonfinder"
|
|
38
36
|
|
|
39
|
-
[project.optional-dependencies]
|
|
40
|
-
testing = ["pytest", "pytest-cov", "pyfakefs"]
|
|
41
|
-
|
|
42
37
|
[project.scripts]
|
|
43
38
|
"ducktools-pythonfinder" = "ducktools.pythonfinder.__main__:main"
|
|
44
39
|
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=8.4",
|
|
43
|
+
"pytest-cov>=6.1",
|
|
44
|
+
"pyfakefs>=5.8",
|
|
45
|
+
"mypy>=1.16",
|
|
46
|
+
]
|
|
47
|
+
|
|
45
48
|
[tool.setuptools.packages.find]
|
|
46
49
|
where = ["src"]
|
|
47
50
|
|
|
@@ -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),
|
|
@@ -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,12 +300,12 @@ def display_remote_binaries(
|
|
|
294
300
|
print("No Python releases found matching specification")
|
|
295
301
|
|
|
296
302
|
|
|
297
|
-
def main():
|
|
298
|
-
if sys.version_info < (3,
|
|
303
|
+
def main() -> int:
|
|
304
|
+
if sys.version_info < (3, 10):
|
|
299
305
|
v = sys.version_info
|
|
300
306
|
raise UnsupportedPythonError(
|
|
301
307
|
f"Python {v.major}.{v.minor}.{v.micro} is not supported. "
|
|
302
|
-
f"ducktools.pythonfinder requires Python 3.
|
|
308
|
+
f"ducktools.pythonfinder requires Python 3.10 or later."
|
|
303
309
|
)
|
|
304
310
|
|
|
305
311
|
if sys.argv[1:]:
|
|
@@ -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)
|
|
@@ -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.2 → ducktools_pythonfinder-0.10.0}/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,
|
|
@@ -373,7 +378,6 @@ class PythonInstall(Prefab):
|
|
|
373
378
|
metadata = {} if metadata is None else metadata
|
|
374
379
|
paths = {} if paths is None else paths
|
|
375
380
|
|
|
376
|
-
# noinspection PyArgumentList
|
|
377
381
|
return cls(
|
|
378
382
|
version=version_tuple,
|
|
379
383
|
executable=executable,
|
|
@@ -387,20 +391,19 @@ class PythonInstall(Prefab):
|
|
|
387
391
|
@classmethod
|
|
388
392
|
def from_json(
|
|
389
393
|
cls,
|
|
390
|
-
version,
|
|
391
|
-
executable,
|
|
392
|
-
architecture,
|
|
393
|
-
implementation,
|
|
394
|
-
metadata,
|
|
395
|
-
paths=None,
|
|
396
|
-
managed_by=None,
|
|
394
|
+
version: list[int | str], # This is actually the list version of [int, int, int, str, int]
|
|
395
|
+
executable: str,
|
|
396
|
+
architecture: str,
|
|
397
|
+
implementation: str,
|
|
398
|
+
metadata: dict,
|
|
399
|
+
paths: dict | None = None,
|
|
400
|
+
managed_by: str | None = None,
|
|
397
401
|
):
|
|
398
402
|
if arch_ver := metadata.get(f"{implementation}_version"):
|
|
399
403
|
metadata[f"{implementation}_version"] = tuple(arch_ver)
|
|
400
404
|
|
|
401
405
|
paths = {} if paths is None else paths
|
|
402
406
|
|
|
403
|
-
# noinspection PyArgumentList
|
|
404
407
|
return cls(
|
|
405
408
|
version=tuple(version), # type: ignore
|
|
406
409
|
executable=executable,
|
|
@@ -431,6 +434,7 @@ class PythonInstall(Prefab):
|
|
|
431
434
|
return pip_call.stdout
|
|
432
435
|
|
|
433
436
|
|
|
437
|
+
# Return type missing due to import requirements
|
|
434
438
|
def _python_exe_regex(basename: str = "python"):
|
|
435
439
|
if sys.platform == "win32":
|
|
436
440
|
return _laz.re.compile(rf"{basename}\d?\.?\d*\.exe")
|
|
@@ -443,7 +447,7 @@ def get_folder_pythons(
|
|
|
443
447
|
basenames: tuple[str, ...] = ("python", "pypy", "micropython"),
|
|
444
448
|
finder: DetailFinder | None = None,
|
|
445
449
|
managed_by: str | None = None,
|
|
446
|
-
):
|
|
450
|
+
) -> Iterator[PythonInstall]:
|
|
447
451
|
regexes = [_python_exe_regex(name) for name in basenames]
|
|
448
452
|
|
|
449
453
|
finder = DetailFinder() if finder is None else finder
|
{ducktools_pythonfinder-0.9.2 → ducktools_pythonfinder-0.10.0}/src/ducktools/pythonfinder/venv.py
RENAMED
|
@@ -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
|
|
@@ -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,17 +83,23 @@ 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
|
|
91
90
|
if sys.platform == "win32":
|
|
92
91
|
parent_exe = os.path.join(self.parent_path, "python.exe")
|
|
93
92
|
else:
|
|
94
|
-
# try with additional numbers in order eg: python313, python3, python
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
# try with additional numbers in order eg: python3.13, python313, python3, python
|
|
94
|
+
suffixes = [
|
|
95
|
+
f"{self.version[0]}.{self.version[1]}",
|
|
96
|
+
f"{self.version[0]}{self.version[1]}",
|
|
97
|
+
f"{self.version[0]}",
|
|
98
|
+
""
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for suffix in suffixes:
|
|
102
|
+
parent_exe = os.path.join(self.parent_path, f"python{suffix}")
|
|
98
103
|
if os.path.exists(parent_exe):
|
|
99
104
|
break
|
|
100
105
|
|
|
@@ -122,7 +127,9 @@ class PythonVEnv(Prefab):
|
|
|
122
127
|
|
|
123
128
|
@property
|
|
124
129
|
def parent_exists(self) -> bool:
|
|
125
|
-
|
|
130
|
+
if self.parent_executable and os.path.exists(self.parent_executable):
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
126
133
|
|
|
127
134
|
def get_parent_install(
|
|
128
135
|
self,
|
|
@@ -135,6 +142,9 @@ class PythonVEnv(Prefab):
|
|
|
135
142
|
finder = DetailFinder() if finder is None else finder
|
|
136
143
|
|
|
137
144
|
if self.parent_exists:
|
|
145
|
+
# parent_exists forces this check
|
|
146
|
+
assert self.parent_executable is not None
|
|
147
|
+
|
|
138
148
|
exe = self.parent_executable
|
|
139
149
|
|
|
140
150
|
# Python installs may be cached, can skip querying exe.
|
|
@@ -253,6 +263,7 @@ def get_python_venvs(
|
|
|
253
263
|
:param search_parent_folders: Also search parent folders
|
|
254
264
|
:yield: PythonVEnv details.
|
|
255
265
|
"""
|
|
266
|
+
# This converts base_dir to a Path, but mypy doesn't know that
|
|
256
267
|
base_dir = _laz.Path.cwd() if base_dir is None else _laz.Path(base_dir)
|
|
257
268
|
|
|
258
269
|
cwd_pattern = pattern = f"*/{VENV_CONFIG_NAME}"
|
|
@@ -261,7 +272,7 @@ def get_python_venvs(
|
|
|
261
272
|
# Only search cwd recursively, parents are searched non-recursively
|
|
262
273
|
cwd_pattern = "*" + pattern
|
|
263
274
|
|
|
264
|
-
for conf in base_dir.glob(cwd_pattern):
|
|
275
|
+
for conf in base_dir.glob(cwd_pattern): # type: ignore
|
|
265
276
|
try:
|
|
266
277
|
env = PythonVEnv.from_cfg(conf)
|
|
267
278
|
except InvalidVEnvError:
|
|
@@ -270,7 +281,7 @@ def get_python_venvs(
|
|
|
270
281
|
|
|
271
282
|
if search_parent_folders:
|
|
272
283
|
# Search parent folders
|
|
273
|
-
for fld in base_dir.parents:
|
|
284
|
+
for fld in base_dir.parents: # type: ignore
|
|
274
285
|
try:
|
|
275
286
|
for conf in fld.glob(pattern):
|
|
276
287
|
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
|