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.
Files changed (41) hide show
  1. {ducktools_pythonfinder-0.6.7/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.7.0}/PKG-INFO +13 -2
  2. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/README.md +11 -0
  3. ducktools_pythonfinder-0.7.0/src/ducktools/pythonfinder/_version.py +2 -0
  4. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/linux/pyenv_search.py +2 -1
  5. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/shared.py +16 -5
  6. ducktools_pythonfinder-0.7.0/src/ducktools/pythonfinder/venv.py +196 -0
  7. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/win32/pyenv_search.py +4 -2
  8. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/win32/registry_search.py +1 -0
  9. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0/src/ducktools_pythonfinder.egg-info}/PKG-INFO +13 -2
  10. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +2 -0
  11. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/conftest.py +23 -0
  12. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_pyenv.py +7 -4
  13. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_uv_finder.py +2 -0
  14. ducktools_pythonfinder-0.7.0/tests/test_venv_finder.py +118 -0
  15. ducktools_pythonfinder-0.6.7/src/ducktools/pythonfinder/_version.py +0 -2
  16. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/.gitignore +0 -0
  17. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/LICENSE +0 -0
  18. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/MANIFEST.in +0 -0
  19. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/pyproject.toml +0 -0
  20. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/build_zipapp.py +0 -0
  21. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/detail_this_python.py +0 -0
  22. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/scripts/print_python_versions.py +0 -0
  23. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/setup.cfg +0 -0
  24. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/__init__.py +0 -0
  25. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/__main__.py +0 -0
  26. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
  27. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/details_script.py +0 -0
  28. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/linux/__init__.py +0 -0
  29. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
  30. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools/pythonfinder/win32/__init__.py +0 -0
  31. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
  32. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
  33. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
  34. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
  35. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/python_versions.txt +0 -0
  36. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/release.json +0 -0
  37. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/sources/release_file.json +0 -0
  38. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_details_script.py +0 -0
  39. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_foldersearch.py +0 -0
  40. {ducktools_pythonfinder-0.6.7 → ducktools_pythonfinder-0.7.0}/tests/test_orgsearch.py +0 -0
  41. {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
1
+ Metadata-Version: 2.2
2
2
  Name: ducktools-pythonfinder
3
- Version: 0.6.7
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.
@@ -0,0 +1,2 @@
1
+ __version__ = "0.7.0"
2
+ __version_tuple__ = (0, 7, 0)
@@ -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
@@ -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), executable, architecture, implementation, metadata # noqa
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
@@ -126,6 +126,7 @@ def get_registered_pythons() -> Iterator[PythonInstall]:
126
126
  executable=python_path,
127
127
  architecture=architecture,
128
128
  metadata=metadata,
129
+ managed_by=metadata["Company"],
129
130
  )
130
131
  except ValueError:
131
132
  pass
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: ducktools-pythonfinder
3
- Version: 0.6.7
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 == [PythonInstall.from_str(version="3.12.1", executable=py_exe, architecture="32bit")]
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 == []
@@ -1,2 +0,0 @@
1
- __version__ = "0.6.7"
2
- __version_tuple__ = (0, 6, 7)