Architecture
August 4, 2026 ยท View on GitHub
mpvQC is a PySide6 desktop application that follows an MVVM split: QML owns presentation, Python owns logic, and a dependency-injection container wires them together. This document is a starting point for reading the codebase. It explains what the layers are, how they communicate, and where tests live. It is intentionally high-level. The code is the source of truth.
For setup and daily workflow, see development.md.
MVVM split
flowchart LR
QML["QML Views<br/>qt/qml/"]
VM["View models<br/>mpvqc/viewmodels/"]
SVC["Services<br/>mpvqc/services/"]
QML -->|"properties, slots,<br/>signals"| VM
VM -->|"inject.attr"| SVC
classDef qml fill:#e3f2fd,stroke:#1565c0
classDef vm fill:#f3e5f5,stroke:#6a1b9a
classDef svc fill:#fff3e0,stroke:#e65100
class QML qml
class VM vm
class SVC svc
Views: qt/qml/
QML files describe what the user sees and how they interact. Views hold no business logic. They bind to a view model's properties, call its slots in response to user actions, and react to its signals.
QML modules under qt/qml/ follow a reverse-DNS naming convention that starts at the project's namespace. Imports use
the full module URI, not relative paths. The QQuickStyle override directory is the one intentional exception. It lives
outside the dotted tree because Qt resolves styles by a single directory name at the root of an import path.
View models: mpvqc/viewmodels/
View models are Python QObject subclasses exposed to QML via PySide6's @QmlElement. They translate between Qt's
signal/slot world and the underlying services: a view model pulls in services with inject.attr, exposes the data the
view needs as Qt properties, and turns user actions (Slots) into service calls. They register into a single QML module
that follows the same reverse-DNS convention as the QML-side modules. The folder layout under mpvqc/viewmodels/ groups
files by the consuming QML module. Not every view model lives there, though: see feature packages below.
Services: mpvqc/services/
Services hold the application's logic and own its mutable state. QML never talks to them directly: they are Python
classes that other services and view models pull in via inject.attr. A few define Qt types that view models hand
through to QML, such as the comment store and its selection state. Each service sits in its own module or package, and
mpvqc/injections.py binds them for the inject container. It is also the composition root: a feature package brings
its own bindings, and mpvqc/injections.py calls them.
Feature packages: mpvqc/<feature>/
Some areas are grouped by what they are about instead of by layer. A feature package holds its own domain types and the services, models, and view models the area owns, and brings its own bindings. It also owns the settings keys its area means, reading the file through the shared settings file service. A layer packages hold everything no feature package has claimed.
Bootstrap
The application's entry point sets up the inject container, hands it to the QML engine, and loads the root window. From there, view models resolve their service dependencies on demand. Startup wires the window-level services so they're available before the first user interaction.
Testing
Tests sit at three layers:
flowchart TB
subgraph Integration["QML integration tests: tst_MpvqcApplicationContent_*.qml"]
I1["Drives the application end-to-end<br/>through real menus, dialogs, services"]
end
subgraph QmlUnit["QML unit tests: qt/qml/.../tst_*.qml (colocated)"]
Q1["A single component against<br/>a mocked or real view model"]
end
subgraph PyUnit["Python tests: test/"]
P1["A service or view model<br/>in isolation, with pytest"]
end
Integration --> QmlUnit
QmlUnit --> PyUnit
classDef int fill:#ffebee,stroke:#b71c1c
classDef qml fill:#e3f2fd,stroke:#1565c0
classDef py fill:#e8f5e9,stroke:#1b5e20
class Integration int
class QmlUnit qml
class PyUnit py
Python tests: test/
Standard pytest suite. Each service and view model has its own test module that exercises it in isolation, often with
stubbed collaborators. Run with just test-python.
QML unit tests: colocated tst_*.qml
Each non-trivial QML file has a sibling tst_<Name>.qml that exercises that component in isolation. Where the component
depends on a view model, the test instantiates a mock view model inline. A small number of tests use a real view model
to cover model-binding paths that mocks can't fake. Run together with the integration tests via just test-qml.
QML integration tests: tst_MpvqcApplicationContent_*.qml
These drive the application content end-to-end against real, injected services. They click menu items, accept dialogs,
and assert against application state through a Python test bridge. The harness lives entirely under testqml/: a Python
entry point that boots a stripped-down application with the player swapped for a stub, bridges that expose inject state
to QML, service overrides that keep tests off the real OS, and shared fixtures.
TestHelpers.qml files keep test code short by exposing the shared interactions (opening menus, finding dialogs,
asserting state) as nested namespaces. They sit alongside the QML they help test. Each file's tests use the namespace
shape that fits its scope.
Build & resources
QML, icons, fonts, default configs, and translations are bundled into a single binary file via Qt's resource compiler:
just build-developrunspyside6-rcc --binaryto produce a resource bundle (project.rcc) at the repo root that packs every asset behindqrc:/...URLs. On startup,main.pyregisters the file withQResource.registerResource()before anything reads fromqrc:/. The file is gitignored. It is a build artifact and can be regenerated from sources.just prepare-testsrebuilds the bundle and stages copies for the test harnesses, which register it the same way the application does.- Release builds pre-compile QML files to bytecode for faster startup. Development and test runs load plain QML directly.
A helper under build-aux/ maintains the [tool.pyside6-project] files = [...] entry in pyproject.toml from the
project's source directories. It excludes generated files so the list stays a description of sources.
See also
- development.md: setup, build, test commands
- configuration.md: runtime environment variables
- internationalization.md: translation workflow
- releasing.md: release checklist