Skip to content

Stalker

Qt Base Class: QObject

Signature: QObject(self, parent: Optional[PySide6.QtCore.QObject] = None) -> None

Base classes

Name Children Inherits
Object
prettyqt.core.object

⋔ Inheritance diagram

graph TD
  1473367677120["debugging.Stalker"]
  1473299782816["core.Object"]
  1473299815024["core.ObjectMixin"]
  140713234304496["builtins.object"]
  1473288842240["QtCore.QObject"]
  1473291690208["Shiboken.Object"]
  1473299782816 --> 1473367677120
  1473299815024 --> 1473299782816
  140713234304496 --> 1473299815024
  1473288842240 --> 1473299782816
  1473291690208 --> 1473288842240
  140713234304496 --> 1473291690208

🛈 DocStrings

Bases: Object

Source code in prettyqt\debugging\stalker.py
class Stalker(core.Object):
    # @core.Enum
    # class LogLevel(enum.IntEnum):
    #     """Log level."""

    #     DEBUG = logging.DEBUG
    #     INFO = logging.INFO
    #     WARNING = logging.WARNING
    #     CRITICAL = logging.CRITICAL
    #     ERROR = logging.ERROR

    keypress_detected = core.Signal(str)
    leftclick_detected = core.Signal(core.QPointF)
    rightclick_detected = core.Signal(core.QPointF)
    event_detected = core.Signal(core.QEvent)
    signal_emitted = core.Signal(core.MetaMethod, object)  # signal, args
    signal_connected = core.Signal(core.MetaMethod)
    signal_disconnected = core.Signal(core.MetaMethod)

    def __init__(
        self,
        qobject: core.QObject,
        include=None,
        exclude=None,
        **kwargs,
    ):
        self._log_level = logging.INFO
        super().__init__(**kwargs)
        self._obj = qobject
        self._meta = core.MetaObject(self._obj.metaObject())
        self.counter: collections.defaultdict[int] = collections.defaultdict(int)
        self.signal_counter: collections.defaultdict[int] = collections.defaultdict(int)
        self.exclude = ["meta_call", "timer"] if exclude is None else exclude
        self.include = include
        self._handles: list[core.QMetaObject.Connection] = []

    def __enter__(self):
        self.hook()
        return self

    def __exit__(self, typ, value, traceback):
        self.unhook()

    @property
    def eventsignals(self):
        return EventSignaller(self._obj)

    def hook(self):
        # enable event logging by installing EventCatcher, which includes logging
        self.eventcatcher = eventfilters.EventCatcher(
            self.include, self.exclude, self._on_event_detected, parent=self._obj
        )
        self._obj.installEventFilter(self.eventcatcher)
        # enable logging of signals emitted by connecting all signals to our fn
        for signal in self._meta.get_signals(only_notifiers=False):
            signal_name = signal.get_name()
            # PyQt reports non-existing signals in MetaObject.
            if hasattr(self._obj, signal_name):
                signal_instance = self._obj.__getattribute__(signal_name)
            fn = self._on_signal_emitted(signal)
            handle = signal_instance.connect(fn)
            self._handles.append(handle)
        self.log(f"Stalking {len(self._handles)} signals")
        # enable logging of all signal (dis)connections by hooking to connectNotify
        self.old_connectNotify = self._obj.connectNotify
        self.old_disconnectNotify = self._obj.disconnectNotify
        self._obj.connectNotify = self._on_signal_connected
        self._obj.disconnectNotify = self._on_signal_disconnected
        # self._obj.destroyed.connect(self.unhook)
        # self.destroyed.connect(self.unhook)

    def unhook(self):
        if self.eventcatcher is None:
            logger.warning("unhook() called before hook()")
            return None
        """Clean up our mess."""
        self._obj.connectNotify = self.old_connectNotify
        self._obj.disconnectNotify = self.old_disconnectNotify
        self.old_connectNotify = None
        self.old_disconnectNotify = None
        for handle in self._handles:
            self._obj.disconnect(handle)
        self._handles = []
        with contextlib.suppress(RuntimeError):
            self._obj.removeEventFilter(self.eventcatcher)

    def log(self, message: str):
        if self.log_level:
            with contextlib.suppress(RuntimeError):
                logger.log(self._log_level, f"{self._obj!r}: {message}")

    def _on_signal_connected(self, qsignal: core.QMetaMethod):
        signal = core.MetaMethod(qsignal)
        self.log(f"Connected signal {signal.get_name()}")
        self.signal_connected.emit(signal)

    def _on_signal_disconnected(self, qsignal: core.QMetaMethod):
        signal = core.MetaMethod(qsignal)
        self.log(f"Disconnected signal {signal.get_name()}")
        self.signal_disconnected.emit(signal)

    def _on_event_detected(self, event) -> bool:
        """Used for EventCatcher, returns false to not eat signals."""
        try:
            self.event_detected.emit(event)
        except RuntimeError:
            return
        match event.type():
            case core.Event.Type.KeyPress:
                combo = gui.KeySequence(event.keyCombination()).toString()
                self.keypress_detected.emit(combo)
            case core.Event.Type.MouseButtonRelease:
                if event.button() == constants.MouseButton.LeftButton:
                    self.leftclick_detected.emit(event.position())
                if event.button() == constants.MouseButton.RightButton:
                    self.rightclick_detected.emit(event.position())
        self.log(f"Received event {event.type()!r}")
        self.counter[event.type()] += 1
        return False

    def _on_signal_emitted(self, signal: core.MetaMethod):
        def fn(*args, **kwargs):
            try:
                self.signal_emitted.emit(signal, args)
                self.signal_counter[signal.get_name()] += 1
                self.log(f"Emitted signal {signal.get_name()}{args}")
            except RuntimeError:
                pass

        return fn

    def set_log_level(self, level: int):
        self._log_level = level

    def get_log_level(self) -> int:
        return self._log_level

    def count_children(self, type_filter: type[T] = core.QObject) -> collections.Counter:
        objects = self.findChildren(type_filter)
        return collections.Counter([type(o) for o in objects])

    def show(self):
        from prettyqt import custom_widgets

        widget = custom_widgets.LogRecordTableView()
        widget.set_logger(logger, level=self.log_level)
        widget.show()

    log_level = core.Property(
        int,
        get_log_level,
        set_log_level,
        doc="Level to use for logging",
    )

⌗ Property table

Qt Property Type Doc
objectName QString
log_level int Level to use for logging