python-discovery 1.3.0__py3-none-any.whl → 1.3.2__py3-none-any.whl
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.
- python_discovery/_discovery.py +9 -7
- python_discovery/_py_info.py +29 -31
- python_discovery/_py_spec.py +5 -1
- python_discovery/_windows/_pep514.py +11 -11
- {python_discovery-1.3.0.dist-info → python_discovery-1.3.2.dist-info}/METADATA +3 -1
- {python_discovery-1.3.0.dist-info → python_discovery-1.3.2.dist-info}/RECORD +8 -8
- {python_discovery-1.3.0.dist-info → python_discovery-1.3.2.dist-info}/WHEEL +0 -0
- {python_discovery-1.3.0.dist-info → python_discovery-1.3.2.dist-info}/licenses/LICENSE +0 -0
python_discovery/_discovery.py
CHANGED
|
@@ -314,13 +314,7 @@ class LazyPathDump:
|
|
|
314
314
|
content += " with =>"
|
|
315
315
|
for file_path in self.path.iterdir():
|
|
316
316
|
try:
|
|
317
|
-
if
|
|
318
|
-
continue
|
|
319
|
-
if IS_WIN: # pragma: win32 cover
|
|
320
|
-
pathext = self.env.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
|
|
321
|
-
if not any(file_path.name.upper().endswith(ext) for ext in pathext):
|
|
322
|
-
continue
|
|
323
|
-
elif not (file_path.stat().st_mode & os.X_OK):
|
|
317
|
+
if not self._is_executable(file_path):
|
|
324
318
|
continue
|
|
325
319
|
except OSError:
|
|
326
320
|
pass
|
|
@@ -328,6 +322,14 @@ class LazyPathDump:
|
|
|
328
322
|
content += file_path.name
|
|
329
323
|
return content
|
|
330
324
|
|
|
325
|
+
def _is_executable(self, file_path: Path) -> bool:
|
|
326
|
+
if file_path.is_dir():
|
|
327
|
+
return False
|
|
328
|
+
if IS_WIN: # pragma: win32 cover
|
|
329
|
+
pathext = self.env.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
|
|
330
|
+
return any(file_path.name.upper().endswith(ext) for ext in pathext)
|
|
331
|
+
return bool(file_path.stat().st_mode & os.X_OK)
|
|
332
|
+
|
|
331
333
|
|
|
332
334
|
def path_exe_finder(
|
|
333
335
|
spec: PythonSpec, *, all_implementations: bool = False
|
python_discovery/_py_info.py
CHANGED
|
@@ -16,6 +16,7 @@ from string import digits
|
|
|
16
16
|
from typing import TYPE_CHECKING, ClassVar, Final, NamedTuple
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
|
+
import tkinter as tk
|
|
19
20
|
from collections.abc import Generator, Mapping
|
|
20
21
|
|
|
21
22
|
from ._cache import PyInfoCache
|
|
@@ -59,6 +60,8 @@ class PythonInfo: # noqa: PLR0904
|
|
|
59
60
|
def _init_identity(self) -> None:
|
|
60
61
|
self.platform = sys.platform
|
|
61
62
|
self.implementation = platform.python_implementation()
|
|
63
|
+
if self.implementation == "GraalVM":
|
|
64
|
+
self.implementation = "GraalPy"
|
|
62
65
|
if self.implementation == "PyPy":
|
|
63
66
|
self.pypy_version_info = tuple(sys.pypy_version_info) # ty: ignore[unresolved-attribute] # pypy only
|
|
64
67
|
|
|
@@ -163,42 +166,37 @@ class PythonInfo: # noqa: PLR0904
|
|
|
163
166
|
try:
|
|
164
167
|
tcl = tk.Tcl()
|
|
165
168
|
tcl_lib = tcl.eval("info library")
|
|
166
|
-
|
|
167
|
-
# Try to get TK library path directly first
|
|
168
|
-
try:
|
|
169
|
-
tk_lib = tcl.eval("set tk_library")
|
|
170
|
-
if tk_lib and os.path.isdir(tk_lib):
|
|
171
|
-
pass # We found it directly
|
|
172
|
-
else:
|
|
173
|
-
tk_lib = None # Reset if invalid
|
|
174
|
-
except tk.TclError:
|
|
175
|
-
tk_lib = None
|
|
176
|
-
|
|
177
|
-
# If direct query failed, try constructing the path
|
|
178
|
-
if tk_lib is None:
|
|
179
|
-
tk_version = tcl.eval("package require Tk")
|
|
180
|
-
tcl_parent = os.path.dirname(tcl_lib)
|
|
181
|
-
|
|
182
|
-
# Try different version formats
|
|
183
|
-
version_variants = [
|
|
184
|
-
tk_version, # Full version like "8.6.12"
|
|
185
|
-
".".join(tk_version.split(".")[:2]), # Major.minor like "8.6"
|
|
186
|
-
tk_version.split(".")[0], # Just major like "8"
|
|
187
|
-
]
|
|
188
|
-
|
|
189
|
-
for version in version_variants:
|
|
190
|
-
tk_lib_path = os.path.join(tcl_parent, f"tk{version}")
|
|
191
|
-
if not os.path.isdir(tk_lib_path):
|
|
192
|
-
continue
|
|
193
|
-
if os.path.exists(os.path.join(tk_lib_path, "tk.tcl")):
|
|
194
|
-
tk_lib = tk_lib_path
|
|
195
|
-
break
|
|
196
|
-
|
|
169
|
+
tk_lib = PythonInfo._resolve_tk_lib(tcl, tcl_lib)
|
|
197
170
|
except tk.TclError:
|
|
198
171
|
pass
|
|
199
172
|
|
|
200
173
|
return tcl_lib, tk_lib
|
|
201
174
|
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _query_tk_library(tcl: tk.Tk) -> str | None: # pragma: no cover
|
|
177
|
+
"""Try to get the TK library path directly from Tcl."""
|
|
178
|
+
import tkinter as tk # noqa: PLC0415
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
if (tk_lib := tcl.eval("set tk_library")) and os.path.isdir(tk_lib):
|
|
182
|
+
return tk_lib
|
|
183
|
+
except tk.TclError:
|
|
184
|
+
pass
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _resolve_tk_lib(tcl: tk.Tk, tcl_lib: str) -> str | None: # pragma: no cover
|
|
189
|
+
"""Resolve the TK library path by direct query or path construction."""
|
|
190
|
+
if (tk_lib := PythonInfo._query_tk_library(tcl)) is not None:
|
|
191
|
+
return tk_lib
|
|
192
|
+
tk_version = tcl.eval("package require Tk")
|
|
193
|
+
tcl_parent = os.path.dirname(tcl_lib)
|
|
194
|
+
for version in (tk_version, ".".join(tk_version.split(".")[:2]), tk_version.split(".")[0]):
|
|
195
|
+
tk_lib_path = os.path.join(tcl_parent, f"tk{version}")
|
|
196
|
+
if os.path.isdir(tk_lib_path) and os.path.exists(os.path.join(tk_lib_path, "tk.tcl")):
|
|
197
|
+
return tk_lib_path
|
|
198
|
+
return None
|
|
199
|
+
|
|
202
200
|
def _fast_get_system_executable(self) -> str | None:
|
|
203
201
|
"""Try to get the system executable by just looking at properties."""
|
|
204
202
|
# if we're not in a virtual environment, this is already a system python, so return the original executable
|
python_discovery/_py_spec.py
CHANGED
|
@@ -17,7 +17,7 @@ PATTERN = re.compile(
|
|
|
17
17
|
(?P<version>[0-9.]+)? # version (e.g. 3.12, 3.12.1)
|
|
18
18
|
(?P<threaded>t)? # free-threaded flag
|
|
19
19
|
(?:-(?P<arch>32|64))? # architecture bitness
|
|
20
|
-
(?:-(?P<machine>[a-zA-Z0-9_]+))? # ISA (e.g. arm64, x86_64)
|
|
20
|
+
(?:-(?P<machine>[a-zA-Z0-9_.]+))? # ISA (e.g. arm64, x86_64, i86pc.64bit)
|
|
21
21
|
$
|
|
22
22
|
""",
|
|
23
23
|
re.VERBOSE,
|
|
@@ -77,6 +77,8 @@ def _parse_spec_pattern(string_spec: str) -> PythonSpec | None:
|
|
|
77
77
|
impl = groups["impl"]
|
|
78
78
|
if impl in {"py", "python"}:
|
|
79
79
|
impl = None
|
|
80
|
+
if impl == "graalvm":
|
|
81
|
+
impl = "graalpy"
|
|
80
82
|
arch = _int_or_none(groups["arch"])
|
|
81
83
|
machine = groups.get("machine")
|
|
82
84
|
if machine is not None:
|
|
@@ -97,6 +99,8 @@ def _parse_specifier(string_spec: str) -> PythonSpec | None:
|
|
|
97
99
|
return None
|
|
98
100
|
if impl in {"py", "python"}:
|
|
99
101
|
impl = None
|
|
102
|
+
if impl == "graalvm":
|
|
103
|
+
impl = "graalpy"
|
|
100
104
|
return PythonSpec(string_spec, impl, None, None, None, None, None, version_specifier=version_specifier)
|
|
101
105
|
|
|
102
106
|
|
|
@@ -124,23 +124,23 @@ def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[
|
|
|
124
124
|
key_path = f"{hive_name}/{company}/{tag}"
|
|
125
125
|
try:
|
|
126
126
|
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
|
|
127
|
-
exe
|
|
128
|
-
|
|
129
|
-
ip = get_value(ip_key, None)
|
|
130
|
-
if ip is None:
|
|
131
|
-
msg(key_path, "no ExecutablePath or default for it")
|
|
132
|
-
|
|
133
|
-
else:
|
|
134
|
-
exe = os.path.join(ip, "python.exe")
|
|
135
|
-
if exe is not None and os.path.exists(exe):
|
|
136
|
-
args = get_value(ip_key, "ExecutableArguments")
|
|
137
|
-
return exe, args
|
|
127
|
+
if (exe := _resolve_exe(ip_key, key_path)) is not None and os.path.exists(exe):
|
|
128
|
+
return exe, get_value(ip_key, "ExecutableArguments")
|
|
138
129
|
msg(key_path, f"could not load exe with value {exe}")
|
|
139
130
|
except OSError:
|
|
140
131
|
msg(f"{key_path}/InstallPath", "missing")
|
|
141
132
|
return None
|
|
142
133
|
|
|
143
134
|
|
|
135
|
+
def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # noqa: ANN401
|
|
136
|
+
if (exe := get_value(ip_key, "ExecutablePath")) is not None:
|
|
137
|
+
return exe
|
|
138
|
+
if (ip := get_value(ip_key, None)) is None:
|
|
139
|
+
msg(key_path, "no ExecutablePath or default for it")
|
|
140
|
+
return None
|
|
141
|
+
return os.path.join(ip, "python.exe")
|
|
142
|
+
|
|
143
|
+
|
|
144
144
|
def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # noqa: ANN401
|
|
145
145
|
arch_str = get_value(tag_key, "SysArchitecture")
|
|
146
146
|
if arch_str is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-discovery
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Python interpreter discovery
|
|
5
5
|
Project-URL: Changelog, https://github.com/tox-dev/python-discovery/releases
|
|
6
6
|
Project-URL: Documentation, https://python-discovery.readthedocs.io
|
|
@@ -53,6 +53,8 @@ Requires-Dist: furo>=2025.12.19; extra == 'docs'
|
|
|
53
53
|
Requires-Dist: sphinx-autodoc-typehints>=3.6.3; extra == 'docs'
|
|
54
54
|
Requires-Dist: sphinx>=9.1; extra == 'docs'
|
|
55
55
|
Requires-Dist: sphinxcontrib-mermaid>=2; extra == 'docs'
|
|
56
|
+
Requires-Dist: sphinxcontrib-towncrier>=0.4; extra == 'docs'
|
|
57
|
+
Requires-Dist: towncrier>=25.8; extra == 'docs'
|
|
56
58
|
Provides-Extra: testing
|
|
57
59
|
Requires-Dist: covdefaults>=2.3; extra == 'testing'
|
|
58
60
|
Requires-Dist: coverage>=7.5.4; extra == 'testing'
|
|
@@ -2,15 +2,15 @@ python_discovery/__init__.py,sha256=Hi4j3pMvC8Fo8KoNL-wOHsPSCUHwY4c0qvv5sjnFzME,
|
|
|
2
2
|
python_discovery/_cache.py,sha256=bG4fD-h2pdPmb4v9BldbXmJ5Tlt1boiH1Qrz8kp9Dds,5063
|
|
3
3
|
python_discovery/_cached_py_info.py,sha256=1AFEoc_-OaCi1PXmaFz9Iy_bEuqOhJdFSDFHljzD-EE,8540
|
|
4
4
|
python_discovery/_compat.py,sha256=iqxGuhK9rPaiui80G1H1KrweoMJ16gyLYEmUrSXrp5c,709
|
|
5
|
-
python_discovery/_discovery.py,sha256=
|
|
6
|
-
python_discovery/_py_info.py,sha256=
|
|
7
|
-
python_discovery/_py_spec.py,sha256=
|
|
5
|
+
python_discovery/_discovery.py,sha256=cdo6xlktNMEwsQn2R8pn4T1VDE4LD2EJPUyCT7vlHnM,16830
|
|
6
|
+
python_discovery/_py_info.py,sha256=ls4ZOe93RCsfjN2uEz1r4l3K8m-rtm3ZKRp59xJDJHg,34354
|
|
7
|
+
python_discovery/_py_spec.py,sha256=Kky0LXi8BU_ojfMdvvRhtDk6I4WPjovwXOu6E2QYHTo,10421
|
|
8
8
|
python_discovery/_specifier.py,sha256=PTh6suNTC0hMnFSxFYF_vggdciFLnwxoh1YflSbgCXA,10368
|
|
9
9
|
python_discovery/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
python_discovery/_windows/__init__.py,sha256=qthcyGh36b3WkeSbZ4ECRRYLbCOCaQu5X_DRZ98tXfk,315
|
|
11
|
-
python_discovery/_windows/_pep514.py,sha256=
|
|
11
|
+
python_discovery/_windows/_pep514.py,sha256=zXxje07QIXfrCB3eYbZHOBgXcvAQQpZ-ZWun3_zrapA,7688
|
|
12
12
|
python_discovery/_windows/_propose.py,sha256=iMJf6l0TU4SvuV8UsCHHINAe9L5dUT949xyeyMVdong,1565
|
|
13
|
-
python_discovery-1.3.
|
|
14
|
-
python_discovery-1.3.
|
|
15
|
-
python_discovery-1.3.
|
|
16
|
-
python_discovery-1.3.
|
|
13
|
+
python_discovery-1.3.2.dist-info/METADATA,sha256=nmMtjtmWU2DTdKudFjFa6Nrtlwk3OEDbB_BkeRYaw5A,5592
|
|
14
|
+
python_discovery-1.3.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
python_discovery-1.3.2.dist-info/licenses/LICENSE,sha256=kOJeH68qSq6o2V7o5_VziwUqGswMnFgGgQH5Mahr1Yg,1023
|
|
16
|
+
python_discovery-1.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|