move to src layout (#32)

* move to src layout

* fix manifest and version

* fix test structure

* undo

* undo

* undo change

* remove pyargs

* waitsignal

* update label test

* soften eliding test

* another fix

* update again

* more fixes

* more skips

* stupid fixes
This commit is contained in:
Talley Lambert
2021-10-13 09:33:46 -04:00
committed by GitHub
parent 8d76579122
commit 67035a0f0b
42 changed files with 64 additions and 36 deletions

View File

@@ -0,0 +1,70 @@
import platform
from superqt import QElidingLabel
from superqt.qtcompat.QtCore import QSize, Qt
from superqt.qtcompat.QtGui import QResizeEvent
TEXT = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
"minim ven iam, quis nostrud exercitation ullamco laborisnisi ut aliquip "
"ex ea commodo consequat. Duis aute irure dolor inreprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nullapariatur."
)
ELLIPSIS = ""
def test_eliding_label(qtbot):
wdg = QElidingLabel(TEXT)
qtbot.addWidget(wdg)
assert wdg._elidedText().endswith(ELLIPSIS)
oldsize = wdg.size()
newsize = QSize(200, 20)
wdg.resize(newsize)
wdg.resizeEvent(QResizeEvent(oldsize, newsize)) # for test coverage
assert wdg.text() == TEXT
def test_wrapped_eliding_label(qtbot):
wdg = QElidingLabel(TEXT)
qtbot.addWidget(wdg)
assert not wdg.wordWrap()
assert 630 < wdg.sizeHint().width() < 638
assert wdg._elidedText().endswith("")
wdg.resize(QSize(200, 100))
assert wdg.text() == TEXT
assert wdg._elidedText().endswith("")
wdg.setWordWrap(True)
assert wdg.wordWrap()
assert wdg.text() == TEXT
assert wdg._elidedText().endswith("")
# just empirically from CI ... stupid
if platform.system() == "Linux":
assert wdg.sizeHint() in (QSize(200, 198), QSize(200, 154))
elif platform.system() == "Windows":
assert wdg.sizeHint() in (QSize(200, 160), QSize(200, 118))
elif platform.system() == "Darwin":
assert wdg.sizeHint() == QSize(200, 176)
# TODO: figure out how to test these on all platforms on CI
wdg.resize(wdg.sizeHint())
assert wdg._elidedText() == TEXT
def test_shorter_eliding_label(qtbot):
short = "asd a ads sd flksdf dsf lksfj sd lsdjf sd lsdfk sdlkfj s"
wdg = QElidingLabel()
qtbot.addWidget(wdg)
wdg.setText(short)
assert not wdg._elidedText().endswith(ELLIPSIS)
wdg.resize(100, 20)
assert wdg._elidedText().endswith(ELLIPSIS)
wdg.setElideMode(Qt.TextElideMode.ElideLeft)
assert wdg._elidedText().startswith(ELLIPSIS)
assert wdg.elideMode() == Qt.TextElideMode.ElideLeft
def test_wrap_text():
wrap = QElidingLabel.wrapText(TEXT, 200)
assert isinstance(wrap, list)
assert all(isinstance(x, str) for x in wrap)
assert 9 <= len(wrap) <= 13

212
tests/test_ensure_thread.py Normal file
View File

