pytest-qt-4.0.2/0000755000175100001710000000000014061506274014273 5ustar runnerdocker00000000000000pytest-qt-4.0.2/.gitattributes0000644000175100001710000000003514061506270017160 0ustar runnerdocker00000000000000CHANGELOG.rst merge=union pytest-qt-4.0.2/.github/0000755000175100001710000000000014061506274015633 5ustar runnerdocker00000000000000pytest-qt-4.0.2/.github/FUNDING.yml0000644000175100001710000000002514061506270017441 0ustar runnerdocker00000000000000github: The-Compiler pytest-qt-4.0.2/.github/workflows/0000755000175100001710000000000014061506274017670 5ustar runnerdocker00000000000000pytest-qt-4.0.2/.github/workflows/main.yml0000644000175100001710000000432314061506270021335 0ustar runnerdocker00000000000000name: build on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: [3.6, 3.7, 3.8, 3.9] qt-lib: [pyqt5, pyqt6, pyside2, pyside6] os: [ubuntu-20.04, windows-latest, macos-latest] include: - python-version: "3.6" tox-env: "py36" - python-version: "3.7" tox-env: "py37" - python-version: "3.8" tox-env: "py38" - python-version: "3.9" tox-env: "py39" steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox if [ "${{ matrix.os }}" == "ubuntu-20.04" ]; then sudo apt-get update -y sudo apt-get install -y libgles2-mesa-dev fi shell: bash - name: Test with tox run: | tox -e ${{ matrix.tox-env }}-${{ matrix.qt-lib }} -- -ra --color=yes checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: "3.7" - name: Install tox run: | python -m pip install --upgrade pip pip install tox - name: Linting run: | tox -e linting - name: Docs run: | tox -e docs deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest needs: [build, checks] steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: "3.7" - name: Build package run: | python -m pip install --upgrade pip setuptools pip install wheel python setup.py sdist bdist_wheel - name: Publish package to PyPI if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_token }} pytest-qt-4.0.2/.gitignore0000644000175100001710000000035414061506270016261 0ustar runnerdocker00000000000000*.pyc __pycache__ *.log /distribute-0.6.35.tar.gz /distribute-0.6.35-py2.7.egg src/pytest_qt.egg-info /build /dist /.tox .env* .coverage /.cache /.venv /.eggs /.pytest_cache # auto-generated by setuptools_scm /src/pytestqt/_version.py pytest-qt-4.0.2/.pre-commit-config.yaml0000644000175100001710000000207414061506270020553 0ustar runnerdocker00000000000000repos: - repo: https://github.com/psf/black rev: 21.5b2 hooks: - id: black args: [--safe, --quiet] language_version: python3 - repo: https://github.com/asottile/blacken-docs rev: v1.10.0 hooks: - id: blacken-docs additional_dependencies: [black==20.8b1] language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements - repo: https://github.com/asottile/pyupgrade rev: v2.19.1 hooks: - id: pyupgrade - repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.8.0 hooks: - id: rst-backticks - repo: local hooks: - id: rst name: rst entry: rst-lint --encoding utf-8 files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst)$ language: python additional_dependencies: [pygments, restructuredtext_lint] pytest-qt-4.0.2/.project0000644000175100001710000000055314061506270015741 0ustar runnerdocker00000000000000 pytest-qt org.python.pydev.PyDevBuilder org.python.pydev.pythonNature pytest-qt-4.0.2/.pydevproject0000644000175100001710000000073014061506270017006 0ustar runnerdocker00000000000000 /${PROJECT_DIR_NAME} /${PROJECT_DIR_NAME}/tests python 2.7 Default pytest-qt-4.0.2/CHANGELOG.rst0000644000175100001710000006524214061506270016321 0ustar runnerdocker000000000000004.0.2 (2021-06-14) ------------------ - Restored compatibility with PySide2 5.11, which doesn't depend on the ``shiboken2`` project, used by pytest-qt 4.0.0. The dependency is now not needed anymore, and the ``.isdeleted`` attribute of ``qt_compat`` (which isn't intended for public use) is removed. 4.0.1 (2021-06-07) ------------------ - The ``sip`` module now gets imported directly if ``PyQt5.sip`` / ``PyQt6.sip`` wasn't found, as it's still packaged like that in some distributions (`#369`_). Thanks `@The-Compiler`_ for the PR. .. _#369: https://github.com/pytest-dev/pytest-qt/pull/369 4.0.0 (2021-06-03) ------------------ - `PySide6 `__ and `PyQt6 `__ (6.1+) are now supported. Thanks `@jensheilman`_ and `@The-Compiler`_ for the PRs (`#328`_, `#330`_). - ``pytest-qt`` now requires Python 3.6+. - When using PyQt5, ``pytest-qt`` now requires PyQt5 5.11 or newer (`#330`_). - Support for Qt4 (i.e. ``PyQt4`` and ``PySide``) is now dropped (`#279`_). - The ``qtbot.waitActive`` and ``qtbot.waitExposed`` context managers are now available with all Qt APIs, rather than only PyQt5 (`#361`_). Thanks `@The-Compiler`_ for the PR. - The ``qtbot.waitForWindowShown`` method is deprecated, as the underlying Qt method was obsoleted in Qt 5.0 and removed in Qt 6.0. Its name is imprecise and the pytest-qt wrapper does not raise TimeoutError if the window wasn't shown. Please use the ``qtbot.waitExposed`` context manager instead (`#361`_). Thanks `@The-Compiler`_ for the PR. - The old ``qtbot.stopForInteraction()`` name is now removed as it was cumbersome and rarely used. Use ``qtbot.stop()`` (added in 1.1.1) instead (`#306`_). Thanks `@The-Compiler`_ for the PR. - The old ``SignalTimeoutError`` exception alias is now removed, as it was renamed to ``TimeoutError`` in 2.1 (`#306`_). Thanks `@The-Compiler`_ for the PR. - The old ``qt_wait_signal_raising`` option is now removed, as it was renamed to ``qt_default_raising`` in 3.1 (`#306`_). Thanks `@The-Compiler`_ for the PR. - ``qtbot.waitSignal`` and ``waitSignals`` (as well as their PEP-8 aliases) supported passing ``None`` as signal, making them wait for the given timeout instead. This is not supported anymore, use ``qtbot.wait(ms)`` instead (`#306`_). Thanks `@The-Compiler`_ for the PR. - Various arguments to ``qtbot`` methods are now keyword-only (`#366`_): * ``qtbot.waitActive``: ``timeout`` (``widget`` being the only positional argument) * ``qtbot.waitExposed``: ``timeout`` (``widget`` being the only positional argument) * ``qtbot.waitSignal``: ``timeout``, ``raising`` and ``check_params_cb`` (``signal`` being the only positional argument) * ``qtbot.waitSignals``: ``timeout``, ``raising`` and ``check_params_cbs`` (``signals`` being the only positional argument) * ``qtbot.assertNotEmitted``: ``wait`` (``signal`` being the only positional argument) * ``qtbot.waitUntil``: ``timeout`` (``callback`` being the only positional argument) * ``qtbot.waitCallback``: ``timeout`` and ``raising`` (with no positional arguments) The same applies to the respective PEP-8 aliases. Thanks `@The-Compiler`_ for the PR. - Various classes are now not importable from ``pytestqt.plugin`` anymore, and should instead be imported from the module they're residing in since the 1.6.0 release (`#306`_): * ``pytestqt.plugin.QtBot`` -> ``pytestqt.qtbot.QtBot`` * ``pytestqt.plugin.SignalBlocker`` -> ``pytestqt.wait_signal.SignalBlocker`` * ``pytestqt.plugin.MultiSignalBlocker`` -> ``pytestqt.wait_signal.MultiSignalBlocker`` * ``pytestqt.plugin.Record`` -> ``pytestqt.logging.Record`` * ``pytestqt.plugin.capture_exceptions`` -> ``pytestqt.exceptions.capture_exceptions`` (but consider using ``qtbot.capture_exceptions`` instead) * ``pytestqt.plugin.format_captured_exceptions`` -> ``pytestqt.exceptions.format_captured_exceptions`` - The ``qt_api.extract_from_variant`` and ``qt_api.make_variant`` functions (which were never intended for public usage) as well as all class aliases (such as ``qt_api.QWidget`` or ``qt_api.QEvent``, among others) are now removed. Thanks `@The-Compiler`_ for the PR. - The default timeouts for ``qtbot.waitSignal``, ``waitSignals``, ``waitUntil`` and ``waitCallback``, ``waitActive`` and ``waitExposed`` have been raised from 1s to 5s. This makes them in line the default timeout used by Qt's underlying methods such as ``QSignalSpy::wait``. To get the old behavior back, explicitly pass ``timeout=1000`` to those functions (`#306`_). Thanks `@The-Compiler`_ for the PR. - ``waitUntil`` now raises a ``TimeoutError`` when a timeout occurs to make the cause of the timeout more explict (`#222`_). Thanks `@karlch`_ for the PR. - The ``QtTest::keySequence`` method is now exposed (if available, with Qt >= 5.10) (`#289`_). Thanks `@The-Compiler`_ for the PR. - ``addWidget`` now enforces that its argument is a ``QWidget`` in order to display a clearer error when this isn't the case (`#290`_). Thanks `@The-Compiler`_ for the PR. - New option ``qt_qapp_name`` can be used to set the name of the ``QApplication`` created by ``pytest-qt``, defaulting to ``"pytest-qt-qapp"`` (`#302`_). Thanks `@The-Compiler`_ for the PR. - When the ``-s`` (``--capture=no``) argument is passed to pytest, Qt log capturing is now disabled as well (`#300`_). Thanks `@The-Compiler`_ for the PR. - PEP-8 aliases (``add_widget``, ``wait_active``, etc) are no longer just simple assignments to the methods, but they are real methods which call the normal implementations. This makes subclasses work as expected, instead of having to duplicate the assignment (`#326`_, `#333`_). Thanks `@oliveira-mauricio`_ and `@jensheilman`_ for the PRs. - Errors related to the ``qt_compat`` module (such as an invalid ``PYTEST_QT_API`` setting or missing Qt API wrappers) are now shown as a more human-readable error message rather than an internal pytest error (`#355`_). Thanks `@The-Compiler`_ for the PR. .. _#222: https://github.com/pytest-dev/pytest-qt/pull/222 .. _#326: https://github.com/pytest-dev/pytest-qt/pull/326 .. _#328: https://github.com/pytest-dev/pytest-qt/issues/328 .. _#330: https://github.com/pytest-dev/pytest-qt/pull/330 .. _#279: https://github.com/pytest-dev/pytest-qt/pull/279 .. _#361: https://github.com/pytest-dev/pytest-qt/pull/361 .. _#306: https://github.com/pytest-dev/pytest-qt/pull/306 .. _#289: https://github.com/pytest-dev/pytest-qt/pull/289 .. _#290: https://github.com/pytest-dev/pytest-qt/issues/290 .. _#302: https://github.com/pytest-dev/pytest-qt/pull/302 .. _#300: https://github.com/pytest-dev/pytest-qt/pull/300 .. _#333: https://github.com/pytest-dev/pytest-qt/issue/333 .. _#355: https://github.com/pytest-dev/pytest-qt/issue/355 .. _#366: https://github.com/pytest-dev/pytest-qt/issue/366 .. _@karlch: https://github.com/karlch .. _@oliveira-mauricio: https://github.com/oliveira-mauricio .. _@jensheilman: https://github.com/jensheilman 3.3.0 (2019-12-07) ------------------ - Improve message in uncaught exceptions by mentioning the Qt event loop instead of Qt virtual methods (`#255`_). - ``pytest-qt`` now requires ``pytest`` version >= 3.0. - ``qtbot.addWiget`` now supports an optional ``before_close_func`` keyword-only argument, which if given is a function which is called before the widget is closed, with the widget as first argument. .. _#255: https://github.com/pytest-dev/pytest-qt/pull/255 3.2.2 (2018-12-13) ------------------ - Fix Off-by-one error in ``modeltester`` (`#249`_). Thanks `@ext-jmmugnes`_ for the PR. .. _#249: https://github.com/pytest-dev/pytest-qt/pull/249 3.2.1 (2018-10-01) ------------------ - Fixed compatibility with PyQt5 5.11.3 3.2.0 (2018-09-26) ------------------ - The ``CallbackBlocker`` returned by ``qtbot.waitCallback()`` now has a new ``assert_called_with(...)`` convenience method. 3.1.0 (2018-09-23) ------------------ - If Qt's model tester implemented in C++ is available (PyQt5 5.11 or newer), the ``qtmodeltester`` fixture now uses that instead of the Python implementation. This can be turned off by passing ``force_py=True`` to ``qtmodeltester.check()``. - The Python code used by ``qtmodeltester`` is now based on the latest Qt modeltester. This also means that the ``data_display_may_return_none`` attribute for ``qtmodeltester`` isn't used anymore. - New ``qtbot.waitCallback()`` method that returns a ``CallbackBlocker``, which can be used to wait for a callback to be called. - ``qtbot.assertNotEmitted`` now has a new ``wait`` parameter which can be used to make sure asynchronous signals aren't emitted by waiting after the code in the ``with`` block finished. - The ``qt_wait_signal_raising`` option was renamed to ``qt_default_raising``. The old name continues to work, but is deprecated. - The docs still referred to ``SignalTimeoutError`` in some places, despite it being renamed to ``TimeoutError`` in the 2.1 release. This is now corrected. - Improve debugging output when no Qt wrapper was found. - When no context is available for warnings on Qt 5, no ``None:None:0`` line is shown anymore. - The ``no_qt_log`` marker is now registered with pytest so ``--strict`` can be used. - ``qtbot.waitSignal`` with timeout ``0`` now expects the signal to arrive directly in the code enclosed by it. Thanks `@The-Compiler`_ for the PRs. 3.0.2 (2018-08-31) ------------------ - Another fix related to ``QtInfoMsg`` objects during logging (`#225`_). 3.0.1 (2018-08-30) ------------------ - Fix handling of ``QtInfoMsg`` objects during logging (`#225`_). Thanks `@willsALMANJ`_ for the report. .. _#225: https://github.com/pytest-dev/pytest-qt/issues/225 3.0.0 (2018-07-12) ------------------ - Removed ``qtbot.mouseEvent`` proxy, it was an internal Qt function which has now been removed in PyQt 5.11 (`#219`_). Thanks `@mitya57`_ for the PR. - Fix memory leak when tests raised an exception inside Qt virtual methods (`#187`_). Thanks `@fabioz`_ for the report and PR. .. _#187: https://github.com/pytest-dev/pytest-qt/issues/187 .. _#219: https://github.com/pytest-dev/pytest-qt/pull/219 2.4.1 (2018-06-14) ------------------ - Properly handle chained exceptions when capturing them inside virtual methods (`#215`_). Thanks `@fabioz`_ for the report and sample code with the fix. .. _#215: https://github.com/pytest-dev/pytest-qt/pull/215 2.4.0 ----- - Use new pytest 3.6 marker API when possible (`#212`_). Thanks `@The-Compiler`_ for the PR. .. _#212: https://github.com/pytest-dev/pytest-qt/pull/212 2.3.2 ----- - Fix ``QStringListModel`` import when using ``PySide2`` (`#209`_). Thanks `@rth`_ for the PR. .. _#209: https://github.com/pytest-dev/pytest-qt/pull/209 2.3.1 ----- - ``PYTEST_QT_API`` environment variable correctly wins over ``qt_api`` ini variable if both are set at the same time (`#196`_). Thanks `@mochick`_ for the PR. .. _#196: https://github.com/pytest-dev/pytest-qt/pull/196 2.3.0 ----- - New ``qapp_args`` fixture which can be used to pass custom arguments to ``QApplication``. Thanks `@The-Compiler`_ for the PR. 2.2.1 ----- - ``modeltester`` now accepts ``QBrush`` for ``BackgroundColorRole`` and ``TextColorRole`` (`#189`_). Thanks `@p0las`_ for the PR. .. _#189: https://github.com/pytest-dev/pytest-qt/issues/189 2.2.0 ----- - ``pytest-qt`` now supports `PySide2`_ thanks to `@rth`_! .. _PySide2: https://wiki.qt.io/PySide2 2.1.2 ----- - Fix issue where ``pytestqt`` was hiding the information when there's an exception raised from another exception on Python 3. 2.1.1 ----- - Fixed tests on Python 3.6. 2.1 --- - ``waitSignal`` and ``waitSignals`` now provide much more detailed messages when expected signals are not emitted. Many thanks to `@MShekow`_ for the PR (`#153`_). - ``qtbot`` fixture now can capture Qt virtual method exceptions in a block using ``captureExceptions`` (`#154`_). Thanks to `@fogo`_ for the PR. - New `qtbot.waitActive`_ and `qtbot.waitExposed`_ methods for PyQt5. Thanks `@The-Compiler`_ for the request (`#158`_). - ``SignalTimeoutError`` has been renamed to ``TimeoutError``. ``SignalTimeoutError`` is kept as a backward compatibility alias. .. _qtbot.waitActive: http://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.waitActive .. _qtbot.waitExposed: http://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.waitExposed .. _#153: https://github.com/pytest-dev/pytest-qt/issues/153 .. _#154: https://github.com/pytest-dev/pytest-qt/issues/154 .. _#158: https://github.com/pytest-dev/pytest-qt/issues/158 2.0 --- Breaking Changes ~~~~~~~~~~~~~~~~ With ``pytest-qt`` 2.0, we changed some defaults to values we think are much better, however this required some backwards-incompatible changes: - ``pytest-qt`` now defaults to using ``PyQt5`` if ``PYTEST_QT_API`` is not set. Before, it preferred ``PySide`` which is using the discontinued Qt4. - Python 3 versions prior to 3.4 are no longer supported. - The ``@pytest.mark.qt_log_ignore`` mark now defaults to ``extend=True``, i.e. extends the patterns defined in the config file rather than overriding them. You can pass ``extend=False`` to get the old behaviour of overriding the patterns. - ``qtbot.waitSignal`` now defaults to ``raising=True`` and raises an exception on timeouts. You can set ``qt_wait_signal_raising = false`` in your config to get back the old behaviour. - ``PYTEST_QT_FORCE_PYQT`` environment variable is no longer supported. Set ``PYTEST_QT_API`` to the appropriate value instead or the new ``qt_api`` configuration option in your ``pytest.ini`` file. New Features ~~~~~~~~~~~~ * From this version onward, ``pytest-qt`` is licensed under the MIT license (`#134`_). * New ``qtmodeltester`` fixture to test ``QAbstractItemModel`` subclasses. Thanks `@The-Compiler`_ for the initiative and port of the original C++ code for ModelTester (`#63`_). * New ``qtbot.waitUntil`` method, which continuously calls a callback until a condition is met or a timeout is reached. Useful for testing asynchronous features (like in X window environments for example). * ``waitSignal`` and ``waitSignals`` can receive an optional callback (or list of callbacks) that can evaluate if the arguments of emitted signals should resume execution or not. Additionally ``waitSignals`` has a new ``order`` parameter that allows to expect signals and their arguments in a strict, semi-strict or no specific order. Thanks `@MShekow`_ for the PR (`#141`_). * Now which Qt binding ``pytest-qt`` will use can be configured by the ``qt_api`` config option. Thanks `@The-Compiler`_ for the request (`#129`_). * While ``pytestqt.qt_compat`` is an internal module and shouldn't be imported directly, it is known that some test suites did import it. This module now uses a lazy-load mechanism to load Qt classes and objects, so the old symbols (``QtCore``, ``QApplication``, etc.) are no longer available from it. .. _#134: https://github.com/pytest-dev/pytest-qt/issues/134 .. _#141: https://github.com/pytest-dev/pytest-qt/pull/141 .. _#63: https://github.com/pytest-dev/pytest-qt/pull/63 .. _#129: https://github.com/pytest-dev/pytest-qt/issues/129 Other Changes ~~~~~~~~~~~~~ - Exceptions caught by ``pytest-qt`` in ``sys.excepthook`` are now also printed to ``stderr``, making debugging them easier from within an IDE. Thanks `@fabioz`_ for the PR (`126`_)! .. _126: https://github.com/pytest-dev/pytest-qt/pull/126 1.11.0 ------ .. note:: The default value for ``raising`` is planned to change to ``True`` starting in pytest-qt version ``1.12``. Users wishing to preserve the current behavior (``raising`` is ``False`` by default) should make use of the new ``qt_wait_signal_raising`` ini option below. - New ``qt_wait_signal_raising`` ini option can be used to override the default value of the ``raising`` parameter of the ``qtbot.waitSignal`` and ``qtbot.waitSignals`` functions when omitted: .. code-block:: ini [pytest] qt_wait_signal_raising = true Calls which explicitly pass the ``raising`` parameter are not affected. Thanks `@The-Compiler`_ for idea and initial work on a PR (`120`_). - ``qtbot`` now has a new ``assertNotEmitted`` context manager which can be used to ensure the given signal is not emitted (`92`_). Thanks `@The-Compiler`_ for the PR! .. _92: https://github.com/pytest-dev/pytest-qt/issues/92 .. _120: https://github.com/pytest-dev/pytest-qt/issues/120 1.10.0 ------ - ``SignalBlocker`` now has a ``args`` attribute with the arguments of the signal that triggered it, or ``None`` on a time out (`115`_). Thanks `@billyshambrook`_ for the request and `@The-Compiler`_ for the PR. - ``MultiSignalBlocker`` is now properly disconnects from signals upon exit. .. _115: https://github.com/pytest-dev/pytest-qt/issues/115 1.9.0 ----- - Exception capturing now happens as early/late as possible in order to catch all possible exceptions (including fixtures)(`105`_). Thanks `@The-Compiler`_ for the request. - Widgets registered by ``qtbot.addWidget`` are now closed before all other fixtures are tear down (`106`_). Thanks `@The-Compiler`_ for request. - ``qtbot`` now has a new ``wait`` method which does a blocking wait while the event loop continues to run, similar to ``QTest::qWait``. Thanks `@The-Compiler`_ for the PR (closes `107`_)! - raise ``RuntimeError`` instead of ``ImportError`` when failing to import any Qt binding: raising the latter causes ``pluggy`` in ``pytest-2.8`` to generate a subtle warning instead of a full blown error. Thanks `@Sheeo`_ for bringing this problem to attention (closes `109`_). .. _105: https://github.com/pytest-dev/pytest-qt/issues/105 .. _106: https://github.com/pytest-dev/pytest-qt/issues/106 .. _107: https://github.com/pytest-dev/pytest-qt/issues/107 .. _109: https://github.com/pytest-dev/pytest-qt/issues/109 1.8.0 ----- - ``pytest.mark.qt_log_ignore`` now supports an ``extend`` parameter that will extend the list of regexes used to ignore Qt messages (defaults to False). Thanks `@The-Compiler`_ for the PR (`99`_). - Fixed internal error when interacting with other plugins that raise an error, hiding the original exception (`98`_). Thanks `@The-Compiler`_ for the PR! - Now ``pytest-qt`` is properly tested with PyQt5 on Travis-CI. Many thanks to `@The-Compiler`_ for the PR! .. _99: https://github.com/pytest-dev/pytest-qt/issues/99 .. _98: https://github.com/pytest-dev/pytest-qt/issues/98 1.7.0 ----- - ``PYTEST_QT_API`` can now be set to ``pyqt4v2`` in order to use version 2 of the PyQt4 API. Thanks `@montefra`_ for the PR (`93`_)! .. _93: https://github.com/pytest-dev/pytest-qt/issues/93 1.6.0 ----- - Reduced verbosity when exceptions are captured in virtual methods (`77`_, thanks `@The-Compiler`_). - ``pytestqt.plugin`` has been split in several files (`74`_) and tests have been moved out of the ``pytestqt`` package. This should not affect users, but it is worth mentioning nonetheless. - ``QApplication.processEvents()`` is now called before and after other fixtures and teardown hooks, to better try to avoid non-processed events from leaking from one test to the next. (67_, thanks `@The-Compiler`_). - Show Qt/PyQt/PySide versions in pytest header (68_, thanks `@The-Compiler`_!). - Disconnect SignalBlocker functions after its loop exits to ensure second emissions that call the internal functions on the now-garbage-collected SignalBlocker instance (#69, thanks `@The-Compiler`_ for the PR). .. _77: https://github.com/pytest-dev/pytest-qt/issues/77 .. _74: https://github.com/pytest-dev/pytest-qt/issues/74 .. _67: https://github.com/pytest-dev/pytest-qt/issues/67 .. _68: https://github.com/pytest-dev/pytest-qt/issues/68 1.5.1 ----- - Exceptions are now captured also during test tear down, as delayed events will get processed then and might raise exceptions in virtual methods; this is specially problematic in ``PyQt5.5``, which `changed the behavior `_ to call ``abort`` by default, which will crash the interpreter. (65_, thanks `@The-Compiler`_). .. _65: https://github.com/pytest-dev/pytest-qt/issues/65 1.5.0 ----- - Fixed log line number in messages, and provide better contextual information in Qt5 (55_, thanks `@The-Compiler`_); - Fixed issue where exceptions inside a ``waitSignals`` or ``waitSignal`` with-statement block would be swallowed and a ``SignalTimeoutError`` would be raised instead. (59_, thanks `@The-Compiler`_ for bringing up the issue and providing a test case); - Fixed issue where the first usage of ``qapp`` fixture would return ``None``. Thanks to `@gqmelo`_ for noticing and providing a PR; - New ``qtlog`` now sports a context manager method, ``disabled`` (58_). Thanks `@The-Compiler`_ for the idea and testing; .. _55: https://github.com/pytest-dev/pytest-qt/issues/55 .. _58: https://github.com/pytest-dev/pytest-qt/issues/58 .. _59: https://github.com/pytest-dev/pytest-qt/issues/59 1.4.0 ----- - Messages sent by ``qDebug``, ``qWarning``, ``qCritical`` are captured and displayed when tests fail, similar to `pytest-catchlog`_. Also, tests can be configured to automatically fail if an unexpected message is generated. - New method ``waitSignals``: will block untill **all** signals given are triggered (thanks `@The-Compiler`_ for idea and complete PR). - New parameter ``raising`` to ``waitSignals`` and ``waitSignals``: when ``True`` will raise a ``qtbot.SignalTimeoutError`` exception when timeout is reached (defaults to ``False``). (thanks again to `@The-Compiler`_ for idea and complete PR). - ``pytest-qt`` now requires ``pytest`` version >= 2.7. .. _pytest-catchlog: https://pypi.python.org/pypi/pytest-catchlog Internal changes to improve memory management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``QApplication.exit()`` is no longer called at the end of the test session and the ``QApplication`` instance is not garbage collected anymore; - ``QtBot`` no longer receives a QApplication as a parameter in the constructor, always referencing ``QApplication.instance()`` now; this avoids keeping an extra reference in the ``qtbot`` instances. - ``deleteLater`` is called on widgets added in ``QtBot.addWidget`` at the end of each test; - ``QApplication.processEvents()`` is called at the end of each test to make sure widgets are cleaned up; 1.3.0 ----- - pytest-qt now supports `PyQt5`_! Which Qt api will be used is still detected automatically, but you can choose one using the ``PYTEST_QT_API`` environment variable (the old ``PYTEST_QT_FORCE_PYQT`` is still supported for backward compatibility). Many thanks to `@jdreaver`_ for helping to test this release! .. _PyQt5: http://pyqt.sourceforge.net/Docs/PyQt5/introduction.html 1.2.3 ----- - Now the module ````qt_compat```` no longer sets ``QString`` and ``QVariant`` APIs to ``2`` for PyQt, making it compatible for those still using version ``1`` of the API. 1.2.2 ----- - Now it is possible to disable automatic exception capture by using markers or a ``pytest.ini`` option. Consult the documentation for more information. (`26`_, thanks `@datalyze-solutions`_ for bringing this up). - ``QApplication`` instance is created only if it wasn't created yet (`21`_, thanks `@fabioz`_!) - ``addWidget`` now keeps a weak reference its widgets (`20`_, thanks `@fabioz`_) .. _26: https://github.com/pytest-dev/pytest-qt/issues/26 .. _21: https://github.com/pytest-dev/pytest-qt/issues/21 .. _20: https://github.com/pytest-dev/pytest-qt/issues/20 1.2.1 ----- - Fixed 16_: a signal emitted immediately inside a ``waitSignal`` block now works as expected (thanks `@baudren`_). .. _16: https://github.com/pytest-dev/pytest-qt/issues/16 1.2.0 ----- This version include the new ``waitSignal`` function, which makes it easy to write tests for long running computations that happen in other threads or processes: .. code-block:: python def test_long_computation(qtbot): app = Application() # Watch for the app.worker.finished signal, then start the worker. with qtbot.waitSignal(app.worker.finished, timeout=10000) as blocker: blocker.connect(app.worker.failed) # Can add other signals to blocker app.worker.start() # Test will wait here until either signal is emitted, or 10 seconds has elapsed assert blocker.signal_triggered # Assuming the work took less than 10 seconds assert_application_results(app) Many thanks to `@jdreaver`_ for discussion and complete PR! (`12`_, `13`_) .. _12: https://github.com/pytest-dev/pytest-qt/issues/12 .. _13: https://github.com/pytest-dev/pytest-qt/issues/13 1.1.1 ----- - Added ``stop`` as an alias for ``stopForInteraction`` (`10`_, thanks `@itghisi`_) - Now exceptions raised in virtual methods make tests fail, instead of silently passing (`11`_). If an exception is raised, the test will fail and it exceptions that happened inside virtual calls will be printed as such:: E Failed: Qt exceptions in virtual methods: E ________________________________________________________________________________ E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event E raise ValueError('mistakes were made') E E ValueError: mistakes were made E ________________________________________________________________________________ E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event E raise ValueError('mistakes were made') E E ValueError: mistakes were made E ________________________________________________________________________________ Thanks to `@jdreaver`_ for request and sample code! - Fixed documentation for ``QtBot``: it was not being rendered in the docs due to an import error. .. _10: https://github.com/pytest-dev/pytest-qt/issues/10 .. _11: https://github.com/pytest-dev/pytest-qt/issues/11 1.1.0 ----- Python 3 support. 1.0.2 ----- Minor documentation fixes. 1.0.1 ----- Small bug fix release. 1.0.0 ----- First working version. .. _@baudren: https://github.com/baudren .. _@billyshambrook: https://github.com/billyshambrook .. _@datalyze-solutions: https://github.com/datalyze-solutions .. _@ext-jmmugnes: https://github.com/ext-jmmugnes .. _@fabioz: https://github.com/fabioz .. _@fogo: https://github.com/fogo .. _@gqmelo: https://github.com/gqmelo .. _@itghisi: https://github.com/itghisi .. _@jdreaver: https://github.com/jdreaver .. _@mitya57: https://github.com/mitya57 .. _@mochick: https://github.com/mochick .. _@montefra: https://github.com/montefra .. _@MShekow: https://github.com/MShekow .. _@p0las: https://github.com/p0las .. _@rth: https://github.com/rth .. _@Sheeo: https://github.com/Sheeo .. _@The-Compiler: https://github.com/The-Compiler .. _@willsALMANJ: https://github.com/willsALMANJ pytest-qt-4.0.2/HOWTORELEASE.rst0000644000175100001710000000045514061506270016726 0ustar runnerdocker00000000000000Here are the steps on how to make a new release. 1. Create a ``release-VERSION`` branch from ``upstream/master``. 2. Update ``CHANGELOG.rst``. 3. Push a branch with the changes. 4. Once all builds pass, push a tag named ``VERSION`` to ``upstream``. 5. After the deployment is complete, merge the PR. pytest-qt-4.0.2/LICENSE0000644000175100001710000000213614061506270015276 0ustar runnerdocker00000000000000The MIT License (MIT) Copyright (c) 2013-2016 Bruno Oliveira and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-qt-4.0.2/PKG-INFO0000644000175100001710000001622314061506274015374 0ustar runnerdocker00000000000000Metadata-Version: 2.1 Name: pytest-qt Version: 4.0.2 Summary: pytest support for PyQt and PySide applications Home-page: http://github.com/pytest-dev/pytest-qt Author: Bruno Oliveira Author-email: nicoddemus@gmail.com License: MIT Keywords: pytest qt test unittest Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Desktop Environment :: Window Managers Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: User Interfaces Requires-Python: >=3.6 Provides-Extra: doc Provides-Extra: dev License-File: LICENSE ========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PyQt6`_ applications. The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp`` creation as needed and provides methods to simulate user interaction, like key presses and mouse clicks: .. code-block:: python def test_hello(qtbot): widget = HelloWidget() qtbot.addWidget(widget) # click in the Greet button and make sure it updates the appropriate label qtbot.mouseClick(widget.button_greet, qt_api.QtCore.Qt.MouseButton.LeftButton) assert widget.greet_label.text() == "Hello!" .. _PySide2: https://pypi.org/project/PySide2/ .. _PySide6: https://pypi.org/project/PySide6/ .. _PyQt5: https://pypi.org/project/PyQt5/ .. _PyQt6: https://pypi.org/project/PyQt6/ .. _pytest: http://pytest.org This allows you to test and make sure your view layer is behaving the way you expect after each code change. .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |ci| image:: https://github.com/pytest-dev/pytest-qt/workflows/build/badge.svg :target: https://github.com/pytest-dev/pytest-qt/actions .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-qt.svg :target: https://coveralls.io/r/pytest-dev/pytest-qt .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |ci| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ Since version 4.0.0, ``pytest-qt`` requires Python 3.6+. Works with either PySide6_, PySide2_, PyQt6_ or PyQt5_, picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide6`` - ``PySide2`` - ``PyQt6`` - ``PyQt5`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt6``, ``pyside2``, ``pyqt6`` or ```pyqt5``: .. code-block:: ini [pytest] qt_api=pyqt5 Alternatively, you can set the ``PYTEST_QT_API`` environment variable to the same values described above (the environment variable wins over the configuration if both are set). Documentation ============= Full documentation and tutorial available at `Read the Docs`_. .. _Read The Docs: https://pytest-qt.readthedocs.io Change Log ========== Please consult the `changelog page`_. .. _changelog page: https://pytest-qt.readthedocs.io/en/latest/changelog.html Bugs/Requests ============= Please report any issues or feature requests in the `issue tracker`_. .. _issue tracker: https://github.com/pytest-dev/pytest-qt/issues Contributing ============ Contributions are welcome, so feel free to submit a bug or feature request. Pull requests are highly appreciated! If you can, include some tests that exercise the new code or test that a bug has been fixed, and make sure to include yourself in the contributors list. :) To prepare your environment, create a virtual environment and install ``pytest-qt`` in editable mode with ``dev`` extras:: $ pip install --editable .[dev] After that install ``pre-commit`` for pre-commit checks:: $ pre-commit install Running tests ------------- Tests are run using `tox`_:: $ tox -e py37-pyside2,py37-pyqt5 ``pytest-qt`` is formatted using `black `_ and uses `pre-commit `_ for linting checks before commits. You can install ``pre-commit`` locally with:: $ pip install pre-commit $ pre-commit install Related projects ---------------- - `pytest-xvfb `_ allows to run a virtual xserver (Xvfb) on Linux to avoid GUI elements popping up on the screen or for easy CI testing - `pytest-qml `_ allows running QML tests from pytest Contributors ------------ Many thanks to: - Igor T. Ghisi (`@itghisi `_); - John David Reaver (`@jdreaver `_); - Benjamin Hedrich (`@bh `_); - Benjamin Audren (`@baudren `_); - Fabio Zadrozny (`@fabioz `_); - Datalyze Solutions (`@datalyze-solutions `_); - Florian Bruhin (`@The-Compiler `_); - Guilherme Quentel Melo (`@gqmelo `_); - Francesco Montesano (`@montefra `_); - Roman Yurchak (`@rth `_) - Christian Karl (`@karlch `_) **Powered by** .. |pycharm| image:: https://www.jetbrains.com/pycharm/docs/logo_pycharm.png :target: https://www.jetbrains.com/pycharm .. |pydev| image:: http://www.pydev.org/images/pydev_banner3.png :target: https://www.pydev.org |pycharm| |pydev| .. _tox: https://tox.readthedocs.io pytest-qt-4.0.2/README.rst0000644000175100001710000001412514061506270015761 0ustar runnerdocker00000000000000========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PyQt6`_ applications. The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp`` creation as needed and provides methods to simulate user interaction, like key presses and mouse clicks: .. code-block:: python def test_hello(qtbot): widget = HelloWidget() qtbot.addWidget(widget) # click in the Greet button and make sure it updates the appropriate label qtbot.mouseClick(widget.button_greet, qt_api.QtCore.Qt.MouseButton.LeftButton) assert widget.greet_label.text() == "Hello!" .. _PySide2: https://pypi.org/project/PySide2/ .. _PySide6: https://pypi.org/project/PySide6/ .. _PyQt5: https://pypi.org/project/PyQt5/ .. _PyQt6: https://pypi.org/project/PyQt6/ .. _pytest: http://pytest.org This allows you to test and make sure your view layer is behaving the way you expect after each code change. .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |ci| image:: https://github.com/pytest-dev/pytest-qt/workflows/build/badge.svg :target: https://github.com/pytest-dev/pytest-qt/actions .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-qt.svg :target: https://coveralls.io/r/pytest-dev/pytest-qt .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |ci| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ Since version 4.0.0, ``pytest-qt`` requires Python 3.6+. Works with either PySide6_, PySide2_, PyQt6_ or PyQt5_, picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide6`` - ``PySide2`` - ``PyQt6`` - ``PyQt5`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt6``, ``pyside2``, ``pyqt6`` or ```pyqt5``: .. code-block:: ini [pytest] qt_api=pyqt5 Alternatively, you can set the ``PYTEST_QT_API`` environment variable to the same values described above (the environment variable wins over the configuration if both are set). Documentation ============= Full documentation and tutorial available at `Read the Docs`_. .. _Read The Docs: https://pytest-qt.readthedocs.io Change Log ========== Please consult the `changelog page`_. .. _changelog page: https://pytest-qt.readthedocs.io/en/latest/changelog.html Bugs/Requests ============= Please report any issues or feature requests in the `issue tracker`_. .. _issue tracker: https://github.com/pytest-dev/pytest-qt/issues Contributing ============ Contributions are welcome, so feel free to submit a bug or feature request. Pull requests are highly appreciated! If you can, include some tests that exercise the new code or test that a bug has been fixed, and make sure to include yourself in the contributors list. :) To prepare your environment, create a virtual environment and install ``pytest-qt`` in editable mode with ``dev`` extras:: $ pip install --editable .[dev] After that install ``pre-commit`` for pre-commit checks:: $ pre-commit install Running tests ------------- Tests are run using `tox`_:: $ tox -e py37-pyside2,py37-pyqt5 ``pytest-qt`` is formatted using `black `_ and uses `pre-commit `_ for linting checks before commits. You can install ``pre-commit`` locally with:: $ pip install pre-commit $ pre-commit install Related projects ---------------- - `pytest-xvfb `_ allows to run a virtual xserver (Xvfb) on Linux to avoid GUI elements popping up on the screen or for easy CI testing - `pytest-qml `_ allows running QML tests from pytest Contributors ------------ Many thanks to: - Igor T. Ghisi (`@itghisi `_); - John David Reaver (`@jdreaver `_); - Benjamin Hedrich (`@bh `_); - Benjamin Audren (`@baudren `_); - Fabio Zadrozny (`@fabioz `_); - Datalyze Solutions (`@datalyze-solutions `_); - Florian Bruhin (`@The-Compiler `_); - Guilherme Quentel Melo (`@gqmelo `_); - Francesco Montesano (`@montefra `_); - Roman Yurchak (`@rth `_) - Christian Karl (`@karlch `_) **Powered by** .. |pycharm| image:: https://www.jetbrains.com/pycharm/docs/logo_pycharm.png :target: https://www.jetbrains.com/pycharm .. |pydev| image:: http://www.pydev.org/images/pydev_banner3.png :target: https://www.pydev.org |pycharm| |pydev| .. _tox: https://tox.readthedocs.io pytest-qt-4.0.2/docs/0000755000175100001710000000000014061506274015223 5ustar runnerdocker00000000000000pytest-qt-4.0.2/docs/.gitignore0000644000175100001710000000001114061506270017177 0ustar runnerdocker00000000000000_build/ pytest-qt-4.0.2/docs/Makefile0000644000175100001710000001271014061506270016660 0ustar runnerdocker00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pytest-qt.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pytest-qt.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pytest-qt" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pytest-qt" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pytest-qt-4.0.2/docs/_static/0000755000175100001710000000000014061506274016651 5ustar runnerdocker00000000000000pytest-qt-4.0.2/docs/_static/find_files_dialog.png0000644000175100001710000007531514061506270023007 0ustar runnerdocker00000000000000‰PNG  IHDRR_P¶sRGB®ÎégAMA± üa pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.100ôr¡zl_ ™{î½wþ‚…/¾ôòüEK ŠË´TaÉIÅSŠJ¦ M.-/.›Æ4…©djT •WŠánš¢)1—R“ËÊ…Ä2'+.}&¿Tˆ¯W\Z0¹4¿¨4¯°$· ÊΟœ_4)*œ˜[MÈɇÆgçeM’• eNÌʘ“>>;}ü$(-KRjæÄ”ŒñBãÒ™’Ó2¡¤ÔÌÄ” (a\:—œ—œÆ^“˜bSbS¡˜„qQñÉ‘±I‘±‰áÑñaQq!‘±L±Á1PPxLPXt`X”H¤H„_p¸o`¨w@¨—_§o»w€›·¿«—Ÿ‹§Ÿ‹‡¯“›·“›—£«—½‹§³»­“‡­^ÝmÝ­ñêfíàjeïbiëdaëdfí`jiolikdncdfchfm`je`b52µÔ7±kl©ol©gl1ÆÈBÏ¯æ£ ¹ ÌtÇšB#õMFèc4|´ÁºcŸ5vèHý¡#ô†Žóø=è±ác1æÑᣕzd˜.ô›'F)4zx¨Z=>BÖƒ ×Ðë[ÄÃ\˜ËÁÒFÉb-|[Σ|9Hºÿ‘'~uÿªLÊߥüÕRÿMÄßJüõ•Åÿ¤ø#C Ô:JzBW˜îX¦ÑÃÇ0c8BÏ©o4JßXw¬‰®éhCÓ1†fzFæzÆæúÆcñÿnji`f…õÁÈÜÚÈÂÆØÂÖÄÒÎÔÊ2·q0·uÄÊciçdiïlíàbãèjãä†uÌÞÅÃÁÕÓ«Ÿ›·³‡‹§/„5ë§»O€‡o §_—°O@ˆO`(䆕9 4"04«wpxtHD Sd,¶‚ð˜x("&ÛET\Rt|2^cÇA±‰)b#ŠONƒSÒ¹2°ÅAÉéYØ¡ÔÌ ØB…ÄË¶ß ÙØ–…Ħ-¶t.iÛ‡œyý¬©™ÅÕk׎?y²›N¨õ䉧Ô:ùݺpñÒÚ 7ŸæÜæµÐ…-k/63]i^{•iÍM®·š×¼³ZýîÖտݺúw}ªé·[›ð* oßÝÚôÓÚ··®}këÚ›[×ÞØºö*ÓºË[×]äºÐ¼îÜ–u×OKö;÷ÆyX=3ü aþR „Â…‹v´w(¾É1PTªáæeS+§L«bª¨.¯ª™V5}ZumEu­xí_Óª1±Z˜]–ÃÄ(L\7­º¯•ü•5² ¦—WÖL­¨Æ§—•Wâ›”ÈqRZŽo[P<%ri.φœüÉ<Š&æNÈA*t„¼Ì‰ˆ„\Bˆ„l‘B) d$ ƒ,ˆ…Ï$A|2„— ðH¯iqIi±I©ÑñãXïãX„"XD³À@D,ã¢ýC#ý‚#|ƒÂ¼ýC¼üC<|Ý}ݼþØðœÜ}°)"°YÚ9{Ø)’ÀÃÎxëaãèaåàjiçjaçlfíhbeMI`À’À1;@° VúÒƒŒ-ÇðxÐ54×50ÅòÀhøC„ÁPØÐH=˜‹fOR=:\Ò#Ã\OèÊú —¦qw×Ã*ÁÐ%IÖ‰±Ša±¶@| >QïÑúLü£¥O᳨#b‘ éÝw\rôTÏTPåA·T´[*h "D$ ”‘!D*ô &VvèC(#}  <Ðÿ#Áɽ—H@(#k2ò] Xçå<b‘-åAdœ È)’Y$`yð‘ Ì9äÞžFˆ0@ ¥Bß+kòÆßxöÔÙõ+Ïq_¿òâhù• ˯nX~}Ãò›™ÞÞ¸üMLï~‡Vhè•ÞÞ´âæ¦76­¸¾qÅÕ+.o\qq㊠Vß°ê×Õc‡Çø" „“ÃöaþêúÅÂK§_Vv«ûŒUÝýçÀ zj³ìŠªÚÊšúªé3jêfÖÖÏ®›1§~fCý¬93f7ÌœÝ(kFÕÏÂdLu’æHšÉT;s6‹öÆz6ýÜsæÍl˜a ~ö\4bl-¦œ1{zݬêÚ•Óë+jµå•ÓñÝð QO©àÁ0ˆ¼¢Ò¤BʅɈ&ž <òY¨”ÁR!7- y“†HŸš5)%s’ȃqéL<Æ'¦f±0"60<60,: 4yà†âÀÓ/ØÃ7ÈÍ;aàìá‹™je 0!<í\¼ì]} ¬X$˜Û:›Z9 ,0D ˜r뇰ÊÁ`c`n šÛ Xà­ÐXsÛ±f6z&ÖzF–(F5E}0L×@¸Tð0-éñ‘L,„ÃÙÀ£r †UiÑ-04…QcàòLš£D˘G¸û?6rì㣠†ê>1ÚxØè‰ÑFxûøÈ±ø,¾¤‚ˆ„U‚$ɦ1Љ?XêëMrÚA¿:JÖ#°~IRv>:l4ô@þžÈö—gÅÙHý' Q¬Vƒð_ƒÒmøhChÄ£‘H=£QzÆr–cMF5Eµ'%z< L,Q&¢‹€¾‚±¹­TXÚ›¡>°v´°áa`ëlÍÂÀÕÆÁÕÖÑ +V?OVž¢wâìîãêé‡þ JXÝT´^~ÁèÍø„ú†ù…û£>‰daÍêƒð˜P^p±0ày— ¡’R ÉR$# ÞC¤ 2&0! 2&¤#X°ž”1>'sBNÖÄ\&UŒWiBvþ™×ÏñÊàºF t³þOÍš³89µPÞ*ÝÿÄÉSJ]¸tiõ¤´ëOŸ|mÕbèìªÅçV->ß´øBÓâ‹M‹¯¬Y|uí¢ëkÝäz{Ý¢w˜¾ÛM‹”ºÕý-Ÿ~ñÛÐÚÅo®]|síâk\W×,¹´fÉÅ5K.¬^òÆê%çV/9³zɵCû“ý<ß8Aös˜?"@ ƒ†Æ¹ /+*-×F“Ùt·YP:µ²¬¼Šg@-zèðßšú™pízøþœ¹³ç7Ì[Ø8ÑÜKæ-\2Ѳ‹— Í_¼¬»–jj!›…k1„ï7ÂÛEK¡ùXÈ’‹–®Z¼|õ"hÙª…KW°ÅbìÂ%˜Ÿ‹OŸ…¨˜3·~V#búŒ9Õõ³ªjgVÔÔO­ª-«¨.)¯ÂÁßåB^QYnaiv~qvAIvAñD” yEr¡Âñ†œ‚ÌIùr0@ér¹XÅ€„HÍD¹0i\ÆD(ÁÀ^'&¥‡ð6 Ãì­j }bbêø¸äŒØÄÔȸ$ôƒß/„Ê 68*!$:14&)$*ÃÁñAq,B¢|‚"¼Â<ýC=|ƒÝ¼]¼ü±íñ½CÊ$pGغxÛ»ù:z8z:zø;¸ùÚ¹ùØ8yZ:¸šÙ8a#g;ˆÌØÞ!Ô(`ô†vFVNÆ6N&ÖÎLbÀ† 39Z:ZØXØé›Z6²©o‚.çÐQc!áû,tÇ>Áz£FÃÆÈ%1w†¤·Ëßra”®!óëQ\ps!ñËgcÅòpO=1Æx˜žÉp}³cÍGZêY6±t¬FXŒÐ7c߇¥«D¡À"A.TÀì[ÚÝÄ Ž;!Qôô#Q!)%G&UdŠe¥˜$ö—ÿ l¯+ ˜FB¨ÞLzÆøÿ‚F±$@M`6ÚiŒ¡9Û¨* Ñ9@be0·1Bù(Uöè@ 80³q4·a;-í]xqàŠk+I=Ñ)Aׄ…*?tV T±ô` ÔµèÍ |YD0±<ˆbÕp8ŠTqè1±0HdûQã’¢P T»Xy°}°¼2Ê–¬`}2Ñ?›¤¨ xYÀ6UUMÀ*~udçC¢ 9{ÎÔÜââ¥ËQ騆êg-@üOÞÖÏZxøÈ±^uöÜ«2®žAz-< X ø†#ü‚#‘èÜ@XçƒÑûAg(2.4*>ŒÅ@"„¢™)~\t|JLb*—”ÆJjQgC Æ3¡"—2`¢*XùέŸUó™ó„²&¡Ö‡ï£ègÝ;hBNáÄÜ"Hì.ž”7zýìfVçÎ_سw羞:Á…¥P€Æ½]¸ÊÚ·Ÿéµ3¯¯}娡çÕ½4¯î4×kó™Îί;7¿îÂüº‹óë®Ì¯»6¿öÆüÚ›ókßœ?]HùY^V\¦Ñòæ‚Z®º ê®-¨»º îÒ‚zè‚ú7ÔŸ]PfAýkóë_æ:=¿þbgGœ»<_vu˜9Þ"t†Ü{ßû|8¥bº6šZY[^UW^]W1e@MýìÚY °×Y,æ-\ ÿ…þW6­‡_¯]¿yÝÆæ[Z67oß²µu˶jé¦Í[…¶oÞºmSó¶M[Z6nÙºaSóúM[ֳ׿ ›·nØ‚QÛ7·´6oß¹uGGËŽŽ­­mÍÛvböM[·azL¼FdCÓúå«×.[µvÉŠ¦ÅË›PC,XºjÞâ –Íš»¸¾aAí¬¹Uõs*jg±T¨¬-.¯)žZ3yjuaYeTZ‘_2-¯˜%DNÑ”œ¢²I¥“ KÙ+”_:1¿d<²©h|nñø¼â¬œÉY¹ü•+3{rfN1”‘[œ™["†3s&§O,HJŸ„5;2.E1¶llÛÀ†™÷HHHÌŒL̈LȈLLˆK ‹C*Œ ŽJ ŒHc‘Ì"y€-$/ @qÀöñ½Ò¢wÞ7LîŒ.¹ž©ž™>×Xs»±–Ž–Npj+Ix §kî on¯ofÏf1µO7±å³Ûsa¬ãX 6£j^Ck#köʆт·°~;{/3G '_Kk·@k×@Kg?s/S;w#¤‚™ýhc«‘cÍà¡p[ô²Å.#UttÉÇ< S†ã‰R5ÇÒ¨ñ,T ß¶W!Ø ]C !áò£,¡1ìäõl·ž "Ÿ Ž?ÿÝflO ´3ÐÂÞÈ’ÉØÒÁØÊÑÄÚÑÔÚ ÿ¹™­³¹­ ú ö®–önVnÖŽèg`ó´Å æâe‡póqp÷ut÷uöô‡Xxb…t÷ FOB ë¡¢E ´e†: –UÀ‘q!(‚£¢Ãb’ÂY€H¢Y¤Å&§CR¤d±°ÜýE .”š•ÆöÜæˆY|ŸñLYÙèºñ*Ÿ•ûÌñó‹¡ìI9…¥Pn‘¤³oœ·°´~åÕ3-Ûw2µJÚÖÚ& üO”ÃÛw´÷¢íÏ¿øÒ’˜ ‹÷š^öÌô)ÏNŸòÜô)/ÖMy©®ì庲WëJ_¯+=[Wz¾¾ôB}éåúÒ+L%׸–aÉ’÷s÷—†ø0Æ^ç“]ås]ä:__v®¾ìõú²×êÊ^©›rT;åùÚ)ÏÕ²>×¶-ÚÑúÂÅËJcG tî¹ïþ>üÝd Í]°DKÍ[´lîâå —¯^ºjí ôÙ›˜eï·noÛ¶³£µ}÷Î]m»÷vìé’Õ¾g_7햄ɘvíÙÙ±{GûîÖ¶ŽÖíÒûËvloÛÕÚ†îmßÓµ{ïÁÝûvtîÇÚvwbL¿mGÛ––í"HÖoܺvÃÔЪu[V­Ý²lÍÆ%«Ö/\Þ4wÉʆ…ËfÎ]\7gaõ¬y•õÓjç”OŸ=µfÖ”êY%õ%uÅÓj'—O/šZS8¥:¿´’«¯y%Pîär(»¨<iÉå9ÅL“§eC¬}Ú¤¢i“&WM,*ŸXX>±`jfNIJV^ü¸¬¨¸„zFØ6ÐëcûˆP D&eE›=nb4{“<>*y|dbfD|FX\ZH̸ ÈDÿð8Ÿhw¿g/{Wdë¦Ù"\|ìÝý}BÝ£½Ãý"“}½C㼂cÝý#œ<‘èè[Áøn{ck'sOk7ïP'ß'¿pgßp'ß0'ßPGïPG¯{Ï`; ·kW?+gos{7CK‡10V}¶;b ß)§CYÁÁáÑÆ¶î&¶n&¶î0b3T^æN>Î~°iö*É×ÂÉÇÜÑcMí=LìÜmÝŒl\!ÌΖ`çi wðfþÎçµb °rògB‹k€µ[G°­g¨½w¸ƒO¤“_´K@Œ³Œ£O„½g¨­{•³¯™‡¡¥“ž© +Æ?>Rÿ~Aµ£fô#Ãõ쇰Z5‡ê”bÓÒžB”ÊÒ7W Õ$D,OYYˆ[&C+'ÈHÈÚ2¶a2±AaçbbëjjÇdfçffïnîànáàÁäèaéäiåäeåìeíì ¡Ä´qõ±uõE+äàáïèàäèääìäâìââæ ¹û‡y„{FxExGù„@Ñ~¡1þX·QÔFÄE&G'†D'¡ö M‹—™™E'¥Ç$gÄŽËŒKÉŠOŸ6!1m"””1)ÊÌ—•“2>J›—6y~Ƥ(+§ˆ‰uÈXÏlB~1$õØÐu+,ÍF•Zò(wòTˆõð¸ÐÛƒ J¡ ¨°¬âÜù‹–6v§_y•wI¥î©ºÃÊ»°ÓªgÈ!1ðGš·ïèUÏ>ÿââ¿‹:Ÿª˜ =]1ùÙŠÉÏWM~±ªz¥ªðµêÂ׫ ÏÕž¯)¼TSx™ëªJKò °p)T íb1ý¥éEç¹^‡jŠ^«)zµºè¥ê¢«‹^¨,~®²ø™ÊâS•Ågwl²³¸xùŠÒíûîðƒ>®™9OCgξѫΞ;êì^?w^´ Ðã^¹vÓš[×£ÛÞ²³¹µc[Ûî°õ®]{ìé:Ôyàð¾ƒGº“tPCG1V­‡;÷êì:¸gßÝH޽í»;Ûwía¯»‘û::»öì;¸÷_àáãûÝ‹YØôû;+»öˆHhÙÎ*†Í-;66·nܺcãÖкæM›¶­\·eiÓ¦E+×Í[ÚÔ°hÕ¬ùËgÌ]:}΢šÙ «gί˜1¯¼¶qjmÃÔésʪg•VÍ(©¬/žV7™eCmQymáÔéPÁ”¡ü²j60£ê Ëë ØkÞLÅk]>4¥Ê-«É)©Êž\1>¯,mBABʄȸq¡‘ƒ¸À0¶ÿǦÈÄÐø´H¸ÿ¸‰1©ÙPlZNl*”›2):ebTò„ðDDBjpÌ8äW`„«O½›7;qÈÙI`‹íÖ#ÐÅ/Ü#8Î7bœtšTŠ_D²oDòÀÅ'6º{è¢'hdåhdé׆WÂ4]c<‚܃â܃bÝ‚bÜ£]ý#}#}¼ö,|-=‘z¦SÒþzèS{CkW˜;¼6ÍJ ·÷ ;ÏØ´£o¤,'öáànïjël㎎|€¥‹|ß½{fýÜâÝCì<ÃØ¼°xß('?(Ú™‹Krˆq Œs Np IôMö çžâ6Î#$Ñ-0ÎÅ/ÚÁ+ _ÆÜÞþ(òà‰Ñ†âøÛ;?œ~|”á0=Ó‘†VcLíôQ² Ž±B ‚pBDÝa‰´ë_&¨u2UÊ1É„?µ,Ô@LŽÞB¨ä˜P*ùX:3á™Éÿ5~Èu‚Ln(Q¢ ´ó´÷rð r𠆜¼CœÐ?ðEßÂÅ7ÌÅ7ÜÕ/ÜÕ?ÂÍ?Â= §gP4äãëççïžà‘™•=+jHLJhl*VÚðøt%oTRfTRV̸ñPlÊ„¸Ô‰P|ڤČœ¤Ì\(9+oÜø¼” © Ò&¦M*LŸT”‘3ÊDñW2>¯tB~4± lbá”좩\åè–A¹ÅÓÐ]SuàPèWAèÒ ¡{¡ŸOª…Ðÿ;ñ²µ­Ã‹§_AwV©²š%•W΂#¬Û¶¶*%gÉÓϾ°(ÔëB×îeyÐSeyOOÉ{vjÞóSs ÓSs^-Ï9SžsvZÎÓr.N˹Uä\Qhqn–ÿwF‹<ö2×ÅŠœ7*r¡3дÜW§å¾\žûByîóå¹ÏMÉfJþ©)ù'§ä¿¾ms„µé¥+וn@èüúÁ‡?üø“s+´úúßÿÑRƒe«7¬onݼ­ÇÀž»övt¶L÷䇧ù¯+¿yëOy²?:vòÐÚB‚UGŽ8tÁ°—»ü’‰bAÏÙÍÛ¼tt¼w:¾1GG'§‰MÜuO¼·«}WçÎvVXloÛ½mGÇV¦ÝPóÎ=[Zwoli‘°b}óâÕ›®Ü0w麆ÅM³®¬Ÿ·¢nî²é Kªg/ªšµ°bÆüòú¹†)5,JX*Ì*®œM®¨/šV×b4VÍ.­™SRÓPZÓPR=‡©jNqÕlhrå¬Âi3 yeÓsKª&äOIE¤ŽŒŽš Ú78Ò7$*02!$65"i|³þIH‚¸ô¼øô¼¸´< Ħ寤æ hˆHÌ K ŒLDçËÕ7»­ ;hlËûqØž]ü"6Êð‰1&H=3Ck7æ³Þæl¯HX"W$ª¢DÒ÷l翯²ðVV·v¤©Jä»ò ü•‚UBÜÊB|†à?KÈAÈ;^G&üW:á•U~a(ý" ¿HWÿ(d¿k@¤[@”{ y¡ëë‹úò K€|Ãý"’ü#™¢’£’ƒbR‚bRƒcSCâÒCãÒÂâÓÃÒÑq‰HÌb›d©Òï§MŠKËNÈÈ…3ó’Çç 1ëŸT˜:irZöäôœ’Œœ’Ì<¦,˜~Á…õ¤"hZv1S.ªó’ʼÒ*ôÆ„Dç¬Õú©™5þúʵ›Âç…ûúÍGŸ|:{á }ûí·Ï>w%eNWÚ¼½‹;³V®í“?þ÷¿þ)˧d^èhoÞÞ±uçîí{wîÙß±ïàžý;ê½u<çt:ö,þØÉgŽ?õì“§žÓÐq =={lÃd¢¦ãO>öäÁÃÇöB­phY¶ŽNöª½û¡@õpàðqÄÆ‘'Ÿ>zbsžŽNîÚ“‡Ž?€‰‘ûîîÜ×±»³ ‘ÐÑÙÚ¾g{ÛžííÛ;ömïèjéØ×¼³sÓ¶]ë¶¶7mn]¹aÛҵ͋Vmž·bCãÒus¯™½hõŒù+kE$,–"¡nîT 5³Ë 5sJkf3믞]RÍ^K«ÑÒPV;wJÝü)u ¦Ôâu^Y-4·tzcIMcIuÃäÊÙ…å3PäWGŒÏ—“$ŽøEz†yEú‡Å¡3–™<1jò•A.b@ žÑã&aC•äŒ0±w÷±uö´vöbu½‹{ºó°r¿¨Ô€˜Œ€èô€¨TÿÈŸðDwÿHGÏ k'/ {ws¶‡íÆAÿ†â ‡…{úD¤xG${#B<‚ãÝc]¢œ|#à,ð ô(ÍÖ!ö>‘Žè¿³{¬[pzë^©>Q¾1™¾1~Lé¾QiÞ)H÷à >áv^¡‹ÌçÊb Ù+<Õ'Ógp±!öU#Ó¼ñŠ·Ñ~±Yþqâ'BBqüc²0=" Å¥“·‰‹¾©íȱfbGÉŒ6Fž6±ÃO0uð¶@À…=‚í<ñ}jBá\ò[Yß³¿·»"¸4!ÖŽ¼ì!Qfáo…ÔdÂoäŠÂÓÙ¯Qø¿ƒè\1®P´[ ʾ· X”€þ—™Bâ°Îx†2á¿Þ;,ë„ÕÆ'" •¥_d2ºþQ(7S¢Sc˜‚cÓ‚ãÒC⡌°„¬°Äñá‰YÉã#“'@(p£Rx‹—õlrã3ó2ó³ò“Æ$O(7‘)…@qZNIznIF^ifÞ”Ìü)ã §B Ë'²}­lwkvIjëÜRÖ_“ÏÊñé*rôÉŠP»WˆžÙŒ’*tÎf–V¡¬ŸUV3kJÍì)Ó™PëCèÞAÓê¸ê+êçB•3 y¯\·stiL,(…ÄŽ¦I…eÙ(>Ø«Zpdq› dL˜œS$eÛ,kCóöé£~ýâª%í ·Ø6;ns0Üá`Øæ`°ÛÉ`¯ÓØNzGœõŽºèpÑ{ÚEïY½ç¹^pч#ÂñRì¨`y.&€žwÑÆEÿ”‹þIýc.ú‡œõ8éïs2ØãdØî`Øê`´ÍÑh«£Q³ƒÑÉúJ·ßÜsõÆ›J·G txè ÍY´RCÿåa0uÃñŠæcU­‡k:Öííê+ V¯ßÚ¼c÷¶¶Î»º:öØÝ¿nŸé­ãÕ°ûð“§Ž" N=wâéçO>óÂÉg^To%€ž~^èÉMÅ::“מ8uôøI)^Áûÿèûw<²ÿбƒGŸ<|üÔ±Ï;¹%ŸO>~âÐ1ä«:ùþ¢vìaßÎ]{wàuw׎]ûwì9غëÀÖ¶}[vtnØÖ±¶¹}õ¦Ö¶-YÓ¼PäÁ²u³—¬™µpuݼÓ—Õ4,­œµ¨ræ‚òºySQ°<€¸ûÏ)­n`Ãl`NƲ ˜?µ~ÁTäA=æa0·¤fnquCQåìüòúܲšì¢ŠÌÜ’ä¬ÜØäŒ°è„€Ð(ß ïÀpvÀ- Ô'$Æ?"Ý«ÐøŒðDlQ‘1)9’P¤d£n‹ÏŽIÁ”^A‘ÎÞA¶®^ÖŽVŽ|‡/œÝÍ]?lä>ØtQ¤úEŠÊ ÁÍ?ÊÁ3aéèaîà ™Ù{Z8ùØy„8ûE£Óí‰$çÎà ,司Gó0¶u°vñ1·w7°pmd9lŒÑÐna`c`åjæèkãêèåäÏvÝð$HòŠHñ‰J÷‹Íôï×Î Àp œ= ŸåšèÇëƒHa”˜ÝÙ?Ö50Å„g¾Ü?Ó/:Ë<TJGÆ`É,¸x°0ÈÄô>©(\ü£ì=ƒ­œ}Lm]ÇšÛ20Š0àeÁãìp±ñð±|7—›™“¯H[¯P{x1ªÕÞ-…”v¬Ôm·sWïòRIø»F#ów.x½†`úìH ÄL_sæû\¨±˜Üƒ!d@Ï&ÏOÔ‚¡ ^È€°Dï0É>lã8¬<, Q)ˆž(:Ó‚bÓ‘< 2C‘" ’ –‘HÞ¡Áz‹NL,’ #JÈÊOÌ*H_ˆ0€R&C©ÙŨÒsË2òÊYùSÇ”‹$€²‹+³‹« œÒê¼²I–×ò½²ƒâŠL¼d/©B?«¬½7&l¹Pym‹:¦iu,*êçII0s>T5sþ%„“뎎N¸??mdªZ|T¼¾¸ʘX GfI0±˜DœÆÅŽ& å«´qkëtÝ^Zµt³½I³½I‹£ñv'ãNÆmNF»ö9t18êjpÜÍà)7ƒgÜ žs7xA¥Æ¨|ÄïTh c¬˜ìyw6ãÓî'Ý Ž¹r58àb°ÏÅp‹q»³ñg“íN&-N&[LŸš0¸÷ê·”nxaÐWeðù—_# vŠ_²5jÙÚØ¦Õ‰—‹0øôϯ1¸i¹ƒ•ë›·´îjAìîjß{pwס½wJaËaйØO'`þ¾ŸÚ·ÔW' pr€Øåã»ð€ÈVˆ?'Ÿ>vâÔ‘c'9vàБ¹èÿ78t:xäØácPl|öø)ùN}ò©#k ¤EèèLXÖµkOWûÆz7©!{Áž;»¶´wmÞ±a°®¹mÂ`= ƒE«7Ï_±¡aéºÙ‹×Ì\¸ªvîòéK«f/ª˜¹`Z=Ofú³X5PÅU=»¸rVqÕ,þоƒˆ¹ÿ¼Òª^TÍ)ª˜]0mfþÔúœÒš‰“+²ò§¤M*JH›•çîåêáìæäêìŽ<ð‹L ŒNAŸ+4.#<>+XÂx¼²HÈ ‰MCõ™äëæêèáoíäaaïjaïÆúû–Î>vžAÎþ‘!èÜ%û ‡m;,Á3(ÖÕ/aCg§:xZØ# <̽mÜÑ÷d6’ßäîÀºŠ( `(èl:ú„Û³ý,½Ð³†™êZ mÈNog§Ü˜ cGmÆZ¢[ícålç ã“÷%¢æ`•A4\;ïL¬wï–ÌvëÅÁÂàtø¼2@—Y]¸‡$‹<`u@D*? ÚS$v:…óŽd ÷‹ÉD}À„l–pÉîAñN¾¬2ÂÀ¬{ŒâaÐke€<ÐìѳN}o?´]T\šeF{\IÃÔ-9äZçO &©PdÂ1+Xðó„ˆãUB‚H¯Pdƒ(y•€x`%z¼J@<¤ð*!-˜eCJ^%d†%f¡JàÁ0‘ßó“ÆƒÅ¥ç°`Èd©ÀƒUB¡(R³KÒ œÒ  ¥(°Éð© ÎÅ@0Tð¡âÁ€ú B6ÈõÁd l§.+˜X‰Àê Ï&U‰ÀRAT;;öåLžåN®È\ å3å—¨/¾ÅÁ?2Q%« ¬Z¨kSËν‡_Z»²ÙòÙËr›—åv/Ë6oË]>–>–û|,úXõ±<îcyÂÏòi?‹gý-ž÷7ÂÂßQÁÜ?6Z£å èyËgü-OùYô³:æcuÄÇê U—Õ«]>VíÞV­^VÛ½¬¶zY=Ý8Ýí±û{¯ Ä1ƒúÆÅ²Ä‘ƒ/xD,Z½rMüºUI›—¥l_„0ø×¿þ…*>1U„ÁŠ57mc'znoÛ³s÷¾ŽÎ{ö·Îð–løÌí|rÏ"_ÿ¹,|Ñä·h×ÓÏíZß/Ùpê¹] ¤'OíkôÃhVAepôI) PÈä®a¿á™ãO©Â c¾·ŽïìÇ1ñ>6uö’ÎY::YKºPìܳÇîý< öomÛ»¹u÷ú­mM›‘-|7Ѧy+Ö7,];{QSýü•uó–WÏYŒ‚€Å@Ý\üt ¨¢žõG¦á•fGŒ1£a( ,õÌŒl=,œý¥c>Žâ€ªDðãâûúùÞ!)Øi?^a¶ø¾sœä×Ã7AŸ%B¼{p"äïÀ*E}áX9aã žxeBÁà³ÆáçØ{†°cÈžÆÖÎøþ#ÇšŠÝDâÒåîÇ \Mì= òPLŠýþR —ÜxGÚÅqù¸?´ VÏvqAi°áRQ´õŽ4`@uP!X%~\‹NuôáòcâÇøá„pv8QpñÄá„h hà v8Á38Î+$Î',Þ7<ò‹`‡ ¬«X{cƱ ìX:7é¡ñé¨kÃ3#“²"“³¢ÇgR&ŦNŠMËŽOÏIÈÈQH(7b°á¤ç°S´³¹¥YùeãóÙ„IESù„òìâiât¾¼Ò*~ ¡ª`J5T8•H˜:©œ(VB`' VÎ$TÏœR=óÂ¥«6öNí»÷ç!lŠ™òKT*­Î/áÂ@i5Œøm Êj„ ˦ MaÛ·µU<þÒ¦¦– —mÁ.­Á.;ƒ]:‚]v;ï v>æt0Üáh¸Ã“á§"žŽtx6Òáy•ã±|! ?騳z.Òñi(ÊñD„ãñÇ#áŽÃ÷‡:ï qÙâÒâÚâº#Äu{ˆëÓ f{ }˜3/fÇ x˜MôáGOWVÆžûÅWÿDÄ®nJذ"¹yiÚŽ™»ßü[Bƒ¥+֮߼móÖ[[ÛÙyD»öîêl©óÔñœµsÿá㇎ž8|ü©c»úèø5îzúønÕúþRcg£¯ŽÏ¼N”G¡õE::…MÇNˆ$8xø¨¼›hÿ¡#ÐÃÇ]R!oécü˜AÞº§v`Ý𜾹«}ÏòLÏÊõûZwíkíÀk×¶ö}RlÚ¾b]óÒÕ›®X?w);T0sþŠ:~Nb¥@;•ÝâÜ!q*—ïµDÝšƒÒ•ïÊBË$¦ª‰“+QL(š†"Õ.ú5èã §“2±(9+/1=;&9+,690<Ö; Ìtc Gw_'Ogï W¿07ÿ€HÏÀ(ïàïàX¼zAüä €7¿PgŸ`GO/K{7SkGcK;vθ…ƒ¡…;UÔÞÃÚÙÏÎ#XÚÔ½B±åÃ`@N^fvî&6®ÆVÎF–NކVÎ&¶nì ?á2ð}~† óa1ð) Go„>ÖÜe8;“ïsg»Y×5Bq kl;ÖÒÙÄÞËÜIwe?ùÑ`~Ò§¢'‹î-ºÃhç§Á˘9:ûÁ!v2û&ð²`[P;Ï0dƒª>½Êø˜ ÂïDÿÂCêûFáƒXop| ;»ÔÏÌÞÓ˜•5ö£,‡ë‹«ÏøÅeZM$.dêÙ"ôÃÛMØY¹ªŠz¨g»8¿HyÆ‘™,ù¤#{Oé\#. ÕyGø?EÀ«äÃÄÏ;²vöEÙ¸úÙºúÛºI'Ù»Ú³3Ž<‚=ƒ½‚ѽpòqöuág¹ú†»ù…»óÓ ¬ÀXi±ö¢ÞE!ëçÆN7 ˆH ŒL ŽJŽ3.4–ŸnŸŸ‘•˜”…Í$–Ÿk›:•4””‘%gæŽËÊKáœÙ‰F 3&e°kzØU>™ìŠŸÒ ù¥ Ê&Nøv xnñ4v–Qñ´ü’Š‚ÒJ¨PœeTVU¤:Åè —lœwí9P\^_P:½PVY-T4¥®kò”ºñÙì°„¼<•©˜©^Rù ¡æí»êLFœnÙÐç¿#ο=ÎWœg‚g¢ÿDÿCI~G’üŽ'ùLñ;•âû\Š÷s©Þ/¤z¿Ô·^Tû@/¦ú<ÇuŠ-ÁïÉqþÇ’ü$ùHòߟ°71 31 #> JyfÕ"Ÿa_¾r]X½"€M¤qÁ4•D$nZžÒ²8½mþøÝs²»êÙn"¾ƒH# .]±zݦu›¶nܺ½™][°kç®ÍÕ:îõ-ûu±ã½Ç휋žûœ¶“GÚæ‰Ã°{><»m×lvjk9vâКôõWñ@7¿ëàá}-ga°zßÃxËó`-Ûo´ ÙÀÖ<¹}[Ä6Làðž}ûwuò«vuîX_ëʲaBãÎ=-m{¶´îZ¿ugÓÆ–åk6/^µn>;µtåÌyKëÕÌZ0­®…$*ÊÉÓð_ŽEu^ienIE6»ž`*V¯ \XÛ²òJ²rK°æa-„X·%›‘ÆÏ…HT”2‘ujлIBdæÆ§eÇ¥LŒNΊLHIàáîè‚‚ÀÝÇÁÕK\Óoçâåàæëàîïèˆ^¿³W°‹wW0†Ø…ÄØýíݰ‰z[;yšÛ¹˜X9šÛòˉ­Çš‰Wvc ·…£—Û¼U›º£§ˆ#~c L¦oj;ÆØzŒ‰¾™½¥£‘• 7#7S;wS{!øˆìÜmœ -ÆšÙÂIGè›>¡«:5Sê\³c°# ,F› œ ­ÝŒ`Uü$Hv⣣èeû¢£Wáø0#3GvN$¦a^›ƒ3Â|Åõöp7é:sÈÓû˜Ù{1ããS#™„3Âõ`mÊ^¼>ËI\Ê€®†–NúfvâûÕ5xtøh~8v‘»êXó:v½´®±õhvyóO*k´Tß—&@Ý.Mè~BW'ôwi‚£æ¥ ×%`Euð ×%h\”àyExEÊW$H%ð+‚¢Ù ±ÉPxܸˆøÔȄԨDõåúŠ„t~9‚¸_ŽÀ®Eà—#ddB™\Y¹ìr„ yìBhb~‰¸ b"ðkξqÁÙͳ³ëPÍŒ%å3¡Òi*U0•UÌRi¶Ð”JIS«zÕœm;;ë,Ǿº³eWVÜ=㙺&Ę{hBì‘I1'rbžÊ‰y&'êÙ¼¨ò£^„ ¢^î[§{¼Åô˜ ³COó¥a™G'Åž»Ÿ«sB<ÔU˜õÔÆuQFc.^¾“—m_ºÎ@yrYE,©­ 3;æN蜕s .ÿH5Âà ÿúç?ÿñ¿ÿýoE4.X²|õÚÕë6®Û¸ecó¶æm;¶íXWé®ã6}3LÖ¼oÿ¡ýÛæxéxÏÜ~ôÀöiàöÁ0;$ “·âÐQLÆOIÍ]qàæÚ»ÿ »Â{ߥ“tt&­dÃhÙpß&vzÑêcH¶Khfœ=gnëì:°›%AçÎŽ=­m»·ïÜÕ²cQªŽNêÜöÍ-më›[›64/oÚÀ®;[´|ö¼%õ «g4–OŸUZQW\ŽZ•`¬}Љ¬‹!.-ΘT˜ÎNyÎOÀn/aÍÃú—”> ëbb:z.è¿0Å¡/ƒÍ8tm2£“ vqx|j»„81("Þ?4Ú+ ÌÝ'ˆüÖÓ¶NîÖnV®ÖŽìRO¸¼3äÅ.à²aò²Á†‡ÍÏÞÝ‚írFA`dig`j­gl!n4­Ën8c³Ó3±AÿÞ‡î?«¬ÙŽ·,l1vŒ±ÕhC ]óQìŠVq-«åh wæÜ>`c-ìÑ8šÝêÇ|„¾É£ Ñ­†Š+¶Ä­âO ƲsLÙÍ!°~!1·!&t·»‰7b,»ÒØ3Sc_@1/»™i¬9¿VSòiص`*æ>h…³+œ­œ…øÎ~2»Î3b.˜ûHÅM)ªË¢+¿‡ðmoKòÕËBÊë–•Âÿ£P·k˜û¸Œ¹ç5ÌÒÝ ù5Ì0ËW/‹K—­Ø­ÔÙÕË—.³{Wt¿nòô×-£«äáå%]´Ç.ZŽŒN‹IŠˆMŽŒKŽŒ‰+–ã’3âS2!vŲêæ`ã2&¦dNJÍÊR^®,®X–/T†>–œ:qRNkÛÞYK+kç–WÏáj`ªi˜ÆUQÓÈ4}®¤ZI˜^RÝßí ä_ ¹÷½÷?(™Ênç©ÔW_ÿ«­ý ò@)e|õå—ù Â`vã|qŠÕk7°<ØÜ²¥¥©ÜUǵjúæ»÷îéìÚÛ<ÓSÇ«~ëÁ½-³0P‡غ<ܵ­Nu]þ'P,Ù»¤.˜;»Î`âr ìÞÛÅ÷­d +«øÀÁÎ}6É‹îÓ›v.J“Þè8OmZ¿yÛºM-«×oY¶zýÂe«.ÉîHÑXY;«¬²vò”ê‚’i¹ES² J&äNÎÊ.LŸT>‘õ/Ð×—™œÁo6‡n:#©ããyß$&)=:)- }–ô\R"âRЋ Gw&&)ýšè¤`ôq"ãÃãüÙeeìNsXÑ=ýB° x Ø`ƒ±w¶`·a73ǶdçÌî`çÂ6*¾i™Ú8±-ÍÚÑØÊÁ˜í²c7@ ˜°{ˆêŽe7 “nJÃoI6ÊÀ [8Œ‰5^±I³acK42 €G0ã`7´¿c7’cWs© –ÁÌÂb›… -ÅÎ %n:šßÎA܈M\ÁËvÁFÙBôMÙmãØ-†T7bÒ½†Øí†ÔÃÒ˜žÍ¥Ç¥ž]9=Z0ßAí•ìî¤lz6%»‘|Ã"!~S#c±XL+îE ×WÞ˜èǾ7Ñ÷–Æ-Œ´‘|#!厾÷ÍŽä;I7;’osu¿Í»Ç‘¹|›#ùG½ÜàÕ°³tw#'w_g?OW¯7ï@ùÖF^ˆ‡€0Ÿ@v_#qk#~Kǘàp~S#ÄCT|x4»©Qd,»©»£‹‡ÔØ$vÇxqC ~;ᬤTvkaùvFìÆÃŠÛA3çÌ;xøˆ•mJZú¾ýŸ<ùì¡£'… {JCGŽŸêKGUZènyý¥ç_Û¾ñõíÏnßx®•é|ëÆ ­/îdºÚ¾ºÞ¾þ×ÍïÒ›=„FÌxëjÛúK;7^Ú±éâŽMoul;Ù²ieÝô(K“ãOž\±z­lõ0D»7Ñÿ ¹ç½÷Þ—og*ëoŸÿóÿý÷ÿþ÷Í·ÿþêÿý×ÿþ÷_ ÿ¿ÿ|ý¯~ýϯ¿þâ‹Ïÿþ¿£2øâó êg5Î_¼lñ2äAÓêµë×nØ´qËÖ--Û[ZwnßÙ¾³}wû®={öÂÊQ(¨µgo_êØ‘Ô¾‹_~,Inß‹îÚƒ…°å°·Lm»ö´I7¥`7±@²yëö ›[Q«×mZ¹fÃÒ•k,Y¯[?g^MýìòªºÒòêÂ’òÜ¢²Iyì&tè, ï€U„»ÿxfýüÖÓìIéX½DO$". I ‹I J€‚Ñs‰@ÿ%60Œ]Dæå)ÜŸŸ/Ân5êÈŽó[OÛ¹xÚ8²°d1À;Yaã±7á¯ØŒ-±9¡ÿ…Ž60öø~k9ÕcgØgüi3ìfÅ» Œ°YŸ:òÌ÷áþì‰4ØàÅöoÀmÆÁî‚ùØpv'äÇá##¹ð»Î±Þ±®¸#4›-û(¿ï´¨ Ø}Ô7øä‘ÀßMš/b‡g¹Ä[•÷(åË“Éâíâ{ÊÓ0§ãß‹ß[fHß¹»Ø1É_žÇ« Š[–BŠ»–²ß% ?¿xâÞ¶ø0µØ…zÞëCÏ»¢jÜUã~¨X !ù~¨=o†*ß ë³|Ôž÷@•o€Šx7@…\½üÝø­°y0ó{Þ…ò{Þ±»Ÿ†F‰[Ÿ³‡" tn}*nz E³êAºï©x¦ˆ¸ïi"¿Õ¼¸÷µ¸é©xþAUíÌ#GÇ%$Û;:›š›šC&fB²L!sM™™[J²è¦ Æ#b $ÅŽˆ5o4*ÞX7ÁD7Ñtt¢¹^’¹~²…~²¥Á8+ƒT+ÃTk#(ÍÚ8ÝÚ¯’l¤kãTE;ÞB㬒­Œ’, - ÌõãÍõãLõbMÆDŽ2M²1jŒ 9yêé…‹—k¸="AÀzü‡÷Þ/,fO+“U0¹lvã¸|O-Zºbñ²Õó-ÓëfÖÍ@m°hÁ¢¥‹—­dõÁšõk×oZ¿iË¦æ–æmÛ·µîlÝÙ¾£­£­}·¬í»díhëU­míL;{Û!¦â}Ûv£­ìxÛ77oÛ°yëÚ ››Öm\¹fý²•k-[5oѲÙsÖÍj¬ª›9µ²¦¸¬"¿xJv>b  cB.¿õt ô&°Þð›&¦ ¯ÁžM‡â„°htFØj'žJÆŠY¿¬ ì)4¡Þþ!Xq=|ÙÑÇAˆcŽâŽÓÒ~!W¨X˜2ß·5²°1b5¸µxòŒ){¼ ë…acÀ:ÆnI?J}7ø;Ü_ÚV±ÑŠûÚK·eÏàL¬QlÛð}iã‡AhˆÛ„Ú8ød¬›¬j‡˜Ñ0‡‚iŠ[~ª TåY|2L Ïåãsq‰%|o©ØCÒl׿ôýUoU³cbn¯øÂ?Õó î XD}/ÉL’Ÿš)œ±u@ åƒ ùÙ ìÁ üÙ "†ó'ˆ§&°G&¨žš ?2=/Áˆ=/A<,ÁÐÌZ~X‚™•=RAù¤ñ˜l â1 â .ˆO?H< ÁÓWý€„~žŽ.ž“Ãï†ÍŸ“Ê€H€øsØCø­°“R¤‡"¤ð'"̘ՀbÓ³O—~‚BÊå°%ÓgÐí©dw…T‰uÛRü¹˜T+&Õ³Õ„¤•Aùx5H~šX£z¼¿‹­ül5ôWăÕz>hS~¤šôž âyjʇ©i4-%£ÛCÓÄp処õúÍñÙ’ÄSu…&æHâv*›ª,¸k_Ê)˜¬Tna±Ry…%š*Ò”lìJɶßKhL*¤±Äölá)“KËÙÎø“m*ªk«jë§Ï˜U?»Bãü…,–,C•°låê«× šÖm@0¬Û°ICkÂj­“µAK` ÁëZIâq\ëW¯Y‡OZÙ´°lÅj|Eü‘¨Zf5ÌGõ2½~vÕô¬ ˜ZQX\†¿ãÄœü¬‰¹©Y“Æ¥g%¦dòR€ípŒb!C$BȬR!1ȬaXÕ°ÂA~Áa¼Áꈕ«¦ütbg¬¸ŠRºÛ:ñg#ìœÑ’c€U<ø£f,±`k#mÀ@ oÌŽ°ª\Ú4œõÈD  §÷g›%w|ö(D¾ÑJ;Ñ×SöƒœKX<¼½N!•kºmw‰±Âb˜¤EçúǑ܅ï))ÕÝ|Õ[õ,š&{×Iý£nGšÿ•}¦‚ © ñÐMm"Aù¬M)¾+øî#gD‚£+ºPŠHð‘ÐËc5UÏÔ„Ø35â°µ²BG‚ú˪<ˆNž¦y[y)ó@Ž<è5~¼<€~Hh†ÆDƲ ü"ö|yL,åA{Þý”iUåUÓ+kêªëf¢D¨ŸÕ0³U Í_´dÁ⥋–._¼lÊdƒRh‘…Î{oZ)„Ù…-•µ\há¦K–IZ¼š»`1`Îܳæ±'͘]S7³¢¦ÑU2eZQÉüí²ó ÇOÊARjFBJz‹”¬¢d¥@L<ú1€•Œ—aXóÐ+"bë%„u1€õUÄÏ”®èã`µF½±SÈÄJÚ)¤è:aSϧe ÏeÎ2€?ÜœmclwØ $Å€è¦)b@êlj RmÒêí\¹ñkø¤áZJçH?™4þ#´”Æ:¤\+”k‹2 ^#AJ…‘ ÖÛn‘ zör/U‚x겋ÍA Ýž¿/¶;Q"@ÊÇ,ËÏXfyÓ³DŸ®¬,”VNNc‘ òây EÂÈE$üXyõôðî&i†ÆhHcL|éÊ<(,A$L-ž2­´¼’=þ¾²fª„éõìá—3Ø#‹ÑG0 \€/7Ì_8wÁ"!T²æ)4W-Ì"4{î|I²æA³æÎœÃ4CHªœ^WU[Šñ0}Æ,$›–…vµ0WuÝ I˜— £!æòÕLˆ®éÐÔÊIÕe啨ZŠË¦•²gß³ßçá/Žj ƒ=Î~BJúxüO³‚ )IÀ UHA$KeAàÇ å~!Ñ1Áê( 9Ћ‘c¯¶1À +c€eÔ#DhÊ-ÒØþ5ÜA{iXég—ÆöÒX%4VyEÒ&ÄúÙ3D*t‹„îÇú‰¶×H {z–üŒ#õQ„n%‚ÆQ„¸n%‚Øe—Ü£DÐn—Q<Ð.@ z y˜4–[XÂ>O5VžQ¤œWÊx1²¡lZÕ¨¢zj%TƒµÄ(¡rI0q¡RÌÎU¾Ï5M¨x ûˆ¢²©’JYRP,$öwáK–°"  Vüà?ÿIøoKɘ0.=‹ÅÀ¸´x^Ä&²]CˆˆXvšVÖ!Hl×òð€;< :&Ê‚@¹_ˆÅ€­#Öf¶_¨ïâÀ†!Å€2 ¾k§PÐ{ hlðvp»Ò°!Ò‘ÆÓíJc%Q®?òJ%¯fòЧNU$H©ÐÛŽ#‘"nã@‚-;ãH*ø^£^KËb—6a±-‹í86slìr$Èy •=ó@±ËyÐK‰ðCvÝé<€4L›I¶ôîÝKhLʤ±8žêÏ+R¿Í+ê–<ä„`€B…ì@´¦x©Á7’—É—Ï>ˆõñ™ËOfâ—IyèòràO<>›‰¹ÿ¤\üÇà¿*-s"2 9-+)-ÿ¯øoÆ6b@:BßýAœ"\½üT1 .”û…DA ˆ‚ ÏýÚÅ€œòö)o±ÊÍXc רþoKÖC°Òø»-i¬0ÊuI^ÁäUN™ÐmE‚\"`CK ¢D”%B÷½F½Xyp'J–?Ù.#eüHаn&µJê= ”S<|ô™gŸï©§Ÿ}®zæY!vYÏ<÷ùÔÓ'$zòÄSÇOœ:ö$t:zìÉ#LÇe:tôØ¡#ÐQHÜ Úèðþƒ‡ ®LûöÜ·ÿ» F×Î}û!vEôÞ}êú8v±›x0g[Çnh'¿ŠBº‚_ý°}GįP=b{ûH}rm ;­–2+Κݺm3;_¶Eh“?kvÓ–­5´¹YC”Ú´E©õ?DIw¡4þoG+r½ÒX嘺¯–XQ¥5–¯½òÊ,Ÿ.Öv~ú8ÛĶ ¶ Õ3èwb“ÛŽØŽÄ¥Eüâ$¶¡AìBTÕU¨b{Û&6R±µŠ-›06d±E‹­›¹ØÞ±á `Vpô˜pXŒvqô¸s¡ãO W½<“yò$tJ¶£3"fJj1›zJ–ÂÄN=ýl/Rø^?êæœ=¤a³BøÉJ·× å8LZQYýø#H$‰4È{×ȃ>ÃѤG/AƒØ;L^éù}„A!…AÄ E Å!„Þ€d 0 ‚¬¨Ã@•ÝÃ@Œ 0 ‚Ôt .Eü€ âA/að‡>ÂàégŸ£0 ‚”ÀÞaòJÏï% Ä¥ AƒÊ Ó4Ã@Œ£0 ‚Ä<>LfÈ# ‚ ˆÁŠF@A¿8´ ƒœ‚ÉAƒ) úùÖwßÉÍ7ß~æ¹Ó]Ž‘H$ÒL Ö$™TßÈa _3 ä$€( ¾“÷?øãgþûÿ‚  0¥[¿ÿX2©¾ÑH³2…醣0è„ôç'‚0hC‡da 8ƒTCîá÷&*…øè’§Ÿ}SKó½Aa@ÄD›0xbø(v27|áüìvCîÑùÕ{ÿðÞû…ÅSUœòìs/ ¡+ÍGô†6a ££# Aü$hÃFކɨ<æ@è ¹÷¾÷Þÿ ¤¼RÖó/¾4BWOšïgáâb7·Å¥7 ‚  Ú„Á]ýç_<]R^ÅÅ<€ йç¾ûßÿàéÕõBSªê^8ýŠ®ž¡4_߸éè¨ûÎÚ÷Ý' OHƒAüÄhºú†0ù©U’çCˆÎ}÷?øÁ‡UÕ7@•uL/½rFÏÐTš¯oÜܲ³eϦ08Q¨£ãÕÔTXØÔäņnQñ£Mèš~åŒð|!D‚@ç×>üáÇŸÔ7.¬kôÊ™³¦–Ò|}Ãüº+['»‹½QÙ7þ… rTïYf°÷Ù]ÒH1‡bR•ñ+f–ÚX‹jì@¢Êà–”ü ~ÿ— â'B›000µ‚Ɉ_߸€ йÿ¡ß|ôñ§3ç/“õêëçÌm¤ùúF¸¿Ìô»›¶!»9ÞÊì_9FäyÙêœK»‹Â€*‚ ~dxOYiG›00¶°ÉÏœ¿\¥eˆÎ=‚¡9 VÌY°|6ÓŠ×Ξ7¶°•æëµ_c ›}«Ðhט^šHÀË1râ 3 âgA8¦ŒÔªB«0°´ƒÉÃê!îü+ De0kÁòYó%ÝVe`ÝÙ‹e‹W´ŠÑ 1 ¿•Q¶ô;Àè3 ôü"‚øáˆÒ{ÚU¶¯ž=/þ,¬2P3X,ë•3ç´=f ²=<ÝêîçýUÊBÒþ# ÍÛmh`Aa@ÄÏH_ö¢Ý1K˜|ýÜ%\‹!vÌà‡ÙÙD~ôñôYój„fÎ;ýêëÚM¤pi•“óX`H§õÂèUˆÕÌêÓ”( ‚ ´F›0Ð32ƒÉÃíaûBˆv6Ñ=÷ÝÿÁ‡UÖÍ®¨%ôÒ˯jsÁ/ ‚  Ú„®¾áK¯¼VY+y>ÌÀ®3rï}좳ªZY/¼ôòÏ|ò€‡Â€ ˆˆ6a0BWÿ…Ó/O­ªãbž`W ÿjȽïðÁ”Š¡²i5/¼xšîMÔ?A @´ ƒa#G¿ðÒé2•çCˆvo¢ÿrÏ{ïPZ^U2µRèù^¢»–ö6a@ñ£M<1|Ôó/ž–=ˆv×ÒÿûÕ÷ßÿ ¬¼R¨tj zžA¿ èá6A (´|¸ úú/¼øR©Êó!D€ôp M)¯‚ÄLGO:ëŸ?ýé¯|ö7‰DP‚5I&Õ7ñ‹§Ë¸á çW„ÁL­¨ž¢¦£0 ‚”°0xé´lø0D€–WÕM­¬ÆtAƒ„Á‹§O—WJž!Ôa0­zºhV5ýÅ—^¦0 ‚”ˆ0€Õ ϯ¨®ý@ƒ>ü°²¦ª¨©Åˆ—NSA N/~¥²Zåù5µˆuT×΀ª¦3~ùU ‚ ˆA Â&_ÍÝ^8¿: >üð£éõ3kê$~…€ bpÂÂà•WeÇù#ÔaP;c¶¬—_} n¾ùö3Ïî:pŒD"‘ˆ`J°&ɤúa“Wz¾" >ú¸~vcý¬¦Ù ¯P|tÑA -/:C¼òÚ…ç7"¤0øè£g6ÌešÓ½úÚ ƒþ¡ÛQ1Ñ2 ^=óúÌ9Üð¹ó#TaðñÇsæÎŸÝ8OÓQô…A-Ãàµ3¯Ïi”=>"@ ƒ?þ¤aÞBYg^?KaÐ?A @´ ˜¼Òóª0øä“y Ï]ÀÔ¸`Ñ™³ç( ú‡Â€ ˆˆ–aðúÙsÂð!˜?"@ ƒO>ùtÁâeÐüÅK¡×Ͻ¡mÈO¹ÝŸhÜò“/{Òר~fé‡ÛKëé) ‚€hgφ/œ ƒEKW,Zº\èÜçµ 8§2º²µNƒïçìß ‚ ~Ih0y…ç¯P„Á§Ÿ-]±JÖç/h½'u¥Àíu±ª~P5Io1¿úxð½°ãžsõÕÞmyªëK¨G°æîïDQ#MÎF‰¥+¦ÙøžP1Ñ2 `òJÏGHaðégŸ-_½fùª&hÙʦó.jnв@sXÑʼCÜ`¥é0R¸+{Ú,ÆŠéU¶Üm®¾ÚŸØËbÕ-b鹑-MþÈî£Ä0…A ©»ÚiGË0€É/_É ŸiõD€: V5­ƒVr]¸xI«0èi•šfšÝ¥Ñ"†5'SF1ªç\ý·+[İ@nÁ€ôu|àz¦žKè ‚ ~„…ÉH­*´ ˜¼0|áüê0øì³?6­ÛȵaõÚõ/]Ö& Tý|J3eFû]aÀ¦QÊWÞüíR} hù¥ç(«+( ‚¸›1¤÷ ´ ˜|ÓZÉó›Ön@¨Âà\»aóÚõ› 5ë7^º|åû@Vï´ac»ïƒQK㺙/kðhQ´KÓôÕÎ>QÕ¤+@‹ôuÔ_LÑÖ} òTlXcqj( ‚øé5 €–a“_»~£ð|˜?"@ ƒ?þñO65Cë7m._¹ªMf˜2ÂEÕMü=ÞÊŽªfÞÌ»äb€ gg³±bšžsõÕŽy@«Bþ ÅïÙÉø5ÕÓ÷Xœ ‚  Z†L~w{áüˆUüéO›·ncjnÙÔÜråê5-Ã``#Wuël( ‚€hW¯^ƒÛ3qçGHað§?ÿ¹eûŽ­ÛZ›¹®^¿q…èËs~ª( 0 b@¢m\¿! ¯0D€þó_Zw¶CÛ¹®ß¸yWV?!A @´ ˜¼0|áüˆ) þò—¿´ïÚÓ&Ô±ûæ›oQô…A-Ã&ßÖ!y>Ì  ƒ¿þu÷Þ.¦Î}»öì{ó­·) úa@·!b@¡ýÃmÞzëíÝ{öÁð…ó#¤0øë_ÿ¶·ëÔ¹o?ôÖÛïPôÏŸþô×>û‰D" (Áš$“êo¿³w3|áüˆ) þö·¿8tÚ:ôέ[Aƒ„Lž»=³}  ƒ¿ÿýðÑã‡;|ôØ¡#Çn½û[ ‚ ˆA Â&«žóGHað÷¿ÿãØñÇž„ÂàÿøÇÉSOC'ž:ýî÷ 0 ‚” `ò'Ÿb†/œ ‡ÁçO?óÜ©gžúÃÞ£0 ‚” þðÞ{²áÃüR|þùçϽðâsÏ3=ûü ï½ÿ>…AÄ a“†Ïô‹ˆU|ñÅ‹/½,éôËïð!…AÿÜ|óígž;Ýuà‰D˜ÂŠíTÚb ˜ü‹/f:Í< …Á_|ùò+¯q½zú•W?üè# ƒþ¡‹Îb€ÓÿX;ž¿žºúiŸ†#ƒCø-øEÒoû.0yX= _8?"@_~ùê™×™^;}ôÑÇýC·£ ˆO_a°ýÙkkO{œýÜ÷æ·ƒCø-øEø]Ò/ì„LþÕ׸ásçGHaðå—_ž9{îÌëç^g¯g?úø ƒþ¡0 ˆO_a¼ê”ç¹ÏK~÷Íùzp¿¿¿Kú…ý‚0øøãO^{ý¬ð|  ƒ¯¾:÷Æù³Loœ=÷Æ'Ÿ|JaÐ?1ðé+ |ŽøÞøöÜ_¾þëƒDø-øEø]Ò/ì„LVÏ ÿó0D€_}õÕù ¡7ÎC>ý”Âà; 0 ˆÏw„ÁŸ¿úËçƒDø-·0yX= _8?"@ƒ¯/\ºtáÒå /AŸ~ö…AÿPÄÀ§ÿ08û'Øè—ƒCø-·Ÿ}&Üþâ¥Ë"@_}éòÑ }öÙ”0P>Áø.ç{†Á­&/¯¦[Ò›Æ\Ô÷f |‚è›~Ãà?¯ÿéË?ÿc¿¿Hû0€ÉˆóGHaðõ×__¹zí2tå*ôÇ?þI›0èöà{maðý"a€‰fœ(ÔÑ)<¡~#wƒ™çÌ™^Òß ô>“JÓè0¿íi»Ú±b!œ;íÜÚ|‚øùè' |nüçÌg_üñï}ëâRWiÃaLìê1Á@~ ~‘öa“nÛ‡ù#¤0øòË/_?û;•è¶Î&º]ƒþ!†>ÀÀ'€0F8d_¢Ð«é…Êóh¢q¢°×Åiß9Í€Øô¯}úùgë[–¸º-y½çð€~Ëm…L^2|®/¿T3øèãgÍ™;šÝX?«áÕ×ÎÜ^ðŪÓkV +ÑÑÁôb.õ,|rõ4²å«tŸÝÕ}´új^õ¢Ø8ÕÀøÒX›¼Ø;Co»‰¸ƒŸPø#¼²›ÙóŠ¡›ò>ÊðE ˆvò\lZš-ÊÏâtû,–¬šŽÍÈ‹äcØ\……¼]1Э]Ñ—¥^„bÙÝâÎ#­cÝ‘Æqú ƒëÿyõÓÏ?ýë?úÔùÅ.n‹_Ãû²u&í-ó&áC²w‹ ÄGÊoÅô¬·üõÒ<76ðú"µ©&ÞÏ;¯øÄ üü"íÃà•×Î̘ÕÇíÃüRܺõnDlbDLBhTtàБÛɘÕ¬‚yº4JL/¿ÊílÕ ´L6ZÕÄ‘ÚzÒDÝ¥X˜<À&èö~8½3à¨0?ö^ñVv~U›z¼lþªæÙ@4i.Ö,µ©[T)–,Á>@Ë1KÏ©ßPLÊ>M5€åv1%&•–Ñã;°)¥–>ꂸ3Hë¶ ©UE?aà~å_'~ûÙ÷>íS/Ís”– \ê^’ZRvˆ v§¨†O4¸èdîæ-6ó·)™ø¨Ý)ÎóN°¹X;¦Üœ‰i2é#0þaÂoÁ/Ò> `òaQñp{Ø>ÌÿÖ»ïJaðέ[!1aQ¡‘¡]}Ê@2[å0è9Jù*·Kq¯” r‹æŒ='–B@‘0wžÞ ËîÙ’e2£T¡žV©°U[ ·ÙdåYÔ¶«¤Û'°ã½}1zŒr!ÊOÑß2ðÍT»¼”³3ðäïM?:|­cHïôn—ÿùä­O®ý¾o½0×Áyî“Êáž-ò°Îø¿ÿdc†Kí x¿-»X;^¿ ±¡Ã'PM¬DÕþÄ߂_¤}ÀäáöLaQ0D€* Þ¹þ!~Áá]ûþÔa œôÓ¢1coÉÁcàGÍ‚¾vžà&ÙÓÕÎÚÍRÕ¨fîi ¼±§÷léI_c±H9 0ü5–©1 ¿eðß)‡º] ÿŠâÇë™4ÔþÃàØ;Ÿ\ùÝÇ} ®í4÷˜ô¶cœÎø ÊaŒýÝÇÇf»ŒÛÞ1޵¿:Ýiü†íãÇm³‹%€î ¹sÂo¹Ý0ðŽ€áÃöaþˆ) Þ~çò5_P˜oPؾýîX0g–Þ3ëÆ ˜ Ûd˜FùٴÝZÔ ê‰¥¶n‹â°ŠCì­Æ?”žaK䶇UþÇU6);f_ÖÉ&V´l¡bzi.nÁªIø 3Ü>íVšKÌ‚9T‹RL#-SnÑPL)Í-µôø·š Õ‹”ç!ˆŸš~ÂÀõò?¼ýÑÅw?ìSÏ5Ú95ÃÛ²t0¬ly·=YG'y>2ËE'½]Ì’ìäb7ëeÞ˜•œžUýoä-ß}¹ÚÉ¥ú9õŒjaÉ:YkûîWø-øEÚ‡Á¾ýáölæP‡K‚àp$O@È^íÂ@µo§{ß¼§53÷f¸eg«c@c2æ×*D»¢Eb¤z£±(F÷&6½æ?Í0PY+(¼‘ H­jSìÓ•#Øœ*x£<’/o-,”ZzL«F9ŠÑ ãV ª©Z¦üYÝ–¦úòR»øò"5¿Aü”ô—þyèÍÏßú O=#–ÉZ#Zœõ2ËÔ–¤ã\õ naùÁ'F£„ÝÌ—5—,¦a-ª…ô5ܯð[ð‹n' ø„Âðaû0e¼ƒ&¤„O`¨—ðÞ®ýZUu5òcÑû1ƒ¾;vë;÷S pã”­Ÿ îú —‹_¸ñþ¹·ßÂoÁ/Ò> `òÞþ!0|V‡#a JO¿ Î}ww D1ñãq›a U|Ç?;ÄÝFÿaÐuý½3oþ¡Iw á·ÜVÀä½ü‚aø"ÔaðÖÛï É; Iàá¸go×Ý]üøÜne@ÄOO?aà|ñ«}×þðÚÍßá·àÝVxúÁðaû0D€: ä$póö§0øN( bàÓW$¯:åøò_Ò.ý½óêï‡ð[ð‹´žLÞÝ;†/ò [ ÉÝ'rõòÛ½w…AÿPÄÀ§¯0Øþì5·5/9¼ò§ _á·àiÿ¤³Ý{»Ü¼ü…çÃüÕaðæ[o‹šIàâé»»“Âà;@Ð3 b Óÿ3·>s5qÅS>ªgßíÂoÁ/’~ÛwÁÃ`Ÿ‹§ ¶óG¨Ã@N'wï]{öRôÏŸþô×>û‰DÈÂv*m±„{Ý}`ø"4Ã@$£›WÇn ‚ ˆÁ Â=~'7o>l_3 ä$°wñìØÝIa@1(AÀä\læP‡£›7FØ9»Û8¹µíÚMa@1(A´ïÚcëäÃçyàÝ- D`´µƒK[…AÄàa€¿ƒ«œÝÂÀÞÅ#l]­ì]v¶SA NXtì¶¶w…áÃöaþê0¸qóM9 ,íœv¶ïÒ& æ-n"‘H$Ò€’dÐ}ƒ0€É[Ù9ÃðE Ôa '¹­ã޶-Ã@ººƒ âÇáóÏ?—†ˆï+-Ã`GÛ. ['¾Èƒna`í %™µC+…A í¹0è0·v€áÃöaþÊ0¸iiï,’ÀÄÒ®ug;…A íÑ> ZÛÚM-í`ø,ìR\¿qSJ+;c ÛÖmA þñHCÄw¿•¶a°³ÝĆ/ò S+{”FæÖÛwP1  0ÐíÃ&odnÇíÃü»…”6fVÛZ) ‚PhÏm…¡™5 _ä: ®]¿!’£ÇšZnkÝIa@Ä@V# ß…°enÏý0@ßÀÔJÎD€: D ,Ð7¶h¡0 Ôœ(”žÿÊ`OA¾ÕäÅÿÕî)þ|önÏwÖf.‚€ÕHC? êÕŸ¯Äê• "l™Ûs `òc-aø"º‡¹”zFæ-ÛwP*úñn-ÃÀËËKÄmð·¿ýMúéaÖ¯Zso55)»4ü­´ƒzF0|Ø>Ì¿[ i¬‰¥ž±ùC³­Z‡Áþóí?ÿõ iPëhžŽçòBýŒ’ŧ9²ÒÃsåueËwÌE"1}ûí?ùô³ŸÍgŽäë¨×Û.ñ·Ò6 ¶ïÐ34‡áÃöaþê0¸zýºœ£ M·nkÕ2 ¾ùÏ·_ÿóߤA­#¹:žË®÷Ú¢u}…‡TMçVO©žæpžŽÇòÝggyGÔ£K‹Ê=üïkË=ù q/Ó`ã[q ‡óÙ X˜b^Í" BýÜaÝXÆ=ž¯ùÆÚ‹·eÍÔ> †¹uûŽÑ¨ ŒÌaû0Ue0ä 5µÒ7¶cd1ÆÐÓaji¾¾¡0øeHcíW¶¨Ô}"ŽÔÍט˜{:Õ½E¢û5fÔêƒHƒPÿýï?üèãŸßgøŠ×½'Ä„ÒV ‰°fŠ¿•6aðÄðQ-ÛwêÁíÙaK˜? ƒ!÷èüjȽ׮ß42·34³kj£obÕÒÚ6l„®4_ß°0øæ?_}ý/Ò _û¯õÚ¢¸†-!ïz¥”³³NVîážs¡½û5gÔæƒHƒPßþ÷¿|øñ@ð™«Ë;Ûfoa‡V\•5Ø×p?D„ú™Ãàð ÕÈú1|ÅS®ŠJ÷ÿù×LíÃ`„®>LÞÈÊÁØÒ¶óG tî¹ïþ7ß²rò²tò´pð0wpßѱGWÏPš¯oð©ÿþæ?_~õOÒ Öá\¥W{mQŒºº\]&çîmbIWØ.XÑr}©˜Ç#/×£ÇûîóƒHƒPß~ûß÷Þÿðçó¬x*¤•MZªG`^~£~î5Sü­´ ]}Ãp{¶óG tî»ÿÁ›o½ãàdïdçhãæß¾§KÏÐTš¯oð©ÿú÷7_|ù5‰D"ý‚Áýá½Èg´‘ø[iz†fí{öÛºØ{Âöaþˆί|øÍ·o¹F¹DºøE8û…ïÞwÈÀÔRš¯o( H$Ò* í¥}˜Zíî:äìáâéå@èÜÿÐoÞzç]¯ÐÏÐxÏxà¸ÎýGŒÌm¤ùú†…Á¿¾ùü‹¯H$éÇзß~ûû?¼O>£ÄßJ›00¶°éõŸÿú÷?>ÿ’D"‘~ ýçÛo÷û÷Èg´‘ø[i–vz†2Ã÷K‚ù#@ Þ|ç]V„ĹźÅîѺ2 ÿ$‰ôã‰Â@{ÝFXØvî?«wŠóaõ"€U☓o¸“O¸£w˜ƒw讽µKg_kÈæ`gÝsßý×n¼ibãblãldådhå´½m—–×H7N%‚ Ú„®¾áö¶ÝFVÌðaû0D»Î`Ƚ÷]½vCÏÄZÏØjŒ±åh#Ë–í;µ¼Yú|‚ b MŒÐÕoimÍ ß ¶óG°+5äÞ+W¯Ô7¡g2|ŒñðÑF[ZZµ¼7‘ôùAÄ@›06r4Lž¹½ž1læ`÷&ú¿!÷\¾ru許Ô|¤Þc#ô6miÑò®¥ÒçAmÂà‰á£65·ÃÇ+ÌÀîZú¿‚¡Ç†yløèG‡~ä Ý[¶jù<éó ‚ ˆ€6a€¾>zü ýèðѰ}˜? ñp›K—¯<:L÷‘aº¿ybÔÃCGnÜÜLÏ@&‚¸ëÐ& 6bã–æß ÇíÃüê0“àá¡#( ‚ îFn' àö#Et U°0Ø@a@q¢ml–Â@äA·0“à¡Ç) ‚ îJ´ƒ‡g†/œ¿g`ƒM[( ‚ î:´ ôøUaÀò@# ¤$ 0 ‚¸K¹­0€Û ç× ‘>6œÂ€ ânDû0xè1Éó5Ã@Nh=…AÄ]ˆ¶a°i Ân/œ_yˆ$xàÑaAw#Ú‡Áƒ2Ãy€P‡h¥0 ‚¸{ùaõH ‚ ˆ»”Û ¸}0¸tYN ‚ ˆ»”Û ‘ˆuÈIQAÜ|0€( ‚ A…AAa@A ‚ ⎇ÁF ‚ ˆ»-Ã=þïƒûy‚€ ânDû0xàÉó! ‚ ˆAŠĀ…AÄÝÈm…0|0¸Ângúèp¡ ›¶ Fa@q—¡M >rææù-¬aø0Dƒ!÷\d·°)kÃæfL-Í×7A mÂà‰á£6nÞúðÐQ²ç³Õ ¹GçWCî½tùê£ÃõdmÚÒ2l„®4_ßPA (´ ƒa#GojÞöèýGGHž@è ¹÷¾ËW¯ c,kKKë]=i¾¾¡0 ‚Ph#tõaòÃôL†˜ç#:÷Üwÿ•k7F›ØÈjim×Õ3”æë ‚ ˆ…6a «oز£}Œ‰­ìùˆÎ}÷?xíÆ›F6n²ZÛ÷èšJóõ …AÄ€B›0Ð34kmï4¶u7¶•<€ Ðùõƒ_óm+WYm{ö˜ZJóõ …AÄ€B›000µ‚É[¹ÈB tîè77ÞzÇÞ;LVÇÞƒFæ6Ò|}Ca@1 Ð& Œ-l:ö²÷W) € Ðyà¡Gn¾uËÑ7ÒÑ7‚+r×¾CƶÒ|}Ca@1 Ð* ,ívuvô‹â¶Ï„xaÐ[epˆ*‚ ˆ»í*[ôø•A¸Tˆc–.þ²ÚötÑ1‚ ˆ»mÂö®8fÀŽ_¿ùö¯xX:›ÈXu*èl"‚ ˆ»mÂ@ÏH}6 ’Î&¢ë ‚ Ú„AŸ×ÐÈAƒm Ï+éÞDAƒm Ï{Ñ]K ‚ Ú„AŸw-·‘f@Ï3 ‚¸KÑ& 4žgu{Ò™ü˜3ˆžtFq7¢Mh<é ¢0 ‚TPAA…A( ‚  ‚ ‚€ ‚A…AAa@A ‚ ‚€ ‚ 0 ‚ …AAa@APA€Â€ ‚ 0 ‚ ( ‚ @a@APAA 0 ‚ ( ‚  ‚ PAA…A( ‚  ‚ ‚€ ‚A…AAa@A ‚ ‚€ ‚ 0 ‚ …AAa@APA€Â€ ‚ 0 ‚ ( ‚ @a@APAA 0 ‚ ( ‚  ‚ PAA…A( ‚  ‚ ‚€ ‚A…AAa@A ‚ ‚€ ‚ 0 ‚ …AAa@APA€Â€ ‚ 0 ‚ ( ‚ @a@APAA 0 ‚ ( ‚  ‚ Ü0xðÑá²6lÚ2t…AÄ]†6a0tøÈ ›š|lăIž¯ ƒ!÷\¼|å¡ÇGÊÚ°¹SKóõ …AÄ€B›0xbø¨›·>"A óë¾þæÛV®þ²Úöì70µ”æë|*‰D"‘”$ƒîS+˜¼•k€,D‚@çþ‡~sã­wì½Ãduì=hdn#ÍGA "Œ-l:ö²÷W) € Ðyà¡Gn¾uËÑ7ÒÑ7‚+r×¾CƶÒ|AÄ ÂØÒnW×aG¿(nûLˆ€½U‡¨2 ‚” ¯¿¢2—*qÌÀÒÅ_VÛž.mŽAw°wÅ1vœøúÍ·ýÀÃÒÙDƪS‰0 åÙDAÄ]‡ž‘úl">$Mô½¯3 ‚ î:ú¼Îà{_LAÜuôyò÷¾7Aq×Ñ罉¾÷]K ‚ ˆ»Ž>ïZú½Ÿg@AÜuôý<ƒïû¤3‚ â®ãÎ?ö’ ‚¸ë 0 ‚ ( ‚  ‚ PAA…A( ‚  ‚ ‚€ ‚A…AAa@A ‚ ‚€ ‚ 0 ‚ijjª­­6XÀoÁ/’~ÛwAa@ÁX¼dÉš5k>ýôÓo ø-øEø]Ò/ì ‚ FMMÍÿøÇo¾ùæóÁ~ ~~—ô û…€ ‚1mÚ4ô¦%,àáwI¿°_( ‚ 0Íÿüç?ÿ\àQAÜ" ¤7ƒ…;>:\Ö†M[†£0 bp"Âào?#¹¹-º ½¹3hC‡Ü°©ùÁÇF<ø˜äùª0rÏÅËWz|¤¬ ››1µ4AÄà¦ùÍ7ßüåŽp~¡«Ž’I{Ñâºð¼4º´™æ6Á/Ò2 ž>jãæ­%{>"A ó«!÷^º|õÑáz²6mi6BWš bp!ÂàÏw„s \]œ“ÞhÍ÷›«_´ƒa#GojÞöèýGGHž@è ¹÷¾ËW¯ c,kKKë]=i>‚ ˆÁ…ƒ?ÝÎ2[?+½áˆþº`¢T/LìT’tt4æúÁh#tõaòÃôL†˜ç#:÷Üwÿ•k7F›ØÈjim×Õ3”æ#‚\À4¿þúëßÞžŸí$¹» cZœf?ÏÛ3¶ñi¶eè …ý£jÓðÁ;~‘–a «oز£}Œ‰­ìùˆÎ}÷?xíÆ›F6n²ZÛ÷èšJóA .Dܺ#<3ÓÉiæ3ÒŽhQ¶÷Õ"†ïÚ‡ž¡Yk{§±­»±­äùˆί|øú›o[¹úËjÛ³ßÀÔRš bp!Âà;ÂÓÌÖŸ–ÞpD‹²½¯1|‡Ð> L­`òV®²ûúÍ·Þ±÷“Õ±÷ ‘¹4AÄà¦ùÕW_½uG85ÃÑqÆ)é G´(Û¥áæ4© ªÁ;~‘–a`laÓ±ï½w¸JaˆÎ=ró­[޾‘޾\‘»ö2¶°•æ#‚\ˆ0¸yGxªÞѱþ)é G´(Ûåá-©âÀ‚cjªæ\?˜ÛK»]]‡ý¢¸í3!@ôV¢Ê€ ˆÁ LóË/¿¼>¸À/Òº2°E_Q„K•8f`éâ/«mO3 b°"ÂàêCô÷•H#~B´Ø»â˜;N|ýæÛ¿~àaél"cÕ©D ³‰‚ÄÀ4¿øâ‹+ƒ ü"-Ã@ÏH}6 ’Î&¢ë ‚øEQSSóþûïÿùϾ4XÀoÁ/Òòy}^g@W ñ‹bÉ’%«W¯þðÃÑ›à·àáwI¿°_ú¼™îMDÄ/E‹¡=m°€ß‚_$ý¶ï¢Ï{Ñ]K ‚ ~9ôy×RzžAÄ/‡¾Ÿg@O:#‚øÅ@½$‚ xl¤0 ‚øeCa@AüöÎÃ@íùA¿8D(=ŸÂ€ â…AAa@APA€Â€ ‚¸0hœ¿0&.3H$i ö“Wz~ŸaÃ&EtH$i ö“Wz¾ qîô‰-[·iLA"‘H¤A/˜?"@ ƒ_ ¹gÁÂE“K§jLD"‘H¤A,Ø>Ì …¸çÞ{Ñ„ˆðô Ò˜šD"‘HƒLî>0|Ø>Ì_ЄЅ9 /]&‘H$Ò ¬†ß­&Ðàÿ~5„D"‘Hƒ^’é ttþ?¦MóñËÚIEND®B`‚pytest-qt-4.0.2/docs/changelog.rst0000644000175100001710000000010414061506270017673 0ustar runnerdocker00000000000000 .. _changelog: Changelog ========= .. include:: ../CHANGELOG.rst pytest-qt-4.0.2/docs/conf.py0000644000175100001710000001734214061506270016525 0ustar runnerdocker00000000000000# -*- coding: utf-8 -*- # # pytest-qt documentation build configuration file, created by # sphinx-quickstart on Mon Mar 04 22:54:36 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"pytest-qt" copyright = u"2013, Bruno Oliveira" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "pytest-qtdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "pytest-qt.tex", u"pytest-qt Documentation", u"Bruno Oliveira", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "pytest-qt", u"pytest-qt Documentation", [u"Bruno Oliveira"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "pytest-qt", u"pytest-qt Documentation", u"Bruno Oliveira", "pytest-qt", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' pytest-qt-4.0.2/docs/index.rst0000644000175100001710000000065114061506270017062 0ustar runnerdocker00000000000000========= pytest-qt ========= :Repository: `GitHub `_ :Version: |version| :License: `MIT `_ :Author: Bruno Oliveira .. toctree:: :maxdepth: 2 intro tutorial logging signals wait_until wait_callback virtual_methods modeltester qapplication note_dialogs troubleshooting reference changelog pytest-qt-4.0.2/docs/intro.rst0000644000175100001710000000663314061506270017114 0ustar runnerdocker00000000000000========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PySide6`_ applications. The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp`` creation as needed and provides methods to simulate user interaction, like key presses and mouse clicks: .. code-block:: python def test_hello(qtbot): widget = HelloWidget() qtbot.addWidget(widget) # click in the Greet button and make sure it updates the appropriate label qtbot.mouseClick(widget.button_greet, QtCore.Qt.LeftButton) assert widget.greet_label.text() == "Hello!" .. _PySide2: https://pypi.org/project/PySide2/ .. _PySide6: https://pypi.org/project/PySide6/ .. _PyQt5: https://pypi.org/project/PyQt5/ .. _PyQt6: https://pypi.org/project/PyQt6/ .. _pytest: http://pytest.org This allows you to test and make sure your view layer is behaving the way you expect after each code change. .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |ci| image:: https://github.com/pytest-dev/pytest-qt/workflows/build/badge.svg :target: https://github.com/pytest-dev/pytest-qt/actions .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-qt.svg :target: https://coveralls.io/r/pytest-dev/pytest-qt .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |ci| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ Since version 4.0.0, ``pytest-qt`` requires Python 3.6+. Works with either PySide6_, PySide2_, PyQt6_ or PyQt5_, picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide6`` - ``PySide2`` - ``PyQt6`` - ``PyQt5`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt6``, ``pyside2``, ``pyqt6`` or ```pyqt5``: .. code-block:: ini [pytest] qt_api=pyqt5 Alternatively, you can set the ``PYTEST_QT_API`` environment variable to the same values described above (the environment variable wins over the configuration if both are set). pytest-qt-4.0.2/docs/logging.rst0000644000175100001710000001616714061506270017412 0ustar runnerdocker00000000000000Qt Logging Capture ================== .. versionadded:: 1.4 Qt features its own logging mechanism through ``qInstallMessageHandler`` and ``qDebug``, ``qWarning``, ``qCritical`` functions. These are used by Qt to print warning messages when internal errors occur. ``pytest-qt`` automatically captures these messages and displays them when a test fails, similar to what ``pytest`` does for ``stderr`` and ``stdout`` and the `pytest-catchlog `_ plugin. For example: .. code-block:: python from pytestqt.qt_compat import qt_api def do_something(): qt_api.qWarning("this is a WARNING message") def test_foo(): do_something() assert 0 .. code-block:: bash $ pytest test.py -q F ================================== FAILURES =================================== _________________________________ test_types __________________________________ def test_foo(): do_something() > assert 0 E assert 0 test.py:8: AssertionError ---------------------------- Captured Qt messages ----------------------------- QtWarningMsg: this is a WARNING message 1 failed in 0.01 seconds Disabling Logging Capture ------------------------- Qt logging capture can be disabled altogether by passing the ``--no-qt-log`` to the command line, which will fallback to the default Qt behavior of printing emitted messages directly to ``stderr``: .. code-block:: bash pytest test.py -q --no-qt-log F ================================== FAILURES =================================== _________________________________ test_types __________________________________ def test_foo(): do_something() > assert 0 E assert 0 test.py:8: AssertionError ---------------------------- Captured stderr call ----------------------------- this is a WARNING message Using pytest's ``-s`` (``--capture=no``) option will also disable Qt log capturing. qtlog fixture ------------- ``pytest-qt`` also provides a ``qtlog`` fixture that can used to check if certain messages were emitted during a test:: def do_something(): qWarning('this is a WARNING message') def test_foo(qtlog): do_something() emitted = [(m.type, m.message.strip()) for m in qtlog.records] assert emitted == [(QtWarningMsg, 'this is a WARNING message')] ``qtlog.records`` is a list of :class:`Record ` instances. Logging can also be disabled on a block of code using the ``qtlog.disabled()`` context manager, or with the ``pytest.mark.no_qt_log`` mark: .. code-block:: python def test_foo(qtlog): with qtlog.disabled(): # logging is disabled within the context manager do_something() @pytest.mark.no_qt_log def test_bar(): # logging is disabled for the entire test do_something() Keep in mind that when logging is disabled, ``qtlog.records`` will always be an empty list. Log Formatting -------------- The output format of the messages can also be controlled by using the ``--qt-log-format`` command line option, which accepts a string with standard ``{}`` formatting which can make use of attribute interpolation of the record objects: .. code-block:: bash $ pytest test.py --qt-log-format="{rec.when} {rec.type_name}: {rec.message}" Keep in mind that you can make any of the options above the default for your project by using pytest's standard ``addopts`` option in you ``pytest.ini`` file: .. code-block:: ini [pytest] qt_log_format = {rec.when} {rec.type_name}: {rec.message} Automatically failing tests when logging messages are emitted ------------------------------------------------------------- Printing messages to ``stderr`` is not the best solution to notice that something might not be working as expected, specially when running in a continuous integration server where errors in logs are rarely noticed. You can configure ``pytest-qt`` to automatically fail a test if it emits a message of a certain level or above using the ``qt_log_level_fail`` ini option: .. code-block:: ini [pytest] qt_log_level_fail = CRITICAL With this configuration, any test which emits a CRITICAL message or above will fail, even if no actual asserts fail within the test: .. code-block:: python from pytestqt.qt_compat import qCritical def do_something(): qCritical("WM_PAINT failed") def test_foo(qtlog): do_something() .. code-block:: bash >pytest test.py --color=no -q F ================================== FAILURES =================================== __________________________________ test_foo ___________________________________ test.py:5: Failure: Qt messages with level CRITICAL or above emitted ---------------------------- Captured Qt messages ----------------------------- QtCriticalMsg: WM_PAINT failed The possible values for ``qt_log_level_fail`` are: * ``NO``: disables test failure by log messages. * ``DEBUG``: messages emitted by ``qDebug`` function or above. * ``WARNING``: messages emitted by ``qWarning`` function or above. * ``CRITICAL``: messages emitted by ``qCritical`` function only. If some failures are known to happen and considered harmless, they can be ignored by using the ``qt_log_ignore`` ini option, which is a list of regular expressions matched using ``re.search``: .. code-block:: ini [pytest] qt_log_level_fail = CRITICAL qt_log_ignore = WM_DESTROY.*sent WM_PAINT failed .. code-block:: bash pytest test.py --color=no -q . 1 passed in 0.01 seconds Messages which do not match any of the regular expressions defined by ``qt_log_ignore`` make tests fail as usual: .. code-block:: python def do_something(): qCritical("WM_PAINT not handled") qCritical("QObject: widget destroyed in another thread") def test_foo(qtlog): do_something() .. code-block:: bash pytest test.py --color=no -q F ================================== FAILURES =================================== __________________________________ test_foo ___________________________________ test.py:6: Failure: Qt messages with level CRITICAL or above emitted ---------------------------- Captured Qt messages ----------------------------- QtCriticalMsg: WM_PAINT not handled (IGNORED) QtCriticalMsg: QObject: widget destroyed in another thread You can also override the ``qt_log_level_fail`` setting and extend ``qt_log_ignore`` patterns from ``pytest.ini`` in some tests by using a mark with the same name: .. code-block:: python def do_something(): qCritical("WM_PAINT not handled") qCritical("QObject: widget destroyed in another thread") @pytest.mark.qt_log_level_fail("CRITICAL") @pytest.mark.qt_log_ignore("WM_DESTROY.*sent", "WM_PAINT failed") def test_foo(qtlog): do_something() If you would like to override the list of ignored patterns instead, pass ``extend=False`` to the ``qt_log_ignore`` mark: .. code-block:: python @pytest.mark.qt_log_ignore("WM_DESTROY.*sent", extend=False) def test_foo(qtlog): do_something() pytest-qt-4.0.2/docs/make.bat0000644000175100001710000001175614061506270016636 0ustar runnerdocker00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pytest-qt.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pytest-qt.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end pytest-qt-4.0.2/docs/modeltester.rst0000644000175100001710000000425014061506270020301 0ustar runnerdocker00000000000000Model Tester ============ .. versionadded:: 2.0 ``pytest-qt`` includes a fixture that helps testing `QAbstractItemModel`_ implementations. The implementation is copied from the C++ code as described on the `Qt Wiki `_, and it continuously checks a model as it changes, helping to verify the state and catching many common errors the moment they show up. Some of the conditions caught include: * Verifying X number of rows have been inserted in the correct place after the signal ``rowsAboutToBeInserted()`` says X rows will be inserted. * The parent of the first index of the first row is a ``QModelIndex()`` * Calling ``index()`` twice in a row with the same values will return the same ``QModelIndex`` * If ``rowCount()`` says there are X number of rows, model test will verify that is true. * Many possible off by one bugs * ``hasChildren()`` returns true if ``rowCount()`` is greater then zero. * and many more... To use it, create an instance of your model implementation, fill it with some items and call ``qtmodeltester.check``: .. code-block:: python def test_standard_item_model(qtmodeltester): model = QStandardItemModel() items = [QStandardItem(str(i)) for i in range(4)] model.setItem(0, 0, items[0]) model.setItem(0, 1, items[1]) model.setItem(1, 0, items[2]) model.setItem(1, 1, items[3]) qtmodeltester.check(model) If the tester finds a problem the test will fail with an assert pinpointing the issue. Qt/Python tester ---------------- Starting with PyQt5 5.11, Qt's ``QAbstractItemModelTester`` is exposed to Python. If it's available, by default, ``qtmodeltester.check`` will use the C++ implementation and fail tests if it emits any warnings. To use the Python implementation instead, use ``qtmodeltester.check(model, force_py=True)``. Credits ------- The source code was ported from `qabstractitemmodeltester.cpp`_ by `Florian Bruhin`_, many thanks! .. _qabstractitemmodeltester.cpp: http://code.qt.io/cgit/qt/qtbase.git/tree/src/testlib/qabstractitemmodeltester.cpp .. _Florian Bruhin: https://github.com/The-Compiler .. _QAbstractItemModel: http://doc.qt.io/qt-5/qabstractitemmodel.html pytest-qt-4.0.2/docs/note_dialogs.rst0000644000175100001710000000345514061506270020427 0ustar runnerdocker00000000000000A note about Modal Dialogs ========================== Simple Dialogs -------------- For QMessageBox.question one approach is to mock the function using the `monkeypatch `_ fixture: .. code-block:: python def test_Qt(qtbot, monkeypatch): simple = Simple() qtbot.addWidget(simple) monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) simple.query() assert simple.answer Custom Dialogs -------------- Suppose you have a custom dialog that asks the user for their name and age, and a form that uses it. One approach is to add a convenience function that also has the nice benefit of making testing easier, like this: .. code-block:: python class AskNameAndAgeDialog(QDialog): @classmethod def ask(cls, text, parent): dialog = cls(parent) dialog.text.setText(text) if dialog.exec_() == QDialog.Accepted: return dialog.getName(), dialog.getAge() else: return None, None This allows clients of the dialog to use it this way: .. code-block:: python name, age = AskNameAndAgeDialog.ask("Enter name and age because of bananas:", parent) if name is not None: # use name and age for bananas ... And now it is also easy to mock ``AskNameAndAgeDialog.ask`` when testing the form: .. code-block:: python def test_form_registration(qtbot, monkeypatch): form = RegistrationForm() monkeypatch.setattr( AskNameAndAgeDialog, "ask", classmethod(lambda *args: ("Jonh", 30)) ) qtbot.click(form.enter_info()) # calls AskNameAndAgeDialog.ask # test that the rest of the form correctly behaves as if # user entered "Jonh" and 30 as name and age pytest-qt-4.0.2/docs/qapplication.rst0000644000175100001710000000473314061506270020444 0ustar runnerdocker00000000000000Testing QApplication ==================== If your tests need access to a full ``QApplication`` instance to e.g. test exit behavior or custom application classes, you can use the techniques described below: Testing QApplication.exit() -------------------------------- Some ``pytest-qt`` features, most notably ``waitSignal`` and ``waitSignals``, depend on the Qt event loop being active. Calling ``QApplication.exit()`` from a test will cause the main event loop and auxiliary event loops to exit and all subsequent event loops to fail to start. This is a problem if some of your tests call an application functionality that calls ``QApplication.exit()``. One solution is to *monkeypatch* ``QApplication.exit()`` in such tests to ensure it was called by the application code but without effectively calling it. For example: .. code-block:: python def test_exit_button(qtbot, monkeypatch): exit_calls = [] monkeypatch.setattr(QApplication, "exit", lambda: exit_calls.append(1)) button = get_app_exit_button() qtbot.click(button) assert exit_calls == [1] Or using the ``mock`` package: .. code-block:: python def test_exit_button(qtbot): with mock.patch.object(QApplication, "exit"): button = get_app_exit_button() qtbot.click(button) assert QApplication.exit.call_count == 1 Testing Custom QApplications ---------------------------- It's possible to test custom ``QApplication`` classes, but you need to be careful to avoid multiple app instances in the same test. Assuming one defines a custom application like below: .. code-block:: python from pytestqt.qt_compat import qt_api class CustomQApplication(qt_api.QtWidgets.QApplication): def __init__(self, *argv): super().__init__(*argv) self.custom_attr = "xxx" def custom_function(self): pass If your tests require access to app-level functions, like ``CustomQApplication.custom_function()``, you can override the built-in ``qapp`` fixture in your ``conftest.py`` to use your own app: .. code-block:: python @pytest.fixture(scope="session") def qapp(): yield CustomQApplication([]) Setting a QApplication name --------------------------- By default, pytest-qt sets the ``QApplication.applicationName()`` to ``pytest-qt-qapp``. To use a custom name, you can set the ``qt_qapp_name`` option in ``pytest.ini``: .. code-block:: ini [pytest] qt_qapp_name = frobnicate-tests pytest-qt-4.0.2/docs/reference.rst0000644000175100001710000000105014061506270017703 0ustar runnerdocker00000000000000Reference ========= QtBot ----- .. module:: pytestqt.qtbot .. autoclass:: QtBot TimeoutError ------------ .. autoclass:: TimeoutError SignalBlocker ------------- .. module:: pytestqt.wait_signal .. autoclass:: SignalBlocker MultiSignalBlocker ------------------ .. autoclass:: MultiSignalBlocker SignalEmittedError ------------------ .. autoclass:: SignalEmittedError Record ------ .. module:: pytestqt.logging .. autoclass:: Record qapp fixture ------------ .. module:: pytestqt.plugin .. autofunction:: qapp .. autofunction:: qapp_args pytest-qt-4.0.2/docs/signals.rst0000644000175100001710000002151514061506270017415 0ustar runnerdocker00000000000000waitSignal: Waiting for threads, processes, etc. ================================================ .. versionadded:: 1.2 If your program has long running computations running in other threads or processes, you can use :meth:`qtbot.waitSignal ` to block a test until a signal is emitted (such as ``QThread.finished``) or a timeout is reached. This makes it easy to write tests that wait until a computation running in another thread or process is completed before ensuring the results are correct: .. code-block:: python def test_long_computation(qtbot): app = Application() # Watch for the app.worker.finished signal, then start the worker. with qtbot.waitSignal(app.worker.finished, timeout=10000) as blocker: blocker.connect(app.worker.failed) # Can add other signals to blocker app.worker.start() # Test will block at this point until either the "finished" or the # "failed" signal is emitted. If 10 seconds passed without a signal, # TimeoutError will be raised. assert_application_results(app) raising parameter ----------------- .. versionadded:: 1.4 .. versionchanged:: 2.0 You can pass ``raising=False`` to avoid raising a :class:`qtbot.TimeoutError ` if the timeout is reached before the signal is triggered: .. code-block:: python def test_long_computation(qtbot): ... with qtbot.waitSignal(app.worker.finished, raising=False) as blocker: app.worker.start() assert_application_results(app) # qtbot.TimeoutError is not raised, but you can still manually # check whether the signal was triggered: assert blocker.signal_triggered, "process timed-out" .. _qt_default_raising: qt_default_raising ini option ----------------------------- .. versionadded:: 1.11 .. versionchanged:: 2.0 .. versionchanged:: 3.1 The ``qt_default_raising`` ini option can be used to override the default value of the ``raising`` parameter of the ``qtbot.waitSignal`` and ``qtbot.waitSignals`` functions when omitted: .. code-block:: ini [pytest] qt_default_raising = false Calls which explicitly pass the ``raising`` parameter are not affected. check_params_cb parameter ------------------------- .. versionadded:: 2.0 If the signal has parameters you want to compare with expected values, you can pass ``check_params_cb=some_callable`` that compares the provided signal parameters to some expected parameters. It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. .. code-block:: python def test_status_100(status): """Return true if status has reached 100%.""" return status == 100 def test_status_complete(qtbot): app = Application() # the following raises if the worker's status signal (which has an int parameter) wasn't raised # with value=100 within the default timeout with qtbot.waitSignal( app.worker.status, raising=True, check_params_cb=test_status_100 ) as blocker: app.worker.start() timeout parameter ----------------- The ``timeout`` parameter specifies how long ``waitSignal`` should wait for a signal to arrive. If the timeout is ``None``, there won't be any timeout, i.e. it'll wait indefinitely. If the timeout is set to ``0``, it's expected that the signal arrives directly in the code inside the ``with qtbot.waitSignal(...):`` block. Getting arguments of the emitted signal --------------------------------------- .. versionadded:: 1.10 The arguments emitted with the signal are available as the ``args`` attribute of the blocker: .. code-block:: python def test_signal(qtbot): ... with qtbot.waitSignal(app.got_cmd) as blocker: app.listen() assert blocker.args == ["test"] Signals without arguments will set ``args`` to an empty list. If the time out is reached instead, ``args`` will be ``None``. Getting all arguments of non-matching arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 2.1 When using the ``check_params_cb`` parameter, it may happen that the provided signal is received multiple times with different parameter values, which may or may not match the requirements of the callback. ``all_args`` then contains the list of signal parameters (as tuple) in the order they were received. waitSignals ----------- .. versionadded:: 1.4 If you have to wait until **all** signals in a list are triggered, use :meth:`qtbot.waitSignals `, which receives a list of signals instead of a single signal. As with :meth:`qtbot.waitSignal `, it also supports the ``raising`` parameter:: def test_workers(qtbot): workers = spawn_workers() with qtbot.waitSignals([w.finished for w in workers]): for w in workers: w.start() # this will be reached after all workers emit their "finished" # signal or a qtbot.TimeoutError will be raised assert_application_results(app) check_params_cbs parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 2.0 Corresponding to the ``check_params_cb`` parameter of ``waitSignal`` you can use the ``check_params_cbs`` parameter to check whether one or more of the provided signals are emitted with expected parameters. Provide a list of callables, each matching the signature of the corresponding signal in ``signals`` (just like a slot function would). Like for ``waitSignal``, each callable has to return ``True`` if parameters match, ``False`` otherwise. Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the corresponding signal. If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised. The following example shows that the ``app.worker.status`` signal has to be emitted with values 50 and 100, and the ``app.worker.finished`` signal has to be emitted too (for which no signal parameter evaluation takes place). .. code-block:: python def test_status_100(status): """Return true if status has reached 100%.""" return status == 100 def test_status_50(status): """Return true if status has reached 50%.""" return status == 50 def test_status_complete(qtbot): app = Application() signals = [app.worker.status, app.worker.status, app.worker.finished] callbacks = [test_status_50, test_status_100, None] with qtbot.waitSignals( signals, raising=True, check_params_cbs=callbacks ) as blocker: app.worker.start() order parameter ^^^^^^^^^^^^^^^ .. versionadded:: 2.0 By default a test using ``qtbot.waitSignals`` completes successfully if *all* signals in ``signals`` are emitted, irrespective of their exact order. The ``order`` parameter can be set to ``"strict"`` to enforce strict signal order. Exemplary, this means that ``blocker.signal_triggered`` will be ``False`` if ``waitSignals`` expects the signals ``[a, b]`` but the sender emitted signals ``[a, a, b]``. .. note:: The tested component can still emit signals unknown to the blocker. E.g. ``blocker.waitSignals([a, b], raising=True, order="strict")`` won't raise if the signal-sender emits signals ``[a, c, b]``, as ``c`` is not part of the observed signals. A third option is to set ``order="simple"`` which is like "strict", but signals may be emitted in-between the provided ones, e.g. if the expected signals are ``[a, b, c]`` and the sender actually emits ``[a, a, b, a, c]``, the test completes successfully (it would fail with ``order="strict"``). Getting emitted signals and arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 2.1 To determine which of the expected signals were emitted during a ``wait()`` you can use ``blocker.all_signals_and_args`` which contains a list of :class:`wait_signal.SignalAndArgs ` objects, indicating the signals (and their arguments) in the order they were received. Making sure a given signal is not emitted ----------------------------------------- .. versionadded:: 1.11 If you want to ensure a signal is **not** emitted in a given block of code, use the :meth:`qtbot.assertNotEmitted ` context manager: .. code-block:: python def test_no_error(qtbot): ... with qtbot.assertNotEmitted(app.worker.error): app.worker.start() By default, this only catches signals emitted directly inside the block. You can pass ``wait=...`` to wait for a given duration (in milliseconds) for asynchronous signals to (not) arrive: .. code-block:: python def test_no_error(qtbot): ... with qtbot.assertNotEmitted(page.loadFinished, wait=100): page.runJavaScript("document.getElementById('not-a-link').click()") pytest-qt-4.0.2/docs/troubleshooting.rst0000644000175100001710000000760314061506270021206 0ustar runnerdocker00000000000000Troubleshooting =============== tox: ``InvocationError`` without further information ---------------------------------------------------- It might happen that your ``tox`` run finishes abruptly without any useful information, e.g.:: ERROR: InvocationError: '/path/to/project/.tox/py36/bin/python setup.py test --addopts --doctest-modules' ___ summary _____ ERROR: py36: commands failed ``pytest-qt`` needs a ``DISPLAY`` to run, otherwise ``Qt`` calls ``abort()`` and the process crashes immediately. One solution is to use the `pytest-xvfb`_ plugin which takes care of the grifty details automatically, starting up a virtual framebuffer service, initializing variables, etc. This is the recommended solution if you are running in CI servers without a GUI, for example in Travis or CircleCI. Alternatively, ``tox`` users may edit ``tox.ini`` to allow the relevant variables to be passed to the underlying ``pytest`` invocation: .. code-block:: ini [testenv] passenv = DISPLAY XAUTHORITY Note that this solution will only work in boxes with a GUI. More details can be found in `issue #170`_. .. _pytest-xvfb: https://pypi.python.org/pypi/pytest-xvfb/ .. _issue #170: https://github.com/pytest-dev/pytest-qt/issues/170 xvfb: ``AssertionError``, ``TimeoutError`` when using ``waitUntil``, ``waitExposed`` and UI events. --------------------------------------------------------------------------------------------------- When using ``xvfb`` or equivalent make sure to have a window manager running otherwise UI events will not work properly. If you are running your code on Travis-CI make sure that your ``.travis.yml`` has the following content: .. code-block:: yaml sudo: required before_install: - sudo apt-get update - sudo apt-get install -y xvfb herbstluftwm install: - "export DISPLAY=:99.0" - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset" - sleep 3 before_script: - "herbstluftwm &" - sleep 1 More details can be found in `issue #206`_. .. _issue #206: https://github.com/pytest-dev/pytest-qt/issues/206 GitHub Actions ---------------- When using ``ubuntu-latest`` on Github Actions, the package ``libxkbcommon-x11-0`` has to be installed, ``DISPLAY`` should be set and ``xvfb`` run. More details can be found in `issue #293`_. .. _issue #293: https://github.com/pytest-dev/pytest-qt/issues/293 Since Qt in version 5.15 ``xcb`` libraries are not distributed with Qt so this library in version at least 1.11 on runner. See more in https://codereview.qt-project.org/c/qt/qtbase/+/253905 For Github Actions, Azure pipelines and Travis-CI you will need to install ``libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0`` As an example, here is a working config : .. code-block:: yaml name: my qt ci in github actions on: [push, pull_request] jobs: Linux: runs-on: ${{ matrix.os }} strategy: matrix: os : [ubuntu-latest] python: [3.7] env: DISPLAY: ':99.0' steps: - name: get repo uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: ${{ matrix.python }} - name: setup ${{ matrix.os }} run: | sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX pytest-xvfb ~~~~~~~~~~~ Instead of running Xvfb manually it is possible to use ``pytest-xvfb`` plugin. pytest-qt-4.0.2/docs/tutorial.rst0000644000175100001710000000500014061506270017607 0ustar runnerdocker00000000000000Tutorial ======== ``pytest-qt`` registers a new fixture_ named ``qtbot``, which acts as *bot* in the sense that it can send keyboard and mouse events to any widgets being tested. This way, the programmer can simulate user interaction while checking if GUI controls are behaving in the expected manner. .. _fixture: http://pytest.org/latest/fixture.html To illustrate that, consider a widget constructed to allow the user to find files in a given directory inside an application. .. image:: _static/find_files_dialog.png :align: center It is a very simple dialog, where the user enters a standard file mask, optionally enters file text to search for and a button to browse for the desired directory. Its source code is available here_, .. _here: https://github.com/nicoddemus/PySide-Examples/blob/master/examples/dialogs/findfiles.py To test this widget's basic functionality, create a test function:: def test_basic_search(qtbot, tmpdir): ''' test to ensure basic find files functionality is working. ''' tmpdir.join('video1.avi').ensure() tmpdir.join('video1.srt').ensure() tmpdir.join('video2.avi').ensure() tmpdir.join('video2.srt').ensure() Here the first parameter indicates that we will be using a ``qtbot`` fixture to control our widget. The other parameter is pytest's standard tmpdir_ that we use to create some files that will be used during our test. .. _tmpdir: http://pytest.org/latest/tmpdir.html Now we create the widget to test and register it:: window = Window() window.show() qtbot.addWidget(window) .. tip:: Registering widgets is not required, but recommended because it will ensure those widgets get properly closed after each test is done. Now we use ``qtbot`` methods to simulate user interaction with the dialog:: window.fileComboBox.clear() qtbot.keyClicks(window.fileComboBox, '*.avi') window.directoryComboBox.clear() qtbot.keyClicks(window.directoryComboBox, str(tmpdir)) The method ``keyClicks`` is used to enter text in the editable combo box, selecting the desired mask and directory. We then simulate a user clicking the button with the ``mouseClick`` method:: qtbot.mouseClick(window.findButton, QtCore.Qt.LeftButton) Once this is done, we inspect the results widget to ensure that it contains the expected files we created earlier:: assert window.filesTable.rowCount() == 2 assert window.filesTable.item(0, 0).text() == 'video1.avi' assert window.filesTable.item(1, 0).text() == 'video2.avi' pytest-qt-4.0.2/docs/virtual_methods.rst0000644000175100001710000000471114061506270021165 0ustar runnerdocker00000000000000Exceptions in virtual methods ============================= .. versionadded:: 1.1 It is common in Qt programming to override virtual C++ methods to customize behavior, like listening for mouse events, implement drawing routines, etc. Fortunately, all Python bindings for Qt support overriding these virtual methods naturally in your Python code:: class MyWidget(QWidget): # mouseReleaseEvent def mouseReleaseEvent(self, ev): print('mouse released at: %s' % ev.pos()) In ``PyQt5`` and ``PyQt6``, exceptions in virtual methods will by default call abort(), which will crash the interpreter. All other Qt wrappers will print the exception stacktrace and return a default value back to C++/Qt (if a return value is required). This might be surprising for Python users which are used to exceptions being raised at the calling point: For example, the following code will just print a stack trace without raising any exception:: class MyWidget(QWidget): def mouseReleaseEvent(self, ev): raise RuntimeError('unexpected error') w = MyWidget() QTest.mouseClick(w, QtCore.Qt.LeftButton) To make testing Qt code less surprising, ``pytest-qt`` automatically installs an exception hook which captures errors and fails tests when exceptions are raised inside virtual methods, like this:: E Failed: Qt exceptions in virtual methods: E ________________________________________________________________________________ E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event E raise RuntimeError('unexpected error') E E RuntimeError: unexpected error Disabling the automatic exception hook -------------------------------------- You can disable the automatic exception hook on individual tests by using a ``qt_no_exception_capture`` marker:: @pytest.mark.qt_no_exception_capture def test_buttons(qtbot): ... Or even disable it for your entire project in your ``pytest.ini`` file: .. code-block:: ini [pytest] qt_no_exception_capture = 1 This might be desirable if you plan to install a custom exception hook. .. note:: Starting with ``PyQt5.5``, exceptions raised during virtual methods will actually trigger an ``abort()``, crashing the Python interpreter. For this reason, disabling exception capture in ``PyQt5.5+`` and ``PyQt6`` is not recommended unless you install your own exception hook. pytest-qt-4.0.2/docs/wait_callback.rst0000644000175100001710000000356614061506270020543 0ustar runnerdocker00000000000000waitCallback: Waiting for methods taking a callback =================================================== .. versionadded:: 3.1 Some methods in Qt (especially ``QtWebEngine``) take a callback as argument, which gets called by Qt once a given operation is done. To test such code, you can use :meth:`qtbot.waitCallback ` which waits until the callback has been called or a timeout is reached. The ``qtbot.waitCallback()`` method returns a callback which is callable directly. For example: .. code-block:: python def test_js(qtbot): page = QWebEnginePage() with qtbot.waitCallback() as cb: page.runJavaScript("1 + 1", cb) cb.assert_called_with(2) # result of the last js statement Anything following the ``with`` block will be run only after the callback has been called. If the callback doesn't get called during the given timeout, :class:`qtbot.TimeoutError ` is raised. If it is called more than once, :class:`qtbot.CallbackCalledTwiceError ` is raised. raising parameter ----------------- Similarly to ``qtbot.waitSignal``, you can pass a ``raising=False`` parameter (or set the ``qt_default_raising`` ini option) to avoid raising an exception on timeouts. See :doc:`signals` for details. Getting arguments the callback was called with ---------------------------------------------- After the callback is called, the arguments and keyword arguments passed to it are available via ``.args`` (as a list) and ``.kwargs`` (as a dict), respectively. In the example above, we could check the result via: .. code-block:: python assert cb.args == [2] assert cb.kwargs == {} Instead of checking the arguments by hand, you can use ``.assert_called_with()`` to make sure the callback was called with the given arguments: .. code-block:: python cb.assert_called_with(2) pytest-qt-4.0.2/docs/wait_until.rst0000644000175100001710000000567414061506270020144 0ustar runnerdocker00000000000000waitUntil: Waiting for arbitrary conditions =========================================== .. versionadded:: 2.0 Sometimes your tests need to wait a certain condition which does not trigger a signal, for example that a certain control gained focus or a ``QListView`` has been populated with all items. For those situations you can use :meth:`qtbot.waitUntil ` to wait until a certain condition has been met or a timeout is reached. This is specially important in X window systems due to their asynchronous nature, where you can't rely on the fact that the result of an action will be immediately available. For example: .. code-block:: python def test_validate(qtbot): window = MyWindow() window.edit.setText("not a number") # after focusing, should update status label window.edit.setFocus() assert window.status.text() == "Please input a number" The ``window.edit.setFocus()`` may not be processed immediately, only in a future event loop, which might lead to this test to work sometimes and fail in others (a *flaky* test). A better approach in situations like this is to use ``qtbot.waitUntil`` with a callback with your assertion: .. code-block:: python def test_validate(qtbot): window = MyWindow() window.edit.setText("not a number") # after focusing, should update status label window.edit.setFocus() def check_label(): assert window.status.text() == "Please input a number" qtbot.waitUntil(check_label) ``qtbot.waitUntil`` will periodically call ``check_label`` until it no longer raises ``AssertionError`` or a timeout is reached. If a timeout is reached, a :class:`qtbot.TimeoutError ` is raised from the last assertion error and the test will fail: :: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def check_label(): > assert window.status.text() == "Please input a number" E AssertionError: assert 'OK' == 'Please input a number' E - OK E + Please input a number _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > qtbot.waitUntil(check_label) E pytestqt.exceptions.TimeoutError: waitUntil timed out in 1000 miliseconds A second way to use ``qtbot.waitUntil`` is to pass a callback which returns ``True`` when the condition is met or ``False`` otherwise. It is usually terser than using a separate callback with ``assert`` statement, but it produces a generic message when it fails because it can't make use of ``pytest``'s assertion rewriting: .. code-block:: python def test_validate(qtbot): window = MyWindow() window.edit.setText("not a number") # after focusing, should update status label window.edit.setFocus() qtbot.waitUntil(lambda: window.edit.hasFocus()) assert window.status.text() == "Please input a number" pytest-qt-4.0.2/requirements.txt0000644000175100001710000000000714061506270017550 0ustar runnerdocker00000000000000pytest pytest-qt-4.0.2/setup.cfg0000644000175100001710000000033614061506274016116 0ustar runnerdocker00000000000000[bdist_wheel] universal = 1 [tool:pytest] testpaths = tests addopts = --strict-markers --strict-config xfail_strict = true markers = filterwarnings: pytest's filterwarnings marker [egg_info] tag_build = tag_date = 0 pytest-qt-4.0.2/setup.py0000644000175100001710000000313114061506270015777 0ustar runnerdocker00000000000000from pathlib import Path from setuptools import setup, find_packages setup( name="pytest-qt", packages=find_packages(where="src"), package_dir={"": "src"}, entry_points={"pytest11": ["pytest-qt = pytestqt.plugin"]}, install_requires=["pytest>=3.0.0"], extras_require={ "doc": ["sphinx", "sphinx_rtd_theme"], "dev": ["pre-commit", "tox"], }, # metadata for upload to PyPI author="Bruno Oliveira", author_email="nicoddemus@gmail.com", description="pytest support for PyQt and PySide applications", long_description=Path("README.rst").read_text(encoding="UTF-8"), license="MIT", keywords="pytest qt test unittest", url="http://github.com/pytest-dev/pytest-qt", use_scm_version={"write_to": "src/pytestqt/_version.py"}, setup_requires=["setuptools_scm"], python_requires=">=3.6", classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Desktop Environment :: Window Managers", "Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Testing", "Topic :: Software Development :: User Interfaces", ], tests_require=["pytest"], ) pytest-qt-4.0.2/src/0000755000175100001710000000000014061506274015062 5ustar runnerdocker00000000000000pytest-qt-4.0.2/src/pytest_qt.egg-info/0000755000175100001710000000000014061506274020610 5ustar runnerdocker00000000000000pytest-qt-4.0.2/src/pytest_qt.egg-info/PKG-INFO0000644000175100001710000001622314061506274021711 0ustar runnerdocker00000000000000Metadata-Version: 2.1 Name: pytest-qt Version: 4.0.2 Summary: pytest support for PyQt and PySide applications Home-page: http://github.com/pytest-dev/pytest-qt Author: Bruno Oliveira Author-email: nicoddemus@gmail.com License: MIT Keywords: pytest qt test unittest Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Desktop Environment :: Window Managers Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: User Interfaces Requires-Python: >=3.6 Provides-Extra: doc Provides-Extra: dev License-File: LICENSE ========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PyQt6`_ applications. The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp`` creation as needed and provides methods to simulate user interaction, like key presses and mouse clicks: .. code-block:: python def test_hello(qtbot): widget = HelloWidget() qtbot.addWidget(widget) # click in the Greet button and make sure it updates the appropriate label qtbot.mouseClick(widget.button_greet, qt_api.QtCore.Qt.MouseButton.LeftButton) assert widget.greet_label.text() == "Hello!" .. _PySide2: https://pypi.org/project/PySide2/ .. _PySide6: https://pypi.org/project/PySide6/ .. _PyQt5: https://pypi.org/project/PyQt5/ .. _PyQt6: https://pypi.org/project/PyQt6/ .. _pytest: http://pytest.org This allows you to test and make sure your view layer is behaving the way you expect after each code change. .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |ci| image:: https://github.com/pytest-dev/pytest-qt/workflows/build/badge.svg :target: https://github.com/pytest-dev/pytest-qt/actions .. |coverage| image:: http://img.shields.io/coveralls/pytest-dev/pytest-qt.svg :target: https://coveralls.io/r/pytest-dev/pytest-qt .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |ci| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ Since version 4.0.0, ``pytest-qt`` requires Python 3.6+. Works with either PySide6_, PySide2_, PyQt6_ or PyQt5_, picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide6`` - ``PySide2`` - ``PyQt6`` - ``PyQt5`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt6``, ``pyside2``, ``pyqt6`` or ```pyqt5``: .. code-block:: ini [pytest] qt_api=pyqt5 Alternatively, you can set the ``PYTEST_QT_API`` environment variable to the same values described above (the environment variable wins over the configuration if both are set). Documentation ============= Full documentation and tutorial available at `Read the Docs`_. .. _Read The Docs: https://pytest-qt.readthedocs.io Change Log ========== Please consult the `changelog page`_. .. _changelog page: https://pytest-qt.readthedocs.io/en/latest/changelog.html Bugs/Requests ============= Please report any issues or feature requests in the `issue tracker`_. .. _issue tracker: https://github.com/pytest-dev/pytest-qt/issues Contributing ============ Contributions are welcome, so feel free to submit a bug or feature request. Pull requests are highly appreciated! If you can, include some tests that exercise the new code or test that a bug has been fixed, and make sure to include yourself in the contributors list. :) To prepare your environment, create a virtual environment and install ``pytest-qt`` in editable mode with ``dev`` extras:: $ pip install --editable .[dev] After that install ``pre-commit`` for pre-commit checks:: $ pre-commit install Running tests ------------- Tests are run using `tox`_:: $ tox -e py37-pyside2,py37-pyqt5 ``pytest-qt`` is formatted using `black `_ and uses `pre-commit `_ for linting checks before commits. You can install ``pre-commit`` locally with:: $ pip install pre-commit $ pre-commit install Related projects ---------------- - `pytest-xvfb `_ allows to run a virtual xserver (Xvfb) on Linux to avoid GUI elements popping up on the screen or for easy CI testing - `pytest-qml `_ allows running QML tests from pytest Contributors ------------ Many thanks to: - Igor T. Ghisi (`@itghisi `_); - John David Reaver (`@jdreaver `_); - Benjamin Hedrich (`@bh `_); - Benjamin Audren (`@baudren `_); - Fabio Zadrozny (`@fabioz `_); - Datalyze Solutions (`@datalyze-solutions `_); - Florian Bruhin (`@The-Compiler `_); - Guilherme Quentel Melo (`@gqmelo `_); - Francesco Montesano (`@montefra `_); - Roman Yurchak (`@rth `_) - Christian Karl (`@karlch `_) **Powered by** .. |pycharm| image:: https://www.jetbrains.com/pycharm/docs/logo_pycharm.png :target: https://www.jetbrains.com/pycharm .. |pydev| image:: http://www.pydev.org/images/pydev_banner3.png :target: https://www.pydev.org |pycharm| |pydev| .. _tox: https://tox.readthedocs.io pytest-qt-4.0.2/src/pytest_qt.egg-info/SOURCES.txt0000644000175100001710000000233414061506274022476 0ustar runnerdocker00000000000000.gitattributes .gitignore .pre-commit-config.yaml .project .pydevproject CHANGELOG.rst HOWTORELEASE.rst LICENSE README.rst requirements.txt setup.cfg setup.py tox.ini .github/FUNDING.yml .github/workflows/main.yml docs/.gitignore docs/Makefile docs/changelog.rst docs/conf.py docs/index.rst docs/intro.rst docs/logging.rst docs/make.bat docs/modeltester.rst docs/note_dialogs.rst docs/qapplication.rst docs/reference.rst docs/signals.rst docs/troubleshooting.rst docs/tutorial.rst docs/virtual_methods.rst docs/wait_callback.rst docs/wait_until.rst docs/_static/find_files_dialog.png src/pytest_qt.egg-info/PKG-INFO src/pytest_qt.egg-info/SOURCES.txt src/pytest_qt.egg-info/dependency_links.txt src/pytest_qt.egg-info/entry_points.txt src/pytest_qt.egg-info/requires.txt src/pytest_qt.egg-info/top_level.txt src/pytestqt/__init__.py src/pytestqt/_version.py src/pytestqt/exceptions.py src/pytestqt/logging.py src/pytestqt/modeltest.py src/pytestqt/plugin.py src/pytestqt/qt_compat.py src/pytestqt/qtbot.py src/pytestqt/utils.py src/pytestqt/wait_signal.py tests/conftest.py tests/test_basics.py tests/test_exceptions.py tests/test_logging.py tests/test_modeltest.py tests/test_qtest_proxies.py tests/test_wait_signal.py tests/test_wait_until.pypytest-qt-4.0.2/src/pytest_qt.egg-info/dependency_links.txt0000644000175100001710000000000114061506274024656 0ustar runnerdocker00000000000000 pytest-qt-4.0.2/src/pytest_qt.egg-info/entry_points.txt0000644000175100001710000000005014061506274024101 0ustar runnerdocker00000000000000[pytest11] pytest-qt = pytestqt.plugin pytest-qt-4.0.2/src/pytest_qt.egg-info/requires.txt0000644000175100001710000000010314061506274023202 0ustar runnerdocker00000000000000pytest>=3.0.0 [dev] pre-commit tox [doc] sphinx sphinx_rtd_theme pytest-qt-4.0.2/src/pytest_qt.egg-info/top_level.txt0000644000175100001710000000001114061506274023332 0ustar runnerdocker00000000000000pytestqt pytest-qt-4.0.2/src/pytestqt/0000755000175100001710000000000014061506274016757 5ustar runnerdocker00000000000000pytest-qt-4.0.2/src/pytestqt/__init__.py0000644000175100001710000000016514061506270021066 0ustar runnerdocker00000000000000# _version is automatically generated by setuptools_scm from pytestqt._version import version __version__ = version pytest-qt-4.0.2/src/pytestqt/_version.py0000644000175100001710000000021614061506274021154 0ustar runnerdocker00000000000000# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = '4.0.2' version_tuple = (4, 0, 2) pytest-qt-4.0.2/src/pytestqt/exceptions.py0000644000175100001710000000564514061506270021520 0ustar runnerdocker00000000000000import functools import sys import traceback from contextlib import contextmanager import pytest from pytestqt.utils import get_marker @contextmanager def capture_exceptions(): """ Context manager that captures exceptions that happen insides its context, and returns them as a list of (type, value, traceback) after the context ends. """ manager = _QtExceptionCaptureManager() manager.start() try: yield manager.exceptions finally: manager.finish() def _except_hook(type_, value, tback, exceptions=None): """Hook functions installed by _QtExceptionCaptureManager""" exceptions.append((type_, value, tback)) sys.stderr.write(format_captured_exceptions([(type_, value, tback)])) class _QtExceptionCaptureManager: """ Manages exception capture context. """ def __init__(self): self.old_hook = None self.exceptions = [] def start(self): """Start exception capturing by installing a hook into sys.excepthook that records exceptions received into ``self.exceptions``. """ self.old_hook = sys.excepthook sys.excepthook = functools.partial(_except_hook, exceptions=self.exceptions) def finish(self): """Stop exception capturing, restoring the original hook. Can be called multiple times. """ if self.old_hook is not None: sys.excepthook = self.old_hook self.old_hook = None def fail_if_exceptions_occurred(self, when): """calls pytest.fail() with an informative message if exceptions have been captured so far. Before pytest.fail() is called, also finish capturing. """ if self.exceptions: self.finish() exceptions = self.exceptions self.exceptions = [] prefix = "%s ERROR: " % when msg = prefix + format_captured_exceptions(exceptions) del exceptions[:] # Don't keep exceptions alive longer. pytest.fail(msg, pytrace=False) def format_captured_exceptions(exceptions): """ Formats exceptions given as (type, value, traceback) into a string suitable to display as a test failure. """ from io import StringIO stream = StringIO() stream.write("Exceptions caught in Qt event loop:\n") sep = "_" * 80 + "\n" stream.write(sep) for (exc_type, value, tback) in exceptions: traceback.print_exception(exc_type, value, tback, file=stream) stream.write(sep) return stream.getvalue() def _is_exception_capture_enabled(item): """returns if exception capture is disabled for the given test item.""" disabled = get_marker(item, "qt_no_exception_capture") or item.config.getini( "qt_no_exception_capture" ) return not disabled class TimeoutError(Exception): """ .. versionadded:: 2.1 Exception thrown by :class:`pytestqt.qtbot.QtBot` methods. """ pass pytest-qt-4.0.2/src/pytestqt/logging.py0000644000175100001710000002663214061506270020764 0ustar runnerdocker00000000000000from collections import namedtuple from contextlib import contextmanager import datetime import re from py._code.code import TerminalRepr, ReprFileLocation import pytest from pytestqt.qt_compat import qt_api from pytestqt.utils import get_marker class QtLoggingPlugin: """ Pluging responsible for installing a QtMessageHandler before each test and augment reporting if the test failed with the messages captured. """ LOG_FAIL_OPTIONS = ["NO", "CRITICAL", "WARNING", "DEBUG", "INFO"] def __init__(self, config): self.config = config def pytest_runtest_setup(self, item): if get_marker(item, "no_qt_log"): return m = get_marker(item, "qt_log_ignore") if m: if not set(m.kwargs).issubset({"extend"}): raise ValueError( "Invalid keyword arguments in {!r} for " "qt_log_ignore mark.".format(m.kwargs) ) if m.kwargs.get("extend", True): config_regexes = self.config.getini("qt_log_ignore") ignore_regexes = config_regexes + list(m.args) else: ignore_regexes = m.args else: ignore_regexes = self.config.getini("qt_log_ignore") item.qt_log_capture = _QtMessageCapture(ignore_regexes) item.qt_log_capture._start() @pytest.mark.hookwrapper def pytest_runtest_makereport(self, item, call): """Add captured Qt messages to test item report if the call failed.""" outcome = yield if not hasattr(item, "qt_log_capture"): return if call.when == "call": report = outcome.get_result() m = get_marker(item, "qt_log_level_fail") if m: log_fail_level = m.args[0] else: log_fail_level = self.config.getini("qt_log_level_fail") assert log_fail_level in QtLoggingPlugin.LOG_FAIL_OPTIONS # make test fail if any records were captured which match # log_fail_level if report.outcome != "failed": for rec in item.qt_log_capture.records: is_modeltest_error = ( rec.context is not None and rec.context.category == "qt.modeltest" and rec.matches_level("WARNING") ) if ( rec.matches_level(log_fail_level) and not rec.ignored ) or is_modeltest_error: report.outcome = "failed" if report.longrepr is None: report.longrepr = _QtLogLevelErrorRepr( item, log_fail_level, is_modeltest_error ) break # if test has failed, add recorded messages to its terminal # representation if not report.passed: long_repr = getattr(report, "longrepr", None) if hasattr(long_repr, "addsection"): # pragma: no cover log_format = self.config.getoption("qt_log_format") context_format = None if log_format is None: context_format = "{rec.context.file}:{rec.context.function}:{rec.context.line}:\n" log_format = " {rec.type_name}: {rec.message}" lines = [] for rec in item.qt_log_capture.records: suffix = " (IGNORED)" if rec.ignored else "" if ( rec.context is not None and ( rec.context.file is not None or rec.context.function is not None or rec.context.line != 0 ) and context_format is not None ): context_line = context_format.format(rec=rec) lines.append(context_line) else: log_format = log_format.lstrip() line = log_format.format(rec=rec) + suffix lines.append(line) if lines: long_repr.addsection("Captured Qt messages", "\n".join(lines)) item.qt_log_capture._stop() del item.qt_log_capture class _QtMessageCapture: """ Captures Qt messages when its `handle` method is installed using qInstallMessageHandler, and stores them into `records` attribute. :attr _records: list of Record instances. :attr _ignore_regexes: list of regexes (as strings) that define if a record should be ignored. """ def __init__(self, ignore_regexes): self._records = [] self._ignore_regexes = ignore_regexes or [] self._previous_handler = None def _start(self): """ Start receiving messages from Qt. """ previous_handler = qt_api.QtCore.qInstallMessageHandler( self._handle_with_context ) self._previous_handler = previous_handler def _stop(self): """ Stop receiving messages from Qt, restoring the previously installed handler. """ qt_api.QtCore.qInstallMessageHandler(self._previous_handler) @contextmanager def disabled(self): """ Context manager that temporarily disables logging capture while inside it. """ self._stop() try: yield finally: self._start() _Context = namedtuple("_Context", "file function line category") def _append_new_record(self, msg_type, message, context): """ Creates a new Record instance and stores it. :param msg_type: Qt message typ :param message: message string, if bytes it will be converted to str. :param context: QMessageLogContext object or None """ def to_unicode(s): if isinstance(s, bytes): s = s.decode("utf-8", "replace") return s message = to_unicode(message) ignored = False for regex in self._ignore_regexes: if re.search(regex, message) is not None: ignored = True break if context is not None: context = self._Context( to_unicode(context.file), to_unicode(context.function), context.line, to_unicode(context.category), ) self._records.append(Record(msg_type, message, ignored, context)) def _handle_with_context(self, msg_type, context, message): """ Method to be installed using qInstallMessageHandler, stores each message into the `_records` attribute. """ self._append_new_record(msg_type, message, context=context) @property def records(self): """Access messages captured so far. :rtype: list of `Record` instances. """ return self._records[:] class Record: """Hold information about a message sent by one of Qt log functions. :ivar str message: message contents. :ivar Qt.QtMsgType type: enum that identifies message type :ivar str type_name: ``type`` as string: ``"QtInfoMsg"``, ``"QtDebugMsg"``, ``"QtWarningMsg"`` or ``"QtCriticalMsg"``. :ivar str log_type_name: type name similar to the logging package: ``INFO``, ``DEBUG``, ``WARNING`` and ``CRITICAL``. :ivar datetime.datetime when: when the message was captured :ivar bool ignored: If this record matches a regex from the "qt_log_ignore" option. :ivar context: a namedtuple containing the attributes ``file``, ``function``, ``line``. Can be None if no context is available for the message. """ def __init__(self, msg_type, message, ignored, context): self._type = msg_type self._message = message self._type_name = self._get_msg_type_name(msg_type) self._log_type_name = self._get_log_type_name(msg_type) self._when = datetime.datetime.now() self._ignored = ignored self._context = context message = property(lambda self: self._message) type = property(lambda self: self._type) type_name = property(lambda self: self._type_name) log_type_name = property(lambda self: self._log_type_name) when = property(lambda self: self._when) ignored = property(lambda self: self._ignored) context = property(lambda self: self._context) @classmethod def _get_msg_type_name(cls, msg_type): """ Return a string representation of the given QtMsgType enum value. """ if not getattr(cls, "_type_name_map", None): cls._type_name_map = { qt_api.QtCore.QtMsgType.QtDebugMsg: "QtDebugMsg", qt_api.QtCore.QtMsgType.QtWarningMsg: "QtWarningMsg", qt_api.QtCore.QtMsgType.QtCriticalMsg: "QtCriticalMsg", qt_api.QtCore.QtMsgType.QtFatalMsg: "QtFatalMsg", qt_api.QtCore.QtMsgType.QtInfoMsg: "QtInfoMsg", } return cls._type_name_map[msg_type] @classmethod def _get_log_type_name(cls, msg_type): """ Return a string representation of the given QtMsgType enum value in the same style used by the builtin logging package. """ if not getattr(cls, "_log_type_name_map", None): cls._log_type_name_map = { qt_api.QtCore.QtMsgType.QtDebugMsg: "DEBUG", qt_api.QtCore.QtMsgType.QtWarningMsg: "WARNING", qt_api.QtCore.QtMsgType.QtCriticalMsg: "CRITICAL", qt_api.QtCore.QtMsgType.QtFatalMsg: "FATAL", qt_api.QtCore.QtMsgType.QtInfoMsg: "INFO", } return cls._log_type_name_map[msg_type] def matches_level(self, level): assert level in QtLoggingPlugin.LOG_FAIL_OPTIONS if level == "NO": return False elif level == "INFO": return self.log_type_name in ("INFO", "DEBUG", "WARNING", "CRITICAL") elif level == "DEBUG": return self.log_type_name in ("DEBUG", "WARNING", "CRITICAL") elif level == "WARNING": return self.log_type_name in ("WARNING", "CRITICAL") elif level == "CRITICAL": return self.log_type_name in ("CRITICAL",) else: # pragma: no cover raise ValueError(f"log_fail_level unknown: {level}") class _QtLogLevelErrorRepr(TerminalRepr): """ TerminalRepr of a test which didn't fail by normal means, but emitted messages at or above the allowed level. """ def __init__(self, item, level, is_modeltest_error): if is_modeltest_error: msg = "Qt modeltester errors" else: msg = "Failure: Qt messages with level {0} or above emitted" path, line_index, _ = item.location self.fileloc = ReprFileLocation( path, lineno=line_index + 1, message=msg.format(level.upper()) ) self.sections = [] def addsection(self, name, content, sep="-"): self.sections.append((name, content, sep)) def toterminal(self, out): self.fileloc.toterminal(out) for name, content, sep in self.sections: out.sep(sep, name) out.line(content) pytest-qt-4.0.2/src/pytestqt/modeltest.py0000644000175100001710000007064414061506270021340 0ustar runnerdocker00000000000000# This file is based on the original C++ qabstractitemmodeltester.cpp from: # http://code.qt.io/cgit/qt/qtbase.git/tree/src/testlib/qabstractitemmodeltester.cpp # Commit 4af292fe5158c2d19e8ab1351c71c3940c7f1032 # # Licensed under the following terms: # # Copyright (C) 2016 The Qt Company Ltd. # Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, # info@kdab.com, author Giuseppe D'Angelo # Contact: https://www.qt.io/licensing/ # # This file is part of the QtTest module of the Qt Toolkit. # # $QT_BEGIN_LICENSE:LGPL$ # Commercial License Usage # Licensees holding valid commercial Qt licenses may use this file in # accordance with the commercial license agreement provided with the # Software or, alternatively, in accordance with the terms contained in # a written agreement between you and The Qt Company. For licensing terms # and conditions see https://www.qt.io/terms-conditions. For further # information use the contact form at https://www.qt.io/contact-us. # # GNU Lesser General Public License Usage # Alternatively, this file may be used under the terms of the GNU Lesser # General Public License version 3 as published by the Free Software # Foundation and appearing in the file LICENSE.LGPL3 included in the # packaging of this file. Please review the following information to # ensure the GNU Lesser General Public License version 3 requirements # will be met: https://www.gnu.org/licenses/lgpl-3.0.html. # # GNU General Public License Usage # Alternatively, this file may be used under the terms of the GNU # General Public License version 2.0 or (at your option) the GNU General # Public license version 3 or any later version approved by the KDE Free # Qt Foundation. The licenses are as published by the Free Software # Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 # included in the packaging of this file. Please review the following # information to ensure the GNU General Public License requirements will # be met: https://www.gnu.org/licenses/gpl-2.0.html and # https://www.gnu.org/licenses/gpl-3.0.html. # # $QT_END_LICENSE$ import collections from pytestqt.qt_compat import qt_api _Changing = collections.namedtuple("_Changing", "parent, old_size, last, next") HAS_QT_TESTER = hasattr(qt_api.QtTest, "QAbstractItemModelTester") class ModelTester: """A tester for Qt's QAbstractItemModels.""" def __init__(self, config): self._model = None self._fetching_more = None self._insert = None self._remove = None self._changing = [] self._qt_tester = None def _debug(self, text): print("modeltest: " + text) def _modelindex_debug(self, index): """Get a string for debug output for a QModelIndex.""" if index is None: return "" elif not index.isValid(): return " (0x{:x})".format(id(index)) else: data = self._model.data(index, qt_api.QtCore.Qt.ItemDataRole.DisplayRole) return "{}/{} {!r} (0x{:x})".format( index.row(), index.column(), data, id(index), ) def check(self, model, force_py=False): """Runs a series of checks in the given model. Connect to all of the models signals. Whenever anything happens recheck everything. :param model: The ``QAbstractItemModel`` to test. :param force_py: Force using the Python implementation, even if the C++ implementation is available. """ assert model is not None if HAS_QT_TESTER and not force_py: reporting_mode = ( qt_api.QtTest.QAbstractItemModelTester.FailureReportingMode.Warning ) self._qt_tester = qt_api.QtTest.QAbstractItemModelTester( model, reporting_mode ) self._debug("Using Qt C++ tester") return self._debug("Using Python tester") self._model = model self._fetching_more = False self._insert = [] self._remove = [] self._changing = [] self._model.columnsAboutToBeInserted.connect(self._run) self._model.columnsAboutToBeRemoved.connect(self._run) self._model.columnsInserted.connect(self._run) self._model.columnsRemoved.connect(self._run) self._model.dataChanged.connect(self._run) self._model.headerDataChanged.connect(self._run) self._model.layoutAboutToBeChanged.connect(self._run) self._model.layoutChanged.connect(self._run) self._model.modelReset.connect(self._run) self._model.rowsAboutToBeInserted.connect(self._run) self._model.rowsAboutToBeRemoved.connect(self._run) self._model.rowsInserted.connect(self._run) self._model.rowsRemoved.connect(self._run) # Special checks for changes self._model.layoutAboutToBeChanged.connect(self._on_layout_about_to_be_changed) self._model.layoutChanged.connect(self._on_layout_changed) self._model.rowsAboutToBeInserted.connect(self._on_rows_about_to_be_inserted) self._model.rowsAboutToBeRemoved.connect(self._on_rows_about_to_be_removed) self._model.rowsInserted.connect(self._on_rows_inserted) self._model.rowsRemoved.connect(self._on_rows_removed) self._model.dataChanged.connect(self._on_data_changed) self._model.headerDataChanged.connect(self._on_header_data_changed) self._run() def _cleanup(self): """Not API intended for users, but called from the fixture function.""" if self._model is None: return self._model.columnsAboutToBeInserted.disconnect(self._run) self._model.columnsAboutToBeRemoved.disconnect(self._run) self._model.columnsInserted.disconnect(self._run) self._model.columnsRemoved.disconnect(self._run) self._model.dataChanged.disconnect(self._run) self._model.headerDataChanged.disconnect(self._run) self._model.layoutAboutToBeChanged.disconnect(self._run) self._model.layoutChanged.disconnect(self._run) self._model.modelReset.disconnect(self._run) self._model.rowsAboutToBeInserted.disconnect(self._run) self._model.rowsAboutToBeRemoved.disconnect(self._run) self._model.rowsInserted.disconnect(self._run) self._model.rowsRemoved.disconnect(self._run) self._model.layoutAboutToBeChanged.disconnect( self._on_layout_about_to_be_changed ) self._model.layoutChanged.disconnect(self._on_layout_changed) self._model.rowsAboutToBeInserted.disconnect(self._on_rows_about_to_be_inserted) self._model.rowsAboutToBeRemoved.disconnect(self._on_rows_about_to_be_removed) self._model.rowsInserted.disconnect(self._on_rows_inserted) self._model.rowsRemoved.disconnect(self._on_rows_removed) self._model.dataChanged.disconnect(self._on_data_changed) self._model.headerDataChanged.disconnect(self._on_header_data_changed) self._model = None def _run(self): assert self._model is not None assert self._fetching_more is not None if self._fetching_more: return self._test_basic() self._test_row_count_and_column_count() self._test_has_index() self._test_index() self._test_parent() self._test_data() def _test_basic(self): """Try to call a number of the basic functions (not all). Make sure the model doesn't outright segfault, testing the functions which make sense. """ assert not self._model.buddy(qt_api.QtCore.QModelIndex()).isValid() self._model.canFetchMore(qt_api.QtCore.QModelIndex()) assert self._column_count(qt_api.QtCore.QModelIndex()) >= 0 self._fetch_more(qt_api.QtCore.QModelIndex()) flags = self._model.flags(qt_api.QtCore.QModelIndex()) assert flags == qt_api.QtCore.Qt.ItemFlag.ItemIsDropEnabled or not flags self._has_children(qt_api.QtCore.QModelIndex()) has_row = self._model.hasIndex(0, 0) if has_row: cache = None self._model.match(self._model.index(0, 0), -1, cache) self._model.mimeTypes() assert not self._parent(qt_api.QtCore.QModelIndex()).isValid() assert self._model.rowCount() >= 0 self._model.span(qt_api.QtCore.QModelIndex()) self._model.supportedDropActions() self._model.roleNames() def _test_row_count_and_column_count(self): """Test model's implementation of row/columnCount() and hasChildren(). Models that are dynamically populated are not as fully tested here. The models rowCount() is tested more extensively in _check_children(), but this catches the big mistakes. """ # check top row top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) rows = self._model.rowCount(top_index) assert rows >= 0 columns = self._column_count(top_index) assert columns >= 0 if rows == 0 or columns == 0: return assert self._has_children(top_index) second_level_index = self._model.index(0, 0, top_index) assert second_level_index.isValid() rows = self._model.rowCount(second_level_index) assert rows >= 0 columns = self._column_count(second_level_index) assert columns >= 0 if rows == 0 or columns == 0: return assert self._has_children(second_level_index) def _test_has_index(self): """Test model's implementation of hasIndex(). hasIndex() is tested more extensively in _check_children(), but this catches the big mistakes. """ # Make sure that invalid values return an invalid index assert not self._model.hasIndex(-2, -2) assert not self._model.hasIndex(-2, 0) assert not self._model.hasIndex(0, -2) rows = self._model.rowCount() columns = self._column_count() # check out of bounds assert not self._model.hasIndex(rows, columns) assert not self._model.hasIndex(rows + 1, columns + 1) if rows > 0 and columns > 0: assert self._model.hasIndex(0, 0) def _test_index(self): """Test model's implementation of index(). index() is tested more extensively in _check_children(), but this catches the big mistakes. """ rows = self._model.rowCount() columns = self._column_count() for row in range(rows): for column in range(columns): # Make sure that the same index is *always* returned a = self._model.index(row, column) b = self._model.index(row, column) assert a.isValid() assert b.isValid() assert a == b def _test_parent(self): """Tests model's implementation of QAbstractItemModel::parent().""" # Make sure the model won't crash and will return an invalid # QModelIndex when asked for the parent of an invalid index. assert not self._parent(qt_api.QtCore.QModelIndex()).isValid() if not self._has_children(): return # Column 0 | Column 1 | # QModelIndex() | | # \- top_index | top_index_1 | # \- child_index | child_index_1 | # Common error test #1, make sure that a top level index has a parent # that is a invalid QModelIndex. top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) assert not self._parent(top_index).isValid() # Common error test #2, make sure that a second level index has a # parent that is the first level index. if self._has_children(top_index): child_index = self._model.index(0, 0, top_index) assert self._parent(child_index) == top_index # Common error test #3, the second column should NOT have the same # children as the first column in a row. # Usually the second column shouldn't have children. if self._model.hasIndex(0, 1): top_index_1 = self._model.index(0, 1, qt_api.QtCore.QModelIndex()) if self._has_children(top_index) and self._has_children(top_index_1): child_index = self._model.index(0, 0, top_index) assert child_index.isValid() child_index_1 = self._model.index(0, 0, top_index_1) assert child_index_1.isValid() assert child_index != child_index_1 # Full test, walk n levels deep through the model making sure that all # parent's children correctly specify their parent. self._check_children(qt_api.QtCore.QModelIndex()) def _check_children(self, parent, current_depth=0): """Check parent/children relationships. Called from the parent() test. A model that returns an index of parent X should also return X when asking for the parent of the index. This recursive function does pretty extensive testing on the whole model in an effort to catch edge cases. This function assumes that rowCount(), columnCount() and index() already work. If they have a bug it will point it out, but the above tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one. """ # First just try walking back up the tree. p = parent while p.isValid(): p = p.parent() # For models that are dynamically populated if self._model.canFetchMore(parent): self._fetch_more(parent) rows = self._model.rowCount(parent) columns = self._column_count(parent) if rows > 0: assert self._has_children(parent) # Some further testing against rows(), columns(), and hasChildren() assert rows >= 0 assert columns >= 0 if rows > 0 and columns > 0: assert self._has_children(parent) self._debug( "Checking children of {} with depth {} " "({} rows, {} columns)".format( self._modelindex_debug(parent), current_depth, rows, columns ) ) top_left_child = self._model.index(0, 0, parent) assert not self._model.hasIndex(rows, 0, parent) assert not self._model.hasIndex(rows + 1, 0, parent) for r in range(rows): assert not self._model.hasIndex(r, columns, parent) assert not self._model.hasIndex(r, columns + 1, parent) for c in range(columns): assert self._model.hasIndex(r, c, parent) index = self._model.index(r, c, parent) # rowCount() and columnCount() said that it existed... if not index.isValid(): self._debug( "Got invalid index at row={} col={} parent={}".format( r, c, self._modelindex_debug(parent) ) ) assert index.isValid() # index() should always return the same index when called twice # in a row modified_index = self._model.index(r, c, parent) assert index == modified_index sibling = self._model.sibling(r, c, top_left_child) assert index == sibling sibling2 = top_left_child.sibling(r, c) assert index == sibling2 # Some basic checking on the index that is returned assert index.model() == self._model assert index.row() == r assert index.column() == c # If the next test fails here is some somewhat useful debug you # play with. if self._parent(index) != parent: self._debug( "Inconsistent parent() implementation detected\n" " index={} exp. parent={} act. parent={}\n" " row={} col={} depth={}\n" " data for child: {}\n" " data for parent: {}\n".format( self._modelindex_debug(index), self._modelindex_debug(parent), self._modelindex_debug(self._parent(index)), r, c, current_depth, self._model.data(index), self._model.data(parent), ) ) # Check that we can get back our real parent. assert self._parent(index) == parent # recursively go down the children if self._has_children(index) and current_depth < 10: self._debug( "{} has {} children".format( self._modelindex_debug(index), self._model.rowCount(index) ) ) self._check_children(index, current_depth + 1) # make sure that after testing the children that the index # doesn't change. newer_index = self._model.index(r, c, parent) assert index == newer_index self._debug("Children check for {} done".format(self._modelindex_debug(parent))) def _test_data(self): """Test model's implementation of data()""" if not self._has_children(): return # A valid index should have a valid QVariant data assert self._model.index(0, 0).isValid() types = [ (qt_api.QtCore.Qt.ItemDataRole.DisplayRole, (str,)), (qt_api.QtCore.Qt.ItemDataRole.ToolTipRole, (str,)), (qt_api.QtCore.Qt.ItemDataRole.StatusTipRole, (str,)), (qt_api.QtCore.Qt.ItemDataRole.WhatsThisRole, (str,)), (qt_api.QtCore.Qt.ItemDataRole.SizeHintRole, qt_api.QtCore.QSize), (qt_api.QtCore.Qt.ItemDataRole.FontRole, qt_api.QtGui.QFont), ( qt_api.QtCore.Qt.ItemDataRole.BackgroundRole, (qt_api.QtGui.QColor, qt_api.QtGui.QBrush), ), ( qt_api.QtCore.Qt.ItemDataRole.ForegroundRole, (qt_api.QtGui.QColor, qt_api.QtGui.QBrush), ), ( qt_api.QtCore.Qt.ItemDataRole.DecorationRole, ( qt_api.QtGui.QPixmap, qt_api.QtGui.QImage, qt_api.QtGui.QIcon, qt_api.QtGui.QColor, qt_api.QtGui.QBrush, ), ), ] # General purpose roles with a fixed expected type for role, typ in types: data = self._model.data(self._model.index(0, 0), role) assert data is None or isinstance(data, typ), role # noqa # Check that the alignment is one we know about alignment = self._model.data( self._model.index(0, 0), qt_api.QtCore.Qt.ItemDataRole.TextAlignmentRole ) if alignment is not None: try: alignment = int(alignment) except (TypeError, ValueError): assert 0, "%r should be a TextAlignmentRole enum" % alignment mask = int( qt_api.QtCore.Qt.AlignmentFlag.AlignHorizontal_Mask | qt_api.QtCore.Qt.AlignmentFlag.AlignVertical_Mask ) assert alignment == alignment & mask # Check that the "check state" is one we know about. state = self._model.data( self._model.index(0, 0), qt_api.QtCore.Qt.ItemDataRole.CheckStateRole ) assert state in [ None, qt_api.QtCore.Qt.CheckState.Unchecked, qt_api.QtCore.Qt.CheckState.PartiallyChecked, qt_api.QtCore.Qt.CheckState.Checked, ] def _on_rows_about_to_be_inserted(self, parent, start, end): """Store what is about to be inserted. This gets stored to make sure it actually happens in rowsInserted. """ last_index = self._model.index(start - 1, 0, parent) next_index = self._model.index(start, 0, parent) parent_rowcount = self._model.rowCount(parent) self._debug( "rows about to be inserted: start {}, end {}, parent {}, " "parent row count {}, last item {}, next item {}".format( start, end, self._modelindex_debug(parent), parent_rowcount, self._modelindex_debug(last_index), self._modelindex_debug(next_index), ) ) last_data = self._model.data(last_index) if start > 0 else None next_data = self._model.data(next_index) if start < parent_rowcount else None c = _Changing( parent=parent, old_size=parent_rowcount, last=last_data, next=next_data ) self._insert.append(c) def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._insert.pop() last_data = ( self._model.data(self._model.index(start - 1, 0, parent)) if start - 1 >= 0 else None ) next_data = ( self._model.data(self._model.index(end + 1, 0, c.parent)) if end + 1 < self._model.rowCount(c.parent) else None ) expected_size = c.old_size + (end - start + 1) current_size = self._model.rowCount(parent) self._debug(f"rows inserted: start {start}, end {end}") self._debug( " from rowsAboutToBeInserted: parent {}, " "size {} (-> {} expected), " "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), c.old_size, expected_size, c.next, c.last, ) ) self._debug( " now in rowsInserted: parent {}, size {}, " "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, next_data, last_data, ) ) assert c.parent == parent for ii in range(start, end + 1): idx = self._model.index(ii, 0, parent) self._debug(" item {} inserted: {}".format(ii, self._modelindex_debug(idx))) self._debug("") assert current_size == expected_size if last_data is not None: assert c.last == last_data if next_data is not None: assert c.next == next_data def _on_layout_about_to_be_changed(self): for i in range(max(self._model.rowCount(), 100)): idx = qt_api.QtCore.QPersistentModelIndex(self._model.index(i, 0)) self._changing.append(idx) def _on_layout_changed(self): for p in self._changing: assert p == self._model.index(p.row(), p.column(), p.parent()) self._changing = [] def _on_rows_about_to_be_removed(self, parent, start, end): """Store what is about to be removed to make sure it actually happens. This gets stored to make sure it actually happens in rowsRemoved. """ parent_rowcount = self._model.rowCount(parent) last_index = self._model.index(start - 1, 0, parent) if start > 0 else None next_index = ( self._model.index(end + 1, 0, parent) if end < parent_rowcount - 1 else None ) self._debug( "rows about to be removed: start {}, end {}, parent {}, " "parent row count {}, last item {}, next item {}".format( start, end, self._modelindex_debug(parent), parent_rowcount, self._modelindex_debug(last_index), self._modelindex_debug(next_index), ) ) if last_index is not None: assert last_index.isValid() if next_index is not None: assert next_index.isValid() last_data = None if last_index is None else self._model.data(last_index) next_data = None if next_index is None else self._model.data(next_index) c = _Changing( parent=parent, old_size=parent_rowcount, last=last_data, next=next_data ) self._remove.append(c) def _on_rows_removed(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._remove.pop() last_data = ( self._model.data(self._model.index(start - 1, 0, c.parent)) if start > 0 else None ) next_data = ( self._model.data(self._model.index(start, 0, c.parent)) if end < c.old_size - 1 else None ) current_size = self._model.rowCount(parent) expected_size = c.old_size - (end - start + 1) self._debug(f"rows removed: start {start}, end {end}") self._debug( " from rowsAboutToBeRemoved: parent {}, " "size {} (-> {} expected), " "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), c.old_size, expected_size, c.next, c.last, ) ) self._debug( " now in rowsRemoved: parent {}, size {}, " "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, next_data, last_data, ) ) assert c.parent == parent assert current_size == expected_size if last_data is not None: assert c.last == last_data if next_data is not None: assert c.next == next_data def _on_data_changed(self, top_left, bottom_right): assert top_left.isValid() assert bottom_right.isValid() common_parent = bottom_right.parent() assert top_left.parent() == common_parent assert top_left.row() <= bottom_right.row() assert top_left.column() <= bottom_right.column() row_count = self._model.rowCount(common_parent) column_count = self._column_count(common_parent) assert bottom_right.row() < row_count assert bottom_right.column() < column_count def _on_header_data_changed(self, orientation, start, end): assert orientation in [ qt_api.QtCore.Qt.Orientation.Horizontal, qt_api.QtCore.Qt.Orientation.Vertical, ] assert start >= 0 assert end >= 0 assert start <= end if orientation == qt_api.QtCore.Qt.Orientation.Vertical: item_count = self._model.rowCount() else: item_count = self._column_count() assert start < item_count assert end < item_count def _column_count(self, parent=qt_api.QtCore.QModelIndex()): """ Workaround for the fact that ``columnCount`` is a private method in QAbstractListModel/QAbstractTableModel subclasses. """ if isinstance(self._model, qt_api.QtCore.QAbstractListModel): return 1 if parent == qt_api.QtCore.QModelIndex() else 0 else: return self._model.columnCount(parent) def _parent(self, index): """ .. see:: ``_column_count`` """ model_types = ( qt_api.QtCore.QAbstractListModel, qt_api.QtCore.QAbstractTableModel, ) if isinstance(self._model, model_types): return qt_api.QtCore.QModelIndex() else: return self._model.parent(index) def _has_children(self, parent=qt_api.QtCore.QModelIndex()): """ .. see:: ``_column_count`` """ model_types = ( qt_api.QtCore.QAbstractListModel, qt_api.QtCore.QAbstractTableModel, ) if isinstance(self._model, model_types): return parent == qt_api.QtCore.QModelIndex() and self._model.rowCount() > 0 else: return self._model.hasChildren(parent) def _fetch_more(self, parent): """Call ``fetchMore`` on the model and set ``self._fetching_more``.""" self._fetching_more = True self._model.fetchMore(parent) self._fetching_more = False pytest-qt-4.0.2/src/pytestqt/plugin.py0000644000175100001710000001426714061506270020635 0ustar runnerdocker00000000000000import pytest from pytestqt.exceptions import ( _is_exception_capture_enabled, _QtExceptionCaptureManager, ) from pytestqt.logging import QtLoggingPlugin, _QtMessageCapture from pytestqt.qt_compat import qt_api from pytestqt.qtbot import QtBot, _close_widgets @pytest.fixture(scope="session") def qapp_args(): """ Fixture that provides QApplication arguments to use. You can override this fixture to pass different arguments to ``QApplication``: .. code-block:: python @pytest.fixture(scope="session") def qapp_args(): return ["--arg"] """ return [] @pytest.fixture(scope="session") def qapp(qapp_args, pytestconfig): """ Fixture that instantiates the QApplication instance that will be used by the tests. You can use the ``qapp`` fixture in tests which require a ``QApplication`` to run, but where you don't need full ``qtbot`` functionality. """ app = qt_api.QtWidgets.QApplication.instance() if app is None: global _qapp_instance _qapp_instance = qt_api.QtWidgets.QApplication(qapp_args) name = pytestconfig.getini("qt_qapp_name") _qapp_instance.setApplicationName(name) return _qapp_instance else: return app # pragma: no cover # holds a global QApplication instance created in the qapp fixture; keeping # this reference alive avoids it being garbage collected too early _qapp_instance = None @pytest.fixture def qtbot(qapp, request): """ Fixture used to create a QtBot instance for using during testing. Make sure to call addWidget for each top-level widget you create to ensure that they are properly closed after the test ends. """ result = QtBot(request) return result @pytest.fixture def qtlog(request): """Fixture that can access messages captured during testing""" if hasattr(request._pyfuncitem, "qt_log_capture"): return request._pyfuncitem.qt_log_capture else: return _QtMessageCapture([]) # pragma: no cover @pytest.fixture def qtmodeltester(request): """ Fixture used to create a ModelTester instance to test models. """ from pytestqt.modeltest import ModelTester tester = ModelTester(request.config) yield tester tester._cleanup() def pytest_addoption(parser): parser.addini( "qt_api", 'Qt api version to use: "pyside6" , "pyside2", "pyqt6", "pyqt5"' ) parser.addini("qt_no_exception_capture", "disable automatic exception capture") parser.addini( "qt_default_raising", "Default value for the raising parameter of qtbot.waitSignal/waitCallback", ) parser.addini( "qt_qapp_name", "The Qt application name to use", default="pytest-qt-qapp" ) default_log_fail = QtLoggingPlugin.LOG_FAIL_OPTIONS[0] parser.addini( "qt_log_level_fail", 'log level in which tests can fail: {} (default: "{}")'.format( QtLoggingPlugin.LOG_FAIL_OPTIONS, default_log_fail ), default=default_log_fail, ) parser.addini( "qt_log_ignore", "list of regexes for messages that should not cause a tests " "to fails", type="linelist", ) group = parser.getgroup("qt", "qt testing") group.addoption( "--no-qt-log", dest="qt_log", action="store_false", default=True, help="disable pytest-qt logging capture", ) group.addoption( "--qt-log-format", dest="qt_log_format", default=None, help="defines how qt log messages are displayed.", ) @pytest.mark.hookwrapper @pytest.mark.tryfirst def pytest_runtest_setup(item): """ Hook called after before test setup starts, to start capturing exceptions as early as possible. """ capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: item.qt_exception_capture_manager = _QtExceptionCaptureManager() item.qt_exception_capture_manager.start() yield _process_events() if capture_enabled: item.qt_exception_capture_manager.fail_if_exceptions_occurred("SETUP") @pytest.mark.hookwrapper @pytest.mark.tryfirst def pytest_runtest_call(item): yield _process_events() capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: item.qt_exception_capture_manager.fail_if_exceptions_occurred("CALL") @pytest.mark.hookwrapper @pytest.mark.trylast def pytest_runtest_teardown(item): """ Hook called after each test tear down, to process any pending events and avoiding leaking events to the next test. Also, if exceptions have been captured during fixtures teardown, fail the test. """ _process_events() _close_widgets(item) _process_events() yield _process_events() capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: item.qt_exception_capture_manager.fail_if_exceptions_occurred("TEARDOWN") item.qt_exception_capture_manager.finish() def _process_events(): """Calls app.processEvents() while taking care of capturing exceptions or not based on the given item's configuration. """ app = qt_api.QtWidgets.QApplication.instance() if app is not None: app.processEvents() def pytest_configure(config): config.addinivalue_line( "markers", "qt_no_exception_capture: Disables pytest-qt's automatic exception " "capture for just one test item.", ) config.addinivalue_line( "markers", "qt_log_level_fail: overrides qt_log_level_fail ini option." ) config.addinivalue_line( "markers", "qt_log_ignore: overrides qt_log_ignore ini option." ) config.addinivalue_line("markers", "no_qt_log: Turn off Qt logging capture.") if config.getoption("qt_log") and config.getoption("capture") != "no": config.pluginmanager.register(QtLoggingPlugin(config), "_qt_logging") qt_api.set_qt_api(config.getini("qt_api")) def pytest_report_header(): from pytestqt.qt_compat import qt_api v = qt_api.get_versions() fields = [ f"{v.qt_api} {v.qt_api_version}", "Qt runtime %s" % v.runtime, "Qt compiled %s" % v.compiled, ] version_line = " -- ".join(fields) return [version_line] pytest-qt-4.0.2/src/pytestqt/qt_compat.py0000644000175100001710000001433614061506270021323 0ustar runnerdocker00000000000000""" Provide a common way to import Qt classes used by pytest-qt in a unique manner, abstracting API differences between PyQt5 and PySide2/6. .. note:: This module is not part of pytest-qt public API, hence its interface may change between releases and users should not rely on it. Based on from https://github.com/epage/PythonUtils. """ from collections import namedtuple import os import pytest VersionTuple = namedtuple("VersionTuple", "qt_api, qt_api_version, runtime, compiled") def _import(name): """Think call so we can mock it during testing""" return __import__(name) class _QtApi: """ Interface to the underlying Qt API currently configured for pytest-qt. This object lazily loads all class references and other objects when the ``set_qt_api`` method gets called, providing a uniform way to access the Qt classes. """ def __init__(self): self._import_errors = {} def _get_qt_api_from_env(self): api = os.environ.get("PYTEST_QT_API") supported_apis = [ "pyside6", "pyside2", "pyqt6", "pyqt5", ] if api is not None: api = api.lower() if api not in supported_apis: # pragma: no cover msg = f"Invalid value for $PYTEST_QT_API: {api}, expected one of {supported_apis}" raise pytest.UsageError(msg) return api def _guess_qt_api(self): # pragma: no cover def _can_import(name): try: _import(name) return True except ModuleNotFoundError as e: self._import_errors[name] = str(e) return False # Note, not importing only the root namespace because when uninstalling from conda, # the namespace can still be there. if _can_import("PySide6.QtCore"): return "pyside6" elif _can_import("PySide2.QtCore"): return "pyside2" elif _can_import("PyQt6.QtCore"): return "pyqt6" elif _can_import("PyQt5.QtCore"): return "pyqt5" return None def set_qt_api(self, api): self.pytest_qt_api = self._get_qt_api_from_env() or api or self._guess_qt_api() self.is_pyside = self.pytest_qt_api in ["pyside2", "pyside6"] self.is_pyqt = self.pytest_qt_api in ["pyqt5", "pyqt6"] if not self.pytest_qt_api: # pragma: no cover errors = "\n".join( f" {module}: {reason}" for module, reason in sorted(self._import_errors.items()) ) msg = ( "pytest-qt requires either PySide2, PySide6, PyQt5 or PyQt6 installed.\n" + errors ) raise pytest.UsageError(msg) _root_modules = { "pyside6": "PySide6", "pyside2": "PySide2", "pyqt6": "PyQt6", "pyqt5": "PyQt5", } _root_module = _root_modules[self.pytest_qt_api] def _import_module(module_name): m = __import__(_root_module, globals(), locals(), [module_name], 0) return getattr(m, module_name) self.QtCore = QtCore = _import_module("QtCore") self.QtGui = _import_module("QtGui") self.QtTest = _import_module("QtTest") self.QtWidgets = _import_module("QtWidgets") self._check_qt_api_version() # qInfo is not exposed in PySide2/6 (#232) if hasattr(QtCore, "QMessageLogger"): self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg) elif hasattr(QtCore, "qInfo"): self.qInfo = QtCore.qInfo else: self.qInfo = None self.qDebug = QtCore.qDebug self.qWarning = QtCore.qWarning self.qCritical = QtCore.qCritical self.qFatal = QtCore.qFatal if self.is_pyside: self.Signal = QtCore.Signal self.Slot = QtCore.Slot self.Property = QtCore.Property elif self.is_pyqt: self.Signal = QtCore.pyqtSignal self.Slot = QtCore.pyqtSlot self.Property = QtCore.pyqtProperty else: assert False, "Expected either is_pyqt or is_pyside" def _check_qt_api_version(self): if not self.is_pyqt: # We support all PySide versions return if self.QtCore.PYQT_VERSION == 0x060000: # 6.0.0 raise pytest.UsageError( "PyQt 6.0 is not supported by pytest-qt, use 6.1+ instead." ) elif self.QtCore.PYQT_VERSION < 0x050B00: # 5.11.0 raise pytest.UsageError( "PyQt < 5.11 is not supported by pytest-qt, use 5.11+ instead." ) def exec(self, obj, *args, **kwargs): # exec was a keyword in Python 2, so PySide2 (and also PySide6 6.0) # name the corresponding method "exec_" instead. # # The old _exec() alias is removed in PyQt6 and also deprecated as of # PySide 6.1: # https://codereview.qt-project.org/c/pyside/pyside-setup/+/342095 if hasattr(obj, "exec"): return obj.exec(*args, **kwargs) return obj.exec_(*args, **kwargs) def get_versions(self): if self.pytest_qt_api == "pyside6": import PySide6 version = PySide6.__version__ return VersionTuple( "PySide6", version, self.QtCore.qVersion(), self.QtCore.__version__ ) elif self.pytest_qt_api == "pyside2": import PySide2 version = PySide2.__version__ return VersionTuple( "PySide2", version, self.QtCore.qVersion(), self.QtCore.__version__ ) elif self.pytest_qt_api == "pyqt6": return VersionTuple( "PyQt6", self.QtCore.PYQT_VERSION_STR, self.QtCore.qVersion(), self.QtCore.QT_VERSION_STR, ) elif self.pytest_qt_api == "pyqt5": return VersionTuple( "PyQt5", self.QtCore.PYQT_VERSION_STR, self.QtCore.qVersion(), self.QtCore.QT_VERSION_STR, ) assert False, f"Internal error, unknown pytest_qt_api: {self.pytest_qt_api}" qt_api = _QtApi() pytest-qt-4.0.2/src/pytestqt/qtbot.py0000644000175100001710000006522214061506270020465 0ustar runnerdocker00000000000000import contextlib import weakref import warnings from pytestqt.exceptions import TimeoutError from pytestqt.qt_compat import qt_api from pytestqt.wait_signal import ( SignalBlocker, MultiSignalBlocker, SignalEmittedSpy, SignalEmittedError, CallbackBlocker, CallbackCalledTwiceError, ) def _parse_ini_boolean(value): if value in (True, False): return value try: return {"true": True, "false": False}[value.lower()] except KeyError: raise ValueError("unknown string for bool: %r" % value) class QtBot: """ Instances of this class are responsible for sending events to `Qt` objects (usually widgets), simulating user input. .. important:: Instances of this class should be accessed only by using a ``qtbot`` fixture, never instantiated directly. **Widgets** .. automethod:: addWidget .. automethod:: captureExceptions .. automethod:: waitActive .. automethod:: waitExposed .. automethod:: waitForWindowShown .. automethod:: stop .. automethod:: wait **Signals and Events** .. automethod:: waitSignal .. automethod:: waitSignals .. automethod:: assertNotEmitted .. automethod:: waitUntil **Raw QTest API** Methods below provide very low level functions, as sending a single mouse click or a key event. Those methods are just forwarded directly to the `QTest API`_. Consult the documentation for more information. --- Below are methods used to simulate sending key events to widgets: .. staticmethod:: keyClick (widget, key[, modifier=Qt.NoModifier[, delay=-1]]) .. staticmethod:: keyClicks (widget, key_sequence[, modifier=Qt.NoModifier[, delay=-1]]) .. staticmethod:: keyEvent (action, widget, key[, modifier=Qt.NoModifier[, delay=-1]]) .. staticmethod:: keyPress (widget, key[, modifier=Qt.NoModifier[, delay=-1]]) .. staticmethod:: keyRelease (widget, key[, modifier=Qt.NoModifier[, delay=-1]]) Sends one or more keyboard events to a widget. :param QWidget widget: the widget that will receive the event :param str|int key: key to send, it can be either a Qt.Key_* constant or a single character string. .. _keyboard modifiers: :param Qt.KeyboardModifier modifier: flags OR'ed together representing other modifier keys also pressed. Possible flags are: * ``Qt.NoModifier``: No modifier key is pressed. * ``Qt.ShiftModifier``: A Shift key on the keyboard is pressed. * ``Qt.ControlModifier``: A Ctrl key on the keyboard is pressed. * ``Qt.AltModifier``: An Alt key on the keyboard is pressed. * ``Qt.MetaModifier``: A Meta key on the keyboard is pressed. * ``Qt.KeypadModifier``: A keypad button is pressed. * ``Qt.GroupSwitchModifier``: X11 only. A Mode_switch key on the keyboard is pressed. :param int delay: after the event, delay the test for this milliseconds (if > 0). .. staticmethod:: keyToAscii (key) Auxiliary method that converts the given constant ot its equivalent ascii. :param Qt.Key_* key: one of the constants for keys in the Qt namespace. :return type: str :returns: the equivalent character string. .. note:: This method is not available in PyQt. --- Below are methods used to simulate sending mouse events to widgets. .. staticmethod:: mouseClick (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) .. staticmethod:: mouseDClick (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) .. staticmethod:: mouseMove (widget[, pos=QPoint()[, delay=-1]]) .. staticmethod:: mousePress (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) .. staticmethod:: mouseRelease (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) Sends a mouse moves and clicks to a widget. :param QWidget widget: the widget that will receive the event :param Qt.MouseButton button: flags OR'ed together representing the button pressed. Possible flags are: * ``Qt.MouseButton.NoButton``: The button state does not refer to any button (see QMouseEvent.button()). * ``Qt.MouseButton.LeftButton``: The left button is pressed, or an event refers to the left button. (The left button may be the right button on left-handed mice.) * ``Qt.MouseButton.RightButton``: The right button. * ``Qt.MouseButton.MidButton``: The middle button. * ``Qt.MouseButton.MiddleButton``: The middle button. * ``Qt.MouseButton.XButton1``: The first X button. * ``Qt.MouseButton.XButton2``: The second X button. :param Qt.KeyboardModifier modifier: flags OR'ed together representing other modifier keys also pressed. See `keyboard modifiers`_. :param QPoint position: position of the mouse pointer. :param int delay: after the event, delay the test for this milliseconds (if > 0). .. _QTest API: http://doc.qt.io/qt-5/qtest.html """ def __init__(self, request): self._request = request def _should_raise(self, raising_arg): ini_val = self._request.config.getini("qt_default_raising") if raising_arg is not None: return raising_arg elif ini_val: return _parse_ini_boolean(ini_val) else: return True def addWidget(self, widget, *, before_close_func=None): """ Adds a widget to be tracked by this bot. This is not required, but will ensure that the widget gets closed by the end of the test, so it is highly recommended. :param QWidget widget: Widget to keep track of. :kwparam before_close_func: A function that receives the widget as single parameter, which is called just before the ``.close()`` method gets called. .. note:: This method is also available as ``add_widget`` (pep-8 alias) """ if not isinstance(widget, qt_api.QtWidgets.QWidget): raise TypeError("Need to pass a QWidget to addWidget!") _add_widget(self._request.node, widget, before_close_func=before_close_func) def waitActive(self, widget, *, timeout=5000): """ Context manager that waits for ``timeout`` milliseconds or until the window is active. If window is not exposed within ``timeout`` milliseconds, raise ``TimeoutError``. This is mainly useful for asynchronous systems like X11, where a window will be mapped to screen some time after being asked to show itself on the screen. .. code-block:: python with qtbot.waitActive(widget, timeout=500): show_action() :param QWidget widget: Widget to wait for. :param int|None timeout: How many milliseconds to wait for. .. note:: This method is also available as ``wait_active`` (pep-8 alias) """ __tracebackhide__ = True return _WaitWidgetContextManager( "qWaitForWindowActive", "activated", widget, timeout ) def waitExposed(self, widget, *, timeout=5000): """ Context manager that waits for ``timeout`` milliseconds or until the window is exposed. If the window is not exposed within ``timeout`` milliseconds, raise ``TimeoutError``. This is mainly useful for asynchronous systems like X11, where a window will be mapped to screen some time after being asked to show itself on the screen. .. code-block:: python with qtbot.waitExposed(splash, timeout=500): startup() :param QWidget widget: Widget to wait for. :param int|None timeout: How many milliseconds to wait for. .. note:: This method is also available as ``wait_exposed`` (pep-8 alias) """ __tracebackhide__ = True return _WaitWidgetContextManager( "qWaitForWindowExposed", "exposed", widget, timeout ) def waitForWindowShown(self, widget): """ Waits until the window is shown in the screen. This is mainly useful for asynchronous systems like X11, where a window will be mapped to screen some time after being asked to show itself on the screen. .. warning:: This method does **not** raise ``TimeoutError`` if the window wasn't shown. .. deprecated:: 4.0 Use the qtbot.waitForWindowExposed context manager instead. :param QWidget widget: Widget to wait on. :returns: ``True`` if the window was shown, ``False`` if ``.show()`` was never called or a timeout occurred. .. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias) """ warnings.warn( "waitForWindowShown is deprecated, as the underlying Qt method was " "obsoleted in Qt 5.0 and removed in Qt 6.0. Its name is imprecise and " "the pytest-qt wrapper does not raise TimeoutError if the window " "wasn't shown. Please use the qtbot.waitExposed context manager " "instead.", DeprecationWarning, ) return qt_api.QtTest.QTest.qWaitForWindowExposed(widget) def stop(self): """ Stops the current test flow, letting the user interact with any visible widget. This is mainly useful so that you can verify the current state of the program while writing tests. Closing the windows should resume the test run, with ``qtbot`` attempting to restore visibility of the widgets as they were before this call. """ widget_and_visibility = [] for weak_widget in _iter_widgets(self._request.node): widget = weak_widget() if widget is not None: widget_and_visibility.append((widget, widget.isVisible())) qt_api.exec(qt_api.QtWidgets.QApplication.instance()) for widget, visible in widget_and_visibility: widget.setVisible(visible) def waitSignal(self, signal, *, timeout=5000, raising=None, check_params_cb=None): """ .. versionadded:: 1.2 Stops current test until a signal is triggered. Used to stop the control flow of a test until a signal is emitted, or a number of milliseconds, specified by ``timeout``, has elapsed. Best used as a context manager:: with qtbot.waitSignal(signal, timeout=1000): long_function_that_calls_signal() Also, you can use the :class:`SignalBlocker` directly if the context manager form is not convenient:: blocker = qtbot.waitSignal(signal, timeout=1000) blocker.connect(another_signal) long_function_that_calls_signal() blocker.wait() Any additional signal, when triggered, will make :meth:`wait` return. .. versionadded:: 1.4 The *raising* parameter. .. versionadded:: 2.0 The *check_params_cb* parameter. :param Signal signal: A signal to wait for, or a tuple ``(signal, signal_name_as_str)`` to improve the error message that is part of ``TimeoutError``. :param int timeout: How many milliseconds to wait before resuming control flow. :param bool raising: If :class:`QtBot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` is set in the config. :param Callable check_params_cb: Optional ``callable`` that compares the provided signal parameters to some expected parameters. It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. :returns: ``SignalBlocker`` object. Call ``SignalBlocker.wait()`` to wait. .. note:: This method is also available as ``wait_signal`` (pep-8 alias) """ if signal is None: raise ValueError( f"Passing None as signal isn't supported anymore, use qtbot.wait({timeout}) instead." ) raising = self._should_raise(raising) blocker = SignalBlocker( timeout=timeout, raising=raising, check_params_cb=check_params_cb ) blocker.connect(signal) return blocker def waitSignals( self, signals, *, timeout=5000, raising=None, check_params_cbs=None, order="none", ): """ .. versionadded:: 1.4 Stops current test until all given signals are triggered. Used to stop the control flow of a test until all (and only all) signals are emitted or the number of milliseconds specified by ``timeout`` has elapsed. Best used as a context manager:: with qtbot.waitSignals([signal1, signal2], timeout=1000): long_function_that_calls_signals() Also, you can use the :class:`MultiSignalBlocker` directly if the context manager form is not convenient:: blocker = qtbot.waitSignals(signals, timeout=1000) long_function_that_calls_signal() blocker.wait() :param list signals: A list of :class:`Signal` objects to wait for. Alternatively: a list of (``Signal, str``) tuples of the form ``(signal, signal_name_as_str)`` to improve the error message that is part of ``TimeoutError``. :param int timeout: How many milliseconds to wait before resuming control flow. :param bool raising: If :class:`QtBot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` is set in the config. :param list check_params_cbs: optional list of callables that compare the provided signal parameters to some expected parameters. Each callable has to match the signature of the corresponding signal in ``signals`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the corresponding signal. If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised. :param str order: Determines the order in which to expect signals: - ``"none"``: no order is enforced - ``"strict"``: signals have to be emitted strictly in the provided order (e.g. fails when expecting signals [a, b] and [a, a, b] is emitted) - ``"simple"``: like "strict", but signals may be emitted in-between the provided ones, e.g. expected ``signals == [a, b, c]`` and actually emitted ``signals = [a, a, b, a, c]`` works (would fail with ``"strict"``). :returns: ``MultiSignalBlocker`` object. Call ``MultiSignalBlocker.wait()`` to wait. .. note:: This method is also available as ``wait_signals`` (pep-8 alias) """ if order not in ["none", "simple", "strict"]: raise ValueError("order has to be set to 'none', 'simple' or 'strict'") if not signals: raise ValueError( f"Passing {signals} as signals isn't supported anymore, consider using qtbot.wait({timeout}) instead." ) raising = self._should_raise(raising) if check_params_cbs: if len(check_params_cbs) != len(signals): raise ValueError( "Number of callbacks ({}) does not " "match number of signals ({})!".format( len(check_params_cbs), len(signals) ) ) blocker = MultiSignalBlocker( timeout=timeout, raising=raising, order=order, check_params_cbs=check_params_cbs, ) blocker.add_signals(signals) return blocker def wait(self, ms): """ .. versionadded:: 1.9 Waits for ``ms`` milliseconds. While waiting, events will be processed and your test will stay responsive to user interface events or network communication. """ blocker = MultiSignalBlocker(timeout=ms, raising=False) blocker.wait() @contextlib.contextmanager def assertNotEmitted(self, signal, *, wait=0): """ .. versionadded:: 1.11 Make sure the given ``signal`` doesn't get emitted. :param int wait: How many milliseconds to wait to make sure the signal isn't emitted asynchronously. By default, this method returns immediately and only catches signals emitted inside the ``with``-block. This is intended to be used as a context manager. .. note:: This method is also available as ``assert_not_emitted`` (pep-8 alias) """ spy = SignalEmittedSpy(signal) with spy, self.waitSignal(signal, timeout=wait, raising=False): yield spy.assert_not_emitted() def waitUntil(self, callback, *, timeout=5000): """ .. versionadded:: 2.0 Wait in a busy loop, calling the given callback periodically until timeout is reached. ``callback()`` should raise ``AssertionError`` to indicate that the desired condition has not yet been reached, or just return ``None`` when it does. Useful to ``assert`` until some condition is satisfied: .. code-block:: python def view_updated(): assert view_model.count() > 10 qtbot.waitUntil(view_updated) Another possibility is for ``callback()`` to return ``True`` when the desired condition is met, ``False`` otherwise. Useful specially with ``lambda`` for terser code, but keep in mind that the error message in those cases is usually not very useful because it is not using an ``assert`` expression. .. code-block:: python qtbot.waitUntil(lambda: view_model.count() > 10) Note that this usage only accepts returning actual ``True`` and ``False`` values, so returning an empty list to express "falseness" raises a ``ValueError``. :param callback: callable that will be called periodically. :param timeout: timeout value in ms. :raises ValueError: if the return value from the callback is anything other than ``None``, ``True`` or ``False``. .. note:: This method is also available as ``wait_until`` (pep-8 alias) """ __tracebackhide__ = True import time start = time.time() def timed_out(): elapsed = time.time() - start elapsed_ms = elapsed * 1000 return elapsed_ms > timeout timeout_msg = f"waitUntil timed out in {timeout} milliseconds" while True: try: result = callback() except AssertionError as e: if timed_out(): raise TimeoutError(timeout_msg) from e else: if result not in (None, True, False): msg = "waitUntil() callback must return None, True or False, returned %r" raise ValueError(msg % result) # 'assert' form if result is None: return # 'True/False' form if result: return if timed_out(): raise TimeoutError(timeout_msg) self.wait(10) def waitCallback(self, *, timeout=5000, raising=None): """ .. versionadded:: 3.1 Stops current test until a callback is called. Used to stop the control flow of a test until the returned callback is called, or a number of milliseconds, specified by ``timeout``, has elapsed. Best used as a context manager:: with qtbot.waitCallback() as callback: function_taking_a_callback(callback) assert callback.args == [True] Also, you can use the :class:`CallbackBlocker` directly if the context manager form is not convenient:: blocker = qtbot.waitCallback(timeout=1000) function_calling_a_callback(blocker) blocker.wait() :param int timeout: How many milliseconds to wait before resuming control flow. :param bool raising: If :class:`QtBot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` is set in the config. :returns: A ``CallbackBlocker`` object which can be used directly as a callback as it implements ``__call__``. .. note:: This method is also available as ``wait_callback`` (pep-8 alias) """ raising = self._should_raise(raising) blocker = CallbackBlocker(timeout=timeout, raising=raising) return blocker @contextlib.contextmanager def captureExceptions(self): """ .. versionadded:: 2.1 Context manager that captures Qt virtual method exceptions that happen in block inside context. .. code-block:: python with qtbot.capture_exceptions() as exceptions: qtbot.click(button) # exception is a list of sys.exc_info tuples assert len(exceptions) == 1 .. note:: This method is also available as ``capture_exceptions`` (pep-8 alias) """ from pytestqt.exceptions import capture_exceptions with capture_exceptions() as exceptions: yield exceptions capture_exceptions = captureExceptions @staticmethod def keyClick(*args, **kwargs): qt_api.QtTest.QTest.keyClick(*args, **kwargs) @staticmethod def keyClicks(*args, **kwargs): qt_api.QtTest.QTest.keyClicks(*args, **kwargs) @staticmethod def keyEvent(*args, **kwargs): qt_api.QtTest.QTest.keyEvent(*args, **kwargs) @staticmethod def keyPress(*args, **kwargs): qt_api.QtTest.QTest.keyPress(*args, **kwargs) @staticmethod def keyRelease(*args, **kwargs): qt_api.QtTest.QTest.keyRelease(*args, **kwargs) @staticmethod def keySequence(widget, key_sequence): if not hasattr(qt_api.QtTest.QTest, "keySequence"): raise NotImplementedError("This method is available from Qt 5.10 upwards.") qt_api.QtTest.QTest.keySequence(widget, key_sequence) @staticmethod def keyToAscii(key): if not hasattr(qt_api.QtTest.QTest, "keyToAscii"): raise NotImplementedError("This method isn't available on PyQt5.") qt_api.QtTest.QTest.keyToAscii(key) @staticmethod def mouseClick(*args, **kwargs): qt_api.QtTest.QTest.mouseClick(*args, **kwargs) @staticmethod def mouseDClick(*args, **kwargs): qt_api.QtTest.QTest.mouseDClick(*args, **kwargs) @staticmethod def mouseMove(*args, **kwargs): qt_api.QtTest.QTest.mouseMove(*args, **kwargs) @staticmethod def mousePress(*args, **kwargs): qt_api.QtTest.QTest.mousePress(*args, **kwargs) @staticmethod def mouseRelease(*args, **kwargs): qt_api.QtTest.QTest.mouseRelease(*args, **kwargs) # pep-8 aliases def add_widget(self, *args, **kwargs): return self.addWidget(*args, **kwargs) def wait_active(self, *args, **kwargs): return self.waitActive(*args, **kwargs) def wait_exposed(self, *args, **kwargs): return self.waitExposed(*args, **kwargs) def wait_for_window_shown(self, *args, **kwargs): return self.waitForWindowShown(*args, **kwargs) def wait_signal(self, *args, **kwargs): return self.waitSignal(*args, **kwargs) def wait_signals(self, *args, **kwargs): return self.waitSignals(*args, **kwargs) def assert_not_emitted(self, *args, **kwargs): return self.assertNotEmitted(*args, **kwargs) def wait_until(self, *args, **kwargs): return self.waitUntil(*args, **kwargs) def wait_callback(self, *args, **kwargs): return self.waitCallback(*args, **kwargs) # provide easy access to exceptions to qtbot fixtures QtBot.SignalEmittedError = SignalEmittedError QtBot.TimeoutError = TimeoutError QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError def _add_widget(item, widget, *, before_close_func=None): """ Register a widget into the given pytest item for later closing. """ qt_widgets = getattr(item, "qt_widgets", []) qt_widgets.append((weakref.ref(widget), before_close_func)) item.qt_widgets = qt_widgets def _close_widgets(item): """ Close all widgets registered in the pytest item. """ widgets = getattr(item, "qt_widgets", None) if widgets: for w, before_close_func in item.qt_widgets: w = w() if w is not None: if before_close_func is not None: before_close_func(w) w.close() w.deleteLater() del item.qt_widgets def _iter_widgets(item): """ Iterates over widgets registered in the given pytest item. """ qt_widgets = getattr(item, "qt_widgets", []) return (w for (w, _) in qt_widgets) class _WaitWidgetContextManager: """ Context manager implementation used by ``waitActive`` and ``waitExposed`` methods. """ def __init__(self, method_name, adjective_name, widget, timeout): """ :param str method_name: name ot the ``QtTest`` method to call to check if widget is active/exposed. :param str adjective_name: "activated" or "exposed". :param widget: :param timeout: """ self._method_name = method_name self._adjective_name = adjective_name self._widget = widget self._timeout = timeout def __enter__(self): __tracebackhide__ = True return self def __exit__(self, exc_type, exc_val, exc_tb): __tracebackhide__ = True try: if exc_type is None: method = getattr(qt_api.QtTest.QTest, self._method_name) r = method(self._widget, self._timeout) if not r: msg = "widget {} not {} in {} ms.".format( self._widget, self._adjective_name, self._timeout ) raise TimeoutError(msg) finally: self._widget = None pytest-qt-4.0.2/src/pytestqt/utils.py0000644000175100001710000000052314061506270020465 0ustar runnerdocker00000000000000def get_marker(item, name): """Get a marker from a pytest item. This is here in order to stay compatible with pytest < 3.6 and not produce any deprecation warnings with >= 3.6. """ try: return item.get_closest_marker(name) except AttributeError: # pytest < 3.6 return item.get_marker(name) pytest-qt-4.0.2/src/pytestqt/wait_signal.py0000644000175100001710000006542614061506270021643 0ustar runnerdocker00000000000000import functools from pytestqt.exceptions import TimeoutError from pytestqt.qt_compat import qt_api class _AbstractSignalBlocker: """ Base class for :class:`SignalBlocker` and :class:`MultiSignalBlocker`. Provides :meth:`wait` and a context manager protocol, but no means to add new signals and to detect when the signals should be considered "done". This needs to be implemented by subclasses. Subclasses also need to provide ``self._signals`` which should evaluate to ``False`` if no signals were configured. """ def __init__(self, timeout=5000, raising=True): self._loop = qt_api.QtCore.QEventLoop() self.timeout = timeout self.signal_triggered = False self.raising = raising self._signals = None # will be initialized by inheriting implementations self._timeout_message = "" if timeout is None or timeout == 0: self._timer = None else: self._timer = qt_api.QtCore.QTimer(self._loop) self._timer.setSingleShot(True) self._timer.setInterval(timeout) def wait(self): """ Waits until either a connected signal is triggered or timeout is reached. :raise ValueError: if no signals are connected and timeout is None; in this case it would wait forever. """ __tracebackhide__ = True if self.signal_triggered: return if self.timeout is None and not self._signals: raise ValueError("No signals or timeout specified.") if self._timer is not None: self._timer.timeout.connect(self._quit_loop_by_timeout) self._timer.start() if self.timeout != 0: qt_api.exec(self._loop) if not self.signal_triggered and self.raising: raise TimeoutError(self._timeout_message) def _quit_loop_by_timeout(self): try: self._cleanup() finally: self._loop.quit() def _cleanup(self): # store timeout message before the data to construct it is lost self._timeout_message = self._get_timeout_error_message() if self._timer is not None: _silent_disconnect(self._timer.timeout, self._quit_loop_by_timeout) self._timer.stop() self._timer = None def _get_timeout_error_message(self): """Subclasses have to implement this, returning an appropriate error message for a TimeoutError.""" raise NotImplementedError # pragma: no cover def _extract_pyqt_signal_name(self, potential_pyqt_signal): signal_name = potential_pyqt_signal.signal # type: str if not isinstance(signal_name, str): raise TypeError( "Invalid 'signal' attribute in {}. " "Expected str but got {}".format(signal_name, type(signal_name)) ) # strip magic number "2" that PyQt prepends to the signal names signal_name = signal_name.lstrip("2") return signal_name def _extract_signal_from_signal_tuple(self, potential_signal_tuple): if isinstance(potential_signal_tuple, tuple): if len(potential_signal_tuple) != 2: raise ValueError( "Signal tuple must have length of 2 (first element is the signal, " "the second element is the signal's name)." ) signal_tuple = potential_signal_tuple signal_name = signal_tuple[1] if not isinstance(signal_name, str): raise TypeError( "Invalid type for provided signal name, " "expected str but got {}".format(type(signal_name)) ) if not signal_name: raise ValueError("The provided signal name may not be empty") return signal_name return "" def determine_signal_name(self, potential_signal_tuple): """ Attempts to determine the signal's name. If the user provided the signal name as 2nd value of the tuple, this name has preference. Bad values cause a ``ValueError``. Otherwise it attempts to get the signal from the ``signal`` attribute of ``signal`` (which only exists for PyQt signals). :returns: str name of the signal, an empty string if no signal name can be determined, or raises an error in case the user provided an invalid signal name manually """ signal_name = self._extract_signal_from_signal_tuple(potential_signal_tuple) if not signal_name: try: signal_name = self._extract_pyqt_signal_name(potential_signal_tuple) except AttributeError: # not a PyQt signal # -> no signal name could be determined signal_name = "" return signal_name def get_callback_name(self, callback): """Attempts to extract the name of the callback. Returns empty string in case of failure.""" try: name = callback.__name__ except AttributeError: try: name = ( callback.func.__name__ ) # e.g. for callbacks wrapped with functools.partial() except AttributeError: name = "" return name @staticmethod def get_signal_from_potential_signal_tuple(signal_tuple): if isinstance(signal_tuple, tuple): return signal_tuple[0] return signal_tuple def __enter__(self): return self def __exit__(self, type, value, traceback): __tracebackhide__ = True if value is None: # only wait if no exception happened inside the "with" block self.wait() class SignalBlocker(_AbstractSignalBlocker): """ Returned by :meth:`pytestqt.qtbot.QtBot.waitSignal` method. :ivar int timeout: maximum time to wait for a signal to be triggered. Can be changed before :meth:`wait` is called. :ivar bool signal_triggered: set to ``True`` if a signal (or all signals in case of :class:`MultipleSignalBlocker`) was triggered, or ``False`` if timeout was reached instead. Until :meth:`wait` is called, this is set to ``None``. :ivar bool raising: If :class:`TimeoutError` should be raised if a timeout occurred. .. note:: contrary to the parameter of same name in :meth:`pytestqt.qtbot.QtBot.waitSignal`, this parameter does not consider the :ref:`qt_default_raising` option. :ivar list args: The arguments which were emitted by the signal, or None if the signal wasn't emitted at all. .. versionadded:: 1.10 The *args* attribute. .. automethod:: wait .. automethod:: connect """ def __init__(self, timeout=5000, raising=True, check_params_cb=None): super().__init__(timeout, raising=raising) self._signals = [] self.args = None self.all_args = [] self.check_params_callback = check_params_cb self.signal_name = "" def connect(self, signal): """ Connects to the given signal, making :meth:`wait()` return once this signal is emitted. More than one signal can be connected, in which case **any** one of them will make ``wait()`` return. :param signal: QtCore.Signal or tuple (QtCore.Signal, str) """ self.signal_name = self.determine_signal_name(potential_signal_tuple=signal) actual_signal = self.get_signal_from_potential_signal_tuple(signal) actual_signal.connect(self._quit_loop_by_signal) self._signals.append(actual_signal) def _quit_loop_by_signal(self, *args): """ quits the event loop and marks that we finished because of a signal. """ if self.check_params_callback: self.all_args.append(args) if not self.check_params_callback(*args): return # parameter check did not pass try: self.signal_triggered = True self.args = list(args) self._cleanup() finally: self._loop.quit() def _cleanup(self): super()._cleanup() for signal in self._signals: _silent_disconnect(signal, self._quit_loop_by_signal) self._signals = [] def get_params_as_str(self): if not self.all_args: return "" if len(self.all_args[0]) == 1: # we have a list of tuples with 1 element each (i.e. the signal has 1 parameter), it doesn't make sense # to return something like "[(someParam,), (someParam,)]", it's just ugly. Instead return something like # "[someParam, someParam]" args_list = [arg[0] for arg in self.all_args] else: args_list = self.all_args return str(args_list) def _get_timeout_error_message(self): if self.check_params_callback is not None: return ( "Signal {signal_name} emitted with parameters {params} " "within {timeout} ms, but did not satisfy " "the {cb_name} callback" ).format( signal_name=self.signal_name, params=self.get_params_as_str(), timeout=self.timeout, cb_name=self.get_callback_name(self.check_params_callback), ) else: return "Signal {signal_name} not emitted after {timeout} ms".format( signal_name=self.signal_name, timeout=self.timeout ) class SignalAndArgs: def __init__(self, signal_name, args): self.signal_name = signal_name self.args = args def _get_readable_signal_with_optional_args(self): args = repr(self.args) if self.args else "" # remove signal parameter signature, e.g. turn "some_signal(str,int)" to "some_signal", because we're adding # the actual parameters anyways signal_name = self.signal_name signal_name = signal_name.partition("(")[0] return signal_name + args def __str__(self): return self._get_readable_signal_with_optional_args() def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ else: return False # Returns e.g. "3rd" for 3, or "21st" for 21 def get_ordinal_str(n): return "%d%s" % (n, {1: "st", 2: "nd", 3: "rd"}.get(n if n < 20 else n % 10, "th")) class NoMatchingIndexFoundError(Exception): pass class MultiSignalBlocker(_AbstractSignalBlocker): """ Returned by :meth:`pytestqt.qtbot.QtBot.waitSignals` method, blocks until all signals connected to it are triggered or the timeout is reached. Variables identical to :class:`SignalBlocker`: - ``timeout`` - ``signal_triggered`` - ``raising`` .. automethod:: wait """ def __init__(self, timeout=5000, raising=True, check_params_cbs=None, order="none"): super().__init__(timeout, raising=raising) self._order = order self._check_params_callbacks = check_params_cbs self._signals_emitted = ( [] ) # list of booleans, indicates whether the signal was already emitted self._signals_map = ( {} ) # maps from a unique Signal to a list of indices where to expect signal instance emits self._signals = ( [] ) # list of all Signals (for compatibility with _AbstractSignalBlocker) self._slots = [] # list of slot functions self._signal_expected_index = 0 # only used when forcing order self._strict_order_violated = False self._actual_signal_and_args_at_violation = None self._signal_names = ( {} ) # maps from the unique Signal to the name of the signal (as string) self.all_signals_and_args = [] # list of SignalAndArgs instances def add_signals(self, signals): """ Adds the given signal to the list of signals which :meth:`wait()` waits for. :param list signals: list of QtCore.Signal`s or tuples (QtCore.Signal, str) """ self._determine_unique_signals(signals) self._create_signal_emitted_indices(signals) self._connect_unique_signals() def _get_timeout_error_message(self): if not self._are_signal_names_available(): error_message = self._get_degenerate_error_message() else: error_message = self._get_expected_and_actual_signals_message() if self._strict_order_violated: error_message = self._get_order_violation_message() + error_message return error_message def _determine_unique_signals(self, signals): # create a map that maps from a unique signal to a list of indices # (positions) where this signal is expected (in case order matters) signals_as_str = [ str(self.get_signal_from_potential_signal_tuple(signal)) for signal in signals ] # maps from a signal-string to one of the signal instances (the first one found) signal_str_to_unique_signal = {} for index, signal_str in enumerate(signals_as_str): signal = self.get_signal_from_potential_signal_tuple(signals[index]) potential_tuple = signals[index] if signal_str not in signal_str_to_unique_signal: unique_signal_tuple = potential_tuple signal_str_to_unique_signal[signal_str] = signal self._signals_map[signal] = [index] # create a new list else: # append to existing list unique_signal = signal_str_to_unique_signal[signal_str] self._signals_map[unique_signal].append(index) unique_signal_tuple = signals[index] self._determine_and_save_signal_name(unique_signal_tuple) def _determine_and_save_signal_name(self, unique_signal_tuple): signal_name = self.determine_signal_name(unique_signal_tuple) if signal_name: # might be an empty string if no name could be determined unique_signal = self.get_signal_from_potential_signal_tuple( unique_signal_tuple ) self._signal_names[unique_signal] = signal_name def _create_signal_emitted_indices(self, signals): for signal in signals: self._signals_emitted.append(False) def _connect_unique_signals(self): for unique_signal in self._signals_map: slot = functools.partial(self._unique_signal_emitted, unique_signal) self._slots.append(slot) unique_signal.connect(slot) self._signals.append(unique_signal) def _unique_signal_emitted(self, unique_signal, *args): """ Called when a given signal is emitted. If all expected signals have been emitted, quits the event loop and marks that we finished because signals. """ self._record_emitted_signal_if_possible(unique_signal, *args) self._check_signal_match(unique_signal, *args) if self._all_signals_emitted(): self.signal_triggered = True try: self._cleanup() finally: self._loop.quit() def _record_emitted_signal_if_possible(self, unique_signal, *args): if self._are_signal_names_available(): self.all_signals_and_args.append( SignalAndArgs(signal_name=self._signal_names[unique_signal], args=args) ) def _check_signal_match(self, unique_signal, *args): if self._order == "none": # perform the test for every matching index (stop after the first one that matches) try: successful_index = self._get_first_matching_index(unique_signal, *args) self._signals_emitted[successful_index] = True except NoMatchingIndexFoundError: # none found pass elif self._order == "simple": if self._check_signal_matches_expected_index(unique_signal, *args): self._signals_emitted[self._signal_expected_index] = True self._signal_expected_index += 1 else: # self.order == "strict" if not self._strict_order_violated: # only do the check if the strict order has not been violated yet self._strict_order_violated = ( True # assume the order has been violated this time ) if self._check_signal_matches_expected_index(unique_signal, *args): self._signals_emitted[self._signal_expected_index] = True self._signal_expected_index += 1 self._strict_order_violated = ( False # order has not been violated after all! ) else: if self._are_signal_names_available(): self._actual_signal_and_args_at_violation = SignalAndArgs( signal_name=self._signal_names[unique_signal], args=args ) def _all_signals_emitted(self): return not self._strict_order_violated and all(self._signals_emitted) def _get_first_matching_index(self, unique_signal, *args): successfully_emitted = False successful_index = -1 potential_indices = self._get_unemitted_signal_indices(unique_signal) for potential_index in potential_indices: if not self._violates_callback_at_index(potential_index, *args): successful_index = potential_index successfully_emitted = True break if not successfully_emitted: raise NoMatchingIndexFoundError return successful_index def _check_signal_matches_expected_index(self, unique_signal, *args): potential_indices = self._get_unemitted_signal_indices(unique_signal) if potential_indices: if self._signal_expected_index == potential_indices[0]: if not self._violates_callback_at_index( self._signal_expected_index, *args ): return True return False def _violates_callback_at_index(self, index, *args): """ Checks if there's a callback at the provided index that is violates due to invalid parameters. Returns False if there is no callback for that index, or if a callback exists but it wasn't violated (returned True). Returns True otherwise. """ if self._check_params_callbacks: callback_func = self._check_params_callbacks[index] if callback_func: if not callback_func(*args): return True return False def _get_unemitted_signal_indices(self, signal): """Returns the indices for the provided signal for which NO signal instance has been emitted yet.""" return [ index for index in self._signals_map[signal] if not self._signals_emitted[index] ] def _are_signal_names_available(self): if self._signal_names: return True return False def _get_degenerate_error_message(self): received_signals = sum(self._signals_emitted) total_signals = len(self._signals_emitted) return ( "Received {actual} of the {total} expected signals. " "To improve this error message, provide the names of the signals " "in the waitSignals() call." ).format(actual=received_signals, total=total_signals) def _get_expected_and_actual_signals_message(self): if not self.all_signals_and_args: emitted_signals = "None" else: emitted_signal_string_list = [str(_) for _ in self.all_signals_and_args] emitted_signals = self._format_as_array(emitted_signal_string_list) missing_signal_strings = [] for missing_signal_index in self._get_missing_signal_indices(): missing_signal_strings.append( self._get_signal_string_representation_for_index(missing_signal_index) ) missing_signals = self._format_as_array(missing_signal_strings) return "Emitted signals: {}. Missing: {}".format( emitted_signals, missing_signals ) @staticmethod def _format_as_array(list_of_strings): return "[{}]".format(", ".join(list_of_strings)) def _get_order_violation_message(self): expected_signal_as_str = self._get_signal_string_representation_for_index( self._signal_expected_index ) actual_signal_as_str = str(self._actual_signal_and_args_at_violation) return ( "Signal order violated! Expected {expected} as {ordinal} signal, " "but received {actual} instead. " ).format( expected=expected_signal_as_str, ordinal=get_ordinal_str(self._signal_expected_index + 1), actual=actual_signal_as_str, ) def _get_missing_signal_indices(self): return [ index for index, value in enumerate(self._signals_emitted) if not self._signals_emitted[index] ] def _get_signal_string_representation_for_index(self, index): """Returns something like or (callback: )""" signal = self._get_signal_for_index(index) signal_str_repr = self._signal_names[signal] if self._check_params_callbacks: potential_callback = self._check_params_callbacks[index] if potential_callback: callback_name = self.get_callback_name(potential_callback) if callback_name: signal_str_repr += f" (callback: {callback_name})" return signal_str_repr def _get_signal_for_index(self, index): for signal in self._signals_map: if index in self._signals_map[signal]: return signal def _cleanup(self): super()._cleanup() for i in range(len(self._signals)): signal = self._signals[i] slot = self._slots[i] _silent_disconnect(signal, slot) del self._signals_emitted[:] self._signals_map.clear() del self._slots[:] class SignalEmittedSpy: """ .. versionadded:: 1.11 An object which checks if a given signal has ever been emitted. Intended to be used as a context manager. """ def __init__(self, signal): self.signal = signal self.emitted = False self.args = None def slot(self, *args): self.emitted = True self.args = args def __enter__(self): self.signal.connect(self.slot) def __exit__(self, type, value, traceback): self.signal.disconnect(self.slot) def assert_not_emitted(self): if self.emitted: if self.args: raise SignalEmittedError( "Signal %r unexpectedly emitted with " "arguments %r" % (self.signal, list(self.args)) ) else: raise SignalEmittedError(f"Signal {self.signal!r} unexpectedly emitted") class CallbackBlocker: """ .. versionadded:: 3.1 An object which checks if the returned callback gets called. Intended to be used as a context manager. :ivar int timeout: maximum time to wait for the callback to be called. :ivar bool raising: If :class:`TimeoutError` should be raised if a timeout occured. .. note:: contrary to the parameter of same name in :meth:`pytestqt.qtbot.QtBot.waitCallback`, this parameter does not consider the :ref:`qt_default_raising` option. :ivar list args: The arguments with which the callback was called, or None if the callback wasn't called at all. :ivar dict kwargs: The keyword arguments with which the callback was called, or None if the callback wasn't called at all. """ def __init__(self, timeout=5000, raising=True): self.timeout = timeout self.raising = raising self.args = None self.kwargs = None self.called = False self._loop = qt_api.QtCore.QEventLoop() if timeout is None: self._timer = None else: self._timer = qt_api.QtCore.QTimer(self._loop) self._timer.setSingleShot(True) self._timer.setInterval(timeout) def wait(self): """ Waits until either the returned callback is called or timeout is reached. """ __tracebackhide__ = True if self.called: return if self._timer is not None: self._timer.timeout.connect(self._quit_loop_by_timeout) self._timer.start() qt_api.exec(self._loop) if not self.called and self.raising: raise TimeoutError("Callback wasn't called after %sms." % self.timeout) def assert_called_with(self, *args, **kwargs): """ Check that the callback was called with the same arguments as this function. """ assert self.called assert self.args == list(args) assert self.kwargs == kwargs def _quit_loop_by_timeout(self): try: self._cleanup() finally: self._loop.quit() def _cleanup(self): if self._timer is not None: _silent_disconnect(self._timer.timeout, self._quit_loop_by_timeout) self._timer.stop() self._timer = None def __call__(self, *args, **kwargs): # Not inside the try: block, as if self.called is True, we did quit the # loop already. if self.called: raise CallbackCalledTwiceError("Callback called twice") try: self.args = list(args) self.kwargs = kwargs self.called = True self._cleanup() finally: self._loop.quit() def __enter__(self): return self def __exit__(self, type, value, traceback): __tracebackhide__ = True if value is None: # only wait if no exception happened inside the "with" block self.wait() class SignalEmittedError(Exception): """ .. versionadded:: 1.11 The exception thrown by :meth:`pytestqt.qtbot.QtBot.assertNotEmitted` if a signal was emitted unexpectedly. """ pass class CallbackCalledTwiceError(Exception): """ .. versionadded:: 3.1 The exception thrown by :meth:`pytestqt.qtbot.QtBot.waitCallback` if a callback was called twice. """ pass def _silent_disconnect(signal, slot): """Disconnects a signal from a slot, ignoring errors. Sometimes Qt might disconnect a signal automatically for unknown reasons. """ try: signal.disconnect(slot) except (TypeError, RuntimeError): # pragma: no cover pass pytest-qt-4.0.2/tests/0000755000175100001710000000000014061506274015435 5ustar runnerdocker00000000000000pytest-qt-4.0.2/tests/conftest.py0000644000175100001710000000443714061506270017640 0ustar runnerdocker00000000000000import functools import time import pytest from pytestqt.qt_compat import qt_api pytest_plugins = "pytester" @pytest.fixture def stop_watch(): """ Fixture that makes it easier for tests to ensure signals emitted and timeouts are being respected. """ class StopWatch: def __init__(self): self._start_time = None self.elapsed = None def start(self): self._start_time = time.monotonic() def stop(self): self.elapsed = (time.monotonic() - self._start_time) * 1000.0 def check(self, timeout, *delays): """ Make sure either timeout (if given) or at most of the given delays used to trigger a signal has passed. """ self.stop() if timeout is None: timeout = max(delays) * 1.35 # 35% tolerance max_wait_ms = max(delays + (timeout,)) assert self.elapsed < max_wait_ms return StopWatch() @pytest.fixture def timer(): """ Returns a Timer-like object which can be used to trigger signals and callbacks after some time. It is recommended to use this instead of ``QTimer.singleShot`` uses a static timer which may trigger after a test finishes, possibly causing havoc. """ class Timer(qt_api.QtCore.QObject): def __init__(self): qt_api.QtCore.QObject.__init__(self) self.timers_and_slots = [] def shutdown(self): while self.timers_and_slots: t, slot = self.timers_and_slots.pop(-1) t.stop() t.timeout.disconnect(slot) def single_shot(self, signal, delay): t = qt_api.QtCore.QTimer(self) t.setSingleShot(True) slot = functools.partial(self._emit, signal) t.timeout.connect(slot) t.start(delay) self.timers_and_slots.append((t, slot)) def single_shot_callback(self, callback, delay): t = qt_api.QtCore.QTimer(self) t.setSingleShot(True) t.timeout.connect(callback) t.start(delay) self.timers_and_slots.append((t, callback)) def _emit(self, signal): signal.emit() timer = Timer() yield timer timer.shutdown() pytest-qt-4.0.2/tests/test_basics.py0000644000175100001710000003425614061506270020320 0ustar runnerdocker00000000000000import weakref import pytest from pytestqt import qt_compat from pytestqt.qt_compat import qt_api def test_basics(qtbot): """ Basic test that works more like a sanity check to ensure we are setting up a QApplication properly and are able to display a simple event_recorder. """ assert qt_api.QtWidgets.QApplication.instance() is not None widget = qt_api.QtWidgets.QWidget() qtbot.addWidget(widget) widget.setWindowTitle("W1") widget.show() assert widget.isVisible() assert widget.windowTitle() == "W1" def test_qapp_default_name(qapp): assert qapp.applicationName() == "pytest-qt-qapp" def test_qapp_name(testdir): testdir.makepyfile( """ def test_name(qapp): assert qapp.applicationName() == "frobnicator" """ ) testdir.makeini( """ [pytest] qt_qapp_name = frobnicator """ ) res = testdir.runpytest_subprocess() res.stdout.fnmatch_lines("*1 passed*") def test_key_events(qtbot, event_recorder): """ Basic key events test. """ def extract(key_event): return ( key_event.type(), qt_api.QtCore.Qt.Key(key_event.key()), key_event.text(), ) event_recorder.registerEvent(qt_api.QtGui.QKeyEvent, extract) qtbot.keyPress(event_recorder, "a") assert event_recorder.event_data == ( qt_api.QtCore.QEvent.Type.KeyPress, qt_api.QtCore.Qt.Key.Key_A, "a", ) qtbot.keyRelease(event_recorder, "a") assert event_recorder.event_data == ( qt_api.QtCore.QEvent.Type.KeyRelease, qt_api.QtCore.Qt.Key.Key_A, "a", ) def test_mouse_events(qtbot, event_recorder): """ Basic mouse events test. """ def extract(mouse_event): return (mouse_event.type(), mouse_event.button(), mouse_event.modifiers()) event_recorder.registerEvent(qt_api.QtGui.QMouseEvent, extract) qtbot.mousePress(event_recorder, qt_api.QtCore.Qt.MouseButton.LeftButton) assert event_recorder.event_data == ( qt_api.QtCore.QEvent.Type.MouseButtonPress, qt_api.QtCore.Qt.MouseButton.LeftButton, qt_api.QtCore.Qt.KeyboardModifier.NoModifier, ) qtbot.mousePress( event_recorder, qt_api.QtCore.Qt.MouseButton.RightButton, qt_api.QtCore.Qt.KeyboardModifier.AltModifier, ) assert event_recorder.event_data == ( qt_api.QtCore.QEvent.Type.MouseButtonPress, qt_api.QtCore.Qt.MouseButton.RightButton, qt_api.QtCore.Qt.KeyboardModifier.AltModifier, ) def test_stop(qtbot, timer): """ Test qtbot.stop() """ widget = qt_api.QtWidgets.QWidget() qtbot.addWidget(widget) with qtbot.waitExposed(widget): widget.show() timer.single_shot_callback(widget.close, 0) qtbot.stop() @pytest.mark.parametrize("show", [True, False]) @pytest.mark.parametrize("method_name", ["waitExposed", "waitActive"]) def test_wait_window(show, method_name, qtbot): """ Using one of the wait-widget methods should not raise anything if the widget is properly displayed, otherwise should raise a TimeoutError. """ method = getattr(qtbot, method_name) widget = qt_api.QtWidgets.QWidget() qtbot.add_widget(widget) if show: with method(widget, timeout=1000): widget.show() else: with pytest.raises(qtbot.TimeoutError): with method(widget, timeout=100): pass @pytest.mark.parametrize("show", [True, False]) def test_wait_for_window_shown(qtbot, show): widget = qt_api.QtWidgets.QWidget() qtbot.add_widget(widget) if show: widget.show() with pytest.deprecated_call(match="waitForWindowShown is deprecated"): shown = qtbot.waitForWindowShown(widget) assert shown == show @pytest.mark.parametrize("method_name", ["waitExposed", "waitActive"]) def test_wait_window_propagates_other_exception(method_name, qtbot): """ Exceptions raised inside the with-statement of wait-widget methods should propagate properly. """ method = getattr(qtbot, method_name) widget = qt_api.QtWidgets.QWidget() qtbot.add_widget(widget) with pytest.raises(ValueError, match="some other error"): with method(widget, timeout=100): widget.show() raise ValueError("some other error") def test_widget_kept_as_weakref(qtbot): """ Test if the widget is kept as a weak reference in QtBot """ widget = qt_api.QtWidgets.QWidget() qtbot.add_widget(widget) widget = weakref.ref(widget) assert widget() is None def test_event_processing_before_and_after_teardown(testdir): """ Make sure events are processed before and after fixtures are torn down. The test works by creating a session object which pops() one of its events whenever a processEvents() occurs. Fixture and tests append values to the event list but expect the list to have been processed (by the pop()) at each point of interest. https://github.com/pytest-dev/pytest-qt/issues/67 """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import pytest @pytest.fixture(scope='session') def events_queue(qapp): class EventsQueue(qt_api.QtCore.QObject): def __init__(self): qt_api.QtCore.QObject.__init__(self) self.events = [] def pop_later(self): qapp.postEvent(self, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.Type.User)) def event(self, ev): if ev.type() == qt_api.QtCore.QEvent.Type.User: self.events.pop(-1) return qt_api.QtCore.QObject.event(self, ev) return EventsQueue() @pytest.fixture def fix(events_queue, qapp): assert events_queue.events == [] yield assert events_queue.events == [] events_queue.events.append('fixture teardown') events_queue.pop_later() @pytest.mark.parametrize('i', range(3)) def test_events(events_queue, fix, i): assert events_queue.events == [] events_queue.events.append('test event') events_queue.pop_later() """ ) res = testdir.runpytest() res.stdout.fnmatch_lines(["*3 passed in*"]) def test_header(testdir): testdir.makeconftest( """ from pytestqt import qt_compat from pytestqt.qt_compat import qt_api def mock_get_versions(): return qt_compat.VersionTuple('PyQtAPI', '1.0', '2.5', '3.5') assert hasattr(qt_api, 'get_versions') qt_api.get_versions = mock_get_versions """ ) res = testdir.runpytest() res.stdout.fnmatch_lines( ["*test session starts*", "PyQtAPI 1.0 -- Qt runtime 2.5 -- Qt compiled 3.5"] ) def test_qvariant(tmpdir): """Test that QVariant works in the same way across all supported Qt bindings.""" settings = qt_api.QtCore.QSettings( str(tmpdir / "foo.ini"), qt_api.QtCore.QSettings.Format.IniFormat ) settings.setValue("int", 42) settings.setValue("str", "Hello") settings.setValue("empty", None) assert settings.value("int") == 42 assert settings.value("str") == "Hello" assert settings.value("empty") is None def test_widgets_closed_before_fixtures(testdir): """ Ensure widgets added by "qtbot.add_widget" are closed before all other fixtures are teardown. (#106). """ testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class Widget(qt_api.QtWidgets.QWidget): closed = False def closeEvent(self, e): e.accept() self.closed = True @pytest.fixture def widget(qtbot): w = Widget() qtbot.add_widget(w) yield w assert w.closed def test_foo(widget): pass """ ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*= 1 passed in *"]) def test_qtbot_wait(qtbot, stop_watch): stop_watch.start() qtbot.wait(250) stop_watch.stop() assert stop_watch.elapsed >= 220 @pytest.fixture def event_recorder(qtbot): class EventRecorder(qt_api.QtWidgets.QWidget): """ Widget that records some kind of events sent to it. When this event_recorder receives a registered event (by calling `registerEvent`), it will call the associated *extract* function and hold the return value from the function in the `event_data` member. """ def __init__(self): qt_api.QtWidgets.QWidget.__init__(self) self._event_types = {} self.event_data = None def registerEvent(self, event_type, extract_func): self._event_types[event_type] = extract_func def event(self, ev): for event_type, extract_func in self._event_types.items(): if isinstance(ev, event_type): self.event_data = extract_func(ev) return True return False widget = EventRecorder() qtbot.addWidget(widget) return widget @pytest.mark.parametrize( "value, expected", [ (True, True), (False, False), ("True", True), ("False", False), ("true", True), ("false", False), ], ) def test_parse_ini_boolean_valid(value, expected): import pytestqt.qtbot assert pytestqt.qtbot._parse_ini_boolean(value) == expected def test_parse_ini_boolean_invalid(): import pytestqt.qtbot with pytest.raises(ValueError): pytestqt.qtbot._parse_ini_boolean("foo") @pytest.mark.parametrize("option_api", ["pyqt5", "pyqt6", "pyside2", "pyside6"]) def test_qt_api_ini_config(testdir, monkeypatch, option_api): """ Test qt_api ini option handling. """ from pytestqt.qt_compat import qt_api monkeypatch.delenv("PYTEST_QT_API", raising=False) testdir.makeini( """ [pytest] qt_api={option_api} """.format( option_api=option_api ) ) testdir.makepyfile( """ import pytest def test_foo(qtbot): pass """ ) result = testdir.runpytest_subprocess() if qt_api.pytest_qt_api == option_api: result.stdout.fnmatch_lines(["* 1 passed in *"]) else: try: ModuleNotFoundError except NameError: # Python < 3.6 result.stderr.fnmatch_lines(["*ImportError:*"]) else: # Python >= 3.6 result.stderr.fnmatch_lines(["*ModuleNotFoundError:*"]) @pytest.mark.parametrize("envvar", ["pyqt5", "pyqt6", "pyside2", "pyside6"]) def test_qt_api_ini_config_with_envvar(testdir, monkeypatch, envvar): """ensure environment variable wins over config value if both are present""" testdir.makeini( """ [pytest] qt_api={option_api} """.format( option_api="piecute" ) ) monkeypatch.setenv("PYTEST_QT_API", envvar) testdir.makepyfile( """ import pytest def test_foo(qtbot): pass """ ) result = testdir.runpytest_subprocess() if qt_api.pytest_qt_api == envvar: result.stdout.fnmatch_lines(["* 1 passed in *"]) else: try: ModuleNotFoundError except NameError: # Python < 3.6 result.stderr.fnmatch_lines(["*ImportError:*"]) else: # Python >= 3.6 result.stderr.fnmatch_lines(["*ModuleNotFoundError:*"]) def test_invalid_qt_api_envvar(testdir, monkeypatch): """ Make sure the error message with an invalid PYQTEST_QT_API is correct. """ testdir.makepyfile( """ import pytest def test_foo(qtbot): pass """ ) monkeypatch.setenv("PYTEST_QT_API", "piecute") result = testdir.runpytest_subprocess() result.stderr.fnmatch_lines( ["* Invalid value for $PYTEST_QT_API: piecute, expected one of *"] ) def test_qapp_args(testdir): """ Test customizing of QApplication arguments. """ testdir.makeconftest( """ import pytest @pytest.fixture(scope='session') def qapp_args(): return ['--test-arg'] """ ) testdir.makepyfile( """ def test_args(qapp): assert '--test-arg' in list(qapp.arguments()) """ ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*= 1 passed in *"]) def test_importerror(monkeypatch): def _fake_import(name, *args): raise ModuleNotFoundError(f"Failed to import {name}") monkeypatch.delenv("PYTEST_QT_API", raising=False) monkeypatch.setattr(qt_compat, "_import", _fake_import) expected = ( "pytest-qt requires either PySide2, PySide6, PyQt5 or PyQt6 installed.\n" " PyQt5.QtCore: Failed to import PyQt5.QtCore\n" " PyQt6.QtCore: Failed to import PyQt6.QtCore\n" " PySide2.QtCore: Failed to import PySide2.QtCore\n" " PySide6.QtCore: Failed to import PySide6.QtCore" ) with pytest.raises(pytest.UsageError, match=expected): qt_api.set_qt_api(api=None) def test_before_close_func(testdir): """ Test the `before_close_func` argument of qtbot.addWidget. """ import sys testdir.makepyfile( """ import sys import pytest from pytestqt.qt_compat import qt_api def widget_closed(w): assert w.some_id == 'my id' sys.pytest_qt_widget_closed = True @pytest.fixture def widget(qtbot): w = qt_api.QtWidgets.QWidget() w.some_id = 'my id' qtbot.add_widget(w, before_close_func=widget_closed) return w def test_foo(widget): pass """ ) result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines(["*= 1 passed in *"]) assert sys.pytest_qt_widget_closed def test_addwidget_typeerror(testdir, qtbot): """ Make sure addWidget catches type errors early. """ obj = qt_api.QtCore.QObject() with pytest.raises(TypeError): qtbot.addWidget(obj) pytest-qt-4.0.2/tests/test_exceptions.py0000644000175100001710000002521514061506270021230 0ustar runnerdocker00000000000000import sys import pytest from pytestqt.exceptions import capture_exceptions, format_captured_exceptions @pytest.mark.parametrize("raise_error", [False, True]) def test_catch_exceptions_in_virtual_methods(testdir, raise_error): """ Catch exceptions that happen inside Qt's event loop and make the tests fail if any. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api class Receiver(qt_api.QtCore.QObject): def event(self, ev): if {raise_error}: try: raise RuntimeError('original error') except RuntimeError: raise ValueError('mistakes were made') return qt_api.QtCore.QObject.event(self, ev) def test_exceptions(qtbot): v = Receiver() app = qt_api.QtWidgets.QApplication.instance() app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.Type.User)) app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.Type.User)) app.processEvents() """.format( raise_error=raise_error ) ) result = testdir.runpytest() if raise_error: expected_lines = ["*Exceptions caught in Qt event loop:*"] if sys.version_info.major == 3: expected_lines.append("RuntimeError: original error") expected_lines.extend(["*ValueError: mistakes were made*", "*1 failed*"]) result.stdout.fnmatch_lines(expected_lines) assert "pytest.fail" not in "\n".join(result.outlines) else: result.stdout.fnmatch_lines("*1 passed*") def test_format_captured_exceptions(): try: raise ValueError("errors were made") except ValueError: exceptions = [sys.exc_info()] obtained_text = format_captured_exceptions(exceptions) lines = obtained_text.splitlines() assert "Exceptions caught in Qt event loop:" in lines assert "ValueError: errors were made" in lines @pytest.mark.skipif(sys.version_info.major == 2, reason="Python 3 only") def test_format_captured_exceptions_chained(): try: try: raise ValueError("errors were made") except ValueError: raise RuntimeError("error handling value error") except RuntimeError: exceptions = [sys.exc_info()] obtained_text = format_captured_exceptions(exceptions) lines = obtained_text.splitlines() assert "Exceptions caught in Qt event loop:" in lines assert "ValueError: errors were made" in lines assert "RuntimeError: error handling value error" in lines @pytest.mark.parametrize("no_capture_by_marker", [True, False]) def test_no_capture(testdir, no_capture_by_marker): """ Make sure options that disable exception capture are working (either marker or ini configuration value). :type testdir: TmpTestdir """ if no_capture_by_marker: marker_code = "@pytest.mark.qt_no_exception_capture" else: marker_code = "" testdir.makeini( """ [pytest] qt_no_exception_capture = 1 """ ) testdir.makepyfile( """ import pytest import sys from pytestqt.qt_compat import qt_api # PyQt 5.5+ will crash if there's no custom exception handler installed sys.excepthook = lambda *args: None class MyWidget(qt_api.QtWidgets.QWidget): def mouseReleaseEvent(self, ev): raise RuntimeError {marker_code} def test_widget(qtbot): w = MyWidget() qtbot.addWidget(w) qtbot.mouseClick(w, qt_api.QtCore.Qt.MouseButton.LeftButton) """.format( marker_code=marker_code ) ) res = testdir.runpytest() res.stdout.fnmatch_lines(["*1 passed*"]) def test_no_capture_preserves_custom_excepthook(testdir): """ Capturing must leave custom excepthooks alone when disabled. :type testdir: TmpTestdir """ testdir.makepyfile( """ import pytest import sys from pytestqt.qt_compat import qt_api def custom_excepthook(*args): sys.__excepthook__(*args) sys.excepthook = custom_excepthook @pytest.mark.qt_no_exception_capture def test_no_capture(qtbot): assert sys.excepthook is custom_excepthook def test_capture(qtbot): assert sys.excepthook is not custom_excepthook """ ) res = testdir.runpytest() res.stdout.fnmatch_lines(["*2 passed*"]) def test_exception_capture_on_call(testdir): """ Exceptions should also be captured during test execution. :type testdir: TmpTestdir """ testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class MyWidget(qt_api.QtWidgets.QWidget): def event(self, ev): raise RuntimeError('event processed') def test_widget(qtbot, qapp): w = MyWidget() qapp.postEvent(w, qt_api.QtCore.QEvent(QEvent.Type.User)) qapp.processEvents() """ ) res = testdir.runpytest("-s") res.stdout.fnmatch_lines(["*RuntimeError('event processed')*", "*1 failed*"]) def test_exception_capture_on_widget_close(testdir): """ Exceptions should also be captured when widget is being closed. :type testdir: TmpTestdir """ testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class MyWidget(qt_api.QtWidgets.QWidget): def closeEvent(self, ev): raise RuntimeError('close error') def test_widget(qtbot, qapp): w = MyWidget() test_widget.w = w # keep it alive qtbot.addWidget(w) """ ) res = testdir.runpytest("-s") res.stdout.fnmatch_lines(["*RuntimeError('close error')*", "*1 error*"]) @pytest.mark.parametrize("mode", ["setup", "teardown"]) def test_exception_capture_on_fixture_setup_and_teardown(testdir, mode): """ Setup/teardown exception capturing as early/late as possible to catch all exceptions, even from other fixtures (#105). :type testdir: TmpTestdir """ if mode == "setup": setup_code = "send_event(w, qapp)" teardown_code = "" else: setup_code = "" teardown_code = "send_event(w, qapp)" testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class MyWidget(qt_api.QtWidgets.QWidget): def event(self, ev): if ev.type() == qt_api.QtCore.QEvent.Type.User: raise RuntimeError('event processed') return True @pytest.fixture def widget(qapp): w = MyWidget() {setup_code} yield w {teardown_code} def send_event(w, qapp): qapp.postEvent(w, qt_api.QtCore.QEvent( qt_api.QtCore.QEvent.Type.User)) qapp.processEvents() def test_capture(widget): pass """.format( setup_code=setup_code, teardown_code=teardown_code ) ) res = testdir.runpytest("-s") res.stdout.fnmatch_lines( [ "*__ ERROR at %s of test_capture __*" % mode, "*RuntimeError('event processed')*", "*1 error*", ] ) @pytest.mark.qt_no_exception_capture def test_capture_exceptions_context_manager(qapp): """Test capture_exceptions() context manager. While not used internally anymore, it is still part of the API and therefore should be properly tested. """ from pytestqt.qt_compat import qt_api from pytestqt.exceptions import capture_exceptions class Receiver(qt_api.QtCore.QObject): def event(self, ev): raise ValueError("mistakes were made") r = Receiver() with capture_exceptions() as exceptions: qapp.sendEvent(r, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.Type.User)) qapp.processEvents() assert [str(e) for (t, e, tb) in exceptions] == ["mistakes were made"] def test_capture_exceptions_qtbot_context_manager(testdir): """Test capturing exceptions in a block by using `capture_exceptions` method provided by `qtbot`. """ testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class MyWidget(qt_api.QtWidgets.QWidget): on_event = qt_api.Signal() def test_widget(qtbot): widget = MyWidget() qtbot.addWidget(widget) def raise_on_event(): raise RuntimeError("error") widget.on_event.connect(raise_on_event) with qtbot.capture_exceptions() as exceptions: widget.on_event.emit() assert len(exceptions) == 1 assert str(exceptions[0][1]) == "error" """ ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) def test_exceptions_to_stderr(qapp, capsys): """ Exceptions should still be reported to stderr. """ called = [] from pytestqt.qt_compat import qt_api class MyWidget(qt_api.QtWidgets.QWidget): def event(self, ev): called.append(1) raise RuntimeError("event processed") w = MyWidget() with capture_exceptions() as exceptions: qapp.postEvent(w, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.Type.User)) qapp.processEvents() assert called del exceptions[:] _out, err = capsys.readouterr() assert 'raise RuntimeError("event processed")' in err @pytest.mark.xfail( condition=sys.version_info[:2] == (3, 4), reason="failing in Python 3.4, which is about to be dropped soon anyway", ) def test_exceptions_dont_leak(testdir): """ Ensure exceptions are cleared when an exception occurs and don't leak (#187). """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import gc import weakref class MyWidget(qt_api.QtWidgets.QWidget): def event(self, ev): called.append(1) raise RuntimeError('event processed') weak_ref = None called = [] def test_1(qapp): global weak_ref w = MyWidget() weak_ref = weakref.ref(w) qapp.postEvent(w, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.Type.User)) qapp.processEvents() def test_2(qapp): assert called gc.collect() assert weak_ref() is None """ ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 failed, 1 passed*"]) pytest-qt-4.0.2/tests/test_logging.py0000644000175100001710000003713014061506270020474 0ustar runnerdocker00000000000000import datetime import pytest from pytestqt.qt_compat import qt_api @pytest.mark.parametrize("test_succeeds", [True, False]) @pytest.mark.parametrize("qt_log", [True, False]) def test_basic_logging(testdir, test_succeeds, qt_log): """ Test Qt logging capture output. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ import sys from pytestqt.qt_compat import qt_api def to_unicode(s): return s.decode('utf-8', 'replace') if isinstance(s, bytes) else s def print_msg(msg_type, context, message): sys.stderr.write(to_unicode(message) + '\\n') qt_api.QtCore.qInstallMessageHandler(print_msg) def test_types(): # qInfo is not exposed by the bindings yet (#225) # qt_api.qInfo('this is an INFO message') qt_api.qDebug('this is a DEBUG message') qt_api.qWarning('this is a WARNING message') qt_api.qCritical('this is a CRITICAL message') assert {} """.format( test_succeeds ) ) res = testdir.runpytest(*(["--no-qt-log"] if not qt_log else [])) if test_succeeds: assert "Captured Qt messages" not in res.stdout.str() assert "Captured stderr call" not in res.stdout.str() else: if qt_log: res.stdout.fnmatch_lines( [ "*-- Captured Qt messages --*", # qInfo is not exposed by the bindings yet (#232) # '*QtInfoMsg: this is an INFO message*', "*QtDebugMsg: this is a DEBUG message*", "*QtWarningMsg: this is a WARNING message*", "*QtCriticalMsg: this is a CRITICAL message*", ] ) else: res.stdout.fnmatch_lines( [ "*-- Captured stderr call --*", # qInfo is not exposed by the bindings yet (#232) # '*QtInfoMsg: this is an INFO message*', # 'this is an INFO message*', "this is a DEBUG message*", "this is a WARNING message*", "this is a CRITICAL message*", ] ) def test_qinfo(qtlog): """Test INFO messages when we have means to do so. Should be temporary until bindings catch up and expose qInfo (or at least QMessageLogger), then we should update the other logging tests properly. #232 """ if qt_api.is_pyside: assert ( qt_api.qInfo is None ), "pyside2/6 does not expose qInfo. If it does, update this test." return qt_api.qInfo("this is an INFO message") records = [(m.type, m.message.strip()) for m in qtlog.records] assert records == [(qt_api.QtCore.QtMsgType.QtInfoMsg, "this is an INFO message")] def test_qtlog_fixture(qtlog): """ Test qtlog fixture. """ # qInfo is not exposed by the bindings yet (#232) qt_api.qDebug("this is a DEBUG message") qt_api.qWarning("this is a WARNING message") qt_api.qCritical("this is a CRITICAL message") records = [(m.type, m.message.strip()) for m in qtlog.records] assert records == [ (qt_api.QtCore.QtMsgType.QtDebugMsg, "this is a DEBUG message"), (qt_api.QtCore.QtMsgType.QtWarningMsg, "this is a WARNING message"), (qt_api.QtCore.QtMsgType.QtCriticalMsg, "this is a CRITICAL message"), ] # `records` attribute is read-only with pytest.raises(AttributeError): qtlog.records = [] @pytest.mark.parametrize("arg", ["--no-qt-log", "--capture=no", "-s"]) def test_fixture_with_logging_disabled(testdir, arg): """ Test that qtlog fixture doesn't capture anything if logging is disabled in the command line. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_types(qtlog): qt_api.qWarning('message') assert qtlog.records == [] """ ) res = testdir.runpytest(arg) res.stdout.fnmatch_lines("*1 passed*") @pytest.mark.parametrize("use_context_manager", [True, False]) def test_disable_qtlog_context_manager(testdir, use_context_manager): """ Test qtlog.disabled() context manager. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = CRITICAL """ ) if use_context_manager: code = "with qtlog.disabled():" else: code = "if 1:" testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_1(qtlog): {code} qt_api.qCritical('message') """.format( code=code ) ) res = testdir.inline_run() passed = 1 if use_context_manager else 0 res.assertoutcome(passed=passed, failed=int(not passed)) @pytest.mark.parametrize("use_mark", [True, False]) def test_disable_qtlog_mark(testdir, use_mark): """ Test mark which disables logging capture for a test. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = CRITICAL """ ) mark = "@pytest.mark.no_qt_log" if use_mark else "" testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import pytest {mark} def test_1(): qt_api.qCritical('message') """.format( mark=mark ) ) res = testdir.inline_run() passed = 1 if use_mark else 0 res.assertoutcome(passed=passed, failed=int(not passed)) def test_logging_formatting(testdir): """ Test custom formatting for logging messages. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_types(): qt_api.qWarning('this is a WARNING message') assert 0 """ ) f = "{rec.type_name} {rec.log_type_name} {rec.when:%Y-%m-%d}: {rec.message}" res = testdir.runpytest(f"--qt-log-format={f}") today = "{:%Y-%m-%d}".format(datetime.datetime.now()) res.stdout.fnmatch_lines( [ "*-- Captured Qt messages --*", f"QtWarningMsg WARNING {today}: this is a WARNING message*", ] ) @pytest.mark.parametrize( "level, expect_passes", [("DEBUG", 1), ("WARNING", 2), ("CRITICAL", 3), ("NO", 4)] ) def test_logging_fails_tests(testdir, level, expect_passes): """ Test qt_log_level_fail ini option. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = {level} """.format( level=level ) ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_1(): qt_api.qDebug('this is a DEBUG message') def test_2(): qt_api.qWarning('this is a WARNING message') def test_3(): qt_api.qCritical('this is a CRITICAL message') def test_4(): assert 1 """ ) res = testdir.runpytest() lines = [] if level != "NO": lines.extend( [ "*Failure: Qt messages with level {} or above emitted*".format( level.upper() ), "*-- Captured Qt messages --*", ] ) lines.append(f"*{expect_passes} passed*") res.stdout.fnmatch_lines(lines) def test_logging_fails_tests_mark(testdir): """ Test mark overrides what's configured in the ini file. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = CRITICAL """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qWarning import pytest @pytest.mark.qt_log_level_fail('WARNING') def test_1(): qWarning('message') """ ) res = testdir.inline_run() res.assertoutcome(failed=1) def test_logging_fails_ignore(testdir): """ Test qt_log_ignore config option. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = CRITICAL qt_log_ignore = WM_DESTROY.*sent WM_PAINT not handled """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import pytest def test1(): qt_api.qCritical('a critical message') def test2(): qt_api.qCritical('WM_DESTROY was sent') def test3(): qt_api.qCritical('WM_DESTROY was sent') assert 0 def test4(): qt_api.qCritical('WM_PAINT not handled') qt_api.qCritical('another critical message') """ ) res = testdir.runpytest() lines = [ # test1 fails because it has emitted a CRITICAL message and that message # does not match any regex in qt_log_ignore "*_ test1 _*", "*Failure: Qt messages with level CRITICAL or above emitted*", "*QtCriticalMsg: a critical message*", # test2 succeeds because its message matches qt_log_ignore # test3 fails because of an assert, but the ignored message should # still appear in the failure message "*_ test3 _*", "*AssertionError*", "*QtCriticalMsg: WM_DESTROY was sent*(IGNORED)*", # test4 fails because one message is ignored but the other isn't "*_ test4 _*", "*Failure: Qt messages with level CRITICAL or above emitted*", "*QtCriticalMsg: WM_PAINT not handled*(IGNORED)*", "*QtCriticalMsg: another critical message*", # summary "*3 failed, 1 passed*", ] res.stdout.fnmatch_lines(lines) @pytest.mark.parametrize("message", ["match-global", "match-mark"]) @pytest.mark.parametrize("marker_args", ["'match-mark', extend=True", "'match-mark'"]) def test_logging_mark_with_extend(testdir, message, marker_args): """ Test qt_log_ignore mark with extend=True. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = CRITICAL qt_log_ignore = match-global """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import pytest @pytest.mark.qt_log_ignore({marker_args}) def test1(): qt_api.qCritical('{message}') """.format( message=message, marker_args=marker_args ) ) res = testdir.inline_run() res.assertoutcome(passed=1, failed=0) @pytest.mark.parametrize( "message, error_expected", [("match-global", True), ("match-mark", False)] ) def test_logging_mark_without_extend(testdir, message, error_expected): """ Test qt_log_ignore mark with extend=False. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = CRITICAL qt_log_ignore = match-global """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import pytest @pytest.mark.qt_log_ignore('match-mark', extend=False) def test1(): qt_api.qCritical('{message}') """.format( message=message ) ) res = testdir.inline_run() if error_expected: res.assertoutcome(passed=0, failed=1) else: res.assertoutcome(passed=1, failed=0) def test_logging_mark_with_invalid_argument(testdir): """ Test qt_log_ignore mark with invalid keyword argument. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ import pytest @pytest.mark.qt_log_ignore('match-mark', does_not_exist=True) def test1(): pass """ ) res = testdir.runpytest() lines = [ "*= ERRORS =*", "*_ ERROR at setup of test1 _*", "*ValueError: Invalid keyword arguments in {'does_not_exist': True} " "for qt_log_ignore mark.", # summary "*= 1 error in*", ] res.stdout.fnmatch_lines(lines) @pytest.mark.parametrize("apply_mark", [True, False]) def test_logging_fails_ignore_mark_multiple(testdir, apply_mark): """ Make sure qt_log_ignore mark supports multiple arguments. :type testdir: _pytest.pytester.TmpTestdir """ if apply_mark: mark = '@pytest.mark.qt_log_ignore("WM_DESTROY", "WM_PAINT")' else: mark = "" testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import pytest @pytest.mark.qt_log_level_fail('CRITICAL') {mark} def test1(): qt_api.qCritical('WM_PAINT was sent') """.format( mark=mark ) ) res = testdir.inline_run() passed = 1 if apply_mark else 0 res.assertoutcome(passed=passed, failed=int(not passed)) def test_lineno_failure(testdir): """ Test that tests when failing because log messages were emitted report the correct line number. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makeini( """ [pytest] qt_log_level_fail = WARNING """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_foo(): assert foo() == 10 def foo(): qt_api.qWarning('this is a WARNING message') return 10 """ ) res = testdir.runpytest() if qt_api.is_pyqt: res.stdout.fnmatch_lines( [ "*test_lineno_failure.py:2: Failure*", "*test_lineno_failure.py:foo:5:*", " QtWarningMsg: this is a WARNING message", ] ) else: res.stdout.fnmatch_lines( [ "*test_lineno_failure.py:2: Failure*", "QtWarningMsg: this is a WARNING message", ] ) def test_context_none(testdir): """ Sometimes PyQt will emit a context with some/all attributes set as None instead of appropriate file, function and line number. Test that when this happens the plugin doesn't break, and it filters out the context information. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_foo(request): log_capture = request.node.qt_log_capture context = log_capture._Context(None, None, 0, None) log_capture._handle_with_context(qt_api.QtCore.QtMsgType.QtWarningMsg, context, "WARNING message") assert 0 """ ) res = testdir.runpytest() assert "*None:None:0:*" not in str(res.stdout) res.stdout.fnmatch_lines(["QtWarningMsg: WARNING message"]) def test_logging_broken_makereport(testdir): """ Make sure logging's makereport hookwrapper doesn't hide exceptions. See https://github.com/pytest-dev/pytest-qt/issues/98 :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( conftest=""" import pytest @pytest.mark.hookwrapper(tryfirst=True) def pytest_runtest_makereport(call): if call.when == 'call': raise Exception("This should not be hidden") yield """ ) p = testdir.makepyfile( """ def test_foo(): pass """ ) res = testdir.runpytest_subprocess(p) res.stdout.fnmatch_lines(["*This should not be hidden*"]) pytest-qt-4.0.2/tests/test_modeltest.py0000644000175100001710000002612614061506270021051 0ustar runnerdocker00000000000000import pytest from pytestqt.qt_compat import qt_api from pytestqt import modeltest pytestmark = pytest.mark.usefixtures("qtbot") class BasicModel(qt_api.QtCore.QAbstractItemModel): def data(self, index, role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole): return None def rowCount(self, parent=qt_api.QtCore.QModelIndex()): return 0 def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return 0 def index(self, row, column, parent=qt_api.QtCore.QModelIndex()): return qt_api.QtCore.QModelIndex() def parent(self, index): return qt_api.QtCore.QModelIndex() def test_standard_item_model(qtmodeltester): """ Basic test which uses qtmodeltester with a qt_api.QtGui.QStandardItemModel. """ model = qt_api.QtGui.QStandardItemModel() items = [qt_api.QtGui.QStandardItem(str(i)) for i in range(6)] model.setItem(0, 0, items[0]) model.setItem(0, 1, items[1]) model.setItem(1, 0, items[2]) model.setItem(1, 1, items[3]) items[0].setChild(0, items[4]) items[4].setChild(0, items[5]) qtmodeltester.check(model, force_py=True) def test_string_list_model(qtmodeltester): model = qt_api.QtCore.QStringListModel() model.setStringList(["hello", "world"]) qtmodeltester.check(model, force_py=True) def test_sort_filter_proxy_model(qtmodeltester): model = qt_api.QtCore.QStringListModel() model.setStringList(["hello", "world"]) proxy = qt_api.QtCore.QSortFilterProxyModel() proxy.setSourceModel(model) qtmodeltester.check(proxy, force_py=True) @pytest.mark.parametrize( "broken_role", [ qt_api.QtCore.Qt.ItemDataRole.ToolTipRole, qt_api.QtCore.Qt.ItemDataRole.StatusTipRole, qt_api.QtCore.Qt.ItemDataRole.WhatsThisRole, qt_api.QtCore.Qt.ItemDataRole.SizeHintRole, qt_api.QtCore.Qt.ItemDataRole.FontRole, qt_api.QtCore.Qt.ItemDataRole.BackgroundRole, qt_api.QtCore.Qt.ItemDataRole.ForegroundRole, qt_api.QtCore.Qt.ItemDataRole.TextAlignmentRole, qt_api.QtCore.Qt.ItemDataRole.CheckStateRole, ], ) def test_broken_types(check_model, broken_role): """ Check that qtmodeltester correctly captures data() returning invalid values for various display roles. """ class BrokenTypeModel(qt_api.QtCore.QAbstractListModel): def rowCount(self, parent=qt_api.QtCore.QModelIndex()): if parent == qt_api.QtCore.QModelIndex(): return 1 else: return 0 def data( self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole, ): if role == broken_role: return object() # This will fail the type check for any role else: return None check_model(BrokenTypeModel(), should_pass=False) @pytest.mark.parametrize( "role_value, should_pass", [ (qt_api.QtCore.Qt.AlignmentFlag.AlignLeft, True), (qt_api.QtCore.Qt.AlignmentFlag.AlignRight, True), (0xFFFFFF, False), ("foo", False), (object(), False), ], ) def test_data_alignment(role_value, should_pass, check_model): """Test a custom model which returns a good and alignments from data(). qtmodeltest should capture this problem and fail when that happens. """ class MyModel(qt_api.QtCore.QAbstractListModel): def rowCount(self, parent=qt_api.QtCore.QModelIndex()): return 1 if parent == qt_api.QtCore.QModelIndex() else 0 def data( self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole, ): if role == qt_api.QtCore.Qt.ItemDataRole.TextAlignmentRole: return role_value elif role == qt_api.QtCore.Qt.ItemDataRole.DisplayRole: if index == self.index(0, 0): return "Hello" return None check_model(MyModel(), should_pass=should_pass) def test_header_handling(check_model): class MyModel(qt_api.QtCore.QAbstractListModel): def rowCount(self, parent=qt_api.QtCore.QModelIndex()): return 1 if parent == qt_api.QtCore.QModelIndex() else 0 def set_header_text(self, header): self._header_text = header self.headerDataChanged.emit(qt_api.QtCore.Qt.Orientation.Vertical, 0, 0) self.headerDataChanged.emit(qt_api.QtCore.Qt.Orientation.Horizontal, 0, 0) def headerData( self, section, orientation, role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole ): return self._header_text def data( self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole, ): if ( role == qt_api.QtCore.Qt.ItemDataRole.DisplayRole and index == self.index(0, 0) ): return "Contents" return None model = MyModel() model.set_header_text("Start Header") check_model(model, should_pass=True) model.set_header_text("New Header") @pytest.fixture def check_model(qtmodeltester): """ Return a check_model(model, should_pass=True) function that uses qtmodeltester to check if the model is OK or not according to the ``should_pass`` parameter. """ def check(model, should_pass=True): if should_pass: qtmodeltester.check(model, force_py=True) else: with pytest.raises(AssertionError): qtmodeltester.check(model, force_py=True) return check def test_invalid_column_count(qtmodeltester): """Basic check with an invalid model.""" class Model(BasicModel): def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return -1 model = Model() with pytest.raises(AssertionError): qtmodeltester.check(model, force_py=True) def test_changing_model_insert(qtmodeltester): model = qt_api.QtGui.QStandardItemModel() item = qt_api.QtGui.QStandardItem("foo") qtmodeltester.check(model, force_py=True) model.insertRow(0, item) def test_changing_model_remove(qtmodeltester): model = qt_api.QtGui.QStandardItemModel() item = qt_api.QtGui.QStandardItem("foo") model.setItem(0, 0, item) qtmodeltester.check(model, force_py=True) model.removeRow(0) def test_changing_model_data(qtmodeltester): model = qt_api.QtGui.QStandardItemModel() item = qt_api.QtGui.QStandardItem("foo") model.setItem(0, 0, item) qtmodeltester.check(model, force_py=True) model.setData(model.index(0, 0), "hello world") @pytest.mark.parametrize( "orientation", [qt_api.QtCore.Qt.Orientation.Horizontal, qt_api.QtCore.Qt.Orientation.Vertical], ) def test_changing_model_header_data(qtmodeltester, orientation): model = qt_api.QtGui.QStandardItemModel() item = qt_api.QtGui.QStandardItem("foo") model.setItem(0, 0, item) qtmodeltester.check(model, force_py=True) model.setHeaderData(0, orientation, "blah") def test_changing_model_sort(qtmodeltester): """Sorting emits layoutChanged""" model = qt_api.QtGui.QStandardItemModel() item = qt_api.QtGui.QStandardItem("foo") model.setItem(0, 0, item) qtmodeltester.check(model, force_py=True) model.sort(0) def test_nop(qtmodeltester): """We should not get a crash on cleanup with no model.""" pass def test_overridden_methods(qtmodeltester): """Make sure overriden methods of a model are actually run. With a previous implementation of the modeltester using sip.cast, the custom implementations did never actually run. """ class Model(BasicModel): def __init__(self, parent=None): super().__init__(parent) self.row_count_did_run = False def rowCount(self, parent=None): self.row_count_did_run = True return 0 model = Model() assert not model.row_count_did_run qtmodeltester.check(model, force_py=True) assert model.row_count_did_run def test_fetch_more(qtmodeltester): class Model(qt_api.QtGui.QStandardItemModel): def canFetchMore(self, parent): return True def fetchMore(self, parent): """Force a re-check while fetching more.""" self.setData(self.index(0, 0), "bar") model = Model() item = qt_api.QtGui.QStandardItem("foo") model.setItem(0, 0, item) qtmodeltester.check(model, force_py=True) def test_invalid_parent(qtmodeltester): class Model(qt_api.QtGui.QStandardItemModel): def parent(self, index): if index == self.index(0, 0, parent=self.index(0, 0)): return self.index(0, 0) else: return qt_api.QtCore.QModelIndex() model = Model() item = qt_api.QtGui.QStandardItem("foo") item2 = qt_api.QtGui.QStandardItem("bar") item3 = qt_api.QtGui.QStandardItem("bar") model.setItem(0, 0, item) item.setChild(0, item2) item2.setChild(0, item3) with pytest.raises(AssertionError): qtmodeltester.check(model, force_py=True) @pytest.mark.skipif(not modeltest.HAS_QT_TESTER, reason="No Qt modeltester available") def test_qt_tester_valid(testdir): testdir.makepyfile( """ from pytestqt.qt_compat import qt_api from pytestqt import modeltest assert modeltest.HAS_QT_TESTER def test_ok(qtmodeltester): model = qt_api.QtGui.QStandardItemModel() qtmodeltester.check(model) """ ) res = testdir.inline_run() res.assertoutcome(passed=1, failed=0) @pytest.mark.skipif(not modeltest.HAS_QT_TESTER, reason="No Qt modeltester available") def test_qt_tester_invalid(testdir): testdir.makeini( """ [pytest] qt_log_level_fail = NO """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api from pytestqt import modeltest assert modeltest.HAS_QT_TESTER class Model(qt_api.QtCore.QAbstractItemModel): def data(self, index, role=qt_api.QtCore.Qt.ItemDataRole.DisplayRole): return None def rowCount(self, parent=qt_api.QtCore.QModelIndex()): return 0 def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return -1 def index(self, row, column, parent=qt_api.QtCore.QModelIndex()): return qt_api.QtCore.QModelIndex() def parent(self, index): return qt_api.QtCore.QModelIndex() def test_ok(qtmodeltester): model = Model() qtmodeltester.check(model) """ ) res = testdir.runpytest() res.stdout.fnmatch_lines( [ "*__ test_ok __*", "test_qt_tester_invalid.py:*: Qt modeltester errors", "*-- Captured Qt messages --*", "*QtWarningMsg: FAIL! model->columnCount(QModelIndex()) >= 0 () returned FALSE " "(*qabstractitemmodeltester.cpp:*)", "*-- Captured stdout call --*", "modeltest: Using Qt C++ tester", "*== 1 failed in * ==*", ] ) pytest-qt-4.0.2/tests/test_qtest_proxies.py0000644000175100001710000000254514061506270021761 0ustar runnerdocker00000000000000import pytest from pytestqt.qt_compat import qt_api @pytest.mark.parametrize( "expected_method", [ "keyPress", "keyClick", "keyClicks", "keyEvent", "keyPress", "keyRelease", "keyToAscii", "keySequence", "mouseClick", "mouseDClick", "mouseMove", "mousePress", "mouseRelease", ], ) def test_expected_qtest_proxies(qtbot, expected_method): """ This test originates from the implementation where QTest API methods were exported on runtime. """ assert hasattr(qtbot, expected_method) assert getattr(qtbot, expected_method).__name__ == expected_method @pytest.mark.skipif(qt_api.is_pyside, reason="PyQt test only") def test_keyToAscii_not_available_on_pyqt(testdir): """ Test that qtbot.keyToAscii() is not available on PyQt5 and calling the method raises a NotImplementedError. """ testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api def test_foo(qtbot): widget = qt_api.QtWidgets.QWidget() qtbot.add_widget(widget) with pytest.raises(NotImplementedError): qtbot.keyToAscii(qt_api.QtCore.Qt.Key.Key_Escape) """ ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*= 1 passed in *"]) pytest-qt-4.0.2/tests/test_wait_signal.py0000644000175100001710000013637714061506270021364 0ustar runnerdocker00000000000000import functools import fnmatch import pytest import sys from pytestqt.qt_compat import qt_api from pytestqt.wait_signal import ( SignalEmittedError, TimeoutError, SignalAndArgs, CallbackCalledTwiceError, ) flaky_on_macos = pytest.mark.xfail( sys.platform == "darwin", run=False, reason="Flaky on macOS: #313" ) @pytest.mark.parametrize( "method, signal, timeout", [ ("waitSignal", None, None), ("waitSignal", None, 1000), ("waitSignals", [], None), ("waitSignals", [], 1000), ("waitSignals", None, None), ("waitSignals", None, 1000), ], ) def test_signal_blocker_none(qtbot, method, signal, timeout): """ Make sure waitSignal without signals isn't supported anymore. """ meth = getattr(qtbot, method) with pytest.raises(ValueError): meth(signal, timeout=timeout).wait() def explicit_wait(qtbot, signal, timeout, multiple, raising, should_raise): """ Explicit wait for the signal using blocker API. """ func = qtbot.waitSignals if multiple else qtbot.waitSignal blocker = func(signal, timeout=timeout, raising=raising) assert not blocker.signal_triggered if should_raise: with pytest.raises(qtbot.TimeoutError): blocker.wait() else: blocker.wait() return blocker def context_manager_wait(qtbot, signal, timeout, multiple, raising, should_raise): """ Waiting for signal using context manager API. """ func = qtbot.waitSignals if multiple else qtbot.waitSignal if should_raise: with pytest.raises(qtbot.TimeoutError): with func(signal, timeout=timeout, raising=raising) as blocker: pass else: with func(signal, timeout=timeout, raising=raising) as blocker: pass return blocker def build_signal_tests_variants(params): """ Helper function to use with pytest's parametrize, to generate additional combinations of parameters in a parametrize call: - explicit wait and context-manager wait - raising True and False (since we check for the correct behavior inside each test). """ result = [] for param in params: for wait_function in (explicit_wait, context_manager_wait): for raising in (True, False): result.append(param + (wait_function, raising)) return result @pytest.mark.parametrize( ("delay", "timeout", "expected_signal_triggered", "wait_function", "raising"), build_signal_tests_variants( [ # delay, timeout, expected_signal_triggered (200, None, True), (200, 400, True), (400, 200, False), ] ), ) @flaky_on_macos def test_signal_triggered( qtbot, timer, stop_watch, wait_function, delay, timeout, expected_signal_triggered, raising, signaller, ): """ Testing for a signal in different conditions, ensuring we are obtaining the expected results. """ timer.single_shot(signaller.signal, delay) should_raise = raising and not expected_signal_triggered stop_watch.start() blocker = wait_function( qtbot, signaller.signal, timeout=timeout, raising=raising, should_raise=should_raise, multiple=False, ) # ensure that either signal was triggered or timeout occurred assert blocker.signal_triggered == expected_signal_triggered stop_watch.check(timeout, delay) @pytest.mark.parametrize("delayed", [True, False]) def test_zero_timeout(qtbot, timer, delayed, signaller): """ With a zero timeout, we don't run a main loop, so only immediate signals are processed. """ with qtbot.waitSignal(signaller.signal, raising=False, timeout=0) as blocker: if delayed: timer.single_shot(signaller.signal, 0) else: signaller.signal.emit() assert blocker.signal_triggered != delayed @pytest.mark.parametrize( "configval, raises", [("false", False), ("true", True), (None, True)] ) def test_raising(qtbot, testdir, configval, raises): if configval is not None: testdir.makeini( f""" [pytest] qt_default_raising = {configval} """ ) testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class Signaller(qt_api.QtCore.QObject): signal = qt_api.Signal() def test_foo(qtbot): signaller = Signaller() with qtbot.waitSignal(signaller.signal, timeout=10): pass """ ) res = testdir.runpytest() if raises: res.stdout.fnmatch_lines(["*1 failed*"]) else: res.stdout.fnmatch_lines(["*1 passed*"]) def test_raising_by_default_overridden(qtbot, testdir): testdir.makeini( """ [pytest] qt_default_raising = false """ ) testdir.makepyfile( """ import pytest from pytestqt.qt_compat import qt_api class Signaller(qt_api.QtCore.QObject): signal = qt_api.Signal() def test_foo(qtbot): signaller = Signaller() signal = signaller.signal with qtbot.waitSignal(signal, raising=True, timeout=10) as blocker: pass """ ) res = testdir.runpytest() res.stdout.fnmatch_lines(["*1 failed*"]) @pytest.mark.parametrize( ( "delay_1", "delay_2", "timeout", "expected_signal_triggered", "wait_function", "raising", ), build_signal_tests_variants( [ # delay1, delay2, timeout, expected_signal_triggered (200, 300, 400, True), (300, 200, 400, True), (200, 300, None, True), (400, 400, 200, False), (200, 400, 300, False), (400, 200, 200, False), (200, 1000, 400, False), ] ), ) @flaky_on_macos def test_signal_triggered_multiple( qtbot, timer, stop_watch, wait_function, delay_1, delay_2, timeout, signaller, expected_signal_triggered, raising, ): """ Testing for a signal in different conditions, ensuring we are obtaining the expected results. """ timer.single_shot(signaller.signal, delay_1) timer.single_shot(signaller.signal_2, delay_2) should_raise = raising and not expected_signal_triggered stop_watch.start() blocker = wait_function( qtbot, [signaller.signal, signaller.signal_2], timeout=timeout, multiple=True, raising=raising, should_raise=should_raise, ) # ensure that either signal was triggered or timeout occurred assert blocker.signal_triggered == expected_signal_triggered stop_watch.check(timeout, delay_1, delay_2) def test_explicit_emit(qtbot, signaller): """ Make sure an explicit emit() inside a waitSignal block works. """ with qtbot.waitSignal(signaller.signal, timeout=5000) as waiting: signaller.signal.emit() assert waiting.signal_triggered def test_explicit_emit_multiple(qtbot, signaller): """ Make sure an explicit emit() inside a waitSignal block works. """ with qtbot.waitSignals( [signaller.signal, signaller.signal_2], timeout=5000 ) as waiting: signaller.signal.emit() signaller.signal_2.emit() assert waiting.signal_triggered @pytest.fixture def signaller(timer): """ Fixture that provides an object with to signals that can be emitted by tests. .. note:: we depend on "timer" fixture to ensure that signals emitted with "timer" are disconnected before the Signaller() object is destroyed. This was the reason for some random crashes experienced on Windows (#80). """ class Signaller(qt_api.QtCore.QObject): signal = qt_api.Signal() signal_2 = qt_api.Signal() signal_args = qt_api.Signal(str, int) signal_args_2 = qt_api.Signal(str, int) signal_single_arg = qt_api.Signal(int) assert timer return Signaller() @pytest.mark.parametrize("blocker", ["single", "multiple", "callback"]) @pytest.mark.parametrize("raising", [True, False]) def test_blockers_handle_exceptions(qtbot, blocker, raising, signaller): """ Make sure blockers handle exceptions correctly. """ class TestException(Exception): pass if blocker == "multiple": func = qtbot.waitSignals args = [[signaller.signal, signaller.signal_2]] elif blocker == "single": func = qtbot.waitSignal args = [signaller.signal] elif blocker == "callback": func = qtbot.waitCallback args = [] else: assert False with pytest.raises(TestException): with func(*args, timeout=10, raising=raising): raise TestException @pytest.mark.parametrize("multiple", [True, False]) @pytest.mark.parametrize("do_timeout", [True, False]) def test_wait_twice(qtbot, timer, multiple, do_timeout, signaller): """ https://github.com/pytest-dev/pytest-qt/issues/69 """ if multiple: func = qtbot.waitSignals arg = [signaller.signal] else: func = qtbot.waitSignal arg = signaller.signal if do_timeout: with func(arg, timeout=100, raising=False): timer.single_shot(signaller.signal, 200) with func(arg, timeout=100, raising=False): timer.single_shot(signaller.signal, 200) else: with func(arg): signaller.signal.emit() with func(arg): signaller.signal.emit() def test_wait_signals_invalid_strict_parameter(qtbot, signaller): with pytest.raises(ValueError): qtbot.waitSignals([signaller.signal], order="invalid") def test_destroyed(qtbot): """Test that waitSignal works with the destroyed signal (#82).""" class Obj(qt_api.QtCore.QObject): pass obj = Obj() with qtbot.waitSignal(obj.destroyed): obj.deleteLater() with pytest.raises(RuntimeError): obj.objectName() class TestArgs: """Try to get the signal arguments from the signal blocker.""" def test_simple(self, qtbot, signaller): """The blocker should store the signal args in an 'args' attribute.""" with qtbot.waitSignal(signaller.signal_args) as blocker: signaller.signal_args.emit("test", 123) assert blocker.args == ["test", 123] def test_timeout(self, qtbot, signaller): """If there's a timeout, the args attribute is None.""" with qtbot.waitSignal(signaller.signal, timeout=100, raising=False) as blocker: pass assert blocker.args is None def test_without_args(self, qtbot, signaller): """If a signal has no args, the args attribute is an empty list.""" with qtbot.waitSignal(signaller.signal) as blocker: signaller.signal.emit() assert blocker.args == [] def test_multi(self, qtbot, signaller): """A MultiSignalBlocker doesn't have an args attribute.""" with qtbot.waitSignals([signaller.signal]) as blocker: signaller.signal.emit() with pytest.raises(AttributeError): blocker.args def test_connected_signal(self, qtbot, signaller): """A second signal connected via .connect also works.""" with qtbot.waitSignal(signaller.signal_args) as blocker: blocker.connect(signaller.signal_args_2) signaller.signal_args_2.emit("foo", 2342) assert blocker.args == ["foo", 2342] def test_signal_identity(signaller): """ Tests that the identity of signals can be determined correctly, using str(signal). PyQt5 has the following issue: x = signaller.signal y = signaller.signal x == y # is False id(signaller.signal) == id(signaller.signal) # only True because of garbage collection between first and second id() call id(x) == id(y) # is False str(x) == str(y) # is True (for all Qt frameworks) """ assert str(signaller.signal) == str(signaller.signal) x = signaller.signal y = signaller.signal assert str(x) == str(y) # different signals should also be recognized as different ones z = signaller.signal_2 assert str(x) != str(z) def test_invalid_signal(qtbot): """Tests that a TypeError is raised when providing a signal object that actually is not a Qt signal at all.""" class NotReallyASignal: def __init__(self): self.signal = False with pytest.raises(TypeError): with qtbot.waitSignal(signal=NotReallyASignal(), raising=False): pass def test_invalid_signal_tuple_length(qtbot, signaller): """ Test that a ValueError is raised when not providing a signal+name tuple with exactly 2 elements as signal parameter. """ with pytest.raises(ValueError): signal_tuple_with_invalid_length = ( signaller.signal, "signal()", "does not belong here", ) with qtbot.waitSignal(signal=signal_tuple_with_invalid_length, raising=False): pass def test_provided_empty_signal_name(qtbot, signaller): """Test that a ValueError is raised when providing a signal+name tuple where the name is an empty string.""" with pytest.raises(ValueError): invalid_signal_tuple = (signaller.signal, "") with qtbot.waitSignal(signal=invalid_signal_tuple, raising=False): pass def test_provided_invalid_signal_name_type(qtbot, signaller): """Test that a TypeError is raised when providing a signal+name tuple where the name is not actually string.""" with pytest.raises(TypeError): invalid_signal_tuple = (signaller.signal, 12345) # 12345 is not a signal name with qtbot.waitSignal(signal=invalid_signal_tuple, raising=False): pass def test_signalandargs_equality(): signal_args1 = SignalAndArgs(signal_name="signal", args=(1, 2)) signal_args2 = SignalAndArgs(signal_name="signal", args=(1, 2)) assert signal_args1 == signal_args2 def test_signalandargs_inequality(): signal_args1_1 = SignalAndArgs(signal_name="signal", args=(1, 2)) signal_args1_2 = "foo" assert signal_args1_1 != signal_args1_2 def get_waitsignals_cases_all(order): """ Returns the list of tuples (emitted-signal-list, expected-signal-list, expect_signal_triggered) for the given order parameter of waitSignals(). """ cases = get_waitsignals_cases(order, working=True) cases.extend(get_waitsignals_cases(order, working=False)) return cases def get_waitsignals_cases(order, working): """ Builds combinations for signals to be emitted and expected for working cases (i.e. blocker.signal_triggered == True) and non-working cases, depending on the order. Note: The order ("none", "simple", "strict") becomes stricter from left to right. Working cases of stricter cases also work in less stricter cases. Non-working cases in less stricter cases also are non-working in stricter cases. """ if order == "none": if working: cases = get_waitsignals_cases(order="simple", working=True) cases.extend( [ # allow even out-of-order signals (("A1", "A2"), ("A2", "A1"), True), (("A1", "A2"), ("A2", "Ax"), True), (("A1", "B1"), ("B1", "A1"), True), (("A1", "B1"), ("B1", "Ax"), True), (("A1", "B1", "B1"), ("B1", "A1", "B1"), True), ] ) return cases else: return [ (("A2",), ("A1",), False), (("A1",), ("B1",), False), (("A1",), ("Bx",), False), (("A1", "A1"), ("A1", "B1"), False), (("A1", "A1"), ("A1", "Bx"), False), (("A1", "A1"), ("B1", "A1"), False), (("A1", "B1"), ("A1", "A1"), False), (("A1", "B1"), ("B1", "B1"), False), (("A1", "B1", "B1"), ("A1", "A1", "B1"), False), ] elif order == "simple": if working: cases = get_waitsignals_cases(order="strict", working=True) cases.extend( [ # allow signals that occur in-between, before or after the expected signals (("B1", "A1", "A1", "B1", "A1"), ("A1", "B1"), True), (("A1", "A1", "A1"), ("A1", "A1"), True), (("A1", "A1", "A1"), ("A1", "Ax"), True), (("A1", "A2", "A1"), ("A1", "A1"), True), ] ) return cases else: cases = get_waitsignals_cases(order="none", working=False) cases.extend( [ # don't allow out-of-order signals (("A1", "B1"), ("B1", "A1"), False), (("A1", "B1"), ("B1", "Ax"), False), (("A1", "B1", "B1"), ("B1", "A1", "B1"), False), (("A1", "B1", "B1"), ("B1", "B1", "A1"), False), ] ) return cases elif order == "strict": if working: return [ # only allow exactly the same signals to be emitted that were also expected (("A1",), ("A1",), True), (("A1",), ("Ax",), True), (("A1", "A1"), ("A1", "A1"), True), (("A1", "A1"), ("A1", "Ax"), True), (("A1", "A1"), ("Ax", "Ax"), True), (("A1", "A2"), ("A1", "A2"), True), (("A2", "A1"), ("A2", "A1"), True), (("A1", "B1"), ("A1", "B1"), True), (("A1", "A1", "B1"), ("A1", "A1", "B1"), True), (("A1", "A2", "B1"), ("A1", "A2", "B1"), True), ( ("A1", "B1", "A1"), ("A1", "A1"), True, ), # blocker doesn't know about signal B1 -> test passes (("A1", "B1", "A1"), ("Ax", "A1"), True), ] else: cases = get_waitsignals_cases(order="simple", working=False) cases.extend( [ # don't allow in-between signals (("A1", "A1", "A2", "B1"), ("A1", "A2", "B1"), False) ] ) return cases class TestCallback: """ Tests the callback parameter for waitSignal (callbacks in case of waitSignals). Uses so-called "signal codes" such as "A1", "B1" or "Ax" which are converted to signals and callback functions. The first letter ("A" or "B" is allowed) specifies the signal (signaller.signal_args or signaller.signal_args_2 respectively), the second letter specifies the parameter to expect or emit ('x' stands for "don't care", i.e. allow any value - only applicable for expected signals (not for emitted signals)). """ @staticmethod def get_signal_from_code(signaller, code): """Converts a code such as 'A1' to a signal (signaller.signal_args for example).""" assert type(code) == str and len(code) == 2 signal = signaller.signal_args if code[0] == "A" else signaller.signal_args_2 return signal @staticmethod def emit_parametrized_signals(signaller, emitted_signal_codes): """Emits the signals as specified in the list of emitted_signal_codes using the signaller.""" for code in emitted_signal_codes: signal = TestCallback.get_signal_from_code(signaller, code) param_str = code[1] assert ( param_str != "x" ), "x is not allowed in emitted_signal_codes, only in expected_signal_codes" param_int = int(param_str) signal.emit(param_str, param_int) @staticmethod def parameter_evaluation_callback( param_str, param_int, expected_param_str, expected_param_int ): """ This generic callback method evaluates that the two provided parameters match the expected ones (which are bound using functools.partial). """ return param_str == expected_param_str and param_int == expected_param_int @staticmethod def parameter_evaluation_callback_accept_any(param_str, param_int): return True @staticmethod def get_signals_and_callbacks(signaller, expected_signal_codes): """ Converts an iterable of strings, such as ('A1', 'A2') to a tuple of the form (list of Qt signals, matching parameter-evaluation callbacks) Example: ('A1', 'A2') is converted to ([signaller.signal_args, signaller.signal_args], [callback(str,int), callback(str,int)]) where the first callback expects the values to be '1' and 1, and the second one '2' and 2 respectively. I.e. the first character of each signal-code determines the Qt signal, the second one the parameter values. """ signals_to_expect = [] callbacks = [] for code in expected_signal_codes: # e.g. "A2" means to use signaller.signal_args with parameters "2", 2 signal = TestCallback.get_signal_from_code(signaller, code) signals_to_expect.append(signal) param_value_as_string = code[1] if param_value_as_string == "x": callback = TestCallback.parameter_evaluation_callback_accept_any else: param_value_as_int = int(param_value_as_string) callback = functools.partial( TestCallback.parameter_evaluation_callback, expected_param_str=param_value_as_string, expected_param_int=param_value_as_int, ) callbacks.append(callback) return signals_to_expect, callbacks @pytest.mark.parametrize( ("emitted_signal_codes", "expected_signal_codes", "expected_signal_triggered"), [ # working cases (("A1",), ("A1",), True), (("A1",), ("Ax",), True), (("A1", "A1"), ("A1",), True), (("A1", "A2"), ("A1",), True), (("A2", "A1"), ("A1",), True), # non working cases (("A2",), ("A1",), False), (("B1",), ("A1",), False), (("A1",), ("Bx",), False), ], ) def test_wait_signal( self, qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, ): """Tests that waitSignal() correctly checks the signal parameters using the provided callback""" signals_to_expect, callbacks = TestCallback.get_signals_and_callbacks( signaller, expected_signal_codes ) with qtbot.waitSignal( signal=signals_to_expect[0], check_params_cb=callbacks[0], timeout=200, raising=False, ) as blocker: TestCallback.emit_parametrized_signals(signaller, emitted_signal_codes) assert blocker.signal_triggered == expected_signal_triggered @pytest.mark.parametrize( ("emitted_signal_codes", "expected_signal_codes", "expected_signal_triggered"), get_waitsignals_cases_all(order="none"), ) def test_wait_signals_none_order( self, qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, ): """Tests waitSignals() with order="none".""" self._test_wait_signals( qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, order="none", ) @pytest.mark.parametrize( ("emitted_signal_codes", "expected_signal_codes", "expected_signal_triggered"), get_waitsignals_cases_all(order="simple"), ) def test_wait_signals_simple_order( self, qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, ): """Tests waitSignals() with order="simple".""" self._test_wait_signals( qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, order="simple", ) @pytest.mark.parametrize( ("emitted_signal_codes", "expected_signal_codes", "expected_signal_triggered"), get_waitsignals_cases_all(order="strict"), ) def test_wait_signals_strict_order( self, qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, ): """Tests waitSignals() with order="strict".""" self._test_wait_signals( qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, order="strict", ) @staticmethod def _test_wait_signals( qtbot, signaller, emitted_signal_codes, expected_signal_codes, expected_signal_triggered, order, ): signals_to_expect, callbacks = TestCallback.get_signals_and_callbacks( signaller, expected_signal_codes ) with qtbot.waitSignals( signals=signals_to_expect, order=order, check_params_cbs=callbacks, timeout=200, raising=False, ) as blocker: TestCallback.emit_parametrized_signals(signaller, emitted_signal_codes) assert blocker.signal_triggered == expected_signal_triggered def test_signals_and_callbacks_length_mismatch(self, qtbot, signaller): """ Tests that a ValueError is raised if the number of expected signals doesn't match the number of provided callbacks. """ expected_signal_codes = ("A1", "A2") signals_to_expect, callbacks = TestCallback.get_signals_and_callbacks( signaller, expected_signal_codes ) callbacks.append(None) with pytest.raises(ValueError): with qtbot.waitSignals( signals=signals_to_expect, order="none", check_params_cbs=callbacks, raising=False, ): pass class TestAllArgs: """ Tests blocker.all_args (waitSignal() blocker) which is filled with the args of the emitted signals in case the signal has args and the user provided a callable for the check_params_cb argument of waitSignal(). """ def test_no_signal_without_args(self, qtbot, signaller): """When not emitting any signal and expecting one without args, all_args has to be empty.""" with qtbot.waitSignal( signal=signaller.signal, timeout=200, check_params_cb=None, raising=False ) as blocker: pass # don't emit anything assert blocker.all_args == [] def test_one_signal_without_args(self, qtbot, signaller): """When emitting an expected signal without args, all_args has to be empty.""" with qtbot.waitSignal( signal=signaller.signal, timeout=200, check_params_cb=None, raising=False ) as blocker: signaller.signal.emit() assert blocker.all_args == [] def test_one_signal_with_args_matching(self, qtbot, signaller): """ When emitting an expected signals with args that match the expected one (satisfy the cb), all_args must contain these args. """ def cb(str_param, int_param): return True with qtbot.waitSignal( signal=signaller.signal_args, timeout=200, check_params_cb=cb, raising=False ) as blocker: signaller.signal_args.emit("1", 1) assert blocker.all_args == [("1", 1)] def test_two_signals_with_args_partially_matching(self, qtbot, signaller): """ When emitting an expected signals with non-matching args followed by emitting it again with matching args, all_args must contain both of these args. """ def cb(str_param, int_param): return str_param == "1" and int_param == 1 with qtbot.waitSignal( signal=signaller.signal_args, timeout=200, check_params_cb=cb, raising=False ) as blocker: signaller.signal_args.emit("2", 2) signaller.signal_args.emit("1", 1) assert blocker.all_args == [("2", 2), ("1", 1)] def get_mixed_signals_with_guaranteed_name(signaller): """ Returns a list of signals with the guarantee that the signals have names (i.e. the names are manually provided in case of using PySide2, where the signal names cannot be determined at run-time). """ if qt_api.is_pyside: signals = [ (signaller.signal, "signal()"), (signaller.signal_args, "signal_args(QString,int)"), (signaller.signal_args, "signal_args(QString,int)"), ] else: signals = [signaller.signal, signaller.signal_args, signaller.signal_args] return signals class TestAllSignalsAndArgs: """ Tests blocker.all_signals_and_args (waitSignals() blocker) is a list of SignalAndArgs objects, one for each received expected signal (irrespective of the order parameter). """ def test_empty_when_no_signal(self, qtbot, signaller): """Tests that all_signals_and_args is empty when no expected signal is emitted.""" signals = get_mixed_signals_with_guaranteed_name(signaller) with qtbot.waitSignals( signals=signals, timeout=200, check_params_cbs=None, order="none", raising=False, ) as blocker: pass assert blocker.all_signals_and_args == [] def test_empty_when_no_signal_name_available(self, qtbot, signaller): """ Tests that all_signals_and_args is empty even though expected signals are emitted, but signal names aren't available. """ if qt_api.pytest_qt_api != "pyside2": pytest.skip( "test only makes sense for PySide2, whose signals don't contain a name!" ) with qtbot.waitSignals( signals=[signaller.signal, signaller.signal_args, signaller.signal_args], timeout=200, check_params_cbs=None, order="none", raising=False, ) as blocker: signaller.signal.emit() signaller.signal_args.emit("1", 1) assert blocker.all_signals_and_args == [] def test_non_empty_on_timeout_no_cb(self, qtbot, signaller): """ Tests that all_signals_and_args contains the emitted signals. No callbacks for arg-evaluation are provided. The signals are emitted out of order, causing a timeout. """ signals = get_mixed_signals_with_guaranteed_name(signaller) with qtbot.waitSignals( signals=signals, timeout=200, check_params_cbs=None, order="simple", raising=False, ) as blocker: signaller.signal_args.emit("1", 1) signaller.signal.emit() assert not blocker.signal_triggered assert blocker.all_signals_and_args == [ SignalAndArgs(signal_name="signal_args(QString,int)", args=("1", 1)), SignalAndArgs(signal_name="signal()", args=()), ] def test_non_empty_no_cb(self, qtbot, signaller): """ Tests that all_signals_and_args contains the emitted signals. No callbacks for arg-evaluation are provided. The signals are emitted in order. """ signals = get_mixed_signals_with_guaranteed_name(signaller) with qtbot.waitSignals( signals=signals, timeout=200, check_params_cbs=None, order="simple", raising=False, ) as blocker: signaller.signal.emit() signaller.signal_args.emit("1", 1) signaller.signal_args.emit("2", 2) assert blocker.signal_triggered assert blocker.all_signals_and_args == [ SignalAndArgs(signal_name="signal()", args=()), SignalAndArgs(signal_name="signal_args(QString,int)", args=("1", 1)), SignalAndArgs(signal_name="signal_args(QString,int)", args=("2", 2)), ] class TestWaitSignalTimeoutErrorMessage: """Tests that the messages of TimeoutError are formatted correctly, for waitSignal() calls.""" def test_without_callback_and_args(self, qtbot, signaller): """ In a situation where a signal without args is expected but not emitted, tests that the TimeoutError message contains the name of the signal (without arguments). """ if qt_api.is_pyside: signal = (signaller.signal, "signal()") else: signal = signaller.signal with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignal( signal=signal, timeout=200, check_params_cb=None, raising=True ): pass # don't emit any signals ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == "Signal signal() not emitted after 200 ms" def test_with_single_arg(self, qtbot, signaller): """ In a situation where a signal with one argument is expected but the emitted instances have values that are rejected by a callback, tests that the TimeoutError message contains the name of the signal and the list of non-accepted arguments. """ if qt_api.is_pyside: signal = (signaller.signal_single_arg, "signal_single_arg(int)") else: signal = signaller.signal_single_arg def arg_validator(int_param): return int_param == 1337 with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignal( signal=signal, timeout=200, check_params_cb=arg_validator, raising=True ): signaller.signal_single_arg.emit(1) signaller.signal_single_arg.emit(2) ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ( "Signal signal_single_arg(int) emitted with parameters [1, 2] within 200 ms, " "but did not satisfy the arg_validator callback" ) def test_with_multiple_args(self, qtbot, signaller): """ In a situation where a signal with two arguments is expected but the emitted instances have values that are rejected by a callback, tests that the TimeoutError message contains the name of the signal and the list of tuples of the non-accepted arguments. """ if qt_api.is_pyside: signal = (signaller.signal_args, "signal_args(QString,int)") else: signal = signaller.signal_args def arg_validator(str_param, int_param): return str_param == "1337" and int_param == 1337 with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignal( signal=signal, timeout=200, check_params_cb=arg_validator, raising=True ): signaller.signal_args.emit("1", 1) signaller.signal_args.emit("2", 2) ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) parameters = "[('1', 1), ('2', 2)]" assert ex_msg == ( "Signal signal_args(QString,int) emitted with parameters {} " "within 200 ms, but did not satisfy the arg_validator callback" ).format(parameters) class TestWaitSignalsTimeoutErrorMessage: """Tests that the messages of TimeoutError are formatted correctly, for waitSignals() calls.""" @pytest.mark.parametrize("order", ["none", "simple", "strict"]) def test_no_signal_emitted_with_some_callbacks(self, qtbot, signaller, order): """ Tests that the TimeoutError message contains that none of the expected signals were emitted, and lists the expected signals correctly, with the name of the callbacks where applicable. """ def my_callback(str_param, int_param): return True with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=get_mixed_signals_with_guaranteed_name(signaller), timeout=200, check_params_cbs=[None, None, my_callback], order=order, raising=True, ): pass # don't emit any signals ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ( "Emitted signals: None. Missing: " "[signal(), signal_args(QString,int), signal_args(QString,int) (callback: my_callback)]" ) @pytest.mark.parametrize("order", ["none", "simple", "strict"]) def test_no_signal_emitted_no_callbacks(self, qtbot, signaller, order): """ Tests that the TimeoutError message contains that none of the expected signals were emitted, and lists the expected signals correctly (without any callbacks). """ with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=get_mixed_signals_with_guaranteed_name(signaller), timeout=200, check_params_cbs=None, order=order, raising=True, ): pass # don't emit any signals ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ( "Emitted signals: None. Missing: " "[signal(), signal_args(QString,int), signal_args(QString,int)]" ) def test_none_order_one_signal_emitted(self, qtbot, signaller): """ When expecting 3 signals but only one of them is emitted, test that the TimeoutError message contains the emitted signal and the 2 missing expected signals. order is set to "none". """ def my_callback_1(str_param, int_param): return str_param == "1" and int_param == 1 def my_callback_2(str_param, int_param): return str_param == "2" and int_param == 2 with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=get_mixed_signals_with_guaranteed_name(signaller), timeout=200, check_params_cbs=[None, my_callback_1, my_callback_2], order="none", raising=True, ): signaller.signal_args.emit("1", 1) ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) signal_args = "'1', 1" assert ex_msg == ( "Emitted signals: [signal_args({})]. Missing: " "[signal(), signal_args(QString,int) (callback: my_callback_2)]" ).format(signal_args) def test_simple_order_first_signal_emitted(self, qtbot, signaller): """ When expecting 3 signals in a simple order but only the first one is emitted, test that the TimeoutError message contains the emitted signal and the 2nd+3rd missing expected signals. """ with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=get_mixed_signals_with_guaranteed_name(signaller), timeout=200, check_params_cbs=None, order="simple", raising=True, ): signaller.signal.emit() ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ( "Emitted signals: [signal]. Missing: " "[signal_args(QString,int), signal_args(QString,int)]" ) def test_simple_order_second_signal_emitted(self, qtbot, signaller): """ When expecting 3 signals in a simple order but only the second one is emitted, test that the TimeoutError message contains the emitted signal and all 3 missing expected signals. """ with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=get_mixed_signals_with_guaranteed_name(signaller), timeout=200, check_params_cbs=None, order="simple", raising=True, ): signaller.signal_args.emit("1", 1) ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) signal_args = "'1', 1" assert ex_msg == ( "Emitted signals: [signal_args({})]. Missing: " "[signal(), signal_args(QString,int), signal_args(QString,int)]" ).format(signal_args) def test_strict_order_violation(self, qtbot, signaller): """ When expecting 3 signals in a strict order but only the second and then the first one is emitted, test that the TimeoutError message contains the order violation, the 2 emitted signals and all 3 missing expected signals. """ with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=get_mixed_signals_with_guaranteed_name(signaller), timeout=200, check_params_cbs=None, order="strict", raising=True, ): signaller.signal_args.emit("1", 1) signaller.signal.emit() ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) signal_args = "'1', 1" assert ex_msg == ( "Signal order violated! Expected signal() as 1st signal, " "but received signal_args({}) instead. Emitted signals: [signal_args({}), signal]. " "Missing: [signal(), signal_args(QString,int), signal_args(QString,int)]" ).format(signal_args, signal_args) def test_degenerate_error_msg(self, qtbot, signaller): """ Tests that the TimeoutError message is degenerate when using PySide2 signals for which no name is provided by the user. This degenerate messages doesn't contain the signals' names, and includes a hint to the user how to fix the situation. """ if qt_api.pytest_qt_api != "pyside2": pytest.skip( "test only makes sense for PySide, whose signals don't contain a name!" ) with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignals( signals=[ signaller.signal, signaller.signal_args, signaller.signal_args, ], timeout=200, check_params_cbs=None, order="none", raising=True, ): signaller.signal.emit() ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ( "Received 1 of the 3 expected signals. " "To improve this error message, provide the names of the signals " "in the waitSignals() call." ) def test_self_defined_signal_name(self, qtbot, signaller): """ Tests that the waitSignals implementation prefers the user-provided signal names over the names that can be determined at runtime from the signal objects themselves. """ def my_cb(str_param, int_param): return True with pytest.raises(TimeoutError) as excinfo: signals = [ (signaller.signal, "signal_without_args"), (signaller.signal_args, "signal_with_args"), ] callbacks = [None, my_cb] with qtbot.waitSignals( signals=signals, timeout=200, check_params_cbs=callbacks, order="none", raising=True, ): pass ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ( "Emitted signals: None. " "Missing: [signal_without_args, signal_with_args (callback: my_cb)]" ) @staticmethod def get_exception_message(excinfo): return excinfo.value.args[0] class TestAssertNotEmitted: """Tests for qtbot.assertNotEmitted.""" def test_not_emitted(self, qtbot, signaller): with qtbot.assertNotEmitted(signaller.signal): pass def test_emitted(self, qtbot, signaller): with pytest.raises(SignalEmittedError) as excinfo: with qtbot.assertNotEmitted(signaller.signal): signaller.signal.emit() fnmatch.fnmatchcase(str(excinfo.value), "Signal * unexpectedly emitted.") def test_emitted_args(self, qtbot, signaller): with pytest.raises(SignalEmittedError) as excinfo: with qtbot.assertNotEmitted(signaller.signal_args): signaller.signal_args.emit("foo", 123) fnmatch.fnmatchcase( str(excinfo.value), "Signal * unexpectedly emitted with arguments " "['foo', 123]", ) def test_disconnected(self, qtbot, signaller): with qtbot.assertNotEmitted(signaller.signal): pass signaller.signal.emit() def test_emitted_late(self, qtbot, signaller, timer): with pytest.raises(SignalEmittedError): with qtbot.assertNotEmitted(signaller.signal, wait=100): timer.single_shot(signaller.signal, 10) def test_continues_when_emitted(self, qtbot, signaller, stop_watch): stop_watch.start() with pytest.raises(SignalEmittedError): with qtbot.assertNotEmitted(signaller.signal, wait=5000): signaller.signal.emit() stop_watch.check(4000) class TestWaitCallback: def test_immediate(self, qtbot): with qtbot.waitCallback() as callback: assert not callback.called callback() assert callback.called def test_later(self, qtbot): t = qt_api.QtCore.QTimer() t.setSingleShot(True) t.setInterval(50) with qtbot.waitCallback() as callback: t.timeout.connect(callback) t.start() assert callback.called def test_args(self, qtbot): with qtbot.waitCallback() as callback: callback(23, answer=42) assert callback.args == [23] assert callback.kwargs == {"answer": 42} def test_assert_called_with(self, qtbot): with qtbot.waitCallback() as callback: callback(23, answer=42) callback.assert_called_with(23, answer=42) def test_assert_called_with_wrong(self, qtbot): with qtbot.waitCallback() as callback: callback(23, answer=42) with pytest.raises(AssertionError): callback.assert_called_with(23) def test_explicit(self, qtbot): blocker = qtbot.waitCallback() assert not blocker.called blocker() blocker.wait() assert blocker.called def test_called_twice(self, qtbot): with pytest.raises(CallbackCalledTwiceError): with qtbot.waitCallback() as callback: callback() callback() def test_timeout_raising(self, qtbot): with pytest.raises(TimeoutError): with qtbot.waitCallback(timeout=10): pass def test_timeout_not_raising(self, qtbot): with qtbot.waitCallback(timeout=10, raising=False) as callback: pass assert not callback.called assert callback.args is None assert callback.kwargs is None pytest-qt-4.0.2/tests/test_wait_until.py0000644000175100001710000000323414061506270021223 0ustar runnerdocker00000000000000import pytest from pytestqt.exceptions import TimeoutError def test_wait_until(qtbot, wait_4_ticks_callback, tick_counter): tick_counter.start(100) qtbot.waitUntil(wait_4_ticks_callback, timeout=1000) assert tick_counter.ticks >= 4 def test_wait_until_timeout(qtbot, wait_4_ticks_callback, tick_counter): tick_counter.start(200) with pytest.raises(TimeoutError): qtbot.waitUntil(wait_4_ticks_callback, timeout=100) assert tick_counter.ticks < 4 def test_invalid_callback_return_value(qtbot): with pytest.raises(ValueError): qtbot.waitUntil(lambda: []) def test_pep8_alias(qtbot): qtbot.wait_until @pytest.fixture(params=["predicate", "assert"]) def wait_4_ticks_callback(request, tick_counter): """Parametrized fixture which returns the two possible callback methods that can be passed to ``waitUntil``: predicate and assertion. """ if request.param == "predicate": return lambda: tick_counter.ticks >= 4 else: def check_ticks(): assert tick_counter.ticks >= 4 return check_ticks @pytest.fixture def tick_counter(): """ Returns an object which counts timer "ticks" periodically. """ from pytestqt.qt_compat import qt_api class Counter: def __init__(self): self._ticks = 0 self.timer = qt_api.QtCore.QTimer() self.timer.timeout.connect(self._tick) def start(self, ms): self.timer.start(ms) def _tick(self): self._ticks += 1 @property def ticks(self): return self._ticks counter = Counter() yield counter counter.timer.stop() pytest-qt-4.0.2/tox.ini0000644000175100001710000000136014061506270015602 0ustar runnerdocker00000000000000[tox] envlist = py{36,37,38}-pyqt5, py{36,37,38}-pyside2, py{37,38}-pyside6, py{36,37,38}-pyqt6, linting [testenv] deps= pytest pyside6: pyside6 pyside2: pyside2 pyqt5: pyqt5 pyqt6: pyqt6 commands= pytest {posargs} setenv= pyside6: PYTEST_QT_API=pyside6 pyside2: PYTEST_QT_API=pyside2 pyqt5: PYTEST_QT_API=pyqt5 pyqt6: PYTEST_QT_API=pyqt6 QT_QPA_PLATFORM=offscreen passenv=DISPLAY XAUTHORITY USER USERNAME COLUMNS [testenv:linting] skip_install = True deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [testenv:docs] usedevelop = True deps = sphinx sphinx_rtd_theme commands = sphinx-build -W --keep-going -b html docs docs/_build [flake8] max-line-length = 120