migrate pyqt5 to pyside6
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
"""Binding policy for AutoCal.
|
||||
"""Binding policy for the Orisys apps.
|
||||
|
||||
AutoCal is **PySide6 only**, and this file is the guard for that:
|
||||
Both shipped apps are **PySide6 only**, and this file is AutoCal's guard for that:
|
||||
|
||||
* no module may import PyQt5, PySide2 or PyQt6 — the app's environment has
|
||||
PySide6 and nothing else, so such an import is a hard start-up failure;
|
||||
* no module may use an unscoped Qt enumerator (``Qt.AlignLeft``), which PySide6
|
||||
removed in favour of ``Qt.AlignmentFlag.AlignLeft``;
|
||||
* ``shared/`` — which serves both this app and the PyQt5-based studio — must
|
||||
import no Qt binding at all;
|
||||
* ``shared/`` may import PySide6 and nothing else: it is used by both apps, so it
|
||||
must not pull in a second binding, and the studio's own guard
|
||||
(``apps/studio/tests/test_binding_purity.py``) enforces the same rule from its
|
||||
side. (Until the studio moved to PySide6 this file forbade *any* binding in
|
||||
``shared/``; that rule existed for a two-binding workspace, which no longer
|
||||
exists. ``shared/theme/qss.py`` paints its chevron icons with PySide6 now, and
|
||||
under the old rule those imports were failing silently instead.)
|
||||
* the controller layer must stay importable from a headless process.
|
||||
|
||||
The reason the binding matters at all is licensing, not taste: PyQt5's free
|
||||
option is GPLv3-or-commercial, while PySide6 is LGPLv3, so an internal
|
||||
closed-source tool needs no licence purchase. See ``README.md``.
|
||||
option is GPLv3-or-commercial, while PySide6 is LGPLv3, so a closed-source tool
|
||||
needs no licence purchase. See ``README.md``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -23,13 +28,19 @@ from pathlib import Path
|
||||
APP_SRC = Path(__file__).resolve().parents[1] / "src"
|
||||
SHARED_SRC = Path(__file__).resolve().parents[3] / "shared" / "src"
|
||||
|
||||
#: Bindings the app must never import.
|
||||
#: Bindings no module may import.
|
||||
FORBIDDEN_BINDINGS = {"PyQt5", "PyQt6", "PySide2"}
|
||||
|
||||
#: The one binding allowed, and where it may be named.
|
||||
ALLOWED_BINDING = "PySide6"
|
||||
|
||||
#: Scoped enum holders we care about, with the value names actually used.
|
||||
#:
|
||||
#: Hand-maintained, and therefore incomplete by construction: the PySide6 migration
|
||||
#: found ``QPainter.RenderHint.Antialiasing`` and ``QDialogButtonBox.StandardButton.Ok`` slipping through a list
|
||||
#: like this one. The studio's guard asks PySide6 instead (``getattr`` + enum check),
|
||||
#: which is the better rule; this map is kept because AutoCal cannot import the binding
|
||||
#: in a headless test run.
|
||||
_SCOPED_ENUMS = {
|
||||
"Qt": (
|
||||
"AlignmentFlag",
|
||||
@@ -41,6 +52,12 @@ _SCOPED_ENUMS = {
|
||||
"ToolButtonStyle",
|
||||
"FocusPolicy",
|
||||
"ScrollBarPolicy",
|
||||
"CursorShape",
|
||||
"MouseButton",
|
||||
"Key",
|
||||
"PenStyle",
|
||||
"GlobalColor",
|
||||
"ItemFlag",
|
||||
),
|
||||
"QTableWidget": ("SelectionMode", "EditTrigger"),
|
||||
"QSizePolicy": ("Policy",),
|
||||
@@ -48,6 +65,11 @@ _SCOPED_ENUMS = {
|
||||
"QMessageBox": ("StandardButton", "Icon"),
|
||||
"QDialog": ("DialogCode",),
|
||||
"QAbstractItemView": ("SelectionMode", "EditTrigger"),
|
||||
"QDialogButtonBox": ("StandardButton",),
|
||||
"QPainter": ("RenderHint", "CompositionMode"),
|
||||
"QImage": ("Format",),
|
||||
"QSurfaceFormat": ("RenderableType", "OpenGLContextProfile", "SwapBehavior"),
|
||||
"QFrame": ("Shape",),
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +94,13 @@ def _unscoped_enum_references(path: Path) -> list[str]:
|
||||
"""Attribute reads like ``Qt.AlignLeft`` — the spelling PySide6 rejects.
|
||||
|
||||
Parsed rather than pattern-matched for two reasons: a regex also hits prose
|
||||
in a comment or docstring (which is exactly where the scoped spelling gets
|
||||
*explained*), and ``QMessageBox.information(...)`` looks like an enumerator
|
||||
but is a static method. The parent map separates the two, and
|
||||
``Qt.AlignmentFlag.AlignLeft`` is an inner attribute on an allow-listed enum
|
||||
class rather than a plain name, so it is never a candidate.
|
||||
in a comment or docstring (which is exactly where the *unscoped* spelling gets
|
||||
explained, so a rewrite would destroy the explanation — this file quotes it
|
||||
three times, which is why the migration scripts skip it), and
|
||||
``QMessageBox.information(...)`` looks like an enumerator but is a static
|
||||
method. The parent map separates the two, and ``Qt.AlignmentFlag.AlignLeft`` is
|
||||
an inner attribute on an allow-listed enum class rather than a plain name, so it
|
||||
is never a candidate.
|
||||
"""
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
called = {
|
||||
@@ -152,15 +176,22 @@ def test_the_controller_interface_is_qt_free():
|
||||
assert not offenders, offenders
|
||||
|
||||
|
||||
def test_shared_imports_no_qt_binding():
|
||||
"""Shared code serves both products, so it must not pick a side."""
|
||||
def test_shared_imports_no_forbidden_binding():
|
||||
"""Shared code serves both products, so it must not pull in a second binding.
|
||||
|
||||
PySide6 is allowed and expected (``shared/theme/qss.py`` paints the chevron icons with
|
||||
it); what must never appear here is PyQt5/PyQt6/PySide2, which would either fail to
|
||||
import in a PySide6-only environment or — worse — work in one app's environment and not
|
||||
the other's. Before the studio moved to PySide6 this test forbade PySide6 itself, and
|
||||
the PyQt5 import in ``qss.py`` was failing silently as a result.
|
||||
"""
|
||||
offenders = []
|
||||
for path in _python_files(SHARED_SRC):
|
||||
qt_imports = _imported_packages(path) & {"PySide2", "PySide6"}
|
||||
qt_imports = _imported_packages(path) & FORBIDDEN_BINDINGS
|
||||
if qt_imports:
|
||||
offenders.append(f"{path.name}: {sorted(qt_imports)}")
|
||||
assert not offenders, (
|
||||
"shared code must not import PySide2/PySide6 (studio is PyQt5): "
|
||||
"shared code may import PySide6 only; these files import another binding: "
|
||||
f"{offenders}"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user