Skip to content

Add support for qt5reactor #16

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

Merged
merged 20 commits into from
Mar 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ __pycache__/
/dist/
/.tox/
/README.html
.idea/
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ python:
- "3.5"
- "3.6"

addons:
apt:
sources:
# https://bugreports.qt.io/browse/QTBUG-64928?focusedCommentId=389951&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-389951
- sourceline: "deb http://us.archive.ubuntu.com/ubuntu/ xenial main universe"
packages:
- xvfb

install:
- pip install tox
- export TOX_ENV=`tox --listenvs | grep "py${TRAVIS_PYTHON_VERSION/./}" | tr '\n' ','`
Expand Down
14 changes: 14 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ Using the plugin
The plugin is available after installation and can be disabled using
``-p no:twisted``.

By default ``twisted.internet.default`` is used to install the reactor.
This creates the same reactor that ``import twisted.internet.reactor``
would. Alternative reactors can be specified using the ``--reactor``
option. Presently only ``qt5reactor`` is supported for use with
``pyqt5`` and ``pytest-qt``.

The reactor is automatically created prior to the first test but can
be explicitly installed earlier by calling
``pytest_twisted.init_default_reactor()`` or the corresponding function
for the desired alternate reactor. Beware that in situations such as
a ``conftest.py`` file that the name ``pytest_twisted`` may be
undesirably detected by ``pytest`` as an unknown hook. One alternative
is to ``import pytest_twisted as pt``.


inlineCallbacks
=================
Expand Down
2 changes: 2 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ install:
build: off

test_script:
- where tox
- tox --version
- tox

# https://www.appveyor.com/docs/how-to/rdp-to-build-worker/
Expand Down
157 changes: 123 additions & 34 deletions pytest_twisted.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import inspect

import decorator
import greenlet
import pytest

from twisted.internet import defer, reactor
from twisted.internet import error, defer
from twisted.internet.threads import blockingCallFromThread
from twisted.python import failure

gr_twisted = None

class WrongReactorAlreadyInstalledError(Exception):
pass


class _instances:
gr_twisted = None
reactor = None


def pytest_namespace():
return dict(inlineCallbacks=inlineCallbacks, blockon=blockon)


def blockon(d):
if reactor.running:
if _instances.reactor.running:
return block_from_thread(d)

return blockon_default(d)


def blockon_default(d):
current = greenlet.getcurrent()
assert current is not gr_twisted, \
"blockon cannot be called from the twisted greenlet"
assert current is not _instances.gr_twisted, \
'blockon cannot be called from the twisted greenlet'
result = []

def cb(r):
Expand All @@ -29,8 +42,8 @@ def cb(r):

d.addCallbacks(cb, cb)
if not result:
_result = gr_twisted.switch()
assert _result is result, "illegal switch in blockon"
_result = _instances.gr_twisted.switch()
assert _result is result, 'illegal switch in blockon'

if isinstance(result[0], failure.Failure):
result[0].raiseException()
Expand All @@ -39,37 +52,28 @@ def cb(r):


def block_from_thread(d):
return blockingCallFromThread(reactor, lambda x: x, d)
return blockingCallFromThread(_instances.reactor, lambda x: x, d)


@decorator.decorator
def inlineCallbacks(fun, *args, **kw):
return defer.inlineCallbacks(fun)(*args, **kw)


def pytest_namespace():
return dict(inlineCallbacks=inlineCallbacks,
blockon=blockon)


def stop_twisted_greenlet():
if gr_twisted:
reactor.stop()
gr_twisted.switch()

def init_twisted_greenlet():
if _instances.reactor is None:
return

def pytest_addhooks(pluginmanager):
global gr_twisted
if not gr_twisted and not reactor.running:
gr_twisted = greenlet.greenlet(reactor.run)
if not _instances.gr_twisted and not _instances.reactor.running:
_instances.gr_twisted = greenlet.greenlet(_instances.reactor.run)
# give me better tracebacks:
failure.Failure.cleanFailure = lambda self: None


