-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
"In the face of ambiguity refuse the temptation to guess" -- Tim Peters #29
base: master
Are you sure you want to change the base?
Changes from 7 commits
847df31
53d6fef
ec60641
4ee2e75
7e552e4
fed7806
09586b3
556208d
e660d42
abb907a
9b0e646
79a6458
63500e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,51 +12,20 @@ | |
import os | ||
import asyncio | ||
import time | ||
from functools import wraps | ||
import itertools | ||
from queue import Queue | ||
from concurrent.futures import Future | ||
import logging | ||
logger = logging.getLogger('quamash') | ||
|
||
try: | ||
QtModuleName = os.environ['QUAMASH_QTIMPL'] | ||
except KeyError: | ||
QtModule = None | ||
else: | ||
logger.info('Forcing use of {} as Qt Implementation'.format(QtModuleName)) | ||
QtModule = __import__(QtModuleName) | ||
|
||
if not QtModule: | ||
for QtModuleName in ('PyQt5', 'PyQt4', 'PySide'): | ||
try: | ||
QtModule = __import__(QtModuleName) | ||
except ImportError: | ||
continue | ||
else: | ||
break | ||
else: | ||
raise ImportError('No Qt implementations found') | ||
|
||
logger.info('Using Qt Implementation: {}'.format(QtModuleName)) | ||
|
||
QtCore = __import__(QtModuleName + '.QtCore', fromlist=(QtModuleName,)) | ||
QtGui = __import__(QtModuleName + '.QtGui', fromlist=(QtModuleName,)) | ||
if QtModuleName == 'PyQt5': | ||
from PyQt5 import QtWidgets | ||
QApplication = QtWidgets.QApplication | ||
else: | ||
QApplication = QtGui.QApplication | ||
|
||
if not hasattr(QtCore, 'Signal'): | ||
QtCore.Signal = QtCore.pyqtSignal | ||
|
||
from importlib import import_module | ||
import warnings | ||
|
||
from ._common import with_logger | ||
|
||
if 'QUAMASH_QTIMPL' in os.environ: | ||
warnings.warn("QUAMASH_QTIMPL environment variable set, this version of quamash ignores it.") | ||
|
||
|
||
@with_logger | ||
class _QThreadWorker(QtCore.QThread): | ||
class _QThreadWorker: | ||
|
||
""" | ||
Read from the queue. | ||
|
@@ -104,25 +73,31 @@ def wait(self): | |
|
||
|
||
@with_logger | ||
class QThreadExecutor(QtCore.QObject): | ||
class QThreadExecutor: | ||
|
||
""" | ||
ThreadExecutor that produces QThreads. | ||
|
||
Same API as `concurrent.futures.Executor` | ||
|
||
>>> from quamash import QThreadExecutor | ||
>>> with QThreadExecutor(5) as executor: | ||
>>> QtCore = getfixture('qtcore') | ||
>>> with QThreadExecutor(QtCore.QThread, 5) as executor: | ||
... f = executor.submit(lambda x: 2 + x, 2) | ||
... r = f.result() | ||
... assert r == 4 | ||
""" | ||
|
||
def __init__(self, max_workers=10, parent=None): | ||
super().__init__(parent) | ||
def __init__(self, qthread_class, max_workers=10): | ||
super().__init__() | ||
assert isinstance(qthread_class, type) | ||
|
||
class QThreadWorker(_QThreadWorker, qthread_class): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit ugly, maybe we could have a small nested wrapper class that calls _QThreadWorker instead? I.e., composition rather than inheritance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _QThreadWorker has to inherit from QThread. This is how Qt is designed. Or maybe I misunderstand? Or do you mean something like: class QThreadHelper(qthread_class):
def run(self):
# stuff
worker = QThreadWorker(QThreadHelper()) Or the other way around might be more clean class QThreadHelper(qthread_class):
def __init__(self, worker):
self.worker = worker
def run(self, *a, **kw):
self.worker.run(*a, **kw)
worker = QThreadWorker(QThreadHelper()) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a pattern in which you don't have to inherit from QThread (see the official documentation for an example), but that wasn't the point I was making. I think the nested wrapper class should call the implementation rather than inherit from it. |
||
pass | ||
|
||
self.__max_workers = max_workers | ||
self.__queue = Queue() | ||
self.__workers = [_QThreadWorker(self.__queue, i + 1) for i in range(max_workers)] | ||
self.__workers = [QThreadWorker(self.__queue, i + 1) for i in range(max_workers)] | ||
self.__been_shutdown = False | ||
|
||
for w in self.__workers: | ||
|
@@ -165,57 +140,13 @@ def __exit__(self, *args): | |
self.shutdown() | ||
|
||
|
||
def _easycallback(fn): | ||
""" | ||
Decorator that wraps a callback in a signal. | ||
|
||
It also packs & unpacks arguments, and makes the wrapped function effectively | ||
threadsafe. If you call the function from one thread, it will be executed in | ||
the thread the QObject has affinity with. | ||
|
||
Remember: only objects that inherit from QObject can support signals/slots | ||
|
||
>>> import asyncio | ||
>>> | ||
>>> import quamash | ||
>>> QThread, QObject = quamash.QtCore.QThread, quamash.QtCore.QObject | ||
>>> | ||
>>> app = getfixture('application') | ||
>>> | ||
>>> global_thread = QThread.currentThread() | ||
>>> class MyObject(QObject): | ||
... @_easycallback | ||
... def mycallback(self): | ||
... global global_thread, mythread | ||
... cur_thread = QThread.currentThread() | ||
... assert cur_thread is not global_thread | ||
... assert cur_thread is mythread | ||
>>> | ||
>>> mythread = QThread() | ||
>>> mythread.start() | ||
>>> myobject = MyObject() | ||
>>> myobject.moveToThread(mythread) | ||
>>> | ||
>>> @asyncio.coroutine | ||
... def mycoroutine(): | ||
... myobject.mycallback() | ||
>>> | ||
>>> loop = QEventLoop(app) | ||
>>> asyncio.set_event_loop(loop) | ||
>>> with loop: | ||
... loop.run_until_complete(mycoroutine()) | ||
""" | ||
@wraps(fn) | ||
def in_wrapper(self, *args, **kwargs): | ||
return signaler.signal.emit(self, args, kwargs) | ||
|
||
class Signaler(QtCore.QObject): | ||
signal = QtCore.Signal(object, tuple, dict) | ||
|
||
signaler = Signaler() | ||
signaler.signal.connect(lambda self, args, kwargs: fn(self, *args, **kwargs)) | ||
return in_wrapper | ||
|
||
def _make_signaller(qtimpl_qtcore, *args): | ||
class Signaller(qtimpl_qtcore.QObject): | ||
try: | ||
signal = qtimpl_qtcore.Signal(*args) | ||
except AttributeError: | ||
signal = qtimpl_qtcore.pyqtSignal(*args) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (minor) To avoid shadowing errors in an implementation, I’d suggest: try:
signal_cls = qtimpl_qtcore.Signal
except AttributeError:
signal_cls = qtimpl_qtcore.pyqtSignal
signal = signal_cls(*args) Or just going straight away with hasattr(), which seems to be the preferred way in ducktyping terms from the python code I have seen. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with your suggested code @horazont. I don't think we should go with hasattr though, after all it's easier to ask for forgiveness than permission, which is a typical rule of thumb in Python (at least this is what I learnt many years ago). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went a slightly different route style-wise, (I like to reduce indentations, even if it means more lines, and I like to spell out variables in words instead of weird abbreviations), but it's been pushed. see: abb907a |
||
return Signaller() | ||
|
||
if os.name == 'nt': | ||
from . import _windows | ||
|
@@ -247,9 +178,9 @@ class QEventLoop(_baseclass): | |
... loop.run_until_complete(xplusy(2, 2)) | ||
""" | ||
|
||
def __init__(self, app=None): | ||
def __init__(self, app): | ||
self.__timers = [] | ||
self.__app = app or QApplication.instance() | ||
self.__app = app | ||
assert self.__app is not None, 'No QApplication has been instantiated' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer a clearer error message, which informs the user that this is an intended change. Also perhaps a normal exception, such as NotImplementedError (as it is was legal in previous versions) or just a ValueError. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that's definitely an oversight. |
||
self.__is_running = False | ||
self.__debug_enabled = False | ||
|
@@ -258,6 +189,12 @@ def __init__(self, app=None): | |
self._read_notifiers = {} | ||
self._write_notifiers = {} | ||
|
||
self._qtcore = import_module('..QtCore', type(app).__module__) | ||
|
||
self.__call_soon_signaller = signaller = _make_signaller(self._qtcore, object, tuple) | ||
self.__call_soon_signal = signaller.signal | ||
signaller.signal.connect(lambda callback, args: self.call_soon(callback, *args)) | ||
|
||
assert self.__app is not None | ||
|
||
super().__init__() | ||
|
@@ -352,7 +289,7 @@ def upon_timeout(): | |
handle._run() | ||
|
||
self._logger.debug('Adding callback {} with delay {}'.format(handle, delay)) | ||
timer = QtCore.QTimer(self.__app) | ||
timer = self._qtcore.QTimer(self.__app) | ||
timer.timeout.connect(upon_timeout) | ||
timer.setSingleShot(True) | ||
timer.start(delay * 1000) | ||
|
@@ -384,7 +321,7 @@ def add_reader(self, fd, callback, *args): | |
existing.activated.disconnect() | ||
# will get overwritten by the assignment below anyways | ||
|
||
notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read) | ||
notifier = self._qtcore.QSocketNotifier(fd, self._qtcore.QSocketNotifier.Read) | ||
notifier.setEnabled(True) | ||
self._logger.debug('Adding reader callback for file descriptor {}'.format(fd)) | ||
notifier.activated.connect( | ||
|
@@ -416,7 +353,7 @@ def add_writer(self, fd, callback, *args): | |
existing.activated.disconnect() | ||
# will get overwritten by the assignment below anyways | ||
|
||
notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Write) | ||
notifier = self._qtcore.QSocketNotifier(fd, self._qtcore.QSocketNotifier.Write) | ||
notifier.setEnabled(True) | ||
self._logger.debug('Adding writer callback for file descriptor {}'.format(fd)) | ||
notifier.activated.connect( | ||
|
@@ -472,10 +409,9 @@ def __on_notifier_ready(self, notifiers, notifier, fd, callback, args): | |
|
||
# Methods for interacting with threads. | ||
|
||
@_easycallback | ||
def call_soon_threadsafe(self, callback, *args): | ||
"""Thread-safe version of call_soon.""" | ||
self.call_soon(callback, *args) | ||
self.__call_soon_signal.emit(callback, args) | ||
|
||
def run_in_executor(self, executor, callback, *args): | ||
"""Run callback in executor. | ||
|
@@ -496,7 +432,7 @@ def run_in_executor(self, executor, callback, *args): | |
executor = executor or self.__default_executor | ||
if executor is None: | ||
self._logger.debug('Creating default executor') | ||
executor = self.__default_executor = QThreadExecutor() | ||
executor = self.__default_executor = QThreadExecutor(self._qtcore.QThread) | ||
self._logger.debug('Using default executor') | ||
|
||
return asyncio.wrap_future(executor.submit(callback, *args)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,22 +15,23 @@ | |
|
||
import math | ||
|
||
from . import QtCore | ||
from . import _make_signaller | ||
from ._common import with_logger | ||
|
||
UINT32_MAX = 0xffffffff | ||
|
||
|
||
class _ProactorEventLoop(QtCore.QObject, asyncio.ProactorEventLoop): | ||
class _ProactorEventLoop(asyncio.ProactorEventLoop): | ||
|
||
"""Proactor based event loop.""" | ||
|
||
def __init__(self): | ||
QtCore.QObject.__init__(self) | ||
asyncio.ProactorEventLoop.__init__(self, _IocpProactor()) | ||
super().__init__(_IocpProactor()) | ||
|
||
self.__event_poller = _EventPoller() | ||
self.__event_poller.sig_events.connect(self._process_events) | ||
self.__event_signaller = _make_signaller(self._qtcore, list) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not obvious where the specific loop classes get their _qtcore attribute from. Could we pass it down through the constructor? Or at least document it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so keep qtcore on self.__qtcore instead of self._qtcore (two underscores instead of one). and then pass it up through super()? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally would go for passing it through super() and let the base class deal with setting the (Or just document it.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see where you're coming from, but the IOCP Loop for windows and the It seems like you're saying QEventLoop should pass qtcore up to it's parent That feels like more spaghetti, not less. Maybe if there was a shared OTOH it does violate some OOP principle (and laws of nature) that a parent Anyway after some consideration I've made those changes. On Mon, Jan 19, 2015 at 12:01 PM, Jonas Wielicki notifications@github.com
|
||
self.__event_signal = self.__event_signaller.signal | ||
self.__event_signal.connect(self._process_events) | ||
self.__event_poller = _EventPoller(self.__event_signal, self._qtcore) | ||
|
||
def _process_events(self, events): | ||
"""Process events from proactor.""" | ||
|
@@ -113,14 +114,14 @@ def _poll(self, timeout=None): | |
|
||
|
||
@with_logger | ||
class _EventWorker(QtCore.QThread): | ||
def __init__(self, proactor, parent): | ||
class _EventWorker: | ||
def __init__(self, proactor, parent, semaphore_factory): | ||
super().__init__() | ||
|
||
self.__stop = False | ||
self.__proactor = proactor | ||
self.__sig_events = parent.sig_events | ||
self.__semaphore = QtCore.QSemaphore() | ||
self.__semaphore = semaphore_factory() | ||
|
||
def start(self): | ||
super().start() | ||
|
@@ -145,15 +146,21 @@ def run(self): | |
|
||
|
||
@with_logger | ||
class _EventPoller(QtCore.QObject): | ||
class _EventPoller: | ||
|
||
"""Polling of events in separate thread.""" | ||
|
||
sig_events = QtCore.Signal(list) | ||
def __init__(self, sig_events, qtcore): | ||
self.sig_events = sig_events | ||
self._qtcore = qtcore | ||
|
||
def start(self, proactor): | ||
self._logger.debug('Starting (proactor: {})...'.format(proactor)) | ||
self.__worker = _EventWorker(proactor, self) | ||
|
||
class EventWorker(_EventWorker, self._qtcore.QThread): | ||
pass | ||
|
||
self.__worker = EventWorker(proactor, self, self._qtcore.QSemaphore) | ||
self.__worker.start() | ||
|
||
def stop(self): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't look right to me to make clients of Quamash have to specify the thread class, considering QThreadExecutor is public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but I'm not sure what the alternative is.
One of the ideas floated around was some sort of
quamash.set_qt(PyQt5/PyQt4/PySide)
type thing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think there should be a single way of saying which Qt implementation Quamash is to use.
quamash.set_qt
still looks good to me, since we then don't have to infer which Qt package to use. It's logically analogous to Qt widgets' refusal to be constructed unless there's a QApplication running; the complication in our case being that we don't know the type of QApplication until someone points us to the containing package.