Found while modernizing this repo's packaging (PR pending on claude/rollout-modernize).
Symptom
SourceReader's documented contract allows a reader to be closed and re-opened — its own class docstring demonstrates exactly that, with open_count reaching 2 on the second with block. StatusInfoReader cannot do this:
from pchealthstream2py.pchealth import StatusInfoReader
import time
r = StatusInfoReader(read_interval_ms=50)
with r:
time.sleep(0.3)
with r: # RuntimeError: threads can only be started once
time.sleep(0.3)
Cause
StatusInfoReader is a threading.Thread (it subclasses both SourceReader and threading.Thread), and open() calls self.start(). A Thread object can only be started once, so the second open() always raises.
Suggested fix
Stop being a thread and have one instead: keep the polling loop in a plain function (or keep run() as the target) and have open() create a fresh threading.Thread(target=..., daemon=True) each time, storing it on the instance; close() sets the stop event and (optionally) joins it. That makes open/close/open work, keeps the buffering behaviour identical, and removes the remaining friction of inheriting Thread's API surface (start, join, is_alive, name, ...) into what is really a SourceReader.
This is a behaviour-visible change (isinstance(reader, threading.Thread) becomes False), which is why it was left out of the packaging-modernization PR rather than bundled into it.
Related, already fixed on claude/rollout-modernize
- the stop flag was named
_stop, shadowing threading.Thread._stop and making join() raise TypeError: 'Event' object is not callable;
_index / _data / _stop / _bt were class attributes, so all instances shared one queue and one stop flag.
Found while modernizing this repo's packaging (PR pending on
claude/rollout-modernize).Symptom
SourceReader's documented contract allows a reader to be closed and re-opened — its own class docstring demonstrates exactly that, withopen_countreaching 2 on the secondwithblock.StatusInfoReadercannot do this:Cause
StatusInfoReaderis athreading.Thread(it subclasses bothSourceReaderandthreading.Thread), andopen()callsself.start(). AThreadobject can only be started once, so the secondopen()always raises.Suggested fix
Stop being a thread and have one instead: keep the polling loop in a plain function (or keep
run()as the target) and haveopen()create a freshthreading.Thread(target=..., daemon=True)each time, storing it on the instance;close()sets the stop event and (optionally) joins it. That makes open/close/open work, keeps the buffering behaviour identical, and removes the remaining friction of inheritingThread's API surface (start,join,is_alive,name, ...) into what is really aSourceReader.This is a behaviour-visible change (
isinstance(reader, threading.Thread)becomes False), which is why it was left out of the packaging-modernization PR rather than bundled into it.Related, already fixed on
claude/rollout-modernize_stop, shadowingthreading.Thread._stopand makingjoin()raiseTypeError: 'Event' object is not callable;_index/_data/_stop/_btwere class attributes, so all instances shared one queue and one stop flag.