Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
hooks:
- id: pyproject-fmt
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.13"
rev: "v0.15.14"
hooks:
- id: ruff-format
- id: ruff
Expand Down
16 changes: 9 additions & 7 deletions src/python_discovery/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,20 +314,22 @@ def __repr__(self) -> str:
content += " with =>"
for file_path in self.path.iterdir():
try:
if file_path.is_dir():
continue
if IS_WIN: # pragma: win32 cover
pathext = self.env.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
if not any(file_path.name.upper().endswith(ext) for ext in pathext):
continue
elif not (file_path.stat().st_mode & os.X_OK):
if not self._is_executable(file_path):
continue
except OSError:
pass
content += " "
content += file_path.name
return content

def _is_executable(self, file_path: Path) -> bool:
if file_path.is_dir():
return False
if IS_WIN: # pragma: win32 cover
pathext = self.env.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")
return any(file_path.name.upper().endswith(ext) for ext in pathext)
return bool(file_path.stat().st_mode & os.X_OK)


def path_exe_finder(
spec: PythonSpec, *, all_implementations: bool = False
Expand Down
58 changes: 27 additions & 31 deletions src/python_discovery/_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from typing import TYPE_CHECKING, ClassVar, Final, NamedTuple

if TYPE_CHECKING:
import tkinter as tk
from collections.abc import Generator, Mapping

from ._cache import PyInfoCache
Expand Down Expand Up @@ -165,42 +166,37 @@ def _get_tcl_tk_libs() -> tuple[
try:
tcl = tk.Tcl()
tcl_lib = tcl.eval("info library")

# Try to get TK library path directly first
try:
tk_lib = tcl.eval("set tk_library")
if tk_lib and os.path.isdir(tk_lib):
pass # We found it directly
else:
tk_lib = None # Reset if invalid
except tk.TclError:
tk_lib = None

# If direct query failed, try constructing the path
if tk_lib is None:
tk_version = tcl.eval("package require Tk")
tcl_parent = os.path.dirname(tcl_lib)

# Try different version formats
version_variants = [
tk_version, # Full version like "8.6.12"
".".join(tk_version.split(".")[:2]), # Major.minor like "8.6"
tk_version.split(".")[0], # Just major like "8"
]

for version in version_variants:
tk_lib_path = os.path.join(tcl_parent, f"tk{version}")
if not os.path.isdir(tk_lib_path):
continue
if os.path.exists(os.path.join(tk_lib_path, "tk.tcl")):
tk_lib = tk_lib_path
break

tk_lib = PythonInfo._resolve_tk_lib(tcl, tcl_lib)
except tk.TclError:
pass

return tcl_lib, tk_lib

@staticmethod
def _query_tk_library(tcl: tk.Tk) -> str | None: # pragma: no cover
"""Try to get the TK library path directly from Tcl."""
import tkinter as tk # noqa: PLC0415

try:
if (tk_lib := tcl.eval("set tk_library")) and os.path.isdir(tk_lib):
return tk_lib
except tk.TclError:
pass
return None

@staticmethod
def _resolve_tk_lib(tcl: tk.Tk, tcl_lib: str) -> str | None: # pragma: no cover
"""Resolve the TK library path by direct query or path construction."""
if (tk_lib := PythonInfo._query_tk_library(tcl)) is not None:
return tk_lib
tk_version = tcl.eval("package require Tk")
tcl_parent = os.path.dirname(tcl_lib)
for version in (tk_version, ".".join(tk_version.split(".")[:2]), tk_version.split(".")[0]):
tk_lib_path = os.path.join(tcl_parent, f"tk{version}")
if os.path.isdir(tk_lib_path) and os.path.exists(os.path.join(tk_lib_path, "tk.tcl")):
return tk_lib_path
return None

def _fast_get_system_executable(self) -> str | None:
"""Try to get the system executable by just looking at properties."""
# if we're not in a virtual environment, this is already a system python, so return the original executable
Expand Down
22 changes: 11 additions & 11 deletions src/python_discovery/_windows/_pep514.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,23 @@ def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[
key_path = f"{hive_name}/{company}/{tag}"
try:
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
exe = get_value(ip_key, "ExecutablePath")
if exe is None:
ip = get_value(ip_key, None)
if ip is None:
msg(key_path, "no ExecutablePath or default for it")

else:
exe = os.path.join(ip, "python.exe")
if exe is not None and os.path.exists(exe):
args = get_value(ip_key, "ExecutableArguments")
return exe, args
if (exe := _resolve_exe(ip_key, key_path)) is not None and os.path.exists(exe):
return exe, get_value(ip_key, "ExecutableArguments")
msg(key_path, f"could not load exe with value {exe}")
except OSError:
msg(f"{key_path}/InstallPath", "missing")
return None


def _resolve_exe(ip_key: Any, key_path: str) -> str | None: # noqa: ANN401
if (exe := get_value(ip_key, "ExecutablePath")) is not None:
return exe
if (ip := get_value(ip_key, None)) is None:
msg(key_path, "no ExecutablePath or default for it")
return None
return os.path.join(ip, "python.exe")


def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # noqa: ANN401
arch_str = get_value(tag_key, "SysArchitecture")
if arch_str is not None:
Expand Down
44 changes: 25 additions & 19 deletions tasks/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,13 @@ def main(version_str: str) -> None:
msg = "Current repository is dirty. Please commit any changes and try again."
raise RuntimeError(msg)
upstream, release_branch = create_release_branch(repo, version)
original_main_sha = upstream.refs.main.commit.hexsha
main_pushed = False
tag_pushed = False
release_created = False
original_main_sha = upstream.refs.main.commit.hexsha
try:
release_commit = release_changelog(repo, version)
tag = tag_release_commit(release_commit, repo, version)
print("push release commit") # noqa: T201
repo.git.push(upstream.name, f"{release_branch}:main", "-f")
main_pushed = True
print("push release tag") # noqa: T201
repo.git.push(upstream.name, tag, "-f")
tag_pushed = True
create_github_release(version)
release_created = True
print("checkout main to new release and delete release branch") # noqa: T201
repo.heads.main.checkout()
repo.delete_head(release_branch, force=True)
print("delete remote release branch") # noqa: T201
repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
upstream.fetch()
repo.git.reset("--hard", f"{upstream.name}/main")
print("All done!") # noqa: T201
main_pushed, tag_pushed, release_created = push_release(repo, upstream, release_branch, version)
finalize_release(repo, upstream, release_branch)
except Exception:
cleanup_failed_release(
repo,
Expand Down Expand Up @@ -76,6 +60,17 @@ def get_upstream(repo: Repo) -> Remote:
raise RuntimeError(msg)


def push_release(repo: Repo, upstream: Remote, release_branch: Head, version: Version) -> tuple[bool, bool, bool]:
release_commit = release_changelog(repo, version)
tag = tag_release_commit(release_commit, repo, version)
print("push release commit") # noqa: T201
repo.git.push(upstream.name, f"{release_branch}:main", "-f")
print("push release tag") # noqa: T201
repo.git.push(upstream.name, tag, "-f")
create_github_release(version)
return True, True, True


def release_changelog(repo: Repo, version: Version) -> Commit:
print("generate release commit") # noqa: T201
check_call(["towncrier", "build", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) # noqa: S607
Expand Down Expand Up @@ -121,6 +116,17 @@ def create_github_release(version: Version) -> None:
raise


def finalize_release(repo: Repo, upstream: Remote, release_branch: Head) -> None:
print("checkout main to new release and delete release branch") # noqa: T201
repo.heads.main.checkout()
repo.delete_head(release_branch, force=True)
print("delete remote release branch") # noqa: T201
repo.git.push(upstream.name, f":{release_branch}", "--no-verify")
upstream.fetch()
repo.git.reset("--hard", f"{upstream.name}/main")
print("All done!") # noqa: T201


def cleanup_failed_release( # noqa: PLR0913
repo: Repo,
upstream: Remote,
Expand Down