@@ -0,0 +1,212 @@
import inspect
import time
from concurrent.futures import Future, TimeoutError
import pytest
from superqt.qtcompat.QtCore import QCoreApplication, QObject, QThread, Signal
from superqt.utils import ensure_main_thread, ensure_object_thread
class SampleObject(QObject):
assigment_done = Signal()
def __init__(self):
super().__init__()
self.main_thread_res = {}
self.object_thread_res = {}
self.main_thread_prop_val = None
self.sample_thread_prop_val = None
def long_wait(self):
time.sleep(1)
@property
def sample_main_thread_property(self):
return self.main_thread_prop_val
@sample_main_thread_property.setter # type: ignore
@ensure_main_thread()
def sample_main_thread_property(self, value):
if QThread.currentThread() is not QCoreApplication.instance().thread():
raise RuntimeError("Wrong thread")
self.main_thread_prop_val = value
self.assigment_done.emit()
@property
def sample_object_thread_property(self):
return self.sample_thread_prop_val
@sample_object_thread_property.setter # type: ignore
@ensure_object_thread()
def sample_object_thread_property(self, value):
if QThread.currentThread() is not self.thread():
raise RuntimeError("Wrong thread")
self.sample_thread_prop_val = value
self.assigment_done.emit()
@ensure_main_thread
def check_main_thread(self, a, *, b=1):
if QThread.currentThread() is not QCoreApplication.instance().thread():
raise RuntimeError("Wrong thread")
self.main_thread_res = {"a": a, "b": b}
self.assigment_done.emit()
@ensure_object_thread
def check_object_thread(self, a, *, b=1):
if QThread.currentThread() is not self.thread():
raise RuntimeError("Wrong thread")
self.object_thread_res = {"a": a, "b": b}
self.assigment_done.emit()
@ensure_object_thread(await_return=True)
def check_object_thread_return(self, a):
if QThread.currentThread() is not self.thread():
raise RuntimeError("Wrong thread")
return a * 7
@ensure_object_thread(await_return=True, timeout=200)
def check_object_thread_return_timeout(self, a):
if QThread.currentThread() is not self.thread():
raise RuntimeError("Wrong thread")
time.sleep(1)
return a * 7
@ensure_object_thread(await_return=False)
def check_object_thread_return_future(self, a: int):
"""sample docstring"""
if QThread.currentThread() is not self.thread():
raise RuntimeError("Wrong thread")
time.sleep(0.4)
return a * 7
@ensure_main_thread(await_return=True)
def check_main_thread_return(self, a):
if QThread.currentThread() is not QCoreApplication.instance().thread():
raise RuntimeError("Wrong thread")
return a * 8
class LocalThread(QThread):
def __init__(self, ob):
super().__init__()
self.ob = ob
def run(self):
assert QThread.currentThread() is not QCoreApplication.instance().thread()
self.ob.check_main_thread(5, b=8)
self.ob.main_thread_prop_val = "text2"
class LocalThread2(QThread):
def __init__(self, ob):
super().__init__()
self.ob = ob
self.executed = False
def run(self):
assert QThread.currentThread() is not QCoreApplication.instance().thread()
assert self.ob.check_main_thread_return(5) == 40
self.executed = True
def test_only_main_thread(qapp):
ob = SampleObject()
ob.check_main_thread(1, b=3)
assert ob.main_thread_res == {"a": 1, "b": 3}
ob.check_object_thread(2, b=4)
assert ob.object_thread_res == {"a": 2, "b": 4}
ob.sample_main_thread_property = 5
assert ob.sample_main_thread_property == 5
ob.sample_object_thread_property = 7
assert ob.sample_object_thread_property == 7
def test_object_thread(qtbot):
ob = SampleObject()
thread = QThread()
thread.start()
ob.moveToThread(thread)
with qtbot.waitSignal(ob.assigment_done):
ob.check_object_thread(2, b=4)
assert ob.object_thread_res == {"a": 2, "b": 4}
with qtbot.waitSignal(ob.assigment_done):
ob.sample_object_thread_property = "text"
assert ob.sample_object_thread_property == "text"
assert ob.thread() is thread
with qtbot.waitSignal(thread.finished):
thread.exit(0)
def test_main_thread(qtbot):
ob = SampleObject()
t = LocalThread(ob)
with qtbot.waitSignal(t.finished):
t.start()
assert ob.main_thread_res == {"a": 5, "b": 8}
assert ob.sample_main_thread_property == "text2"
def test_object_thread_return(qtbot):
ob = SampleObject()
thread = QThread()
thread.start()
ob.moveToThread(thread)
assert ob.check_object_thread_return(2) == 14
assert ob.thread() is thread
with qtbot.waitSignal(thread.finished):
thread.exit(0)
def test_object_thread_return_timeout(qtbot):
ob = SampleObject()
thread = QThread()
thread.start()
ob.moveToThread(thread)
with pytest.raises(TimeoutError):
ob.check_object_thread_return_timeout(2)
with qtbot.waitSignal(thread.finished):
thread.exit(0)
def test_object_thread_return_future(qtbot):
ob = SampleObject()
thread = QThread()
thread.start()
ob.moveToThread(thread)
future = ob.check_object_thread_return_future(2)
assert isinstance(future, Future)
assert future.result() == 14
with qtbot.waitSignal(thread.finished):
thread.exit(0)
def test_main_thread_return(qtbot):
ob = SampleObject()
t = LocalThread2(ob)
with qtbot.wait_signal(t.finished):
t.start()
assert t.executed
def test_names(qapp):
ob = SampleObject()
assert ob.check_object_thread.__name__ == "check_object_thread"
assert ob.check_object_thread_return.__name__ == "check_object_thread_return"
assert (
ob.check_object_thread_return_timeout.__name__
== "check_object_thread_return_timeout"
)
assert (
ob.check_object_thread_return_future.__name__
== "check_object_thread_return_future"
)
assert ob.check_object_thread_return_future.__doc__ == "sample docstring"
signature = inspect.signature(ob.check_object_thread_return_future)
assert len(signature.parameters) == 1
assert list(signature.parameters.values())[0].name == "a"
assert list(signature.parameters.values())[0].annotation == int
assert ob.check_main_thread_return.__name__ == "check_main_thread_return"

129
tests/test_enum_comb_box.py Normal file
View File

@@ -0,0 +1,129 @@
from enum import Enum
import pytest
from superqt.combobox import QEnumComboBox
from superqt.combobox._enum_combobox import NONE_STRING
class Enum1(Enum):
a = 1
b = 2
c = 3
class Enum2(Enum):
d = 1
e = 2
f = 3
g = 4
class Enum3(Enum):
a = 1
b = 2
c = 3
def __str__(self):
return self.name + "1"
class Enum4(Enum):
a_1 = 1
b_2 = 2
c_3 = 3
def test_simple_create(qtbot):
enum = QEnumComboBox(enum_class=Enum1)
qtbot.addWidget(enum)
assert enum.count() == 3
assert [enum.itemText(i) for i in range(enum.count())] == ["a", "b", "c"]
def test_simple_create2(qtbot):
enum = QEnumComboBox()
qtbot.addWidget(enum)
assert enum.count() == 0
enum.setEnumClass(Enum1)
assert enum.count() == 3
assert [enum.itemText(i) for i in range(enum.count())] == ["a", "b", "c"]
def test_replace(qtbot):
enum = QEnumComboBox(enum_class=Enum1)
qtbot.addWidget(enum)
assert enum.count() == 3
assert enum.enumClass() == Enum1
assert isinstance(enum.currentEnum(), Enum1)
enum.setEnumClass(Enum2)
assert enum.enumClass() == Enum2
assert isinstance(enum.currentEnum(), Enum2)
assert enum.count() == 4
assert [enum.itemText(i) for i in range(enum.count())] == ["d", "e", "f", "g"]
def test_str_replace(qtbot):
enum = QEnumComboBox(enum_class=Enum3)
qtbot.addWidget(enum)
assert enum.count() == 3
assert [enum.itemText(i) for i in range(enum.count())] == ["a1", "b1", "c1"]
def test_underscore_replace(qtbot):
enum = QEnumComboBox(enum_class=Enum4)
qtbot.addWidget(enum)
assert enum.count() == 3
assert [enum.itemText(i) for i in range(enum.count())] == ["a 1", "b 2", "c 3"]
def test_change_value(qtbot):
enum = QEnumComboBox(enum_class=Enum1)
qtbot.addWidget(enum)
assert enum.currentEnum() == Enum1.a
with qtbot.waitSignal(
enum.currentEnumChanged, check_params_cb=lambda x: isinstance(x, Enum)
):
enum.setCurrentEnum(Enum1.c)
assert enum.currentEnum() == Enum1.c
def test_no_enum(qtbot):
enum = QEnumComboBox()
assert enum.enumClass() is None
qtbot.addWidget(enum)
assert enum.currentEnum() is None
def test_prohibited_methods(qtbot):
enum = QEnumComboBox(enum_class=Enum1)
qtbot.addWidget(enum)
with pytest.raises(RuntimeError):
enum.addItem("aaa")
with pytest.raises(RuntimeError):
enum.addItems(["aaa", "bbb"])
with pytest.raises(RuntimeError):
enum.insertItem(0, "aaa")
with pytest.raises(RuntimeError):
enum.insertItems(0, ["aaa", "bbb"])
assert enum.count() == 3
def test_optional(qtbot):
enum = QEnumComboBox(enum_class=Enum1, allow_none=True)
qtbot.addWidget(enum)
assert [enum.itemText(i) for i in range(enum.count())] == [
NONE_STRING,
"a",
"b",
"c",
]
assert enum.currentText() == NONE_STRING
assert enum.currentEnum() is None
enum.setCurrentEnum(Enum1.a)
assert enum.currentText() == "a"
assert enum.currentEnum() == Enum1.a
assert enum.enumClass() is Enum1
enum.setCurrentEnum(None)
assert enum.currentText() == NONE_STRING
assert enum.currentEnum() is None

