quamash-version-0.6.0/000077500000000000000000000000001321060431600146335ustar00rootroot00000000000000quamash-version-0.6.0/.coveragerc000066400000000000000000000010711321060431600167530ustar00rootroot00000000000000[run] branch=True [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: [paths] source= /quamash/ # change to linux source path C:\quamash\ # change to windows source path quamash-version-0.6.0/.dockerignore000066400000000000000000000000641321060431600173070ustar00rootroot00000000000000*.env .tox .git __pycache__ *.pyc *.egg-info .cache quamash-version-0.6.0/.editorconfig000066400000000000000000000003441321060431600173110ustar00rootroot00000000000000# Top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true # 4 space tabbed indentation [*.py] indent_style = tab indent_size = 4quamash-version-0.6.0/.gitignore000066400000000000000000000005641321060431600166300ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 .coverage* *.env # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml .cache/ htmlcov/ # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # IDEA /.idea/ # VSCode .vscode quamash-version-0.6.0/.travis.yml000066400000000000000000000051351321060431600167500ustar00rootroot00000000000000language: python python: - "3.4" - "3.5" - "3.6" before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start # Cached Downloads - sudo mkdir -p /downloads - sudo chmod a+rw /downloads - if [ ! -f /downloads/sip.tar.gz ]; then curl -L -o /downloads/sip.tar.gz https://sourceforge.net/projects/pyqt/files/sip/sip-4.19.3/sip-4.19.3.tar.gz; fi - if [ ! -f /downloads/pyqt4.tar.gz ]; then curl -L -o /downloads/pyqt4.tar.gz https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12.1/PyQt4_gpl_x11-4.12.1.tar.gz; fi - if [ ! -f /downloads/pyqt5.tar.gz ]; then curl -L -o /downloads/pyqt5.tar.gz https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.9/PyQt5_gpl-5.9.tar.gz; fi - echo '4708187f74a4188cb4e294060707106f /downloads/sip.tar.gz' | md5sum -c - - echo '0112e15858cd7d318a09e7366922f874 /downloads/pyqt4.tar.gz' | md5sum -c - - echo 'a409ac0d65ead9178b90c2822759a84b /downloads/pyqt5.tar.gz' | md5sum -c - # Builds - sudo mkdir -p /builds - sudo chmod a+rw /builds install: # Qt4 - sudo apt-get update - sudo apt-get install -y libqt4-dev # Qt5 - sudo add-apt-repository -y ppa:beineri/opt-qt591-trusty - sudo apt-get update - sudo apt-get install -y qt59base # Builds - pushd /builds # SIP - tar xzf /downloads/sip.tar.gz --keep-newer-files - pushd sip-4.19.3 - python configure.py - make - sudo make install - popd # PyQt4 - tar xzf /downloads/pyqt4.tar.gz --keep-newer-files - pushd PyQt4_gpl_x11-4.12.1 - python configure.py -c --confirm-license --no-designer-plugin -e QtCore -e QtGui - make - sudo make install - popd # PyQt5 - source /opt/qt59/bin/qt59-env.sh # switch to Qt5 - tar xzf /downloads/pyqt5.tar.gz --keep-newer-files - pushd PyQt5_gpl-5.9 - python configure.py -c --confirm-license --no-designer-plugin -e QtCore -e QtGui -e QtWidgets - make - sudo make install - popd # Builds Complete - popd # PySide - if [ $TRAVIS_PYTHON_VERSION != '3.5' && $TRAVIS_PYTHON_VERSION != '3.6' ]; then pip install --find-links wheelhouse/ pyside; fi # flake8 style checker - pip install flake8 pep8-naming flake8-debugger flake8-docstrings pytest-timeout flake8-commas script: - flake8 - flake8 --select=D1 quamash/*.py - if [ $TRAVIS_PYTHON_VERSION != '3.5' && $TRAVIS_PYTHON_VERSION != '3.6']; then QUAMASH_QTIMPL=PySide py.test; fi - QUAMASH_QTIMPL=PyQt4 py.test - QUAMASH_QTIMPL=PyQt5 py.test cache: directories: - /downloads apt: true notifications: webhooks: urls: - https://webhooks.gitter.im/e/0ed6fa8828890c4a49ea on_success: change on_failure: always on_start: false quamash-version-0.6.0/Dockerfile000066400000000000000000000041141321060431600166250ustar00rootroot00000000000000FROM ubuntu:14.04 RUN apt-get update -y && apt-get install -y curl build-essential software-properties-common python-software-properties RUN \ sudo mkdir -p /downloads && \ sudo chmod a+rw /downloads && \ if [ ! -f /downloads/sip.tar.gz ]; then curl -L -o /downloads/sip.tar.gz https://sourceforge.net/projects/pyqt/files/sip/sip-4.19.3/sip-4.19.3.tar.gz; fi && \ if [ ! -f /downloads/pyqt4.tar.gz ]; then curl -L -o /downloads/pyqt4.tar.gz https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.12.1/PyQt4_gpl_x11-4.12.1.tar.gz; fi && \ if [ ! -f /downloads/pyqt5.tar.gz ]; then curl -L -o /downloads/pyqt5.tar.gz https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.9/PyQt5_gpl-5.9.tar.gz; fi && \ echo '4708187f74a4188cb4e294060707106f /downloads/sip.tar.gz' | md5sum -c - && \ echo '0112e15858cd7d318a09e7366922f874 /downloads/pyqt4.tar.gz' | md5sum -c - && \ echo 'a409ac0d65ead9178b90c2822759a84b /downloads/pyqt5.tar.gz' | md5sum -c - && \ sudo mkdir -p /builds && \ sudo chmod a+rw /builds && \ cd /builds && \ tar xzf /downloads/sip.tar.gz --keep-newer-files && \ tar xzf /downloads/pyqt4.tar.gz --keep-newer-files && \ tar xzf /downloads/pyqt5.tar.gz --keep-newer-files && \ sudo apt-get install -y libqt4-dev && \ sudo add-apt-repository -y ppa:beineri/opt-qt591-trusty && \ sudo add-apt-repository -y ppa:deadsnakes/ppa && \ sudo apt-get update && \ sudo apt-get install -y qt59base python3.4-dev python3.5-dev python3.6-dev SHELL ["/bin/bash", "-c"] RUN \ for python in python3.4 python3.5 python3.6; do \ cd /builds/sip-4.19.3 && \ $python configure.py && \ make clean && make && make install && \ cd /builds/PyQt4_gpl_x11-4.12.1 && \ $python configure.py -c --confirm-license --no-designer-plugin -e QtCore -e QtGui && \ make clean && make && make install && \ cd /builds/PyQt5_gpl-5.9 && \ ( \ . /opt/qt59/bin/qt59-env.sh && \ $python configure.py -c --confirm-license --no-designer-plugin -e QtCore -e QtGui -e QtWidgets && \ make clean && make && make install; \ ) \ done ADD . /quamash WORKDIR /quamash quamash-version-0.6.0/LICENSE000066400000000000000000000024621321060431600156440ustar00rootroot00000000000000Copyright (c) 2014, Mark Harviston, Arve Knudsen All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. quamash-version-0.6.0/MANIFEST.in000066400000000000000000000000331321060431600163650ustar00rootroot00000000000000include README.rst LICENSE quamash-version-0.6.0/README.rst000066400000000000000000000224301321060431600163230ustar00rootroot00000000000000======= Quamash ======= Implementation of the `PEP 3156`_ Event-Loop with Qt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :author: Mark Harviston , Arve Knudsen .. image:: https://img.shields.io/pypi/v/quamash.svg :target: https://pypi.python.org/pypi/quamash/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/quamash.svg :target: https://pypi.python.org/pypi/quamash/ :alt: Downloads .. image:: https://img.shields.io/pypi/pyversions/quamash.svg :target: https://pypi.python.org/pypi/quamash/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/l/quamash.svg :target: https://pypi.python.org/pypi/quamash/ :alt: License .. image:: https://img.shields.io/pypi/status/Django.svg :target: https://pypi.python.org/pypi/quamash/ :alt: Development Status .. image:: https://travis-ci.org/harvimt/quamash.svg?branch=master :target: https://travis-ci.org/harvimt/quamash :alt: Linux (Travis CI) Build Status .. image:: https://img.shields.io/appveyor/ci/harvimt/quamash.svg :target: https://ci.appveyor.com/project/harvimt/quamash/branch/master :alt: Windows (Appveyor) Build Status .. image:: https://badges.gitter.im/Join%20Chat.svg :target: https://gitter.im/harvimt/quamash?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge :alt: Gitter Requirements ============ Quamash requires Python 3.4 or Python 3.3 with the backported ``asyncio`` library and either PyQt4, PyQt5 or PySide. Installation ============ ``pip install quamash`` Upgrade from Version 0.4 to 0.5 =============================== The loop context manager will no longer set the event loop only close it. Instead of: .. code:: python with loop: loop.run_forever() do: .. code:: python asyncio.set_event_loop(loop) # ... with loop: loop.run_forever() It is recommended that you call ``asyncio.set_event_loop`` as early as possible (immediately after instantiating the loop), to avoid running asynchronous code before ``asyncio.set_event_loop`` is called. If you're using multiple different loops in the same application, you know what you're doing (or at least you hope you do), then you can ignore this advice. Usage ===== .. code:: python import sys import asyncio import time from PyQt5.QtWidgets import QApplication, QProgressBar from quamash import QEventLoop, QThreadExecutor app = QApplication(sys.argv) loop = QEventLoop(app) asyncio.set_event_loop(loop) # NEW must set the event loop progress = QProgressBar() progress.setRange(0, 99) progress.show() async def master(): await first_50() with QThreadExecutor(1) as exec: await loop.run_in_executor(exec, last_50) # TODO announce completion? async def first_50(): for i in range(50): progress.setValue(i) await asyncio.sleep(.1) def last_50(): for i in range(50,100): loop.call_soon_threadsafe(progress.setValue, i) time.sleep(.1) with loop: ## context manager calls .close() when loop completes, and releases all resources loop.run_until_complete(master()) Changelog ========= Version 0.6.0 ------------- * Lots of bugfixes and performance improvements. Version 0.5.5 ------------- * Fix `#62`_ a serious memory leak by switching from ``QTimer`` to ``QObject.timerEvent``. Version 0.5.4 ------------- * Remove unnecessary QObjects * Officially add Python 3.5 support (CI configuration and setup.py change) * Fix `#55`_ * Better compatibility with behavior of default event loop (`#59`_) * Remove _easycallback and replace with _makeSignaller Version 0.5.3 ------------- * Fix to `#34`_ Version 0.5.2 ------------- * Fixes to tests, and CI configuration * Fixes `#35`_ and `#31`_ (both minor bugs) * Uploade wheels to PyPI Version 0.5.1 ------------- * Fixes rst syntax error in this README Version 0.5 ----------- * Deprecation of event loop as means to ``asyncio.set_event_loop``, now must be called explicitly. * Possible fix to notifiers being called out-of-order (see #25, #27, and e64119e) * Better loop cleanup * CI Tests pass on windows now * Testing improvements * Python 3.3 Support. (probably always supported, but it's offially supported/tested now) Version 0.4.1 ------------- * Improvements to PEP-3156 Conformance * Minor Test Improvements Version 0.4 ----------- * Major improvements to tests - integration with Travis CI - more tests - all tests pass - cross platform/configuration tests * Bug #13 discovered and fixed * Force which Qt Implementation to use with ``QUQMASH_QTIMPL`` environment variable. * Implement ``QEventLoop.remove_reader`` and ``QEventLoop.remove_writer`` * PyQt4 Support * PyQt5 Support * Support ``multiprocessing`` executors (``ProcessPoolExecutor``)) * Improvements to code quality Version 0.3 ----------- First version worth using. Testing ======= Quamash is tested with pytest_; in order to run the test suite, just install pytest and execute py.test on the commandline. The tests themselves are beneath the 'tests' directory. Testing can also be done with tox_. The current tox setup in tox.ini requires PyQT4/5 and PySide to be installed globally. (pip can't install PyQt into a virtualenv which is what tox will try to do). For this reason it may be good to run tox tests while specificying which environments to run. e.g. ``tox -e py34-pyqt5`` to test python 3.4 with PyQt5. It is unlikely this tox configuration will work well on Windows especially since PyQt5 and PyQt4 cannot coexist in the same python installation on Windows. Also the PyQt4 w/ Qt5 oddity appears to be mostly a windows only thing too. Style testing is also handled by tox. Run ``tox -e flake8``. Code Coverage ------------- Getting a full coverage support is quite time consuming. In theory this could by done with `pytest-xdist`_, but I haven't had time to make that work. Install ``pytest-cov`` with ``pip install pytest-cov`` then run ``py.test --cov quamash`` then append a dot and an identifier the generated ``.coverage`` file. For example, ``mv .coverage .coverage.nix.p33.pyside`` then repeat on all the platforms you want to run on. (at least linux and windows). Put all the ``.coverage.*`` files in one directory that also has quamash source code in it. ``cd`` to that directory and run ``coverage combine`` finally run ``coverage html`` for html based reports or ``coverage report`` for a simple report. These last commands may fail with errors about not being able to find source code. Use the ``.coveragerc`` file to specify equivelant paths. The default configuration has linux source code in ``/quamash`` and windows source at ``C:\quamash``. Continuous Integration & Supported Platforms -------------------------------------------- This project uses Travis CI to perform tests on linux (Ubuntu 12.04 LTS "Precise Pangolin") and Appveyor (Windows Server 2012 R2, similar to Windows 8) to perform continuous integration. On linux, Python 3.3 and 3.4 with PySide, PyQt4, and PyQt5 are tested. On windows, Python 3.4 with PySide, PyQt4 and PyQt5 are tested, but Python 3.3 is only tested with PySide since binary installers for PyQt are not provided for Python 3.3 (at least not the newest versions of PyQt), and compiling from source probably isn't worth it. Python 3.5 is now tested on linux with PyQt4 and PyQt5. Testing Matrix ~~~~~~~~~~~~~~ +----------------------+---------+---------+--------------+----------------+ | | PyQt4 | PyQt5 | PySide (Qt4) | PySide 2 (Qt5) | +======================+=========+=========+==============+================+ | Linux - Python 3.3 | yes | yes | yes | planned | +----------------------+---------+---------+--------------+----------------+ | Linux - Python 3.4 | yes | yes | yes | planned | +----------------------+---------+---------+--------------+----------------+ | Linux - Python 3.5 | yes | yes | n/a | planned | +----------------------+---------+---------+--------------+----------------+ | Windows - Python 3.3 | no | no | yes | no | +----------------------+---------+---------+--------------+----------------+ | Windows - Python 3.4 | yes | yes | yes | planned | +----------------------+---------+---------+--------------+----------------+ | Windows - Python 3.5 | planned | planned | planned | planned | +----------------------+---------+---------+--------------+----------------+ License ======= You may use, modify, and redistribute this software under the terms of the `BSD License`_. See LICENSE. Name ==== Tulip related projects are being named after other flowers, Quamash is one of the few flowers that starts with a "Q". .. _`PEP 3156`: http://python.org/dev/peps/pep-3156/ .. _`pytest`: http://pytest.org .. _`BSD License`: http://opensource.org/licenses/BSD-2-Clause .. _tox: https://tox.readthedocs.org/ .. _pytest-xdist: https://pypi.python.org/pypi/pytest-xdist .. _#31: https://github.com/harvimt/quamash/issues/31 .. _#34: https://github.com/harvimt/quamash/issues/34 .. _#35: https://github.com/harvimt/quamash/issues/35 .. _#55: https://github.com/harvimt/quamash/issues/55 .. _#59: https://github.com/harvimt/quamash/pull/59 .. _#62: https://github.com/harvimt/quamash/pull/62 quamash-version-0.6.0/appveyor.yml000066400000000000000000000021431321060431600172230ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "64" QTIMPL: "PySide" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "64" QTIMPL: "PyQt4" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.x" PYTHON_ARCH: "64" QTIMPL: "PyQt5" - PYTHON: "C:\\Python35-x64" PYTHON_VERSION: "3.5.x" PYTHON_ARCH: "64" QTIMPL: "PyQt4" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" QTIMPL: "PyQt4" - PYTHON: "C:\\Python36-x64" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "64" QTIMPL: "PyQt5" - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.x" PYTHON_ARCH: "32" QTIMPL: "PyQt5" init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH% %QTIMPL%" install: - "powershell appveyor\\install.ps1" build: off test_script: - "%PYTHON%\\Scripts\\py.test.exe" notifications: - provider: Webhook url: https://webhooks.gitter.im/e/9723bec82b34a9f3faf0 on_build_success: false on_build_failure: True quamash-version-0.6.0/appveyor/000077500000000000000000000000001321060431600165005ustar00rootroot00000000000000quamash-version-0.6.0/appveyor/install.ps1000066400000000000000000000027771321060431600206100ustar00rootroot00000000000000# Adapted from Sample script to install Python and pip under Windows # Authors: Olivier Grisel and Kyle Kastner # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ # Adapted by Mark Harviston $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" $GET_PIP_PATH = "C:\get-pip.py" function InstallPip ($python_home) { $pip_path = $python_home + "\Scripts\pip.exe" $python_path = $python_home + "\python.exe" if (-not(Test-Path $pip_path)) { Write-Host "Installing pip..." $webclient = New-Object System.Net.WebClient $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) Write-Host "Executing:" $python_path $GET_PIP_PATH Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru } else { Write-Host "pip already installed." } } function InstallPackage ($python_home, $pkg) { $pip_path = $python_home + "\Scripts\pip.exe" & $pip_path install --only-binary=PySide --only-binary=PyQt4 --only-binary=PyQt5 --find-links .\wheelhouse $pkg } function main () { $PYTHON_MAJ_VERSION = $env:PYTHON_VERSION -replace '(\d+)\.(\d+)\.(\d+)', '$1.$2' & REG ADD "HKCU\Software\Python\PythonCore\${PYTHON_MAJ_VERSION}\InstallPath" /f /ve /t REG_SZ /d $env:PYTHON InstallPip $env:PYTHON InstallPackage $env:PYTHON wheel InstallPackage $env:PYTHON pytest switch ($env:QTIMPL){ "PySide" { InstallPackage $env:Python PySide } "PyQt4" { InstallPackage $env:Python PyQt4 } "PyQt5" { InstallPackage $env:Python PyQt5 } } } main quamash-version-0.6.0/conftest.py000066400000000000000000000016721321060431600170400ustar00rootroot00000000000000import sys import os.path import logging from pytest import fixture sys.path.insert(0, os.path.dirname(__file__)) try: import colorlog handler = colorlog.StreamHandler() formatter = colorlog.ColoredFormatter( "%(log_color)s%(levelname)-8s%(reset)s %(name)-32s %(message)s", datefmt=None, reset=True, log_colors={ 'DEBUG': 'cyan', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red,bg_white', }, secondary_log_colors={}, style='%', ) handler.setFormatter(formatter) logger = colorlog.getLogger() logger.addHandler(handler) logger.setLevel(logging.DEBUG) except ImportError: logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') if os.name == 'nt': collect_ignore = ['quamash/_unix.py'] else: collect_ignore = ['quamash/_windows.py'] @fixture(scope='session') def application(): from quamash import QApplication return QApplication([]) quamash-version-0.6.0/quamash/000077500000000000000000000000001321060431600162725ustar00rootroot00000000000000quamash-version-0.6.0/quamash/__init__.py000066400000000000000000000413401321060431600204050ustar00rootroot00000000000000# © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License """Implementation of the PEP 3156 Event-Loop with Qt.""" __author__ = 'Mark Harviston , Arve Knudsen ' __version__ = '0.6.0' __url__ = 'https://github.com/harvimt/quamash' __license__ = 'BSD' __all__ = ['QEventLoop', 'QThreadExecutor'] import sys import os import asyncio import time 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 from ._common import with_logger @with_logger class _QThreadWorker(QtCore.QThread): """ Read jobs from the queue and then execute them. For use by the QThreadExecutor """ def __init__(self, queue, num): self.__queue = queue self.__stop = False self.__num = num super().__init__() def run(self): queue = self.__queue while True: command = queue.get() if command is None: # Stopping... break future, callback, args, kwargs = command self._logger.debug( '#{} got callback {} with args {} and kwargs {} from queue' .format(self.__num, callback, args, kwargs), ) if future.set_running_or_notify_cancel(): self._logger.debug('Invoking callback') try: r = callback(*args, **kwargs) except Exception as err: self._logger.debug('Setting Future exception: {}'.format(err)) future.set_exception(err) else: self._logger.debug('Setting Future result: {}'.format(r)) future.set_result(r) else: self._logger.debug('Future was canceled') self._logger.debug('Thread #{} stopped'.format(self.__num)) def wait(self): self._logger.debug('Waiting for thread #{} to stop...'.format(self.__num)) super().wait() @with_logger class QThreadExecutor: """ ThreadExecutor that produces QThreads. Same API as `concurrent.futures.Executor` >>> from quamash import QThreadExecutor >>> with QThreadExecutor(5) as executor: ... f = executor.submit(lambda x: 2 + x, 2) ... r = f.result() ... assert r == 4 """ def __init__(self, max_workers=10): super().__init__() self.__max_workers = max_workers self.__queue = Queue() self.__workers = [_QThreadWorker(self.__queue, i + 1) for i in range(max_workers)] self.__been_shutdown = False for w in self.__workers: w.start() def submit(self, callback, *args, **kwargs): if self.__been_shutdown: raise RuntimeError("QThreadExecutor has been shutdown") future = Future() self._logger.debug( 'Submitting callback {} with args {} and kwargs {} to thread worker queue' .format(callback, args, kwargs)) self.__queue.put((future, callback, args, kwargs)) return future def map(self, func, *iterables, timeout=None): raise NotImplementedError("use as_completed on the event loop") def shutdown(self, wait=True): if self.__been_shutdown: raise RuntimeError("QThreadExecutor has been shutdown") self.__been_shutdown = True self._logger.debug('Shutting down') for i in range(len(self.__workers)): # Signal workers to stop self.__queue.put(None) if wait: for w in self.__workers: w.wait() def __enter__(self, *args): if self.__been_shutdown: raise RuntimeError("QThreadExecutor has been shutdown") return self def __exit__(self, *args): self.shutdown() def _make_signaller(qtimpl_qtcore, *args): class Signaller(qtimpl_qtcore.QObject): try: signal = qtimpl_qtcore.Signal(*args) except AttributeError: signal = qtimpl_qtcore.pyqtSignal(*args) return Signaller() @with_logger class _SimpleTimer(QtCore.QObject): def __init__(self): super().__init__() self.__callbacks = {} self._stopped = False def add_callback(self, handle, delay=0): timerid = self.startTimer(delay * 1000) self._logger.debug("Registering timer id {0}".format(timerid)) assert timerid not in self.__callbacks self.__callbacks[timerid] = handle return handle def timerEvent(self, event): # noqa: N802 timerid = event.timerId() self._logger.debug("Timer event on id {0}".format(timerid)) if self._stopped: self._logger.debug("Timer stopped, killing {}".format(timerid)) self.killTimer(timerid) del self.__callbacks[timerid] else: try: handle = self.__callbacks[timerid] except KeyError as e: self._logger.debug(str(e)) pass else: if handle._cancelled: self._logger.debug("Handle {} cancelled".format(handle)) else: self._logger.debug("Calling handle {}".format(handle)) handle._run() finally: del self.__callbacks[timerid] handle = None self.killTimer(timerid) def stop(self): self._logger.debug("Stopping timers") self._stopped = True @with_logger class _QEventLoop: """ Implementation of asyncio event loop that uses the Qt Event loop. >>> import asyncio >>> >>> app = getfixture('application') >>> >>> @asyncio.coroutine ... def xplusy(x, y): ... yield from asyncio.sleep(.1) ... assert x + y == 4 ... yield from asyncio.sleep(.1) >>> >>> loop = QEventLoop(app) >>> asyncio.set_event_loop(loop) >>> with loop: ... loop.run_until_complete(xplusy(2, 2)) """ def __init__(self, app=None): self.__app = app or QApplication.instance() assert self.__app is not None, 'No QApplication has been instantiated' self.__is_running = False self.__debug_enabled = False self.__default_executor = None self.__exception_handler = None self._read_notifiers = {} self._write_notifiers = {} self._timer = _SimpleTimer() self.__call_soon_signaller = signaller = _make_signaller(QtCore, object, tuple) self.__call_soon_signal = signaller.signal signaller.signal.connect(lambda callback, args: self.call_soon(callback, *args)) assert self.__app is not None super().__init__() def run_forever(self): """Run eventloop forever.""" self.__is_running = True self._before_run_forever() try: self._logger.debug('Starting Qt event loop') rslt = self.__app.exec_() self._logger.debug('Qt event loop ended with result {}'.format(rslt)) return rslt finally: self._after_run_forever() self.__is_running = False def run_until_complete(self, future): """Run until Future is complete.""" self._logger.debug('Running {} until complete'.format(future)) future = asyncio.async(future, loop=self) def stop(*args): self.stop() # noqa future.add_done_callback(stop) try: self.run_forever() finally: future.remove_done_callback(stop) self.__app.processEvents() # run loop one last time to process all the events if not future.done(): raise RuntimeError('Event loop stopped before Future completed.') self._logger.debug('Future {} finished running'.format(future)) return future.result() def stop(self): """Stop event loop.""" if not self.__is_running: self._logger.debug('Already stopped') return self._logger.debug('Stopping event loop...') self.__is_running = False self.__app.exit() self._logger.debug('Stopped event loop') def is_running(self): """Return True if the event loop is running, False otherwise.""" return self.__is_running def close(self): """ Release all resources used by the event loop. The loop cannot be restarted after it has been closed. """ if self.is_running(): raise RuntimeError("Cannot close a running event loop") if self.is_closed(): return self._logger.debug('Closing event loop...') if self.__default_executor is not None: self.__default_executor.shutdown() super().close() self._timer.stop() self.__app = None for notifier in itertools.chain(self._read_notifiers.values(), self._write_notifiers.values()): notifier.setEnabled(False) self._read_notifiers = None self._write_notifiers = None def call_later(self, delay, callback, *args): """Register callback to be invoked after a certain delay.""" if asyncio.iscoroutinefunction(callback): raise TypeError("coroutines cannot be used with call_later") if not callable(callback): raise TypeError('callback must be callable: {}'.format(type(callback).__name__)) self._logger.debug( 'Registering callback {} to be invoked with arguments {} after {} second(s)' .format(callback, args, delay)) return self._add_callback(asyncio.Handle(callback, args, self), delay) def _add_callback(self, handle, delay=0): return self._timer.add_callback(handle, delay) def call_soon(self, callback, *args): """Register a callback to be run on the next iteration of the event loop.""" return self.call_later(0, callback, *args) def call_at(self, when, callback, *args): """Register callback to be invoked at a certain time.""" return self.call_later(when - self.time(), callback, *args) def time(self): """Get time according to event loop's clock.""" return time.monotonic() def add_reader(self, fd, callback, *args): """Register a callback for when a file descriptor is ready for reading.""" self._check_closed() try: existing = self._read_notifiers[fd] except KeyError: pass else: # this is necessary to avoid race condition-like issues existing.setEnabled(False) existing.activated.disconnect() # will get overwritten by the assignment below anyways notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read) notifier.setEnabled(True) self._logger.debug('Adding reader callback for file descriptor {}'.format(fd)) notifier.activated.connect( lambda: self.__on_notifier_ready( self._read_notifiers, notifier, fd, callback, args) # noqa: C812 ) self._read_notifiers[fd] = notifier def remove_reader(self, fd): """Remove reader callback.""" if self.is_closed(): return self._logger.debug('Removing reader callback for file descriptor {}'.format(fd)) try: notifier = self._read_notifiers.pop(fd) except KeyError: return False else: notifier.setEnabled(False) return True def add_writer(self, fd, callback, *args): """Register a callback for when a file descriptor is ready for writing.""" self._check_closed() try: existing = self._write_notifiers[fd] except KeyError: pass else: # this is necessary to avoid race condition-like issues existing.setEnabled(False) existing.activated.disconnect() # will get overwritten by the assignment below anyways notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Write) notifier.setEnabled(True) self._logger.debug('Adding writer callback for file descriptor {}'.format(fd)) notifier.activated.connect( lambda: self.__on_notifier_ready( self._write_notifiers, notifier, fd, callback, args) # noqa: C812 ) self._write_notifiers[fd] = notifier def remove_writer(self, fd): """Remove writer callback.""" if self.is_closed(): return self._logger.debug('Removing writer callback for file descriptor {}'.format(fd)) try: notifier = self._write_notifiers.pop(fd) except KeyError: return False else: notifier.setEnabled(False) return True def __notifier_cb_wrapper(self, notifiers, notifier, fd, callback, args): # This wrapper gets called with a certain delay. We cannot know # for sure that the notifier is still the current notifier for # the fd. if notifiers.get(fd, None) is not notifier: return try: callback(*args) finally: # The notifier might have been overriden by the # callback. We must not re-enable it in that case. if notifiers.get(fd, None) is notifier: notifier.setEnabled(True) else: notifier.activated.disconnect() def __on_notifier_ready(self, notifiers, notifier, fd, callback, args): if fd not in notifiers: self._logger.warning( 'Socket notifier for fd {} is ready, even though it should be disabled, not calling {} and disabling' .format(fd, callback), ) notifier.setEnabled(False) return # It can be necessary to disable QSocketNotifier when e.g. checking # ZeroMQ sockets for events assert notifier.isEnabled() self._logger.debug('Socket notifier for fd {} is ready'.format(fd)) notifier.setEnabled(False) self.call_soon( self.__notifier_cb_wrapper, notifiers, notifier, fd, callback, args) # Methods for interacting with threads. def call_soon_threadsafe(self, callback, *args): """Thread-safe version of call_soon.""" self.__call_soon_signal.emit(callback, args) def run_in_executor(self, executor, callback, *args): """Run callback in executor. If no executor is provided, the default executor will be used, which defers execution to a background thread. """ self._logger.debug('Running callback {} with args {} in executor'.format(callback, args)) if isinstance(callback, asyncio.Handle): assert not args assert not isinstance(callback, asyncio.TimerHandle) if callback._cancelled: f = asyncio.Future() f.set_result(None) return f callback, args = callback.callback, callback.args if executor is None: self._logger.debug('Using default executor') executor = self.__default_executor if executor is None: self._logger.debug('Creating default executor') executor = self.__default_executor = QThreadExecutor() return asyncio.wrap_future(executor.submit(callback, *args)) def set_default_executor(self, executor): self.__default_executor = executor # Error handlers. def set_exception_handler(self, handler): self.__exception_handler = handler def default_exception_handler(self, context): """Handle exceptions. This is the default exception handler. This is called when an exception occurs and no exception handler is set, and can be called by a custom exception handler that wants to defer to the default behavior. context parameter has the same meaning as in `call_exception_handler()`. """ self._logger.debug('Default exception handler executing') message = context.get('message') if not message: message = 'Unhandled exception in event loop' try: exception = context['exception'] except KeyError: exc_info = False else: exc_info = (type(exception), exception, exception.__traceback__) log_lines = [message] for key in [k for k in sorted(context) if k not in {'message', 'exception'}]: log_lines.append('{}: {!r}'.format(key, context[key])) self.__log_error('\n'.join(log_lines), exc_info=exc_info) def call_exception_handler(self, context): if self.__exception_handler is None: try: self.default_exception_handler(context) except Exception: # Second protection layer for unexpected errors # in the default implementation, as well as for subclassed # event loops with overloaded "default_exception_handler". self.__log_error('Exception in default exception handler', exc_info=True) return try: self.__exception_handler(self, context) except Exception as exc: # Exception in the user set custom exception handler. try: # Let's try the default handler. self.default_exception_handler({ 'message': 'Unhandled error in custom exception handler', 'exception': exc, 'context': context, }) except Exception: # Guard 'default_exception_handler' in case it's # overloaded. self.__log_error( 'Exception in default exception handler while handling an unexpected error ' 'in custom exception handler', exc_info=True) # Debug flag management. def get_debug(self): return self.__debug_enabled def set_debug(self, enabled): super().set_debug(enabled) self.__debug_enabled = enabled def __enter__(self): return self def __exit__(self, *args): self.stop() self.close() @classmethod def __log_error(cls, *args, **kwds): # In some cases, the error method itself fails, don't have a lot of options in that case try: cls._logger.error(*args, **kwds) except: # noqa E722 sys.stderr.write('{!r}, {!r}\n'.format(args, kwds)) from ._unix import _SelectorEventLoop QSelectorEventLoop = type('QSelectorEventLoop', (_QEventLoop, _SelectorEventLoop), {}) if os.name == 'nt': from ._windows import _ProactorEventLoop QIOCPEventLoop = type('QIOCPEventLoop', (_QEventLoop, _ProactorEventLoop), {}) QEventLoop = QIOCPEventLoop else: QEventLoop = QSelectorEventLoop class _Cancellable: def __init__(self, timer, loop): self.__timer = timer self.__loop = loop def cancel(self): self.__timer.stop() quamash-version-0.6.0/quamash/_common.py000066400000000000000000000010001321060431600202620ustar00rootroot00000000000000# © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License """Mostly irrelevant, but useful utilities common to UNIX and Windows.""" import logging def with_logger(cls): """Class decorator to add a logger to a class.""" attr_name = '_logger' cls_name = cls.__qualname__ module = cls.__module__ if module is not None: cls_name = module + '.' + cls_name else: raise AssertionError setattr(cls, attr_name, logging.getLogger(cls_name)) return cls quamash-version-0.6.0/quamash/_unix.py000066400000000000000000000136441321060431600177760ustar00rootroot00000000000000# © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License """UNIX specific Quamash functionality.""" import asyncio from asyncio import selectors import collections from . import QtCore, with_logger EVENT_READ = (1 << 0) EVENT_WRITE = (1 << 1) def _fileobj_to_fd(fileobj): """ Return a file descriptor from a file object. Parameters: fileobj -- file object or file descriptor Returns: corresponding file descriptor Raises: ValueError if the object is invalid """ if isinstance(fileobj, int): fd = fileobj else: try: fd = int(fileobj.fileno()) except (AttributeError, TypeError, ValueError) as ex: raise ValueError("Invalid file object: {!r}".format(fileobj)) from ex if fd < 0: raise ValueError("Invalid file descriptor: {}".format(fd)) return fd class _SelectorMapping(collections.Mapping): """Mapping of file objects to selector keys.""" def __init__(self, selector): self._selector = selector def __len__(self): return len(self._selector._fd_to_key) def __getitem__(self, fileobj): try: fd = self._selector._fileobj_lookup(fileobj) return self._selector._fd_to_key[fd] except KeyError: raise KeyError("{!r} is not registered".format(fileobj)) from None def __iter__(self): return iter(self._selector._fd_to_key) @with_logger class _Selector(selectors.BaseSelector): def __init__(self, parent): # this maps file descriptors to keys self._fd_to_key = {} # read-only mapping returned by get_map() self.__map = _SelectorMapping(self) self.__read_notifiers = {} self.__write_notifiers = {} self.__parent = parent def select(self, *args, **kwargs): """Implement abstract method even though we don't need it.""" raise NotImplementedError def _fileobj_lookup(self, fileobj): """Return a file descriptor from a file object. This wraps _fileobj_to_fd() to do an exhaustive search in case the object is invalid but we still have it in our map. This is used by unregister() so we can unregister an object that was previously registered even if it is closed. It is also used by _SelectorMapping. """ try: return _fileobj_to_fd(fileobj) except ValueError: # Do an exhaustive search. for key in self._fd_to_key.values(): if key.fileobj is fileobj: return key.fd # Raise ValueError after all. raise def register(self, fileobj, events, data=None): if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)): raise ValueError("Invalid events: {!r}".format(events)) key = selectors.SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data) if key.fd in self._fd_to_key: raise KeyError("{!r} (FD {}) is already registered".format(fileobj, key.fd)) self._fd_to_key[key.fd] = key if events & EVENT_READ: notifier = QtCore.QSocketNotifier(key.fd, 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.activated.connect(self.__on_write_activated) self.__write_notifiers[key.fd] = notifier return key def __on_read_activated(self, fd): self._logger.debug('File {} ready to read'.format(fd)) key = self._key_from_fd(fd) if key: self.__parent._process_event(key, EVENT_READ & key.events) def __on_write_activated(self, fd): self._logger.debug('File {} ready to write'.format(fd)) key = self._key_from_fd(fd) if key: self.__parent._process_event(key, EVENT_WRITE & key.events) def unregister(self, fileobj): def drop_notifier(notifiers): try: notifier = notifiers.pop(key.fd) except KeyError: pass else: notifier.activated.disconnect() try: key = self._fd_to_key.pop(self._fileobj_lookup(fileobj)) except KeyError: raise KeyError("{!r} is not registered".format(fileobj)) from None drop_notifier(self.__read_notifiers) drop_notifier(self.__write_notifiers) return key def modify(self, fileobj, events, data=None): try: key = self._fd_to_key[self._fileobj_lookup(fileobj)] except KeyError: raise KeyError("{!r} is not registered".format(fileobj)) from None if events != key.events: self.unregister(fileobj) key = self.register(fileobj, events, data) elif data != key.data: # Use a shortcut to update the data. key = key._replace(data=data) self._fd_to_key[key.fd] = key return key def close(self): self._logger.debug('Closing') self._fd_to_key.clear() self.__read_notifiers.clear() self.__write_notifiers.clear() def get_map(self): return self.__map def _key_from_fd(self, fd): """ Return the key associated to a given file descriptor. Parameters: fd -- file descriptor Returns: corresponding key, or None if not found """ try: return self._fd_to_key[fd] except KeyError: return None class _SelectorEventLoop(asyncio.SelectorEventLoop): def __init__(self): self._signal_safe_callbacks = [] selector = _Selector(self) asyncio.SelectorEventLoop.__init__(self, selector) def _before_run_forever(self): pass def _after_run_forever(self): pass def _process_event(self, key, mask): """Selector has delivered us an event.""" self._logger.debug('Processing event with key {} and mask {}'.format(key, mask)) fileobj, (reader, writer) = key.fileobj, key.data if mask & selectors.EVENT_READ and reader is not None: if reader._cancelled: self.remove_reader(fileobj) else: self._logger.debug('Invoking reader callback: {}'.format(reader)) reader._run() if mask & selectors.EVENT_WRITE and writer is not None: if writer._cancelled: self.remove_writer(fileobj) else: self._logger.debug('Invoking writer callback: {}'.format(writer)) writer._run() quamash-version-0.6.0/quamash/_windows.py000066400000000000000000000115601321060431600205000ustar00rootroot00000000000000# © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License """Windows specific Quamash functionality.""" import asyncio import sys try: import _winapi from asyncio import windows_events from asyncio import _overlapped except ImportError: # noqa pass # w/o guarding this import py.test can't gather doctests on platforms w/o _winapi import math from . import QtCore, _make_signaller from ._common import with_logger UINT32_MAX = 0xffffffff class _ProactorEventLoop(asyncio.ProactorEventLoop): """Proactor based event loop.""" def __init__(self): super().__init__(_IocpProactor()) self.__event_signaller = _make_signaller(QtCore, list) self.__event_signal = self.__event_signaller.signal self.__event_signal.connect(self._process_events) self.__event_poller = _EventPoller(self.__event_signal) def _process_events(self, events): """Process events from proactor.""" for f, callback, transferred, key, ov in events: try: self._logger.debug('Invoking event callback {}'.format(callback)) value = callback(transferred, key, ov) except OSError: self._logger.warning('Event callback failed', exc_info=sys.exc_info()) else: f.set_result(value) def _before_run_forever(self): self.__event_poller.start(self._proactor) def _after_run_forever(self): self.__event_poller.stop() @with_logger class _IocpProactor(windows_events.IocpProactor): def __init__(self): self.__events = [] super(_IocpProactor, self).__init__() self._lock = QtCore.QMutex() def select(self, timeout=None): """Override in order to handle events in a threadsafe manner.""" if not self.__events: self._poll(timeout) tmp = self.__events self.__events = [] return tmp def close(self): self._logger.debug('Closing') super(_IocpProactor, self).close() def recv(self, conn, nbytes, flags=0): with QtCore.QMutexLocker(self._lock): return super(_IocpProactor, self).recv(conn, nbytes, flags) def send(self, conn, buf, flags=0): with QtCore.QMutexLocker(self._lock): return super(_IocpProactor, self).send(conn, buf, flags) def _poll(self, timeout=None): """Override in order to handle events in a threadsafe manner.""" if timeout is None: ms = UINT32_MAX # wait for eternity elif timeout < 0: raise ValueError("negative timeout") else: # GetQueuedCompletionStatus() has a resolution of 1 millisecond, # round away from zero to wait *at least* timeout seconds. ms = math.ceil(timeout * 1e3) if ms >= UINT32_MAX: raise ValueError("timeout too big") with QtCore.QMutexLocker(self._lock): while True: # self._logger.debug('Polling IOCP with timeout {} ms in thread {}...'.format( # ms, threading.get_ident())) status = _overlapped.GetQueuedCompletionStatus(self._iocp, ms) if status is None: break err, transferred, key, address = status try: f, ov, obj, callback = self._cache.pop(address) except KeyError: # key is either zero, or it is used to return a pipe # handle which should be closed to avoid a leak. if key not in (0, _overlapped.INVALID_HANDLE_VALUE): _winapi.CloseHandle(key) ms = 0 continue if obj in self._stopped_serving: f.cancel() # Futures might already be resolved or cancelled elif not f.done(): self.__events.append((f, callback, transferred, key, ov)) ms = 0 def _wait_for_handle(self, handle, timeout, _is_cancel): with QtCore.QMutexLocker(self._lock): return super(_IocpProactor, self)._wait_for_handle(handle, timeout, _is_cancel) def accept(self, listener): with QtCore.QMutexLocker(self._lock): return super(_IocpProactor, self).accept(listener) def connect(self, conn, address): with QtCore.QMutexLocker(self._lock): return super(_IocpProactor, self).connect(conn, address) @with_logger class _EventWorker(QtCore.QThread): def __init__(self, proactor, parent): super().__init__() self.__stop = False self.__proactor = proactor self.__sig_events = parent.sig_events self.__semaphore = QtCore.QSemaphore() def start(self): super().start() self.__semaphore.acquire() def stop(self): self.__stop = True # Wait for thread to end self.wait() def run(self): self._logger.debug('Thread started') self.__semaphore.release() while not self.__stop: events = self.__proactor.select(0.01) if events: self._logger.debug('Got events from poll: {}'.format(events)) self.__sig_events.emit(events) self._logger.debug('Exiting thread') @with_logger class _EventPoller: """Polling of events in separate thread.""" def __init__(self, sig_events): self.sig_events = sig_events def start(self, proactor): self._logger.debug('Starting (proactor: {})...'.format(proactor)) self.__worker = _EventWorker(proactor, self) self.__worker.start() def stop(self): self._logger.debug('Stopping worker thread...') self.__worker.stop() quamash-version-0.6.0/setup.cfg000066400000000000000000000001001321060431600164430ustar00rootroot00000000000000[metadata] license-file = LICENSE description-file = README.rst quamash-version-0.6.0/setup.py000066400000000000000000000023571321060431600163540ustar00rootroot00000000000000from setuptools import setup import quamash import re import os.path groups = re.findall(r'(.+?) <(.+?)>(?:,\s*)?', quamash.__author__) authors = [x[0].strip() for x in groups] emails = [x[1].strip() for x in groups] desc_path = os.path.join(os.path.dirname(__file__), 'README.rst') with open(desc_path, encoding='utf8') as desc_file: long_description = desc_file.read() setup( name='Quamash', version=quamash.__version__, url=quamash.__url__, author=', '.join(authors), author_email=', '.join(emails), packages=['quamash', ], license=quamash.__license__, description=quamash.__doc__, long_description=long_description, keywords=['Qt', 'asyncio'], classifiers=[ 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: BSD License', 'Intended Audience :: Developers', 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3 :: Only', 'Environment :: X11 Applications :: Qt', ], # FIXME depends on PyQt4, PyQt5 or PySide, but cannot put that in a setup.py extras_require={ 'test': ['pytest'], }, ) quamash-version-0.6.0/tests/000077500000000000000000000000001321060431600157755ustar00rootroot00000000000000quamash-version-0.6.0/tests/test_qeventloop.py000066400000000000000000000461251321060431600216120ustar00rootroot00000000000000# © 2013 Mark Harviston # © 2014 Arve Knudsen # BSD License import asyncio import logging import sys import os import ctypes import multiprocessing from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor import socket import subprocess import quamash import pytest @pytest.fixture def loop(request, application): lp = quamash.QEventLoop(application) asyncio.set_event_loop(lp) additional_exceptions = [] def fin(): sys.excepthook = orig_excepthook try: lp.close() finally: asyncio.set_event_loop(None) for exc in additional_exceptions: if ( os.name == 'nt' and isinstance(exc['exception'], WindowsError) and exc['exception'].winerror == 6 ): # ignore Invalid Handle Errors continue raise exc['exception'] def except_handler(loop, ctx): additional_exceptions.append(ctx) def excepthook(type, *args): lp.stop() orig_excepthook(type, *args) orig_excepthook = sys.excepthook sys.excepthook = excepthook lp.set_exception_handler(except_handler) request.addfinalizer(fin) return lp @pytest.fixture( params=[None, quamash.QThreadExecutor, ThreadPoolExecutor, ProcessPoolExecutor], ) def executor(request): exc_cls = request.param if exc_cls is None: return None exc = exc_cls(1) # FIXME? fixed number of workers? request.addfinalizer(exc.shutdown) return exc ExceptionTester = type('ExceptionTester', (Exception,), {}) # to make flake8 not complain class TestCanRunTasksInExecutor: """ Test Cases Concerning running jobs in Executors. This needs to be a class because pickle can't serialize closures, but can serialize bound methods. multiprocessing can only handle pickleable functions. """ def test_can_run_tasks_in_executor(self, loop, executor): """Verify that tasks can be run in an executor.""" logging.debug('Loop: {!r}'.format(loop)) logging.debug('Executor: {!r}'.format(executor)) manager = multiprocessing.Manager() was_invoked = manager.Value(ctypes.c_int, 0) logging.debug('running until complete') loop.run_until_complete(self.blocking_task(loop, executor, was_invoked)) logging.debug('ran') assert was_invoked.value == 1 def test_can_handle_exception_in_executor(self, loop, executor): with pytest.raises(ExceptionTester) as excinfo: loop.run_until_complete(asyncio.wait_for( loop.run_in_executor(executor, self.blocking_failure), timeout=3.0, )) assert str(excinfo.value) == 'Testing' def blocking_failure(self): logging.debug('raising') try: raise ExceptionTester('Testing') finally: logging.debug('raised!') def blocking_func(self, was_invoked): logging.debug('start blocking_func()') was_invoked.value = 1 logging.debug('end blocking_func()') @asyncio.coroutine def blocking_task(self, loop, executor, was_invoked): logging.debug('start blocking task()') fut = loop.run_in_executor(executor, self.blocking_func, was_invoked) yield from asyncio.wait_for(fut, timeout=5.0) logging.debug('start blocking task()') def test_can_execute_subprocess(loop): """Verify that a subprocess can be executed.""" @asyncio.coroutine def mycoro(): process = yield from asyncio.create_subprocess_exec( sys.executable or 'python', '-c', 'import sys; sys.exit(5)') yield from process.wait() assert process.returncode == 5 loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3)) def test_can_read_subprocess(loop): """Verify that a subprocess's data can be read from stdout.""" @asyncio.coroutine def mycoro(): process = yield from asyncio.create_subprocess_exec( sys.executable or 'python', '-c', 'print("Hello async world!")', stdout=subprocess.PIPE) received_stdout = yield from process.stdout.readexactly(len(b'Hello async world!\n')) yield from process.wait() assert process.returncode == 0 assert received_stdout.strip() == b'Hello async world!' loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3)) def test_can_communicate_subprocess(loop): """Verify that a subprocess's data can be passed in/out via stdin/stdout.""" @asyncio.coroutine def mycoro(): process = yield from asyncio.create_subprocess_exec( sys.executable or 'python', '-c', 'print(input())', stdout=subprocess.PIPE, stdin=subprocess.PIPE) received_stdout, received_stderr = yield from process.communicate(b'Hello async world!\n') yield from process.wait() assert process.returncode == 0 assert received_stdout.strip() == b'Hello async world!' loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3)) def test_can_terminate_subprocess(loop): """Verify that a subprocess can be terminated.""" # Start a never-ending process @asyncio.coroutine def mycoro(): process = yield from asyncio.create_subprocess_exec( sys.executable or 'python', '-c', 'import time\nwhile True: time.sleep(1)') process.terminate() yield from process.wait() assert process.returncode != 0 loop.run_until_complete(mycoro()) @pytest.mark.raises(ExceptionTester) def test_loop_callback_exceptions_bubble_up(loop): """Verify that test exceptions raised in event loop callbacks bubble up.""" def raise_test_exception(): raise ExceptionTester("Test Message") loop.call_soon(raise_test_exception) loop.run_until_complete(asyncio.sleep(.1)) def test_loop_running(loop): """Verify that loop.is_running returns True when running.""" @asyncio.coroutine def is_running(): nonlocal loop assert loop.is_running() loop.run_until_complete(is_running()) def test_loop_not_running(loop): """Verify that loop.is_running returns False when not running.""" assert not loop.is_running() def test_can_function_as_context_manager(application): """Verify that a QEventLoop can function as its own context manager.""" with quamash.QEventLoop(application) as loop: assert isinstance(loop, quamash.QEventLoop) loop.call_soon(loop.stop) loop.run_forever() def test_future_not_done_on_loop_shutdown(loop): """Verify RuntimError occurs when loop stopped before Future completed with run_until_complete.""" loop.call_later(.1, loop.stop) fut = asyncio.Future() with pytest.raises(RuntimeError): loop.run_until_complete(fut) def test_call_later_must_not_coroutine(loop): """Verify TypeError occurs call_later is given a coroutine.""" mycoro = asyncio.coroutine(lambda: None) with pytest.raises(TypeError): loop.call_soon(mycoro) def test_call_later_must_be_callable(loop): """Verify TypeError occurs call_later is not given a callable.""" not_callable = object() with pytest.raises(TypeError): loop.call_soon(not_callable) def test_call_at(loop): """Verify that loop.call_at works as expected.""" def mycallback(): nonlocal was_invoked was_invoked = True was_invoked = False loop.call_at(loop.time() + .05, mycallback) loop.run_until_complete(asyncio.sleep(.1)) assert was_invoked def test_get_set_debug(loop): """Verify get_debug and set_debug work as expected.""" loop.set_debug(True) assert loop.get_debug() loop.set_debug(False) assert not loop.get_debug() @pytest.fixture def sock_pair(request): """Create socket pair. If socket.socketpair isn't available, we emulate it. """ def fin(): if client_sock is not None: client_sock.close() if srv_sock is not None: srv_sock.close() client_sock = srv_sock = None request.addfinalizer(fin) # See if socketpair() is available. have_socketpair = hasattr(socket, 'socketpair') if have_socketpair: client_sock, srv_sock = socket.socketpair() return client_sock, srv_sock # Create a non-blocking temporary server socket temp_srv_sock = socket.socket() temp_srv_sock.setblocking(False) temp_srv_sock.bind(('', 0)) port = temp_srv_sock.getsockname()[1] temp_srv_sock.listen(1) # Create non-blocking client socket client_sock = socket.socket() client_sock.setblocking(False) try: client_sock.connect(('localhost', port)) except socket.error as err: # Error 10035 (operation would block) is not an error, as we're doing this with a # non-blocking socket. if err.errno != 10035: raise # Use select to wait for connect() to succeed. import select timeout = 1 readable = select.select([temp_srv_sock], [], [], timeout)[0] if temp_srv_sock not in readable: raise Exception('Client socket not connected in {} second(s)'.format(timeout)) srv_sock, _ = temp_srv_sock.accept() return client_sock, srv_sock def test_can_add_reader(loop, sock_pair): """Verify that we can add a reader callback to an event loop.""" def can_read(): if fut.done(): return data = srv_sock.recv(1) if len(data) != 1: return nonlocal got_msg got_msg = data # Indicate that we're done fut.set_result(None) srv_sock.close() def write(): client_sock.send(ref_msg) client_sock.close() ref_msg = b'a' client_sock, srv_sock = sock_pair loop.call_soon(write) exp_num_notifiers = len(loop._read_notifiers) + 1 got_msg = None fut = asyncio.Future() loop.add_reader(srv_sock.fileno(), can_read) assert len(loop._read_notifiers) == exp_num_notifiers, 'Notifier should be added' loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) assert got_msg == ref_msg def test_can_remove_reader(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" def can_read(): data = srv_sock.recv(1) if len(data) != 1: return nonlocal got_msg got_msg = data client_sock, srv_sock = sock_pair got_msg = None loop.add_reader(srv_sock.fileno(), can_read) exp_num_notifiers = len(loop._read_notifiers) - 1 loop.remove_reader(srv_sock.fileno()) assert len(loop._read_notifiers) == exp_num_notifiers, 'Notifier should be removed' client_sock.send(b'a') client_sock.close() # Run for a short while to see if we get a read notification loop.call_later(0.1, loop.stop) loop.run_forever() assert got_msg is None, 'Should not have received a read notification' def test_remove_reader_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop.add_reader(srv_sock.fileno(), lambda: None) loop.close() loop.remove_reader(srv_sock.fileno()) def test_remove_writer_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop.add_writer(client_sock.fileno(), lambda: None) loop.close() loop.remove_writer(client_sock.fileno()) def test_add_reader_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop.close() with pytest.raises(RuntimeError): loop.add_reader(srv_sock.fileno(), lambda: None) def test_add_writer_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop.close() with pytest.raises(RuntimeError): loop.add_writer(client_sock.fileno(), lambda: None) def test_can_add_writer(loop, sock_pair): """Verify that we can add a writer callback to an event loop.""" def can_write(): if not fut.done(): # Indicate that we're done fut.set_result(None) client_sock.close() client_sock, _ = sock_pair fut = asyncio.Future() loop.add_writer(client_sock.fileno(), can_write) assert len(loop._write_notifiers) == 1, 'Notifier should be added' loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) def test_can_remove_writer(loop, sock_pair): """Verify that we can remove a writer callback from an event loop.""" client_sock, _ = sock_pair loop.add_writer(client_sock.fileno(), lambda: None) loop.remove_writer(client_sock.fileno()) assert not loop._write_notifiers, 'Notifier should be removed' def test_add_reader_should_disable_qsocket_notifier_on_callback(loop, sock_pair): """Verify that add_reader disables QSocketNotifier during callback.""" def can_read(): nonlocal num_calls num_calls += 1 if num_calls == 2: # Since we get called again, the QSocketNotifier should've been re-enabled before # this call (although disabled during) assert not notifier.isEnabled() srv_sock.recv(1) fut.set_result(None) srv_sock.close() return assert not notifier.isEnabled() def write(): client_sock.send(b'a') client_sock.close() num_calls = 0 client_sock, srv_sock = sock_pair loop.call_soon(write) fut = asyncio.Future() loop.add_reader(srv_sock.fileno(), can_read) notifier = loop._read_notifiers[srv_sock.fileno()] loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) def test_add_writer_should_disable_qsocket_notifier_on_callback(loop, sock_pair): """Verify that add_writer disables QSocketNotifier during callback.""" def can_write(): nonlocal num_calls num_calls += 1 if num_calls == 2: # Since we get called again, the QSocketNotifier should've been re-enabled before # this call (although disabled during) assert not notifier.isEnabled() fut.set_result(None) client_sock.close() return assert not notifier.isEnabled() num_calls = 0 client_sock, _ = sock_pair fut = asyncio.Future() loop.add_writer(client_sock.fileno(), can_write) notifier = loop._write_notifiers[client_sock.fileno()] loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) def test_reader_writer_echo(loop, sock_pair): """Verify readers and writers can send data to each other.""" c_sock, s_sock = sock_pair @asyncio.coroutine def mycoro(): c_reader, c_writer = yield from asyncio.open_connection(sock=c_sock) s_reader, s_writer = yield from asyncio.open_connection(sock=s_sock) data = b'Echo... Echo... Echo...' s_writer.write(data) yield from s_writer.drain() read_data = yield from c_reader.readexactly(len(data)) assert data == read_data s_writer.close() loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=1.0)) def test_regression_bug13(loop, sock_pair): """Verify that a simple handshake between client and server works as expected.""" c_sock, s_sock = sock_pair client_done, server_done = asyncio.Future(), asyncio.Future() @asyncio.coroutine def server_coro(): s_reader, s_writer = yield from asyncio.open_connection(sock=s_sock) s_writer.write(b'1') yield from s_writer.drain() assert (yield from s_reader.readexactly(1)) == b'2' s_writer.write(b'3') yield from s_writer.drain() server_done.set_result(True) result1 = None result3 = None @asyncio.coroutine def client_coro(): def cb1(): nonlocal result1 assert result1 is None loop.remove_reader(c_sock.fileno()) result1 = c_sock.recv(1) loop.add_writer(c_sock.fileno(), cb2) def cb2(): nonlocal result3 assert result3 is None c_sock.send(b'2') loop.remove_writer(c_sock.fileno()) loop.add_reader(c_sock.fileno(), cb3) def cb3(): nonlocal result3 assert result3 is None result3 = c_sock.recv(1) client_done.set_result(True) loop.add_reader(c_sock.fileno(), cb1) asyncio.async(client_coro()) asyncio.async(server_coro()) both_done = asyncio.gather(client_done, server_done) loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0)) assert result1 == b'1' assert result3 == b'3' def test_add_reader_replace(loop, sock_pair): c_sock, s_sock = sock_pair callback_invoked = asyncio.Future() called1 = False called2 = False def any_callback(): if not callback_invoked.done(): callback_invoked.set_result(True) loop.remove_reader(c_sock.fileno()) def callback1(): # the "bad" callback: if this gets invoked, something went wrong nonlocal called1 called1 = True any_callback() def callback2(): # the "good" callback: this is the one which should get called nonlocal called2 called2 = True any_callback() @asyncio.coroutine def server_coro(): s_reader, s_writer = yield from asyncio.open_connection( sock=s_sock) s_writer.write(b"foo") yield from s_writer.drain() @asyncio.coroutine def client_coro(): loop.add_reader(c_sock.fileno(), callback1) loop.add_reader(c_sock.fileno(), callback2) yield from callback_invoked loop.remove_reader(c_sock.fileno()) assert (yield from loop.sock_recv(c_sock, 3)) == b"foo" client_done = asyncio.async(client_coro()) server_done = asyncio.async(server_coro()) both_done = asyncio.wait( [server_done, client_done], return_when=asyncio.FIRST_EXCEPTION) loop.run_until_complete(asyncio.wait_for(both_done, timeout=0.1)) assert not called1 assert called2 def test_add_writer_replace(loop, sock_pair): c_sock, s_sock = sock_pair callback_invoked = asyncio.Future() called1 = False called2 = False def any_callback(): if not callback_invoked.done(): callback_invoked.set_result(True) loop.remove_writer(c_sock.fileno()) def callback1(): # the "bad" callback: if this gets invoked, something went wrong nonlocal called1 called1 = True any_callback() def callback2(): # the "good" callback: this is the one which should get called nonlocal called2 called2 = True any_callback() @asyncio.coroutine def client_coro(): loop.add_writer(c_sock.fileno(), callback1) loop.add_writer(c_sock.fileno(), callback2) yield from callback_invoked loop.remove_writer(c_sock.fileno()) loop.run_until_complete(asyncio.wait_for(client_coro(), timeout=0.1)) assert not called1 assert called2 def test_remove_reader_idempotence(loop, sock_pair): fd = sock_pair[0].fileno() def cb(): pass removed0 = loop.remove_reader(fd) loop.add_reader(fd, cb) removed1 = loop.remove_reader(fd) removed2 = loop.remove_reader(fd) assert not removed0 assert removed1 assert not removed2 def test_remove_writer_idempotence(loop, sock_pair): fd = sock_pair[0].fileno() def cb(): pass removed0 = loop.remove_writer(fd) loop.add_writer(fd, cb) removed1 = loop.remove_writer(fd) removed2 = loop.remove_writer(fd) assert not removed0 assert removed1 assert not removed2 def test_scheduling(loop, sock_pair): s1, s2 = sock_pair fd = s1.fileno() cb_called = asyncio.Future() def writer_cb(fut): if fut.done(): cb_called.set_exception(ValueError("writer_cb called twice")) fut.set_result(None) def fut_cb(fut): loop.remove_writer(fd) cb_called.set_result(None) fut = asyncio.Future() fut.add_done_callback(fut_cb) loop.add_writer(fd, writer_cb, fut) loop.run_until_complete(cb_called) @pytest.mark.xfail( 'sys.version_info < (3,4)', reason="Doesn't work on python older than 3.4", ) def test_exception_handler(loop): handler_called = False coro_run = False loop.set_debug(True) @asyncio.coroutine def future_except(): nonlocal coro_run coro_run = True loop.stop() raise ExceptionTester() def exct_handler(loop, data): nonlocal handler_called handler_called = True loop.set_exception_handler(exct_handler) asyncio.async(future_except()) loop.run_forever() assert coro_run assert handler_called def test_exception_handler_simple(loop): handler_called = False def exct_handler(loop, data): nonlocal handler_called handler_called = True loop.set_exception_handler(exct_handler) fut1 = asyncio.Future() fut1.set_exception(ExceptionTester()) asyncio.async(fut1) del fut1 loop.call_later(0.1, loop.stop) loop.run_forever() assert handler_called def test_not_running_immediately_after_stopped(loop): @asyncio.coroutine def mycoro(): assert loop.is_running() yield from asyncio.sleep(0) loop.stop() assert not loop.is_running() assert not loop.is_running() loop.run_until_complete(mycoro()) assert not loop.is_running() quamash-version-0.6.0/tests/test_qthreadexec.py000066400000000000000000000013341321060431600217040ustar00rootroot00000000000000# © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License import pytest import quamash @pytest.fixture def executor(request): exe = quamash.QThreadExecutor(5) request.addfinalizer(exe.shutdown) return exe @pytest.fixture def shutdown_executor(): exe = quamash.QThreadExecutor(5) exe.shutdown() return exe def test_shutdown_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): shutdown_executor.shutdown() def test_ctx_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): with shutdown_executor: pass def test_submit_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): shutdown_executor.submit(None) quamash-version-0.6.0/tox.ini000066400000000000000000000017061321060431600161520ustar00rootroot00000000000000[tox] envlist=flake8,py36-pyqt5,{py34,py35,py36}-pyqt4,py34-pyside [testenv] install_command = pip install --only-binary=PySide --only-binary=PyQt4 --only-binary=PyQt5 --find-links .\wheelhouse {opts} {packages} sitepackages=True deps= colorlog pytest pyqt4: PyQt4 pyqt5: PyQt5 pyside: PySide commands=py.test setenv= pyqt4: QUAMASH_QTIMPL=PyQt4 pyqt5: QUAMASH_QTIMPL=PyQt5 pyside: QUAMASH_QTIMPL=PySide [pytest] addopts=-rxs --doctest-modules quamash quamash tests timeout=10 [testenv:py34-pyside] commands=-py.test [testenv:flake8] setenv=QUAMASH_QTIMPL=PyQt5 basepython=python3.6 deps= PyQt5 flake8 pep8-naming flake8-debugger flake8-docstrings flake8-commas commands= # different parameters for different parts of the project flake8 flake8 --select=D1 quamash/*.py [flake8] max-complexity=15 ignore=D1,W191,E501,E402,E704,E701,D211 exclude=build,.git,__pycache__,wheelhouse,htmlcov,dist,.cache,*.egg-info,appveyor,*.env,.tox