diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 8259c29a..8bebe263 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -159,32 +159,33 @@ jobs: name: napari tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 path: superqt - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: repository: napari/napari - path: napari + path: napari-repo fetch-depth: 2 - uses: tlambert03/setup-qt-libs@v1 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.10' - name: install run: | python -m pip install -U pip - python -m pip install -e ./napari[testing,pyqt5] - python -m pip install -e ./superqt + python -m pip install ./superqt + python -m pip install ./napari-repo[testing,pyqt5] - - name: Test napari magicgui + - name: Test napari uses: GabrielBB/xvfb-action@v1 with: - run: python -m pytest --color=yes napari/napari/_qt + working-directory: napari-repo + run: python -m pytest --color=yes napari/_qt check_manifest: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9faa9c7..7c1e46f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,17 +5,18 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 + rev: v2.0.0 hooks: - id: setup-cfg-fmt + args: ["--include-version-classifiers"] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.7.0] exclude: examples - - repo: https://github.com/myint/autoflake - rev: v1.4 + - repo: https://github.com/PyCQA/autoflake + rev: v1.6.1 hooks: - id: autoflake args: ["--in-place", "--remove-all-unused-imports"] @@ -24,16 +25,16 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus, --keep-runtime-typing] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.8.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.961 + rev: v0.971 hooks: - id: mypy exclude: examples diff --git a/CHANGELOG.md b/CHANGELOG.md index de34dab8..058a5617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,38 @@ # Changelog -## [0.3.2](https://github.com/napari/superqt/tree/0.3.2) (2022-05-02) +## [0.3.5](https://github.com/napari/superqt/tree/0.3.5) (2022-08-17) -[Full Changelog](https://github.com/napari/superqt/compare/v0.3.1...0.3.2) +[Full Changelog](https://github.com/napari/superqt/compare/v0.3.4...0.3.5) + +**Fixed bugs:** + +- fix range slider drag crash on PyQt6 [\#108](https://github.com/napari/superqt/pull/108) ([sfhbarnett](https://github.com/sfhbarnett)) +- Fix float value error in pyqt configuration [\#106](https://github.com/napari/superqt/pull/106) ([mstabrin](https://github.com/mstabrin)) + +## [v0.3.4](https://github.com/napari/superqt/tree/v0.3.4) (2022-07-24) + +[Full Changelog](https://github.com/napari/superqt/compare/v0.3.3...v0.3.4) + +**Fixed bugs:** + +- fix: relax runtime typing extensions requirement [\#101](https://github.com/napari/superqt/pull/101) ([tlambert03](https://github.com/tlambert03)) +- fix: catch qpixmap deprecation [\#99](https://github.com/napari/superqt/pull/99) ([tlambert03](https://github.com/tlambert03)) + +## [v0.3.3](https://github.com/napari/superqt/tree/v0.3.3) (2022-07-10) + +[Full Changelog](https://github.com/napari/superqt/compare/v0.3.2...v0.3.3) + +**Implemented enhancements:** + +- Add code syntax highlight utils [\#88](https://github.com/napari/superqt/pull/88) ([Czaki](https://github.com/Czaki)) + +**Fixed bugs:** + +- fix: fix deprecation warning on fonticon plugin discovery on python 3.10 [\#95](https://github.com/napari/superqt/pull/95) ([tlambert03](https://github.com/tlambert03)) + +## [v0.3.2](https://github.com/napari/superqt/tree/v0.3.2) (2022-05-03) + +[Full Changelog](https://github.com/napari/superqt/compare/v0.3.1...v0.3.2) **Implemented enhancements:** @@ -18,6 +48,10 @@ - Fix deprecation warnings in tests [\#82](https://github.com/napari/superqt/pull/82) ([tlambert03](https://github.com/tlambert03)) +**Merged pull requests:** + +- Add changelog for v0.3.2 [\#86](https://github.com/napari/superqt/pull/86) ([tlambert03](https://github.com/tlambert03)) + ## [v0.3.1](https://github.com/napari/superqt/tree/v0.3.1) (2022-03-02) [Full Changelog](https://github.com/napari/superqt/compare/v0.3.0...v0.3.1) @@ -139,21 +173,13 @@ ## [v0.2.1](https://github.com/napari/superqt/tree/v0.2.1) (2021-07-10) -[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0rc0...v0.2.1) +[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0...v0.2.1) **Fixed bugs:** - Fix QLabeledRangeSlider API \(fix slider proxy\) [\#10](https://github.com/napari/superqt/pull/10) ([tlambert03](https://github.com/tlambert03)) - Fix range slider with negative min range [\#9](https://github.com/napari/superqt/pull/9) ([tlambert03](https://github.com/tlambert03)) -## [v0.2.0rc0](https://github.com/napari/superqt/tree/v0.2.0rc0) (2021-06-26) - -[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0rc1...v0.2.0rc0) - -## [v0.2.0rc1](https://github.com/napari/superqt/tree/v0.2.0rc1) (2021-06-26) - -[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0...v0.2.0rc1) - \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/setup.cfg b/setup.cfg index 7ba0f0d1..b5fa5db7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ install_requires = packaging pygments>=2.4.0 qtpy>=1.1.0 - typing-extensions>=3.10.0.0 + typing-extensions python_requires = >=3.7 include_package_data = True package_dir = diff --git a/src/superqt/combobox/_enum_combobox.py b/src/superqt/combobox/_enum_combobox.py index f9baa886..75d4c503 100644 --- a/src/superqt/combobox/_enum_combobox.py +++ b/src/superqt/combobox/_enum_combobox.py @@ -12,7 +12,10 @@ def _get_name(enum_value: Enum): """Create human readable name if user does not provide own implementation of __str__""" - if enum_value.__str__.__module__ != "enum": + if ( + enum_value.__str__.__module__ != "enum" + and not enum_value.__str__.__module__.startswith("shibokensupport") + ): # check if function was overloaded name = str(enum_value) else: diff --git a/src/superqt/fonticon/_qfont_icon.py b/src/superqt/fonticon/_qfont_icon.py index 47c50a28..60f21092 100644 --- a/src/superqt/fonticon/_qfont_icon.py +++ b/src/superqt/fonticon/_qfont_icon.py @@ -243,7 +243,9 @@ def paint( def pixmap(self, size: QSize, mode: QIcon.Mode, state: QIcon.State) -> QPixmap: # first look in cache pmckey = self._pmcKey(size, mode, state) - pm = QPixmapCache.find(pmckey) if pmckey else None + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "QPixmapCache.find") + pm = QPixmapCache.find(pmckey) if pmckey else None if pm: return pm pixmap = QPixmap(size) diff --git a/src/superqt/sliders/_generic_range_slider.py b/src/superqt/sliders/_generic_range_slider.py index f5142da1..7452c41c 100644 --- a/src/superqt/sliders/_generic_range_slider.py +++ b/src/superqt/sliders/_generic_range_slider.py @@ -146,11 +146,17 @@ def event(self, ev: QEvent) -> bool: def mouseMoveEvent(self, ev: QtGui.QMouseEvent) -> None: if self._pressedControl == SC_BAR: ev.accept() - delta = self._clickOffset - self._pixelPosToRangeValue(self._pick(ev.pos())) + delta = self._clickOffset - self._pixelPosToRangeValue( + self._pick(self._event_position(ev)) + ) self._offsetAllPositions(-delta, self._sldPosAtPress) else: super().mouseMoveEvent(ev) + def _event_position(self, event): + # API changes between PyQt5 (.pos()) and PyQt6 (.position()) + return event.pos() if hasattr(event, "pos") else event.position() + # ############### Implementation Details ####################### def _setPosition(self, val): diff --git a/src/superqt/sliders/_generic_slider.py b/src/superqt/sliders/_generic_slider.py index 1711f63f..d531d5c0 100644 --- a/src/superqt/sliders/_generic_slider.py +++ b/src/superqt/sliders/_generic_slider.py @@ -44,9 +44,9 @@ class _GenericSlider(QSlider, Generic[_T]): - _fvalueChanged = Signal(float) - _fsliderMoved = Signal(float) - _frangeChanged = Signal(float, float) + _fvalueChanged = Signal(int) + _fsliderMoved = Signal(int) + _frangeChanged = Signal(int, int) MAX_DISPLAY = 5000 @@ -134,8 +134,8 @@ def setMaximum(self, max: float) -> None: self.setRange(min(self._minimum, max), max) def setRange(self, min: float, max_: float) -> None: - oldMin, self._minimum = self._minimum, float(min) - oldMax, self._maximum = self._maximum, float(max(min, max_)) + oldMin, self._minimum = self._minimum, self._type_cast(min) + oldMax, self._maximum = self._maximum, self._type_cast(max(min, max_)) if oldMin != self._minimum or oldMax != self._maximum: self.sliderChange(self.SliderChange.SliderRangeChange) diff --git a/src/superqt/sliders/_labeled.py b/src/superqt/sliders/_labeled.py index f39754ce..a891ae36 100644 --- a/src/superqt/sliders/_labeled.py +++ b/src/superqt/sliders/_labeled.py @@ -128,7 +128,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(parent) self._slider = self._slider_class() - self._label = SliderLabel(self._slider, connect=self._slider.setValue) + self._label = SliderLabel(self._slider, connect=self._setValue) self._edge_label_mode: EdgeLabelMode = EdgeLabelMode.LabelIsValue self._rename_signals() @@ -142,6 +142,13 @@ def __init__(self, *args, **kwargs) -> None: self.setOrientation(orientation) + def _setValue(self, value: float): + """ + Convert the value from float to int before + setting the slider value + """ + self._slider.setValue(int(value)) + def _rename_signals(self): # for subclasses pass diff --git a/src/superqt/sliders/_sliders.py b/src/superqt/sliders/_sliders.py index 78747b66..f82a3253 100644 --- a/src/superqt/sliders/_sliders.py +++ b/src/superqt/sliders/_sliders.py @@ -14,6 +14,10 @@ def _type_cast(self, value) -> int: class _FloatMixin: + _fvalueChanged = Signal(float) + _fsliderMoved = Signal(float) + _frangeChanged = Signal(float, float) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._singleStep = 0.01 diff --git a/src/superqt/utils/_qthreading.py b/src/superqt/utils/_qthreading.py index 58164907..2494b464 100644 --- a/src/superqt/utils/_qthreading.py +++ b/src/superqt/utils/_qthreading.py @@ -21,10 +21,8 @@ ) from qtpy.QtCore import QObject, QRunnable, QThread, QThreadPool, QTimer, Signal -from typing_extensions import Literal, ParamSpec if TYPE_CHECKING: - _T = TypeVar("_T") class SigInst(Generic[_T]): @@ -40,11 +38,21 @@ def disconnect(slot: Callable[[_T], Any] = ...) -> None: def emit(*args: _T) -> None: ... + from typing_extensions import Literal, ParamSpec + + _P = ParamSpec("_P") +# maintain runtime compatibility with older typing_extensions +else: + try: + from typing_extensions import ParamSpec + + _P = ParamSpec("_P") + except ImportError: + _P = TypeVar("_P") _Y = TypeVar("_Y") _S = TypeVar("_S") _R = TypeVar("_R") -_P = ParamSpec("_P") def as_generator_function( diff --git a/src/superqt/utils/_throttler.py b/src/superqt/utils/_throttler.py index abd046a5..fbb51766 100644 --- a/src/superqt/utils/_throttler.py +++ b/src/superqt/utils/_throttler.py @@ -33,10 +33,22 @@ from typing import TYPE_CHECKING, Callable, Generic, Optional, TypeVar, Union, overload from qtpy.QtCore import QObject, Qt, QTimer, Signal -from typing_extensions import Literal, ParamSpec if TYPE_CHECKING: from qtpy.QtCore import SignalInstance + from typing_extensions import Literal, ParamSpec + + P = ParamSpec("P") +# maintain runtime compatibility with older typing_extensions +else: + try: + from typing_extensions import ParamSpec + + P = ParamSpec("P") + except ImportError: + P = TypeVar("P") + +R = TypeVar("R") class Kind(IntFlag): @@ -179,8 +191,6 @@ def __init__( # below here part is unique to superqt (not from KD) -P = ParamSpec("P") -R = TypeVar("R") if TYPE_CHECKING: from typing_extensions import Protocol @@ -199,12 +209,12 @@ def set_timeout(self, timeout: int) -> None: if sys.version_info < (3, 9): - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Future: + def __call__(self, *args: "P.args", **kwargs: "P.kwargs") -> Future: ... else: - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Future[R]: + def __call__(self, *args: "P.args", **kwargs: "P.kwargs") -> Future[R]: ... @@ -220,7 +230,7 @@ def qthrottled( @overload def qthrottled( - func: Literal[None] = None, + func: "Literal[None]" = None, timeout: int = 100, leading: bool = True, timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer, @@ -279,7 +289,7 @@ def qdebounced( @overload def qdebounced( - func: Literal[None] = None, + func: "Literal[None]" = None, timeout: int = 100, leading: bool = False, timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer, @@ -347,7 +357,7 @@ def deco(func: Callable[P, R]) -> "ThrottledCallable[P, R]": future: Optional[Future] = None @wraps(func) - def inner(*args: P.args, **kwargs: P.kwargs) -> Future: + def inner(*args: "P.args", **kwargs: "P.kwargs") -> Future: nonlocal last_f nonlocal future if last_f is not None: diff --git a/tests/test_sliders/_testutil.py b/tests/test_sliders/_testutil.py index 150e3fd4..7b61497f 100644 --- a/tests/test_sliders/_testutil.py +++ b/tests/test_sliders/_testutil.py @@ -79,7 +79,7 @@ def _hover_event(_type, position, old_position, widget=None): return QHoverEvent(_type, position, old_position) -def _linspace(start, stop, n): +def _linspace(start: int, stop: int, n: int): h = (stop - start) / (n - 1) for i in range(n): yield start + h * i diff --git a/tests/test_sliders/test_float.py b/tests/test_sliders/test_float.py index 5e89d9c0..ad9979cf 100644 --- a/tests/test_sliders/test_float.py +++ b/tests/test_sliders/test_float.py @@ -1,7 +1,9 @@ +import math import os import pytest from qtpy import API_NAME +from qtpy.QtWidgets import QStyleOptionSlider from superqt import ( QDoubleRangeSlider, @@ -10,6 +12,8 @@ QLabeledDoubleSlider, ) +from ._testutil import _linspace + range_types = {QDoubleRangeSlider, QLabeledDoubleRangeSlider} @@ -122,3 +126,15 @@ def test_signals(ds, qtbot): with qtbot.waitSignal(ds.rangeChanged): ds.setRange(1.2, 3.3) + + +@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4))) +def test_slider_extremes(mag, qtbot): + sld = QDoubleSlider() + _mag = 10**mag + with qtbot.waitSignal(sld.rangeChanged): + sld.setRange(-_mag, _mag) + for i in _linspace(-_mag, _mag, 10): + sld.setValue(i) + assert math.isclose(sld.value(), i, rel_tol=1e-8) + sld.initStyleOption(QStyleOptionSlider()) diff --git a/tests/test_sliders/test_generic_slider.py b/tests/test_sliders/test_generic_slider.py index 56f02311..2acd9bf7 100644 --- a/tests/test_sliders/test_generic_slider.py +++ b/tests/test_sliders/test_generic_slider.py @@ -7,13 +7,7 @@ from superqt.sliders._generic_slider import _GenericSlider, _sliderValueFromPosition -from ._testutil import ( - _hover_event, - _linspace, - _mouse_event, - _wheel_event, - skip_on_linux_qt6, -) +from ._testutil import _hover_event, _mouse_event, _wheel_event, skip_on_linux_qt6 @pytest.fixture(params=[Qt.Orientation.Horizontal, Qt.Orientation.Vertical]) @@ -169,17 +163,6 @@ def test_steps(gslider: _GenericSlider, qtbot): 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", diff --git a/tests/test_sliders/test_labeled_slider.py b/tests/test_sliders/test_labeled_slider.py index 8e0dfc9a..5abf454a 100644 --- a/tests/test_sliders/test_labeled_slider.py +++ b/tests/test_sliders/test_labeled_slider.py @@ -1,4 +1,10 @@ -from superqt import QLabeledRangeSlider +import sys +from typing import Any, Iterable +from unittest.mock import Mock + +import pytest + +from superqt import QLabeledDoubleSlider, QLabeledRangeSlider, QLabeledSlider def test_labeled_slider_api(qtbot): @@ -9,3 +15,50 @@ def test_labeled_slider_api(qtbot): slider.setBarVisible() slider.setBarMovesAllHandles() slider.setBarIsRigid() + + +def test_slider_connect_works(qtbot): + slider = QLabeledSlider() + qtbot.addWidget(slider) + + slider._label.editingFinished.emit() + + +def _assert_types(args: Iterable[Any], type_: type): + # sourcery skip: comprehension-to-generator + if sys.version_info >= (3, 8): + assert all([isinstance(v, type_) for v in args]), "invalid type" + + +@pytest.mark.parametrize("cls", [QLabeledDoubleSlider, QLabeledSlider]) +def test_labeled_signals(cls, qtbot): + gslider = cls() + qtbot.addWidget(gslider) + + type_ = float if cls == QLabeledDoubleSlider else int + + mock = Mock() + gslider.valueChanged.connect(mock) + with qtbot.waitSignal(gslider.valueChanged): + gslider.setValue(10) + mock.assert_called_once_with(10) + _assert_types(mock.call_args.args, type_) + + mock = Mock() + gslider.rangeChanged.connect(mock) + with qtbot.waitSignal(gslider.rangeChanged): + gslider.setMinimum(3) + mock.assert_called_once_with(3, 99) + _assert_types(mock.call_args.args, type_) + + mock.reset_mock() + with qtbot.waitSignal(gslider.rangeChanged): + gslider.setMaximum(15) + mock.assert_called_once_with(3, 15) + _assert_types(mock.call_args.args, type_) + + mock.reset_mock() + with qtbot.waitSignal(gslider.rangeChanged): + gslider.setRange(1, 2) + mock.assert_called_once_with(1, 2) + _assert_types(mock.call_args.args, type_) diff --git a/tests/test_sliders/test_range_slider.py b/tests/test_sliders/test_range_slider.py index aa2a6b9b..49b8272d 100644 --- a/tests/test_sliders/test_range_slider.py +++ b/tests/test_sliders/test_range_slider.py @@ -1,10 +1,14 @@ import math +import sys +from itertools import product +from typing import Any, Iterable +from unittest.mock import Mock import pytest from qtpy.QtCore import QEvent, QPoint, QPointF, Qt from qtpy.QtWidgets import QStyle, QStyleOptionSlider -from superqt import QDoubleRangeSlider, QRangeSlider +from superqt import QDoubleRangeSlider, QLabeledRangeSlider, QRangeSlider from ._testutil import ( _hover_event, @@ -14,161 +18,240 @@ skip_on_linux_qt6, ) +ALL_SLIDER_COMBOS = list( + product( + [QDoubleRangeSlider, QRangeSlider, QLabeledRangeSlider], + [Qt.Orientation.Horizontal, Qt.Orientation.Vertical], + ) +) +FLOAT_SLIDERS = [c for c in ALL_SLIDER_COMBOS if c[0] == QDoubleRangeSlider] -@pytest.fixture(params=[Qt.Orientation.Horizontal, Qt.Orientation.Vertical]) -def gslider(qtbot, request): - slider = QDoubleRangeSlider(request.param) - qtbot.addWidget(slider) + +@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS) +def test_slider_init(qtbot, cls, orientation): + slider = cls(orientation) assert slider.value() == (20, 80) assert slider.minimum() == 0 assert slider.maximum() == 99 - yield slider - slider.initStyleOption(QStyleOptionSlider()) + slider.show() + qtbot.addWidget(slider) -def test_change_floatslider_range(gslider: QRangeSlider, qtbot): - with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]): - gslider.setMinimum(30) +@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS) +def test_change_floatslider_range(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) - assert gslider.value()[0] == 30 == gslider.minimum() - assert gslider.maximum() == 99 + with qtbot.waitSignals([sld.rangeChanged, sld.valueChanged]): + sld.setMinimum(30) - with qtbot.waitSignal(gslider.rangeChanged): - gslider.setMaximum(70) - assert gslider.value()[0] == 30 == gslider.minimum() - assert gslider.value()[1] == 70 == gslider.maximum() + assert sld.value()[0] == 30 == sld.minimum() + assert sld.maximum() == 99 - 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(sld.rangeChanged): + sld.setMaximum(70) + assert sld.value()[0] == 30 == sld.minimum() + assert sld.value()[1] == 70 == sld.maximum() - with qtbot.waitSignal(gslider.valueChanged): - gslider.setValue([40, 50]) - assert gslider.value()[0] == 40 == gslider.minimum() - assert gslider.value()[1] == 50 + with qtbot.waitSignals([sld.rangeChanged, sld.valueChanged]): + sld.setRange(40, 60) + assert sld.value()[0] == 40 == sld.minimum() + assert sld.maximum() == 60 - with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]): - gslider.setMaximum(45) - assert gslider.value()[0] == 40 == gslider.minimum() - assert gslider.value()[1] == 45 == gslider.maximum() + with qtbot.waitSignal(sld.valueChanged): + sld.setValue([40, 50]) + assert sld.value()[0] == 40 == sld.minimum() + assert sld.value()[1] == 50 + with qtbot.waitSignals([sld.rangeChanged, sld.valueChanged]): + sld.setMaximum(45) + assert sld.value()[0] == 40 == sld.minimum() + assert sld.value()[1] == 45 == sld.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) +@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS) +def test_float_values(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) - 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() + with qtbot.waitSignal(sld.rangeChanged): + sld.setRange(0.1, 0.9) + assert sld.minimum() == 0.1 + assert sld.maximum() == 0.9 + with qtbot.waitSignal(sld.valueChanged): + sld.setValue([0.4, 0.6]) + assert sld.value() == (0.4, 0.6) -def test_position(gslider: QRangeSlider, qtbot): - gslider.setSliderPosition([10, 80]) - assert gslider.sliderPosition() == (10, 80) + with qtbot.waitSignal(sld.valueChanged): + sld.setValue([0, 1.9]) + assert sld.value()[0] == 0.1 == sld.minimum() + assert sld.value()[1] == 0.9 == sld.maximum() -def test_steps(gslider: QRangeSlider, qtbot): - gslider.setSingleStep(0.1) - assert gslider.singleStep() == 0.1 +@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS) +def test_position(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) - gslider.setSingleStep(1.5e20) - assert gslider.singleStep() == 1.5e20 + sld.setSliderPosition([10, 80]) + assert sld.sliderPosition() == (10, 80) - gslider.setPageStep(0.2) - assert gslider.pageStep() == 0.2 - gslider.setPageStep(1.5e30) - assert gslider.pageStep() == 1.5e30 +@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS) +def test_steps(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) + + 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(gslider: QRangeSlider, mag, qtbot): +@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS) +def test_slider_extremes(cls, orientation, qtbot, mag): + sld = cls(orientation) + qtbot.addWidget(sld) + _mag = 10**mag - with qtbot.waitSignal(gslider.rangeChanged): - gslider.setRange(-_mag, _mag) + with qtbot.waitSignal(sld.rangeChanged): + sld.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()) + sld.setValue((i, _mag)) + assert math.isclose(sld.value()[0], i, rel_tol=0.0001) + sld.initStyleOption(QStyleOptionSlider()) -def test_ticks(gslider: QRangeSlider, qtbot): - gslider.setTickInterval(0.3) - assert gslider.tickInterval() == 0.3 - gslider.setTickPosition(gslider.TickPosition.TicksAbove) - gslider.show() +@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS) +def test_ticks(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) + sld.setTickInterval(0.3) + assert sld.tickInterval() == 0.3 + sld.setTickPosition(sld.TickPosition.TicksAbove) + sld.show() -def test_show(gslider, qtbot): - gslider.show() +@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS) +def test_press_move_release(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) -def test_press_move_release(gslider: QRangeSlider, qtbot): # this fail on vertical came with pyside6.2 ... need to debug # still works in practice, but test fails to catch signals - if gslider.orientation() == Qt.Orientation.Vertical: + if sld.orientation() == Qt.Orientation.Vertical: pytest.xfail() - assert gslider._pressedControl == QStyle.SubControl.SC_None + assert sld._pressedControl == QStyle.SubControl.SC_None opt = QStyleOptionSlider() - gslider.initStyleOption(opt) - style = gslider.style() + sld.initStyleOption(opt) + style = sld.style() hrect = style.subControlRect( QStyle.ComplexControl.CC_Slider, opt, QStyle.SubControl.SC_SliderHandle ) - handle_pos = gslider.mapToGlobal(hrect.center()) + handle_pos = sld.mapToGlobal(hrect.center()) - with qtbot.waitSignal(gslider.sliderPressed): - qtbot.mousePress(gslider, Qt.MouseButton.LeftButton, pos=handle_pos) + with qtbot.waitSignal(sld.sliderPressed): + qtbot.mousePress(sld, Qt.MouseButton.LeftButton, pos=handle_pos) - assert gslider._pressedControl == QStyle.SubControl.SC_SliderHandle + assert sld._pressedControl == QStyle.SubControl.SC_SliderHandle - with qtbot.waitSignals([gslider.sliderMoved, gslider.valueChanged]): + with qtbot.waitSignals([sld.sliderMoved, sld.valueChanged]): shift = ( QPoint(0, -8) - if gslider.orientation() == Qt.Orientation.Vertical + if sld.orientation() == Qt.Orientation.Vertical else QPoint(8, 0) ) - gslider.mouseMoveEvent(_mouse_event(handle_pos + shift)) + sld.mouseMoveEvent(_mouse_event(handle_pos + shift)) - with qtbot.waitSignal(gslider.sliderReleased): - qtbot.mouseRelease(gslider, Qt.MouseButton.LeftButton, pos=handle_pos) + with qtbot.waitSignal(sld.sliderReleased): + qtbot.mouseRelease(sld, Qt.MouseButton.LeftButton, pos=handle_pos) - assert gslider._pressedControl == QStyle.SubControl.SC_None + assert sld._pressedControl == QStyle.SubControl.SC_None - gslider.show() - with qtbot.waitSignal(gslider.sliderPressed): - qtbot.mousePress(gslider, Qt.MouseButton.LeftButton, pos=handle_pos) + sld.show() + with qtbot.waitSignal(sld.sliderPressed): + qtbot.mousePress(sld, Qt.MouseButton.LeftButton, pos=handle_pos) @skip_on_linux_qt6 -def test_hover(gslider: QRangeSlider): +@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS) +def test_hover(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) - hrect = gslider._handleRect(0) - handle_pos = QPointF(gslider.mapToGlobal(hrect.center())) + hrect = sld._handleRect(0) + handle_pos = QPointF(sld.mapToGlobal(hrect.center())) - assert gslider._hoverControl == QStyle.SubControl.SC_None + assert sld._hoverControl == QStyle.SubControl.SC_None - gslider.event(_hover_event(QEvent.Type.HoverEnter, handle_pos, QPointF(), gslider)) - assert gslider._hoverControl == QStyle.SubControl.SC_SliderHandle + sld.event(_hover_event(QEvent.Type.HoverEnter, handle_pos, QPointF(), sld)) + assert sld._hoverControl == QStyle.SubControl.SC_SliderHandle - gslider.event( - _hover_event(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos, gslider) + sld.event( + _hover_event(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos, sld) ) - assert gslider._hoverControl == QStyle.SubControl.SC_None + assert sld._hoverControl == QStyle.SubControl.SC_None + + +@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS) +def test_wheel(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) + + with qtbot.waitSignal(sld.valueChanged): + sld.wheelEvent(_wheel_event(120)) + + sld.wheelEvent(_wheel_event(0)) + + +def _assert_types(args: Iterable[Any], type_: type): + # sourcery skip: comprehension-to-generator + if sys.version_info >= (3, 8): + assert all([isinstance(v, type_) for v in args]), "invalid type" + + +@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS) +def test_rangeslider_signals(cls, orientation, qtbot): + sld = cls(orientation) + qtbot.addWidget(sld) + + type_ = float if cls == QDoubleRangeSlider else int + + mock = Mock() + sld.valueChanged.connect(mock) + with qtbot.waitSignal(sld.valueChanged): + sld.setValue((20, 40)) + mock.assert_called_once_with((20, 40)) + _assert_types(mock.call_args.args, tuple) + _assert_types(mock.call_args.args[0], type_) + mock = Mock() + sld.rangeChanged.connect(mock) + with qtbot.waitSignal(sld.rangeChanged): + sld.setMinimum(3) + mock.assert_called_once_with(3, 99) + _assert_types(mock.call_args.args, type_) -def test_wheel(gslider: QRangeSlider, qtbot): - with qtbot.waitSignal(gslider.valueChanged): - gslider.wheelEvent(_wheel_event(120)) + mock.reset_mock() + with qtbot.waitSignal(sld.rangeChanged): + sld.setMaximum(15) + mock.assert_called_once_with(3, 15) + _assert_types(mock.call_args.args, type_) - gslider.wheelEvent(_wheel_event(0)) + mock.reset_mock() + with qtbot.waitSignal(sld.rangeChanged): + sld.setRange(1, 2) + mock.assert_called_once_with(1, 2) + _assert_types(mock.call_args.args, type_)