Add type hints and ruff/mypy config

Annotate all functions and check them with mypy --strict (needs
python-pygobject-stubs). pyproject.toml holds only tool config since
the PKGBUILD installs the script directly.
This commit is contained in:
Daniele Fucini
2026-10-16 17:21:34 +02:00
parent 13c1afd7b6
commit a36e700df6
3 changed files with 48 additions and 25 deletions
+3
View File
@@ -1 +1,4 @@
CLAUDE.md CLAUDE.md
__pycache__/
.mypy_cache/
.ruff_cache/
+10
View File
@@ -0,0 +1,10 @@
[tool.ruff]
line-length = 120
extend-include = ["xautolock-inhibit"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP"]
[tool.mypy]
files = ["xautolock-inhibit"]
strict = true
+35 -25
View File
@@ -17,7 +17,7 @@ from datetime import timedelta
from gi.repository import Gio, GLib from gi.repository import Gio, GLib
try: try:
from gi.repository import GLibUnix # GLib >= 2.80 from gi.repository import GLibUnix # type: ignore[attr-defined] # GLib >= 2.80, not in pygobject-stubs
signal_add = GLibUnix.signal_add signal_add = GLibUnix.signal_add
except ImportError: except ImportError:
signal_add = GLib.unix_signal_add signal_add = GLib.unix_signal_add
@@ -27,6 +27,8 @@ OBJECT_PATHS = ("/org/freedesktop/ScreenSaver", "/ScreenSaver") # Vivaldi uses
EXIT_NAME_TAKEN = 2 EXIT_NAME_TAKEN = 2
ACTIVITY_QUIET = 300 # seconds without SimulateUserActivity that end a logged burst ACTIVITY_QUIET = 300 # seconds without SimulateUserActivity that end a logged burst
Cookie = tuple[str, str, str, bool] # sender, application_name, reason, counts
INTROSPECTION = """ INTROSPECTION = """
<node> <node>
<interface name="org.freedesktop.ScreenSaver"> <interface name="org.freedesktop.ScreenSaver">
@@ -58,11 +60,11 @@ INTROSPECTION = """
""" """
def log(msg): def log(msg: str) -> None:
print(msg, flush=True) print(msg, flush=True)
def xautolock(arg): def xautolock(arg: str) -> None:
try: try:
subprocess.run(["xautolock", arg], check=True, timeout=10) subprocess.run(["xautolock", arg], check=True, timeout=10)
log(f"xautolock {arg}") log(f"xautolock {arg}")
@@ -77,8 +79,8 @@ class XBlanking:
pause by itself when this client releases it or its connection closes (exit, crash). pause by itself when this client releases it or its connection closes (exit, crash).
""" """
def __init__(self): def __init__(self) -> None:
self.dpy = None self.dpy: int | None = None
try: try:
self.x11 = ctypes.CDLL("libX11.so.6") self.x11 = ctypes.CDLL("libX11.so.6")
self.xss = ctypes.CDLL("libXss.so.1") self.xss = ctypes.CDLL("libXss.so.1")
@@ -102,7 +104,7 @@ class XBlanking:
return return
self.dpy = dpy self.dpy = dpy
def suspend(self, on): def suspend(self, on: bool) -> None:
if self.dpy: if self.dpy:
self.xss.XScreenSaverSuspend(self.dpy, int(on)) self.xss.XScreenSaverSuspend(self.dpy, int(on))
self.x11.XFlush(self.dpy) self.x11.XFlush(self.dpy)
@@ -118,24 +120,24 @@ class ActivityBurst:
class Inhibitor: class Inhibitor:
def __init__(self, ignored_reasons, release_delay, blanking): def __init__(self, ignored_reasons: set[str], release_delay: int, blanking: XBlanking) -> None:
self.ignored_reasons = ignored_reasons self.ignored_reasons = ignored_reasons
self.blanking = blanking self.blanking = blanking
self.release_delay = release_delay self.release_delay = release_delay
self.cookies = {} # cookie -> (sender, application_name, reason, counts) self.cookies: dict[int, Cookie] = {}
self.next_cookie = itertools.count(1) self.next_cookie = itertools.count(1)
self.proc_names = {} # sender -> "comm[pid]", for logging only self.proc_names: dict[str, str] = {} # sender -> "comm[pid]", for logging only
self.activity: dict[str, ActivityBurst] = {} # sender -> SimulateUserActivity burst self.activity: dict[str, ActivityBurst] = {} # sender -> SimulateUserActivity burst
self.disabled = False self.disabled = False
self.release_timer = 0 self.release_timer = 0
self.conn = None self.conn: Gio.DBusConnection | None = None
# --- lock state ------------------------------------------------------- # --- lock state -------------------------------------------------------
def active_count(self): def active_count(self) -> int:
return sum(1 for *_, counts in self.cookies.values() if counts) return sum(1 for *_, counts in self.cookies.values() if counts)
def update(self): def update(self) -> None:
"""Run xautolock only on 0->1 and 1->0 transitions; delay re-enabling.""" """Run xautolock only on 0->1 and 1->0 transitions; delay re-enabling."""
active = self.active_count() active = self.active_count()
if active and self.release_timer: if active and self.release_timer:
@@ -147,18 +149,18 @@ class Inhibitor:
# Browsers send quick Inhibit/UnInhibit bursts when a video starts. # Browsers send quick Inhibit/UnInhibit bursts when a video starts.
self.release_timer = GLib.timeout_add_seconds(self.release_delay, self.on_release_timeout) self.release_timer = GLib.timeout_add_seconds(self.release_delay, self.on_release_timeout)
def on_release_timeout(self): def on_release_timeout(self) -> bool:
self.release_timer = 0 self.release_timer = 0
if not self.active_count() and self.disabled: if not self.active_count() and self.disabled:
self.unblock() self.unblock()
return GLib.SOURCE_REMOVE return GLib.SOURCE_REMOVE
def block(self): def block(self) -> None:
xautolock("-disable") xautolock("-disable")
self.blanking.suspend(True) self.blanking.suspend(True)
self.disabled = True self.disabled = True
def unblock(self): def unblock(self) -> None:
xautolock("-enable") xautolock("-enable")
self.blanking.suspend(False) self.blanking.suspend(False)
self.disabled = False self.disabled = False
@@ -174,8 +176,9 @@ class Inhibitor:
# --- D-Bus ------------------------------------------------------------ # --- D-Bus ------------------------------------------------------------
def proc_name(self, sender): def proc_name(self, sender: str) -> str:
if sender not in self.proc_names: if sender not in self.proc_names:
assert self.conn is not None # set in on_bus_acquired, before any method call
try: try:
pid = self.conn.call_sync( pid = self.conn.call_sync(
"org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus",
@@ -188,7 +191,7 @@ class Inhibitor:
self.proc_names[sender] = "?" self.proc_names[sender] = "?"
return f"{sender} {self.proc_names[sender]}" return f"{sender} {self.proc_names[sender]}"
def inhibit(self, sender, app, reason): def inhibit(self, sender: str, app: str, reason: str) -> int:
cookie = next(self.next_cookie) cookie = next(self.next_cookie)
counts = reason not in self.ignored_reasons counts = reason not in self.ignored_reasons
self.cookies[cookie] = (sender, app, reason, counts) self.cookies[cookie] = (sender, app, reason, counts)
@@ -197,7 +200,7 @@ class Inhibitor:
self.update() self.update()
return cookie return cookie
def uninhibit(self, sender, cookie): def uninhibit(self, sender: str, cookie: int) -> None:
entry = self.cookies.get(cookie) entry = self.cookies.get(cookie)
if entry is None or entry[0] != sender: if entry is None or entry[0] != sender:
log(f"UnInhibit {cookie} from {self.proc_name(sender)}: unknown cookie, ignored") log(f"UnInhibit {cookie} from {self.proc_name(sender)}: unknown cookie, ignored")
@@ -246,7 +249,10 @@ class Inhibitor:
if gone: if gone:
self.update() self.update()
def on_method_call(self, conn, sender, path, iface, method, params, invocation): def on_method_call(
self, conn: Gio.DBusConnection, sender: str, path: str, iface: str, method: str,
params: GLib.Variant, invocation: Gio.DBusMethodInvocation,
) -> None:
if method == "Inhibit": if method == "Inhibit":
app, reason = params.unpack() app, reason = params.unpack()
invocation.return_value(GLib.Variant("(u)", (self.inhibit(sender, app, reason),))) invocation.return_value(GLib.Variant("(u)", (self.inhibit(sender, app, reason),)))
@@ -265,12 +271,15 @@ class Inhibitor:
elif method in ("GetActiveTime", "GetSessionIdleTime"): elif method in ("GetActiveTime", "GetSessionIdleTime"):
invocation.return_value(GLib.Variant("(u)", (0,))) invocation.return_value(GLib.Variant("(u)", (0,)))
def on_name_owner_changed(self, conn, sender, path, iface, signal_name, params): def on_name_owner_changed(
self, conn: Gio.DBusConnection, sender: str, path: str, iface: str, signal_name: str,
params: GLib.Variant,
) -> None:
name, _old, new = params.unpack() name, _old, new = params.unpack()
if not new and name.startswith(":"): if not new and name.startswith(":"):
self.release_sender(name) self.release_sender(name)
def on_bus_acquired(self, conn, name): def on_bus_acquired(self, conn: Gio.DBusConnection, name: str) -> None:
self.conn = conn self.conn = conn
iface = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION).interfaces[0] iface = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION).interfaces[0]
for path in OBJECT_PATHS: for path in OBJECT_PATHS:
@@ -281,7 +290,7 @@ class Inhibitor:
) )
def main(): def main() -> None:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument( parser.add_argument(
"-i", "--ignore-reason", action="append", metavar="REASON", "-i", "--ignore-reason", action="append", metavar="REASON",
@@ -297,6 +306,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
ignored: set[str]
if args.count_all: if args.count_all:
ignored = set() ignored = set()
else: else:
@@ -306,7 +316,7 @@ def main():
exit_code = 0 exit_code = 0
acquired = False acquired = False
def on_name_acquired(conn, name): def on_name_acquired(conn: Gio.DBusConnection, name: str) -> None:
nonlocal acquired nonlocal acquired
acquired = True acquired = True
log(f"Owning {name}; ignored reasons: {sorted(ignored) or 'none'}") log(f"Owning {name}; ignored reasons: {sorted(ignored) or 'none'}")
@@ -314,7 +324,7 @@ def main():
# Only after owning the name, so a second instance can't undo an active inhibit. # Only after owning the name, so a second instance can't undo an active inhibit.
xautolock("-enable") xautolock("-enable")
def on_name_lost(conn, name): def on_name_lost(conn: Gio.DBusConnection | None, name: str) -> None:
nonlocal exit_code nonlocal exit_code
if conn is None: if conn is None:
log("Could not connect to the session bus") log("Could not connect to the session bus")
@@ -327,7 +337,7 @@ def main():
exit_code = EXIT_NAME_TAKEN exit_code = EXIT_NAME_TAKEN
loop.quit() loop.quit()
def on_signal(): def on_signal() -> bool:
log("Signal received, exiting") log("Signal received, exiting")
loop.quit() loop.quit()
return GLib.SOURCE_REMOVE return GLib.SOURCE_REMOVE