pyside6-qml-models-services

PySide6 QML Models & Services

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "pyside6-qml-models-services" with this command: npx skills add ds-codi/project-memory-mcp/ds-codi-project-memory-mcp-pyside6-qml-models-services

PySide6 QML Models & Services

The model and service layers form the backend of the MVC architecture. Models define data structures with Qt signal support; services handle all external interactions; the signal registry provides decoupled cross-layer communication.

Model Layer

BaseModel

All domain models inherit from BaseModel to gain Qt signal support and serialization:

"""models/base.py""" from datetime import datetime from typing import Any, TypeVar, Type from PySide6.QtCore import QObject, Signal

T = TypeVar('T', bound='BaseModel')

class BaseModel(QObject): """ Base class for all domain models.

Provides:
- property_changed(name, value) signal for reactive updates
- changed() signal for any modification
- _set_property() helper for consistent change notification
- to_dict() / from_dict() for serialization
"""

property_changed = Signal(str, object)  # name, value
changed = Signal()

def __init__(self, parent: QObject | None = None) -> None:
    super().__init__(parent)
    self._created_at: datetime = datetime.now()
    self._updated_at: datetime = datetime.now()

@property
def created_at(self) -> datetime:
    return self._created_at

@property
def updated_at(self) -> datetime:
    return self._updated_at

def _set_property(self, name: str, old_value: Any, new_value: Any) -> bool:
    """Set a property, emit signals if changed. Returns True if changed."""
    if old_value != new_value:
        self._updated_at = datetime.now()
        self.property_changed.emit(name, new_value)
        self.changed.emit()
        return True
    return False

def to_dict(self) -> dict[str, Any]:
    raise NotImplementedError("Subclasses must implement to_dict()")

@classmethod
def from_dict(cls: Type[T], data: dict[str, Any]) -> T:
    raise NotImplementedError("Subclasses must implement from_dict()")

Domain Model Example

"""models/job.py""" from dataclasses import dataclass, field from datetime import datetime from enum import Enum from pathlib import Path from typing import Any, Optional

from PySide6.QtCore import Signal from my_app.models.base import BaseModel

class JobStatus(Enum): ACTIVE = "active" COMPLETE = "complete" ARCHIVED = "archived" ON_HOLD = "on_hold"

@dataclass class JobFile: """Value object for files associated with a job.""" path: Path file_type: str file_name: str size_bytes: int = 0 modified_at: datetime = field(default_factory=datetime.now)

def to_dict(self) -> dict[str, Any]:
    return {
        "path": str(self.path),
        "file_type": self.file_type,
        "file_name": self.file_name,
        "size_bytes": self.size_bytes,
        "modified_at": self.modified_at.isoformat(),
    }

@classmethod
def from_dict(cls, data: dict[str, Any]) -> "JobFile":
    return cls(
        path=Path(data["path"]),
        file_type=data["file_type"],
        file_name=data["file_name"],
        size_bytes=data.get("size_bytes", 0),
        modified_at=datetime.fromisoformat(data.get("modified_at", "")),
    )

class Job(BaseModel): """Domain model for a production job."""

files_updated = Signal()

def __init__(
    self,
    job_number: str,
    job_name: str = "",
    status: JobStatus = JobStatus.ACTIVE,
    parent=None,
) -> None:
    super().__init__(parent)
    self._job_number = job_number
    self._job_name = job_name
    self._status = status
    self._files: list[JobFile] = []

# --- Properties with change notification ---

@property
def job_number(self) -> str:
    return self._job_number

@property
def job_name(self) -> str:
    return self._job_name

@job_name.setter
def job_name(self, value: str) -> None:
    if self._set_property("job_name", self._job_name, value):
        self._job_name = value

@property
def status(self) -> JobStatus:
    return self._status

@status.setter
def status(self, value: JobStatus) -> None:
    if self._set_property("status", self._status, value):
        self._status = value

@property
def files(self) -> list[JobFile]:
    return list(self._files)

def add_file(self, file: JobFile) -> None:
    self._files.append(file)
    self.files_updated.emit()
    self.changed.emit()

# --- Serialization ---

def to_dict(self) -> dict[str, Any]:
    return {
        "job_number": self._job_number,
        "job_name": self._job_name,
        "status": self._status.value,
        "files": [f.to_dict() for f in self._files],
        "created_at": self._created_at.isoformat(),
        "updated_at": self._updated_at.isoformat(),
    }

@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Job":
    job = cls(
        job_number=data["job_number"],
        job_name=data.get("job_name", ""),
        status=JobStatus(data.get("status", "active")),
    )
    for f in data.get("files", []):
        job._files.append(JobFile.from_dict(f))
    return job

Model Design Rules

Rule Rationale

Always use _set_property() for mutable fields Ensures signals fire consistently

Use @dataclass for value objects (no signals needed) Lightweight, immutable data

Use BaseModel for entities tracked by controllers Reactive updates to UI

Never import services or views in models Pure data layer

Implement to_dict() / from_dict()

Enables persistence and serialization

Use Enums for constrained values Type safety, IDE autocomplete

Application State