View File

@@ -0,0 +1,73 @@
from superqt.qtcompat.QtCore import Qt
from superqt.spinbox import QLargeIntSpinBox
def test_large_spinbox(qtbot):
sb = QLargeIntSpinBox()
qtbot.addWidget(sb)
for e in range(2, 100, 2):
sb.setMaximum(10 ** e + 2)
with qtbot.waitSignal(sb.valueChanged) as sgnl:
sb.setValue(10 ** e)
assert sgnl.args == [10 ** e]
assert sb.value() == 10 ** e
sb.setMinimum(-(10 ** e) - 2)
with qtbot.waitSignal(sb.valueChanged) as sgnl:
sb.setValue(-(10 ** e))
assert sgnl.args == [-(10 ** e)]
assert sb.value() == -(10 ** e)
def test_large_spinbox_type(qtbot):
sb = QLargeIntSpinBox()
qtbot.addWidget(sb)
assert isinstance(sb.value(), int)
sb.setValue(1.1)
assert isinstance(sb.value(), int)
assert sb.value() == 1
sb.setValue(1.9)
assert isinstance(sb.value(), int)
assert sb.value() == 1
def test_large_spinbox_signals(qtbot):
sb = QLargeIntSpinBox()
qtbot.addWidget(sb)
with qtbot.waitSignal(sb.valueChanged) as sgnl:
sb.setValue(200)
assert sgnl.args == [200]
with qtbot.waitSignal(sb.textChanged) as sgnl:
sb.setValue(240)
assert sgnl.args == ["240"]
def test_keyboard_tracking(qtbot):
sb = QLargeIntSpinBox()
qtbot.addWidget(sb)
assert sb.value() == 0
sb.setKeyboardTracking(False)
with qtbot.assertNotEmitted(sb.valueChanged):
sb.lineEdit().setText("20")
assert sb.lineEdit().text() == "20"
assert sb.value() == 0
assert sb._pending_emit is True
with qtbot.waitSignal(sb.valueChanged) as sgnl:
qtbot.keyPress(sb, Qt.Key_Enter)
assert sgnl.args == [20]
assert sb._pending_emit is False
sb.setKeyboardTracking(True)
with qtbot.waitSignal(sb.valueChanged) as sgnl:
sb.lineEdit().setText("25")
assert sb._pending_emit is False
assert sgnl.args == [25]

View File

@@ -0,0 +1,35 @@
import logging
from superqt import QMessageHandler
from superqt.qtcompat import QtCore
def test_message_handler():
with QMessageHandler() as mh:
QtCore.qDebug("debug")
QtCore.qWarning("warning")
QtCore.qCritical("critical")
assert len(mh.records) == 3
assert mh.records[0].level == logging.DEBUG
assert mh.records[1].level == logging.WARNING
assert mh.records[2].level == logging.CRITICAL
assert "3 records" in repr(mh)
def test_message_handler_with_logger(caplog):
logger = logging.getLogger("test_logger")
caplog.set_level(logging.DEBUG, logger="test_logger")
with QMessageHandler(logger):
QtCore.qDebug("debug")
QtCore.qWarning("warning")
QtCore.qCritical("critical")
assert len(caplog.records) == 3
caplog.records[0].message == "debug"
caplog.records[0].levelno == logging.DEBUG
caplog.records[1].message == "warning"
caplog.records[1].levelno == logging.WARNING
caplog.records[2].message == "critical"
caplog.records[2].levelno == logging.CRITICAL

View File

View File