@pytest.fixture(scope="session", autouse=True)
def twisted_greenlet(request):
request.addfinalizer(stop_twisted_greenlet)
return gr_twisted
def stop_twisted_greenlet():
if _instances.gr_twisted:
_instances.reactor.stop()
_instances.gr_twisted.switch()


def _pytest_pyfunc_call(pyfuncitem):
Expand All @@ -78,7 +82,7 @@ def _pytest_pyfunc_call(pyfuncitem):
return testfunction(*pyfuncitem._args)
else:
funcargs = pyfuncitem.funcargs
if hasattr(pyfuncitem, "_fixtureinfo"):
if hasattr(pyfuncitem, '_fixtureinfo'):
testargs = {}
for arg in pyfuncitem._fixtureinfo.argnames:
testargs[arg] = funcargs[arg]
Expand All @@ -88,18 +92,103 @@ def _pytest_pyfunc_call(pyfuncitem):


def pytest_pyfunc_call(pyfuncitem):
if gr_twisted is not None:
if gr_twisted.dead:
raise RuntimeError("twisted reactor has stopped")
if _instances.gr_twisted is not None:
if _instances.gr_twisted.dead:
raise RuntimeError('twisted reactor has stopped')

def in_reactor(d, f, *args):
return defer.maybeDeferred(f, *args).chainDeferred(d)

d = defer.Deferred()
reactor.callLater(0.0, in_reactor, d, _pytest_pyfunc_call, pyfuncitem)
_instances.reactor.callLater(
0.0, in_reactor, d, _pytest_pyfunc_call, pyfuncitem
)
blockon_default(d)
else:
if not reactor.running:
raise RuntimeError("twisted reactor is not running")
blockingCallFromThread(reactor, _pytest_pyfunc_call, pyfuncitem)
if not _instances.reactor.running:
raise RuntimeError('twisted reactor is not running')
blockingCallFromThread(
_instances.reactor, _pytest_pyfunc_call, pyfuncitem
)
return True


@pytest.fixture(scope="session", autouse=True)
def twisted_greenlet(request, reactor):
request.addfinalizer(stop_twisted_greenlet)
return _instances.gr_twisted


def init_default_reactor():
import twisted.internet.default

module = inspect.getmodule(twisted.internet.default.install)

module_name = module.__name__.split('.')[-1]
reactor_type_name, = (
x
for x in dir(module)
if x.lower() == module_name
)
reactor_type = getattr(module, reactor_type_name)

_install_reactor(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems super hacky but I wasn't sure how else to get the type that the default reactor would be without constructing it (or changing this part of twisted, which I think would be good).

reactor_installer=twisted.internet.default.install,
reactor_type=reactor_type,
)


def init_qt5_reactor(qapp):
import qt5reactor

_install_reactor(
reactor_installer=qt5reactor.install,
reactor_type=qt5reactor.QtReactor,
)


_reactor_fixtures = {
'default': init_default_reactor,
'qt5reactor': init_qt5_reactor,
}


def _init_reactor():
import twisted.internet.reactor
_instances.reactor = twisted.internet.reactor
init_twisted_greenlet()


def _install_reactor(reactor_installer, reactor_type):
try:
reactor_installer()
except error.ReactorAlreadyInstalledError:
import twisted.internet.reactor
if not isinstance(twisted.internet.reactor, reactor_type):
raise WrongReactorAlreadyInstalledError(
'expected {0} but found {1}'.format(
reactor_type,
type(twisted.internet.reactor),
)
)
_init_reactor()


def pytest_addoption(parser):
group = parser.getgroup('twisted')
group.addoption(
'--reactor',
default='default',
choices=tuple(_reactor_fixtures.keys()),
)


def pytest_configure(config):
reactor_fixture = _reactor_fixtures[config.getoption('reactor')]

class ReactorPlugin(object):
reactor = staticmethod(
pytest.fixture(scope='session', autouse=True)(reactor_fixture)
)

config.pluginmanager.register(ReactorPlugin())
Loading