ducktools-pythonfinder 0.6.2__tar.gz → 0.6.4__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 (39) hide show
  1. {ducktools_pythonfinder-0.6.2/src/ducktools_pythonfinder.egg-info → ducktools_pythonfinder-0.6.4}/PKG-INFO +1 -1
  2. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/__main__.py +3 -0
  3. ducktools_pythonfinder-0.6.4/src/ducktools/pythonfinder/_version.py +2 -0
  4. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/details_script.py +6 -1
  5. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/linux/pyenv_search.py +18 -5
  6. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/shared.py +36 -19
  7. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/win32/pyenv_search.py +6 -2
  8. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/win32/registry_search.py +12 -6
  9. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4/src/ducktools_pythonfinder.egg-info}/PKG-INFO +1 -1
  10. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/test_pyenv.py +4 -4
  11. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/test_uv_finder.py +12 -1
  12. ducktools_pythonfinder-0.6.2/src/ducktools/pythonfinder/_version.py +0 -2
  13. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/.gitignore +0 -0
  14. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/LICENSE +0 -0
  15. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/MANIFEST.in +0 -0
  16. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/README.md +0 -0
  17. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/pyproject.toml +0 -0
  18. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/scripts/build_zipapp.py +0 -0
  19. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/scripts/detail_this_python.py +0 -0
  20. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/scripts/print_python_versions.py +0 -0
  21. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/setup.cfg +0 -0
  22. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/__init__.py +0 -0
  23. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/darwin/__init__.py +0 -0
  24. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/linux/__init__.py +0 -0
  25. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/pythonorg_search.py +0 -0
  26. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools/pythonfinder/win32/__init__.py +0 -0
  27. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools_pythonfinder.egg-info/SOURCES.txt +0 -0
  28. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools_pythonfinder.egg-info/dependency_links.txt +0 -0
  29. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools_pythonfinder.egg-info/entry_points.txt +0 -0
  30. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools_pythonfinder.egg-info/requires.txt +0 -0
  31. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/src/ducktools_pythonfinder.egg-info/top_level.txt +0 -0
  32. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/conftest.py +0 -0
  33. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/sources/python_versions.txt +0 -0
  34. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/sources/release.json +0 -0
  35. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/sources/release_file.json +0 -0
  36. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/test_details_script.py +0 -0
  37. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/test_foldersearch.py +0 -0
  38. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/test_orgsearch.py +0 -0
  39. {ducktools_pythonfinder-0.6.2 → ducktools_pythonfinder-0.6.4}/tests/test_shared.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-pythonfinder
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -181,6 +181,9 @@ def display_local_installs(
181
181
  ):
182
182
  version_str = f"**{version_str}"
183
183
 
184
+ if install.metadata.get("freethreaded"):
185
+ version_str = f"{version_str}t"
186
+
184
187
  if install.shadowed:
185
188
  version_str = f"[{version_str}]"