A singleton that tracks transient UI state (not persisted domain data):

"""models/state.py""" from PySide6.QtCore import QObject, Signal

class ApplicationState(QObject): """ Singleton for transient application state.

Tracks current UI state such as active selection,
navigation position, and loading flags.
"""

active_job_changed = Signal(str)  # job_id or ""
navigation_changed = Signal(int)  # page index

_instance = None

def __new__(cls):
    if cls._instance is None:
        cls._instance = super().__new__(cls)
        cls._instance._init_state()
    return cls._instance

def _init_state(self):
    super().__init__()
    self._active_job_id: str | None = None
    self._current_page: int = 0

@property
def active_job_id(self) -> str | None:
    return self._active_job_id

@active_job_id.setter
def active_job_id(self, value: str | None) -> None:
    if self._active_job_id != value:
        self._active_job_id = value
        self.active_job_changed.emit(value or "")

@property
def current_page(self) -> int:
    return self._current_page

@current_page.setter
def current_page(self, value: int) -> None:
    if self._current_page != value:
        self._current_page = value
        self.navigation_changed.emit(value)

Signal Registry

Central registry of all application-wide signals. Views, controllers, and bridges connect to these:

"""utils/signals.py""" from PySide6.QtCore import QObject, Signal

class SignalRegistry(QObject): """ Central signal definitions for the application.

All cross-layer communication flows through these signals.
Organized by domain area.
"""

# --- Job Signals ---
job_changed = Signal(str)           # job_id
job_created = Signal(str)           # job_id
job_updated = Signal(str, str)      # job_id, field
job_deleted = Signal(str)           # job_id
job_files_updated = Signal(str, int)  # job_id, file_count

# --- Settings Signals ---
settings_changed = Signal(str, object)  # key, value
settings_reset = Signal(str)            # category or ""

# --- Connection Signals ---
broker_status_changed = Signal(bool)     # connected
broker_message_received = Signal(str, str)  # topic, payload

# --- UI Signals ---
theme_changed = Signal(str)         # "light" or "dark"
view_focus_requested = Signal(str)  # view_name
error_occurred = Signal(str, str)   # title, message
app_closing = Signal()

Singleton accessor

_registry: SignalRegistry | None = None

def get_signal_registry() -> SignalRegistry: global _registry if _registry is None: _registry = SignalRegistry() return _registry

Service Layer

Repository Pattern

"""services/database/jobs_repository.py""" import sqlite3 import logging from pathlib import Path from typing import Optional

from my_app.models.job import Job

logger = logging.getLogger(name)

class JobsRepository: """ SQLite-backed repository for job persistence.

Follows the repository pattern — controllers call these methods,
never raw SQL.
"""

def __init__(self, db_path: Path) -> None:
    self._db_path = db_path
    self._ensure_schema()

def _ensure_schema(self) -> None:
    with sqlite3.connect(self._db_path) as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS jobs (
                job_number TEXT PRIMARY KEY,
                job_name TEXT DEFAULT '',
                status TEXT DEFAULT 'active',
                data_json TEXT DEFAULT '{}',
                created_at TEXT,
                updated_at TEXT
            )
        """)

def get_job(self, job_number: str) -> Optional[Job]:
    with sqlite3.connect(self._db_path) as conn:
        conn.row_factory = sqlite3.Row
        row = conn.execute(
            "SELECT * FROM jobs WHERE job_number = ?",
            (job_number,),
        ).fetchone()
    if row:
        return self._row_to_job(row)
    return None

def save_job(self, job: Job) -> None:
    import json
    data = job.to_dict()
    with sqlite3.connect(self._db_path) as conn:
        conn.execute("""
            INSERT OR REPLACE INTO jobs
            (job_number, job_name, status, data_json, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (
            data["job_number"],
            data["job_name"],
            data["status"],
            json.dumps(data),
            data["created_at"],
            data["updated_at"],
        ))

def get_all_jobs(self) -> list[Job]:
    with sqlite3.connect(self._db_path) as conn:
        conn.row_factory = sqlite3.Row
        rows = conn.execute(
            "SELECT * FROM jobs ORDER BY updated_at DESC"
        ).fetchall()
    return [self._row_to_job(r) for r in rows]

def delete_job(self, job_number: str) -> bool:
    with sqlite3.connect(self._db_path) as conn:
        cursor = conn.execute(
            "DELETE FROM jobs WHERE job_number = ?",
            (job_number,),
        )
    return cursor.rowcount > 0

def _row_to_job(self, row) -> Job:
    import json
    data = json.loads(row["data_json"])
    return Job.from_dict(data)

Service Pattern

Services wrap external interactions — file I/O, network calls, subprocess execution:

"""services/file_discovery.py""" import logging from pathlib import Path

logger = logging.getLogger(name)

class FileDiscoveryService: """Discovers and indexes files in job directories."""

SUPPORTED_EXTENSIONS = {".dxf", ".pdf", ".top", ".xml", ".nc"}

def __init__(self, base_path: Path) -> None:
    self._base_path = base_path

def initialize(self) -> None:
    self._base_path.mkdir(parents=True, exist_ok=True)

