ducktools-pythonfinder 0.7.1__tar.gz → 0.7.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.
Files changed (42) hide show
  1. {ducktools_pythonfinder-0.7.1/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.7.3}/PKG-INFO +1 -1
  2. ducktools_pythonfinder-0.7.3/scripts/list_python_venvs.py +120 -0
  3. ducktools_pythonfinder-0.7.3/src/ducktools/pythonfinder/_version.py +2 -0
  4. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/venv.py +45 -31
  5. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
  6. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +1 -0
  7. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_venv_finder.py +23 -0
  8. ducktools_pythonfinder-0.7.1/src/ducktools/pythonfinder/_version.py +0 -2
  9. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/.gitignore +0 -0
  10. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/LICENSE +0 -0
  11. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/MANIFEST.in +0 -0
  12. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/README.md +0 -0
  13. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/pyproject.toml +0 -0
  14. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/scripts/build_zipapp.py +0 -0
  15. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/scripts/detail_this_python.py +0 -0
  16. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/scripts/print_python_versions.py +0 -0
  17. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/setup.cfg +0 -0
  18. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/__init__.py +0 -0
  19. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/__main__.py +0 -0
  20. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
  21. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/details_script.py +0 -0
  22. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/linux/__init__.py +0 -0
  23. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/linux/pyenv_search.py +0 -0
  24. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
  25. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/shared.py +0 -0
  26. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/win32/__init__.py +0 -0
  27. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/win32/pyenv_search.py +0 -0
  28. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools/pythonfinder/win32/registry_search.py +0 -0
  29. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
  30. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
  31. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
  32. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
  33. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/conftest.py +0 -0
  34. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/sources/python_versions.txt +0 -0
  35. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/sources/release.json +0 -0
  36. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/sources/release_file.json +0 -0
  37. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_details_script.py +0 -0
  38. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_foldersearch.py +0 -0
  39. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_orgsearch.py +0 -0
  40. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_pyenv.py +0 -0
  41. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_shared.py +0 -0
  42. {ducktools_pythonfinder-0.7.1 → ducktools_pythonfinder-0.7.3}/tests/test_uv_finder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ducktools-pythonfinder
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -0,0 +1,120 @@
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 sys
26
+
27
+ if sys.version_info < (3, 12):
28
+ raise RuntimeError("This script requires Python 3.12 or newer.")
29
+
30
+ from collections.abc import Iterable, Callable, Generator
31
+ from pathlib import Path
32
+
33
+ from ducktools.classbuilder.prefab import Prefab
34
+ from ducktools.pythonfinder.venv import get_python_venvs
35
+
36
+
37
+ # Taken from ducktools-env's main
38
+ def get_columns(
39
+ *,
40
+ data: Iterable,
41
+ headings: list[str],
42
+ attributes: list[str],
43
+ getter: Callable[[object, str], str] = getattr,
44
+ ) -> Generator[str]:
45
+ """
46
+ A helper function to generate a table to print with correct column widths
47
+
48
+ :param data: input data
49
+ :param headings: headings for the top of the table
50
+ :param attributes: attribute names to use for each column
51
+ :param getter: attribute getter function (ex: getattr, dict.get)
52
+ :return: Generator of column lines
53
+ """
54
+ if len(headings) != len(attributes):
55
+ raise TypeError("Must be the same number of headings as attributes")
56
+
57
+ widths = {
58
+ f"{attrib}": len(head) for attrib, head in zip(attributes, headings)
59
+ }
60
+
61
+ data_rows = []
62
+ for d in data:
63
+ row = []
64
+ for attrib in attributes:
65
+ d_text = f"{getter(d, attrib)}"
66
+ d_len = len(d_text)
67
+ widths[f"{attrib}"] = max(widths[attrib], d_len)
68
+ row.append(d_text)
69
+ data_rows.append(row)
70
+
71
+ yield (
72
+ "| "
73
+ + " | ".join(f"{head:<{widths[attrib]}}"
74
+ for head, attrib in zip(headings, attributes))
75
+ + " |"
76
+ )
77
+ yield (
78
+ "| "
79
+ + " | ".join("-" * widths[attrib]
80
+ for attrib in attributes)
81
+ + " |"
82
+ )
83
+
84
+ for row in data_rows:
85
+ yield (
86
+ "| "
87
+ + " | ".join(f"{item:<{widths[attrib]}}"
88
+ for item, attrib in zip(row, attributes))
89
+ + " |"
90
+ )
91
+
92
+
93
+ class DisplayVEnv(Prefab):
94
+ version: str
95
+ path: str
96
+ parent_path: str
97
+
98
+
99
+ def get_all_venvs():
100
+ cwd = Path.cwd()
101
+
102
+ venvs = get_python_venvs(recursive=True, search_parent_folders=True)
103
+ venv_data = [
104
+ DisplayVEnv(
105
+ version=venv.version_str,
106
+ path=str(Path(venv.folder).relative_to(cwd, walk_up=True)),
107
+ parent_path=venv.parent_path,
108
+ )
109
+ for venv in venvs
110
+ ]
111
+
112
+ headings = ["Version", "Path", "Base Runtime"]
113
+ attribs = ["version", "path", "parent_path"]
114
+
115
+ for row in get_columns(data=venv_data, headings=headings, attributes=attribs, getter=getattr):
116
+ print(row)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ get_all_venvs()
@@ -0,0 +1,2 @@
1
+ __version__ = "0.7.3"
2
+ __version_tuple__ = (0, 7, 3)
@@ -22,6 +22,11 @@
22
22
  # SOFTWARE.
