migrate pyqt5 to pyside6
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
The tests render real Qt widgets with the offscreen platform plugin, so they need
|
||||
no display and no camera hardware. A single ``QApplication`` is shared by the
|
||||
whole session (Qt forbids creating more than one).
|
||||
|
||||
The suite requires PySide6, the app's only Qt binding. Where it is missing the
|
||||
modules are skipped rather than failed, so a broken environment cannot look like a
|
||||
broken application (the same rule ``apps/autocal`` follows).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,11 +20,30 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
# In a development environment that also has PyQt5 installed, pyqtgraph would otherwise
|
||||
# bind to it (it takes whichever toolkit it finds first) and the plots would break.
|
||||
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
|
||||
if sys.platform == "win32" and "QT_QPA_FONTDIR" not in os.environ:
|
||||
_fonts = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts"
|
||||
if _fonts.is_dir():
|
||||
os.environ["QT_QPA_FONTDIR"] = str(_fonts)
|
||||
|
||||
|
||||
def _have_pyside6() -> tuple[bool, str]:
|
||||
try:
|
||||
import PySide6
|
||||
except ImportError as exc: # pragma: no cover - depends on the machine
|
||||
return False, str(exc)
|
||||
return True, getattr(PySide6, "__version__", "unknown")
|
||||
|
||||
|
||||
HAVE_PYSIDE6, PYSIDE6_VERSION = _have_pyside6()
|
||||
|
||||
if not HAVE_PYSIDE6: # pragma: no cover - depends on the machine
|
||||
# Mirrors apps/autocal/tests/conftest.py: nothing is collected, so a machine without
|
||||
# the binding reports no tests instead of a wall of import errors.
|
||||
collect_ignore_glob = ["test_*.py"]
|
||||
|
||||
# ``tools/ui_check.py`` owns the palette rules; importing it keeps one source of
|
||||
# truth between the pytest suite and the dependency-free checker.
|
||||
_TOOLS_DIR = Path(__file__).resolve().parents[1] / "tools"
|
||||
@@ -30,7 +53,7 @@ if _TOOLS_DIR.is_dir() and str(_TOOLS_DIR) not in sys.path:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def qt_app():
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
yield app
|
||||
@@ -83,7 +106,7 @@ def visible_descendants(widget):
|
||||
|
||||
def show_window(window, size):
|
||||
"""Show a window at ``size`` and flush pending Qt events."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window.resize(*size)
|
||||
window.show()
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Binding policy for the studio.
|
||||
|
||||
The studio is **PySide6 only** — Qt's LGPLv3 binding, which is why the app was migrated off
|
||||
PyQt5 (GPLv3-or-commercial). AutoCal has the same guard in
|
||||
``apps/autocal/tests/test_binding_purity.py``; this file is the studio's half of it:
|
||||
|
||||
* 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;
|
||||
* every UI and 3D module must name the binding it uses rather than inheriting one;
|
||||
* the PyQt5 signal spellings (``pyqtSignal``, ``pyqtBoundSignal``) do not exist in PySide6;
|
||||
* no module may use an unscoped Qt enumerator (``Qt.AlignLeft``, ``QPainter.Antialiasing``);
|
||||
* importing the application must not drag another binding into ``sys.modules`` — that is
|
||||
what breaks ``pyqtgraph``, which binds to whichever toolkit it finds first.
|
||||
|
||||
Two lessons from the migration are baked in here. The unscoped-enum check asks PySide6
|
||||
instead of using a hand-written list of enum holders: a curated list missed
|
||||
``QPainter.RenderHint.Antialiasing``, ``QPainter.CompositionMode.CompositionMode_SourceIn`` and
|
||||
``QDialogButtonBox.StandardButton.Ok|Cancel|Close``. And the ``sys.modules`` check runs in a **fresh
|
||||
interpreter**, because in-process the answer depends on what some earlier test imported.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import enum
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] # apps/studio
|
||||
SRC = APP / "src"
|
||||
PROJECT_ROOT = APP.parents[1] # orisys-app
|
||||
|
||||
#: Everything that ships or drives the app.
|
||||
SCAN_ROOTS = (SRC, APP / "tools")
|
||||
|
||||
#: Bindings no module may import.
|
||||
FORBIDDEN_BINDINGS = {"PyQt5", "PyQt6", "PySide2"}
|
||||
|
||||
#: The one binding allowed.
|
||||
ALLOWED_BINDING = "PySide6"
|
||||
|
||||
#: Directories that must name their binding explicitly.
|
||||
#:
|
||||
#: ``ui/`` only: every widget there imports PySide6. ``scene3d/`` is deliberately mixed —
|
||||
#: ``coordinator.py``, ``camera.py``, ``adapters.py``, ``prefs.py`` and ``keys.py`` import no
|
||||
#: Qt binding at all so they can be tested headless, and requiring one would forbid that
|
||||
#: design. The modules there that *do* use Qt already name PySide6.
|
||||
BINDING_DECLARING_DIRS = (SRC / "orisys_studio" / "ui",)
|
||||
|
||||
#: Names that only ever existed in PyQt. Quoted here on purpose — the migration scripts that
|
||||
#: do the rewriting skip this file, because a text-based rule cannot tell a forbidden spelling
|
||||
#: in code from one named in the policy that forbids it.
|
||||
PYQT_ONLY_NAMES = ("pyqtSignal", "pyqtBoundSignal", "pyqtSlot", "pyqtProperty")
|
||||
|
||||
_SKIP_PARTS = ("__pycache__",)
|
||||
|
||||
|
||||
def _python_files(root: Path) -> list[Path]:
|
||||
return sorted(
|
||||
path
|
||||
for path in root.rglob("*.py")
|
||||
if not any(part in path.parts for part in _SKIP_PARTS)
|
||||
)
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
# One file in the workspace carries a UTF-8 BOM; `utf-8-sig` keeps `ast` happy either way.
|
||||
return path.read_text(encoding="utf-8-sig", errors="replace")
|
||||
|
||||
|
||||
def _imported_packages(path: Path) -> set[str]:
|
||||
"""Every top-level package name imported by ``path``."""
|
||||
found: set[str] = set()
|
||||
for node in ast.walk(ast.parse(_read(path))):
|
||||
if isinstance(node, ast.Import):
|
||||
found.update(alias.name.split(".")[0] for alias in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
||||
found.add(node.module.split(".")[0])
|
||||
return found
|
||||
|
||||
|
||||
def _qt_namespace() -> dict[str, object]:
|
||||
"""The Qt classes the sources could name, resolved through the binding itself."""
|
||||
from PySide6 import QtCore, QtGui, QtOpenGLWidgets, QtWidgets
|
||||
|
||||
namespace: dict[str, object] = {"Qt": QtCore.Qt}
|
||||
return namespace, (QtCore, QtGui, QtWidgets, QtOpenGLWidgets)
|
||||
|
||||
|
||||
def _unscoped_enums(path: Path, namespace: dict[str, object], modules: tuple) -> list[str]:
|
||||
"""Attribute reads whose value resolves to an enum instance rather than an enum class.
|
||||
|
||||
``Qt.AlignmentFlag.AlignLeft`` is an inner attribute of an enum class, so the outer
|
||||
``Qt.AlignmentFlag`` resolves to a *class* and the inner one is not a plain name — neither
|
||||
is a candidate. ``QMessageBox.information`` resolves to a method. ``Qt.AlignmentFlag.AlignLeft``
|
||||
resolves to an enum instance, which is the spelling this test exists to reject.
|
||||
"""
|
||||
tree = ast.parse(_read(path))
|
||||
called = {
|
||||
id(node.func)
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
|
||||
}
|
||||
inner = {id(node.value) for node in ast.walk(tree) if isinstance(node, ast.Attribute)}
|
||||
found: set[str] = set()
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
|
||||
continue
|
||||
if id(node) in called or id(node) in inner:
|
||||
continue
|
||||
holder, member = node.value.id, node.attr
|
||||
target_holder = namespace.get(holder)
|
||||
if target_holder is None:
|
||||
for module in modules:
|
||||
if hasattr(module, holder):
|
||||
target_holder = namespace[holder] = getattr(module, holder)
|
||||
break
|
||||
else:
|
||||
continue
|
||||
value = getattr(target_holder, member, None)
|
||||
if isinstance(value, (enum.Enum, enum.Flag)) and member not in namespace:
|
||||
found.add(f"{holder}.{member}")
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def test_no_studio_module_imports_a_forbidden_binding():
|
||||
offenders = []
|
||||
for root in SCAN_ROOTS:
|
||||
for path in _python_files(root):
|
||||
bad = _imported_packages(path) & FORBIDDEN_BINDINGS
|
||||
if bad:
|
||||
offenders.append(f"{path.relative_to(APP)}: {sorted(bad)}")
|
||||
assert not offenders, (
|
||||
f"the studio is {ALLOWED_BINDING}-only; these modules import another binding: "
|
||||
f"{offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_every_ui_module_imports_the_allowed_binding():
|
||||
"""A widget module that names no binding would be using one it does not declare."""
|
||||
offenders = []
|
||||
for directory in BINDING_DECLARING_DIRS:
|
||||
for path in _python_files(directory):
|
||||
if path.name == "__init__.py":
|
||||
continue # a re-export barrel, not a widget module
|
||||
if ALLOWED_BINDING not in _imported_packages(path):
|
||||
offenders.append(str(path.relative_to(APP)))
|
||||
assert not offenders, f"these modules do not import {ALLOWED_BINDING}: {offenders}"
|
||||
|
||||
|
||||
def test_no_pyqt_only_spelling_survives():
|
||||
offenders = []
|
||||
for root in SCAN_ROOTS:
|
||||
for path in _python_files(root):
|
||||
text = _read(path)
|
||||
for name in PYQT_ONLY_NAMES:
|
||||
if name in text:
|
||||
offenders.append(f"{path.relative_to(APP)}: {name}")
|
||||
assert not offenders, f"these PyQt-only names do not exist in PySide6: {offenders}"
|
||||
|
||||
|
||||
def test_no_module_uses_an_unscoped_enum():
|
||||
try:
|
||||
import PySide6 # noqa: F401
|
||||
except ImportError: # pragma: no cover - depends on the machine
|
||||
import pytest
|
||||
|
||||
pytest.skip("PySide6 is not installed here, so the enum rule cannot be checked")
|
||||
namespace, modules = _qt_namespace()
|
||||
offenders = {}
|
||||
for root in SCAN_ROOTS:
|
||||
for path in _python_files(root):
|
||||
found = _unscoped_enums(path, namespace, modules)
|
||||
if found:
|
||||
offenders[str(path.relative_to(APP))] = found
|
||||
assert not offenders, (
|
||||
"unscoped Qt enumerators are the spelling PySide6 is removing; use the scoped "
|
||||
f"form: {offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_importing_the_application_pulls_in_no_other_binding():
|
||||
"""Fresh interpreter: PySide6 only, whatever this test session imported before."""
|
||||
import os
|
||||
|
||||
script = (
|
||||
"import sys;"
|
||||
"import orisys_studio.app_window;"
|
||||
"import orisys_studio.scene3d.view;"
|
||||
"import orisys_studio.ui.force_plots;"
|
||||
"print(','.join(m for m in ('PyQt5', 'PyQt6', 'PySide2') if m in sys.modules))"
|
||||
)
|
||||
env = dict(os.environ)
|
||||
entries = [str(PROJECT_ROOT / "shared" / "src"), str(SRC)]
|
||||
if env.get("PYTHONPATH"):
|
||||
entries.append(env["PYTHONPATH"])
|
||||
env["PYTHONPATH"] = os.pathsep.join(entries)
|
||||
# The app's own start-up contract (``main.py``) and a display-free platform: the point
|
||||
# is which *binding* comes in, not whether a window can be shown.
|
||||
env.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
|
||||
env.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=180,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
pulled = [name for name in result.stdout.strip().split(",") if name]
|
||||
assert pulled == [], (
|
||||
f"importing the app pulled in {pulled}; pyqtgraph binds to whichever toolkit it "
|
||||
"finds first, so a second binding in the environment breaks the plots"
|
||||
)
|
||||
|
||||
|
||||
def test_the_app_declares_its_binding_dependency():
|
||||
"""The packaging must state PySide6, not leave it to chance."""
|
||||
pyproject = (APP / "pyproject.toml").read_text(encoding="utf-8")
|
||||
assert ALLOWED_BINDING in pyproject
|
||||
for forbidden in FORBIDDEN_BINDINGS:
|
||||
assert f'"{forbidden}' not in pyproject, forbidden
|
||||
@@ -94,15 +94,16 @@ def _render(widget, qt_app) -> np.ndarray:
|
||||
on a little-endian machine, with a stride that can exceed the width — so the
|
||||
conversion and the stride trim are both load-bearing.
|
||||
"""
|
||||
from PyQt5.QtGui import QImage
|
||||
from PySide6.QtGui import QImage
|
||||
|
||||
widget.repaint()
|
||||
qt_app.processEvents()
|
||||
image = widget.grabFramebuffer()
|
||||
assert not image.isNull(), "Qt returned no framebuffer"
|
||||
image = image.convertToFormat(QImage.Format_RGB888)
|
||||
image = image.convertToFormat(QImage.Format.Format_RGB888)
|
||||
width, height = image.width(), image.height()
|
||||
buffer = image.constBits().asarray(image.byteCount())
|
||||
# PySide6 returns a `memoryview` from constBits(), not a QByteArray: wrap it directly.
|
||||
buffer = np.frombuffer(image.constBits(), dtype=np.uint8)
|
||||
stride = image.bytesPerLine()
|
||||
rows = np.frombuffer(buffer, dtype=np.uint8).reshape(height, stride)
|
||||
return rows[:, : width * 3].reshape(height, width, 3).copy()
|
||||
|
||||
@@ -13,8 +13,8 @@ view menu / the pane's header both reach that dialog.
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio.ui import icons
|
||||
@@ -117,8 +117,8 @@ def test_the_pane_header_offers_the_two_actions(qt_app):
|
||||
|
||||
# Reset forwards to the view when there is one.
|
||||
class _View(QWidget):
|
||||
diagnosticRaised = pyqtSignal(object)
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
diagnosticRaised = Signal(object)
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -229,7 +229,7 @@ def test_the_diagnostics_switch_governs_what_the_pane_explains(panel):
|
||||
|
||||
def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(make_window):
|
||||
"""One line under the canvas: what the picture is, not how it was made."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = make_window()
|
||||
window.show()
|
||||
@@ -262,7 +262,7 @@ def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(make_window):
|
||||
|
||||
def test_the_view_menu_opens_the_3d_settings(make_window):
|
||||
"""视图 ▸ 三维显示设置… is the way in that does not depend on the pane's header."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = make_window(sensor_type="finger", show_finger_3d=True)
|
||||
window.show()
|
||||
|
||||
@@ -19,7 +19,7 @@ import pytest
|
||||
@pytest.fixture
|
||||
def restore_surface_format():
|
||||
"""The default surface format is process-wide; put it back afterwards."""
|
||||
from PyQt5.QtGui import QSurfaceFormat
|
||||
from PySide6.QtGui import QSurfaceFormat
|
||||
|
||||
previous = QSurfaceFormat.defaultFormat()
|
||||
try:
|
||||
@@ -49,21 +49,21 @@ def test_the_skeleton_does_not_import_a_gl_binding(qt_app):
|
||||
|
||||
|
||||
def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_surface_format):
|
||||
from PyQt5.QtGui import QSurfaceFormat
|
||||
from PySide6.QtGui import QSurfaceFormat
|
||||
|
||||
from orisys_studio.scene3d.gl_backend import configure_surface_format
|
||||
|
||||
configure_surface_format("desktop33")
|
||||
desktop = QSurfaceFormat.defaultFormat()
|
||||
assert desktop.renderableType() == QSurfaceFormat.OpenGL
|
||||
assert desktop.renderableType() == QSurfaceFormat.RenderableType.OpenGL
|
||||
assert (desktop.majorVersion(), desktop.minorVersion()) == (3, 3)
|
||||
assert desktop.profile() == QSurfaceFormat.CoreProfile
|
||||
assert desktop.profile() == QSurfaceFormat.OpenGLContextProfile.CoreProfile
|
||||
assert desktop.depthBufferSize() == 24
|
||||
assert desktop.samples() == 0, "v1 does not pay for multisampling"
|
||||
|
||||
configure_surface_format("gles30")
|
||||
embedded = QSurfaceFormat.defaultFormat()
|
||||
assert embedded.renderableType() == QSurfaceFormat.OpenGLES
|
||||
assert embedded.renderableType() == QSurfaceFormat.RenderableType.OpenGLES
|
||||
assert (embedded.majorVersion(), embedded.minorVersion()) == (3, 0)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
@@ -29,8 +29,8 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
import ui_check
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d import synthetic
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -276,8 +276,8 @@ class _RecordingView(QWidget):
|
||||
a real ``QWidget`` because the container puts it in its stack.
|
||||
"""
|
||||
|
||||
diagnosticRaised = pyqtSignal(object)
|
||||
enabledChanged = pyqtSignal(bool)
|
||||
diagnosticRaised = Signal(object)
|
||||
enabledChanged = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -559,7 +559,9 @@ def test_a_package_is_loaded_off_the_gui_thread(qt_app, write_poc):
|
||||
loaded: list = []
|
||||
failed: list = []
|
||||
loader.loaded.connect(lambda assets, description: loaded.append((assets, description)))
|
||||
loader.failed.connect(failed.append)
|
||||
# Same arity caveat as below: this branch should never fire, and under PySide6 a
|
||||
# builtin slot would fail *silently* if it ever did.
|
||||
loader.failed.connect(lambda item, request_id: failed.append(item))
|
||||
try:
|
||||
loader.load(SceneSource(path=path, kind="package", directory=path.parent))
|
||||
assert _pump(qt_app, lambda: bool(loaded or failed)), "the loader never answered"
|
||||
@@ -679,7 +681,10 @@ def test_a_corrupt_package_is_reported_not_raised(qt_app, tmp_path):
|
||||
broken.write_bytes(b"not a zip archive at all")
|
||||
loader = SceneAssetLoader()
|
||||
failed: list = []
|
||||
loader.failed.connect(failed.append)
|
||||
# ``failed`` carries (diagnostic, request id). PySide6 adapts to a *Python* callable's
|
||||
# arity but not to a builtin's, so ``failed.append`` raises TypeError inside the emit,
|
||||
# Qt swallows it, and the failure signal looks like it was never sent.
|
||||
loader.failed.connect(lambda item, request_id: failed.append(item))
|
||||
try:
|
||||
loader.load(SceneSource(path=broken, kind="package", directory=tmp_path))
|
||||
assert _pump(qt_app, lambda: bool(failed)), "a corrupt package produced no answer"
|
||||
|
||||
@@ -559,7 +559,7 @@ def test_force_plots_render_demo_data(make_window):
|
||||
window = _show(make_window(), (1440, 880))
|
||||
for i in range(60):
|
||||
window.force_plots.push(i / 30.0, float(i), -1.0, 1.0, unit="N")
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
assert window.force_plots.history.sample_count > 0
|
||||
@@ -1009,7 +1009,7 @@ def test_top_area_stays_compact(make_window):
|
||||
assert window.toolbar.record_chip.text() == t("record.state_idle")
|
||||
|
||||
window.toolbar.btn_settings.setChecked(True)
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
assert window.toolbar.settings_expanded()
|
||||
@@ -1032,7 +1032,7 @@ def test_settings_area_stays_inside_its_container(make_window, size):
|
||||
The settings area is the one part of the top rail that may scroll: its own
|
||||
content is exempt, everything else must fit.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(sensor_type="finger", show_cal_panel=True), size)
|
||||
window._set_settings_expanded(True)
|
||||
@@ -1063,7 +1063,7 @@ def test_idle_run_is_the_only_filled_action(make_window):
|
||||
|
||||
def test_connecting_cancel_stays_filled(make_window):
|
||||
"""Cancel is the way out of a hung device open, so it keeps the weight."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
stuck = ui_check._StuckWorker()
|
||||
@@ -1197,7 +1197,7 @@ def test_overview_region_is_the_timeline_control(make_window):
|
||||
|
||||
def test_the_charts_start_in_follow_mode(make_window):
|
||||
"""Every live entry point resumes following; only browsing is a gesture."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1229,7 +1229,7 @@ def test_the_charts_start_in_follow_mode(make_window):
|
||||
|
||||
def test_the_charts_zoom_and_pan_in_every_view(make_window):
|
||||
"""Zoom/pan must be reachable from the live view, not only while browsing."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
panel = window.force_plots
|
||||
@@ -1306,7 +1306,7 @@ def test_the_arrow_legend_follows_the_view_mode(make_window):
|
||||
|
||||
def test_the_sensor_summary_opens_the_dialog(make_window):
|
||||
"""The rail states the sensor; the pickers live in the dialog it opens."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1344,7 +1344,7 @@ def test_the_sensor_summary_opens_the_dialog(make_window):
|
||||
|
||||
def test_the_source_picker_follows_the_view(make_window):
|
||||
"""Live offers a camera, the replay a recorded video — and no residue."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
assert window.toolbar.camera_group.isVisible()
|
||||
@@ -1425,11 +1425,11 @@ def test_the_nav_items_switch_the_main_window_layout(make_window):
|
||||
|
||||
|
||||
def test_the_calibration_view_does_not_clip_its_controls(make_window):
|
||||
from PyQt5.QtWidgets import QHeaderView
|
||||
from PySide6.QtWidgets import QHeaderView
|
||||
|
||||
window = _show(make_window(sensor_type="box", show_cal_panel=True), (1440, 880))
|
||||
window.brand_bar.btn_calibration.click()
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
for _ in range(3):
|
||||
QApplication.processEvents()
|
||||
@@ -1437,7 +1437,7 @@ def test_the_calibration_view_does_not_clip_its_controls(make_window):
|
||||
cal = window.cal_panel
|
||||
assert cal.scroll.isVisible(), "the calibration panel cannot scroll"
|
||||
assert (
|
||||
cal.table.horizontalHeader().sectionResizeMode(0) == QHeaderView.ResizeToContents
|
||||
cal.table.horizontalHeader().sectionResizeMode(0) == QHeaderView.ResizeMode.ResizeToContents
|
||||
), "the diagnostic columns can still be elided"
|
||||
for button in (cal.btn_apply, cal.btn_export, cal.btn_toggle_validation):
|
||||
assert button.height() >= button.fontMetrics().height(), button.text()
|
||||
@@ -1468,7 +1468,7 @@ def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_live.click()
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
live_sizes = tuple(window.content_splitter.sizes())
|
||||
@@ -1496,7 +1496,7 @@ def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
|
||||
|
||||
def test_the_replay_view_loads_a_recording_and_scrubs_it(make_window, replay_recording):
|
||||
"""The replay view reads the recording: charts, 2D panel and transport."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1602,7 +1602,7 @@ def test_the_unit_chip_switches_the_charts_and_the_toolbar(make_window):
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio.ui.force_plots import unit_display
|
||||
|
||||
from PyQt5.QtWidgets import QPushButton
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
plots = window.force_plots
|
||||
@@ -1734,7 +1734,7 @@ def test_a_replay_keeps_the_2d_pane_from_the_live_camera(
|
||||
While the replay view is up, its frames own the 2D panel; a live packet used
|
||||
to paint over the recording. Leaving the view hands the pane back.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -1786,7 +1786,7 @@ def test_the_readout_describes_the_point_the_cursor_is_on(
|
||||
A replay used to state the live acquisition in cards sitting next to a chart
|
||||
of 7.5e5 raw counts, which reads as "the replay never opened".
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1820,7 +1820,7 @@ def test_a_raw_only_recording_is_never_shown_in_newton(make_window, replay_recor
|
||||
for the axes it calibrated. Asking for newton anyway drew three blank charts
|
||||
and a dash in every card, which looks exactly like "the replay did not load".
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1847,7 +1847,7 @@ def test_the_replay_transport_plays_and_pauses(make_window, replay_recording):
|
||||
"""Play/pause like an editor: 1× forward, and a manual gesture takes over."""
|
||||
import time
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -1887,7 +1887,7 @@ def test_the_recompute_mode_reruns_the_pipeline_over_the_video(
|
||||
make_window, replay_recording
|
||||
):
|
||||
"""Off: the recorded CSV. On: the recorded video through the vision chain."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -1929,7 +1929,7 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
|
||||
wrong. So the rail's second button becomes an unconditional stop in this view, and
|
||||
it covers both things that can run there: plain playback and a recompute pass.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1989,7 +1989,7 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
|
||||
|
||||
def test_escape_stops_the_replay_too(make_window, replay_recording):
|
||||
"""Esc is the documented way out; in the replay view it stops the replay."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_playback.click()
|
||||
@@ -2007,7 +2007,7 @@ def test_escape_stops_the_replay_too(make_window, replay_recording):
|
||||
|
||||
|
||||
def test_the_live_view_keeps_the_tracker_reset(make_window):
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
window.brand_bar.btn_live.click()
|
||||
@@ -2028,7 +2028,7 @@ def test_a_bare_video_plays_even_though_it_is_not_a_recording(make_window, recor
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2091,7 +2091,7 @@ def test_the_replay_and_the_live_paths_share_one_image_backend(make_window, reco
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import orisys_studio.app_window as app_module
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
@@ -2186,7 +2186,7 @@ def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_ro
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2239,7 +2239,7 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
|
||||
make_window, record_root
|
||||
):
|
||||
"""Not a recording *and* not decodable: say what is missing and offer recompute."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2272,7 +2272,7 @@ def test_a_file_that_is_not_a_video_at_all_is_refused_with_a_reason(
|
||||
def test_a_video_inside_a_recording_opens_the_recording_and_plays(
|
||||
make_window, replay_recording
|
||||
):
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2300,7 +2300,7 @@ def test_a_recording_without_a_video_says_it_cannot_be_played(make_window, recor
|
||||
import csv
|
||||
import json
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -2399,7 +2399,7 @@ def test_the_status_bar_is_a_notice_line_not_a_state_line(make_window):
|
||||
"""
|
||||
import time
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import orisys_studio.app_window as app_module
|
||||
|
||||
|
||||
Reference in New Issue
Block a user