@@ -0,0 +1,70 @@
from contextlib import suppress
from distutils.version import LooseVersion
from platform import system
import pytest
from superqt.qtcompat import QT_VERSION
from superqt.qtcompat.QtCore import QEvent, QPoint, QPointF, Qt
from superqt.qtcompat.QtGui import QMouseEvent, QWheelEvent
QT_VERSION = LooseVersion(QT_VERSION)
SYS_DARWIN = system() == "Darwin"
skip_on_linux_qt6 = pytest.mark.skipif(
system() == "Linux" and QT_VERSION >= LooseVersion("6.0"),
reason="hover events not working on linux pyqt6",
)
def _mouse_event(pos=QPointF(), type_=QEvent.MouseMove):
"""Create a mouse event of `type_` at `pos`."""
return QMouseEvent(type_, QPointF(pos), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
def _wheel_event(arc):
"""Create a wheel event with `arc`."""
with suppress(TypeError):
return QWheelEvent(
QPointF(),
QPointF(),
QPoint(arc, arc),
QPoint(arc, arc),
Qt.NoButton,
Qt.NoModifier,
Qt.ScrollBegin,
False,
Qt.MouseEventSynthesizedByQt,
)
with suppress(TypeError):
return QWheelEvent(
QPointF(),
QPointF(),
QPoint(-arc, -arc),
QPoint(-arc, -arc),
1,
Qt.Vertical,
Qt.NoButton,
Qt.NoModifier,
Qt.ScrollBegin,
False,
Qt.MouseEventSynthesizedByQt,
)
return QWheelEvent(
QPointF(),
QPointF(),
QPoint(arc, arc),
QPoint(arc, arc),
1,
Qt.Vertical,
Qt.NoButton,
Qt.NoModifier,
)
def _linspace(start, stop, n):
h = (stop - start) / (n - 1)
for i in range(n):
yield start + h * i

View File

@@ -0,0 +1,124 @@
import os
import pytest
from superqt import (
QDoubleRangeSlider,
QDoubleSlider,
QLabeledDoubleRangeSlider,
QLabeledDoubleSlider,
)
from superqt.qtcompat import API_NAME
range_types = {QDoubleRangeSlider, QLabeledDoubleRangeSlider}
@pytest.fixture(
params=[
QDoubleSlider,
QLabeledDoubleSlider,
QDoubleRangeSlider,
QLabeledDoubleRangeSlider,
]
)
def ds(qtbot, request):
# convenience fixture that converts value() and setValue()
# to let us use setValue((a, b)) for both range and non-range sliders
cls = request.param
wdg = cls()
qtbot.addWidget(wdg)
def assert_val_type():
type_ = float
if cls in range_types:
assert all([isinstance(i, type_) for i in wdg.value()]) # sourcery skip
else:
assert isinstance(wdg.value(), type_)
def assert_val_eq(val):
assert wdg.value() == val if cls is QDoubleRangeSlider else val[0]
wdg.assert_val_type = assert_val_type
wdg.assert_val_eq = assert_val_eq
if cls not in range_types:
superset = wdg.setValue
def _safe_set(val):
superset(val[0] if isinstance(val, tuple) else val)
wdg.setValue = _safe_set
return wdg
def test_double_sliders(ds):
ds.setMinimum(10)
ds.setMaximum(99)
ds.setValue((20, 40))
ds.setSingleStep(1)
assert ds.minimum() == 10
assert ds.maximum() == 99
ds.assert_val_eq((20, 40))
assert ds.singleStep() == 1
ds.assert_val_eq((20, 40))
ds.assert_val_type()
ds.setValue((20.23, 40.23))
ds.assert_val_eq((20.23, 40.23))
ds.assert_val_type()
assert ds.minimum() == 10
assert ds.maximum() == 99
assert ds.singleStep() == 1
ds.assert_val_eq((20.23, 40.23))
ds.setValue((20.2343, 40.2342))
ds.assert_val_eq((20.2343, 40.2342))
ds.assert_val_eq((20.2343, 40.2342))
assert ds.minimum() == 10
assert ds.maximum() == 99
assert ds.singleStep() == 1
ds.assert_val_eq((20.2343, 40.2342))
assert ds.minimum() == 10
assert ds.maximum() == 99
assert ds.singleStep() == 1
def test_double_sliders_small(ds):
ds.setMaximum(1)
ds.setValue((0.5, 0.9))
assert ds.minimum() == 0
assert ds.maximum() == 1
ds.assert_val_eq((0.5, 0.9))
ds.setValue((0.122233, 0.72644353))
ds.assert_val_eq((0.122233, 0.72644353))
def test_double_sliders_big(ds):
ds.setValue((20, 80))
ds.setMaximum(5e14)
assert ds.minimum() == 0
assert ds.maximum() == 5e14
ds.setValue((1.74e9, 1.432e10))
ds.assert_val_eq((1.74e9, 1.432e10))
@pytest.mark.skipif(
os.name == "nt" and API_NAME == "PyQt6", reason="Not ready for pyqt6"
)
def test_signals(ds, qtbot):
with qtbot.waitSignal(ds.valueChanged):
ds.setValue((10, 20))
with qtbot.waitSignal(ds.rangeChanged):
ds.setMinimum(0.5)
with qtbot.waitSignal(ds.rangeChanged):
ds.setMaximum(3.7)
with qtbot.waitSignal(ds.rangeChanged):
ds.setRange(1.2, 3.3)

View File

@@ -0,0 +1,184 @@
import math
import platform
import pytest
from superqt.qtcompat.QtCore import QEvent, QPoint, QPointF, Qt
from superqt.qtcompat.QtGui import QHoverEvent
from superqt.qtcompat.QtWidgets import QStyle, QStyleOptionSlider
from superqt.sliders._generic_slider import _GenericSlider, _sliderValueFromPosition
from ._testutil import _linspace, _mouse_event, _wheel_event, skip_on_linux_qt6
@pytest.fixture(params=[Qt.Horizontal, Qt.Vertical])
def gslider(qtbot, request):
slider = _GenericSlider(request.param)
qtbot.addWidget(slider)
assert slider.value() == 0
assert slider.minimum() == 0
assert slider.maximum() == 99
yield slider
slider.initStyleOption(QStyleOptionSlider())
def test_change_floatslider_range(gslider: _GenericSlider, qtbot):
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setMinimum(10)
assert gslider.value() == 10 == gslider.minimum()
assert gslider.maximum() == 99
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setMaximum(90)
assert gslider.value() == 10 == gslider.minimum()
assert gslider.maximum() == 90
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setRange(20, 40)
assert gslider.value() == 20 == gslider.minimum()
assert gslider.maximum() == 40
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue(30)
assert gslider.value() == 30
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setMaximum(25)
assert gslider.value() == 25 == gslider.maximum()
assert gslider.minimum() == 20
def test_float_values(gslider: _GenericSlider, qtbot):
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(0.25, 0.75)
assert gslider.minimum() == 0.25
assert gslider.maximum() == 0.75
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue(0.55)
assert gslider.value() == 0.55
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue(1.55)
assert gslider.value() == 0.75 == gslider.maximum()
def test_ticks(gslider: _GenericSlider, qtbot):
gslider.setTickInterval(0.3)
assert gslider.tickInterval() == 0.3
gslider.setTickPosition(gslider.TicksAbove)
gslider.show()
def test_show(gslider, qtbot):
gslider.show()
@pytest.mark.skipif(platform.system() != "Darwin", reason="cross-platform is tricky")
def test_press_move_release(gslider: _GenericSlider, qtbot):
assert gslider._pressedControl == QStyle.SubControl.SC_None
opt = QStyleOptionSlider()
gslider.initStyleOption(opt)
style = gslider.style()
hrect = style.subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle)
handle_pos = gslider.mapToGlobal(hrect.center())
with qtbot.waitSignal(gslider.sliderPressed):
qtbot.mousePress(gslider, Qt.LeftButton, pos=handle_pos)
assert gslider._pressedControl == QStyle.SubControl.SC_SliderHandle
with qtbot.waitSignals([gslider.sliderMoved, gslider.valueChanged]):
shift = QPoint(0, -8) if gslider.orientation() == Qt.Vertical else QPoint(8, 0)
gslider.mouseMoveEvent(_mouse_event(handle_pos + shift))
with qtbot.waitSignal(gslider.sliderReleased):
qtbot.mouseRelease(gslider, Qt.LeftButton, pos=handle_pos)
assert gslider._pressedControl == QStyle.SubControl.SC_None
gslider.show()
with qtbot.waitSignal(gslider.sliderPressed):
qtbot.mousePress(gslider, Qt.LeftButton, pos=handle_pos)
@skip_on_linux_qt6
def test_hover(gslider: _GenericSlider):
opt = QStyleOptionSlider()
gslider.initStyleOption(opt)
style = gslider.style()
hrect = style.subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle)
handle_pos = QPointF(gslider.mapToGlobal(hrect.center()))
assert gslider._hoverControl == QStyle.SubControl.SC_None
gslider.event(QHoverEvent(QEvent.HoverEnter, handle_pos, QPointF()))
assert gslider._hoverControl == QStyle.SubControl.SC_SliderHandle
gslider.event(QHoverEvent(QEvent.HoverLeave, QPointF(-1000, -1000), handle_pos))
assert gslider._hoverControl == QStyle.SubControl.SC_None
def test_wheel(gslider: _GenericSlider, qtbot):
with qtbot.waitSignal(gslider.valueChanged):
gslider.wheelEvent(_wheel_event(120))
gslider.wheelEvent(_wheel_event(0))
def test_position(gslider: _GenericSlider, qtbot):
gslider.setSliderPosition(21.2)
assert gslider.sliderPosition() == 21.2
def test_steps(gslider: _GenericSlider, qtbot):
gslider.setSingleStep(0.1)
assert gslider.singleStep() == 0.1
gslider.setSingleStep(1.5e20)
assert gslider.singleStep() == 1.5e20
gslider.setPageStep(0.2)
assert gslider.pageStep() == 0.2
gslider.setPageStep(1.5e30)
assert gslider.pageStep() == 1.5e30
@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4)))
def test_slider_extremes(gslider: _GenericSlider, mag, qtbot):
_mag = 10 ** mag
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(-_mag, _mag)
for i in _linspace(-_mag, _mag, 10):
gslider.setValue(i)
assert math.isclose(gslider.value(), i, rel_tol=1e-8)
gslider.initStyleOption(QStyleOptionSlider())
# args are (min: float, max: float, position: int, span: int, upsideDown: bool)
@pytest.mark.parametrize(
"args, result",
[
# (min, max, pos, span[, inverted]), expectation
# data range (1, 2)
((1, 2, 50, 100), 1.5),
((1, 2, 70, 100), 1.7),
((1, 2, 70, 100, True), 1.3), # inverted appearance
((1, 2, 170, 100), 2),
((1, 2, 100, 100), 2),
((1, 2, -30, 100), 1),
# data range (-2, 2)
((-2, 2, 50, 100), 0),
((-2, 2, 75, 100), 1),
((-2, 2, 75, 100, True), -1), # inverted appearance
((-2, 2, 170, 100), 2),
((-2, 2, 100, 100), 2),
((-2, 2, -30, 100), -2),
],
)
def test_slider_value_from_position(args, result):
assert math.isclose(_sliderValueFromPosition(*args), result)

