python-discovery 1.3.1__tar.gz → 1.3.2__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 (57) hide show
  1. {python_discovery-1.3.1 → python_discovery-1.3.2}/.pre-commit-config.yaml +2 -2
  2. {python_discovery-1.3.1 → python_discovery-1.3.2}/PKG-INFO +1 -1
  3. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/changelog.rst +7 -0
  4. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_discovery.py +9 -7
  5. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_py_info.py +27 -31
  6. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_py_spec.py +1 -1
  7. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_windows/_pep514.py +11 -11
  8. {python_discovery-1.3.1 → python_discovery-1.3.2}/tasks/release.py +25 -19
  9. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_py_spec.py +1 -0
  10. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/FUNDING.yaml +0 -0
  11. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/SECURITY.md +0 -0
  12. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/dependabot.yaml +0 -0
  13. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/release.yaml +0 -0
  14. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/workflows/check.yaml +0 -0
  15. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/workflows/prepare-release.yaml +0 -0
  16. {python_discovery-1.3.1 → python_discovery-1.3.2}/.github/workflows/release.yaml +0 -0
  17. {python_discovery-1.3.1 → python_discovery-1.3.2}/.gitignore +0 -0
  18. {python_discovery-1.3.1 → python_discovery-1.3.2}/.readthedocs.yaml +0 -0
  19. {python_discovery-1.3.1 → python_discovery-1.3.2}/.readthedocs.yml +0 -0
  20. {python_discovery-1.3.1 → python_discovery-1.3.2}/CODE_OF_CONDUCT.md +0 -0
  21. {python_discovery-1.3.1 → python_discovery-1.3.2}/LICENSE +0 -0
  22. {python_discovery-1.3.1 → python_discovery-1.3.2}/README.md +0 -0
  23. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/_static/custom.css +0 -0
  24. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/_static/logo.svg +0 -0
  25. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/changelog/template.jinja2 +0 -0
  26. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/conf.py +0 -0
  27. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/explanation.rst +0 -0
  28. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/how-to/standalone-usage.rst +0 -0
  29. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/index.rst +0 -0
  30. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/reference/api.rst +0 -0
  31. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/reference/environment-variables.rst +0 -0
  32. {python_discovery-1.3.1 → python_discovery-1.3.2}/docs/tutorial/getting-started.rst +0 -0
  33. {python_discovery-1.3.1 → python_discovery-1.3.2}/pyproject.toml +0 -0
  34. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/__init__.py +0 -0
  35. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_cache.py +0 -0
  36. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_cached_py_info.py +0 -0
  37. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_compat.py +0 -0
  38. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_specifier.py +0 -0
  39. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_windows/__init__.py +0 -0
  40. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/_windows/_propose.py +0 -0
  41. {python_discovery-1.3.1 → python_discovery-1.3.2}/src/python_discovery/py.typed +0 -0
  42. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/conftest.py +0 -0
  43. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/py_info/test_py_info.py +0 -0
  44. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/py_info/test_py_info_exe_based_of.py +0 -0
  45. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_cache.py +0 -0
  46. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_cached_py_info.py +0 -0
  47. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_discovery.py +0 -0
  48. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_discovery_extra.py +0 -0
  49. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_iter_interpreters.py +0 -0
  50. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_py_info_extra.py +0 -0
  51. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_py_spec_extra.py +0 -0
  52. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/test_specifier.py +0 -0
  53. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/windows/conftest.py +0 -0
  54. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/windows/test_windows.py +0 -0
  55. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/windows/test_windows_pep514.py +0 -0
  56. {python_discovery-1.3.1 → python_discovery-1.3.2}/tests/windows/winreg_mock_values.py +0 -0
  57. {python_discovery-1.3.1 → python_discovery-1.3.2}/tox.toml +0 -0
@@ -19,7 +19,7 @@ repos:
19
19
  hooks:
20
20
  - id: pyproject-fmt
21
21
  - repo: https://github.com/astral-sh/ruff-pre-commit
22
- rev: "v0.15.12"
22
+ rev: "v0.15.14"
23
23
  hooks:
24
24
  - id: ruff-format
25
25
  - id: ruff
@@ -32,7 +32,7 @@ repos:
32
32
  - prettier@3.8.1
33
33
  - "@prettier/plugin-xml@3.4.2"
34
34
  - repo: https://github.com/zizmorcore/zizmor-pre-commit
35
- rev: v1.24.1
35
+ rev: v1.25.2
36
36
  hooks:
37
37
  - id: zizmor
38
38
  - repo: meta
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-discovery
3
- Version: 1.3.1
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
@@ -6,6 +6,13 @@
6
6
 
7
7
  .. towncrier release notes start
8
8
 
9
+ ********************
10
+ v1.3.2 (2026-05-27)
11
+ ********************
12
+
13
+ No significant changes.
14
+
15
+
9
16
  ********************
10
17
  v1.3.1 (2026-05-12)
11
18
  ********************
@@ -314,13 +314,7 @@ class LazyPathDump:
314
314
  content += " with =>"
315
315
  for file_path in self.path.iterdir():
316
316
  try:
317
- if file_path.is_dir():
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
@@ -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
@@ -165,42 +166,37 @@ class PythonInfo: # noqa: PLR0904
165
166
  try:
166
167
  tcl = tk.Tcl()
167
168
  tcl_lib = tcl.eval("info library")