23
23
  from __future__ import annotations
24
24
 
25
+ try:
26
+ from _collections_abc import Iterable
27
+ except ImportError:
28
+ from collections.abc import Iterable
29
+
25
30
  import os
26
31
  import sys
27
32
 
@@ -103,7 +108,7 @@ class PythonVEnv(Prefab):
103
108
 
104
109
  return install
105
110
 
106
- def list_packages(self):
111
+ def list_packages(self) -> list[PythonPackage]:
107
112
  if not self.parent_exists:
108
113
  raise FileNotFoundError(
109
114
  f"Parent Python at \"{self.parent_executable}\" does not exist."
@@ -138,17 +143,17 @@ class PythonVEnv(Prefab):
138
143
  return packages
139
144
 
140
145
  @classmethod
141
- def from_cfg(cls, cfg_path):
146
+ def from_cfg(cls, cfg_path: str | os.PathLike) -> PythonVEnv:
142
147
  """
143
148
  Get a PythonVEnv instance from the path to a config file
144
149
 
145
- :param cfg_path:
146
- :return:
150
+ :param cfg_path: Path to a virtualenv config file
151
+ :return: PythonVEnv with details relative to that config file
147
152
  """
148
153
  parent_path, version_str = None, None
149
- venv_base = cfg_path.parent
154
+ venv_base = os.path.dirname(cfg_path)
150
155
 
151
- with cfg_path.open() as f:
156
+ with open(cfg_path, 'r') as f:
152
157
  for line in f:
153
158
  key, value = (item.strip() for item in line.split("="))
154
159
 
@@ -192,15 +197,20 @@ class PythonVEnv(Prefab):
192
197
  folder=venv_base,
193
198
  executable=venv_exe,
194
199
  version=version_tuple,
195
- parent_path=parent_path
200
+ parent_path=parent_path,
196
201
  )
197
202
 
198
203
 
199
- def get_python_venvs(base_dir=None, recursive=False, search_parent_folders=False):
204
+ def get_python_venvs(
205
+ base_dir: str | os.PathLike | None = None,
206
+ recursive: bool = False,
207
+ search_parent_folders: bool = False
208
+ ) -> Iterable[PythonVEnv]:
200
209
  """
201
210
  Yield discoverable python virtual environment information
202
211
 
203
- If recursive=True then search_parent_folders is ignored.
212
+ If recursive=True and search_parent_folders=True *only* the current working
213
+ directory will be searched recursively. Parent folders will not be searched recursively
204
214
 
205
215
  If you're in a project directory and are looking for a potential venv
206
216
  search_parent_folders=True will search parents and yield installs discovered.
@@ -215,36 +225,40 @@ def get_python_venvs(base_dir=None, recursive=False, search_parent_folders=False
215
225
  """
216
226
  base_dir = _laz.Path.cwd() if base_dir is None else _laz.Path(base_dir)
217
227
 
218
- search_folders = [base_dir]
228
+ cwd_pattern = pattern = f"*/{VENV_CONFIG_NAME}"
219
229
 
220
- # Recursive searches don't also search parent folders.
221
230
  if recursive:
