steam-backlog-enforcer/steam_backlog_enforcer/_pacman.py
Krzysztof kuhy Rudnicki 356530a01c
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), `socket.timeout` where `TimeoutError` is (UP041),
deprecated `typing` imports (UP035), PEP 695 `type` aliases (UP040), and ruff's
stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`.
That last one makes the source 3.14-only at the syntax level, which is what
`requires-python = ">=3.14"` already promised; kuhy chose it deliberately over
holding ruff a release back (2026-08-29).

ruff, ruff-format, mypy --strict and pylint all clean at the new target. The
two modified web/ TypeScript files are another session's in-flight work and
are deliberately left unstaged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:44:25 +02:00

79 lines
2.3 KiB
Python

"""Thin, reusable wrappers around the pacman package manager.
These three operations (query owner, query installed, remove) carry no
total-block policy of their own -- they are the generic primitives the
total-block purge is written in terms of, which is why they live here
rather than inside it.
"""
from __future__ import annotations
import logging
import shutil
import subprocess
logger = logging.getLogger(__name__)
_SUDO = shutil.which("sudo") or "/usr/bin/sudo"
# Call pacman.orig directly (bypassing pacman_wrapper's interactive
# word-unscramble challenge for "steam") - this is the tool's own
# authorized action, not a user bypass attempt, and enforce_total_block_tick
# must be able to run unattended.
PACMAN = shutil.which("pacman.orig") or shutil.which("pacman") or "/usr/bin/pacman.orig"
def pacman_owner(path: str) -> str | None:
"""Return the pacman package name that owns *path*, or None."""
result = subprocess.run(
[PACMAN, "-Qo", path],
capture_output=True,
text=True,
timeout=10,
check=False,
)
if result.returncode != 0:
return None
marker = " is owned by "
if marker not in result.stdout:
return None
tail = result.stdout.split(marker, 1)[1].strip()
return tail.split()[0] if tail else None
def is_package_installed(package: str) -> bool:
"""Return True if *package* is currently installed via pacman."""
result = subprocess.run(
[PACMAN, "-Qi", package],
capture_output=True,
timeout=10,
check=False,
)
return result.returncode == 0
def uninstall_package(package: str) -> bool:
"""Remove *package* via pacman. Returns True on success or if absent."""
try:
result = subprocess.run(
[_SUDO, PACMAN, "-R", "--noconfirm", package],
capture_output=True,
text=True,
timeout=120,
check=False,
)
except OSError, subprocess.SubprocessError:
logger.exception("Failed to run pacman -R %s", package)
return False
if result.returncode == 0:
return True
if "target not found" in (result.stderr or "").lower():
return True
logger.error(
"pacman -R %s failed (rc=%d): %s",
package,
result.returncode,
result.stderr[-500:] if result.stderr else "",
)
return False