mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 12:11:07 +02:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8525efd98c | ||
|
f676d7e171 | ||
|
599dff7d02 | ||
|
ed960f4994 | ||
|
7fcba7a485 | ||
|
619daae13f | ||
|
462eeada93 | ||
|
8457563f49 |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,8 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased](https://github.com/pyapp-kit/superqt/tree/HEAD)
|
||||
## [v0.5.4](https://github.com/pyapp-kit/superqt/tree/v0.5.4) (2023-08-31)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.5.0...HEAD)
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.5.3...v0.5.4)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix mysterious segfault [\#192](https://github.com/pyapp-kit/superqt/pull/192) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.5.3](https://github.com/pyapp-kit/superqt/tree/v0.5.3) (2023-08-21)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.5.2...v0.5.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: add error `exceptions_as_dialog` context manager to catch and show Exceptions [\#191](https://github.com/pyapp-kit/superqt/pull/191) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: remove dupes/aliases in QEnumCombo [\#190](https://github.com/pyapp-kit/superqt/pull/190) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.5.2](https://github.com/pyapp-kit/superqt/tree/v0.5.2) (2023-08-18)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.5.1...v0.5.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: allow throttler/debouncer as method decorator [\#188](https://github.com/pyapp-kit/superqt/pull/188) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: Add descriptive exception when fail to add instance to weakref dictionary [\#189](https://github.com/pyapp-kit/superqt/pull/189) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
## [v0.5.1](https://github.com/pyapp-kit/superqt/tree/v0.5.1) (2023-08-17)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.5.0...v0.5.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
|
3
docs/utilities/error_dialog_contexts.md
Normal file
3
docs/utilities/error_dialog_contexts.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Error message context manager
|
||||
|
||||
::: superqt.utils.exceptions_as_dialog
|
@@ -25,6 +25,7 @@ theme:
|
||||
# - navigation.tabs
|
||||
- search.highlight
|
||||
- search.suggest
|
||||
- content.code.copy
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
|
@@ -49,7 +49,9 @@ class QEnumComboBox(QComboBox):
|
||||
self._allow_none = allow_none and enum is not None
|
||||
if allow_none:
|
||||
super().addItem(NONE_STRING)
|
||||
super().addItems(list(map(_get_name, self._enum_class.__members__.values())))
|
||||
names = map(_get_name, self._enum_class.__members__.values())
|
||||
_names = dict.fromkeys(names) # remove duplicates/aliases, keep order
|
||||
super().addItems(list(_names))
|
||||
|
||||
def enumClass(self) -> Optional[EnumMeta]:
|
||||
"""Return current Enum class."""
|
||||
|
@@ -135,8 +135,8 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
fp = self.style().styleHint(QStyle.StyleHint.SH_Button_FocusPolicy)
|
||||
self.setFocusPolicy(Qt.FocusPolicy(fp))
|
||||
|
||||
self._slider = self._slider_class()
|
||||
self._label = SliderLabel(self._slider, connect=self._setValue)
|
||||
self._slider = self._slider_class(parent=self)
|
||||
self._label = SliderLabel(self._slider, connect=self._setValue, parent=self)
|
||||
self._edge_label_mode: EdgeLabelMode = EdgeLabelMode.LabelIsValue
|
||||
|
||||
self._rename_signals()
|
||||
@@ -145,12 +145,15 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
self._slider.sliderMoved.connect(self.sliderMoved.emit)
|
||||
self._slider.sliderPressed.connect(self.sliderPressed.emit)
|
||||
self._slider.sliderReleased.connect(self.sliderReleased.emit)
|
||||
self._slider.valueChanged.connect(self._label.setValue)
|
||||
self._slider.valueChanged.connect(self.valueChanged.emit)
|
||||
self._slider.valueChanged.connect(self._on_slider_value_changed)
|
||||
self._label.editingFinished.connect(self.editingFinished)
|
||||
|
||||
self.setOrientation(orientation)
|
||||
|
||||
def _on_slider_value_changed(self, v):
|
||||
self._label.setValue(v)
|
||||
self.valueChanged.emit(v)
|
||||
|
||||
def _setValue(self, value: float):
|
||||
"""Convert the value from float to int before setting the slider value."""
|
||||
self._slider.setValue(int(value))
|
||||
|
@@ -14,10 +14,12 @@ __all__ = (
|
||||
"signals_blocked",
|
||||
"thread_worker",
|
||||
"WorkerBase",
|
||||
"exceptions_as_dialog",
|
||||
)
|
||||
|
||||
from ._code_syntax_highlight import CodeSyntaxHighlight
|
||||
from ._ensure_thread import ensure_main_thread, ensure_object_thread
|
||||
from ._errormsg_context import exceptions_as_dialog
|
||||
from ._message_handler import QMessageHandler
|
||||
from ._misc import signals_blocked
|
||||
from ._qthreading import (
|
||||
|
165
src/superqt/utils/_errormsg_context.py
Normal file
165
src/superqt/utils/_errormsg_context.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QErrorMessage, QMessageBox, QWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
|
||||
_DEFAULT_FLAGS = Qt.WindowType.Dialog | Qt.WindowType.MSWindowsFixedSizeDialogHint
|
||||
|
||||
|
||||
class exceptions_as_dialog(AbstractContextManager):
|
||||
"""Context manager that shows a dialog when an exception is raised.
|
||||
|
||||
See examples below for common usage patterns.
|
||||
|
||||
To determine whether an exception was raised or not, check the `exception`
|
||||
attribute after the context manager has exited. If `use_error_message` is `False`
|
||||
(the default), you can also access the `dialog` attribute to get/manipulate the
|
||||
`QMessageBox` instance.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exceptions : type[BaseException] | tuple[type[BaseException], ...], optional
|
||||
The exception(s) to catch, by default `Exception` (i.e. all exceptions).
|
||||
icon : QMessageBox.Icon, optional
|
||||
The icon to show in the QMessageBox, by default `QMessageBox.Icon.Critical`
|
||||
title : str, optional
|
||||
The title of the `QMessageBox`, by default `"An error occurred"`.
|
||||
msg_template : str, optional
|
||||
The message to show in the `QMessageBox`. The message will be formatted
|
||||
using three variables:
|
||||
|
||||
- `exc_value`: the exception instance
|
||||
- `exc_type`: the exception type
|
||||
- `tb`: the traceback as a string
|
||||
|
||||
The default template is the content of the exception: `"{exc_value}"`
|
||||
buttons : QMessageBox.StandardButton, optional
|
||||
The buttons to show in the `QMessageBox`, by default
|
||||
`QMessageBox.StandardButton.Ok`
|
||||
parent : QWidget | None, optional
|
||||
The parent widget of the `QMessageBox`, by default `None`
|
||||
use_error_message : bool | QErrorMessage, optional
|
||||
Whether to use a `QErrorMessage` instead of a `QMessageBox`. By default
|
||||
`False`. `QErrorMessage` shows a checkbox that the user can check to
|
||||
prevent seeing the message again (based on the text of the formatted
|
||||
`msg_template`.) If `True`, the global `QMessageError.qtHandler()`
|
||||
instance is used to maintain a history of dismissed messages. You may also pass
|
||||
a `QErrorMessage` instance to use a specific instance. If `use_error_message` is
|
||||
True, or if you pass your own `QErrorMessage` instance, the `parent` argument
|
||||
is ignored.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dialog : QMessageBox | None
|
||||
The `QMessageBox` instance that was created (if `use_error_message` was
|
||||
`False`). This can be used, among other things, to determine the result of
|
||||
the dialog (e.g. `dialog.result()`) or to manipulate the dialog (e.g.
|
||||
`dialog.setDetailedText("some text")`).
|
||||
exception : BaseException | None
|
||||
Will hold the exception instance if an exception was raised and caught.
|
||||
|
||||
Examplez
|
||||
-------
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from superqt.utils import exceptions_as_dialog
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
with exceptions_as_dialog() as ctx:
|
||||
raise Exception("This will be caught and shown in a QMessageBox")
|
||||
|
||||
# you can access the exception instance here
|
||||
assert ctx.exception is not None
|
||||
|
||||
# with exceptions_as_dialog(ValueError):
|
||||
# 1 / 0 # ZeroDivisionError is not caught, so this will raise
|
||||
|
||||
with exceptions_as_dialog(msg_template="Error: {exc_value}"):
|
||||
raise Exception("This message will be inserted at 'exc_value'")
|
||||
|
||||
for _i in range(3):
|
||||
with exceptions_as_dialog(AssertionError, use_error_message=True):
|
||||
assert False, "Uncheck the checkbox to ignore this in the future"
|
||||
|
||||
# use ctx.dialog to get the result of the dialog
|
||||
btns = QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
|
||||
with exceptions_as_dialog(buttons=btns) as ctx:
|
||||
raise Exception("This will be caught and shown in a QMessageBox")
|
||||
print(ctx.dialog.result()) # prints which button was clicked
|
||||
|
||||
app.exec() # needed only for the use_error_message example to show
|
||||
```
|
||||
"""
|
||||
|
||||
dialog: QMessageBox | None
|
||||
exception: BaseException | None
|
||||
exec_result: int | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exceptions: type[BaseException] | tuple[type[BaseException], ...] = Exception,
|
||||
icon: QMessageBox.Icon = QMessageBox.Icon.Critical,
|
||||
title: str = "An error occurred",
|
||||
msg_template: str = "{exc_value}",
|
||||
buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok,
|
||||
parent: QWidget | None = None,
|
||||
flags: Qt.WindowType = _DEFAULT_FLAGS,
|
||||
use_error_message: bool | QErrorMessage = False,
|
||||
):
|
||||
self.exceptions = exceptions
|
||||
self.msg_template = msg_template
|
||||
self.exception = None
|
||||
self.dialog = None
|
||||
|
||||
self._err_msg = use_error_message
|
||||
|
||||
if not use_error_message:
|
||||
# the message will be overwritten in __exit__
|
||||
self.dialog = QMessageBox(
|
||||
icon, title, "An error occurred", buttons, parent, flags
|
||||
)
|
||||
|
||||
def __enter__(self) -> exceptions_as_dialog:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
tb: TracebackType | None,
|
||||
) -> bool:
|
||||
if not (exc_value is not None and isinstance(exc_value, self.exceptions)):
|
||||
return False # let it propagate
|
||||
|
||||
# save the exception for later
|
||||
self.exception = exc_value
|
||||
|
||||
# format the message using the context variables
|
||||
if "{tb}" in self.msg_template:
|
||||
_tb = "\n".join(traceback.format_exception(exc_type, exc_value, tb))
|
||||
else:
|
||||
_tb = ""
|
||||
text = self.msg_template.format(exc_value=exc_value, exc_type=exc_type, tb=_tb)
|
||||
|
||||
# show the dialog
|
||||
if self._err_msg:
|
||||
msg = (
|
||||
self._err_msg
|
||||
if isinstance(self._err_msg, QErrorMessage)
|
||||
else QErrorMessage.qtHandler()
|
||||
)
|
||||
cast("QErrorMessage", msg).showMessage(text)
|
||||
elif self.dialog is not None: # it won't be if use_error_message=False
|
||||
self.dialog.setText(text)
|
||||
self.dialog.exec()
|
||||
|
||||
return True # swallow the exception
|
@@ -32,6 +32,7 @@ from concurrent.futures import Future
|
||||
from enum import IntFlag, auto
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, Generic, TypeVar, overload
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from qtpy.QtCore import QObject, Qt, QTimer, Signal
|
||||
|
||||
@@ -202,18 +203,26 @@ class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
|
||||
super().__init__(kind, emissionPolicy, parent)
|
||||
|
||||
self._future: Future[R] = Future()
|
||||
if isinstance(func, staticmethod):
|
||||
self._func = func.__func__
|
||||
else:
|
||||
self._func = func
|
||||
|
||||
self.__wrapped__ = func
|
||||
|
||||
self._args: tuple = ()
|
||||
self._kwargs: dict = {}
|
||||
self.triggered.connect(self._set_future_result)
|
||||
self._name = None
|
||||
|
||||
self._obj_dkt = WeakKeyDictionary()
|
||||
|
||||
# even if we were to compile __call__ with a signature matching that of func,
|
||||
# PySide wouldn't correctly inspect the signature of the ThrottledCallable
|
||||
# instance: https://bugreports.qt.io/browse/PYSIDE-2423
|
||||
# so we do it ourselfs and limit the number of positional arguments
|
||||
# that we pass to func
|
||||
self._max_args: int | None = get_max_args(func)
|
||||
self._max_args: int | None = get_max_args(self._func)
|
||||
|
||||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "Future[R]": # noqa
|
||||
if not self._future.done():
|
||||
@@ -227,9 +236,53 @@ class ThrottledCallable(GenericSignalThrottler, Generic[P, R]):
|
||||
return self._future
|
||||
|
||||
def _set_future_result(self):
|
||||
result = self.__wrapped__(*self._args[: self._max_args], **self._kwargs)
|
||||
result = self._func(*self._args[: self._max_args], **self._kwargs)
|
||||
self._future.set_result(result)
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if not isinstance(self.__wrapped__, staticmethod):
|
||||
self._name = name
|
||||
|
||||
def _get_throttler(self, instance, owner, parent, obj):
|
||||
throttler = ThrottledCallable(
|
||||
self.__wrapped__.__get__(instance, owner),
|
||||
self._kind,
|
||||
self._emissionPolicy,
|
||||
parent=parent,
|
||||
)
|
||||
throttler.setTimerType(self.timerType())
|
||||
throttler.setTimeout(self.timeout())
|
||||
try:
|
||||
setattr(
|
||||
obj,
|
||||
self._name,
|
||||
throttler,
|
||||
)
|
||||
except AttributeError:
|
||||
try:
|
||||
self._obj_dkt[obj] = throttler
|
||||
except TypeError as e:
|
||||
raise TypeError(
|
||||
"To use qthrottled or qdebounced as a method decorator, "
|
||||
"objects must have `__dict__` or be weak referenceable. "
|
||||
"Please either add `__weakref__` to `__slots__` or use"
|
||||
"qthrottled/qdebounced as a function (not a decorator)."
|
||||
) from e
|
||||
return throttler
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None or not self._name:
|
||||
return self
|
||||
|
||||
if instance in self._obj_dkt:
|
||||
return self._obj_dkt[instance]
|
||||
|
||||
parent = self.parent()
|
||||
if parent is None and isinstance(instance, QObject):
|
||||
parent = instance
|
||||
|
||||
return self._get_throttler(instance, owner, parent, instance)
|
||||
|
||||
|
||||
@overload
|
||||
def qthrottled(
|
||||
@@ -237,6 +290,7 @@ def qthrottled(
|
||||
timeout: int = 100,
|
||||
leading: bool = True,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R]:
|
||||
...
|
||||
|
||||
@@ -247,6 +301,7 @@ def qthrottled(
|
||||
timeout: int = 100,
|
||||
leading: bool = True,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
...
|
||||
|
||||
@@ -256,6 +311,7 @@ def qthrottled(
|
||||
timeout: int = 100,
|
||||
leading: bool = True,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R] | Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
"""Creates a throttled function that invokes func at most once per timeout.
|
||||
|
||||
@@ -284,8 +340,11 @@ def qthrottled(
|
||||
- `Qt.CoarseTimer`: Coarse timers try to keep accuracy within 5% of the
|
||||
desired interval
|
||||
- `Qt.VeryCoarseTimer`: Very coarse timers only keep full second accuracy
|
||||
parent: QObject or None
|
||||
Parent object for timer. If using qthrottled as function it may be usefull
|
||||
for cleaning data
|
||||
"""
|
||||
return _make_decorator(func, timeout, leading, timer_type, Kind.Throttler)
|
||||
return _make_decorator(func, timeout, leading, timer_type, Kind.Throttler, parent)
|
||||
|
||||
|
||||
@overload
|
||||
@@ -294,6 +353,7 @@ def qdebounced(
|
||||
timeout: int = 100,
|
||||
leading: bool = False,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R]:
|
||||
...
|
||||
|
||||
@@ -304,6 +364,7 @@ def qdebounced(
|
||||
timeout: int = 100,
|
||||
leading: bool = False,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
...
|
||||
|
||||
@@ -313,6 +374,7 @@ def qdebounced(
|
||||
timeout: int = 100,
|
||||
leading: bool = False,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R] | Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
"""Creates a debounced function that delays invoking `func`.
|
||||
|
||||
@@ -344,8 +406,11 @@ def qdebounced(
|
||||
- `Qt.CoarseTimer`: Coarse timers try to keep accuracy within 5% of the
|
||||
desired interval
|
||||
- `Qt.VeryCoarseTimer`: Very coarse timers only keep full second accuracy
|
||||
parent: QObject or None
|
||||
Parent object for timer. If using qthrottled as function it may be usefull
|
||||
for cleaning data
|
||||
"""
|
||||
return _make_decorator(func, timeout, leading, timer_type, Kind.Debouncer)
|
||||
return _make_decorator(func, timeout, leading, timer_type, Kind.Debouncer, parent)
|
||||
|
||||
|
||||
def _make_decorator(
|
||||
@@ -354,10 +419,16 @@ def _make_decorator(
|
||||
leading: bool,
|
||||
timer_type: Qt.TimerType,
|
||||
kind: Kind,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R] | Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
def deco(func: Callable[P, R]) -> ThrottledCallable[P, R]:
|
||||
nonlocal parent
|
||||
|
||||
instance: object | None = getattr(func, "__self__", None)
|
||||
if isinstance(instance, QObject) and parent is None:
|
||||
parent = instance
|
||||
policy = EmissionPolicy.Leading if leading else EmissionPolicy.Trailing
|
||||
obj = ThrottledCallable(func, kind, policy)
|
||||
obj = ThrottledCallable(func, kind, policy, parent=parent)
|
||||
obj.setTimerType(timer_type)
|
||||
obj.setTimeout(timeout)
|
||||
return wraps(func)(obj)
|
||||
|
@@ -11,6 +11,8 @@ class Enum1(Enum):
|
||||
b = 2
|
||||
c = 3
|
||||
|
||||
ALIAS = a
|
||||
|
||||
|
||||
class Enum2(Enum):
|
||||
d = 1
|
||||
|
@@ -4,6 +4,7 @@ import pytest
|
||||
from qtpy.QtCore import QObject, Signal
|
||||
|
||||
from superqt.utils import qdebounced, qthrottled
|
||||
from superqt.utils._throttler import ThrottledCallable
|
||||
|
||||
|
||||
def test_debounced(qtbot):
|
||||
@@ -26,6 +27,101 @@ def test_debounced(qtbot):
|
||||
assert mock2.call_count == 10
|
||||
|
||||
|
||||
def test_debouncer_method(qtbot):
|
||||
class A(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.count = 0
|
||||
|
||||
def callback(self):
|
||||
self.count += 1
|
||||
|
||||
a = A()
|
||||
assert all(not isinstance(x, ThrottledCallable) for x in a.children())
|
||||
b = qdebounced(a.callback, timeout=4)
|
||||
assert any(isinstance(x, ThrottledCallable) for x in a.children())
|
||||
for _ in range(10):
|
||||
b()
|
||||
|
||||
qtbot.wait(5)
|
||||
|
||||
assert a.count == 1
|
||||
|
||||
|
||||
def test_debouncer_method_definition(qtbot):
|
||||
mock1 = Mock()
|
||||
mock2 = Mock()
|
||||
|
||||
class A(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.count = 0
|
||||
|
||||
@qdebounced(timeout=4)
|
||||
def callback(self):
|
||||
self.count += 1
|
||||
|
||||
@qdebounced(timeout=4)
|
||||
@staticmethod
|
||||
def call1():
|
||||
mock1()
|
||||
|
||||
@staticmethod
|
||||
@qdebounced(timeout=4)
|
||||
def call2():
|
||||
mock2()
|
||||
|
||||
a = A()
|
||||
assert all(not isinstance(x, ThrottledCallable) for x in a.children())
|
||||
for _ in range(10):
|
||||
a.callback(1)
|
||||
A.call1(34)
|
||||
a.call1(22)
|
||||
a.call2(22)
|
||||
A.call2(32)
|
||||
|
||||
qtbot.wait(5)
|
||||
|
||||
assert a.count == 1
|
||||
mock1.assert_called_once()
|
||||
mock2.assert_called_once()
|
||||
|
||||
|
||||
def test_class_with_slots(qtbot):
|
||||
class A:
|
||||
__slots__ = ("count", "__weakref__")
|
||||
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
@qdebounced(timeout=4)
|
||||
def callback(self):
|
||||
self.count += 1
|
||||
|
||||
a = A()
|
||||
for _ in range(10):
|
||||
a.callback()
|
||||
|
||||
qtbot.wait(5)
|
||||
assert a.count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("qapp")
|
||||
def test_class_with_slots_except():
|
||||
class A:
|
||||
__slots__ = ("count",)
|
||||
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
@qdebounced(timeout=4)
|
||||
def callback(self):
|
||||
self.count += 1
|
||||
|
||||
with pytest.raises(TypeError, match="To use qthrottled or qdebounced"):
|
||||
A().callback()
|
||||
|
||||
|
||||
def test_throttled(qtbot):
|
||||
mock1 = Mock()
|
||||
mock2 = Mock()
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
from qtpy.QtCore import QObject, Signal
|
||||
import pytest
|
||||
import qtpy
|
||||
from qtpy.QtCore import QObject, QTimer, Signal
|
||||
from qtpy.QtWidgets import QApplication, QErrorMessage, QMessageBox
|
||||
|
||||
from superqt.utils import signals_blocked
|
||||
from superqt.utils import exceptions_as_dialog, signals_blocked
|
||||
from superqt.utils._util import get_max_args
|
||||
|
||||
|
||||
@@ -91,3 +96,46 @@ def test_get_max_args_methods():
|
||||
assert get_max_args(A().fun1) == 0
|
||||
assert get_max_args(A().fun2) == 1
|
||||
assert get_max_args(A()) == 2
|
||||
|
||||
|
||||
MAC_CI_PYSIDE6 = bool(
|
||||
sys.platform == "darwin" and os.getenv("CI") and qtpy.API_NAME == "PySide6"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(MAC_CI_PYSIDE6, reason="still hangs on mac ci with pyside6")
|
||||
def test_exception_context(qtbot, qapp: QApplication) -> None:
|
||||
def accept():
|
||||
for wdg in qapp.topLevelWidgets():
|
||||
if isinstance(wdg, QMessageBox):
|
||||
wdg.button(QMessageBox.StandardButton.Ok).click()
|
||||
|
||||
with exceptions_as_dialog():
|
||||
QTimer.singleShot(0, accept)
|
||||
raise Exception("This will be caught and shown in a QMessageBox")
|
||||
|
||||
with pytest.raises(ZeroDivisionError), exceptions_as_dialog(ValueError):
|
||||
1 / 0 # noqa
|
||||
|
||||
with exceptions_as_dialog(msg_template="Error: {exc_value}"):
|
||||
QTimer.singleShot(0, accept)
|
||||
raise Exception("This message will be used as 'exc_value'")
|
||||
|
||||
err = QErrorMessage()
|
||||
with exceptions_as_dialog(use_error_message=err):
|
||||
QTimer.singleShot(0, err.accept)
|
||||
raise AssertionError("Uncheck the checkbox to ignore this in the future")
|
||||
|
||||
# tb formatting smoke test, and return value checking
|
||||
exc = ValueError("Bad Val")
|
||||
with exceptions_as_dialog(
|
||||
msg_template="{tb}",
|
||||
buttons=QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
||||
) as ctx:
|
||||
qtbot.addWidget(ctx.dialog)
|
||||
QTimer.singleShot(100, accept)
|
||||
raise exc
|
||||
|
||||
assert isinstance(ctx.dialog, QMessageBox)
|
||||
assert ctx.dialog.result() == QMessageBox.StandardButton.Ok
|
||||
assert ctx.exception is exc
|
||||
|
Reference in New Issue
Block a user