186
189
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.6.4"
2
+ __version_tuple__ = (0, 6, 4)
@@ -26,7 +26,7 @@ Get the details from a python install as JSON
26
26
  """
27
27
  import sys
28
28
 
29
- FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>[a-zA-Z]*)(?P<serial>\d*)"
29
+ FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>a|b|c|rc)?(?P<serial>\d*)?"
30
30
 
31
31
 
32
32
  def version_str_to_tuple(version):
@@ -80,6 +80,11 @@ def get_details():
80
80
  metadata = {"{}_version".format(implementation): sys.implementation.version}
81
81
  else:
82
82
  metadata = {}
83
+ if sys.version_info >= (3, 13):
84
+ import sysconfig
85
+ freethreaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
86
+ metadata["freethreaded"] = freethreaded
87
+
83
88
 
84
89
  install = dict(
85
90
  version=list(sys.version_info),
@@ -33,7 +33,7 @@ from _collections_abc import Iterator
33
33
 
34
34
  from ducktools.lazyimporter import LazyImporter, ModuleImport
35
35
 
36
- from ..shared import PythonInstall, get_install_details
36
+ from ..shared import PythonInstall, get_install_details, FULL_PY_VER_RE, version_str_to_tuple
37
37
 
38
38
  _laz = LazyImporter(
39
39
  [
@@ -41,8 +41,7 @@ _laz = LazyImporter(
41
41
  ]
42
42
  )
43
43
 
44
- # pyenv folder names
45
- PYTHON_VER_RE = r"\d{1,2}\.\d{1,2}\.\d+[a-z]*\d*"
44
+ # pyenv folder name for pypy
46
45
  PYPY_VER_RE = r"^pypy(?P<pyversion>\d{1,2}\.\d+)-(?P<pypyversion>[\d\.]*)$"
47
46
 
48
47
  PYENV_VERSIONS_FOLDER = os.path.join(os.environ.get("PYENV_ROOT", ""), "versions")
@@ -64,7 +63,21 @@ def get_pyenv_pythons(
64
63
  executable = os.path.join(p.path, "bin/python")
65
64
 
66
65
  if os.path.exists(executable):
67
- if _laz.re.fullmatch(PYTHON_VER_RE, p.name):
68
- yield PythonInstall.from_str(p.name, executable)
66
+ if p.name.endswith("t"):
67
+ freethreaded = True
68
+ version = p.name[:-1]
69
+ else:
70
+ freethreaded = False
71
+ version = p.name
72
+ if _laz.re.fullmatch(FULL_PY_VER_RE, version):
73
+ version_tuple = version_str_to_tuple(version)
74
+ metadata = {}
75
+ if version_tuple >= (3, 13):
76
+ metadata["freethreaded"] = freethreaded
77
+ yield PythonInstall(
78
+ version=version_tuple,
79
+ executable=executable,
80
+ metadata=metadata,
81
+ )
69
82
  elif query_executables and (install := get_install_details(executable)):
70
83
  yield install
@@ -46,7 +46,15 @@ _laz = LazyImporter(
46
46
  )
47
47
 
48
48
 
49
- FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>[a-zA-Z]*)(?P<serial>\d*)"
49
+ FULL_PY_VER_RE = r"(?P<major>\d+)\.(?P<minor>\d+)\.?(?P<micro>\d*)-?(?P<releaselevel>a|b|c|rc)?(?P<serial>\d*)?"
50
+
51
+ UV_PYTHON_RE = (
52
+ r"(?P<implementation>[a-zA-Z]+)"
53
+ r"-(?P<version>\d+\.\d+\.\d*[a-zA-Z]*\d*)\+?(?P<extra>.*?)?"
54
+ r"-(?P<platform>\w*)"
55
+ r"-(?P<arch>\w*)"
56
+ r"-.*"
57
+ )
50
58
 
51
59
 
52
60
  def version_str_to_tuple(version):
@@ -61,7 +69,7 @@ def version_str_to_tuple(version):
61
69
  releaselevel = "alpha"
62
70
  elif releaselevel == "b":
63
71
  releaselevel = "beta"
64
- elif releaselevel == "rc":
72
+ elif releaselevel in {"c", "rc"}:
65
73
  releaselevel = "candidate"
66
74
  else:
67
75
  releaselevel = "final"
@@ -273,8 +281,13 @@ def get_folder_pythons(
273
281
 
274
282
  with os.scandir(base_folder) as fld:
275
283
  for file_path in fld:
284
+ try:
285
+ is_file = file_path.is_file()
286
+ except PermissionError:
287
+ continue
288
+
276
289
  if (
277
- file_path.is_file()
290
+ is_file
278
291
  and any(reg.fullmatch(file_path.name) for reg in regexes)
279
292
  ):
280
293
  p = file_path.path
@@ -318,25 +331,29 @@ def _implementation_from_uv_dir(
318
331
  install: PythonInstall | None = None
319
332
 
320
333
  if os.path.exists(python_path):
321
- try:
322
- implementation, version, platform, arch, _ = direntry.name.split("-")
323
- except ValueError:
324
- # Directory name format has changed
334
+ if match := _laz.re.fullmatch(UV_PYTHON_RE, direntry.name):
335
+ implementation, version, extra, platform, arch = match.groups()
336
+ metadata = {
337
+ "freethreaded": "freethreaded" in extra,
338
+ }
339
+
340
+ try:
341
+ if implementation in {"cpython"}:
342
+ install = PythonInstall.from_str(
343
+ version=version,
344
+ executable=python_path,
345
+ architecture="32bit" if arch in {"i686", "armv7"} else "64bit",
346
+ implementation=implementation,
347
+ metadata=metadata
348
+ )
349
+ except ValueError:
350
+ pass
351
+
352
+ if install is None:
353
+ # Directory name format has changed or this is an alternate implementation
325
354
  # Slow backup - ask python itself
326
355
  if query_executables:
327
356
  install = get_install_details(python_path)
328
- else:
329
- if implementation in {"cpython"}:
330
- install = PythonInstall.from_str(
331
- version=version,
332
- executable=python_path,
333
- architecture="32bit" if arch in {"i686", "armv7"} else "64bit",
334
- implementation=implementation,
335
- )
336
- else:
337
- # Get additional alternate implementation details
338
- if query_executables:
339
- install = get_install_details(python_path)
340
357
 
341
358
  return install
342
359
 
@@ -73,7 +73,9 @@ def get_pyenv_pythons(
73
73
  arch = "32bit" if arch == "win32" else "64bit"
74
74
  try:
75
75
  yield PythonInstall.from_str(
76
- version, executable, architecture=arch
76
+ version=version,
77
+ executable=executable,
78
+ architecture=arch,
77
79
  )
78
80
  except ValueError:
79
81
  pass
@@ -81,7 +83,9 @@ def get_pyenv_pythons(
81
83
  version = split_version[0]
82
84
  try:
83
85
  yield PythonInstall.from_str(
84
- version, executable, architecture="64bit"
86
+ version=version,
87
+ executable=executable,
88
+ architecture="64bit",
85
89
  )
86
90
  except ValueError:
87
91
  pass
@@ -31,7 +31,7 @@ Based on PEP 514 registry entries.
31
31
  import winreg # noqa # pycharm seems to think winreg doesn't exist in python3.12
32
32
  from _collections_abc import Iterator
33
33
 
34
- from ..shared import PythonInstall
34
+ from ..shared import PythonInstall, version_str_to_tuple
35
35
 
36
36
  exclude_companies = {
37
37
  "PyLauncher", # pylauncher is special cased to be ignored
@@ -100,18 +100,24 @@ def get_registered_pythons() -> Iterator[PythonInstall]:
100
100
  winreg.CloseKey(install_key)
101
101
 
102
102
  python_version: str | None = metadata.get("Version")
103
- # Pyenv puts architecture information in the Version value for some reason
104
- if python_version:
105
- python_version = python_version.split("-")[0]
106
103
 
107
104
  architecture = metadata.get("SysArchitecture")
108
105
 
109
106
  metadata["InWindowsRegistry"] = True
110
107
 
111
108
  if python_path and python_version:
109
+ # Pyenv puts architecture information in the Version value for some reason
110
+ python_version = python_version.split("-")[0]
111
+ version_tuple = version_str_to_tuple(python_version)
112
+
113
+ if (
114
+ version_tuple >= (3, 13)
115
+ and "freethreaded" in metadata.get("DisplayName", "")
116
+ ):
117
+ metadata["freethreaded"] = True
112
118
  try:
113
- yield PythonInstall.from_str(
114
- version=python_version,
119
+ yield PythonInstall(
120
+ version=version_tuple,
115
121
  executable=python_path,
116
122
  architecture=architecture,
117
123
  metadata=metadata,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-pythonfinder
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -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(out_ver, out_executable)]
75
+ assert python_versions == [PythonInstall.from_str(version=out_ver, executable=out_executable)]
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("3.12.1", py_exe)]
92
+ assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe)]
93
93
 
94
94
 
95
95
  @pytest.mark.skipif(sys.platform != "win32", reason="Test for Windows only")
@@ -106,7 +106,7 @@ def test_32bit_version(fs):
106
106
 
107
107
  versions = list(get_pyenv_pythons(tmpdir))
108
108
 
109
- assert versions == [PythonInstall.from_str("3.12.1", py_exe, architecture="32bit")]
109
+ assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe, architecture="32bit")]
110
110
 
111
111
 
112
112
  @pytest.mark.skipif(sys.platform != "win32", reason="Test for Windows only")
@@ -155,7 +155,7 @@ def test_fs_versions_nix(fs):
155
155
 
156
156
  versions = list(get_pyenv_pythons(tmpdir))
157
157
 
158
- assert versions == [PythonInstall.from_str("3.12.1", py_exe)]
158
+ assert versions == [PythonInstall.from_str(version="3.12.1", executable=py_exe)]
159
159
 
160
160
 
161
161
  @pytest.mark.skipif(sys.platform == "win32", reason="Test for non-Windows only")
@@ -22,6 +22,7 @@
22
22
  # SOFTWARE.
23
23
 
24
24
  import os
25
+ import re
25
26
  import subprocess
26
27
 
27
28
  from tempfile import TemporaryDirectory
@@ -32,8 +33,8 @@ import unittest.mock as mock
32
33
  import pytest
33
34
 
34
35
  from ducktools.pythonfinder.shared import (
36
+ UV_PYTHON_RE,
35
37
  get_uv_python_path,
36
- _implementation_from_uv_dir,
37
38
  get_uv_pythons,
38
39
  )
39
40
 
@@ -144,3 +145,13 @@ class TestUVReal:
144
145
  assert pythons[0].version >= (3, 10, 14)
145
146
  assert pythons[0].implementation == "pypy"
146
147
  assert pythons[0].implementation_version >= (7, 3, 17)
148
+
149
+
150
+ def test_regex_matches():
151
+ example_312 = "cpython-3.12.7-windows-x86_64-none"
152
+ match = re.match(UV_PYTHON_RE, example_312)
153
+ assert match.groups() == ("cpython", "3.12.7", "", "windows", "x86_64")
154
+
155
+ example_313t = "cpython-3.13.0+freethreaded-windows-x86_64-none"
156
+ match = re.match(UV_PYTHON_RE, example_313t)
157
+ assert match.groups() == ("cpython", "3.13.0", "freethreaded", "windows", "x86_64")
@@ -1,2 +0,0 @@
1
- __version__ = "0.6.2"
2
- __version_tuple__ = (0, 6, 2)