├── .circleci └── config.yml ├── .flake8 ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── asyncqt ├── __init__.py ├── _common.py ├── _unix.py └── _windows.py ├── examples ├── aiohttp_fetch.py └── executor_example.py ├── requirements-dev.txt ├── setup.py └── tests ├── conftest.py ├── test_qeventloop.py └── test_qthreadexec.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | test35: &test-template 4 | working_directory: ~/app 5 | docker: 6 | - image: circleci/python:3.5 7 | 8 | steps: 9 | - checkout 10 | 11 | - restore_cache: 12 | key: pip-{{ checksum "requirements-dev.txt" }} 13 | 14 | - run: pip install -r requirements-dev.txt 15 | - run: pip install -e . 16 | - run: pip install PyQt5 PySide2 17 | 18 | - save_cache: 19 | key: pip-{{ checksum "requirements-dev.txt" }} 20 | paths: 21 | - ~/.cache/pip 22 | 23 | - run: flake8 asyncqt 24 | 25 | - run: flake8 tests 26 | 27 | - run: | 28 | mkdir test-reports 29 | QT_QPA_PLATFORM="offscreen" QT_API=PyQt5 xvfb-run pytest -vv --cov=asyncqt --junitxml=test-reports/pyqt5.xml 30 | QT_QPA_PLATFORM="offscreen" QT_API=PySide2 xvfb-run pytest -vv --cov=asyncqt --junitxml=test-reports/pyside2.xml 31 | 32 | - store_test_results: 33 | path: test-reports 34 | 35 | - run: codecov 36 | 37 | test36: 38 | <<: *test-template 39 | docker: 40 | - image: circleci/python:3.6 41 | 42 | test37: 43 | <<: *test-template 44 | docker: 45 | - image: circleci/python:3.7 46 | 47 | test38: 48 | <<: *test-template 49 | docker: 50 | - image: circleci/python:3.8 51 | 52 | package: 53 | working_directory: ~/app 54 | docker: 55 | - image: circleci/python:3.5 56 | 57 | steps: 58 | - checkout 59 | - run: python setup.py sdist 60 | 61 | - store_artifacts: 62 | path: dist/ 63 | 64 | - persist_to_workspace: 65 | root: ~/app 66 | paths: . 67 | 68 | deploy-gh: 69 | working_directory: ~/app 70 | docker: 71 | - image: cibuilds/github:latest 72 | 73 | steps: 74 | - attach_workspace: 75 | at: ~/app 76 | - run: 77 | name: "publish GitHub release" 78 | command: ghr ${CIRCLE_TAG} dist 79 | 80 | workflows: 81 | version: 2 82 | asyncqt-ci: 83 | jobs: 84 | - test35: 85 | filters: 86 | tags: 87 | only: /.*/ 88 | - test36: 89 | filters: 90 | tags: 91 | only: /.*/ 92 | - test37: 93 | filters: 94 | tags: 95 | only: /.*/ 96 | - test38: 97 | filters: 98 | tags: 99 | only: /.*/ 100 | - package: 101 | filters: 102 | tags: 103 | only: /.*/ 104 | - deploy-gh: 105 | requires: 106 | - test35 107 | - test36 108 | - test37 109 | - test38 110 | - package 111 | filters: 112 | tags: 113 | only: /^v.*/ 114 | branches: 115 | ignore: /.*/ 116 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = __pycache__ 3 | max-line-length = 120 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | .venv 3 | __pycache__ 4 | 5 | # Packages 6 | *.egg 7 | *.egg-info 8 | dist 9 | build 10 | 11 | # Unit test / coverage reports 12 | .coverage 13 | coverage.xml 14 | 15 | .cache/ 16 | htmlcov/ 17 | 18 | # VSCode 19 | .vscode 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Gerard Marull-Paretas 2 | Copyright (c) 2014-2018, Mark Harviston, Arve Knudsen 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | asyncqt - asyncio + PyQt5/PySide2 2 | ================================= 3 | 4 | .. image:: https://circleci.com/gh/gmarull/asyncqt.svg?style=svg 5 | :target: https://circleci.com/gh/gmarull/asyncqt 6 | 7 | .. image:: https://ci.appveyor.com/api/projects/status/s74qrypga40somf1?svg=true 8 | :target: https://ci.appveyor.com/project/gmarull/asyncqt 9 | :alt: Build Status 10 | 11 | .. image:: https://codecov.io/gh/gmarull/asyncqt/branch/master/graph/badge.svg 12 | :target: https://codecov.io/gh/gmarull/asyncqt 13 | :alt: Coverage 14 | 15 | .. image:: https://img.shields.io/pypi/v/asyncqt.svg 16 | :target: https://pypi.python.org/pypi/asyncqt 17 | :alt: PyPI Version 18 | 19 | .. image:: https://img.shields.io/conda/vn/conda-forge/asyncqt.svg 20 | :target: https://anaconda.org/conda-forge/asyncqt 21 | :alt: Conda Version 22 | 23 | **IMPORTANT: This project is unmaintained. Use other alternatives such as https://github.com/CabbageDevelopment/qasync** 24 | 25 | ``asyncqt`` is an implementation of the ``PEP 3156`` event-loop with Qt. This 26 | package is a fork of ``quamash`` focusing on modern Python versions, with 27 | some extra utilities, examples and simplified CI. 28 | 29 | Requirements 30 | ============ 31 | 32 | ``asyncqt`` requires Python >= 3.5 and PyQt5 or PySide2. The Qt API can be 33 | explicitly set by using the ``QT_API`` environment variable. 34 | 35 | Installation 36 | ============ 37 | 38 | ``pip install asyncqt`` 39 | 40 | Examples 41 | ======== 42 | 43 | You can find usage examples in the ``examples`` folder. 44 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python35" 4 | - PYTHON: "C:\\Python36" 5 | - PYTHON: "C:\\Python37" 6 | - PYTHON: "C:\\Python38" 7 | 8 | init: 9 | - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% 10 | 11 | install: 12 | - pip install -r requirements-dev.txt 13 | - pip install -e . 14 | - pip install PyQt5 PySide2 15 | - choco install codecov 16 | 17 | build: off 18 | 19 | test_script: 20 | - set QT_API=PyQt5&& pytest -v --cov=asyncqt --cov-report xml 21 | - set QT_API=PySide2&& pytest -v --cov=asyncqt --cov-report xml 22 | 23 | on_success: 24 | - codecov -f coverage.xml 25 | -------------------------------------------------------------------------------- /asyncqt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the PEP 3156 Event-Loop with Qt. 3 | 4 | Copyright (c) 2018 Gerard Marull-Paretas 5 | Copyright (c) 2014 Mark Harviston 6 | Copyright (c) 2014 Arve Knudsen 7 | 8 | BSD License 9 | """ 10 | 11 | __author__ = ('Gerard Marull-Paretas , ' 12 | 'Mark Harviston , ' 13 | 'Arve Knudsen ') 14 | __version__ = '0.9.0.dev0' 15 | __url__ = 'https://github.com/gmarull/asyncqt' 16 | __license__ = 'BSD' 17 | __all__ = ['QEventLoop', 'QThreadExecutor', 'asyncSlot', 'asyncClose'] 18 | 19 | import sys 20 | import os 21 | import asyncio 22 | import time 23 | import itertools 24 | from queue import Queue 25 | from concurrent.futures import Future 26 | import logging 27 | import importlib 28 | import functools 29 | 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | try: 35 | QtModuleName = os.environ['QT_API'] 36 | except KeyError: 37 | QtModule = None 38 | else: 39 | if QtModuleName == 'pyqt5': 40 | QtModuleName = 'PyQt5' 41 | elif QtModuleName == 'pyside2': 42 | QtModuleName = 'PySide2' 43 | 44 | logger.info('Forcing use of {} as Qt Implementation'.format(QtModuleName)) 45 | QtModule = importlib.import_module(QtModuleName) 46 | 47 | if not QtModule: 48 | for QtModuleName in ('PyQt5', 'PySide2'): 49 | try: 50 | QtModule = importlib.import_module(QtModuleName) 51 | except ImportError: 52 | continue 53 | else: 54 | break 55 | else: 56 | raise ImportError('No Qt implementations found') 57 | 58 | logger.info('Using Qt Implementation: {}'.format(QtModuleName)) 59 | 60 | QtCore = importlib.import_module(QtModuleName + '.QtCore', package=QtModuleName) 61 | QtGui = importlib.import_module(QtModuleName + '.QtGui', package=QtModuleName) 62 | if QtModuleName == 'PyQt5': 63 | from PyQt5 import QtWidgets 64 | from PyQt5.QtCore import pyqtSlot as Slot 65 | QApplication = QtWidgets.QApplication 66 | elif QtModuleName == 'PySide2': 67 | from PySide2 import QtWidgets 68 | from PySide2.QtCore import Slot 69 | QApplication = QtWidgets.QApplication 70 | 71 | 72 | from ._common import with_logger # noqa 73 | 74 | 75 | @with_logger 76 | class _QThreadWorker(QtCore.QThread): 77 | 78 | """ 79 | Read jobs from the queue and then execute them. 80 | 81 | For use by the QThreadExecutor 82 | """ 83 | 84 | def __init__(self, queue, num): 85 | self.__queue = queue 86 | self.__stop = False 87 | self.__num = num 88 | super().__init__() 89 | 90 | def run(self): 91 | queue = self.__queue 92 | while True: 93 | command = queue.get() 94 | if command is None: 95 | # Stopping... 96 | break 97 | 98 | future, callback, args, kwargs = command 99 | self._logger.debug( 100 | '#{} got callback {} with args {} and kwargs {} from queue' 101 | .format(self.__num, callback, args, kwargs), 102 | ) 103 | if future.set_running_or_notify_cancel(): 104 | self._logger.debug('Invoking callback') 105 | try: 106 | r = callback(*args, **kwargs) 107 | except Exception as err: 108 | self._logger.debug('Setting Future exception: {}'.format(err)) 109 | future.set_exception(err) 110 | else: 111 | self._logger.debug('Setting Future result: {}'.format(r)) 112 | future.set_result(r) 113 | else: 114 | self._logger.debug('Future was canceled') 115 | 116 | self._logger.debug('Thread #{} stopped'.format(self.__num)) 117 | 118 | def wait(self): 119 | self._logger.debug('Waiting for thread #{} to stop...'.format(self.__num)) 120 | super().wait() 121 | 122 | 123 | @with_logger 124 | class QThreadExecutor: 125 | 126 | """ 127 | ThreadExecutor that produces QThreads. 128 | 129 | Same API as `concurrent.futures.Executor` 130 | 131 | >>> from asyncqt import QThreadExecutor 132 | >>> with QThreadExecutor(5) as executor: 133 | ... f = executor.submit(lambda x: 2 + x, 2) 134 | ... r = f.result() 135 | ... assert r == 4 136 | """ 137 | 138 | def __init__(self, max_workers=10): 139 | super().__init__() 140 | self.__max_workers = max_workers 141 | self.__queue = Queue() 142 | self.__workers = [_QThreadWorker(self.__queue, i + 1) for i in range(max_workers)] 143 | self.__been_shutdown = False 144 | 145 | for w in self.__workers: 146 | w.start() 147 | 148 | def submit(self, callback, *args, **kwargs): 149 | if self.__been_shutdown: 150 | raise RuntimeError("QThreadExecutor has been shutdown") 151 | 152 | future = Future() 153 | self._logger.debug( 154 | 'Submitting callback {} with args {} and kwargs {} to thread worker queue' 155 | .format(callback, args, kwargs)) 156 | self.__queue.put((future, callback, args, kwargs)) 157 | return future 158 | 159 | def map(self, func, *iterables, timeout=None): 160 | raise NotImplementedError("use as_completed on the event loop") 161 | 162 | def shutdown(self, wait=True): 163 | if self.__been_shutdown: 164 | raise RuntimeError("QThreadExecutor has been shutdown") 165 | 166 | self.__been_shutdown = True 167 | 168 | self._logger.debug('Shutting down') 169 | for i in range(len(self.__workers)): 170 | # Signal workers to stop 171 | self.__queue.put(None) 172 | if wait: 173 | for w in self.__workers: 174 | w.wait() 175 | 176 | def __enter__(self, *args): 177 | if self.__been_shutdown: 178 | raise RuntimeError("QThreadExecutor has been shutdown") 179 | return self 180 | 181 | def __exit__(self, *args): 182 | self.shutdown() 183 | 184 | 185 | def _make_signaller(qtimpl_qtcore, *args): 186 | class Signaller(qtimpl_qtcore.QObject): 187 | try: 188 | signal = qtimpl_qtcore.Signal(*args) 189 | except AttributeError: 190 | signal = qtimpl_qtcore.pyqtSignal(*args) 191 | return Signaller() 192 | 193 | 194 | @with_logger 195 | class _SimpleTimer(QtCore.QObject): 196 | def __init__(self): 197 | super().__init__() 198 | self.__callbacks = {} 199 | self._stopped = False 200 | 201 | def add_callback(self, handle, delay=0): 202 | timerid = self.startTimer(delay * 1000) 203 | self._logger.debug("Registering timer id {0}".format(timerid)) 204 | assert timerid not in self.__callbacks 205 | self.__callbacks[timerid] = handle 206 | return handle 207 | 208 | def timerEvent(self, event): # noqa: N802 209 | timerid = event.timerId() 210 | self._logger.debug("Timer event on id {0}".format(timerid)) 211 | if self._stopped: 212 | self._logger.debug("Timer stopped, killing {}".format(timerid)) 213 | self.killTimer(timerid) 214 | del self.__callbacks[timerid] 215 | else: 216 | try: 217 | handle = self.__callbacks[timerid] 218 | except KeyError as e: 219 | self._logger.debug(str(e)) 220 | pass 221 | else: 222 | if handle._cancelled: 223 | self._logger.debug("Handle {} cancelled".format(handle)) 224 | else: 225 | self._logger.debug("Calling handle {}".format(handle)) 226 | handle._run() 227 | finally: 228 | del self.__callbacks[timerid] 229 | handle = None 230 | self.killTimer(timerid) 231 | 232 | def stop(self): 233 | self._logger.debug("Stopping timers") 234 | self._stopped = True 235 | 236 | 237 | @with_logger 238 | class _QEventLoop: 239 | 240 | """ 241 | Implementation of asyncio event loop that uses the Qt Event loop. 242 | 243 | >>> import asyncio 244 | >>> 245 | >>> app = getfixture('application') 246 | >>> 247 | >>> async def xplusy(x, y): 248 | ... await asyncio.sleep(.1) 249 | ... assert x + y == 4 250 | ... await asyncio.sleep(.1) 251 | >>> 252 | >>> loop = QEventLoop(app) 253 | >>> asyncio.set_event_loop(loop) 254 | >>> with loop: 255 | ... loop.run_until_complete(xplusy(2, 2)) 256 | """ 257 | 258 | def __init__(self, app=None, set_running_loop=True): 259 | self.__app = app or QApplication.instance() 260 | assert self.__app is not None, 'No QApplication has been instantiated' 261 | self.__is_running = False 262 | self.__debug_enabled = False 263 | self.__default_executor = None 264 | self.__exception_handler = None 265 | self._read_notifiers = {} 266 | self._write_notifiers = {} 267 | self._timer = _SimpleTimer() 268 | 269 | self.__call_soon_signaller = signaller = _make_signaller(QtCore, object, tuple) 270 | self.__call_soon_signal = signaller.signal 271 | signaller.signal.connect(lambda callback, args: self.call_soon(callback, *args)) 272 | 273 | assert self.__app is not None 274 | super().__init__() 275 | 276 | if set_running_loop: 277 | asyncio.events._set_running_loop(self) 278 | 279 | def run_forever(self): 280 | """Run eventloop forever.""" 281 | self.__is_running = True 282 | self._before_run_forever() 283 | 284 | try: 285 | self._logger.debug('Starting Qt event loop') 286 | rslt = self.__app.exec_() 287 | self._logger.debug('Qt event loop ended with result {}'.format(rslt)) 288 | return rslt 289 | finally: 290 | self._after_run_forever() 291 | self.__is_running = False 292 | 293 | def run_until_complete(self, future): 294 | """Run until Future is complete.""" 295 | self._logger.debug('Running {} until complete'.format(future)) 296 | future = asyncio.ensure_future(future, loop=self) 297 | 298 | def stop(*args): self.stop() # noqa 299 | future.add_done_callback(stop) 300 | try: 301 | self.run_forever() 302 | finally: 303 | future.remove_done_callback(stop) 304 | self.__app.processEvents() # run loop one last time to process all the events 305 | if not future.done(): 306 | raise RuntimeError('Event loop stopped before Future completed.') 307 | 308 | self._logger.debug('Future {} finished running'.format(future)) 309 | return future.result() 310 | 311 | def stop(self): 312 | """Stop event loop.""" 313 | if not self.__is_running: 314 | self._logger.debug('Already stopped') 315 | return 316 | 317 | self._logger.debug('Stopping event loop...') 318 | self.__is_running = False 319 | self.__app.exit() 320 | self._logger.debug('Stopped event loop') 321 | 322 | def is_running(self): 323 | """Return True if the event loop is running, False otherwise.""" 324 | return self.__is_running 325 | 326 | def close(self): 327 | """ 328 | Release all resources used by the event loop. 329 | 330 | The loop cannot be restarted after it has been closed. 331 | """ 332 | if self.is_running(): 333 | raise RuntimeError("Cannot close a running event loop") 334 | if self.is_closed(): 335 | return 336 | 337 | self._logger.debug('Closing event loop...') 338 | if self.__default_executor is not None: 339 | self.__default_executor.shutdown() 340 | 341 | super().close() 342 | 343 | self._timer.stop() 344 | self.__app = None 345 | 346 | for notifier in itertools.chain(self._read_notifiers.values(), self._write_notifiers.values()): 347 | notifier.setEnabled(False) 348 | 349 | self._read_notifiers = None 350 | self._write_notifiers = None 351 | 352 | def call_later(self, delay, callback, *args, context=None): 353 | """Register callback to be invoked after a certain delay.""" 354 | if asyncio.iscoroutinefunction(callback): 355 | raise TypeError("coroutines cannot be used with call_later") 356 | if not callable(callback): 357 | raise TypeError('callback must be callable: {}'.format(type(callback).__name__)) 358 | 359 | self._logger.debug( 360 | 'Registering callback {} to be invoked with arguments {} after {} second(s)' 361 | .format(callback, args, delay)) 362 | 363 | if sys.version_info >= (3, 7): 364 | return self._add_callback(asyncio.Handle(callback, args, self, context=context), delay) 365 | return self._add_callback(asyncio.Handle(callback, args, self), delay) 366 | 367 | def _add_callback(self, handle, delay=0): 368 | return self._timer.add_callback(handle, delay) 369 | 370 | def call_soon(self, callback, *args, context=None): 371 | """Register a callback to be run on the next iteration of the event loop.""" 372 | return self.call_later(0, callback, *args, context=context) 373 | 374 | def call_at(self, when, callback, *args, context=None): 375 | """Register callback to be invoked at a certain time.""" 376 | return self.call_later(when - self.time(), callback, *args, context=context) 377 | 378 | def time(self): 379 | """Get time according to event loop's clock.""" 380 | return time.monotonic() 381 | 382 | def add_reader(self, fd, callback, *args): 383 | """Register a callback for when a file descriptor is ready for reading.""" 384 | self._check_closed() 385 | 386 | try: 387 | existing = self._read_notifiers[fd] 388 | except KeyError: 389 | pass 390 | else: 391 | # this is necessary to avoid race condition-like issues 392 | existing.setEnabled(False) 393 | existing.activated.disconnect() 394 | # will get overwritten by the assignment below anyways 395 | 396 | notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read) 397 | notifier.setEnabled(True) 398 | self._logger.debug('Adding reader callback for file descriptor {}'.format(fd)) 399 | notifier.activated.connect( 400 | lambda: self.__on_notifier_ready( 401 | self._read_notifiers, notifier, fd, callback, args) # noqa: C812 402 | ) 403 | self._read_notifiers[fd] = notifier 404 | 405 | def remove_reader(self, fd): 406 | """Remove reader callback.""" 407 | if self.is_closed(): 408 | return 409 | 410 | self._logger.debug('Removing reader callback for file descriptor {}'.format(fd)) 411 | try: 412 | notifier = self._read_notifiers.pop(fd) 413 | except KeyError: 414 | return False 415 | else: 416 | notifier.setEnabled(False) 417 | return True 418 | 419 | def add_writer(self, fd, callback, *args): 420 | """Register a callback for when a file descriptor is ready for writing.""" 421 | self._check_closed() 422 | try: 423 | existing = self._write_notifiers[fd] 424 | except KeyError: 425 | pass 426 | else: 427 | # this is necessary to avoid race condition-like issues 428 | existing.setEnabled(False) 429 | existing.activated.disconnect() 430 | # will get overwritten by the assignment below anyways 431 | 432 | notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Write) 433 | notifier.setEnabled(True) 434 | self._logger.debug('Adding writer callback for file descriptor {}'.format(fd)) 435 | notifier.activated.connect( 436 | lambda: self.__on_notifier_ready( 437 | self._write_notifiers, notifier, fd, callback, args) # noqa: C812 438 | ) 439 | self._write_notifiers[fd] = notifier 440 | 441 | def remove_writer(self, fd): 442 | """Remove writer callback.""" 443 | if self.is_closed(): 444 | return 445 | 446 | self._logger.debug('Removing writer callback for file descriptor {}'.format(fd)) 447 | try: 448 | notifier = self._write_notifiers.pop(fd) 449 | except KeyError: 450 | return False 451 | else: 452 | notifier.setEnabled(False) 453 | return True 454 | 455 | def __notifier_cb_wrapper(self, notifiers, notifier, fd, callback, args): 456 | # This wrapper gets called with a certain delay. We cannot know 457 | # for sure that the notifier is still the current notifier for 458 | # the fd. 459 | if notifiers.get(fd, None) is not notifier: 460 | return 461 | try: 462 | callback(*args) 463 | finally: 464 | # The notifier might have been overriden by the 465 | # callback. We must not re-enable it in that case. 466 | if notifiers.get(fd, None) is notifier: 467 | notifier.setEnabled(True) 468 | else: 469 | notifier.activated.disconnect() 470 | 471 | def __on_notifier_ready(self, notifiers, notifier, fd, callback, args): 472 | if fd not in notifiers: 473 | self._logger.warning( 474 | 'Socket notifier for fd {} is ready, even though it should be disabled, not calling {} and disabling' 475 | .format(fd, callback), 476 | ) 477 | notifier.setEnabled(False) 478 | return 479 | 480 | # It can be necessary to disable QSocketNotifier when e.g. checking 481 | # ZeroMQ sockets for events 482 | assert notifier.isEnabled() 483 | self._logger.debug('Socket notifier for fd {} is ready'.format(fd)) 484 | notifier.setEnabled(False) 485 | self.call_soon( 486 | self.__notifier_cb_wrapper, 487 | notifiers, notifier, fd, callback, args) 488 | 489 | # Methods for interacting with threads. 490 | 491 | def call_soon_threadsafe(self, callback, *args, context=None): 492 | """Thread-safe version of call_soon.""" 493 | self.__call_soon_signal.emit(callback, args) 494 | 495 | def run_in_executor(self, executor, callback, *args): 496 | """Run callback in executor. 497 | 498 | If no executor is provided, the default executor will be used, which defers execution to 499 | a background thread. 500 | """ 501 | self._logger.debug('Running callback {} with args {} in executor'.format(callback, args)) 502 | if isinstance(callback, asyncio.Handle): 503 | assert not args 504 | assert not isinstance(callback, asyncio.TimerHandle) 505 | if callback._cancelled: 506 | f = asyncio.Future() 507 | f.set_result(None) 508 | return f 509 | callback, args = callback.callback, callback.args 510 | 511 | if executor is None: 512 | self._logger.debug('Using default executor') 513 | executor = self.__default_executor 514 | 515 | if executor is None: 516 | self._logger.debug('Creating default executor') 517 | executor = self.__default_executor = QThreadExecutor() 518 | 519 | return asyncio.wrap_future(executor.submit(callback, *args)) 520 | 521 | def set_default_executor(self, executor): 522 | self.__default_executor = executor 523 | 524 | # Error handlers. 525 | 526 | def set_exception_handler(self, handler): 527 | self.__exception_handler = handler 528 | 529 | def default_exception_handler(self, context): 530 | """Handle exceptions. 531 | 532 | This is the default exception handler. 533 | 534 | This is called when an exception occurs and no exception 535 | handler is set, and can be called by a custom exception 536 | handler that wants to defer to the default behavior. 537 | 538 | context parameter has the same meaning as in 539 | `call_exception_handler()`. 540 | """ 541 | self._logger.debug('Default exception handler executing') 542 | message = context.get('message') 543 | if not message: 544 | message = 'Unhandled exception in event loop' 545 | 546 | try: 547 | exception = context['exception'] 548 | except KeyError: 549 | exc_info = False 550 | else: 551 | exc_info = (type(exception), exception, exception.__traceback__) 552 | 553 | log_lines = [message] 554 | for key in [k for k in sorted(context) if k not in {'message', 'exception'}]: 555 | log_lines.append('{}: {!r}'.format(key, context[key])) 556 | 557 | self.__log_error('\n'.join(log_lines), exc_info=exc_info) 558 | 559 | def call_exception_handler(self, context): 560 | if self.__exception_handler is None: 561 | try: 562 | self.default_exception_handler(context) 563 | except Exception: 564 | # Second protection layer for unexpected errors 565 | # in the default implementation, as well as for subclassed 566 | # event loops with overloaded "default_exception_handler". 567 | self.__log_error('Exception in default exception handler', exc_info=True) 568 | 569 | return 570 | 571 | try: 572 | self.__exception_handler(self, context) 573 | except Exception as exc: 574 | # Exception in the user set custom exception handler. 575 | try: 576 | # Let's try the default handler. 577 | self.default_exception_handler({ 578 | 'message': 'Unhandled error in custom exception handler', 579 | 'exception': exc, 580 | 'context': context, 581 | }) 582 | except Exception: 583 | # Guard 'default_exception_handler' in case it's 584 | # overloaded. 585 | self.__log_error( 586 | 'Exception in default exception handler while handling an unexpected error ' 587 | 'in custom exception handler', exc_info=True) 588 | 589 | # Debug flag management. 590 | 591 | def get_debug(self): 592 | return self.__debug_enabled 593 | 594 | def set_debug(self, enabled): 595 | super().set_debug(enabled) 596 | self.__debug_enabled = enabled 597 | 598 | def __enter__(self): 599 | return self 600 | 601 | def __exit__(self, *args): 602 | self.stop() 603 | self.close() 604 | 605 | @classmethod 606 | def __log_error(cls, *args, **kwds): 607 | # In some cases, the error method itself fails, don't have a lot of options in that case 608 | try: 609 | cls._logger.error(*args, **kwds) 610 | except: # noqa E722 611 | sys.stderr.write('{!r}, {!r}\n'.format(args, kwds)) 612 | 613 | 614 | from ._unix import _SelectorEventLoop # noqa 615 | QSelectorEventLoop = type('QSelectorEventLoop', (_QEventLoop, _SelectorEventLoop), {}) 616 | 617 | if os.name == 'nt': 618 | from ._windows import _ProactorEventLoop 619 | QIOCPEventLoop = type('QIOCPEventLoop', (_QEventLoop, _ProactorEventLoop), {}) 620 | QEventLoop = QIOCPEventLoop 621 | else: 622 | QEventLoop = QSelectorEventLoop 623 | 624 | 625 | class _Cancellable: 626 | def __init__(self, timer, loop): 627 | self.__timer = timer 628 | self.__loop = loop 629 | 630 | def cancel(self): 631 | self.__timer.stop() 632 | 633 | 634 | def asyncClose(fn): 635 | """Allow to run async code before application is closed.""" 636 | @functools.wraps(fn) 637 | def wrapper(*args, **kwargs): 638 | f = asyncio.ensure_future(fn(*args, **kwargs)) 639 | while not f.done(): 640 | QApplication.instance().processEvents() 641 | 642 | return wrapper 643 | 644 | 645 | def asyncSlot(*args): 646 | """Make a Qt async slot run on asyncio loop.""" 647 | def outer_decorator(fn): 648 | @Slot(*args) 649 | @functools.wraps(fn) 650 | def wrapper(*args, **kwargs): 651 | return asyncio.ensure_future(fn(*args, **kwargs)) 652 | return wrapper 653 | return outer_decorator 654 | -------------------------------------------------------------------------------- /asyncqt/_common.py: -------------------------------------------------------------------------------- 1 | # © 2018 Gerard Marull-Paretas 2 | # © 2014 Mark Harviston 3 | # © 2014 Arve Knudsen 4 | # BSD License 5 | 6 | """Mostly irrelevant, but useful utilities common to UNIX and Windows.""" 7 | import logging 8 | 9 | 10 | def with_logger(cls): 11 | """Class decorator to add a logger to a class.""" 12 | attr_name = '_logger' 13 | cls_name = cls.__qualname__ 14 | module = cls.__module__ 15 | if module is not None: 16 | cls_name = module + '.' + cls_name 17 | else: 18 | raise AssertionError 19 | setattr(cls, attr_name, logging.getLogger(cls_name)) 20 | return cls 21 | -------------------------------------------------------------------------------- /asyncqt/_unix.py: -------------------------------------------------------------------------------- 1 | # © 2018 Gerard Marull-Paretas 2 | # © 2014 Mark Harviston 3 | # © 2014 Arve Knudsen 4 | # BSD License 5 | 6 | """UNIX specific Quamash functionality.""" 7 | 8 | import asyncio 9 | import selectors 10 | import collections 11 | 12 | from . import QtCore, with_logger 13 | 14 | 15 | EVENT_READ = (1 << 0) 16 | EVENT_WRITE = (1 << 1) 17 | 18 | 19 | def _fileobj_to_fd(fileobj): 20 | """ 21 | Return a file descriptor from a file object. 22 | 23 | Parameters: 24 | fileobj -- file object or file descriptor 25 | 26 | Returns: 27 | corresponding file descriptor 28 | 29 | Raises: 30 | ValueError if the object is invalid 31 | 32 | """ 33 | if isinstance(fileobj, int): 34 | fd = fileobj 35 | else: 36 | try: 37 | fd = int(fileobj.fileno()) 38 | except (AttributeError, TypeError, ValueError) as ex: 39 | raise ValueError("Invalid file object: {!r}".format(fileobj)) from ex 40 | if fd < 0: 41 | raise ValueError("Invalid file descriptor: {}".format(fd)) 42 | return fd 43 | 44 | 45 | class _SelectorMapping(collections.abc.Mapping): 46 | 47 | """Mapping of file objects to selector keys.""" 48 | 49 | def __init__(self, selector): 50 | self._selector = selector 51 | 52 | def __len__(self): 53 | return len(self._selector._fd_to_key) 54 | 55 | def __getitem__(self, fileobj): 56 | try: 57 | fd = self._selector._fileobj_lookup(fileobj) 58 | return self._selector._fd_to_key[fd] 59 | except KeyError: 60 | raise KeyError("{!r} is not registered".format(fileobj)) from None 61 | 62 | def __iter__(self): 63 | return iter(self._selector._fd_to_key) 64 | 65 | 66 | @with_logger 67 | class _Selector(selectors.BaseSelector): 68 | def __init__(self, parent): 69 | # this maps file descriptors to keys 70 | self._fd_to_key = {} 71 | # read-only mapping returned by get_map() 72 | self.__map = _SelectorMapping(self) 73 | self.__read_notifiers = {} 74 | self.__write_notifiers = {} 75 | self.__parent = parent 76 | 77 | def select(self, *args, **kwargs): 78 | """Implement abstract method even though we don't need it.""" 79 | raise NotImplementedError 80 | 81 | def _fileobj_lookup(self, fileobj): 82 | """Return a file descriptor from a file object. 83 | 84 | This wraps _fileobj_to_fd() to do an exhaustive search in case 85 | the object is invalid but we still have it in our map. This 86 | is used by unregister() so we can unregister an object that 87 | was previously registered even if it is closed. It is also 88 | used by _SelectorMapping. 89 | """ 90 | try: 91 | return _fileobj_to_fd(fileobj) 92 | except ValueError: 93 | # Do an exhaustive search. 94 | for key in self._fd_to_key.values(): 95 | if key.fileobj is fileobj: 96 | return key.fd 97 | # Raise ValueError after all. 98 | raise 99 | 100 | def register(self, fileobj, events, data=None): 101 | if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)): 102 | raise ValueError("Invalid events: {!r}".format(events)) 103 | 104 | key = selectors.SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data) 105 | 106 | if key.fd in self._fd_to_key: 107 | raise KeyError("{!r} (FD {}) is already registered".format(fileobj, key.fd)) 108 | 109 | self._fd_to_key[key.fd] = key 110 | 111 | if events & EVENT_READ: 112 | notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Read) 113 | notifier.activated.connect(self.__on_read_activated) 114 | self.__read_notifiers[key.fd] = notifier 115 | if events & EVENT_WRITE: 116 | notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Write) 117 | notifier.activated.connect(self.__on_write_activated) 118 | self.__write_notifiers[key.fd] = notifier 119 | 120 | return key 121 | 122 | def __on_read_activated(self, fd): 123 | self._logger.debug('File {} ready to read'.format(fd)) 124 | key = self._key_from_fd(fd) 125 | if key: 126 | self.__parent._process_event(key, EVENT_READ & key.events) 127 | 128 | def __on_write_activated(self, fd): 129 | self._logger.debug('File {} ready to write'.format(fd)) 130 | key = self._key_from_fd(fd) 131 | if key: 132 | self.__parent._process_event(key, EVENT_WRITE & key.events) 133 | 134 | def unregister(self, fileobj): 135 | def drop_notifier(notifiers): 136 | try: 137 | notifier = notifiers.pop(key.fd) 138 | except KeyError: 139 | pass 140 | else: 141 | notifier.activated.disconnect() 142 | 143 | try: 144 | key = self._fd_to_key.pop(self._fileobj_lookup(fileobj)) 145 | except KeyError: 146 | raise KeyError("{!r} is not registered".format(fileobj)) from None 147 | 148 | drop_notifier(self.__read_notifiers) 149 | drop_notifier(self.__write_notifiers) 150 | 151 | return key 152 | 153 | def modify(self, fileobj, events, data=None): 154 | try: 155 | key = self._fd_to_key[self._fileobj_lookup(fileobj)] 156 | except KeyError: 157 | raise KeyError("{!r} is not registered".format(fileobj)) from None 158 | if events != key.events: 159 | self.unregister(fileobj) 160 | key = self.register(fileobj, events, data) 161 | elif data != key.data: 162 | # Use a shortcut to update the data. 163 | key = key._replace(data=data) 164 | self._fd_to_key[key.fd] = key 165 | return key 166 | 167 | def close(self): 168 | self._logger.debug('Closing') 169 | self._fd_to_key.clear() 170 | self.__read_notifiers.clear() 171 | self.__write_notifiers.clear() 172 | 173 | def get_map(self): 174 | return self.__map 175 | 176 | def _key_from_fd(self, fd): 177 | """ 178 | Return the key associated to a given file descriptor. 179 | 180 | Parameters: 181 | fd -- file descriptor 182 | 183 | Returns: 184 | corresponding key, or None if not found 185 | 186 | """ 187 | try: 188 | return self._fd_to_key[fd] 189 | except KeyError: 190 | return None 191 | 192 | 193 | class _SelectorEventLoop(asyncio.SelectorEventLoop): 194 | def __init__(self): 195 | self._signal_safe_callbacks = [] 196 | 197 | selector = _Selector(self) 198 | asyncio.SelectorEventLoop.__init__(self, selector) 199 | 200 | def _before_run_forever(self): 201 | pass 202 | 203 | def _after_run_forever(self): 204 | pass 205 | 206 | def _process_event(self, key, mask): 207 | """Selector has delivered us an event.""" 208 | self._logger.debug('Processing event with key {} and mask {}'.format(key, mask)) 209 | fileobj, (reader, writer) = key.fileobj, key.data 210 | if mask & selectors.EVENT_READ and reader is not None: 211 | if reader._cancelled: 212 | self.remove_reader(fileobj) 213 | else: 214 | self._logger.debug('Invoking reader callback: {}'.format(reader)) 215 | reader._run() 216 | if mask & selectors.EVENT_WRITE and writer is not None: 217 | if writer._cancelled: 218 | self.remove_writer(fileobj) 219 | else: 220 | self._logger.debug('Invoking writer callback: {}'.format(writer)) 221 | writer._run() 222 | -------------------------------------------------------------------------------- /asyncqt/_windows.py: -------------------------------------------------------------------------------- 1 | # © 2018 Gerard Marull-Paretas 2 | # © 2014 Mark Harviston 3 | # © 2014 Arve Knudsen 4 | # BSD License 5 | 6 | """Windows specific Quamash functionality.""" 7 | 8 | import asyncio 9 | import sys 10 | 11 | try: 12 | import _winapi 13 | from asyncio import windows_events 14 | import _overlapped 15 | except ImportError: # noqa 16 | pass # w/o guarding this import py.test can't gather doctests on platforms w/o _winapi 17 | 18 | import math 19 | 20 | from . import QtCore, _make_signaller 21 | from ._common import with_logger 22 | 23 | UINT32_MAX = 0xffffffff 24 | 25 | 26 | class _ProactorEventLoop(asyncio.ProactorEventLoop): 27 | 28 | """Proactor based event loop.""" 29 | 30 | def __init__(self): 31 | super().__init__(_IocpProactor()) 32 | 33 | self.__event_signaller = _make_signaller(QtCore, list) 34 | self.__event_signal = self.__event_signaller.signal 35 | self.__event_signal.connect(self._process_events) 36 | self.__event_poller = _EventPoller(self.__event_signal) 37 | 38 | def _process_events(self, events): 39 | """Process events from proactor.""" 40 | for f, callback, transferred, key, ov in events: 41 | try: 42 | self._logger.debug('Invoking event callback {}'.format(callback)) 43 | value = callback(transferred, key, ov) 44 | except OSError as e: 45 | self._logger.warning('Event callback failed', exc_info=sys.exc_info()) 46 | if not f.done(): 47 | f.set_exception(e) 48 | else: 49 | if not f.cancelled(): 50 | f.set_result(value) 51 | 52 | def _before_run_forever(self): 53 | self.__event_poller.start(self._proactor) 54 | 55 | def _after_run_forever(self): 56 | self.__event_poller.stop() 57 | 58 | 59 | @with_logger 60 | class _IocpProactor(windows_events.IocpProactor): 61 | def __init__(self): 62 | self.__events = [] 63 | super(_IocpProactor, self).__init__() 64 | self._lock = QtCore.QMutex() 65 | 66 | def select(self, timeout=None): 67 | """Override in order to handle events in a threadsafe manner.""" 68 | if not self.__events: 69 | self._poll(timeout) 70 | tmp = self.__events 71 | self.__events = [] 72 | return tmp 73 | 74 | def close(self): 75 | self._logger.debug('Closing') 76 | super(_IocpProactor, self).close() 77 | 78 | def recv(self, conn, nbytes, flags=0): 79 | with QtCore.QMutexLocker(self._lock): 80 | return super(_IocpProactor, self).recv(conn, nbytes, flags) 81 | 82 | def send(self, conn, buf, flags=0): 83 | with QtCore.QMutexLocker(self._lock): 84 | return super(_IocpProactor, self).send(conn, buf, flags) 85 | 86 | def _poll(self, timeout=None): 87 | """Override in order to handle events in a threadsafe manner.""" 88 | if timeout is None: 89 | ms = UINT32_MAX # wait for eternity 90 | elif timeout < 0: 91 | raise ValueError("negative timeout") 92 | else: 93 | # GetQueuedCompletionStatus() has a resolution of 1 millisecond, 94 | # round away from zero to wait *at least* timeout seconds. 95 | ms = math.ceil(timeout * 1e3) 96 | if ms >= UINT32_MAX: 97 | raise ValueError("timeout too big") 98 | 99 | with QtCore.QMutexLocker(self._lock): 100 | while True: 101 | # self._logger.debug('Polling IOCP with timeout {} ms in thread {}...'.format( 102 | # ms, threading.get_ident())) 103 | status = _overlapped.GetQueuedCompletionStatus(self._iocp, ms) 104 | if status is None: 105 | break 106 | 107 | err, transferred, key, address = status 108 | try: 109 | f, ov, obj, callback = self._cache.pop(address) 110 | except KeyError: 111 | # key is either zero, or it is used to return a pipe 112 | # handle which should be closed to avoid a leak. 113 | if key not in (0, _overlapped.INVALID_HANDLE_VALUE): 114 | _winapi.CloseHandle(key) 115 | ms = 0 116 | continue 117 | 118 | if obj in self._stopped_serving: 119 | f.cancel() 120 | # Futures might already be resolved or cancelled 121 | elif not f.done(): 122 | self.__events.append((f, callback, transferred, key, ov)) 123 | 124 | ms = 0 125 | 126 | def _wait_for_handle(self, handle, timeout, _is_cancel): 127 | with QtCore.QMutexLocker(self._lock): 128 | return super(_IocpProactor, self)._wait_for_handle(handle, timeout, _is_cancel) 129 | 130 | def accept(self, listener): 131 | with QtCore.QMutexLocker(self._lock): 132 | return super(_IocpProactor, self).accept(listener) 133 | 134 | def connect(self, conn, address): 135 | with QtCore.QMutexLocker(self._lock): 136 | return super(_IocpProactor, self).connect(conn, address) 137 | 138 | 139 | @with_logger 140 | class _EventWorker(QtCore.QThread): 141 | def __init__(self, proactor, parent): 142 | super().__init__() 143 | 144 | self.__stop = False 145 | self.__proactor = proactor 146 | self.__sig_events = parent.sig_events 147 | self.__semaphore = QtCore.QSemaphore() 148 | 149 | def start(self): 150 | super().start() 151 | self.__semaphore.acquire() 152 | 153 | def stop(self): 154 | self.__stop = True 155 | # Wait for thread to end 156 | self.wait() 157 | 158 | def run(self): 159 | self._logger.debug('Thread started') 160 | self.__semaphore.release() 161 | 162 | while not self.__stop: 163 | events = self.__proactor.select(0.01) 164 | if events: 165 | self._logger.debug('Got events from poll: {}'.format(events)) 166 | self.__sig_events.emit(events) 167 | 168 | self._logger.debug('Exiting thread') 169 | 170 | 171 | @with_logger 172 | class _EventPoller: 173 | 174 | """Polling of events in separate thread.""" 175 | 176 | def __init__(self, sig_events): 177 | self.sig_events = sig_events 178 | 179 | def start(self, proactor): 180 | self._logger.debug('Starting (proactor: {})...'.format(proactor)) 181 | self.__worker = _EventWorker(proactor, self) 182 | self.__worker.start() 183 | 184 | def stop(self): 185 | self._logger.debug('Stopping worker thread...') 186 | self.__worker.stop() 187 | -------------------------------------------------------------------------------- /examples/aiohttp_fetch.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | 4 | import aiohttp 5 | from asyncqt import QEventLoop, asyncSlot, asyncClose 6 | 7 | # from PyQt5.QtWidgets import ( 8 | from PySide2.QtWidgets import ( 9 | QApplication, QWidget, QLabel, QLineEdit, QTextEdit, QPushButton, 10 | QVBoxLayout) 11 | 12 | 13 | class MainWindow(QWidget): 14 | """Main window.""" 15 | 16 | _DEF_URL = 'https://jsonplaceholder.typicode.com/todos/1' 17 | """str: Default URL.""" 18 | 19 | _SESSION_TIMEOUT = 1. 20 | """float: Session timeout.""" 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | self.setLayout(QVBoxLayout()) 26 | 27 | self.lblStatus = QLabel('Idle', self) 28 | self.layout().addWidget(self.lblStatus) 29 | 30 | self.editUrl = QLineEdit(self._DEF_URL, self) 31 | self.layout().addWidget(self.editUrl) 32 | 33 | self.editResponse = QTextEdit('', self) 34 | self.layout().addWidget(self.editResponse) 35 | 36 | self.btnFetch = QPushButton('Fetch', self) 37 | self.btnFetch.clicked.connect(self.on_btnFetch_clicked) 38 | self.layout().addWidget(self.btnFetch) 39 | 40 | self.session = aiohttp.ClientSession( 41 | loop=asyncio.get_event_loop(), 42 | timeout=aiohttp.ClientTimeout(total=self._SESSION_TIMEOUT)) 43 | 44 | @asyncClose 45 | async def closeEvent(self, event): 46 | await self.session.close() 47 | 48 | @asyncSlot() 49 | async def on_btnFetch_clicked(self): 50 | self.btnFetch.setEnabled(False) 51 | self.lblStatus.setText('Fetching...') 52 | 53 | try: 54 | async with self.session.get(self.editUrl.text()) as r: 55 | self.editResponse.setText(await r.text()) 56 | except Exception as exc: 57 | self.lblStatus.setText('Error: {}'.format(exc)) 58 | else: 59 | self.lblStatus.setText('Finished!') 60 | finally: 61 | self.btnFetch.setEnabled(True) 62 | 63 | 64 | if __name__ == '__main__': 65 | app = QApplication(sys.argv) 66 | loop = QEventLoop(app) 67 | asyncio.set_event_loop(loop) 68 | 69 | mainWindow = MainWindow() 70 | mainWindow.show() 71 | 72 | with loop: 73 | sys.exit(loop.run_forever()) -------------------------------------------------------------------------------- /examples/executor_example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | import time 4 | 5 | # from PyQt5.QtWidgets import ( 6 | from PySide2.QtWidgets import ( 7 | QApplication, QProgressBar) 8 | from asyncqt import QEventLoop, QThreadExecutor 9 | 10 | 11 | app = QApplication(sys.argv) 12 | loop = QEventLoop(app) 13 | asyncio.set_event_loop(loop) 14 | 15 | progress = QProgressBar() 16 | progress.setRange(0, 99) 17 | progress.show() 18 | 19 | 20 | async def master(): 21 | await first_50() 22 | with QThreadExecutor(1) as exec: 23 | await loop.run_in_executor(exec, last_50) 24 | 25 | 26 | async def first_50(): 27 | for i in range(50): 28 | progress.setValue(i) 29 | await asyncio.sleep(.1) 30 | 31 | 32 | def last_50(): 33 | for i in range(50, 100): 34 | loop.call_soon_threadsafe(progress.setValue, i) 35 | time.sleep(.1) 36 | 37 | 38 | with loop: 39 | loop.run_until_complete(master()) 40 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | pathlib2 4 | pytest-cov 5 | codecov 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import re 3 | import os.path 4 | 5 | 6 | with open('asyncqt/__init__.py') as f: 7 | version = re.search(r'__version__\s+=\s+\'(.*)\'', f.read()).group(1) 8 | 9 | 10 | desc_path = os.path.join(os.path.dirname(__file__), 'README.rst') 11 | with open(desc_path, encoding='utf8') as desc_file: 12 | long_description = desc_file.read() 13 | 14 | 15 | setup( 16 | name='asyncqt', 17 | version=version, 18 | url='https://github.com/gmarull/asyncqt', 19 | author=', '.join(('Gerard Marull-Paretas' 20 | 'Mark Harviston' 21 | 'Arve Knudsen')), 22 | author_email=', '.join(('gerard@teslabs.com', 23 | 'mark.harviston@gmail.com', 24 | 'arve.knudsen@gmail.com')), 25 | packages=['asyncqt'], 26 | license='BSD', 27 | description='Implementation of the PEP 3156 Event-Loop with Qt.', 28 | long_description=long_description, 29 | keywords=['Qt', 'asyncio'], 30 | classifiers=[ 31 | 'Development Status :: 3 - Alpha', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Intended Audience :: Developers', 34 | 'Operating System :: Microsoft :: Windows', 35 | 'Operating System :: MacOS :: MacOS X', 36 | 'Operating System :: POSIX', 37 | 'Programming Language :: Python :: 3.5', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Programming Language :: Python :: 3.7', 40 | 'Programming Language :: Python :: 3 :: Only', 41 | 'Environment :: X11 Applications :: Qt', 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # © 2018 Gerard Marull-Paretas 2 | # © 2014 Mark Harviston 3 | # © 2014 Arve Knudsen 4 | # BSD License 5 | 6 | import os 7 | import logging 8 | from pytest import fixture 9 | 10 | 11 | logging.basicConfig( 12 | level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') 13 | 14 | 15 | if os.name == 'nt': 16 | collect_ignore = ['quamash/_unix.py'] 17 | else: 18 | collect_ignore = ['quamash/_windows.py'] 19 | 20 | 21 | @fixture(scope='session') 22 | def application(): 23 | from asyncqt import QApplication 24 | return QApplication([]) 25 | -------------------------------------------------------------------------------- /tests/test_qeventloop.py: -------------------------------------------------------------------------------- 1 | # © 2018 Gerard Marull-Paretas 2 | # © 2014 Mark Harviston 3 | # © 2014 Arve Knudsen 4 | # BSD License 5 | 6 | import asyncio 7 | import logging 8 | import sys 9 | import os 10 | import ctypes 11 | import multiprocessing 12 | from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor 13 | import socket 14 | import subprocess 15 | 16 | import asyncqt 17 | 18 | import pytest 19 | 20 | 21 | @pytest.fixture 22 | def loop(request, application): 23 | lp = asyncqt.QEventLoop(application) 24 | asyncio.set_event_loop(lp) 25 | 26 | additional_exceptions = [] 27 | 28 | def fin(): 29 | sys.excepthook = orig_excepthook 30 | 31 | try: 32 | lp.close() 33 | finally: 34 | asyncio.set_event_loop(None) 35 | 36 | for exc in additional_exceptions: 37 | if ( 38 | os.name == 'nt' and 39 | isinstance(exc['exception'], WindowsError) and 40 | exc['exception'].winerror == 6 41 | ): 42 | # ignore Invalid Handle Errors 43 | continue 44 | raise exc['exception'] 45 | 46 | def except_handler(loop, ctx): 47 | additional_exceptions.append(ctx) 48 | 49 | def excepthook(type, *args): 50 | lp.stop() 51 | orig_excepthook(type, *args) 52 | 53 | orig_excepthook = sys.excepthook 54 | sys.excepthook = excepthook 55 | lp.set_exception_handler(except_handler) 56 | 57 | request.addfinalizer(fin) 58 | return lp 59 | 60 | 61 | @pytest.fixture( 62 | params=[None, asyncqt.QThreadExecutor, ThreadPoolExecutor, ProcessPoolExecutor], 63 | ) 64 | def executor(request): 65 | exc_cls = request.param 66 | if exc_cls is None: 67 | return None 68 | 69 | exc = exc_cls(1) # FIXME? fixed number of workers? 70 | request.addfinalizer(exc.shutdown) 71 | return exc 72 | 73 | 74 | ExceptionTester = type('ExceptionTester', (Exception,), {}) # to make flake8 not complain 75 | 76 | 77 | class TestCanRunTasksInExecutor: 78 | 79 | """ 80 | Test Cases Concerning running jobs in Executors. 81 | 82 | This needs to be a class because pickle can't serialize closures, 83 | but can serialize bound methods. 84 | multiprocessing can only handle pickleable functions. 85 | """ 86 | 87 | def test_can_run_tasks_in_executor(self, loop, executor): 88 | """Verify that tasks can be run in an executor.""" 89 | logging.debug('Loop: {!r}'.format(loop)) 90 | logging.debug('Executor: {!r}'.format(executor)) 91 | 92 | manager = multiprocessing.Manager() 93 | was_invoked = manager.Value(ctypes.c_int, 0) 94 | logging.debug('running until complete') 95 | loop.run_until_complete(self.blocking_task(loop, executor, was_invoked)) 96 | logging.debug('ran') 97 | 98 | assert was_invoked.value == 1 99 | 100 | def test_can_handle_exception_in_executor(self, loop, executor): 101 | with pytest.raises(ExceptionTester) as excinfo: 102 | loop.run_until_complete(asyncio.wait_for( 103 | loop.run_in_executor(executor, self.blocking_failure), 104 | timeout=3.0, 105 | )) 106 | 107 | assert str(excinfo.value) == 'Testing' 108 | 109 | def blocking_failure(self): 110 | logging.debug('raising') 111 | try: 112 | raise ExceptionTester('Testing') 113 | finally: 114 | logging.debug('raised!') 115 | 116 | def blocking_func(self, was_invoked): 117 | logging.debug('start blocking_func()') 118 | was_invoked.value = 1 119 | logging.debug('end blocking_func()') 120 | 121 | @asyncio.coroutine 122 | def blocking_task(self, loop, executor, was_invoked): 123 | logging.debug('start blocking task()') 124 | fut = loop.run_in_executor(executor, self.blocking_func, was_invoked) 125 | yield from asyncio.wait_for(fut, timeout=5.0) 126 | logging.debug('start blocking task()') 127 | 128 | 129 | def test_can_execute_subprocess(loop): 130 | """Verify that a subprocess can be executed.""" 131 | @asyncio.coroutine 132 | def mycoro(): 133 | process = yield from asyncio.create_subprocess_exec( 134 | sys.executable or 'python', '-c', 'import sys; sys.exit(5)') 135 | yield from process.wait() 136 | assert process.returncode == 5 137 | loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3)) 138 | 139 | 140 | def test_can_read_subprocess(loop): 141 | """Verify that a subprocess's data can be read from stdout.""" 142 | @asyncio.coroutine 143 | def mycoro(): 144 | process = yield from asyncio.create_subprocess_exec( 145 | sys.executable or 'python', '-c', 'print("Hello async world!")', stdout=subprocess.PIPE) 146 | received_stdout = yield from process.stdout.readexactly(len(b'Hello async world!\n')) 147 | yield from process.wait() 148 | assert process.returncode == 0 149 | assert received_stdout.strip() == b'Hello async world!' 150 | 151 | loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3)) 152 | 153 | 154 | def test_can_communicate_subprocess(loop): 155 | """Verify that a subprocess's data can be passed in/out via stdin/stdout.""" 156 | @asyncio.coroutine 157 | def mycoro(): 158 | process = yield from asyncio.create_subprocess_exec( 159 | sys.executable or 'python', '-c', 'print(input())', stdout=subprocess.PIPE, stdin=subprocess.PIPE) 160 | received_stdout, received_stderr = yield from process.communicate(b'Hello async world!\n') 161 | yield from process.wait() 162 | assert process.returncode == 0 163 | assert received_stdout.strip() == b'Hello async world!' 164 | loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3)) 165 | 166 | 167 | def test_can_terminate_subprocess(loop): 168 | """Verify that a subprocess can be terminated.""" 169 | # Start a never-ending process 170 | @asyncio.coroutine 171 | def mycoro(): 172 | process = yield from asyncio.create_subprocess_exec( 173 | sys.executable or 'python', '-c', 'import time\nwhile True: time.sleep(1)') 174 | process.terminate() 175 | yield from process.wait() 176 | assert process.returncode != 0 177 | loop.run_until_complete(mycoro()) 178 | 179 | 180 | @pytest.mark.raises(ExceptionTester) 181 | def test_loop_callback_exceptions_bubble_up(loop): 182 | """Verify that test exceptions raised in event loop callbacks bubble up.""" 183 | def raise_test_exception(): 184 | raise ExceptionTester("Test Message") 185 | loop.call_soon(raise_test_exception) 186 | loop.run_until_complete(asyncio.sleep(.1)) 187 | 188 | 189 | def test_loop_running(loop): 190 | """Verify that loop.is_running returns True when running.""" 191 | @asyncio.coroutine 192 | def is_running(): 193 | nonlocal loop 194 | assert loop.is_running() 195 | 196 | loop.run_until_complete(is_running()) 197 | 198 | 199 | def test_loop_not_running(loop): 200 | """Verify that loop.is_running returns False when not running.""" 201 | assert not loop.is_running() 202 | 203 | 204 | def test_can_function_as_context_manager(application): 205 | """Verify that a QEventLoop can function as its own context manager.""" 206 | with asyncqt.QEventLoop(application) as loop: 207 | assert isinstance(loop, asyncqt.QEventLoop) 208 | loop.call_soon(loop.stop) 209 | loop.run_forever() 210 | 211 | 212 | def test_future_not_done_on_loop_shutdown(loop): 213 | """Verify RuntimError occurs when loop stopped before Future completed with run_until_complete.""" 214 | loop.call_later(.1, loop.stop) 215 | fut = asyncio.Future() 216 | with pytest.raises(RuntimeError): 217 | loop.run_until_complete(fut) 218 | 219 | 220 | def test_call_later_must_not_coroutine(loop): 221 | """Verify TypeError occurs call_later is given a coroutine.""" 222 | mycoro = asyncio.coroutine(lambda: None) 223 | 224 | with pytest.raises(TypeError): 225 | loop.call_soon(mycoro) 226 | 227 | 228 | def test_call_later_must_be_callable(loop): 229 | """Verify TypeError occurs call_later is not given a callable.""" 230 | not_callable = object() 231 | with pytest.raises(TypeError): 232 | loop.call_soon(not_callable) 233 | 234 | 235 | def test_call_at(loop): 236 | """Verify that loop.call_at works as expected.""" 237 | def mycallback(): 238 | nonlocal was_invoked 239 | was_invoked = True 240 | was_invoked = False 241 | 242 | loop.call_at(loop.time() + .05, mycallback) 243 | loop.run_until_complete(asyncio.sleep(.1)) 244 | 245 | assert was_invoked 246 | 247 | 248 | def test_get_set_debug(loop): 249 | """Verify get_debug and set_debug work as expected.""" 250 | loop.set_debug(True) 251 | assert loop.get_debug() 252 | loop.set_debug(False) 253 | assert not loop.get_debug() 254 | 255 | 256 | @pytest.fixture 257 | def sock_pair(request): 258 | """Create socket pair. 259 | 260 | If socket.socketpair isn't available, we emulate it. 261 | """ 262 | def fin(): 263 | if client_sock is not None: 264 | client_sock.close() 265 | if srv_sock is not None: 266 | srv_sock.close() 267 | 268 | client_sock = srv_sock = None 269 | request.addfinalizer(fin) 270 | 271 | # See if socketpair() is available. 272 | have_socketpair = hasattr(socket, 'socketpair') 273 | if have_socketpair: 274 | client_sock, srv_sock = socket.socketpair() 275 | return client_sock, srv_sock 276 | 277 | # Create a non-blocking temporary server socket 278 | temp_srv_sock = socket.socket() 279 | temp_srv_sock.setblocking(False) 280 | temp_srv_sock.bind(('', 0)) 281 | port = temp_srv_sock.getsockname()[1] 282 | temp_srv_sock.listen(1) 283 | 284 | # Create non-blocking client socket 285 | client_sock = socket.socket() 286 | client_sock.setblocking(False) 287 | try: 288 | client_sock.connect(('localhost', port)) 289 | except socket.error as err: 290 | # Error 10035 (operation would block) is not an error, as we're doing this with a 291 | # non-blocking socket. 292 | if err.errno != 10035: 293 | raise 294 | 295 | # Use select to wait for connect() to succeed. 296 | import select 297 | timeout = 1 298 | readable = select.select([temp_srv_sock], [], [], timeout)[0] 299 | if temp_srv_sock not in readable: 300 | raise Exception('Client socket not connected in {} second(s)'.format(timeout)) 301 | srv_sock, _ = temp_srv_sock.accept() 302 | 303 | return client_sock, srv_sock 304 | 305 | 306 | def test_can_add_reader(loop, sock_pair): 307 | """Verify that we can add a reader callback to an event loop.""" 308 | def can_read(): 309 | if fut.done(): 310 | return 311 | 312 | data = srv_sock.recv(1) 313 | if len(data) != 1: 314 | return 315 | 316 | nonlocal got_msg 317 | got_msg = data 318 | # Indicate that we're done 319 | fut.set_result(None) 320 | srv_sock.close() 321 | 322 | def write(): 323 | client_sock.send(ref_msg) 324 | client_sock.close() 325 | 326 | ref_msg = b'a' 327 | client_sock, srv_sock = sock_pair 328 | loop.call_soon(write) 329 | 330 | exp_num_notifiers = len(loop._read_notifiers) + 1 331 | got_msg = None 332 | fut = asyncio.Future() 333 | loop.add_reader(srv_sock.fileno(), can_read) 334 | assert len(loop._read_notifiers) == exp_num_notifiers, 'Notifier should be added' 335 | loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) 336 | 337 | assert got_msg == ref_msg 338 | 339 | 340 | def test_can_remove_reader(loop, sock_pair): 341 | """Verify that we can remove a reader callback from an event loop.""" 342 | def can_read(): 343 | data = srv_sock.recv(1) 344 | if len(data) != 1: 345 | return 346 | 347 | nonlocal got_msg 348 | got_msg = data 349 | 350 | client_sock, srv_sock = sock_pair 351 | 352 | got_msg = None 353 | loop.add_reader(srv_sock.fileno(), can_read) 354 | exp_num_notifiers = len(loop._read_notifiers) - 1 355 | loop.remove_reader(srv_sock.fileno()) 356 | assert len(loop._read_notifiers) == exp_num_notifiers, 'Notifier should be removed' 357 | client_sock.send(b'a') 358 | client_sock.close() 359 | # Run for a short while to see if we get a read notification 360 | loop.call_later(0.1, loop.stop) 361 | loop.run_forever() 362 | 363 | assert got_msg is None, 'Should not have received a read notification' 364 | 365 | 366 | def test_remove_reader_after_closing(loop, sock_pair): 367 | """Verify that we can remove a reader callback from an event loop.""" 368 | client_sock, srv_sock = sock_pair 369 | 370 | loop.add_reader(srv_sock.fileno(), lambda: None) 371 | loop.close() 372 | loop.remove_reader(srv_sock.fileno()) 373 | 374 | 375 | def test_remove_writer_after_closing(loop, sock_pair): 376 | """Verify that we can remove a reader callback from an event loop.""" 377 | client_sock, srv_sock = sock_pair 378 | 379 | loop.add_writer(client_sock.fileno(), lambda: None) 380 | loop.close() 381 | loop.remove_writer(client_sock.fileno()) 382 | 383 | 384 | def test_add_reader_after_closing(loop, sock_pair): 385 | """Verify that we can remove a reader callback from an event loop.""" 386 | client_sock, srv_sock = sock_pair 387 | 388 | loop.close() 389 | with pytest.raises(RuntimeError): 390 | loop.add_reader(srv_sock.fileno(), lambda: None) 391 | 392 | 393 | def test_add_writer_after_closing(loop, sock_pair): 394 | """Verify that we can remove a reader callback from an event loop.""" 395 | client_sock, srv_sock = sock_pair 396 | 397 | loop.close() 398 | with pytest.raises(RuntimeError): 399 | loop.add_writer(client_sock.fileno(), lambda: None) 400 | 401 | 402 | def test_can_add_writer(loop, sock_pair): 403 | """Verify that we can add a writer callback to an event loop.""" 404 | def can_write(): 405 | if not fut.done(): 406 | # Indicate that we're done 407 | fut.set_result(None) 408 | client_sock.close() 409 | 410 | client_sock, _ = sock_pair 411 | fut = asyncio.Future() 412 | loop.add_writer(client_sock.fileno(), can_write) 413 | assert len(loop._write_notifiers) == 1, 'Notifier should be added' 414 | loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) 415 | 416 | 417 | def test_can_remove_writer(loop, sock_pair): 418 | """Verify that we can remove a writer callback from an event loop.""" 419 | client_sock, _ = sock_pair 420 | loop.add_writer(client_sock.fileno(), lambda: None) 421 | loop.remove_writer(client_sock.fileno()) 422 | assert not loop._write_notifiers, 'Notifier should be removed' 423 | 424 | 425 | def test_add_reader_should_disable_qsocket_notifier_on_callback(loop, sock_pair): 426 | """Verify that add_reader disables QSocketNotifier during callback.""" 427 | def can_read(): 428 | nonlocal num_calls 429 | num_calls += 1 430 | 431 | if num_calls == 2: 432 | # Since we get called again, the QSocketNotifier should've been re-enabled before 433 | # this call (although disabled during) 434 | assert not notifier.isEnabled() 435 | srv_sock.recv(1) 436 | fut.set_result(None) 437 | srv_sock.close() 438 | return 439 | 440 | assert not notifier.isEnabled() 441 | 442 | def write(): 443 | client_sock.send(b'a') 444 | client_sock.close() 445 | 446 | num_calls = 0 447 | client_sock, srv_sock = sock_pair 448 | loop.call_soon(write) 449 | 450 | fut = asyncio.Future() 451 | loop.add_reader(srv_sock.fileno(), can_read) 452 | notifier = loop._read_notifiers[srv_sock.fileno()] 453 | loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) 454 | 455 | 456 | def test_add_writer_should_disable_qsocket_notifier_on_callback(loop, sock_pair): 457 | """Verify that add_writer disables QSocketNotifier during callback.""" 458 | def can_write(): 459 | nonlocal num_calls 460 | num_calls += 1 461 | 462 | if num_calls == 2: 463 | # Since we get called again, the QSocketNotifier should've been re-enabled before 464 | # this call (although disabled during) 465 | assert not notifier.isEnabled() 466 | fut.set_result(None) 467 | client_sock.close() 468 | return 469 | 470 | assert not notifier.isEnabled() 471 | 472 | num_calls = 0 473 | client_sock, _ = sock_pair 474 | fut = asyncio.Future() 475 | loop.add_writer(client_sock.fileno(), can_write) 476 | notifier = loop._write_notifiers[client_sock.fileno()] 477 | loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) 478 | 479 | 480 | def test_reader_writer_echo(loop, sock_pair): 481 | """Verify readers and writers can send data to each other.""" 482 | c_sock, s_sock = sock_pair 483 | 484 | @asyncio.coroutine 485 | def mycoro(): 486 | c_reader, c_writer = yield from asyncio.open_connection(sock=c_sock) 487 | s_reader, s_writer = yield from asyncio.open_connection(sock=s_sock) 488 | 489 | data = b'Echo... Echo... Echo...' 490 | s_writer.write(data) 491 | yield from s_writer.drain() 492 | read_data = yield from c_reader.readexactly(len(data)) 493 | assert data == read_data 494 | s_writer.close() 495 | 496 | loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=1.0)) 497 | 498 | 499 | def test_regression_bug13(loop, sock_pair): 500 | """Verify that a simple handshake between client and server works as expected.""" 501 | c_sock, s_sock = sock_pair 502 | client_done, server_done = asyncio.Future(), asyncio.Future() 503 | 504 | @asyncio.coroutine 505 | def server_coro(): 506 | s_reader, s_writer = yield from asyncio.open_connection(sock=s_sock) 507 | 508 | s_writer.write(b'1') 509 | yield from s_writer.drain() 510 | assert (yield from s_reader.readexactly(1)) == b'2' 511 | s_writer.write(b'3') 512 | yield from s_writer.drain() 513 | server_done.set_result(True) 514 | 515 | result1 = None 516 | result3 = None 517 | 518 | @asyncio.coroutine 519 | def client_coro(): 520 | def cb1(): 521 | nonlocal result1 522 | assert result1 is None 523 | loop.remove_reader(c_sock.fileno()) 524 | result1 = c_sock.recv(1) 525 | loop.add_writer(c_sock.fileno(), cb2) 526 | 527 | def cb2(): 528 | nonlocal result3 529 | assert result3 is None 530 | c_sock.send(b'2') 531 | loop.remove_writer(c_sock.fileno()) 532 | loop.add_reader(c_sock.fileno(), cb3) 533 | 534 | def cb3(): 535 | nonlocal result3 536 | assert result3 is None 537 | result3 = c_sock.recv(1) 538 | client_done.set_result(True) 539 | 540 | loop.add_reader(c_sock.fileno(), cb1) 541 | 542 | asyncio.ensure_future(client_coro()) 543 | asyncio.ensure_future(server_coro()) 544 | 545 | both_done = asyncio.gather(client_done, server_done) 546 | loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0)) 547 | assert result1 == b'1' 548 | assert result3 == b'3' 549 | 550 | 551 | def test_add_reader_replace(loop, sock_pair): 552 | c_sock, s_sock = sock_pair 553 | callback_invoked = asyncio.Future() 554 | 555 | called1 = False 556 | called2 = False 557 | 558 | def any_callback(): 559 | if not callback_invoked.done(): 560 | callback_invoked.set_result(True) 561 | loop.remove_reader(c_sock.fileno()) 562 | 563 | def callback1(): 564 | # the "bad" callback: if this gets invoked, something went wrong 565 | nonlocal called1 566 | called1 = True 567 | any_callback() 568 | 569 | def callback2(): 570 | # the "good" callback: this is the one which should get called 571 | nonlocal called2 572 | called2 = True 573 | any_callback() 574 | 575 | @asyncio.coroutine 576 | def server_coro(): 577 | s_reader, s_writer = yield from asyncio.open_connection( 578 | sock=s_sock) 579 | s_writer.write(b"foo") 580 | yield from s_writer.drain() 581 | 582 | @asyncio.coroutine 583 | def client_coro(): 584 | loop.add_reader(c_sock.fileno(), callback1) 585 | loop.add_reader(c_sock.fileno(), callback2) 586 | yield from callback_invoked 587 | loop.remove_reader(c_sock.fileno()) 588 | assert (yield from loop.sock_recv(c_sock, 3)) == b"foo" 589 | 590 | client_done = asyncio.ensure_future(client_coro()) 591 | server_done = asyncio.ensure_future(server_coro()) 592 | 593 | both_done = asyncio.wait( 594 | [server_done, client_done], 595 | return_when=asyncio.FIRST_EXCEPTION) 596 | loop.run_until_complete(asyncio.wait_for(both_done, timeout=0.1)) 597 | assert not called1 598 | assert called2 599 | 600 | 601 | def test_add_writer_replace(loop, sock_pair): 602 | c_sock, s_sock = sock_pair 603 | callback_invoked = asyncio.Future() 604 | 605 | called1 = False 606 | called2 = False 607 | 608 | def any_callback(): 609 | if not callback_invoked.done(): 610 | callback_invoked.set_result(True) 611 | loop.remove_writer(c_sock.fileno()) 612 | 613 | def callback1(): 614 | # the "bad" callback: if this gets invoked, something went wrong 615 | nonlocal called1 616 | called1 = True 617 | any_callback() 618 | 619 | def callback2(): 620 | # the "good" callback: this is the one which should get called 621 | nonlocal called2 622 | called2 = True 623 | any_callback() 624 | 625 | @asyncio.coroutine 626 | def client_coro(): 627 | loop.add_writer(c_sock.fileno(), callback1) 628 | loop.add_writer(c_sock.fileno(), callback2) 629 | yield from callback_invoked 630 | loop.remove_writer(c_sock.fileno()) 631 | 632 | loop.run_until_complete(asyncio.wait_for(client_coro(), timeout=0.1)) 633 | assert not called1 634 | assert called2 635 | 636 | 637 | def test_remove_reader_idempotence(loop, sock_pair): 638 | fd = sock_pair[0].fileno() 639 | 640 | def cb(): 641 | pass 642 | 643 | removed0 = loop.remove_reader(fd) 644 | loop.add_reader(fd, cb) 645 | removed1 = loop.remove_reader(fd) 646 | removed2 = loop.remove_reader(fd) 647 | 648 | assert not removed0 649 | assert removed1 650 | assert not removed2 651 | 652 | 653 | def test_remove_writer_idempotence(loop, sock_pair): 654 | fd = sock_pair[0].fileno() 655 | 656 | def cb(): 657 | pass 658 | 659 | removed0 = loop.remove_writer(fd) 660 | loop.add_writer(fd, cb) 661 | removed1 = loop.remove_writer(fd) 662 | removed2 = loop.remove_writer(fd) 663 | 664 | assert not removed0 665 | assert removed1 666 | assert not removed2 667 | 668 | 669 | def test_scheduling(loop, sock_pair): 670 | s1, s2 = sock_pair 671 | fd = s1.fileno() 672 | cb_called = asyncio.Future() 673 | 674 | def writer_cb(fut): 675 | if fut.done(): 676 | cb_called.set_exception(ValueError("writer_cb called twice")) 677 | fut.set_result(None) 678 | 679 | def fut_cb(fut): 680 | loop.remove_writer(fd) 681 | cb_called.set_result(None) 682 | 683 | fut = asyncio.Future() 684 | fut.add_done_callback(fut_cb) 685 | loop.add_writer(fd, writer_cb, fut) 686 | loop.run_until_complete(cb_called) 687 | 688 | 689 | @pytest.mark.xfail( 690 | 'sys.version_info < (3,4)', 691 | reason="Doesn't work on python older than 3.4", 692 | ) 693 | def test_exception_handler(loop): 694 | handler_called = False 695 | coro_run = False 696 | loop.set_debug(True) 697 | 698 | @asyncio.coroutine 699 | def future_except(): 700 | nonlocal coro_run 701 | coro_run = True 702 | loop.stop() 703 | raise ExceptionTester() 704 | 705 | def exct_handler(loop, data): 706 | nonlocal handler_called 707 | handler_called = True 708 | 709 | loop.set_exception_handler(exct_handler) 710 | asyncio.ensure_future(future_except()) 711 | loop.run_forever() 712 | 713 | assert coro_run 714 | assert handler_called 715 | 716 | 717 | def test_exception_handler_simple(loop): 718 | handler_called = False 719 | 720 | def exct_handler(loop, data): 721 | nonlocal handler_called 722 | handler_called = True 723 | 724 | loop.set_exception_handler(exct_handler) 725 | fut1 = asyncio.Future() 726 | fut1.set_exception(ExceptionTester()) 727 | asyncio.ensure_future(fut1) 728 | del fut1 729 | loop.call_later(0.1, loop.stop) 730 | loop.run_forever() 731 | assert handler_called 732 | 733 | 734 | def test_not_running_immediately_after_stopped(loop): 735 | @asyncio.coroutine 736 | def mycoro(): 737 | assert loop.is_running() 738 | yield from asyncio.sleep(0) 739 | loop.stop() 740 | assert not loop.is_running() 741 | assert not loop.is_running() 742 | loop.run_until_complete(mycoro()) 743 | assert not loop.is_running() 744 | -------------------------------------------------------------------------------- /tests/test_qthreadexec.py: -------------------------------------------------------------------------------- 1 | # © 2018 Gerard Marull-Paretas 2 | # © 2014 Mark Harviston 3 | # © 2014 Arve Knudsen 4 | # BSD License 5 | 6 | import pytest 7 | import asyncqt 8 | 9 | 10 | @pytest.fixture 11 | def executor(request): 12 | exe = asyncqt.QThreadExecutor(5) 13 | request.addfinalizer(exe.shutdown) 14 | return exe 15 | 16 | 17 | @pytest.fixture 18 | def shutdown_executor(): 19 | exe = asyncqt.QThreadExecutor(5) 20 | exe.shutdown() 21 | return exe 22 | 23 | 24 | def test_shutdown_after_shutdown(shutdown_executor): 25 | with pytest.raises(RuntimeError): 26 | shutdown_executor.shutdown() 27 | 28 | 29 | def test_ctx_after_shutdown(shutdown_executor): 30 | with pytest.raises(RuntimeError): 31 | with shutdown_executor: 32 | pass 33 | 34 | 35 | def test_submit_after_shutdown(shutdown_executor): 36 | with pytest.raises(RuntimeError): 37 | shutdown_executor.submit(None) 38 | --------------------------------------------------------------------------------