View File

@@ -0,0 +1,11 @@
from superqt import QLabeledRangeSlider
def test_labeled_slider_api(qtbot):
slider = QLabeledRangeSlider()
qtbot.addWidget(slider)
slider.hideBar()
slider.showBar()
slider.setBarVisible()
slider.setBarMovesAllHandles()
slider.setBarIsRigid()

View File

@@ -0,0 +1,156 @@
import math
import pytest
from superqt import QDoubleRangeSlider, QRangeSlider
from superqt.qtcompat.QtCore import QEvent, QPoint, QPointF, Qt
from superqt.qtcompat.QtGui import QHoverEvent
from superqt.qtcompat.QtWidgets import QStyle, QStyleOptionSlider
from ._testutil import _linspace, _mouse_event, _wheel_event, skip_on_linux_qt6
@pytest.fixture(params=[Qt.Horizontal, Qt.Vertical])
def gslider(qtbot, request):
slider = QDoubleRangeSlider(request.param)
qtbot.addWidget(slider)
assert slider.value() == (20, 80)
assert slider.minimum() == 0
assert slider.maximum() == 99
yield slider
slider.initStyleOption(QStyleOptionSlider())
def test_change_floatslider_range(gslider: QRangeSlider, qtbot):
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setMinimum(30)
assert gslider.value()[0] == 30 == gslider.minimum()
assert gslider.maximum() == 99
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setMaximum(70)
assert gslider.value()[0] == 30 == gslider.minimum()
assert gslider.value()[1] == 70 == gslider.maximum()
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setRange(40, 60)
assert gslider.value()[0] == 40 == gslider.minimum()
assert gslider.maximum() == 60
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue([40, 50])
assert gslider.value()[0] == 40 == gslider.minimum()
assert gslider.value()[1] == 50
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setMaximum(45)
assert gslider.value()[0] == 40 == gslider.minimum()
assert gslider.value()[1] == 45 == gslider.maximum()
def test_float_values(gslider: QRangeSlider, qtbot):
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(0.1, 0.9)
assert gslider.minimum() == 0.1
assert gslider.maximum() == 0.9
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue([0.4, 0.6])
assert gslider.value() == (0.4, 0.6)
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue([0, 1.9])
assert gslider.value()[0] == 0.1 == gslider.minimum()
assert gslider.value()[1] == 0.9 == gslider.maximum()
def test_position(gslider: QRangeSlider, qtbot):
gslider.setSliderPosition([10, 80])
assert gslider.sliderPosition() == (10, 80)
def test_steps(gslider: QRangeSlider, qtbot):
gslider.setSingleStep(0.1)
assert gslider.singleStep() == 0.1
gslider.setSingleStep(1.5e20)
assert gslider.singleStep() == 1.5e20
gslider.setPageStep(0.2)
assert gslider.pageStep() == 0.2
gslider.setPageStep(1.5e30)
assert gslider.pageStep() == 1.5e30
@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4)))
def test_slider_extremes(gslider: QRangeSlider, mag, qtbot):
_mag = 10 ** mag
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(-_mag, _mag)
for i in _linspace(-_mag, _mag, 10):
gslider.setValue((i, _mag))
assert math.isclose(gslider.value()[0], i, rel_tol=1e-8)
gslider.initStyleOption(QStyleOptionSlider())
def test_ticks(gslider: QRangeSlider, qtbot):
gslider.setTickInterval(0.3)
assert gslider.tickInterval() == 0.3
gslider.setTickPosition(gslider.TicksAbove)
gslider.show()
def test_show(gslider, qtbot):
gslider.show()
def test_press_move_release(gslider: QRangeSlider, qtbot):
assert gslider._pressedControl == QStyle.SubControl.SC_None
opt = QStyleOptionSlider()
gslider.initStyleOption(opt)
style = gslider.style()
hrect = style.subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle)
handle_pos = gslider.mapToGlobal(hrect.center())
with qtbot.waitSignal(gslider.sliderPressed):
qtbot.mousePress(gslider, Qt.LeftButton, pos=handle_pos)
assert gslider._pressedControl == QStyle.SubControl.SC_SliderHandle
with qtbot.waitSignals([gslider.sliderMoved, gslider.valueChanged]):
shift = QPoint(0, -8) if gslider.orientation() == Qt.Vertical else QPoint(8, 0)
gslider.mouseMoveEvent(_mouse_event(handle_pos + shift))
with qtbot.waitSignal(gslider.sliderReleased):
qtbot.mouseRelease(gslider, Qt.LeftButton, pos=handle_pos)
assert gslider._pressedControl == QStyle.SubControl.SC_None
gslider.show()
with qtbot.waitSignal(gslider.sliderPressed):
qtbot.mousePress(gslider, Qt.LeftButton, pos=handle_pos)
@skip_on_linux_qt6
def test_hover(gslider: QRangeSlider):
hrect = gslider._handleRect(0)
handle_pos = QPointF(gslider.mapToGlobal(hrect.center()))
assert gslider._hoverControl == QStyle.SubControl.SC_None
gslider.event(QHoverEvent(QEvent.HoverEnter, handle_pos, QPointF()))
assert gslider._hoverControl == QStyle.SubControl.SC_SliderHandle
gslider.event(QHoverEvent(QEvent.HoverLeave, QPointF(-1000, -1000), handle_pos))
assert gslider._hoverControl == QStyle.SubControl.SC_None
def test_wheel(gslider: QRangeSlider, qtbot):
with qtbot.waitSignal(gslider.valueChanged):
gslider.wheelEvent(_wheel_event(120))
gslider.wheelEvent(_wheel_event(0))