def shutdown(self) -> None:
    pass

def discover_files(self, job_number: str) -> list[dict]:
    job_dir = self._base_path / job_number
    if not job_dir.exists():
        return []

    results = []
    for path in job_dir.rglob("*"):
        if path.is_file() and path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
            results.append({
                "path": str(path),
                "file_type": path.suffix.lstrip(".").lower(),
                "file_name": path.name,
                "size_bytes": path.stat().st_size,
            })
    return results

Service Design Rules

Rule Rationale

Services have initialize() and shutdown() lifecycle methods Clean startup/teardown

Services never import models, views, or controllers Pure I/O layer

Services return plain data (dicts, lists, primitives) or model instances No Qt types leaked

One service per external system Database, file I/O, broker, API each get their own

Services are injected via constructor, never imported directly by views Testable, replaceable

Controller Layer

Controllers wire models and services together, emitting signals for the bridge/view layer:

"""controllers/base.py""" from typing import Any, Protocol from PySide6.QtCore import QObject from my_app.utils.signals import SignalRegistry

class IView(Protocol): def bind(self, controller: "BaseController") -> None: ... def update_display(self, data: dict[str, Any]) -> None: ...

class BaseController(QObject): """ Base class for all controllers.

Provides signal registry access, view registration,
and lifecycle management.
"""

def __init__(self, signals: SignalRegistry, parent: QObject | None = None):
    super().__init__(parent)
    self._signals = signals
    self._views: list[IView] = []
    self._initialized = False

@property
def signals(self) -> SignalRegistry:
    return self._signals

def register_view(self, view: IView) -> None:
    if view not in self._views:
        self._views.append(view)
        view.bind(self)

def notify_views(self, data: dict[str, Any] | None = None) -> None:
    for view in self._views:
        view.update_display(data or {})

def initialize(self) -> None:
    self._initialized = True

def cleanup(self) -> None:
    self._views.clear()

Domain Controller Example

"""controllers/job_controller.py""" import logging from PySide6.QtCore import QObject

from my_app.controllers.base import BaseController from my_app.models.job import Job, JobStatus from my_app.models.state import ApplicationState from my_app.services.database.jobs_repository import JobsRepository from my_app.utils.signals import SignalRegistry

logger = logging.getLogger(name)

class JobController(BaseController): """ Handles job operations: activate, create, update, search.

Delegates persistence to JobsRepository and
emits signals via SignalRegistry for UI updates.
"""

def __init__(
    self,
    signals: SignalRegistry,
    repository: JobsRepository,
    parent: QObject | None = None,
) -> None:
    super().__init__(signals, parent)
    self._repository = repository
    self._state = ApplicationState()

def initialize(self) -> None:
    super().initialize()
    if self._state.active_job_id:
        job = self._repository.get_job(self._state.active_job_id)
        if job is None:
            self._state.active_job_id = None

def activate_job(self, job_id: str) -> bool:
    job = self._repository.get_job(job_id)
    if job is None:
        logger.warning(f"Job {job_id} not found")
        return False

    self._state.active_job_id = job_id
    self._signals.job_changed.emit(job_id)
    logger.info(f"Activated job {job_id}")
    return True

def create_job(self, job_number: str) -> bool:
    if self._repository.get_job(job_number):
        logger.warning(f"Job {job_number} already exists")
        return False

    job = Job(job_number=job_number)
    self._repository.save_job(job)
    self._signals.job_created.emit(job_number)
    return True

def get_job(self, job_id: str) -> Job | None:
    return self._repository.get_job(job_id)

def get_all_jobs(self) -> list[Job]:
    return self._repository.get_all_jobs()

Controller Design Rules

Rule Rationale

Controllers receive services via constructor injection Testable, no hidden dependencies

Controllers emit signals, never manipulate UI Decoupled from view layer

One controller per domain area Job, Settings, Script, etc.

Controllers read/write via repositories, not raw DB Abstraction, testability

Controllers update ApplicationState for UI-relevant state Centralized state tracking

Testing Approach

"""tests/test_job_controller.py""" from unittest.mock import MagicMock from my_app.controllers.job_controller import JobController from my_app.utils.signals import SignalRegistry from my_app.models.job import Job

def test_activate_job_emits_signal(): signals = SignalRegistry() repo = MagicMock() repo.get_job.return_value = Job(job_number="1234567")

controller = JobController(signals=signals, repository=repo)
handler = MagicMock()
signals.job_changed.connect(handler)

result = controller.activate_job("1234567")

assert result is True
handler.assert_called_once_with("1234567")

def test_activate_nonexistent_job_returns_false(): signals = SignalRegistry() repo = MagicMock() repo.get_job.return_value = None

controller = JobController(signals=signals, repository=repo)
result = controller.activate_job("9999999")

assert result is False

References

  • PySide6 Signals and Slots

  • PySide6 QAbstractListModel

  • Repository Pattern

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

pyside6-mvc

No summary provided by upstream source.

Repository SourceNeeds Review
General

pyside6-qml-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
General

pyside6-qml-views

No summary provided by upstream source.

Repository SourceNeeds Review