mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-08-14 22:40:14 +02:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5ab72a0c48 | ||
|
06da62811b | ||
|
bb538cda2a | ||
|
c8a40ba051 |
8
.github_changelog_generator
Normal file
8
.github_changelog_generator
Normal file
@@ -0,0 +1,8 @@
|
||||
# run this with:
|
||||
# export CHANGELOG_GITHUB_TOKEN=......
|
||||
# github_changelog_generator --future-release vX.Y.Z
|
||||
user=napari
|
||||
project=superqt
|
||||
issues=false
|
||||
since-tag=v0.2.0
|
||||
add-sections={"documentation":{"prefix":"**Documentation updates:**","labels":["documentation"]}}
|
45
CHANGELOG.md
Normal file
45
CHANGELOG.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
## [v0.2.4](https://github.com/napari/superqt/tree/v0.2.4) (2021-09-13)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add type stubs for ensure\_thread decorator [\#23](https://github.com/napari/superqt/pull/23) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add `ensure_main_tread` and `ensure_object_thread` [\#22](https://github.com/napari/superqt/pull/22) ([Czaki](https://github.com/Czaki))
|
||||
- Add QMessageHandler context manager [\#21](https://github.com/napari/superqt/pull/21) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.3](https://github.com/napari/superqt/tree/v0.2.3) (2021-08-25)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.2...v0.2.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix warnings on eliding label for 5.12, test more qt versions [\#19](https://github.com/napari/superqt/pull/19) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.2](https://github.com/napari/superqt/tree/v0.2.2) (2021-08-17)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.1...v0.2.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add QElidingLabel [\#16](https://github.com/napari/superqt/pull/16) ([tlambert03](https://github.com/tlambert03))
|
||||
- Enum ComboBox implementation [\#13](https://github.com/napari/superqt/pull/13) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Documentation updates:**
|
||||
|
||||
- fix broken link [\#18](https://github.com/napari/superqt/pull/18) ([haesleinhuepf](https://github.com/haesleinhuepf))
|
||||
|
||||
## [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.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))
|
||||
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
@@ -1,5 +1,7 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
|
||||
include superqt/py.typed
|
||||
recursive-include superqt *.py
|
||||
recursive-include superqt *.pyi
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
86
docs/decorators.md
Normal file
86
docs/decorators.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Decorators
|
||||
|
||||
## Move to thread decorators
|
||||
|
||||
`superqt` provides two decorators that help to ensure that given function is
|
||||
running in the desired thread:
|
||||
|
||||
* `ensure_main_thread` - ensures that the decorated function/method runs in the main thread
|
||||
* `ensure_object_thread` - ensures that a decorated bound method of a `QObject` runs in the
|
||||
thread in which the instance lives ([qt
|
||||
documentation](https://doc.qt.io/qt-5/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
|
||||
|
||||
By default, functions are executed asynchronously (they return immediately with
|
||||
an instance of
|
||||
[`concurrent.futures.Future`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future)).
|
||||
To block and wait for the result, see [Synchronous mode](#synchronous-mode)
|
||||
|
||||
```python
|
||||
from superqt.qtcompat.QtCore import QObject
|
||||
from superqt import ensure_main_thread, ensure_object_thread
|
||||
|
||||
@ensure_main_thread
|
||||
def sample_function():
|
||||
print("This function will run in main thread")
|
||||
|
||||
|
||||
class SampleObject(QObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._value = 1
|
||||
|
||||
@ensure_main_thread
|
||||
def sample_method1(self):
|
||||
print("This method will run in main thread")
|
||||
|
||||
@ensure_object_thread
|
||||
def sample_method3(self):
|
||||
import time
|
||||
print("sleeping")
|
||||
time.sleep(1)
|
||||
print("This method will run in object thread")
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
print("return value")
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
@ensure_object_thread
|
||||
def value(self, value):
|
||||
print("this setter will run in object thread")
|
||||
self._value = value
|
||||
```
|
||||
|
||||
As can be seen in this example these decorators can also be used for setters.
|
||||
|
||||
These decorators should not be used as replacement of Qt Signals but rather to
|
||||
interact with Qt objects from non Qt code.
|
||||
|
||||
### Synchronous mode
|
||||
|
||||
If you'd like for the program to block and wait for the result of your function
|
||||
call, use the `await_return=True` parameter, and optionally specify a timeout.
|
||||
|
||||
> *Note: Using synchronous mode may significantly impact performance.*
|
||||
|
||||
```python
|
||||
from superqt import ensure_main_thread
|
||||
|
||||
@ensure_main_thread
|
||||
def sample_function1():
|
||||
return 1
|
||||
|
||||
@ensure_main_thread(await_return=True)
|
||||
def sample_function2():
|
||||
return 2
|
||||
|
||||
assert sample_function1() is None
|
||||
assert sample_function2() == 2
|
||||
|
||||
# optionally, specify a timeout
|
||||
@ensure_main_thread(await_return=True, timeout=10000)
|
||||
def sample_function():
|
||||
return 1
|
||||
|
||||
```
|
@@ -66,6 +66,9 @@ testing =
|
||||
tox
|
||||
tox-conda
|
||||
|
||||
[options.package_data]
|
||||
superqt = py.typed
|
||||
|
||||
[flake8]
|
||||
exclude = _version.py,.eggs,examples
|
||||
docstring-convention = numpy
|
||||
|
@@ -17,8 +17,11 @@ from .sliders import (
|
||||
QRangeSlider,
|
||||
)
|
||||
from .spinbox import QLargeIntSpinBox
|
||||
from .utils import QMessageHandler, ensure_main_thread, ensure_object_thread
|
||||
|
||||
__all__ = [
|
||||
"ensure_main_thread",
|
||||
"ensure_object_thread",
|
||||
"QDoubleRangeSlider",
|
||||
"QDoubleSlider",
|
||||
"QElidingLabel",
|
||||
@@ -27,6 +30,7 @@ __all__ = [
|
||||
"QLabeledRangeSlider",
|
||||
"QLabeledSlider",
|
||||
"QLargeIntSpinBox",
|
||||
"QMessageHandler",
|
||||
"QRangeSlider",
|
||||
"QEnumComboBox",
|
||||
]
|
||||
|
0
superqt/py.typed
Normal file
0
superqt/py.typed
Normal file
@@ -1,8 +1,7 @@
|
||||
from PyQt5.QtWidgets import QSlider
|
||||
from superqt.qtcompat.QtWidgets import QSlider
|
||||
|
||||
from ._generic_range_slider import _GenericRangeSlider
|
||||
from ._generic_slider import _GenericSlider
|
||||
from .qtcompat.QtWidgets import QSlider
|
||||
|
||||
class QDoubleRangeSlider(_GenericRangeSlider): ...
|
||||
class QDoubleSlider(_GenericSlider): ...
|
||||
|
4
superqt/utils/__init__.py
Normal file
4
superqt/utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
__all__ = ("QMessageHandler", "ensure_object_thread", "ensure_main_thread")
|
||||
|
||||
from ._ensure_thread import ensure_main_thread, ensure_object_thread
|
||||
from ._message_handler import QMessageHandler
|
122
superqt/utils/_ensure_thread.py
Normal file
122
superqt/utils/_ensure_thread.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# https://gist.github.com/FlorianRhiem/41a1ad9b694c14fb9ac3
|
||||
from concurrent.futures import Future
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from superqt.qtcompat.QtCore import (
|
||||
QCoreApplication,
|
||||
QMetaObject,
|
||||
QObject,
|
||||
Qt,
|
||||
QThread,
|
||||
Signal,
|
||||
Slot,
|
||||
)
|
||||
|
||||
|
||||
class CallCallable(QObject):
|
||||
finished = Signal(object)
|
||||
instances: List["CallCallable"] = []
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
super().__init__()
|
||||
self._callable = callable
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
CallCallable.instances.append(self)
|
||||
|
||||
@Slot()
|
||||
def call(self):
|
||||
CallCallable.instances.remove(self)
|
||||
res = self._callable(*self._args, **self._kwargs)
|
||||
self.finished.emit(res)
|
||||
|
||||
|
||||
def ensure_main_thread(
|
||||
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
|
||||
):
|
||||
"""Decorator that ensures a function is called in the main QApplication thread.
|
||||
|
||||
It can be applied to functions or methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : callable
|
||||
The method to decorate, must be a method on a QObject.
|
||||
await_return : bool, optional
|
||||
Whether to block and wait for the result of the function, or return immediately.
|
||||
by default False
|
||||
timeout : int, optional
|
||||
If `await_return` is `True`, time (in milliseconds) to wait for the result
|
||||
before raising a TimeoutError, by default 1000
|
||||
"""
|
||||
|
||||
def _out_func(func):
|
||||
def _func(*args, **kwargs):
|
||||
return _run_in_thread(
|
||||
func,
|
||||
QCoreApplication.instance().thread(),
|
||||
await_return,
|
||||
timeout,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return _func
|
||||
|
||||
if func is None:
|
||||
return _out_func
|
||||
return _out_func(func)
|
||||
|
||||
|
||||
def ensure_object_thread(
|
||||
func: Optional[Callable] = None, await_return: bool = False, timeout: int = 1000
|
||||
):
|
||||
"""Decorator that ensures a QObject method is called in the object's thread.
|
||||
|
||||
It must be applied to methods of QObjects subclasses.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : callable
|
||||
The method to decorate, must be a method on a QObject.
|
||||
await_return : bool, optional
|
||||
Whether to block and wait for the result of the function, or return immediately.
|
||||
by default False
|
||||
timeout : int, optional
|
||||
If `await_return` is `True`, time (in milliseconds) to wait for the result
|
||||
before raising a TimeoutError, by default 1000
|
||||
"""
|
||||
|
||||
def _out_func(func):
|
||||
def _func(self, *args, **kwargs):
|
||||
return _run_in_thread(
|
||||
func, self.thread(), await_return, timeout, self, *args, **kwargs
|
||||
)
|
||||
|
||||
return _func
|
||||
|
||||
if func is None:
|
||||
return _out_func
|
||||
return _out_func(func)
|
||||
|
||||
|
||||
def _run_in_thread(
|
||||
func: Callable,
|
||||
thread: QThread,
|
||||
await_return: bool,
|
||||
timeout: int,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
future = Future() # type: ignore
|
||||
if thread is QThread.currentThread():
|
||||
result = func(*args, **kwargs)
|
||||
if not await_return:
|
||||
future.set_result(result)
|
||||
return future
|
||||
return result
|
||||
f = CallCallable(func, *args, **kwargs)
|
||||
f.moveToThread(thread)
|
||||
f.finished.connect(future.set_result, Qt.ConnectionType.DirectConnection)
|
||||
QMetaObject.invokeMethod(f, "call", Qt.ConnectionType.QueuedConnection) # type: ignore
|
||||
return future.result(timeout=timeout / 1000) if await_return else future
|
52
superqt/utils/_ensure_thread.pyi
Normal file
52
superqt/utils/_ensure_thread.pyi
Normal file
@@ -0,0 +1,52 @@
|
||||
from concurrent.futures import Future
|
||||
from typing import Callable, TypeVar, overload
|
||||
|
||||
from typing_extensions import Literal, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
@overload
|
||||
def ensure_main_thread(
|
||||
await_return: Literal[True],
|
||||
timeout: int = 1000,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
||||
@overload
|
||||
def ensure_main_thread(
|
||||
func: Callable[P, R],
|
||||
await_return: Literal[True],
|
||||
timeout: int = 1000,
|
||||
) -> Callable[P, R]: ...
|
||||
@overload
|
||||
def ensure_main_thread(
|
||||
await_return: Literal[False] = False,
|
||||
timeout: int = 1000,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, Future[R]]]: ...
|
||||
@overload
|
||||
def ensure_main_thread(
|
||||
func: Callable[P, R],
|
||||
await_return: Literal[False] = False,
|
||||
timeout: int = 1000,
|
||||
) -> Callable[P, Future[R]]: ...
|
||||
@overload
|
||||
def ensure_object_thread(
|
||||
await_return: Literal[True],
|
||||
timeout: int = 1000,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
||||
@overload
|
||||
def ensure_object_thread(
|
||||
func: Callable[P, R],
|
||||
await_return: Literal[True],
|
||||
timeout: int = 1000,
|
||||
) -> Callable[P, R]: ...
|
||||
@overload
|
||||
def ensure_object_thread(
|
||||
await_return: Literal[False] = False,
|
||||
timeout: int = 1000,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, Future[R]]]: ...
|
||||
@overload
|
||||
def ensure_object_thread(
|
||||
func: Callable[P, R],
|
||||
await_return: Literal[False] = False,
|
||||
timeout: int = 1000,
|
||||
) -> Callable[P, Future[R]]: ...
|
98
superqt/utils/_message_handler.py
Normal file
98
superqt/utils/_message_handler.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
from superqt.qtcompat.QtCore import (
|
||||
QMessageLogContext,
|
||||
QtMsgType,
|
||||
qInstallMessageHandler,
|
||||
)
|
||||
|
||||
|
||||
class Record(NamedTuple):
|
||||
level: int
|
||||
message: str
|
||||
ctx: dict
|
||||
|
||||
|
||||
class QMessageHandler:
|
||||
"""A context manager to intercept messages from Qt.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
logger : logging.Logger, optional
|
||||
If provided, intercepted messages will be logged with `logger` at the
|
||||
corresponding python log level, by default None
|
||||
|
||||
Attributes
|
||||
----------
|
||||
records: list of tuple
|
||||
Captured messages. This is a 3-tuple of:
|
||||
`(log_level: int, message: str, context: dict)`
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> handler = QMessageHandler()
|
||||
>>> handler.install() # now all Qt output will be available at mh.records
|
||||
|
||||
>>> with QMessageHandler() as handler: # temporarily install
|
||||
... ...
|
||||
|
||||
>>> logger = logging.getLogger(__name__)
|
||||
>>> with QMessageHandler(logger): # re-reoute Qt messages to a python logger.
|
||||
... ...
|
||||
"""
|
||||
|
||||
_qt2loggertype = {
|
||||
QtMsgType.QtDebugMsg: logging.DEBUG,
|
||||
QtMsgType.QtInfoMsg: logging.INFO,
|
||||
QtMsgType.QtWarningMsg: logging.WARNING,
|
||||
QtMsgType.QtCriticalMsg: logging.ERROR, # note
|
||||
QtMsgType.QtFatalMsg: logging.CRITICAL, # note
|
||||
QtMsgType.QtSystemMsg: logging.CRITICAL,
|
||||
}
|
||||
|
||||
def __init__(self, logger: Optional[logging.Logger] = None):
|
||||
self.records: List[Record] = []
|
||||
self._logger = logger
|
||||
self._previous_handler: Optional[object] = "__uninstalled__"
|
||||
|
||||
def install(self):
|
||||
"""Install this handler (override the current QtMessageHandler)."""
|
||||
self._previous_handler = qInstallMessageHandler(self)
|
||||
|
||||
def uninstall(self):
|
||||
"""Uninstall this handler, restoring the previous handler."""
|
||||
if self._previous_handler != "__uninstalled__":
|
||||
qInstallMessageHandler(self._previous_handler)
|
||||
|
||||
def __repr__(self):
|
||||
n = type(self).__name__
|
||||
return f"<{n} object at {hex(id(self))} with {len(self.records)} records>"
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter a context with this handler installed"""
|
||||
self.install()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.uninstall()
|
||||
|
||||
def __call__(self, msgtype: QtMsgType, context: QMessageLogContext, message: str):
|
||||
level = self._qt2loggertype[msgtype]
|
||||
|
||||
# PyQt seems to throw an error if these are simply empty
|
||||
ctx = dict.fromkeys(["category", "file", "function", "line"])
|
||||
with suppress(UnicodeDecodeError):
|
||||
ctx["category"] = context.category
|
||||
with suppress(UnicodeDecodeError):
|
||||
ctx["file"] = context.file
|
||||
with suppress(UnicodeDecodeError):
|
||||
ctx["function"] = context.function
|
||||
with suppress(UnicodeDecodeError):
|
||||
ctx["line"] = context.line
|
||||
|
||||
self.records.append(Record(level, message, ctx))
|
||||
if self._logger is not None:
|
||||
self._logger.log(level, message, extra=ctx)
|
186
superqt/utils/_tests/test_ensure_thread.py
Normal file
186
superqt/utils/_tests/test_ensure_thread.py
Normal file
@@ -0,0 +1,186 @@
|
||||
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):
|
||||
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
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
35
superqt/utils/_tests/test_qmessage_handler.py
Normal file
35
superqt/utils/_tests/test_qmessage_handler.py
Normal 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
|
Reference in New Issue
Block a user