Skip to content
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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ install:
build: off

test_script:
- "%PYTHON%\\Scripts\\py.test.exe"
- "%PYTHON%\\Scripts\\py.test.exe --qtimpl %QTIMPL%"

notifications:
- provider: Webhook
Expand Down
23 changes: 20 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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)
138 changes: 37 additions & 101 deletions quamash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Copy link
Collaborator

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.

Copy link
Owner Author

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.

Copy link
Collaborator

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.

super().__init__()
assert isinstance(qthread_class, type)

class QThreadWorker(_QThreadWorker, qthread_class):
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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())

Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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).

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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__()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions quamash/_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from asyncio import selectors
import collections

from . import QtCore, with_logger
from . import with_logger


EVENT_READ = (1 << 0)
Expand Down Expand Up @@ -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

Expand Down
31 changes: 19 additions & 12 deletions quamash/_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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()?

Copy link
Contributor

Choose a reason for hiding this comment

The 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 _qtcore attribute. Make it part of the ("protected", in C++ terms) interface a base class for QEventLoop must implement.

(Or just document it.)

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
Selector Loop for unix are super-classes because they need to be.

It seems like you're saying QEventLoop should pass qtcore up to it's parent
(which will be different depending on platform) and then IOCP Loop or
Selector loop class should set it's own _qtcore? then QEventLoop inherits
that _qtcore.

That feels like more spaghetti, not less. Maybe if there was a shared
baseclass, but that would mean adding a new class for one protected
variable.

OTOH it does violate some OOP principle (and laws of nature) that a parent
can't inherit properties from it's children.

Anyway after some consideration I've made those changes.

On Mon, Jan 19, 2015 at 12:01 PM, Jonas Wielicki notifications@github.com
wrote:

In quamash/_windows.py
#29 (diff):

  •   self.__event_poller = _EventPoller()
    
  •   self.__event_poller.sig_events.connect(self._process_events)
    
  •   self.__event_signaller = _make_signaller(self._qtcore, list)
    

I personally would go for passing it through super() and let the base
class deal with setting the _qtcore attribute. Make it part of the
("protected", in C++ terms) interface a base class for QEventLoop must
implement.

(Or just document it.)


Reply to this email directly or view it on GitHub
https://github.com/harvimt/quamash/pull/29/files#r23183138.

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."""
Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand Down
Loading