spacr.qt.thread_guard

Report timer starts attempted from the wrong Qt thread.

Qt normally reports only that a timer cannot start from another thread, without naming the responsible object or call site. This module wraps the timer entry points and logs a Python stack when that condition occurs. The recorded stacks can also be attached to a crash report if the process exits before the log is reviewed.

Each timer start adds one thread-affinity comparison. If the Qt entry points cannot be wrapped safely, the guard leaves them unchanged.

Functions

born_off_thread(→ list)

Stacks where a QObject was constructed away from the GUI thread.

install(→ bool)

Wrap the timer entry points. Returns whether it took.

offences(→ list)

Copies of the stacks recorded so far.

Module Contents

spacr.qt.thread_guard.born_off_thread() → list[source]

Stacks where a QObject was constructed away from the GUI thread.

spacr.qt.thread_guard.install() → bool[source]

Wrap the timer entry points. Returns whether it took.

Idempotent: called twice, the second call does nothing rather than wrapping the wrapper, which would double every log line and make the stacks harder to read rather than easier.

spacr.qt.thread_guard.offences() → list[source]

Copies of the stacks recorded so far.

Nested helpers

install._report(what: str, why: str) → None

Record and log one cross-thread offence, with its stack.

The stack is trimmed of this function and its caller, so the top frame is the CODE THAT DID IT rather than the guard that noticed.

spacr/qt/thread_guard.py:59

install._wrong_thread(obj) → str

Why this start is illegal, or “” when it is fine.

ASKS EXACTLY WHAT Qt ASKS: is the object’s own thread the thread calling? Qt refuses whenever they differ, which happens both ways round – a worker touching a GUI object, and the GUI thread touching a WORKER-AFFINE object, the second being far easier to write by accident because the code reads as ordinary GUI-thread code.

COMPARED WITH ==, NOT is. QThread.currentThread() hands back a fresh Python wrapper around the same underlying QThread on each call, so an identity test reports every ordinary start as illegal – which is precisely what the first version of this did, flagging a plain GUI-thread timer.start() as “the caller is Qt mainThread, not the GUI thread”. A guard that cries wolf on the common path is worse than none, because the one line that matters is then buried.

There is deliberately no “is the caller the GUI thread” fallback. It added nothing the affinity test does not already cover, and it was the half that misfired.

spacr/qt/thread_guard.py:75

install.guarded_object_init(self, *args, **kwargs)

Note a QObject built off the main thread, after building it.

spacr/qt/thread_guard.py:128

install.guarded_object_start(self, *args, **kwargs)

Report a QObject timer started off its own thread, then start it.

spacr/qt/thread_guard.py:119

install.guarded_timer_start(self, *args, **kwargs)

Report a QTimer started off its own thread, then start it.

REPORTS AND PROCEEDS. The guard is a diagnostic: refusing the start would change behaviour under the guard and hide the bug it exists to find.

spacr/qt/thread_guard.py:107