View File

@@ -0,0 +1,223 @@
import math
import platform
from contextlib import suppress
from distutils.version import LooseVersion
import pytest
from superqt import QDoubleSlider, QLabeledDoubleSlider, QLabeledSlider
from superqt.qtcompat.QtCore import QEvent, QPoint, QPointF, Qt
from superqt.qtcompat.QtGui import QHoverEvent
from superqt.qtcompat.QtWidgets import QSlider, QStyle, QStyleOptionSlider
from superqt.sliders._generic_slider import _GenericSlider
from ._testutil import (
QT_VERSION,
_linspace,
_mouse_event,
_wheel_event,
skip_on_linux_qt6,
)
@pytest.fixture(params=[Qt.Horizontal, Qt.Vertical], ids=["horizontal", "vertical"])
def orientation(request):
return request.param
START_MI_MAX_VAL = (0, 99, 0)
TEST_SLIDERS = [QDoubleSlider, QLabeledSlider, QLabeledDoubleSlider]
def _assert_value_in_range(sld):
val = sld.value()
if isinstance(val, (int, float)):
val = (val,)
assert all(sld.minimum() <= v <= sld.maximum() for v in val)
@pytest.fixture(params=TEST_SLIDERS)
def sld(request, qtbot, orientation):
Cls = request.param
slider = Cls(orientation)
slider.setRange(*START_MI_MAX_VAL[:2])
slider.setValue(START_MI_MAX_VAL[2])
qtbot.addWidget(slider)
assert (slider.minimum(), slider.maximum(), slider.value()) == START_MI_MAX_VAL
_assert_value_in_range(slider)
yield slider
_assert_value_in_range(slider)
with suppress(AttributeError):
slider.initStyleOption(QStyleOptionSlider())
def called_with(*expected_result):
"""Use in check_params_cbs to assert that a callback is called as expected.
e.g. `called_with(20, 50)` returns a callback that checks that the callback
is called with the arguments (20, 50)
"""
def check_emitted_values(*values):
return values == expected_result
return check_emitted_values
def test_change_floatslider_range(sld: _GenericSlider, qtbot):
BOTH = [sld.rangeChanged, sld.valueChanged]
for signals, checks, funcname, args in [
(BOTH, [called_with(10, 99), called_with(10)], "setMinimum", (10,)),
([sld.rangeChanged], [called_with(10, 90)], "setMaximum", (90,)),
(BOTH, [called_with(20, 40), called_with(20)], "setRange", (20, 40)),
([sld.valueChanged], [called_with(30)], "setValue", (30,)),
(BOTH, [called_with(20, 25), called_with(25)], "setMaximum", (25,)),
([sld.valueChanged], [called_with(23)], "setValue", (23,)),
]:
with qtbot.waitSignals(signals, check_params_cbs=checks, timeout=500):
getattr(sld, funcname)(*args)
_assert_value_in_range(sld)
def test_float_values(sld: _GenericSlider, qtbot):
if type(sld) is QLabeledSlider:
pytest.skip()
for signals, checks, funcname, args in [
(sld.rangeChanged, called_with(0.1, 0.9), "setRange", (0.1, 0.9)),
(sld.valueChanged, called_with(0.4), "setValue", (0.4,)),
(sld.valueChanged, called_with(0.1), "setValue", (0,)),
(sld.valueChanged, called_with(0.9), "setValue", (1.9,)),
]:
with qtbot.waitSignal(signals, check_params_cb=checks, timeout=400):
getattr(sld, funcname)(*args)
_assert_value_in_range(sld)
def test_ticks(sld: _GenericSlider, qtbot):
sld.setTickInterval(3)
assert sld.tickInterval() == 3
sld.setTickPosition(QSlider.TicksAbove)
sld.show()
@pytest.mark.skipif(platform.system() != "Darwin", reason="cross-platform is tricky")
def test_press_move_release(sld: _GenericSlider, qtbot):
if hasattr(sld, "_slider") and sld._slider.orientation() == Qt.Vertical:
pytest.xfail("test failing for vertical at the moment")
_real_sld = getattr(sld, "_slider", sld)
with suppress(AttributeError): # for QSlider
assert _real_sld._pressedControl == QStyle.SubControl.SC_None
opt = QStyleOptionSlider()
_real_sld.initStyleOption(opt)
style = _real_sld.style()
hrect = style.subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle)
handle_pos = _real_sld.mapToGlobal(hrect.center())
with qtbot.waitSignal(_real_sld.sliderPressed, timeout=300):
qtbot.mousePress(_real_sld, Qt.LeftButton, pos=handle_pos)
with suppress(AttributeError):
assert sld._pressedControl == QStyle.SubControl.SC_SliderHandle
with qtbot.waitSignals(
[_real_sld.sliderMoved, _real_sld.valueChanged], timeout=300
):
shift = (
QPoint(0, -8) if _real_sld.orientation() == Qt.Vertical else QPoint(8, 0)
)
_real_sld.mouseMoveEvent(_mouse_event(handle_pos + shift))
with qtbot.waitSignal(_real_sld.sliderReleased, timeout=300):
qtbot.mouseRelease(_real_sld, Qt.LeftButton, pos=handle_pos)
with suppress(AttributeError):
assert _real_sld._pressedControl == QStyle.SubControl.SC_None
sld.show()
with qtbot.waitSignal(_real_sld.sliderPressed, timeout=300):
qtbot.mousePress(_real_sld, Qt.LeftButton, pos=handle_pos)
@skip_on_linux_qt6
def test_hover(sld: _GenericSlider):
_real_sld = getattr(sld, "_slider", sld)
opt = QStyleOptionSlider()
_real_sld.initStyleOption(opt)
hrect = _real_sld.style().subControlRect(
QStyle.CC_Slider, opt, QStyle.SC_SliderHandle
)
handle_pos = QPointF(sld.mapToGlobal(hrect.center()))
with suppress(AttributeError): # for QSlider
assert _real_sld._hoverControl == QStyle.SubControl.SC_None
_real_sld.event(QHoverEvent(QEvent.HoverEnter, handle_pos, QPointF()))
with suppress(AttributeError): # for QSlider
assert _real_sld._hoverControl == QStyle.SubControl.SC_SliderHandle
_real_sld.event(QHoverEvent(QEvent.HoverLeave, QPointF(-1000, -1000), handle_pos))
with suppress(AttributeError): # for QSlider
assert _real_sld._hoverControl == QStyle.SubControl.SC_None
def test_wheel(sld: _GenericSlider, qtbot):
if type(sld) is QLabeledSlider and QT_VERSION < LooseVersion("5.12"):
pytest.skip()
_real_sld = getattr(sld, "_slider", sld)
with qtbot.waitSignal(sld.valueChanged, timeout=400):
_real_sld.wheelEvent(_wheel_event(120))
_real_sld.wheelEvent(_wheel_event(0))
def test_position(sld: _GenericSlider, qtbot):
sld.setSliderPosition(21)
assert sld.sliderPosition() == 21
if type(sld) is not QLabeledSlider:
sld.setSliderPosition(21.5)
assert sld.sliderPosition() == 21.5
def test_steps(sld: _GenericSlider, qtbot):
sld.setSingleStep(11)
assert sld.singleStep() == 11
sld.setPageStep(16)
assert sld.pageStep() == 16
if type(sld) is not QLabeledSlider:
sld.setSingleStep(0.1)
assert sld.singleStep() == 0.1
sld.setSingleStep(1.5e20)
assert sld.singleStep() == 1.5e20
sld.setPageStep(0.2)
assert sld.pageStep() == 0.2
sld.setPageStep(1.5e30)
assert sld.pageStep() == 1.5e30
@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4)))
def test_slider_extremes(sld: _GenericSlider, mag, qtbot):
if type(sld) is QLabeledSlider:
pytest.skip()
_mag = 10 ** mag
with qtbot.waitSignal(sld.rangeChanged, timeout=400):
sld.setRange(-_mag, _mag)
for i in _linspace(-_mag, _mag, 10):
sld.setValue(i)
assert math.isclose(sld.value(), i, rel_tol=1e-8)

