Skip to content

SKILL: PyQt MVP with Service + Pure Model + Qt Bridge

Purpose

Generate a PyQt application using a strict MVP architecture with:

  • Pure Python Model (no Qt)
  • Service layer (data producers, threads, ROS, IO)
  • Python event system (no Qt in model)
  • Presenter as the ONLY Qt bridge
  • Thread-safe UI updates via Qt signals
  • Dedicated package folders for models, services, views, presenters, and tests

Architecture Overview

Service (thread)
Model (pure Python)
Python Event (callback)
Presenter (bridge)
Qt Signal
Qt Slot (main thread)
View (UI)

Rules

0. Project Layout (REQUIRED)

Always create a dedicated folder for each architectural type:

project_root/
  app_package/
    __init__.py
    app.py                  # composition root
    models/
      __init__.py           # Event class lives here
      x_model.py
    services/
      __init__.py
      x_service.py
    views/
      __init__.py
      x_view.py
    presenters/
      __init__.py
      x_presenter.py
  tests/
    __init__.py
    mock_data.py            # generated / mocked model data
    test_x_model.py

Rules:

  • Do NOT place model.py, service.py, view.py, or presenter.py directly in the app package root.
  • Use plural folder names: models, services, views, presenters.
  • Keep app.py as the composition root that wires all objects together.
  • Tests must be outside the runtime package in a top-level tests/ folder.

1. Model (STRICT)

  • Must NOT import Qt
  • Must NOT use pyqtSignal / QObject
  • Must use pure Python (dataclass preferred)
  • Must expose events using a custom Event class
  • Model files must live under models/
  • Models import Event from their own package: from . import Event

Example:

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass, field

from . import Event


@dataclass
class Model:
    value: int = 0
    value_changed: Event = field(default_factory=Event)

2. Event System (REQUIRED)

Implement the shared Python callback system in models/__init__.py.

Example:

from collections.abc import Callable
from threading import Lock
from typing import Generic, TypeVar


T = TypeVar("T")


class Event(Generic[T]):
    def __init__(self) -> None:
        self._subs: list[Callable[[T], None]] = []
        self._lock = Lock()

    def subscribe(self, cb: Callable[[T], None]) -> None:
        with self._lock:
            self._subs.append(cb)

    def emit(self, value: T) -> None:
        with self._lock:
            subscribers = tuple(self._subs)

        for cb in subscribers:
            cb(value)

Minimal form:

class Event:
    def __init__(self):
        self._subs = []

    def subscribe(self, cb):
        self._subs.append(cb)

    def emit(self, value):
        for cb in self._subs:
            cb(value)

3. Service (STRICT)

  • Must NOT import Qt
  • Responsible for:

  • Threads / IO / ROS / external data

  • Must update model only
  • Must NOT touch View
  • Must NOT emit Qt signals
  • Service files must live under services/
  • Services may provide deterministic helper methods such as generate_once() to support tests

Example:

1
2
3
4
5
6
class Service:
    def __init__(self, model):
        self.model = model

    def run(self):
        self.model.set_value(new_value)

4. View (STRICT)

  • Only Qt widgets
  • No logic
  • No model access
  • Expose user actions via Qt signals
  • View files must live under views/

Example:

1
2
3
4
5
class View(QWidget):
    clicked = pyqtSignal()

    def set_value(self, v):
        self.label.setText(str(v))

5. Presenter (CRITICAL COMPONENT)

Responsibilities:

  • Connect View ↔ Service
  • Subscribe to Model events (Python callbacks)
  • Convert Python callbacks → Qt signals
  • Update View ONLY inside Qt slot
  • Presenter files must live under presenters/

Pattern:

class Presenter(QObject):
    model_signal = pyqtSignal(int)

    def __init__(self, model, service, view):
        super().__init__()

        self.model = model
        self.service = service
        self.view = view

        # Python callback
        self.model.value_changed.subscribe(self._on_model_changed)

        # Qt bridge
        self.model_signal.connect(self._on_model_changed_qt)

    def _on_model_changed(self, value):
        # MAY BE FROM ANY THREAD
        self.model_signal.emit(value)

    @pyqtSlot(int)
    def _on_model_changed_qt(self, value):
        # SAFE UI UPDATE
        self.view.set_value(value)

6. Thread Safety (MANDATORY RULE)

NEVER update UI from:

  • service
  • model
  • python callback

ALWAYS use:

callback → pyqtSignal → pyqtSlot → UI

7. Composition Root

All objects must be created in one place:

1
2
3
4
5
6
class App:
    def __init__(self):
        self.model = Model()
        self.service = Service(self.model)
        self.view = View()
        self.presenter = Presenter(self.model, self.service, self.view)

Rules:

  • Keep references to presenters
  • Do not create objects inside other classes implicitly
  • Import models, services, views, and presenters from their dedicated packages

8. Data Flow Rules

  • Service updates Model
  • Model emits Python event
  • Presenter listens
  • Presenter emits Qt signal
  • Presenter updates View

NO shortcuts allowed.


9. Forbidden Patterns

❌ Model inherits QObject ❌ Service uses Qt ❌ View accesses Model directly ❌ Presenter polls Model ❌ UI updated from background thread


10. Extensions

When generating advanced code, support:

  • multiple services
  • multiple presenters
  • shared model
  • background threads
  • ROS / sockets / file IO

11. Tests + Mock Data (REQUIRED)

Every generated app must include a top-level tests/ folder.

Required test files:

1
2
3
4
tests/
  __init__.py
  mock_data.py
  test_x_model.py

Rules:

  • tests/mock_data.py contains generated or mocked data for models.
  • Model tests must verify that mocked data is stored correctly.
  • Model tests must verify that model events emit mocked data.
  • Test code must not import Qt unless it is specifically testing a view or presenter.
  • Prefer pure model tests first because they are fast and do not require a display server.
  • Use standard-library unittest unless the project already has a configured test runner.

Example:

1
2
3
4
5
6
# tests/mock_data.py
MOCK_VALUES = ("one", "two", "three")


def make_values():
    return MOCK_VALUES
import unittest

from app_package.models import XModel

from mock_data import make_values


class TestXModel(unittest.TestCase):
    def test_model_stores_mock_values(self):
        model = XModel()
        values = make_values()

        model.set_values(values)

        self.assertEqual(model.values(), values)

Output Requirements

When generating code:

  • Always include:

  • Event class

  • Model
  • Service
  • View
  • Presenter
  • Main / composition root
  • Tests folder
  • Mock model data
  • Keep files modular using the required folder layout
  • Use clear naming: XModel, XService, XView, XPresenter

Goal

Produce scalable, thread-safe UI architecture suitable for:

  • robotics dashboards
  • streaming data visualization
  • real-time monitoring systems