From 847df31c184125c8d147d74d17b690218e0cec2c Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Sun, 18 Jan 2015 21:08:11 -0800 Subject: [PATCH 01/11] remove unnecessary QObjects & fixup super() calls --- quamash/__init__.py | 6 +++--- quamash/_windows.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index a7a5831..a888b7c 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -104,7 +104,7 @@ def wait(self): @with_logger -class QThreadExecutor(QtCore.QObject): +class QThreadExecutor: """ ThreadExecutor that produces QThreads. @@ -118,8 +118,8 @@ class QThreadExecutor(QtCore.QObject): ... assert r == 4 """ - def __init__(self, max_workers=10, parent=None): - super().__init__(parent) + def __init__(self, max_workers=10): + super().__init__() self.__max_workers = max_workers self.__queue = Queue() self.__workers = [_QThreadWorker(self.__queue, i + 1) for i in range(max_workers)] diff --git a/quamash/_windows.py b/quamash/_windows.py index e12c85e..51c43d0 100644 --- a/quamash/_windows.py +++ b/quamash/_windows.py @@ -21,13 +21,12 @@ 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) From 53d6fef05fd001ac21e3b69f2bbe6059c73fbd95 Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Sun, 18 Jan 2015 21:16:43 -0800 Subject: [PATCH 02/11] replace _easycallback with _make_signaller _easycallback is hard to use indirectly to create signals at runtime, _make_signaller is more flexible & designed specifically to creat signals at runtime. _windows used it's own signal creation mechanism, and now it uses the same one as _call_soon_threadsafe. --- quamash/__init__.py | 69 ++++++++------------------------------------- quamash/_windows.py | 13 +++++---- 2 files changed, 20 insertions(+), 62 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index a888b7c..465b546 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -12,7 +12,6 @@ import os import asyncio import time -from functools import wraps import itertools from queue import Queue from concurrent.futures import Future @@ -48,9 +47,6 @@ else: QApplication = QtGui.QApplication -if not hasattr(QtCore, 'Signal'): - QtCore.Signal = QtCore.pyqtSignal - from ._common import with_logger @@ -165,57 +161,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) + return Signaller() if os.name == 'nt': from . import _windows @@ -258,6 +210,10 @@ def __init__(self, app=None): self._read_notifiers = {} self._write_notifiers = {} + self.__call_soon_signaller = signaller = _make_signaller(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__() @@ -472,10 +428,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. diff --git a/quamash/_windows.py b/quamash/_windows.py index 51c43d0..aba10e5 100644 --- a/quamash/_windows.py +++ b/quamash/_windows.py @@ -15,7 +15,7 @@ import math -from . import QtCore +from . import QtCore, QtModule, _make_signaller from ._common import with_logger UINT32_MAX = 0xffffffff @@ -28,8 +28,10 @@ class _ProactorEventLoop(asyncio.ProactorEventLoop): def __init__(self): super().__init__(_IocpProactor()) - self.__event_poller = _EventPoller() - self.__event_poller.sig_events.connect(self._process_events) + self.__event_signaller = _make_signaller(QtCore, list) + self.__event_signal = self.__event_signaller.signal + self.__event_signal.connect(self._process_events) + self.__event_poller = _EventPoller(self.__event_signal) def _process_events(self, events): """Process events from proactor.""" @@ -144,11 +146,12 @@ 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): + self.sig_events = sig_events def start(self, proactor): self._logger.debug('Starting (proactor: {})...'.format(proactor)) From ec60641360404f0a565b8861136c02f2ec28d920 Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Sun, 18 Jan 2015 21:37:20 -0800 Subject: [PATCH 03/11] style fix QtModule was imported but unused. --- quamash/_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quamash/_windows.py b/quamash/_windows.py index aba10e5..eeea5bb 100644 --- a/quamash/_windows.py +++ b/quamash/_windows.py @@ -15,7 +15,7 @@ import math -from . import QtCore, QtModule, _make_signaller +from . import QtCore, _make_signaller from ._common import with_logger UINT32_MAX = 0xffffffff From 7e552e432ca638aeb50be489e612ff53ee42c1ae Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Sun, 18 Jan 2015 23:09:37 -0800 Subject: [PATCH 04/11] Cleanup imports to use import_module Using import module is cleaner than __import__ in some situations. This code is much easier to read. Also eliminates the QtModule variable. --- quamash/__init__.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index 465b546..60af760 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -16,20 +16,21 @@ from queue import Queue from concurrent.futures import Future import logging +from importlib import import_module logger = logging.getLogger('quamash') try: QtModuleName = os.environ['QUAMASH_QTIMPL'] except KeyError: - QtModule = None + QtModuleName = None else: logger.info('Forcing use of {} as Qt Implementation'.format(QtModuleName)) QtModule = __import__(QtModuleName) -if not QtModule: +if not QtModuleName: for QtModuleName in ('PyQt5', 'PyQt4', 'PySide'): try: - QtModule = __import__(QtModuleName) + __import__(QtModuleName) except ImportError: continue else: @@ -37,15 +38,16 @@ else: raise ImportError('No Qt implementations found') +QtCore = import_module('.QtCore', QtModuleName) + 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 +for module in ('.QtWidgets', '.QtGui'): + try: + QApplication = import_module(module, QtModuleName).QApplication + break + except (ImportError, AttributeError): + continue from ._common import with_logger From fed7806c07fb620594ba4320a8b428e65330cd7b Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Sun, 18 Jan 2015 23:33:23 -0800 Subject: [PATCH 05/11] QtCore indirection QtCore is only referenced directly by the tests, never in code except in the initializer for the loop, but everyone else receives a copy of QtCore from the loop, and never receives it at compile time. --- quamash/__init__.py | 27 +++++++++++++++++---------- quamash/_unix.py | 6 +++--- quamash/_windows.py | 21 +++++++++++++-------- tests/test_qeventloop.py | 6 +++++- tests/test_qthreadexec.py | 5 +++-- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index 60af760..a527fb8 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -54,7 +54,7 @@ @with_logger -class _QThreadWorker(QtCore.QThread): +class _QThreadWorker: """ Read from the queue. @@ -109,18 +109,23 @@ class QThreadExecutor: Same API as `concurrent.futures.Executor` - >>> from quamash import QThreadExecutor - >>> with QThreadExecutor(5) as executor: + >>> from quamash import QThreadExecutor, 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): + def __init__(self, qthread_class, max_workers=10): super().__init__() + assert isinstance(qthread_class, type) + + class QThreadWorker(_QThreadWorker, qthread_class): + 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: @@ -212,7 +217,9 @@ def __init__(self, app=None): self._read_notifiers = {} self._write_notifiers = {} - self.__call_soon_signaller = signaller = _make_signaller(QtCore, object, tuple) + self._qtcore = QtCore + + 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)) @@ -310,7 +317,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) @@ -342,7 +349,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( @@ -374,7 +381,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( @@ -453,7 +460,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)) diff --git a/quamash/_unix.py b/quamash/_unix.py index f6c47e2..c7a2a99 100644 --- a/quamash/_unix.py +++ b/quamash/_unix.py @@ -8,7 +8,7 @@ from asyncio import selectors import collections -from . import QtCore, with_logger +from . import with_logger EVENT_READ = (1 << 0) @@ -106,11 +106,11 @@ def register(self, fileobj, events, data=None): self._fd_to_key[key.fd] = key if events & EVENT_READ: - notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Read) + notifier = self._qtcore.QSocketNotifier(key.fd, self._qtcore.QSocketNotifier.Read) notifier.activated.connect(self.__on_read_activated) self.__read_notifiers[key.fd] = notifier if events & EVENT_WRITE: - notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Write) + notifier = self._qtcore.QSocketNotifier(key.fd, self._qtcore.QSocketNotifier.Write) notifier.activated.connect(self.__on_write_activated) self.__write_notifiers[key.fd] = notifier diff --git a/quamash/_windows.py b/quamash/_windows.py index eeea5bb..e621f88 100644 --- a/quamash/_windows.py +++ b/quamash/_windows.py @@ -15,7 +15,7 @@ import math -from . import QtCore, _make_signaller +from . import _make_signaller from ._common import with_logger UINT32_MAX = 0xffffffff @@ -28,10 +28,10 @@ class _ProactorEventLoop(asyncio.ProactorEventLoop): def __init__(self): super().__init__(_IocpProactor()) - self.__event_signaller = _make_signaller(QtCore, list) + self.__event_signaller = _make_signaller(self._qtcore, list) self.__event_signal = self.__event_signaller.signal self.__event_signal.connect(self._process_events) - self.__event_poller = _EventPoller(self.__event_signal) + self.__event_poller = _EventPoller(self.__event_signal, self._qtcore) def _process_events(self, events): """Process events from proactor.""" @@ -114,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() @@ -150,12 +150,17 @@ class _EventPoller: """Polling of events in separate thread.""" - def __init__(self, sig_events): + 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): diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index fa5b512..064667b 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -13,6 +13,7 @@ import subprocess import quamash +from quamash import QtCore import pytest @@ -78,8 +79,11 @@ def executor(request): exc_cls = request.param if exc_cls is None: return None + elif exc_cls is quamash.QThreadExecutor: + exc = exc_cls(QtCore.QThread, 1) + else: + exc = exc_cls(1) # FIXME? fixed number of workers? - exc = exc_cls(1) # FIXME? fixed number of workers? request.addfinalizer(exc.shutdown) return exc diff --git a/tests/test_qthreadexec.py b/tests/test_qthreadexec.py index 27f0710..64738df 100644 --- a/tests/test_qthreadexec.py +++ b/tests/test_qthreadexec.py @@ -3,18 +3,19 @@ # BSD License import pytest import quamash +from quamash import QtCore @pytest.fixture def executor(request): - exe = quamash.QThreadExecutor(5) + exe = quamash.QThreadExecutor(QtCore.Qthread, 5) request.addfinalizer(exe.shutdown) return exe @pytest.fixture def shutdown_executor(): - exe = quamash.QThreadExecutor(5) + exe = quamash.QThreadExecutor(QtCore.QThread, 5) exe.shutdown() return exe From 09586b3bf24dd04ceca38988c285cf1b8d023634 Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Mon, 19 Jan 2015 01:28:39 -0800 Subject: [PATCH 06/11] Use type of app param to QEventLoop to determine which Qt module to use This is a big commit, but any smaller changes would put the project into a non-working state where everything fails and things crumble and it's all terrible. The big change is QtCore, and QApplication are no longer imported by quamash at import time. Instead, the type of the app parameter to QEventLoop determines which module is imported (PySide, PyQt4, or PyQt5). This is fairly robust and should support PySide with Qt5 when (and if) it is released. QThreadExecutor now takes in a parameter that should reference QtCore.QThread. Changes to tests and CI configuration to use a --qtimpl parameter instead of the QUAMASH_QTIMPL environment variable. a new fixture called qtcore was also created so tests (mostly those that test QThreadExecutor) could access a reference to the QtCore module. TODO update README --- .travis.yml | 6 ++--- appveyor.yml | 2 +- conftest.py | 23 +++++++++++++++++--- quamash/__init__.py | 46 ++++++++------------------------------- tests/test_qeventloop.py | 6 ++--- tests/test_qthreadexec.py | 22 ++++++++++++++----- tox.ini | 9 ++++---- 7 files changed, 56 insertions(+), 58 deletions(-) diff --git a/.travis.yml b/.travis.yml index 907d336..2ebcf5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,9 +58,9 @@ install: # flake8 style checker - pip install flake8 pep8-naming flake8-debugger flake8-docstrings script: - - QUAMASH_QTIMPL=PySide py.test - - QUAMASH_QTIMPL=PyQt4 py.test - - QUAMASH_QTIMPL=PyQt5 py.test + - py.test --qtimpl PySide + - py.test --qtimpl PyQt4 + - py.test --qtimpl PyQt5 - flake8 --ignore=D1,W191,E501 - flake8 --select=D1 quamash/*.py cache: diff --git a/appveyor.yml b/appveyor.yml index 665e124..7a1a295 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,7 +30,7 @@ install: build: off test_script: - - "%PYTHON%\\Scripts\\py.test.exe" + - "%PYTHON%\\Scripts\\py.test.exe --qtimpl %QTIMPL%" notifications: - provider: Webhook diff --git a/conftest.py b/conftest.py index e34e139..c221d7d 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ import sys import os.path import logging +from importlib import import_module from pytest import fixture sys.path.insert(0, os.path.dirname(__file__)) logging.basicConfig( @@ -12,7 +13,23 @@ collect_ignore = ['quamash/_windows.py'] +def pytest_addoption(parser): + parser.addoption("--qtimpl", default='PySide') + + +@fixture(scope='session') +def application(request): + qtimpl = request.config.getoption('qtimpl') + __import__(qtimpl) + for module in ('.QtWidgets', '.QtGui'): + try: + return import_module(module, qtimpl).QApplication([]) + except (ImportError, AttributeError): + continue + + @fixture(scope='session') -def application(): - from quamash import QApplication - return QApplication([]) +def qtcore(request): + qtimpl = request.config.getoption('qtimpl') + __import__(qtimpl) + return import_module('.QtCore', qtimpl) diff --git a/quamash/__init__.py b/quamash/__init__.py index a527fb8..86acdff 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -15,43 +15,14 @@ import itertools from queue import Queue from concurrent.futures import Future -import logging from importlib import import_module -logger = logging.getLogger('quamash') - -try: - QtModuleName = os.environ['QUAMASH_QTIMPL'] -except KeyError: - QtModuleName = None -else: - logger.info('Forcing use of {} as Qt Implementation'.format(QtModuleName)) - QtModule = __import__(QtModuleName) - -if not QtModuleName: - for QtModuleName in ('PyQt5', 'PyQt4', 'PySide'): - try: - __import__(QtModuleName) - except ImportError: - continue - else: - break - else: - raise ImportError('No Qt implementations found') - -QtCore = import_module('.QtCore', QtModuleName) - -logger.info('Using Qt Implementation: {}'.format(QtModuleName)) - -for module in ('.QtWidgets', '.QtGui'): - try: - QApplication = import_module(module, QtModuleName).QApplication - break - except (ImportError, AttributeError): - continue - +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: @@ -109,7 +80,8 @@ class QThreadExecutor: Same API as `concurrent.futures.Executor` - >>> from quamash import QThreadExecutor, QtCore + >>> from quamash import QThreadExecutor + >>> QtCore = getfixture('qtcore') >>> with QThreadExecutor(QtCore.QThread, 5) as executor: ... f = executor.submit(lambda x: 2 + x, 2) ... r = f.result() @@ -206,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' self.__is_running = False self.__debug_enabled = False @@ -217,7 +189,7 @@ def __init__(self, app=None): self._read_notifiers = {} self._write_notifiers = {} - self._qtcore = QtCore + 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 diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index 064667b..9f6fac9 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -13,7 +13,6 @@ import subprocess import quamash -from quamash import QtCore import pytest @@ -66,7 +65,6 @@ def excepthook(type, *args): orig_excepthook = sys.excepthook sys.excepthook = excepthook - lp.set_exception_handler(except_handler) request.addfinalizer(fin) return lp @@ -75,12 +73,12 @@ def excepthook(type, *args): @pytest.fixture( params=[None, quamash.QThreadExecutor, ThreadPoolExecutor, ProcessPoolExecutor] ) -def executor(request): +def executor(request, qtcore): exc_cls = request.param if exc_cls is None: return None elif exc_cls is quamash.QThreadExecutor: - exc = exc_cls(QtCore.QThread, 1) + exc = exc_cls(qtcore.QThread) else: exc = exc_cls(1) # FIXME? fixed number of workers? diff --git a/tests/test_qthreadexec.py b/tests/test_qthreadexec.py index 64738df..6a4ad5b 100644 --- a/tests/test_qthreadexec.py +++ b/tests/test_qthreadexec.py @@ -3,19 +3,18 @@ # BSD License import pytest import quamash -from quamash import QtCore @pytest.fixture -def executor(request): - exe = quamash.QThreadExecutor(QtCore.Qthread, 5) +def executor(request, qtcore): + exe = quamash.QThreadExecutor(qtcore.QThread, 5) request.addfinalizer(exe.shutdown) return exe @pytest.fixture -def shutdown_executor(): - exe = quamash.QThreadExecutor(QtCore.QThread, 5) +def shutdown_executor(qtcore): + exe = quamash.QThreadExecutor(qtcore.QThread, 5) exe.shutdown() return exe @@ -34,3 +33,16 @@ def test_ctx_after_shutdown(shutdown_executor): def test_submit_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): shutdown_executor.submit(None) + + +def test_run_in_executor_without_loop(executor): + f = executor.submit(lambda x: 2 + x, 2) + r = f.result() + assert r == 4 + + +def test_run_in_executor_as_ctx_manager(qtcore): + with quamash.QThreadExecutor(qtcore.QThread) as executor: + f = executor.submit(lambda x: 2 + x, 2) + r = f.result() + assert r == 4 diff --git a/tox.ini b/tox.ini index 4b44c8c..bb4c03f 100644 --- a/tox.ini +++ b/tox.ini @@ -12,11 +12,10 @@ sitepackages=True deps= pytest py33: asyncio -commands=py.test -setenv= - pyqt4: QUAMASH_QTIMPL=PyQt4 - pyqt5: QUAMASH_QTIMPL=PyQt5 - pyside: QUAMASH_QTIMPL=PySide +commands= + pyside: py.test --qtimpl PySide + pyqt4: py.test --qtimpl PyQt4 + pyqt5: py.test --qtimpl PyQt5 [pytest] addopts=--doctest-modules quamash quamash tests From 556208d3716064ee2dabec6ecd8817c04024a608 Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Mon, 19 Jan 2015 12:08:24 -0800 Subject: [PATCH 07/11] Better Documentation and Error Messages Before sending in an app value of None was allowed (and the default), but now that's not allowed, so we should guard against it. More guards would be against duck-typing. Wow. There were TWO self.__app is not None assertions. --- quamash/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index 86acdff..327987d 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -162,6 +162,9 @@ class QEventLoop(_baseclass): """ Implementation of asyncio event loop that uses the Qt Event loop. + Parameters: + :app: Any instance of QApplication + >>> import asyncio >>> >>> app = getfixture('application') @@ -180,8 +183,9 @@ class QEventLoop(_baseclass): def __init__(self, app): self.__timers = [] + if app is None: + raise ValueError("app must be an instance of QApplication") self.__app = app - assert self.__app is not None, 'No QApplication has been instantiated' self.__is_running = False self.__debug_enabled = False self.__default_executor = None @@ -195,8 +199,6 @@ def __init__(self, app): 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__() def run_forever(self): From e660d42c1def5eef24104a63a282d0aa2f2144fd Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Mon, 19 Jan 2015 12:19:20 -0800 Subject: [PATCH 08/11] A parent shouldn't inherit properties from it's children. It's crime against nature. --- quamash/__init__.py | 6 +++--- quamash/_unix.py | 3 ++- quamash/_windows.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index 327987d..ee02f76 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -193,14 +193,14 @@ def __init__(self, app): self._read_notifiers = {} self._write_notifiers = {} - self._qtcore = import_module('..QtCore', type(app).__module__) + qtcore = import_module('..QtCore', type(app).__module__) + + super().__init__(qtcore) 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)) - super().__init__() - def run_forever(self): """Run eventloop forever.""" self.__is_running = True diff --git a/quamash/_unix.py b/quamash/_unix.py index c7a2a99..7c0a223 100644 --- a/quamash/_unix.py +++ b/quamash/_unix.py @@ -186,7 +186,8 @@ def _key_from_fd(self, fd): class _SelectorEventLoop(asyncio.SelectorEventLoop): - def __init__(self): + def __init__(self, qtcore): + self._qtcore = qtcore self._signal_safe_callbacks = [] selector = _Selector(self) diff --git a/quamash/_windows.py b/quamash/_windows.py index e621f88..2a613a0 100644 --- a/quamash/_windows.py +++ b/quamash/_windows.py @@ -25,7 +25,8 @@ class _ProactorEventLoop(asyncio.ProactorEventLoop): """Proactor based event loop.""" - def __init__(self): + def __init__(self, qtcore): + self._qtcore = qtcore super().__init__(_IocpProactor()) self.__event_signaller = _make_signaller(self._qtcore, list) From abb907a0a49838ec9c3dc4994d32ebc8c8c2c3f5 Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Mon, 19 Jan 2015 14:27:09 -0800 Subject: [PATCH 09/11] Only capture necesssary exceptions This way if signal_class() raises AttributeError it won't be caught. --- quamash/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index ee02f76..1c4fdd1 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -141,13 +141,17 @@ def __exit__(self, *args): def _make_signaller(qtimpl_qtcore, *args): + try: + signal_class = qtimpl_qtcore.Signal + except AttributeError: + signal_class = qtimpl_qtcore.pyqtSignal + class Signaller(qtimpl_qtcore.QObject): - try: - signal = qtimpl_qtcore.Signal(*args) - except AttributeError: - signal = qtimpl_qtcore.pyqtSignal(*args) + signal = signal_class(*args) + return Signaller() + if os.name == 'nt': from . import _windows _baseclass = _windows.baseclass From 9b0e646f8e6ef358185a380b8f1ffba52425c337 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 20 Jan 2015 00:09:00 +0100 Subject: [PATCH 10/11] Simplify QThreadWorker --- quamash/__init__.py | 90 +++++++++++++++++---------------------- tests/test_qeventloop.py | 2 +- tests/test_qthreadexec.py | 6 +-- 3 files changed, 44 insertions(+), 54 deletions(-) diff --git a/quamash/__init__.py b/quamash/__init__.py index 1c4fdd1..e945d85 100644 --- a/quamash/__init__.py +++ b/quamash/__init__.py @@ -24,52 +24,32 @@ warnings.warn("QUAMASH_QTIMPL environment variable set, this version of quamash ignores it.") -@with_logger -class _QThreadWorker: - - """ - Read from the queue. - - For use by the QThreadExecutor - """ - - def __init__(self, queue, num): - self.__queue = queue - self.__stop = False - self.__num = num - super().__init__() - - def run(self): - queue = self.__queue - while True: - command = queue.get() - if command is None: - # Stopping... - break - - future, callback, args, kwargs = command - self._logger.debug( - '#{} got callback {} with args {} and kwargs {} from queue' - .format(self.__num, callback, args, kwargs) - ) - if future.set_running_or_notify_cancel(): - self._logger.debug('Invoking callback') - try: - r = callback(*args, **kwargs) - except Exception as err: - self._logger.debug('Setting Future exception: {}'.format(err)) - future.set_exception(err) - else: - self._logger.debug('Setting Future result: {}'.format(r)) - future.set_result(r) +def _run_in_worker(queue, num, logger): + while True: + command = queue.get() + if command is None: + # Stopping... + break + + future, callback, args, kwargs = command + logger.debug( + '#{} got callback {} with args {} and kwargs {} from queue' + .format(num, callback, args, kwargs) + ) + if future.set_running_or_notify_cancel(): + logger.debug('Invoking callback') + try: + r = callback(*args, **kwargs) + except Exception as err: + logger.debug('Setting Future exception: {}'.format(err)) + future.set_exception(err) else: - self._logger.debug('Future was canceled') - - self._logger.debug('Thread #{} stopped'.format(self.__num)) + logger.debug('Setting Future result: {}'.format(r)) + future.set_result(r) + else: + logger.debug('Future was canceled') - def wait(self): - self._logger.debug('Waiting for thread #{} to stop...'.format(self.__num)) - super().wait() + logger.debug('Thread #{} stopped'.format(num)) @with_logger @@ -82,18 +62,28 @@ class QThreadExecutor: >>> from quamash import QThreadExecutor >>> QtCore = getfixture('qtcore') - >>> with QThreadExecutor(QtCore.QThread, 5) as executor: + >>> with QThreadExecutor(QtCore, 5) as executor: ... f = executor.submit(lambda x: 2 + x, 2) ... r = f.result() ... assert r == 4 """ - def __init__(self, qthread_class, max_workers=10): + def __init__(self, qtcore, max_workers=10): super().__init__() - assert isinstance(qthread_class, type) - class QThreadWorker(_QThreadWorker, qthread_class): - pass + @with_logger + class QThreadWorker(qtcore.QThread): + def __init__(self, queue, num): + super().__init__() + self.__queue = queue + self.__num = num + + def run(self): + _run_in_worker(self.__queue, self.__num, self._logger) + + def wait(self): + self._logger.debug('Waiting for thread #{} to stop...'.format(self.__num)) + super().wait() self.__max_workers = max_workers self.__queue = Queue() @@ -438,7 +428,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(self._qtcore.QThread) + executor = self.__default_executor = QThreadExecutor(self._qtcore) self._logger.debug('Using default executor') return asyncio.wrap_future(executor.submit(callback, *args)) diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index 9f6fac9..cb283c4 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -78,7 +78,7 @@ def executor(request, qtcore): if exc_cls is None: return None elif exc_cls is quamash.QThreadExecutor: - exc = exc_cls(qtcore.QThread) + exc = exc_cls(qtcore) else: exc = exc_cls(1) # FIXME? fixed number of workers? diff --git a/tests/test_qthreadexec.py b/tests/test_qthreadexec.py index 6a4ad5b..b9c21aa 100644 --- a/tests/test_qthreadexec.py +++ b/tests/test_qthreadexec.py @@ -7,14 +7,14 @@ @pytest.fixture def executor(request, qtcore): - exe = quamash.QThreadExecutor(qtcore.QThread, 5) + exe = quamash.QThreadExecutor(qtcore, 5) request.addfinalizer(exe.shutdown) return exe @pytest.fixture def shutdown_executor(qtcore): - exe = quamash.QThreadExecutor(qtcore.QThread, 5) + exe = quamash.QThreadExecutor(qtcore, 5) exe.shutdown() return exe @@ -42,7 +42,7 @@ def test_run_in_executor_without_loop(executor): def test_run_in_executor_as_ctx_manager(qtcore): - with quamash.QThreadExecutor(qtcore.QThread) as executor: + with quamash.QThreadExecutor(qtcore) as executor: f = executor.submit(lambda x: 2 + x, 2) r = f.result() assert r == 4 From 79a6458c4d86e87095980401fae03913558fa454 Mon Sep 17 00:00:00 2001 From: Mark Harviston Date: Mon, 19 Jan 2015 15:09:28 -0800 Subject: [PATCH 11/11] Guess qtimpl, but only for tests. Prefer PyQt5, then PyQt4, then PySide --- conftest.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index c221d7d..0a96761 100644 --- a/conftest.py +++ b/conftest.py @@ -14,13 +14,26 @@ def pytest_addoption(parser): - parser.addoption("--qtimpl", default='PySide') + parser.addoption("--qtimpl", default='guess') + + +def guess_qtimpl(): + for guess in ('PyQt5', 'PyQt4', 'PySide'): + try: + __import__(guess) + except ImportError: + continue + else: + return guess @fixture(scope='session') def application(request): qtimpl = request.config.getoption('qtimpl') + if qtimpl == 'guess': + qtimpl = guess_qtimpl() __import__(qtimpl) + for module in ('.QtWidgets', '.QtGui'): try: return import_module(module, qtimpl).QApplication([]) @@ -31,5 +44,8 @@ def application(request): @fixture(scope='session') def qtcore(request): qtimpl = request.config.getoption('qtimpl') + if qtimpl == 'guess': + qtimpl = guess_qtimpl() __import__(qtimpl) + return import_module('.QtCore', qtimpl)