222
- pattern = f"**/{VENV_CONFIG_NAME}"
223
- else:
224
- pattern = f"*/{VENV_CONFIG_NAME}"
225
- if search_parent_folders:
226
- search_folders.extend(base_dir.parents)
231
+ # Only search cwd recursively, parents are searched non-recursively
232
+ cwd_pattern = "*" + pattern
227
233
 
228
- for f in search_folders:
234
+ for conf in base_dir.glob(cwd_pattern):
229
235
  try:
230
- for conf in f.glob(pattern):
231
- try:
232
- env = PythonVEnv.from_cfg(conf)
233
- except InvalidVEnvError:
236
+ env = PythonVEnv.from_cfg(conf)
237
+ except InvalidVEnvError:
238
+ continue
239
+ yield env
240
+
241
+ if search_parent_folders:
242
+ # Search parent folders
243
+ for fld in base_dir.parents:
244
+ try:
245
+ for conf in fld.glob(pattern):
246
+ try:
247
+ env = PythonVEnv.from_cfg(conf)
248
+ except InvalidVEnvError:
249
+ continue
250
+ yield env
251
+ except OSError as e:
252
+ # MacOS can error on searching up folders with an invalid argument
253
+ # On Python 3.11 or earlier.
254
+ if e.errno == 22:
234
255
  continue
235
256
 
236
- yield env
237
- except OSError as e:
238
- # MacOS can error on searching up folders with an invalid argument
239
- # On Python 3.11 or earlier.
240
- if e.errno == 22:
241
- continue
242
-
243
257
 
244
258
  def list_python_venvs(
245
- base_dir=None,
246
- recursive=False,
247
- search_parent_folders=False,
259
+ base_dir: str | os.PathLike | None = None,
260
+ recursive: bool = False,
261
+ search_parent_folders: bool = False,
248
262
  ) -> list[PythonVEnv]:
249
263
  """
250
264
  Get a list of discoverable python virtual environment information
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ducktools-pythonfinder
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -5,6 +5,7 @@ README.md
5
5
  pyproject.toml
6
6
  scripts/build_zipapp.py
7
7
  scripts/detail_this_python.py
8
+ scripts/list_python_venvs.py
8
9
  scripts/print_python_versions.py
9
10
  src/ducktools/pythonfinder/__init__.py
10
11
  src/ducktools/pythonfinder/__main__.py
@@ -76,6 +76,13 @@ def test_local_found(with_venvs):
76
76
  assert os.path.samefile(venvs[0].folder, os.path.join(with_venvs, ".venv"))
77
77
 
78
78
 
79
+ def test_parent_not_always_searched(with_venvs):
80
+ venvs = list_python_venvs(base_dir=os.path.join(with_venvs, "subfolder"), search_parent_folders=False)
81
+
82
+ assert len(venvs) == 1
83
+ assert os.path.samefile(venvs[0].folder, os.path.join(with_venvs, "subfolder/.venv"))
84
+
85
+
79
86
  def test_found_in_parent(with_venvs):
80
87
  venvs = list_python_venvs(base_dir=os.path.join(with_venvs, "subfolder"), search_parent_folders=True)
81
88
 
@@ -95,6 +102,22 @@ def test_all_found(with_venvs):
95
102
  assert os.path.samefile(venvs[2].folder, os.path.join(with_venvs, "subfolder/subsubfolder/env"))
96
103
 
97
104
 
105
+ def test_recursive_parents(with_venvs):
106
+ venvs = sorted(
107
+ list_python_venvs(
108
+ base_dir=os.path.join(with_venvs, "subfolder"),
109
+ recursive=True,
110
+ search_parent_folders=True,
111
+ ),
112
+ key=lambda x: x.folder
113
+ )
114
+
115
+ assert len(venvs) == 3
116
+ assert os.path.samefile(venvs[0].folder, os.path.join(with_venvs, ".venv"))
117
+ assert os.path.samefile(venvs[1].folder, os.path.join(with_venvs, "subfolder/.venv"))
118
+ assert os.path.samefile(venvs[2].folder, os.path.join(with_venvs, "subfolder/subsubfolder/env"))
119
+
120
+
98
121
  def test_found_parent(with_venvs, this_python, this_venv):
99
122
  venv_ex = list_python_venvs(base_dir=with_venvs, recursive=False)[0]
100
123
 
@@ -1,2 +0,0 @@
1
- __version__ = "0.7.1"
2
- __version_tuple__ = (0, 7, 1)