ducktools-pythonfinder 0.6.7__tar.gz → 0.7.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.6.7/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.7.0}/PKG-INFO +13 -2
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/README.md +11 -0
- ducktools_pythonfinder-0.7.0/src/ducktools/pythonfinder/_version.py +2 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/linux/pyenv_search.py +2 -1
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/shared.py +16 -5
- ducktools_pythonfinder-0.7.0/src/ducktools/pythonfinder/venv.py +196 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/win32/pyenv_search.py +4 -2
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/win32/registry_search.py +1 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0/src/ducktools_pythonfinder.egg-info}/PKG-INFO +13 -2
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +2 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/conftest.py +23 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_pyenv.py +7 -4
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_uv_finder.py +2 -0
- ducktools_pythonfinder-0.7.0/tests/test_venv_finder.py +118 -0
- ducktools_pythonfinder-0.6.7/src/ducktools/pythonfinder/_version.py +0 -2
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/.gitignore +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/LICENSE +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/MANIFEST.in +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/pyproject.toml +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/build_zipapp.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/detail_this_python.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/print_python_versions.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/setup.cfg +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/__init__.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/__main__.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/details_script.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/linux/__init__.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/win32/__init__.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/python_versions.txt +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/release.json +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/release_file.json +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_details_script.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_foldersearch.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_orgsearch.py +0 -0
- {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_shared.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: ducktools-pythonfinder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -124,6 +124,17 @@ PythonInstall(version=(3, 8, 10, 'final', 0), executable='~\\.pyenv\\pyenv-win\\
|
|
|
124
124
|
PythonInstall(version=(3, 13, 0, 'candidate', 1), executable='~\\.pyenv\\pyenv-win\\versions\\3.13.0rc1\\python.exe', architecture='64bit', implementation='cpython', metadata={}, shadowed=False)```
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Finding venvs ###
|
|
128
|
+
|
|
129
|
+
There is now a submodule to search for virtual environments.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from ducktools.pythonfinder.venv import list_python_venvs
|
|
133
|
+
|
|
134
|
+
for venv in list_python_venvs():
|
|
135
|
+
print(venv.executable)
|
|
136
|
+
```
|
|
137
|
+
|
|
127
138
|
### Python.org search ###
|
|
128
139
|
|
|
129
140
|
Python.org searches are handled by the `ducktools.pythonfinder.pythonorg_search` module.
|
|
@@ -96,6 +96,17 @@ PythonInstall(version=(3, 8, 10, 'final', 0), executable='~\\.pyenv\\pyenv-win\\
|
|
|
96
96
|
PythonInstall(version=(3, 13, 0, 'candidate', 1), executable='~\\.pyenv\\pyenv-win\\versions\\3.13.0rc1\\python.exe', architecture='64bit', implementation='cpython', metadata={}, shadowed=False)```
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
### Finding venvs ###
|
|
100
|
+
|
|
101
|
+
There is now a submodule to search for virtual environments.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from ducktools.pythonfinder.venv import list_python_venvs
|
|
105
|
+
|
|
106
|
+
for venv in list_python_venvs():
|
|
107
|
+
print(venv.executable)
|
|
108
|
+
```
|
|
109
|
+
|
|
99
110
|
### Python.org search ###
|
|
100
111
|
|
|
101
112
|
Python.org searches are handled by the `ducktools.pythonfinder.pythonorg_search` module.
|
|
@@ -78,6 +78,7 @@ def get_pyenv_pythons(
|
|
|
78
78
|
version=version_tuple,
|
|
79
79
|
executable=executable,
|
|
80
80
|
metadata=metadata,
|
|
81
|
+
managed_by="pyenv",
|
|
81
82
|
)
|
|
82
|
-
elif query_executables and (install := get_install_details(executable)):
|
|
83
|
+
elif query_executables and (install := get_install_details(executable, managed_by="pyenv")):
|
|
83
84
|
yield install
|
{ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/shared.py
RENAMED
|
@@ -137,6 +137,7 @@ class PythonInstall(Prefab):
|
|
|
137
137
|
executable: str
|
|
138
138
|
architecture: str = "64bit"
|
|
139
139
|
implementation: str = "cpython"
|
|
140
|
+
managed_by: str | None = None
|
|
140
141
|
metadata: dict = attribute(default_factory=dict)
|
|
141
142
|
shadowed: bool = False
|
|
142
143
|
_implementation_version: tuple[int, int, int, str, int] | None = attribute(default=None, private=True)
|
|
@@ -180,6 +181,7 @@ class PythonInstall(Prefab):
|
|
|
180
181
|
executable: str,
|
|
181
182
|
architecture: str = "64bit",
|
|
182
183
|
implementation: str = "cpython",
|
|
184
|
+
managed_by: str | None = None,
|
|
183
185
|
metadata: dict | None = None,
|
|
184
186
|
):
|
|
185
187
|
version_tuple = version_str_to_tuple(version)
|
|
@@ -190,16 +192,23 @@ class PythonInstall(Prefab):
|
|
|
190
192
|
executable=executable,
|
|
191
193
|
architecture=architecture,
|
|
192
194
|
implementation=implementation,
|
|
195
|
+
managed_by=managed_by,
|
|
193
196
|
metadata=metadata,
|
|
194
197
|
)
|
|
195
198
|
|
|
196
199
|
@classmethod
|
|
197
|
-
def from_json(cls, version, executable, architecture, implementation, metadata):
|
|
200
|
+
def from_json(cls, version, executable, architecture, implementation, metadata, managed_by=None):
|
|
198
201
|
if arch_ver := metadata.get(f"{implementation}_version"):
|
|
199
202
|
metadata[f"{implementation}_version"] = tuple(arch_ver)
|
|
200
203
|
|
|
204
|
+
# noinspection PyArgumentList
|
|
201
205
|
return cls(
|
|
202
|
-
tuple(version),
|
|
206
|
+
version=tuple(version),
|
|
207
|
+
executable=executable,
|
|
208
|
+
architecture=architecture,
|
|
209
|
+
implementation=implementation,
|
|
210
|
+
managed_by=managed_by,
|
|
211
|
+
metadata=metadata,
|
|
203
212
|
)
|
|
204
213
|
|
|
205
214
|
def get_pip_version(self) -> str | None:
|
|
@@ -229,7 +238,7 @@ def _python_exe_regex(basename: str = "python"):
|
|
|
229
238
|
return _laz.re.compile(rf"{basename}\d?\.?\d*")
|
|
230
239
|
|
|
231
240
|
|
|
232
|
-
def get_install_details(executable: str) -> PythonInstall | None:
|
|
241
|
+
def get_install_details(executable: str, managed_by=None) -> PythonInstall | None:
|
|
233
242
|
try:
|
|
234
243
|
source = details.get_source_code()
|
|
235
244
|
except FileNotFoundError:
|
|
@@ -270,7 +279,7 @@ def get_install_details(executable: str) -> PythonInstall | None:
|
|
|
270
279
|
except _laz.json.JSONDecodeError as e:
|
|
271
280
|
return None
|
|
272
281
|
|
|
273
|
-
return PythonInstall.from_json(**output)
|
|
282
|
+
return PythonInstall.from_json(**output, managed_by=managed_by)
|
|
274
283
|
|
|
275
284
|
|
|
276
285
|
def get_folder_pythons(
|
|
@@ -344,7 +353,8 @@ def _implementation_from_uv_dir(
|
|
|
344
353
|
executable=python_path,
|
|
345
354
|
architecture="32bit" if arch in {"i686", "armv7"} else "64bit",
|
|
346
355
|
implementation=implementation,
|
|
347
|
-
metadata=metadata
|
|
356
|
+
metadata=metadata,
|
|
357
|
+
managed_by="Astral UV",
|
|
348
358
|
)
|
|
349
359
|
except ValueError:
|
|
350
360
|
pass
|
|
@@ -354,6 +364,7 @@ def _implementation_from_uv_dir(
|
|
|
354
364
|
# Slow backup - ask python itself
|
|
355
365
|
if query_executables:
|
|
356
366
|
install = get_install_details(python_path)
|
|
367
|
+
install.managed_by = "Astral UV"
|
|
357
368
|
|
|
358
369
|
return install
|
|
359
370
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# MIT License
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2013-2014 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 sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
from ducktools.classbuilder.prefab import Prefab
|
|
30
|
+
from ducktools.lazyimporter import LazyImporter, FromImport, ModuleImport
|
|
31
|
+
|
|
32
|
+
from .shared import (
|
|
33
|
+
PythonInstall,
|
|
34
|
+
get_install_details,
|
|
35
|
+
version_str_to_tuple,
|
|
36
|
+
version_tuple_to_str,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_laz = LazyImporter(
|
|
41
|
+
[
|
|
42
|
+
ModuleImport("re"),
|
|
43
|
+
ModuleImport("json"),
|
|
44
|
+
FromImport("pathlib", "Path"),
|
|
45
|
+
FromImport("subprocess", "run"),
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
VENV_CONFIG_NAME = "pyvenv.cfg"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# VIRTUALENV can make some invalid regexes that are just the tuple with dots.
|
|
53
|
+
VIRTUALENV_PY_VER_RE = (
|
|
54
|
+
r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)\.(?P<releaselevel>.+)\.(?P<serial>\d*)?"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PythonPackage(Prefab):
|
|
59
|
+
name: str
|
|
60
|
+
version: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PythonVEnv(Prefab):
|
|
64
|
+
folder: str
|
|
65
|
+
executable: str
|
|
66
|
+
version: tuple[int, int, int, str, int]
|
|
67
|
+
parent_path: str
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def version_str(self) -> str:
|
|
71
|
+
return version_tuple_to_str(self.version)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def parent_executable(self) -> str:
|
|
75
|
+
if sys.platform == "win32":
|
|
76
|
+
return os.path.join(self.parent_path, "python.exe")
|
|
77
|
+
else:
|
|
78
|
+
return os.path.join(self.parent_path, "python")
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def parent_exists(self) -> bool:
|
|
82
|
+
return os.path.exists(self.parent_executable)
|
|
83
|
+
|
|
84
|
+
def get_parent_install(self, cache: list[PythonInstall] | None = None) -> PythonInstall | None:
|
|
85
|
+
install = None
|
|
86
|
+
cache = [] if cache is None else cache
|
|
87
|
+
|
|
88
|
+
if self.parent_exists:
|
|
89
|
+
exe = self.parent_executable
|
|
90
|
+
|
|
91
|
+
# Python installs may be cached, can skip querying exe.
|
|
92
|
+
for inst in cache:
|
|
93
|
+
if os.path.samefile(inst.executable, exe):
|
|
94
|
+
install = inst
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
if install is None:
|
|
98
|
+
install = get_install_details(exe)
|
|
99
|
+
|
|
100
|
+
return install
|
|
101
|
+
|
|
102
|
+
def list_packages(self):
|
|
103
|
+
if not self.parent_exists:
|
|
104
|
+
raise FileNotFoundError(
|
|
105
|
+
f"Parent Python at \"{self.parent_executable}\" does not exist."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Should probably use sys.executable and have pip as a dependency
|
|
109
|
+
# We would need to look at possibly changing how ducktools-env works for that however.
|
|
110
|
+
|
|
111
|
+
data = _laz.run(
|
|
112
|
+
[
|
|
113
|
+
self.parent_executable,
|
|
114
|
+
"-m", "pip",
|
|
115
|
+
"--python", self.executable,
|
|
116
|
+
"list",
|
|
117
|
+
"--format", "json"
|
|
118
|
+
],
|
|
119
|
+
capture_output=True,
|
|
120
|
+
text=True,
|
|
121
|
+
check=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
raw_packages = _laz.json.loads(data.stdout)
|
|
125
|
+
|
|
126
|
+
packages = [
|
|
127
|
+
PythonPackage(
|
|
128
|
+
name=p["name"],
|
|
129
|
+
version=p["version"],
|
|
130
|
+
)
|
|
131
|
+
for p in raw_packages
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
return packages
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_python_venvs(base_dir=None, recursive=True):
|
|
138
|
+
base_dir = os.getcwd() if base_dir is None else base_dir
|
|
139
|
+
|
|
140
|
+
if recursive:
|
|
141
|
+
glob_call = _laz.Path(base_dir).glob(f"**/{VENV_CONFIG_NAME}")
|
|
142
|
+
else:
|
|
143
|
+
glob_call = _laz.Path(base_dir).glob(f"*/{VENV_CONFIG_NAME}")
|
|
144
|
+
|
|
145
|
+
for conf in glob_call:
|
|
146
|
+
parent_path, version_str = None, None
|
|
147
|
+
venv_base = conf.parent
|
|
148
|
+
|
|
149
|
+
with conf.open() as f:
|
|
150
|
+
for line in f:
|
|
151
|
+
key, value = (item.strip() for item in line.split("="))
|
|
152
|
+
|
|
153
|
+
if key == "home":
|
|
154
|
+
parent_path = value
|
|
155
|
+
elif key in {"version", "version_info"}:
|
|
156
|
+
# venv and uv use different key names :)
|
|
157
|
+
version_str = value
|
|
158
|
+
|
|
159
|
+
if parent_path and version_str:
|
|
160
|
+
break
|
|
161
|
+
else:
|
|
162
|
+
# Not a valid venv, ignore
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if sys.platform == "win32":
|
|
166
|
+
venv_exe = os.path.join(venv_base, "Scripts", "python.exe")
|
|
167
|
+
else:
|
|
168
|
+
venv_exe = os.path.join(venv_base, "bin", "python")
|
|
169
|
+
|
|
170
|
+
version_tuple = None
|
|
171
|
+
try:
|
|
172
|
+
version_tuple = version_str_to_tuple(version_str)
|
|
173
|
+
except ValueError: # pragma: no cover
|
|
174
|
+
# Might be virtualenv putting in incorrect versions
|
|
175
|
+
parsed_version = _laz.re.fullmatch(VIRTUALENV_PY_VER_RE, version_str)
|
|
176
|
+
if parsed_version:
|
|
177
|
+
major, minor, micro, releaselevel, serial = parsed_version.groups()
|
|
178
|
+
version_tuple = (
|
|
179
|
+
int(major),
|
|
180
|
+
int(minor),
|
|
181
|
+
int(micro) if micro else 0,
|
|
182
|
+
releaselevel,
|
|
183
|
+
int(serial if serial != "" else 0),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if version_tuple is not None:
|
|
187
|
+
yield PythonVEnv(
|
|
188
|
+
folder=venv_base,
|
|
189
|
+
executable=venv_exe,
|
|
190
|
+
version=version_tuple,
|
|
191
|
+
parent_path=parent_path
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def list_python_venvs(base_dir=None, recursive=True) -> list[PythonVEnv]:
|
|
196
|
+
return list(get_python_venvs(base_dir=base_dir, recursive=recursive))
|
|
@@ -48,13 +48,13 @@ def get_pyenv_pythons(
|
|
|
48
48
|
if path_base.startswith("pypy"):
|
|
49
49
|
executable = os.path.join(p.path, "pypy.exe")
|
|
50
50
|
if os.path.exists(executable):
|
|
51
|
-
yield get_install_details(executable)
|
|
51
|
+
yield get_install_details(executable, managed_by="pyenv")
|
|
52
52
|
continue
|
|
53
53
|
elif path_base.startswith("graalpy"):
|
|
54
54
|
# Graalpy exe in bin subfolder
|
|
55
55
|
executable = os.path.join(p.path, "bin", "graalpy.exe")
|
|
56
56
|
if os.path.exists(executable):
|
|
57
|
-
yield get_install_details(executable)
|
|
57
|
+
yield get_install_details(executable, managed_by="pyenv")
|
|
58
58
|
continue
|
|
59
59
|
|
|
60
60
|
# Regular CPython
|
|
@@ -76,6 +76,7 @@ def get_pyenv_pythons(
|
|
|
76
76
|
version=version,
|
|
77
77
|
executable=executable,
|
|
78
78
|
architecture=arch,
|
|
79
|
+
managed_by="pyenv",
|
|
79
80
|
)
|
|
80
81
|
except ValueError:
|
|
81
82
|
pass
|
|
@@ -86,6 +87,7 @@ def get_pyenv_pythons(
|
|
|
86
87
|
version=version,
|
|
87
88
|
executable=executable,
|
|
88
89
|
architecture="64bit",
|
|
90
|
+
managed_by="pyenv",
|
|
89
91
|
)
|
|
90
92
|
except ValueError:
|
|
91
93
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: ducktools-pythonfinder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -124,6 +124,17 @@ PythonInstall(version=(3, 8, 10, 'final', 0), executable='~\\.pyenv\\pyenv-win\\
|
|
|
124
124
|
PythonInstall(version=(3, 13, 0, 'candidate', 1), executable='~\\.pyenv\\pyenv-win\\versions\\3.13.0rc1\\python.exe', architecture='64bit', implementation='cpython', metadata={}, shadowed=False)```
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Finding venvs ###
|
|
128
|
+
|
|
129
|
+
There is now a submodule to search for virtual environments.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from ducktools.pythonfinder.venv import list_python_venvs
|
|
133
|
+
|
|
134
|
+
for venv in list_python_venvs():
|
|
135
|
+
print(venv.executable)
|
|
136
|
+
```
|
|
137
|
+
|
|
127
138
|
### Python.org search ###
|
|
128
139
|
|
|
129
140
|
Python.org searches are handled by the `ducktools.pythonfinder.pythonorg_search` module.
|
|
@@ -12,6 +12,7 @@ src/ducktools/pythonfinder/_version.py
|
|
|
12
12
|
src/ducktools/pythonfinder/details_script.py
|
|
13
13
|
src/ducktools/pythonfinder/pythonorg_search.py
|
|
14
14
|
src/ducktools/pythonfinder/shared.py
|
|
15
|
+
src/ducktools/pythonfinder/venv.py
|
|
15
16
|
src/ducktools/pythonfinder/darwin/__init__.py
|
|
16
17
|
src/ducktools/pythonfinder/linux/__init__.py
|
|
17
18
|
src/ducktools/pythonfinder/linux/pyenv_search.py
|
|
@@ -31,6 +32,7 @@ tests/test_orgsearch.py
|
|
|
31
32
|
tests/test_pyenv.py
|
|
32
33
|
tests/test_shared.py
|
|
33
34
|
tests/test_uv_finder.py
|
|
35
|
+
tests/test_venv_finder.py
|
|
34
36
|
tests/sources/python_versions.txt
|
|
35
37
|
tests/sources/release.json
|
|
36
38
|
tests/sources/release_file.json
|
|
@@ -20,11 +20,14 @@
|
|
|
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
|
+
import sys
|
|
24
|
+
|
|
23
25
|
from pathlib import Path
|
|
24
26
|
|
|
25
27
|
import pytest
|
|
26
28
|
|
|
27
29
|
from ducktools.pythonfinder import details_script
|
|
30
|
+
from ducktools.pythonfinder.shared import get_install_details
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
@pytest.fixture(scope="session")
|
|
@@ -37,6 +40,26 @@ def uses_details_script(fs):
|
|
|
37
40
|
fs.add_real_file(details_script.__file__)
|
|
38
41
|
|
|
39
42
|
|
|
43
|
+
@pytest.fixture(scope="session")
|
|
44
|
+
def this_python():
|
|
45
|
+
if sys.platform == "win32":
|
|
46
|
+
py_exe = Path(sys.base_prefix) / "python.exe"
|
|
47
|
+
else:
|
|
48
|
+
py_exe = Path(sys.base_prefix) / "bin" / "python"
|
|
49
|
+
|
|
50
|
+
return get_install_details(str(py_exe))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture(scope="session")
|
|
54
|
+
def this_venv():
|
|
55
|
+
if sys.platform == "win32":
|
|
56
|
+
exe = sys.executable
|
|
57
|
+
else:
|
|
58
|
+
exe = str(Path(sys.executable).with_name("python"))
|
|
59
|
+
venv = get_install_details(exe)
|
|
60
|
+
return venv
|
|
61
|
+
|
|
62
|
+
|
|
40
63
|
def pytest_addoption(parser):
|
|
41
64
|
parser.addoption(
|
|
42
65
|
"--run-uv-python",
|
|
@@ -72,7 +72,7 @@ def test_mock_versions_folder():
|
|
|
72
72
|
|
|
73
73
|
python_versions = list(get_pyenv_pythons())
|
|
74
74
|
|
|
75
|
-
assert python_versions == [PythonInstall.from_str(version=out_ver, executable=out_executable)]
|
|
75
|
+
assert python_versions == [PythonInstall.from_str(version=out_ver, executable=out_executable, managed_by="pyenv")]
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
@pytest.mark.skipif(sys.platform != "win32", reason="Test for Windows only")
|
|
@@ -89,7 +89,7 @@ def test_fs_versions_win(fs):
|
|
|
89
89
|
|
|
90
90
|
versions = list(get_pyenv_pythons(tmpdir))
|
|
91
91
|
|
|
92
|
-
assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe)]
|
|
92
|
+
assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe, managed_by="pyenv")]
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
@pytest.mark.skipif(sys.platform != "win32", reason="Test for Windows only")
|
|
@@ -106,7 +106,9 @@ def test_32bit_version(fs):
|
|
|
106
106
|
|
|
107
107
|
versions = list(get_pyenv_pythons(tmpdir))
|
|
108
108
|
|
|
109
|
-
assert versions == [
|
|
109
|
+
assert versions == [
|
|
110
|
+
PythonInstall.from_str(version="3.12.1", executable=py_exe, architecture="32bit", managed_by="pyenv")
|
|
111
|
+
]
|
|
110
112
|
|
|
111
113
|
|
|
112
114
|
@pytest.mark.skipif(sys.platform != "win32", reason="Test for Windows only")
|
|
@@ -155,7 +157,7 @@ def test_fs_versions_nix(fs):
|
|
|
155
157
|
|
|
156
158
|
versions = list(get_pyenv_pythons(tmpdir))
|
|
157
159
|
|
|
158
|
-
assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe)]
|
|
160
|
+
assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe, managed_by="pyenv")]
|
|
159
161
|
|
|
160
162
|
|
|
161
163
|
@pytest.mark.skipif(sys.platform == "win32", reason="Test for non-Windows only")
|
|
@@ -235,6 +237,7 @@ def test_pypy_version(fs):
|
|
|
235
237
|
architecture="64bit",
|
|
236
238
|
implementation="pypy",
|
|
237
239
|
metadata={"pypy_version": (7, 3, 15, "final", 0)},
|
|
240
|
+
managed_by="pyenv",
|
|
238
241
|
)
|
|
239
242
|
|
|
240
243
|
assert versions == [out_version]
|
|
@@ -132,6 +132,7 @@ class TestUVReal:
|
|
|
132
132
|
assert len(pythons) == 1
|
|
133
133
|
assert pythons[0].version_str == "3.12.6"
|
|
134
134
|
assert pythons[0].implementation == "cpython"
|
|
135
|
+
assert pythons[0].managed_by == "Astral UV"
|
|
135
136
|
|
|
136
137
|
@pytest.mark.uv_python
|
|
137
138
|
def test_finds_installed_pypy(self, uv_pythondir):
|
|
@@ -145,6 +146,7 @@ class TestUVReal:
|
|
|
145
146
|
assert pythons[0].version >= (3, 10, 14)
|
|
146
147
|
assert pythons[0].implementation == "pypy"
|
|
147
148
|
assert pythons[0].implementation_version >= (7, 3, 17)
|
|
149
|
+
assert pythons[0].managed_by == "Astral UV"
|
|
148
150
|
|
|
149
151
|
|
|
150
152
|
def test_regex_matches():
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ducktools-pythonfinder
|
|
2
|
+
# MIT License
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2013-2014 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
|
+
import os
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
|
|
28
|
+
from ducktools.pythonfinder.venv import list_python_venvs
|
|
29
|
+
|
|
30
|
+
import pytest
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture(scope="module")
|
|
34
|
+
def with_venvs():
|
|
35
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
36
|
+
# We can't actually use venv directly here as
|
|
37
|
+
# Older python on linux makes invalid venvs
|
|
38
|
+
if sys.platform == "win32":
|
|
39
|
+
python_exe = os.path.join(sys.base_prefix, "python.exe")
|
|
40
|
+
else:
|
|
41
|
+
python_exe = os.path.join(sys.base_prefix, "bin", "python")
|
|
42
|
+
|
|
43
|
+
def make_venv(pth):
|
|
44
|
+
subprocess.run(
|
|
45
|
+
[
|
|
46
|
+
python_exe,
|
|
47
|
+
"-m", "venv",
|
|
48
|
+
"--without-pip",
|
|
49
|
+
os.path.join(tmpdir, pth),
|
|
50
|
+
],
|
|
51
|
+
check=True,
|
|
52
|
+
capture_output=True
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
make_venv(".venv")
|
|
56
|
+
make_venv("subfolder/.venv")
|
|
57
|
+
make_venv("subfolder/subsubfolder/env")
|
|
58
|
+
|
|
59
|
+
assert os.path.exists(os.path.join(tmpdir, ".venv"))
|
|
60
|
+
|
|
61
|
+
yield tmpdir
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_no_venvs():
|
|
65
|
+
# Don't use the venv directory here
|
|
66
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
67
|
+
venvs = list_python_venvs(base_dir=tmpdir)
|
|
68
|
+
|
|
69
|
+
assert len(venvs) == 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_local_found(with_venvs):
|
|
73
|
+
venvs = list_python_venvs(base_dir=with_venvs, recursive=False)
|
|
74
|
+
|
|
75
|
+
assert len(venvs) == 1
|
|
76
|
+
assert os.path.samefile(venvs[0].folder, os.path.join(with_venvs, ".venv"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_all_found(with_venvs):
|
|
80
|
+
venvs = sorted(
|
|
81
|
+
list_python_venvs(base_dir=with_venvs, recursive=True),
|
|
82
|
+
key=lambda x: x.folder
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert len(venvs) == 3
|
|
86
|
+
assert os.path.samefile(venvs[0].folder, os.path.join(with_venvs, ".venv"))
|
|
87
|
+
assert os.path.samefile(venvs[1].folder, os.path.join(with_venvs, "subfolder/.venv"))
|
|
88
|
+
assert os.path.samefile(venvs[2].folder, os.path.join(with_venvs, "subfolder/subsubfolder/env"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_found_parent(with_venvs, this_python, this_venv):
|
|
92
|
+
venv_ex = list_python_venvs(base_dir=with_venvs, recursive=False)[0]
|
|
93
|
+
|
|
94
|
+
assert os.path.samefile(this_python.executable, venv_ex.parent_executable)
|
|
95
|
+
|
|
96
|
+
# We found the base env that created this python, all details match
|
|
97
|
+
parent = venv_ex.get_parent_install()
|
|
98
|
+
assert parent == this_python
|
|
99
|
+
|
|
100
|
+
# venv version str works same as parent
|
|
101
|
+
assert venv_ex.version_str == parent.version_str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_found_parent_cache(with_venvs, this_python):
|
|
105
|
+
venv_ex = list_python_venvs(base_dir=with_venvs, recursive=False)[0]
|
|
106
|
+
|
|
107
|
+
parent = venv_ex.get_parent_install(cache=[this_python])
|
|
108
|
+
assert parent == this_python
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_empty_packages(with_venvs):
|
|
112
|
+
venv_ex = list_python_venvs(base_dir=with_venvs, recursive=False)[0]
|
|
113
|
+
|
|
114
|
+
assert os.path.exists(venv_ex.parent_executable)
|
|
115
|
+
assert os.path.exists(venv_ex.executable)
|
|
116
|
+
|
|
117
|
+
packages = venv_ex.list_packages()
|
|
118
|
+
assert packages == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/print_python_versions.py
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/__init__.py
RENAMED
|
File without changes
|
{ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/python_versions.txt
RENAMED
|
File without changes
|
|
File without changes
|
{ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/release_file.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|