View File

@@ -0,0 +1,142 @@
import platform
import pytest
from superqt import QRangeSlider
from superqt.qtcompat import API_NAME
from superqt.qtcompat.QtCore import Qt
from superqt.sliders._generic_range_slider import SC_BAR, SC_HANDLE, SC_NONE
NOT_LINUX = platform.system() != "Linux"
NOT_PYSIDE2 = API_NAME != "PySide2"
skipmouse = pytest.mark.skipif(NOT_LINUX or NOT_PYSIDE2, reason="mouse tests finicky")
@pytest.mark.parametrize("orientation", ["Horizontal", "Vertical"])
def test_basic(qtbot, orientation):
rs = QRangeSlider(getattr(Qt, orientation))
qtbot.addWidget(rs)
@pytest.mark.parametrize("orientation", ["Horizontal", "Vertical"])
def test_value(qtbot, orientation):
rs = QRangeSlider(getattr(Qt, orientation))
qtbot.addWidget(rs)
rs.setValue([10, 20])
assert rs.value() == (10, 20)
@pytest.mark.parametrize("orientation", ["Horizontal", "Vertical"])
def test_range(qtbot, orientation):
rs = QRangeSlider(getattr(Qt, orientation))
qtbot.addWidget(rs)
rs.setValue([10, 20])
assert rs.value() == (10, 20)
rs.setRange(15, 20)
assert rs.value() == (15, 20)
assert rs.minimum() == 15
assert rs.maximum() == 20
@skipmouse
def test_drag_handles(qtbot):
rs = QRangeSlider(Qt.Horizontal)
qtbot.addWidget(rs)
rs.setRange(0, 99)
rs.setValue((20, 80))
rs.setMouseTracking(True)
rs.show()
# press the left handle
pos = rs._handleRect(0).center()
with qtbot.waitSignal(rs.sliderPressed):
qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
assert rs._pressedControl == SC_HANDLE
assert rs._pressedIndex == 0
# drag the left handle
with qtbot.waitSignals([rs.sliderMoved] * 13): # couple less signals
for _ in range(15):
pos.setX(pos.x() + 2)
qtbot.mouseMove(rs, pos)
with qtbot.waitSignal(rs.sliderReleased):
qtbot.mouseRelease(rs, Qt.LeftButton)
# check the values
assert rs.value()[0] > 30
assert rs._pressedControl == SC_NONE
# press the right handle
pos = rs._handleRect(1).center()
with qtbot.waitSignal(rs.sliderPressed):
qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
assert rs._pressedControl == SC_HANDLE
assert rs._pressedIndex == 1
# drag the right handle
with qtbot.waitSignals([rs.sliderMoved] * 13): # couple less signals
for _ in range(15):
pos.setX(pos.x() - 2)
qtbot.mouseMove(rs, pos)
with qtbot.waitSignal(rs.sliderReleased):
qtbot.mouseRelease(rs, Qt.LeftButton)
# check the values
assert rs.value()[1] < 70
assert rs._pressedControl == SC_NONE
@skipmouse
def test_drag_handles_beyond_edge(qtbot):
rs = QRangeSlider(Qt.Horizontal)
qtbot.addWidget(rs)
rs.setRange(0, 99)
rs.setValue((20, 80))
rs.setMouseTracking(True)
rs.show()
# press the right handle
pos = rs._handleRect(1).center()
with qtbot.waitSignal(rs.sliderPressed):
qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
assert rs._pressedControl == SC_HANDLE
assert rs._pressedIndex == 1
# drag the handle off the right edge and make sure the value gets to the max
for _ in range(7):
pos.setX(pos.x() + 10)
qtbot.mouseMove(rs, pos)
with qtbot.waitSignal(rs.sliderReleased):
qtbot.mouseRelease(rs, Qt.LeftButton)
assert rs.value()[1] == 99
@skipmouse
def test_bar_drag_beyond_edge(qtbot):
rs = QRangeSlider(Qt.Horizontal)
qtbot.addWidget(rs)
rs.setRange(0, 99)
rs.setValue((20, 80))
rs.setMouseTracking(True)
rs.show()
# press the right handle
pos = rs.rect().center()
with qtbot.waitSignal(rs.sliderPressed):
qtbot.mousePress(rs, Qt.LeftButton, pos=pos)
assert rs._pressedControl == SC_BAR
assert rs._pressedIndex == 1
# drag the handle off the right edge and make sure the value gets to the max
for _ in range(15):
pos.setX(pos.x() + 10)
qtbot.mouseMove(rs, pos)
with qtbot.waitSignal(rs.sliderReleased):
qtbot.mouseRelease(rs, Qt.LeftButton)
assert rs.value()[1] == 99