168
-
169
- # Try to get TK library path directly first
170
- try:
171
- tk_lib = tcl.eval("set tk_library")
172
- if tk_lib and os.path.isdir(tk_lib):
173
- pass # We found it directly
174
- else:
175
- tk_lib = None # Reset if invalid
176
- except tk.TclError:
177
- tk_lib = None
178
-
179
- # If direct query failed, try constructing the path
180
- if tk_lib is None:
181
- tk_version = tcl.eval("package require Tk")
182
- tcl_parent = os.path.dirname(tcl_lib)
183
-
184
- # Try different version formats
185
- version_variants = [
186
- tk_version, # Full version like "8.6.12"
187
- ".".join(tk_version.split(".")[:2]), # Major.minor like "8.6"
188
- tk_version.split(".")[0], # Just major like "8"
189
- ]
190
-
191
- for version in version_variants:
192
- tk_lib_path = os.path.join(tcl_parent, f"tk{version}")
193
- if not os.path.isdir(tk_lib_path):
194
- continue
195
- if os.path.exists(os.path.join(tk_lib_path, "tk.tcl")):
196
- tk_lib = tk_lib_path
197
- break
198
-
169
+ tk_lib = PythonInfo._resolve_tk_lib(tcl, tcl_lib)
199
170
  except tk.TclError:
200
171
  pass
201
172
 
202
173
  return tcl_lib, tk_lib
203
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
+
204
200
  def _fast_get_system_executable(self) -> str | None:
205
201
  """Try to get the system executable by just looking at properties."""
206
202
  # if we're not in a virtual environment, this is already a system python, so return the original executable
@@ -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,
@@ -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 = get_value(ip_key, "ExecutablePath")
128
- if exe is None:
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:
@@ -19,29 +19,13 @@ def main(version_str: str) -> None:
19
19
  msg = "Current repository is dirty. Please commit any changes and try again."
20
20
  raise RuntimeError(msg)
21
21
  upstream, release_branch = create_release_branch(repo, version)
22
+ original_main_sha = upstream.refs.main.commit.hexsha
22
23
  main_pushed = False
23
24
  tag_pushed = False
24
25
  release_created = False
25
- original_main_sha = upstream.refs.main.commit.hexsha
26
26
  try:
27
- release_commit = release_changelog(repo, version)
28
- tag = tag_release_commit(release_commit, repo, version)
29
- print("push release commit") # noqa: T201
30
- repo.git.push(upstream.name, f"{release_branch}:main", "-f")
31
- main_pushed = True
32
- print("push release tag") # noqa: T201
33
- repo.git.push(upstream.name, tag, "-f")
34
- tag_pushed = True
35
- create_github_release(version)
36
- release_created = True
37
- print("checkout main to new release and delete release branch") # noqa: T201
38
- repo.heads.main.checkout()
39
- repo.delete_head(release_branch, force=True)
40
- print("delete remote release branch") # noqa: T201
41
- repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
42
- upstream.fetch()
43
- repo.git.reset("--hard", f"{upstream.name}/main")
44
- print("All done!") # noqa: T201
27
+ main_pushed, tag_pushed, release_created = push_release(repo, upstream, release_branch, version)
28
+ finalize_release(repo, upstream, release_branch)
45
29
  except Exception:
46
30
  cleanup_failed_release(
47
31
  repo,
@@ -76,6 +60,17 @@ def get_upstream(repo: Repo) -> Remote:
76
60
  raise RuntimeError(msg)
77
61
 
78
62
 
63
+ def push_release(repo: Repo, upstream: Remote, release_branch: Head, version: Version) -> tuple[bool, bool, bool]:
64
+ release_commit = release_changelog(repo, version)
65
+ tag = tag_release_commit(release_commit, repo, version)
66
+ print("push release commit") # noqa: T201
67
+ repo.git.push(upstream.name, f"{release_branch}:main", "-f")
68
+ print("push release tag") # noqa: T201
69
+ repo.git.push(upstream.name, tag, "-f")
70
+ create_github_release(version)
71
+ return True, True, True
72
+
73
+
79
74
  def release_changelog(repo: Repo, version: Version) -> Commit:
80
75
  print("generate release commit") # noqa: T201
81
76
  check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607
@@ -121,6 +116,17 @@ def create_github_release(version: Version) -> None:
121
116
  raise
122
117
 
123
118
 
119
+ def finalize_release(repo: Repo, upstream: Remote, release_branch: Head) -> None:
120
+ print("checkout main to new release and delete release branch") # noqa: T201
121
+ repo.heads.main.checkout()
122
+ repo.delete_head(release_branch, force=True)
123
+ print("delete remote release branch") # noqa: T201
124
+ repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
125
+ upstream.fetch()
126
+ repo.git.reset("--hard", f"{upstream.name}/main")
127
+ print("All done!") # noqa: T201
128
+
129
+
124
130
  def cleanup_failed_release( # noqa: PLR0913
125
131
  repo: Repo,
126
132
  upstream: Remote,
@@ -180,6 +180,7 @@ def test_specifier_satisfies_with_partial_information() -> None:
180
180
  pytest.param("cpython3.12-64", None, id="no-machine"),
181
181
  pytest.param("cpython3.12", None, id="no-arch-no-machine"),
182
182
  pytest.param("python3.12-64-arm64", "arm64", id="python-impl"),
183
+ pytest.param("cpython3.14-64-i86pc.64bit", "i86pc.64bit", id="dotted-machine"),
183
184
  ],
184
185
  )
185
186
  def test_spec_parse_machine(spec_str: str, expected_machine: str | None) -> None: