No description
Find a file
2026-03-06 17:02:26 +03:00
docs Initial Commit 2026-03-06 16:05:24 +03:00
pyqt6_scaffold Initial Commit 2026-03-06 16:05:24 +03:00
.gitignore Initial Commit 2026-03-06 16:05:24 +03:00
LICENSE Initial Commit 2026-03-06 16:05:24 +03:00
pyproject.toml Minor metadata fixes 2026-03-06 17:02:26 +03:00
README.md Initial Commit 2026-03-06 16:05:24 +03:00

PyQt6 Scaffold

A wrapper for PyQt6 designed for a more convenient workflow.

Русскоязычная Документация

License

This project is licensed under the LGPLv3 license. See the LICENSE file for details.

Description

This library provides a set of ready-to-use patterns and tools that can be immediately integrated into development to avoid common pitfalls in complex areas (such as database management or window navigation).

The library is divided into two main modules:

  • Core
  • Contrib

Installation

Base installation (core only):

pip install pyqt6-scaffold

With MySQL support:

pip install pyqt6-scaffold[mysql]

With PostgreSQL support:

pip install pyqt6-scaffold[postgres]

All drivers at once:

pip install pyqt6-scaffold[all]

Quick Start

1. Configure Environment Variables

Create a start.sh (Linux/macOS) or start.cmd (Windows) file:

# start.sh
export MYSQL_HOST=localhost
export MYSQL_PORT=3306
export MYSQL_USER=root
export MYSQL_DATABASE=mydb
export MYSQL_PASSWORD=secret
python main.py
:: start.cmd
set MYSQL_HOST=localhost
set MYSQL_PORT=3306
set MYSQL_USER=root
set MYSQL_DATABASE=mydb
set MYSQL_PASSWORD=secret
python main.py

2. Implement the Database

from pyqt6_scaffold import BaseUser
from pyqt6_scaffold.contrib import MysqlDatabase

class AppDatabase(MysqlDatabase):
    def auth(self, login: str, password: str):
        with self.execute(
            f"SELECT id, name FROM users WHERE login = {self.placeholder} AND password = {self.placeholder}",
            (login, password)
        ) as cursor:
            row = cursor.fetchone()
        if not row:
            return None
        return BaseUser(id=row[0], name=row[1], role=None)

3. Create Windows

from pyqt6_scaffold import BaseWindow
from PyQt6.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget

class LoginWindow(BaseWindow):
    def _define_widgets(self):
        self._login_input = QLineEdit()
        self._password_input = QLineEdit()
        self._password_input.setEchoMode(QLineEdit.EchoMode.Password)
        self._submit_btn = QPushButton("Login")

    def _tune_layouts(self):
        layout = QVBoxLayout()
        layout.addWidget(QLabel("Login:"))
        layout.addWidget(self._login_input)
        layout.addWidget(QLabel("Password:"))
        layout.addWidget(self._password_input)
        layout.addWidget(self._submit_btn)
        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def _connect_slots(self):
        self._submit_btn.clicked.connect(self._on_submit)

    def _apply_windows_settings(self):
        self.setWindowTitle("Login")

    def _on_submit(self):
        user = self._db.auth(
            self._login_input.text(),
            self._password_input.text()
        )
        if user is None:
            return
        from pyqt6_scaffold import NavigateRequest, NavigationContext
        self._composer.navigate_request.emit(
            NavigateRequest(
                target="main",
                context=NavigationContext(data={"user": user})
            )
        )

class MainWindow(BaseWindow):
    def _apply_windows_settings(self):
        self.setWindowTitle("Main Window")

4. Assemble the Application in main.py

import sys
from PyQt6.QtWidgets import QApplication
from pyqt6_scaffold import Composer

def main():
    app = QApplication(sys.argv)
    
    db = AppDatabase()
    db.connect()
    
    composer = Composer(app=app, db=db)
    composer.register("login", LoginWindow)
    composer.register("main", MainWindow)
    
    sys.exit(composer.run(start="login"))

if __name__ == "__main__":
    main()

Core Module

The module providing essential tools for development, usually in the form of abstract classes that implement a convenient pattern. For example, the BaseWindow class implements a minimally acceptable (in the author's opinion) pattern for an application window:

from PyQt6.QtWidgets import QMainWindow

class BaseWindow(QMainWindow):
    def __init__(self, composer, db):
        super().__init__()
        self._db = db
        self._composer = composer

        self._define_widgets()
        self._tune_layouts()
        self._connect_slots()
        self._apply_windows_settings()

    def _define_widgets(self): pass
    def _tune_layouts(self): pass
    def _connect_slots(self): pass
    def _apply_windows_settings(self): pass

The main components of the Core module are the navigation and database interaction classes.

Database

The AbstractDatabase class is responsible for database management within the library. The class structure looks approximately like this:

AbstractDatabase

This is an abstract class that must be overridden in a subclass. Let's break it down in detail:

  • Class Constructor:
    • Contains a single argument: strict. This affects how the system reacts when a user opens a new connection, as the default assumption is that the user will use only one connection for database interaction. If strict = True and the class already holds a reference to an existing connection (_conn != None), the execution will fail with an error. If strict = False, the old connection will be closed and the class will create a new connection to the database.
  • connect(): A context-independent method for creating a database connection. Internally, it calls the private method _connect(), which is context-dependent on the SQL dialect and must be overridden by the user in the subclass.
    • In the _connect() method, the user implements the specific database connection logic for their chosen SQL dialect. The method should return a connection object, typically resembling: your_sql_dialect_connection.connect(**DB_CONFIG)
  • disconnect(): A context-independent method for manually closing the database connection.
  • connection: A getter for the _conn field, returning the current database connection.
  • execute(): A context-independent method for executing database code. It handles opening a connection cursor and executing the user's query. It consists of several components:
    • CursorContext: A context-independent wrapper class for the cursor object. It is required for compatibility and using the cursor within a context manager, proxying all cursor methods to make it programmatically equivalent to a cursor object.
    • placeholder: A context-dependent getter for the SQL dialect's specific placeholder used for value substitution; must be overridden by the user.
    • Method Arguments: execute() takes only 2 arguments: autocommit (bool) and params (tuple). The first regulates database behavior during data changes—whether to apply them immediately or require the user to do so manually. The second argument passes the values for substitution.
    • If query execution fails, the method will automatically roll back any changes in the database.

Database Models

As is common knowledge, interaction between the database and the frontend in PyQt6 is implemented through a system of models and views. This library redefines models for tables, lists, and cards, implementing a compatible interface for the database management logic described above.

The model logic in the library is represented by three model classes and one auxiliary mixin class:

  • DataMixin A mixin for Qt models (QAbstractTableModel / QAbstractListModel). It contains shared logic for data storage, model updates, and row styling. DataMixin is used alongside Qt models and is not intended for standalone use. It simplifies the implementation of common model functions so that specific classes can focus on business logic.
    Provides methods for:
    • data storage (_data)
    • model reloading (refresh)
    • row access (row_data)
    • styling (row_background, row_foreground, row_font)
  • BaseTableModel A table model based on a list of row tuples. It supports displaying data in a table with column headers, access to raw data, and cell styling via mixin methods. Intended for scenarios where data is naturally represented as a table (e.g., lists of objects with fixed fields).
  • BaseListModel A list model where each entry is represented as a list item. Text display is handled via the row_display() method. Suitable for simple lists, comboboxes, and other views where a table structure is not required.
  • BaseCardModel A model for card-based or custom-drawn views.
    Does not implement DisplayRole—visualization is delegated to a QStyledItemDelegate. This allows for building complex UI components (cards, tiles, custom lists) while maintaining data access via UserRole.

Composer

The Composer class is the application's router and window lifecycle manager. It is responsible for window registration, navigation between them, and storing the active database connection. The Composer acts as the application's central hub, through which view switching and context passing occur.

Composer

Let's examine its structure:

  • Class Constructor:
    • Contains 2 arguments: app and db. app is a reference to the PyQt6 event loop object, and db is a reference to the database object.
    • Internally initializes 5 fields:
      • _db: Stores a reference to the database object.
      • _current: Stores a reference to the active window.
      • _app: Stores a reference to the PyQt6 event loop object.
      • _registry: Stores a reference to the composer's registry, which is a "key-value" store (Python dict) containing mappings between a string name and a window class reference.
        • The entry template in the composer registry is defined in the register() method and looks like this:
        {
            "class": window,
            "instance": None,
            "lazy": lazy
        }
        
        • The class field is a reference to the window class.
        • The instance field is a reference to the window object (if created).
        • The lazy field is a setting for window object creation. If lazy = True, the window object is created at the moment of the switch request. If lazy = False, the object is created immediately during registration. The former is better if the window content is context-dependent.
      • _current_ctx: Stores the context of the active window as a "key-value" store. The context is unique to each window and is overwritten when switching. It can contain any key-value pairs used for window needs (e.g., if using RBAC, it is logical to store a user rights object here). The context is set during the Maps() slot call. It is assumed the starting window should not contain a context.
  • register(): A method for creating a new entry in the composer registry. If the class passed in the window argument is not a subclass of BaseWindow, a Python exception will be raised.
  • navigate_request: The composer's only signal. It should be emitted when the user needs to switch windows, passing a reference to the NavigationRequest dataclass.
  • navigate(): The method managing window switching. It is a PyQt6 slot and should not be called directly, as it is triggered automatically by the Maps_request signal.
    • NavigationRequest is a dataclass consisting of target: str and context: NavigationContext fields. It carries the switch request context, which consists of the target window name and the window class reference. In turn, the NavigationContext dataclass consists of a single data field which stores the window context.

Switching between windows is performed via the Composer class's programmatic interface, a reference to which all windows should receive by default (see Window Template above).

Contrib Module

The contrib module contains ready-to-use implementations for common use cases. Unlike core, contrib is context-dependent and provides specific solutions that a student can use as-is or as a foundation.

Backends

Ready-made implementations of AbstractDatabase for three SQL dialects:

from pyqt6_scaffold.contrib import PostgresqlDatabase, MysqlDatabase, SqliteDatabase

Each backend reads configuration from environment variables:

Class Environment Variables
PostgresqlDatabase PG_HOST, PG_PORT, PG_USER, PG_DATABASE, PG_PASSWORD
MysqlDatabase MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_DATABASE, MYSQL_PASSWORD
SqliteDatabase SQLITE_PATH

Auth

Authorization system based on RBAC (Role-Based Access Control):

from pyqt6_scaffold.contrib.auth import RBACMixin, Role, RoleLevel

Role and RoleLevel — a dataclass and an enum for describing user roles:

from pyqt6_scaffold.contrib.auth import Role, RoleLevel
from pyqt6_scaffold import BaseUser

role = Role(name="Manager", level=RoleLevel.MANAGER.value)
user = BaseUser(id=1, name="Ivan", role=role)

RBACMixin — a mixin that adds the can() method to your database. It assumes the existence of a permission_map table in the database:

CREATE TABLE permission_map (
    perm      VARCHAR(50),
    min_level INT
);

INSERT INTO permission_map VALUES ('edit.products', 50);
INSERT INTO permission_map VALUES ('delete.products', 100);
INSERT INTO permission_map VALUES ('view.products', 0);
...

Usage:

from pyqt6_scaffold.contrib import MysqlDatabase
from pyqt6_scaffold.contrib.auth import RBACMixin

class AppDatabase(RBACMixin, MysqlDatabase):
    def auth(self, login, password): ...

db = AppDatabase()
db.connect()

db.can(user, "edit.products")    # True if user.role.level >= 50
db.can(user, "delete.products")  # False if user.role.level < 100

Table and column names can be overridden:

class AppDatabase(RBACMixin, MysqlDatabase):
    permission_table = "rights"
    permission_column = "action"
    level_column = "required_level"