pytest-qt-2.3.1/0000775000372000037200000000000013223543656014342 5ustar travistravis00000000000000pytest-qt-2.3.1/docs/0000775000372000037200000000000013223543656015272 5ustar travistravis00000000000000pytest-qt-2.3.1/docs/wait_until.rst0000664000372000037200000000524413223543324020200 0ustar travistravis00000000000000waitUntil: 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, the last assertion error re-raised and the test will fail: :: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def check_label(): > assert window.status.text() == 'Please input a number' E assert 'OK' == 'Please input a number' E - OK E + Please input a number 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-2.3.1/docs/index.rst0000664000372000037200000000113713223543324017125 0ustar travistravis00000000000000.. pytest-qt documentation master file, created by sphinx-quickstart on Mon Mar 04 22:54:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ========= pytest-qt ========= :Repository: `GitHub `_ :Version: |version| :License: `MIT `_ :Author: Bruno Oliveira .. toctree:: :maxdepth: 2 intro tutorial logging signals wait_until virtual_methods modeltester app_exit note_pyqt4v2 reference changelog pytest-qt-2.3.1/docs/modeltester.rst0000664000372000037200000000427613223543324020354 0ustar travistravis00000000000000Model 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. The following attribute may influence the outcome of the check depending on your model implementation: * ``data_display_may_return_none`` (default: ``False``): While you can technically return ``None`` (or an invalid ``QVariant``) from ``data()`` for ``QtCore.Qt.DisplayRole``, this usually is a sign of a bug in your implementation. Set this variable to ``True`` if this really is OK in your model. The source code was ported from `modeltest.cpp`_ by `Florian Bruhin`_, many thanks! .. _modeltest.cpp: http://code.qt.io/cgit/qt/qtbase.git/tree/tests/auto/other/modeltest/modeltest.cpp .. _Florian Bruhin: https://github.com/The-Compiler .. _QAbstractItemModel: http://doc.qt.io/qt-5/qabstractitemmodel.html pytest-qt-2.3.1/docs/logging.rst0000664000372000037200000001607513223543324017453 0ustar travistravis00000000000000Qt Logging Capture ================== .. versionadded:: 1.4 Qt features its own logging mechanism through ``qInstallMessageHandler`` (``qInstallMsgHandler`` on Qt4) 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 qWarning def do_something(): qWarning('this is a WARNING message') def test_foo(): do_something() assert 0 .. code-block:: bash $ py.test 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 bahavior of printing emitted messages directly to ``stderr``: .. code-block:: bash py.test 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 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 $ py.test 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 >py.test 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 py.test 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 py.test 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-2.3.1/docs/Makefile0000664000372000037200000001271013223543324016723 0ustar travistravis00000000000000# 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-2.3.1/docs/signals.rst0000664000372000037200000002013613223543324017456 0ustar travistravis00000000000000waitSignal: 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:: 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, # SignalTimeoutError 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.SignalTimeoutError ` 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.SignalTimeoutError is not raised, but you can still manually # check whether the signal was triggered: assert blocker.signal_triggered, "process timed-out" .. _qt_wait_signal_raising: qt_wait_signal_raising ini option --------------------------------- .. versionadded:: 1.11 .. versionchanged:: 2.0 The ``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 = 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() 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.SignalTimeoutError 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() pytest-qt-2.3.1/docs/.gitignore0000664000372000037200000000001113223543324017242 0ustar travistravis00000000000000_build/ pytest-qt-2.3.1/docs/intro.rst0000664000372000037200000000510113223543324017144 0ustar travistravis00000000000000Introduction ============ `pytest-qt` is a pytest_ plugin that provides fixtures to help programmers write tests for PySide_ and PyQt_. The main usage is to use the ``qtbot`` fixture, which provides methods to simulate user interaction, like key presses and mouse clicks:: def test_hello(qtbot): widget = HelloWidget() qtbot.addWidget(widget) # click in the Greet button and make sure it updates the appropriate label qtbot.mouseClick(window.button_greet, QtCore.Qt.LeftButton) assert window.greet_label.text() == 'Hello!' .. _pytest: http://www.pytest.org .. _PySide: https://pypi.python.org/pypi/PySide .. _PyQt: http://www.riverbankcomputing.com/software/pyqt Requirements ------------ Python 2.7 or later, including Python 3.4+. Requires pytest version 2.7 or later. Works with either ``PyQt5``, ``PyQt4``, ``PySide`` or ``PySide2``, picking whichever is available on the system giving preference to the first one installed in this order: - ``PySide2`` - ``PyQt5`` - ``PySide`` - ``PyQt4`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt5``, ``pyside``, ``pyside2``, ``pyqt4`` or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` API to `version 2`_. .. 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). From ``pytest-qt`` version 2 the behaviour of ``pyqt4v2`` has changed, as explained in :doc:`note_pyqt4v2`. .. _version 2: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html Installation ------------ The package may be installed by running:: pip install pytest-qt Or alternatively, download the package from pypi_, extract and execute:: python setup.py install .. _pypi: http://pypi.python.org/pypi/pytest-qt/ Both methods will automatically register it for usage in ``pytest``. Development ----------- If you intend to develop ``pytest-qt`` itself, use virtualenv_ to activate a new fresh environment and execute:: git clone https://github.com/pytest-dev/pytest-qt.git cd pytest-qt pip install -e . # or python setup.py develop pip install pyside # or pyqt4/pyqt5 If you also intend to build the documentation locally, you can make sure to have all the needed dependences executing:: pip install -e .[doc] .. _virtualenv: https://virtualenv.readthedocs.io/ Versioning ---------- This projects follows `semantic versioning`_. .. _`semantic versioning`: http://semver.org/ pytest-qt-2.3.1/docs/app_exit.rst0000664000372000037200000000222613223543324017627 0ustar travistravis00000000000000A note about 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 pytest-qt-2.3.1/docs/_static/0000775000372000037200000000000013223543656016720 5ustar travistravis00000000000000pytest-qt-2.3.1/docs/_static/find_files_dialog.png0000664000372000037200000007531513223543324023052 0ustar travistravis00000000000000PNG  IHDRR_PsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.100rzl_ {w/EK ˴TaISJ M.-/.4djT Wn)1Rʅ2'+.}&TW\Z044$Ο_4)*[MɇgeM eNʘ>>;}$(-KRjĔBҙ2Ĕ (a\:^bSbSqQɑIaQq!L1PPxLPXt`XHH_po`w@_ow^mݭfjebidadf`jiolikdncdfchfm`je`b527klolgl1B tǚB#MFc4|c5vH#=c1ᣕzd.'F)4zxZ=>Bփ [\Fb-|[Σ|9H'~uLߥRMJ#C :JzBWX0c8Bo4JXwhC1fzFzcnji`f2q0ucidilbju/5맻Oo_O@O`(9 4"04wpxtHD Sd,x("&ET\Rt|2^cA)b#ONSҹ2AY B˶ ؖĦ-t.iۇyk׎?yN䉧:ݺp 7Ѕ-k/63]i^{iM׼Zտݺw}[* oڷ}kڛ[غ*Ӻ[]мܖuOK;yX=3 aR Âvw(1PTeS+Lb.V5}ZumEux_Ӫ1Z](L\75 WLƧW⛔qRZo[P<%ri.φ<&NA*t̉\BlB)d$ ,$A|2 HiqIiIXãX"XD@D,ã¢C##|¼CC<|}ݼ})"Y9{)xaajijalfhbeMI`1;@ V҃-x5450hCPH=fOR=:\#Ð\O qw*%I։a@| >QLO᳨#bw\rTTPAT[*h "D$ !D* &VvC(#}  <#ɽH@(#k2] X<b-Ad )Y$`y𝑠9ޞF0@ B+kx+q_h ˯nX~}޸ML~Vh޴76qՍ+.o\qq V߰c" aK_VvUzj슪ʚ3jfϮ1~fC93f7̜(kFdLuHT;s6z6sla~\4bl-1{zݬ+j QO0BʅɈ& <YR!7- yH5)%sȃqL<'f0"60<60,: 4y/7;aje 0!<\]} X$:Z9,0D r`c`n  Xଢ଼Xs۱f6z&zF(F5E}0L@T0-L,r Ui-04QcLD˘G?6r >1xFxȱ,U$ɦ1?XMrA:J#~IRv>:l4@gH'QV_mhChH=QzrcMF5E'%z< L,Q&TXڛ>va`l +V?OVw JXT^~>daP^p0y R R$# C 2&0! 2&#X1>'sBN\&UWiBvF tO͚89P*SJ]tiO|mbV->ߴBMY|ukz{ݢwM-~o]|sk\W,f5K.^%V/9zɵC<8As?"@ ƹ /+*-FtYP:g@-zߚpz7[8K-\2Ѳ _jj!k17EKXȒZ|"h٪KWb%O3~V#b9jgVO-.)B^QYnaiv~qvAIvAD yErIr0@rXŀHD0i\D(^'&6 j }bbȸ$/ 68*!$:14&)$*Aq,B|"zFÈ%1wra!Q\ps!gcpO=1xp}cGZY6tFX7c߇D"A.T[ ;!Q#Q!)%G&Ude$ l+ FBސLzF$@M`6ڐi9* 9@be01B(U@80q4a;-]xqk+I=)Aׄ*?tV T` Ե |YD0Az-< X ##@XAg(2.4*>@")~\t|JLb*JjQgC 3"2`*XέU&ևg;hBN"H.7zfV_سw羞:Pƽ]ڷ3}ءս44kί;7̯6ڛkߜ?]HY^V\Z -҂z‚7ԟ]PfAk_:=bgG<_vu9"t{|8b6ZY[^UW^]W1e@MY Y,-\ þW6_]y[Z67o߲u˶j[o޺mSM[Z6nٺaSM[ֳ n؂Q76o߹uGGˎmvbM[azLFdC.[vɊ˛PC,Xj ͚aAUs*jgT-.)Z3yjuaYeTZ_2-%DNєI K+_:1d@qҢw7L.>XsNpj+Ix konoff1O7saX 6j^Ck#kʆт~;{/3G '_Kk@k@Kg?s/S;w#hccp[.#Utt< SR5ǝҨ,T ߶W! ]C !,1l " ?flO3Ȓ nVn֎g` epqputuXxbt FOB Ee: Uq!(¢bYHY&CRdE .Y|LY*I9Pno~3-w2J& OwϿҒ^)N)/My庲WJ_+=[WzB}+L%׸aɒs0^]s]:__v^rT;)ղ>׶-JcG t>d ]DK[l ^j eïno۶}]mvվg_7햄ɘvٱ{Gֶ֝vloچmӵ{vtvwbLmGۖ"HoܺvЪu[Vݲl%/\4wʆf]\7gayjO=f֔Y%%uj'O/ZS8:y%Pr(<i9LeC}ڤi&WM,*XX>`jfNIJV^zF6cP D&eE=nb4{<>*y|dbfD|FX\ZH̸D8hwg/{Wd"\|}B"}C㼂c#<[n{ck'sOk7P''pgp'0'PGPG{`; kW?+gos{7CK10V};b )CYƶ&n&0b3T^N>~i*cM=L܍m݌l\!Ζ`i wfb rgBk[GgwO_K@Og{ +?>R~Af#쇰Z5bӝB7W Տ$D,OYY[&C+'H2a2Aabbjjdfffnnaiee ĴquE+ y{FxExG@~1XQFE&G'D' ME'$gĎˌKɊO6!1m"1)2>J6y~Ƥ(+uXlB~1$u+,FZ(wTۃ J 6v_ywIʻӪgÈ!1GU>: =]1يWM~z׫ )TSxJK p)Tb1E^j^)z^,~Sgwlx>9OCgξѫΞ;^?w^ ^vӚ[ף޲c[]{:yGtPCG1V;:g߁ݝH;wa::;_݋Y;+Hh*-;66nܺc֝кM\eiӦE+[԰hլg]:}΢ gί1qjmsʪgV(/V7eCmQymPj60 kLk]>4-)ʞ\1>,mBABʄȸq0H1PlZNl*2):ebTDDBjp8W`O7;qI`#/#87btT_DoD'6{'hdhd׆W4]c<܃܃b݂b]#}#}Ð,|-=zSzS{CkW;6J  ;شo,'njl㎎||{fC<ؼx('?(ڙKrq s Np IM 6#$-0/+ _(ц;?~|0=ӑVcLQB pBDa_&u2U1Ʉ?,@LBP*X:35~uLn(Q r r C?E77/??= gP4=+jHLJhl*Vt%oTRfTRV̸PlʄԉP|ڤČ\(9+o  &M*LT3DW2>tB~4lb좩\A]SuPWA {O;񲵭Ë_AwV%W΂#Û*%gϾ(BeySeyOO{vjSsSs^-9SsvZr.N˹U\QhqnwF<2Ŋ7*r3дW\ByMfJ)'俾ms+וn@?s+Re7onݼvtL䇧+¿yOy?:vBUG8tbAttw:1GG'MuO}WvVXlo۽mGVP=[Zwolib}՛0w麆Mn Kg/b)5,JX**M/Vb4V.SRPZPR=jNqlhri3 yesK&OIE 787$*02!$65"i|IH< ĦƤhH K LD7 ;hlq؞]"61&H=3Ck7lHX"W$DlVVvJ UBB|?KA;^G&W:U~a(" HW(dk@[@{ y K|"#bRbRcSCCqHbdҍMKNȅ3 1T:irZ<,~"hZv1S.ʼ*ƄD5ʵG|:{ }>w%eNWڼ;V퓏?)˧d^hoޱu{w߱;uBphYN@pqƑ'>zbsNړ?ױ ھg{۞;mj׼sӶ]붶7mn]aҵ͋VmbCush+kE$,"nT 5ː 5sJkf3믞]R^KPV;wJ)u u^Y-4tzcIMcIuم3PWGϏ$EzyEš3<1jA.b@ &aC0wuvbu{rԀTDwHG k'/ {wsA {DxG${#B<c]|#, (!>迳{[pz^>Q11~LQi)H >v^b +<'gp!U#Ӽ~Yq'BBqc0=" ōȱfbG6F6O0u@=<}jB\[Y߳"4!֎!QfodoQ\1P[ ʾXBx2;,'" _d2Q(7SSccӂC⡌Y#'@(pRxlr32$O(7)@qZNIznIF^ifޔ) B '}lwkvIjR_*rɊPWٌ*tfVUV3kJ)әPCA+B3y\stiL,(ĎIe(>ثZpdq dLS$eہ,kC~% 6;ns0```NzGp{EY^pч#R`y.&wEIc.8s2d``hQJsƛJG tx YRCa0ucUk:+ Vڼc:nհ𓧎" N=wO>g^To%~^M::מ8uI)^w<бG<|Ա;%O>~1:va]{wuw׎]w9غֶ}[vtnֱ}-YӼPupuݼ4,rySQ<)n`l`NƲ ?~TA=a0fnquCQܲ좊ܒ䌰脀(ߠpv- '$?"ݫDlQ1)9PdnI^AA^֎V|/]?l>tQE ?3aa {Z8y8E$ ,G0uv1w7pmd9lna`c`jkv$HHJ p = Ha?50ńg?/:<TJG`,x0>(\=}Lm]ǚۍ200ep|7H[P{x1-vmsWRIF#w.x`H L_s\܃!d@&ϐOԂ ^ȀD0>l8<, Q)(:ӂbӑ< 2C" HޡzNL, #JO*H_0R&CŨs2YS$+꼲ILd/B?7&lPym:iu,*II0s>T5s%뎎N??mdZ|TʘX GfI0DŎ& 嫴qkt^ZtIIv'NmNF9t18jp)7g s7xAƨ|Th cyw6' r58bpqgN&-N&[L0ꍷnxaWe_# v_5jئՉ0ϯ1i뛷jAj{pwסwJaaйO'`ڷW'prV?'>vԑc'9vБ78t:xcPl|)N}#k ELXֵkOWz7!{;wmޱam`= E7_aً\vKf/`Z=OfX5PU=rVq,^T)]0mfҚ+M*JH+XxH MCoaaja>vA!%m;,3(/aCg:xZ# <md6( `(l:۳,гZ mNogܘ cGmZ[cl %`A4\;Lwvt2@Y]$<`u@D*?S$v:d D}lpAN2{ake<ѳN}o?]T\eF{\I-9ZO &Pd1+XUBHPd(yx`%zJ@<*!-eCJ^%d%fJ0ť`dUB(RKҠ  ( @0TB6d l.+X &URAT;;LNȝ\ 3嗨/?2Q%ZkS_Zòrv/6o]>>|,XVV^V۽zY=8{ 1Ųđ/xD,ZrMUIl_0׿*>1U57mc'zno۳s{l|r"_,|hZ/p] 'OkhVAepI) PaO c1>6uY::YKPܳ< omۻumM-|7Ѧy+7,];{QSuWY@\tGfG1a( ,l=,c> D!)i?^as7A%B{p"*E}X9a xeB{c#ǚD \M= PLR xGq?VvqAiRQ4`@uP!X%~\Nucpv8Qphh v838+$',7<` X{cƱ X:7k3#"ǍgR&ŦNMˎOIQH(7bSYeفIESit*~ `J5T8H:(VB`' Vΐ$TϜR=¥6N!lKT*/@i5m j ˦ MaÛU<Ҧ m..;]:]v; v>t0hÓ"tx6y|! ?سz.i(D#: q#u{ f{ }3/f xMGOWVWDĮnJذ"yiڎ[B+֮߼m֝[[yDls㇎8|c5znRcgϼNGE::MN$8xh#Ё]R!oécA޺v`𜾹}LZwkk׶}Rlھb]՛X?w);T0s:~Nb@;!q*DݚҕB$+QL(".5㠧2(9+/1=;&9+,690<; tc Gw_'Og W07H(XzA 7Pg`GO/K{7SkGcK;vθ;U#XԽB`@N^fv&6VFNV&n ?2}~ a1) Go>e8;sgY5Bqkl;Iwe?`~ҧ'-h˘9:!v2&`[P;0d> DCFXop| ;Ә5,eZM$.d"MYzg8HyƑ,#{O\#. yG?E;vEٸٺۺI'ٻڳ3<=ѽpqugӍ XiE!N7 H L J3.4n$k:4%gKىF 3&ekzU>슟  &Nv xn4vQJPeTVU: lw9P\^_P:PVY-T4k<^R LFn#ο=WggDCI~G'L;\s/zԷ^T@/Jyf"a_r]X"Mq4D$nZҲ8msn"H# .]zݦunܺ][k:-u휋G扁ð{>;tyKZ0$*_Eu^ienIE6`*V \X۲JrKa-X%υHT2ujлIBdƧeǥLNΊLHI肂K\o^WwW0؅ݰz[;y۹X9ˉǚWvc ۼU#~c Loj;z 7#7S;wS{!܍m -ƚIG>:5S\c# ,F  ݌`U$Hv⣣eW0#3GvN$a^3|öp7:s{1S#3`m^>I\ʀNfv5xth~8vX:vhvyO*kTߗ&@.M~BW'wi %`Eu %h\yExEW$H%+ PxܸȄԨDt~9_E#ddB\Yr yBhb~ b"kξqͳP͌%3i*U0URiДJISz՜m;;,ǾeWV=㙺&{hBI1'rbʉy&'ټ^ ^[{ COaG'sB bb:z.0š/8tm2 vqx|j81("?4+ 'ӶNnV֎RO3.a݂rFA`dig`jgl!n4n8c3Aއ?,l1vhC ]QVq-h w>`c-8| ѭ+ĭO ƲsL!~!1!&t7b,3Sc_@1/i9VSiص`*>h+~23b.HM)ˏ+moKB떕Pk5 50W/Kح.{Wtn-%].ZNIMK+3S2!vŲ`2&dNJR^,X/T>:qRNkYK+kWj`iUQ4}ZI^R _ ?(nW_ @)e| `v|qk7<ܲUǵj/o6n:#y$&)=:)- }\R"RЋ Gw&&)`q"eeNsX=B x `w`a73Ƕd`6*i8-c7@ {e7 nJoI6 [85^IacK42 G0`7c7cWs b - %n:A܈M\vFBMm-T7bҽͥǥ]9=Z0Alz6%|"!~S#cXL+E ×WޘǾ7-|#!坎͎;I7;osuǑ|#Gհtw#'w_g?OW7@F^0@v_#qk#~Kǘp~S#CT|x4Qd,$vxqC ~;ᬤTvkavFÊA3;xmJZ<졣' {JCGKGUZny_۾nx| /dھ޾қ=FxjK;7^ڱMoul;ٲie(KO\zl0D7 ޗog*oͷ_ |~ϯ2 g5_l2Anشq--[Zwnپ}w={Q(go_؍Ծ_~,Inߋڃ尷LmI7`7@y [QmZfҕk,Y[?g^M’ܢIy&t,UxfIXDO$". I I Js@%60]D)ܟ/n5Ȏ[O۹x8d1;Ya7ؐ-960~k9cggi3f Y:4ompv'##αޱ#4-(ﴨ ؍}7M/bg[(˓{0߁[fH߹1_ [B% ?x޶0zCϻjU~X !~=o* |Ԟ@ox7@\y0{ޅ{ޱF["tn}*nz EAxi"ռxAU#G%$;:C&fBL!sM[J #b $5o4*X7D7tt^~~8+T+Tk#(8lkTE;B㬍, - LbMD2M1j 9y酋k="Az/,fO+U0lv|O-Zb-f@mhdkoZi˦m۷lپ}dhUmL;{!â}vx77o۰y m\fk-[5oѲsj9"xJv>b cB.t &&MhtFj'JƊY)4!Xq=|ٍAc~!WX2߷51b5x){ ac:nI?J}7;_VъKeLQl}iAhۄ8dj0i[~ TY|2L sq%|oCl׿UoUcbn? XD}/ɏL)u@  "'&G& ?2=/=/A<,Z~X=RAl 1  .O?H< W~.͟HsCR"'"̘ՀbӳO~B%gdwTuRT+&ճՄAx5H~šXzl5Wăz>hS~ yjʇi4-%Cp処ْSu&Hv*,k_)TnaRy%*ҔlJɶKhL*l)Km*kjϘU?B,,Cl m@0۰ICkjAK` ZIq\WYOZٴlj|EZf5G2~v ZQX\ĜYƥg%dRpb!C$BR!1aXհA~aꈕtbgR:g#cU<f,`k#m@o̎\4D g%w|(DJ;SKX<N!kmwbEǑ܅))|[,&{InG} Mm"AM)+#gD+PHc5UԄ35âBG˪^WU[0},$v0Wu I !LIe啨Z˦g߳/j =~BJxO )I UHA$KeA ~!1( 9Ћc1 +ce#Dh-5A{iXgX%4VyE&3D*tH {z#Qn%Qn%eܣDnQ<.@ z y4[X>O5VQWx1lZzj%T(rI0qRU5Mx JYRP,$wK"V?IoKɘ0.=x^&]CXvV!Hlא;< :&ʂ@_ŀ#f_!ŀ2 kP{ hlvpҰ!Jc%Q?J%fNU$Hێ#"n@-;H*^^Kb6a-86slr$y =@yKCv<4LIKhLʤ8+R+<`€B@x7ɗ>OfIyrO<>\*-s"2 9-+)-o6b@:BA"\T1.DA ŀ)oXc oKC-i0uI^UNmE\"`CK D%BFXyp'J?.#eHаn&J= S<|g逸}zY!vY<'$zSO:$t:z#Le:tء#QH Lܷ Fׁ}!vE}8vx0g[nh'B_}GįP=b{H}rm ;2+Κݺm3;_Eh?kvӖ5YCڴE?DIw4oG+rX嘺XQ5,.v~8Ķ 3wbێ؎ĥE$ABTUb{&6R-06dEޱ `VppXvqsO W<y$tJ3"fJj1zJN=l/R^?=aBJ 8LZQY#H$4{ȃ>сG/A;L^}A!AĠE !€d0 @@0 t .E€ A/a>g0 aJ% ĥ A 4@0 <>Lf#  F@A8 A) w7~]H$L $Ta _3 $( ?g  0[X2H20'0hCda8TC&*蒧}SKAa@D0xb(v27|vCՐ{SUs/ +G6a# A$hF<@ R/4BWOgb77   ڄ]_<]R^< йéBS^84_߸' OHAh0UC}?U7@uL/rFToܲeϦ08QTXņnQM~|!D@>ǟ7.kʙ|}+['Q7 rTYf]H1bR+fXj@ ~ 'B000ˆ_߸ й|3/mF!9_9FyK€* ~dxOYiG00Ϝ\e=9 VY|6ӊΞ7_c}hט^H1r 3 gA8ԪB0!+ De0kY%Ve`ًeWѠ1 Q;3 "{U=/,2P3X,3=f =<UʝB# mh`Aa@H_1K|%\!vD~Yjf;ڝMpiX`HUӔ(  F032aBv6=Uͮ%˯js/   ڄKVY+y>3r}좳ZY/|€ 6a0BW/Ob`W jȽi5/xM?A @ a#G2Cvor{PZ^U2R^6a@M<1|/=vՐRtj zA 6A (| /R!Dp M)LGO:?|7DP5I&7‹˸ WL0 0xl0DWMtAOWJ!a0zhV5ŗ^0 0 ϯ@>ňNSA N/~Z5uT΀3~U  A &_^8: >3k$~€ bpWeÇ#aP;c_} n3ϝ:pD"`J&ɤaWz" >~vc P|tA -/:C7"0裏g6e Q12 ^=9#TasΟ8OQA-3i=>"@ ?aBYg^?Ka?A @ 0y ]Ը`љ( € as!?"@ O>teKϽmOݟh/{ר~f۝K) hgϝ/EKW,Z\ 82N  ~Ih0yP-]J/h'uu~P5Io1xsmyKGDQ#MF+P12 `JGHag-_f&hʦ.jn@sXʼC`0R+{,ƊUmb-b-M0A iG0/_ iD: V5Vr]xI0ifݥ"5'SF1\+[İ@nu|zK  ~H* 0|0?6ȵa/]& T|J3eF]aQWR}h(+( 1 |Zn@\a57^|@VacQK㺙/kðhQK>Qդ+@u_L} TlXcqj( 5 a_~|?"@ ?O65C7m._Mf2EM=ʎf̻b ggbsՎy@B 5X   ZL~w{UOncjnr5-``#Wul( hW^3qGHa?eZ^qs~(0 b@m\! 0D_ZwC۹߸yWV?!A @ 0|) 򗿴&ԱoQA-&!y> u.}{) a@!b@mz{#0_Թo?Pϟ>D" ($ow3|) 8t:έ[AL=} ㇏;|ء#n[  A &GHaǞSOC':0  `'b/ O?ܩgޣ0  {R|Ͻs3= >AĠa‹U|ŋ/,!A|g;uDTb /f:< _|+qzW?# bX;i#C-Eo.0yX= _8?"@_~י^;}C O_akkO{淃C-E]/L׸sGHa_9{^gg? 0 O_aK~zpK0O^{| :Lo='|Ja?1+ |_D-E]/LV 0D_} 7C>;0 wD-0yX= _8?"@/\t /A~AP08'藃C-}&"@_} }0P>.{&/[қ\f |~??cH0ˆGHa__z2t*?I0{ma"af()<~#w̙^ >J0ib!;|' |ng_}RWiaL1@~ ~anۇ#0/_?;&]!>À'0F8d_Ыhqi9€}g[-y~mL^2|/T3g͙;X?^ŪkV +b.,|r4t}j^8X;CoP#󊡛>E v\lZ-t,c\]1Э]—^b#cݑq y??.n_u&-&Cw Go<76"&; "̘ÇRܺnDlbDLBhTtБɘy4JL/lՠL6ZđځzDX<&~830?^Vv~Uzl@4i.,[T),>@1KϩPL>M5v1%&;)>3H UE?a~_'~ٍ>S/s \^ZRv vO4d-6))NX;ܜi2#0ao/> `aQp{>ֻJaέ[!1aQ]}@2[09J*Kq r='B@0w e2TVU[ dYԶ'}1zr!O2T3M?:|cHnOo0y-dcK x-X;^ 'PMDÄ߂_}LaQ0D* ޹!~]aӢ1cocG͂v&RըfilI_cH9 051 e)] 4ԝ;\} 4c af1:im% so0a) ~5_PoPؾX0g3ƠdFùٴZ ꉥnC?aK䶇UU6);f_&Vlbzi.nI 3>VK̂9TRL#-SnPL)- Ջ!~?w?S595۲t0ly=YG'y>2E']̒b7eޘUo-}ɥ9ja:YkW-EڇlPKp$O@^@o{߼53fegc@c2*DEbz(F&6?0PY+( HjS#؜*x</o-,ZzLF9 V ZYݖR"5AyO=#Z#Z2Ԗ\ na'F̗5,a-5ܯ[n' a0e&O`ޮZUu5c1;v;S p㔭  _o/> `!0|V#aJO}ww D1qaU|?;Fau3oIw Va"a; Igo]ne@OO?a|}Vxa0D: $p0N( bW$:_.[L;/['r۽wAP055/9 _i{ܼa[oI黻;@3 b 3>s5qS>go/~w` G@N'w]{Rϟ>Dv*m{}`"4@$Wn  =~'7o>l_3 $wIa@1(A\lP7F98Ma@1(Acy- D`K[Aa#l]]vSA NXt춶wa0qM9 ,v& -n"H$Ҁd}0[9E a '㎶-@ ?+-`G. ['ȃna` %C+A 00va0ii,Үug;A > ZM-`,R\qSJ+;c ֝mA HCwa/S+{FwP1 0&odnÇ6fVZ) Phm5 _: ]!ǚZnkIa@@V# ߅en0@JD: D,7h0 Ԝ(`OA)|nwf.HC? ՟ "ls `c-a"zF-wP*n-KmMa֯Zso55)4zF0|>̿[iCZ? iPhBŧ9suewE"1}?gہ.6 34a0z Mnk2 Ϸ_ߤA#:ˮڢu}TMVOpggyGԣK=k=q/`[q ٠Xb^" BaX=ڋe> uѨ a0Ue0 57cd1aji0eHcW}"ט{:սE5fHP?g׽'ĄV f6aQ-waK? !jȽ׮4234kjob6l4_߰0?_}/Ҡ_ڢ-!zNVs5gHP|@;foaV\5p?D 1|SJL`>LG t7߲rtp0wpѱGWPo?_~OҠ\W{mQ\]&mbIW.Xr}#/ףHP~x*MZG`^~~5S ]}Ýp{G toddh߾KTo7_|5D"g[izf{ۺ{aί|ͷoFDE8wRo( H$ҏ* }Z:@oz]xϐxGmH$з~?O>J00?>D"~ og[ivz2K#@ |]VĹźѺ2$@{FXv?wa"U☁oOww讽Kg_k`gsnibbllddh崽mH7N% ڄFVa0D`Ƚ]vCZjh#˖;Y| bMoim G+5+W7g2|F[ZZ7A@06r4L1l`&!\ru許|c#6miAm65+ZdžylG~ ݍ[j< 6a>z Ѱ}? pK<:LaybCGnL@&& 6b Ç0#(  Fn' #Et U0@a@qml@A0)  Jg/g`M[(  : Ua@# $0 K0 >6€ nD0x15@Nh=A]ai n/_y$xaAw#ڇ2yPh0 {aH  }0tYN  uIQA܍|0(  AAAa@A  ⎇F  -=y€ nD0x!  Aŝ ĀAm0|0ngp Fa@qM >ræ-a0D!\d)kfL-7A m6nQ GWCtdm2l4_PA ( a#GojGGH@ W c,kKK]=i0 Ph#taL#:wk7Fjim3  6aoز}}?xƛF6nZJ AĀB034km4u7< _m+WYm{ZJ AĀB000[B t77z;LVރF6|}Ca@1& -l:W)  yGnu77+r׾C|}Ca@1* ,vuvτxa[ep* *[ATc.t1 m8f_xX:Xu*l" m@H}6 &  ڄAAm +DAm {]K ڄAw-f@3 K& 4gu{ҙ3tFq7Mh< 0 TPAAA(    € AAAa@A  € 0  AAa@APA€ 0  (  @a@APAA0  (    PAAA(    € AAAa@A  € 0  AAa@APA€ 0  (  @a@APAA0  (    PAAA(    € AAAa@A  € 0  AAa@APA€ 0  (  @a@APAA0  (    ܁0x6l2tA]6a0t |lăI !\|GڰSK AĀB0xb>"AV70|*D"$S+k,D@~swdu=hdn#GA "-l:W)  yGnu77+r׾C|A nWaG(nLU2 2*q_V۞.mAww1vͷDƪS0DA]l">$M3  :{_LAuy7Aq罉]K >Zg@Au<3 ? 0  (    PAAA(    € AAAa@A  € 0 ijj6Xo/~wAa@Xdɚ5k>o -E]/  FMMo~ ~~ € 1m4%,wI_(  0?\QA" 7;>:\ֆM[0 bp"o?#- 3hCܰF<0rWz| 14A7p~I{46/2 >j%{>"A!^|z6mi6BW bp!ws \]h_a#GojGGH@ W c,kKK]=i> ?2[?+`T/LTtt4h#taL#:wk7Fjim3#\4$ cZf?3ie蠅j;~aoز}}?xƛF6nZJA .Dܺ#<3i3hQ"ڇYk{ί|o[j۳R bp!;֟pD1|> L`V͍ޱձ4AW_uG85q) G(ۥ4 ;~a`laӱwJa=r[\2#\0yGxѱ) G(-cj\?K]]3!@Vʀ L/>/Һ2E_QK8f`/mO3 b"CH#~Bػ;N|ۿ~al"cթD4+ "-@H}6 & EQSS4Xo/y}^g@W bɒ%WћwI_MD/E=m߂_${]K ~9yRzA/g@O:#@$ xl0 eCa@A@A8D(=€ AAa@APA€ 0h0&.3H$i Wz~a&EtH$i Wzq-[iLA"HA/?"@ _ gEKjLD"HA,> {ф ҘD"HL>0|>_9 /]&H$ ߭&~5D"H^ tt?MIENDB`pytest-qt-2.3.1/docs/tutorial.rst0000664000372000037200000000500113223543324017653 0ustar travistravis00000000000000Tutorial ======== ``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-2.3.1/docs/reference.rst0000664000372000037200000000105013223543324017746 0ustar travistravis00000000000000Reference ========= 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-2.3.1/docs/make.bat0000664000372000037200000001175613223543324016701 0ustar travistravis00000000000000@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-2.3.1/docs/changelog.rst0000664000372000037200000000010413223543324017736 0ustar travistravis00000000000000 .. _changelog: Changelog ========= .. include:: ../CHANGELOG.rst pytest-qt-2.3.1/docs/virtual_methods.rst0000664000372000037200000000510013223543324021221 0ustar travistravis00000000000000Exceptions 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, both ``PyQt`` and ``PySide`` support overriding this virtual methods naturally in your python code:: class MyWidget(QWidget): # mouseReleaseEvent def mouseReleaseEvent(self, ev): print('mouse released at: %s' % ev.pos()) This works fine, but if python code in Qt virtual methods raise an exception ``PyQt4`` and ``PySide`` will just print the exception traceback to standard error, since this method is called deep within Qt's event loop handling and exceptions are not allowed at that point. In ``PyQt5.5+``, exceptions in virtual methods will by default call ``abort()``, which will crash the interpreter. 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+`` is not recommended unless you install your own exception hook. pytest-qt-2.3.1/docs/note_pyqt4v2.rst0000664000372000037200000000221013223543324020365 0ustar travistravis00000000000000A note about ``pyqt4v2`` ------------------------ Starting with ``pytest-qt`` version 2.0, ``PyQt`` or ``PySide`` are lazily loaded when first needed instead of at pytest startup. This usually means ``pytest-qt`` will import ``PyQt`` or ``PySide`` when the tests actually start running, well after ``conftest.py`` files and other plugins have been imported. This can lead to some unexpected behaviour if ``pyqt4v2`` is set. If the ``conftest.py`` files, either directly or indirectly, set the API version to 2 and import ``PyQt4``, one of the following cases can happen: * if all the available types are set to version 2, then using ``pyqt4`` or ``pyqt4v2`` is equivalent * if only some of the types set to version 2, using ``pyqt4v2`` will make ``pytest`` to fail with an error similar to:: INTERNALERROR> sip.setapi("QDate", 2) INTERNALERROR> ValueError: API 'QDate' has already been set to version 1 If this is the case, use ``pyqt4``. If the API is set in the test functions or in the code imported by them, then the new behaviour is indistinguishable from the old one and ``pyqt4v2`` must be used to avoid errors if version 2 is used. pytest-qt-2.3.1/docs/conf.py0000664000372000037200000001721513223543324016567 0ustar travistravis00000000000000# -*- 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, 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. from pytestqt import version # 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-2.3.1/README.rst0000664000372000037200000001326013223543324016023 0ustar travistravis00000000000000========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PySide`_, `PySide2` and `PyQt`_ 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!' .. _PySide: https://pypi.python.org/pypi/PySide .. _PySide2: https://wiki.qt.io/PySide2 .. _PyQt: http://www.riverbankcomputing.com/software/pyqt .. _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 .. |anaconda| image:: https://anaconda.org/conda-forge/pytest-qt/badges/version.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |travis| image:: https://img.shields.io/travis/pytest-dev/pytest-qt/master.svg :target: https://travis-ci.org/pytest-dev/pytest-qt .. |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 .. |appveyor| image:: https://img.shields.io/appveyor/ci/pytest-dev/pytest-qt/master.svg :target: https://ci.appveyor.com/project/nicoddemus/pytest-qt .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions |python| |version| |anaconda| |travis| |appveyor| |coverage| |docs| 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 ============ Works with either PySide_, PySide2_ or PyQt_ (``PyQt5`` and ``PyQt4``) picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide2`` - ``PyQt5`` - ``PySide`` - ``PyQt4`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt5``, ``pyside``, ``pyside2``, ``pyqt4`` or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` API to `version 2`_. .. 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). .. _version 2: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html 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. :) Running tests ------------- Tests are run using `tox`_. The simplest way to test is with `PySide`_, as it is available on pip and can be installed by ``tox`` automatically:: $ tox -e py34-pyside,py27-pyside,docs If you want to test against `PyQt`_, install it into your global python installation and use the ``py27-pyqt4``, ``py34-pyqt4`` or ``py34-pyqt5`` testing environments, and ``tox`` will copy the appropriate files into its virtual environments to ensure isolation. 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 `_) **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-2.3.1/setup.py0000664000372000037200000000372413223543324016052 0ustar travistravis00000000000000import sys import re from setuptools import setup from setuptools.command.test import test as TestCommand class PyTest(TestCommand): """ Overrides setup "test" command, taken from here: http://pytest.org/latest/goodpractises.html """ def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main([]) sys.exit(errno) setup( name="pytest-qt", packages=['pytestqt'], entry_points={ 'pytest11': ['pytest-qt = pytestqt.plugin'], }, install_requires=['pytest>=2.7.0'], extras_require={'doc': ['sphinx', 'sphinx_rtd_theme']}, # metadata for upload to PyPI author="Bruno Oliveira", author_email="nicoddemus@gmail.com", description='pytest support for PyQt and PySide applications', long_description=open('README.rst').read(), license="MIT", keywords="pytest qt test unittest", url="http://github.com/pytest-dev/pytest-qt", use_scm_version={'write_to': 'pytestqt/_version.py'}, setup_requires=['setuptools_scm'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Pytest', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Desktop Environment :: Window Managers', 'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: User Interfaces', ], tests_require=['pytest'], cmdclass={'test': PyTest}, ) pytest-qt-2.3.1/.gitattributes0000664000372000037200000000003413223543324017222 0ustar travistravis00000000000000CHANGELOG.md merge=union pytest-qt-2.3.1/.pydevproject0000664000372000037200000000073013223543324017051 0ustar travistravis00000000000000 /${PROJECT_DIR_NAME} /${PROJECT_DIR_NAME}/tests python 2.7 Default pytest-qt-2.3.1/pytest_qt.egg-info/0000775000372000037200000000000013223543656020070 5ustar travistravis00000000000000pytest-qt-2.3.1/pytest_qt.egg-info/top_level.txt0000664000372000037200000000001113223543656022612 0ustar travistravis00000000000000pytestqt pytest-qt-2.3.1/pytest_qt.egg-info/requires.txt0000664000372000037200000000005513223543656022470 0ustar travistravis00000000000000pytest>=2.7.0 [doc] sphinx sphinx_rtd_theme pytest-qt-2.3.1/pytest_qt.egg-info/PKG-INFO0000664000372000037200000002006613223543656021171 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: pytest-qt Version: 2.3.1 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 Description-Content-Type: UNKNOWN Description: ========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PySide`_, `PySide2` and `PyQt`_ 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!' .. _PySide: https://pypi.python.org/pypi/PySide .. _PySide2: https://wiki.qt.io/PySide2 .. _PyQt: http://www.riverbankcomputing.com/software/pyqt .. _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 .. |anaconda| image:: https://anaconda.org/conda-forge/pytest-qt/badges/version.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |travis| image:: https://img.shields.io/travis/pytest-dev/pytest-qt/master.svg :target: https://travis-ci.org/pytest-dev/pytest-qt .. |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 .. |appveyor| image:: https://img.shields.io/appveyor/ci/pytest-dev/pytest-qt/master.svg :target: https://ci.appveyor.com/project/nicoddemus/pytest-qt .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions |python| |version| |anaconda| |travis| |appveyor| |coverage| |docs| 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 ============ Works with either PySide_, PySide2_ or PyQt_ (``PyQt5`` and ``PyQt4``) picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide2`` - ``PyQt5`` - ``PySide`` - ``PyQt4`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt5``, ``pyside``, ``pyside2``, ``pyqt4`` or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` API to `version 2`_. .. 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). .. _version 2: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html 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. :) Running tests ------------- Tests are run using `tox`_. The simplest way to test is with `PySide`_, as it is available on pip and can be installed by ``tox`` automatically:: $ tox -e py34-pyside,py27-pyside,docs If you want to test against `PyQt`_, install it into your global python installation and use the ``py27-pyqt4``, ``py34-pyqt4`` or ``py34-pyqt5`` testing environments, and ``tox`` will copy the appropriate files into its virtual environments to ensure isolation. 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 `_) **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 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 :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Desktop Environment :: Window Managers Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: User Interfaces pytest-qt-2.3.1/pytest_qt.egg-info/dependency_links.txt0000664000372000037200000000000113223543656024136 0ustar travistravis00000000000000 pytest-qt-2.3.1/pytest_qt.egg-info/entry_points.txt0000664000372000037200000000005013223543656023361 0ustar travistravis00000000000000[pytest11] pytest-qt = pytestqt.plugin pytest-qt-2.3.1/pytest_qt.egg-info/SOURCES.txt0000664000372000037200000000205413223543656021755 0ustar travistravis00000000000000.gitattributes .gitignore .project .pydevproject .travis.yml CHANGELOG.rst LICENSE README.rst appveyor.yml requirements.txt setup.cfg setup.py tox.ini docs/.gitignore docs/Makefile docs/app_exit.rst docs/changelog.rst docs/conf.py docs/index.rst docs/intro.rst docs/logging.rst docs/make.bat docs/modeltester.rst docs/note_pyqt4v2.rst docs/reference.rst docs/signals.rst docs/tutorial.rst docs/virtual_methods.rst docs/wait_until.rst docs/_static/find_files_dialog.png pytest_qt.egg-info/PKG-INFO pytest_qt.egg-info/SOURCES.txt pytest_qt.egg-info/dependency_links.txt pytest_qt.egg-info/entry_points.txt pytest_qt.egg-info/requires.txt pytest_qt.egg-info/top_level.txt pytestqt/__init__.py pytestqt/_version.py pytestqt/exceptions.py pytestqt/logging.py pytestqt/modeltest.py pytestqt/plugin.py pytestqt/qt_compat.py pytestqt/qtbot.py pytestqt/wait_signal.py scripts/link_pyqt.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-2.3.1/LICENSE0000664000372000037200000000213613223543324015341 0ustar travistravis00000000000000The 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-2.3.1/pytestqt/0000775000372000037200000000000013223543656016237 5ustar travistravis00000000000000pytest-qt-2.3.1/pytestqt/wait_signal.py0000664000372000037200000005626713223543324021122 0ustar travistravis00000000000000import functools from pytestqt.exceptions import TimeoutError from pytestqt.qt_compat import qt_api class _AbstractSignalBlocker(object): """ 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=1000, 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: 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() self._loop.exec_() 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_wait_signal_raising`. :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=1000, raising=True, check_params_cb=None): super(SignalBlocker, self).__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(SignalBlocker, self)._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(QString,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 get_ordinal_str = lambda n: "%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=1000, raising=True, check_params_cbs=None, order="none"): super(MultiSignalBlocker, self).__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] signal_str_to_unique_signal = {} # maps from a signal-string to one of the signal instances (the first one found) 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 self._signals_emitted[index] == False] 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 += " (callback: {})".format(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(MultiSignalBlocker, self)._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(object): """ .. 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("Signal %r unexpectedly emitted" % (self.signal,)) class SignalEmittedError(Exception): """ .. versionadded:: 1.11 The exception thrown by :meth:`pytestqt.qtbot.QtBot.assertNotEmitted` if a signal was emitted unexpectedly. """ 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-2.3.1/pytestqt/exceptions.py0000664000372000037200000000554413223543324020772 0ustar travistravis00000000000000from contextlib import contextmanager import sys import traceback import pytest @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() class _QtExceptionCaptureManager(object): """ 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``. """ def hook(type_, value, tback): self.exceptions.append((type_, value, tback)) sys.stderr.write(format_captured_exceptions([(type_, value, tback)])) self.old_hook = sys.excepthook sys.excepthook = hook 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 pytest.fail(prefix + format_captured_exceptions(exceptions), pytrace=False) def format_captured_exceptions(exceptions): """ Formats exceptions given as (type, value, traceback) into a string suitable to display as a test failure. """ message = 'Qt exceptions in virtual methods:\n' message += '_' * 80 + '\n' for (exc_type, value, tback) in exceptions: message += ''.join(traceback.format_exception(exc_type, value, tback)) + '\n' message += '_' * 80 + '\n' return message def _is_exception_capture_enabled(item): """returns if exception capture is disabled for the given test item. """ disabled = item.get_marker('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. .. note:: In versions prior to ``2.1``, this exception was called ``SignalTimeoutError``. An alias is kept for backward compatibility. """ pass # backward compatibility alias SignalTimeoutError = TimeoutError pytest-qt-2.3.1/pytestqt/qt_compat.py0000664000372000037200000002071713223543324020577 0ustar travistravis00000000000000""" Provide a common way to import Qt classes used by pytest-qt in a unique manner, abstracting API differences between PyQt4, PyQt5, PySide and PySide2. .. 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 __future__ import with_statement, division from collections import namedtuple import os VersionTuple = namedtuple('VersionTuple', 'qt_api, qt_api_version, runtime, compiled') 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 _get_qt_api_from_env(self): api = os.environ.get('PYTEST_QT_API') if api is not None: api = api.lower() if api not in ('pyside', 'pyside2', 'pyqt4', 'pyqt4v2', 'pyqt5'): # pragma: no cover msg = 'Invalid value for $PYTEST_QT_API: %s' raise RuntimeError(msg % api) return api def _guess_qt_api(self): # pragma: no cover def _can_import(name): try: __import__(name) return True except ImportError: return False # Note, not importing only the root namespace because when uninstalling from conda, # the namespace can still be there. if _can_import('PySide2.QtCore'): return 'pyside2' elif _can_import('PyQt5.QtCore'): return 'pyqt5' elif _can_import('PySide.QtCore'): return 'pyside' elif _can_import('PyQt4.QtCore'): return 'pyqt4' 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() if not self.pytest_qt_api: # pragma: no cover msg = 'pytest-qt requires either PySide, PySide2, PyQt4 or PyQt5 to be installed' raise RuntimeError(msg) _root_modules = { 'pyside': 'PySide', 'pyside2': 'PySide2', 'pyqt4': 'PyQt4', 'pyqt4v2': 'PyQt4', '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) if self.pytest_qt_api == 'pyqt4v2': # pragma: no cover # the v2 api in PyQt4 # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html import sip sip.setapi("QDate", 2) sip.setapi("QDateTime", 2) sip.setapi("QString", 2) sip.setapi("QTextStream", 2) sip.setapi("QTime", 2) sip.setapi("QUrl", 2) sip.setapi("QVariant", 2) self.QtCore = QtCore = _import_module('QtCore') self.QtGui = QtGui = _import_module('QtGui') self.QtTest = _import_module('QtTest') self.Qt = QtCore.Qt self.QEvent = QtCore.QEvent self.qDebug = QtCore.qDebug self.qWarning = QtCore.qWarning self.qCritical = QtCore.qCritical self.qFatal = QtCore.qFatal self.QtDebugMsg = QtCore.QtDebugMsg self.QtWarningMsg = QtCore.QtWarningMsg self.QtCriticalMsg = QtCore.QtCriticalMsg self.QtFatalMsg = QtCore.QtFatalMsg # Qt4 and Qt5 have different functions to install a message handler; # the plugin will try to use the one that is not None self.qInstallMsgHandler = None self.qInstallMessageHandler = None if self.pytest_qt_api.startswith('pyside'): self.Signal = QtCore.Signal self.Slot = QtCore.Slot self.Property = QtCore.Property self.QStringListModel = QtGui.QStringListModel self.QStandardItem = QtGui.QStandardItem self.QStandardItemModel = QtGui.QStandardItemModel self.QAbstractListModel = QtCore.QAbstractListModel self.QAbstractTableModel = QtCore.QAbstractTableModel self.QStringListModel = QtGui.QStringListModel if self.pytest_qt_api == 'pyside2': _QtWidgets = _import_module('QtWidgets') self.QApplication = _QtWidgets.QApplication self.QWidget = _QtWidgets.QWidget self.QLineEdit = _QtWidgets.QLineEdit self.qInstallMessageHandler = QtCore.qInstallMessageHandler self.QSortFilterProxyModel = QtCore.QSortFilterProxyModel else: self.QApplication = QtGui.QApplication self.QWidget = QtGui.QWidget self.QLineEdit = QtGui.QLineEdit self.qInstallMsgHandler = QtCore.qInstallMsgHandler self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel def extract_from_variant(variant): """PySide does not expose QVariant API""" return variant def make_variant(value=None): """PySide does not expose QVariant API""" return value self.extract_from_variant = extract_from_variant self.make_variant = make_variant elif self.pytest_qt_api in ('pyqt4', 'pyqt4v2', 'pyqt5'): self.Signal = QtCore.pyqtSignal self.Slot = QtCore.pyqtSlot self.Property = QtCore.pyqtProperty if self.pytest_qt_api == 'pyqt5': _QtWidgets = _import_module('QtWidgets') self.QApplication = _QtWidgets.QApplication self.QWidget = _QtWidgets.QWidget self.qInstallMessageHandler = QtCore.qInstallMessageHandler self.QStringListModel = QtCore.QStringListModel self.QSortFilterProxyModel = QtCore.QSortFilterProxyModel def extract_from_variant(variant): """not needed in PyQt5: Qt API always returns pure python objects""" return variant def make_variant(value=None): """Return a QVariant object from the given Python builtin""" # PyQt4 doesn't allow one to instantiate any QVariant at all: # QVariant represents a mapped type and cannot be instantiated return QtCore.QVariant(value) else: self.QApplication = QtGui.QApplication self.QWidget = QtGui.QWidget self.qInstallMsgHandler = QtCore.qInstallMsgHandler self.QStringListModel = QtGui.QStringListModel self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel def extract_from_variant(variant): """returns python object from the given QVariant""" if isinstance(variant, QtCore.QVariant): return variant.toPyObject() return variant def make_variant(value=None): """Return a QVariant object from the given Python builtin""" # PyQt4 doesn't allow one to instantiate any QVariant at all: # QVariant represents a mapped type and cannot be instantiated return value self.QStandardItem = QtGui.QStandardItem self.QStandardItemModel = QtGui.QStandardItemModel self.QAbstractListModel = QtCore.QAbstractListModel self.QAbstractTableModel = QtCore.QAbstractTableModel self.extract_from_variant = extract_from_variant self.make_variant = make_variant def get_versions(self): if self.pytest_qt_api in ('pyside', 'pyside2'): qt_api_name = 'PySide2' if self.pytest_qt_api == 'pyside2' else 'PySide' if self.pytest_qt_api == 'pyside2': import PySide2 version = PySide2.__version__ else: import PySide version = PySide.__version__ return VersionTuple(qt_api_name, version, self.QtCore.qVersion(), self.QtCore.__version__) else: qt_api_name = 'PyQt5' if self.pytest_qt_api == 'pyqt5' else 'PyQt4' return VersionTuple(qt_api_name, self.QtCore.PYQT_VERSION_STR, self.QtCore.qVersion(), self.QtCore.QT_VERSION_STR) qt_api = _QtApi() pytest-qt-2.3.1/pytestqt/__init__.py0000664000372000037200000000016413223543324020341 0ustar travistravis00000000000000# _version is automatically generated by setuptools_scm from pytestqt._version import version __version__ = version pytest-qt-2.3.1/pytestqt/qtbot.py0000664000372000037200000005625113223543324017743 0ustar travistravis00000000000000import contextlib import functools import weakref from pytestqt.exceptions import SignalTimeoutError, TimeoutError from pytestqt.qt_compat import qt_api from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalEmittedSpy, SignalEmittedError 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(object): """ 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:: stopForInteraction .. 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 keyword 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 miliseconds (if > 0). .. staticmethod:: keyToAscii (key) Auxilliary 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:: mouseEvent (action, widget, button, stateKey, pos[, 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.NoButton``: The button state does not refer to any button (see QMouseEvent.button()). * ``Qt.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.RightButton``: The right button. * ``Qt.MidButton``: The middle button. * ``Qt.MiddleButton``: The middle button. * ``Qt.XButton1``: The first X button. * ``Qt.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 miliseconds (if > 0). .. _QTest API: http://doc.qt.digia.com/4.8/qtest.html """ def __init__(self, request): self._request = request def addWidget(self, widget): """ 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. .. note:: This method is also available as ``add_widget`` (pep-8 alias) """ _add_widget(self._request.node, widget) add_widget = addWidget # pep-8 alias def waitActive(self, widget, timeout=1000): """ 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 function is only available in PyQt5, raising a ``RuntimeError`` if called from ``PyQt4`` or ``PySide``. .. note:: This method is also available as ``wait_active`` (pep-8 alias) """ __tracebackhide__ = True return _WaitWidgetContextManager('qWaitForWindowActive', 'activated', widget, timeout) wait_active = waitActive # pep-8 alias def waitExposed(self, widget, timeout=1000): """ 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 function is only available in PyQt5, raising a ``RuntimeError`` if called from ``PyQt4`` or ``PySide``. .. note:: This method is also available as ``wait_exposed`` (pep-8 alias) """ __tracebackhide__ = True return _WaitWidgetContextManager('qWaitForWindowExposed', 'exposed', widget, timeout) wait_exposed = waitExposed # pep-8 alias 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. :param QWidget widget: Widget to wait on. .. note:: In ``PyQt5`` this function is considered deprecated in favor of :meth:`waitExposed`. .. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias) """ if hasattr(qt_api.QtTest.QTest, 'qWaitForWindowExposed'): return qt_api.QtTest.QTest.qWaitForWindowExposed(widget) else: return qt_api.QtTest.QTest.qWaitForWindowShown(widget) wait_for_window_shown = waitForWindowShown # pep-8 alias def stopForInteraction(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. .. note:: As a convenience, it is also aliased as `stop`. """ 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.QApplication.instance().exec_() for widget, visible in widget_and_visibility: widget.setVisible(visible) stop = stopForInteraction def waitSignal(self, signal=None, timeout=1000, 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``. Set to ``None`` to just use timeout. :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_wait_signal_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:: Cannot have both ``signals`` and ``timeout`` equal ``None``, or else you will block indefinitely. We throw an error if this occurs. .. note:: This method is also available as ``wait_signal`` (pep-8 alias) """ if raising is None: raising_val = self._request.config.getini('qt_wait_signal_raising') if not raising_val: raising = True else: raising = _parse_ini_boolean(raising_val) blocker = SignalBlocker(timeout=timeout, raising=raising, check_params_cb=check_params_cb) if signal is not None: blocker.connect(signal) return blocker wait_signal = waitSignal # pep-8 alias def waitSignals(self, signals=None, timeout=1000, 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``. Set to ``None`` to just use timeout. :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_wait_signal_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:: Cannot have both ``signals`` and ``timeout`` equal ``None``, or else you will block indefinitely. We throw an error if this occurs. .. 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 raising is None: raising = self._request.config.getini('qt_wait_signal_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) if signals is not None: blocker.add_signals(signals) return blocker wait_signals = waitSignals # pep-8 alias 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): """ .. versionadded:: 1.11 Make sure the given ``signal`` doesn't get emitted. 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: yield spy.assert_not_emitted() assert_not_emitted = assertNotEmitted # pep-8 alias def waitUntil(self, callback, timeout=1000): """ .. 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 while True: try: result = callback() except AssertionError: if timed_out(): raise 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 else: assert not timed_out(), 'waitUntil timed out in %s miliseconds' % timeout self.wait(10) wait_until = waitUntil # pep-8 alias @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 @classmethod def _inject_qtest_methods(cls): """ Injects QTest methods into the given class QtBot, so the user can access them directly without having to import QTest. """ def create_qtest_proxy_method(method_name): if hasattr(qt_api.QtTest.QTest, method_name): qtest_method = getattr(qt_api.QtTest.QTest, method_name) def result(*args, **kwargs): return qtest_method(*args, **kwargs) functools.update_wrapper(result, qtest_method) return staticmethod(result) else: return None # pragma: no cover # inject methods from QTest into QtBot method_names = [ 'keyPress', 'keyClick', 'keyClicks', 'keyEvent', 'keyPress', 'keyRelease', 'keyToAscii', 'mouseClick', 'mouseDClick', 'mouseEvent', 'mouseMove', 'mousePress', 'mouseRelease', ] for method_name in method_names: method = create_qtest_proxy_method(method_name) if method is not None: setattr(cls, method_name, method) # provide easy access to exceptions to qtbot fixtures QtBot.SignalTimeoutError = SignalTimeoutError QtBot.SignalEmittedError = SignalEmittedError QtBot.TimeoutError = TimeoutError def _add_widget(item, widget): """ Register a widget into the given pytest item for later closing. """ qt_widgets = getattr(item, 'qt_widgets', []) qt_widgets.append(weakref.ref(widget)) 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 in item.qt_widgets: w = w() if w is not None: w.close() w.deleteLater() del item.qt_widgets def _iter_widgets(item): """ Iterates over widgets registered in the given pytest item. """ return iter(getattr(item, 'qt_widgets', [])) class _WaitWidgetContextManager(object): """ 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 if qt_api.pytest_qt_api != 'pyqt5': raise RuntimeError('Available in PyQt5 only') 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-2.3.1/pytestqt/_version.py0000664000372000037200000000016413223543656020436 0ustar travistravis00000000000000# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = '2.3.1' pytest-qt-2.3.1/pytestqt/plugin.py0000664000372000037200000001444413223543324020106 0ustar travistravis00000000000000import pytest from pytestqt.exceptions import capture_exceptions, format_captured_exceptions, \ _is_exception_capture_enabled, _QtExceptionCaptureManager, SignalTimeoutError from pytestqt.logging import QtLoggingPlugin, _QtMessageCapture, Record from pytestqt.qt_compat import qt_api from pytestqt.qtbot import QtBot, _close_widgets from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker # classes/functions imported here just for backward compatibility before we # split the implementation of this file in several modules assert QtBot assert SignalBlocker assert MultiSignalBlocker assert SignalTimeoutError assert Record assert capture_exceptions assert format_captured_exceptions @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.yield_fixture(scope='session') def qapp(qapp_args): """ 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.QApplication.instance() if app is None: global _qapp_instance _qapp_instance = qt_api.QApplication(qapp_args) yield _qapp_instance else: yield 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.yield_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: "pyside", "pyqt4", "pyqt4v2", "pyqt5"') parser.addini('qt_no_exception_capture', 'disable automatic exception capture') parser.addini('qt_wait_signal_raising', 'Default value for the raising parameter of qtbot.waitSignal') default_log_fail = QtLoggingPlugin.LOG_FAIL_OPTIONS[0] parser.addini('qt_log_level_fail', 'log level in which tests can fail: {0} (default: "{1}")' .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.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.') if config.getoption('qt_log'): config.pluginmanager.register(QtLoggingPlugin(config), '_qt_logging') qt_api.set_qt_api(config.getini('qt_api')) from .qtbot import QtBot QtBot._inject_qtest_methods() def pytest_report_header(): from pytestqt.qt_compat import qt_api v = qt_api.get_versions() fields = [ '%s %s' % (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-2.3.1/pytestqt/logging.py0000664000372000037200000002506413223543324020236 0ustar travistravis00000000000000from 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 class QtLoggingPlugin(object): """ 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'] def __init__(self, config): self.config = config def pytest_runtest_setup(self, item): if item.get_marker('no_qt_log'): return m = item.get_marker('qt_log_ignore') if m: if not set(m.kwargs).issubset(set(['extend'])): raise ValueError("Invalid keyword arguments in {0!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 = item.get_marker('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 log_fail_level != 'NO' and report.outcome != 'failed': for rec in item.qt_log_capture.records: if rec.matches_level(log_fail_level) and not rec.ignored: report.outcome = 'failed' if report.longrepr is None: report.longrepr = \ _QtLogLevelErrorRepr(item, log_fail_level) 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') if log_format is None: if qt_api.pytest_qt_api == 'pyqt5': log_format = '{rec.context.file}:{rec.context.function}:' \ '{rec.context.line}:\n {rec.type_name}: {rec.message}' else: log_format = '{rec.type_name}: {rec.message}' lines = [] for rec in item.qt_log_capture.records: suffix = ' (IGNORED)' if rec.ignored else '' 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(object): """ Captures Qt messages when its `handle` method is installed using qInstallMsgHandler, 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. """ if qt_api.qInstallMsgHandler: previous_handler = qt_api.qInstallMsgHandler(self._handle_no_context) else: assert qt_api.qInstallMessageHandler previous_handler = qt_api.qInstallMessageHandler(self._handle_with_context) self._previous_handler = previous_handler def _stop(self): """ Stop receiving messages from Qt, restoring the previously installed handler. """ if qt_api.qInstallMsgHandler: qt_api.qInstallMsgHandler(self._previous_handler) else: assert qt_api.qInstallMessageHandler qt_api.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') 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, ) self._records.append(Record(msg_type, message, ignored, context)) def _handle_no_context(self, msg_type, message): """ Method to be installed using qInstallMsgHandler (Qt4), stores each message into the `_records` attribute. """ self._append_new_record(msg_type, message, context=None) def _handle_with_context(self, msg_type, context, message): """ Method to be installed using qInstallMessageHandler (Qt5), 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(object): """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: ``"QtDebugMsg"``, ``"QtWarningMsg"`` or ``"QtCriticalMsg"``. :ivar str log_type_name: type name similar to the logging package: ``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``. Only available in Qt5, otherwise is None. """ 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.QtDebugMsg: 'QtDebugMsg', qt_api.QtWarningMsg: 'QtWarningMsg', qt_api.QtCriticalMsg: 'QtCriticalMsg', qt_api.QtFatalMsg: 'QtFatalMsg', } 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.QtDebugMsg: 'DEBUG', qt_api.QtWarningMsg: 'WARNING', qt_api.QtCriticalMsg: 'CRITICAL', qt_api.QtFatalMsg: 'FATAL', } return cls._log_type_name_map[msg_type] def matches_level(self, level): assert level in QtLoggingPlugin.LOG_FAIL_OPTIONS if 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('log_fail_level unknown: {0}'.format(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): 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-2.3.1/pytestqt/modeltest.py0000664000372000037200000007026713223543324020615 0ustar travistravis00000000000000# This file is based on the original C++ modeltest.cpp from: # http://code.qt.io/cgit/qt/qtbase.git/tree/tests/auto/other/modeltest/modeltest.cpp # Licensed under the following terms: # # Copyright (C) 2015 The Qt Company Ltd. # Contact: http://www.qt.io/licensing/ # # This file is part of the test suite of the Qt Toolkit. # # $QT_BEGIN_LICENSE:LGPL21$ # 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 http://www.qt.io/terms-conditions. For further # information use the contact form at http://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 2.1 or version 3 as published by the Free # Software Foundation and appearing in the file LICENSE.LGPLv21 and # LICENSE.LGPLv3 included in the packaging of this file. Please review the # following information to ensure the GNU Lesser General Public License # requirements will be met: https://www.gnu.org/licenses/lgpl.html and # http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. # # As a special exception, The Qt Company gives you certain additional # rights. These rights are described in The Qt Company LGPL Exception # version 1.1, included in the file LGPL_EXCEPTION.txt in this package. # # $QT_END_LICENSE$ from __future__ import print_function import collections from pytestqt.qt_compat import qt_api _Changing = collections.namedtuple('_Changing', 'parent, old_size, last, next') class ModelTester: """A tester for Qt's QAbstractItemModels. :ivar bool data_display_may_return_none: if the model implementation is allowed to return None from data() for DisplayRole. """ def __init__(self, config): self._model = None self._fetching_more = None self._insert = None self._remove = None self._changing = [] self.data_display_may_return_none = False def _debug(self, text): print('modeltest: ' + text) def _modelindex_debug(self, index): """Get a string for debug output for a QModelIndex.""" if not index.isValid(): return ' (0x{:x})'.format(id(index)) else: data = self._model.data(index, qt_api.QtCore.Qt.DisplayRole) return '{}/{} {!r} (0x{:x})'.format( index.row(), index.column(), qt_api.extract_from_variant(data), id(index)) def check(self, model): """Runs a series of checks in the given model. Connect to all of the models signals. Whenever anything happens recheck everything. """ assert model is not None 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() self._test_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 self._model.buddy(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() self._model.canFetchMore(qt_api.QtCore.QModelIndex()) assert self._column_count(qt_api.QtCore.QModelIndex()) >= 0 display_data = self._model.data(qt_api.QtCore.QModelIndex(), qt_api.QtCore.Qt.DisplayRole) assert qt_api.extract_from_variant(display_data) is None self._fetch_more(qt_api.QtCore.QModelIndex()) flags = self._model.flags(qt_api.QtCore.QModelIndex()) assert flags == qt_api.QtCore.Qt.ItemIsDropEnabled or not flags self._has_children(qt_api.QtCore.QModelIndex()) self._model.hasIndex(0, 0) self._model.headerData(0, qt_api.QtCore.Qt.Horizontal) self._model.index(0, 0) self._model.itemData(qt_api.QtCore.QModelIndex()) cache = None self._model.match(qt_api.QtCore.QModelIndex(), -1, cache) self._model.mimeTypes() assert self._parent(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() assert self._model.rowCount() >= 0 self._model.setData(qt_api.QtCore.QModelIndex(), None, -1) self._model.setHeaderData(-1, qt_api.QtCore.Qt.Horizontal, None) self._model.setHeaderData(999999, qt_api.QtCore.Qt.Horizontal, None) self._model.sibling(0, 0, qt_api.QtCore.QModelIndex()) self._model.span(qt_api.QtCore.QModelIndex()) self._model.supportedDropActions() def _test_row_count(self): """Test model's implementation of rowCount() 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 if rows > 0: assert self._has_children(top_index) second_level_index = self._model.index(0, 0, top_index) if second_level_index.isValid(): # not the top level # check a row count where parent is valid rows = self._model.rowCount(second_level_index) assert rows >= 0 if rows > 0: assert self._has_children(second_level_index) def _test_column_count(self): """Test model's implementation of columnCount() and hasChildren(). columnCount() 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()) assert self._column_count(top_index) >= 0 # check a column count where parent is valid child_index = self._model.index(0, 0, top_index) if child_index.isValid(): assert self._column_count(child_index) >= 0 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: 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. """ # Make sure that invalid values return an invalid index assert self._model.index(-2, -2) == qt_api.QtCore.QModelIndex() assert self._model.index(-2, 0) == qt_api.QtCore.QModelIndex() assert self._model.index(0, -2) == qt_api.QtCore.QModelIndex() rows = self._model.rowCount() columns = self._column_count() if rows == 0: return # Catch off by one errors assert self._model.index(rows, columns) == qt_api.QtCore.QModelIndex() assert self._model.index(0, 0).isValid() # Make sure that the same index is *always* returned a = self._model.index(0, 0) b = self._model.index(0, 0) 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 self._parent(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() if self._model.rowCount() == 0: 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 self._parent(top_index) == qt_api.QtCore.QModelIndex() # Common error test #2, make sure that a second level index has a # parent that is the first level index. if self._model.rowCount(top_index) > 0: 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. top_index_1 = self._model.index(0, 1, qt_api.QtCore.QModelIndex()) if self._model.rowCount(top_index_1) > 0: child_index = self._model.index(0, 0, top_index) child_index_1 = self._model.index(0, 0, top_index_1) 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: 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 + 1, 0, parent) for r in range(rows): if self._model.canFetchMore(parent): self._fetch_more(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... 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 # Make sure we get the same index if we request it twice in a # row a = self._model.index(r, c, parent) b = self._model.index(r, c, parent) assert a == b sibling = self._model.sibling(r, c, top_left_child) assert index == sibling sibling = top_left_child.sibling(r, c) assert index == sibling # Some basic checking on the index that is returned assert index.model() == self._model assert index.row() == r assert index.column() == c data = self._model.data(index, qt_api.QtCore.Qt.DisplayRole) if not self.data_display_may_return_none: assert qt_api.extract_from_variant(data) is not None # If the next test fails here is some somewhat useful debug you # play with. if self._parent(index) != parent: self._debug( "parent-check failed for index {}:\n" " parent {} != expected {}".format( self._modelindex_debug(index), self._modelindex_debug(self._parent(index)), self._modelindex_debug(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()""" # Invalid index should return an invalid qvariant value = self._model.data(qt_api.QtCore.QModelIndex(), qt_api.QtCore.Qt.DisplayRole) assert qt_api.extract_from_variant(value) is None if self._model.rowCount() == 0: return # A valid index should have a valid QVariant data assert self._model.index(0, 0).isValid() # shouldn't be able to set data on an invalid index ok = self._model.setData(qt_api.QtCore.QModelIndex(), "foo", qt_api.QtCore.Qt.DisplayRole) assert not ok types = [ (qt_api.QtCore.Qt.ToolTipRole, str), (qt_api.QtCore.Qt.StatusTipRole, str), (qt_api.QtCore.Qt.WhatsThisRole, str), (qt_api.QtCore.Qt.SizeHintRole, qt_api.QtCore.QSize), (qt_api.QtCore.Qt.FontRole, qt_api.QtGui.QFont), (qt_api.QtCore.Qt.BackgroundColorRole, (qt_api.QtGui.QColor, qt_api.QtGui.QBrush)), (qt_api.QtCore.Qt.TextColorRole, (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 == None or isinstance(data, typ), role # Check that the alignment is one we know about alignment = self._model.data(self._model.index(0, 0), qt_api.QtCore.Qt.TextAlignmentRole) alignment = qt_api.extract_from_variant(alignment) 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.AlignHorizontal_Mask | qt_api.QtCore.Qt.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.CheckStateRole) assert state in [None, qt_api.QtCore.Qt.Unchecked, qt_api.QtCore.Qt.PartiallyChecked, qt_api.QtCore.Qt.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) next_data = self._model.data(next_index) 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)) next_data = self._model.data(self._model.index(end + 1, 0, c.parent)) expected_size = c.old_size + (end - start + 1) current_size = self._model.rowCount(parent) self._debug("rows inserted: start {}, end {}".format(start, 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, qt_api.extract_from_variant(c.next), qt_api.extract_from_variant(c.last) ) ) self._debug(" now in rowsInserted: parent {}, size {}, " "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, qt_api.extract_from_variant(next_data), qt_api.extract_from_variant(last_data) ) ) if not qt_api.QtCore.qVersion().startswith('4.'): # Skipping this on Qt4 as the parent changes for some reason: # modeltest: rows about to be inserted: [...] # parent (0x7f8f540eacf8), [...] # [...] # modeltest: from rowsAboutToBeInserted: # parent 0/0 None (0x7f8f540eacf8), [...] # modeltest: now in rowsInserted: # parent (0x7f8f60a96cf8) [...] 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 assert c.last == last_data 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. """ last_index = self._model.index(start - 1, 0, parent) next_index = self._model.index(end + 1, 0, parent) parent_rowcount = self._model.rowCount(parent) 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), ) ) last_data = self._model.data(last_index) next_data = 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)) next_data = self._model.data(self._model.index(start, 0, c.parent)) current_size = self._model.rowCount(parent) expected_size = c.old_size - (end - start + 1) self._debug("rows removed: start {}, end {}".format(start, 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, qt_api.extract_from_variant(c.next), qt_api.extract_from_variant(c.last) ) ) self._debug(" now in rowsRemoved: parent {}, size {}, " "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, qt_api.extract_from_variant(next_data), qt_api.extract_from_variant(last_data) ) ) if not qt_api.QtCore.qVersion().startswith('4.'): # Skipping this on Qt4 as the parent changes for some reason # see _on_rows_inserted for details assert c.parent == parent assert current_size == expected_size assert c.last == last_data 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.Horizontal, qt_api.QtCore.Qt.Vertical] assert start >= 0 assert end >= 0 assert start <= end if orientation == qt_api.QtCore.Qt.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.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.QAbstractListModel, qt_api.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.QAbstractListModel, qt_api.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-2.3.1/.project0000664000372000037200000000055313223543324016004 0ustar travistravis00000000000000 pytest-qt org.python.pydev.PyDevBuilder org.python.pydev.pythonNature pytest-qt-2.3.1/appveyor.yml0000664000372000037200000000511413223543324016723 0ustar travistravis00000000000000# Adapted from the qtpy Appveyor setup environment: global: PYTHON: "C:\\conda" CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci-helpers\\appveyor\\windows_sdk.cmd" PYTHON_ARCH: "64" # needs to be set for CMD_IN_ENV to succeed. If a mix # of 32 bit and 64 bit builds are needed, move this # to the matrix section. # Used by atropy ci-helpers CONDA_CHANNELS: "conda-forge" matrix: - PYTHON_VERSION: "2.7" PYTEST_QT_API: "pyqt4" CONDA_DEPENDENCIES: "pytest pyqt=4.*" - PYTHON_VERSION: "2.7" PYTEST_QT_API: "pyqt4v2" CONDA_DEPENDENCIES: "pytest pyqt=4.*" - PYTHON_VERSION: "2.7" PYTEST_QT_API: "pyside" CONDA_DEPENDENCIES: "pytest pyside=1.*" - PYTHON_VERSION: "3.4" PYTEST_QT_API: "pyqt4" CONDA_DEPENDENCIES: "pytest pyqt=4.*" - PYTHON_VERSION: "3.4" PYTEST_QT_API: "pyqt4v2" CONDA_DEPENDENCIES: "pytest pyqt=4.*" - PYTHON_VERSION: "3.4" PYTEST_QT_API: "pyside" CONDA_DEPENDENCIES: "pytest pyside=1.*" - PYTHON_VERSION: "3.5" PYTEST_QT_API: "pyqt5" CONDA_DEPENDENCIES: "pytest pyqt=5.*" - PYTHON_VERSION: "3.5" PYTEST_QT_API: "pyside2" CONDA_DEPENDENCIES: "pytest pyside2=2.*" - PYTHON_VERSION: "3.6" PYTEST_QT_API: "pyqt5" CONDA_DEPENDENCIES: "pytest pyqt=5.*" - PYTHON_VERSION: "3.6" PYTEST_QT_API: "pyside2" CONDA_DEPENDENCIES: "pytest pyside2=2.*" platform: -x64 install: # If there is a newer build queued for the same PR, cancel this one. # The AppVeyor 'rollout builds' option is supposed to serve the same # purpose but it is problematic because it tends to cancel builds pushed # directly to master instead of just PR builds (or the converse). # credits: JuliaLang developers. - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` throw "There are newer queued builds for this pull request, failing early." } - "git clone --depth 1 git://github.com/astropy/ci-helpers.git" - "powershell ci-helpers/appveyor/install-miniconda.ps1" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "activate test" - "pip install -e ." # Not a .NET project, we build in the install step instead build: false test_script: - "%CMD_IN_ENV% python -m pytest -v tests/" pytest-qt-2.3.1/tox.ini0000664000372000037200000000173413223543324015652 0ustar travistravis00000000000000[tox] # note that tox expects interpreters to be found at C:\PythonXY, # with XY being python version ("27" or "34") for instance envlist = py{27,34}-pyqt4, py{34,35}-pyqt5, py{27,34,35}-pyside, lint [testenv] deps=pytest pyside: pyside changedir=tests commands= pyqt5: {envpython} ../scripts/link_pyqt.py --tox {envdir} 5 pyqt4: {envpython} ../scripts/link_pyqt.py --tox {envdir} 4 {envpython} -m pytest {posargs} setenv= pyside: PYTEST_QT_API=pyside pyqt4: PYTEST_QT_API=pyqt4 pyqt4v2: PYTEST_QT_API=pyqt4v2 pyqt5: PYTEST_QT_API=pyqt5 pyqt5: QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms passenv=DISPLAY XAUTHORITY USER USERNAME [testenv:lint] basepython=python3.4 usedevelop=true deps= pytest sphinx sphinx_rtd_theme restructuredtext_lint changedir=docs setenv= READTHEDOCS=True commands= rst-lint {toxinidir}/CHANGELOG.rst {toxinidir}/README.rst sphinx-build -q -E -W -b html . _build pytest-qt-2.3.1/.gitignore0000664000372000037200000000032713223543324016324 0ustar travistravis00000000000000*.pyc __pycache__ *.log /distribute-0.6.35.tar.gz /distribute-0.6.35-py2.7.egg /pytest_qt.egg-info /build /dist /.tox .env* .coverage /.cache /.venv /.eggs # auto-generated by setuptools_scm /pytestqt/_version.py pytest-qt-2.3.1/.travis.yml0000664000372000037200000000371013223543324016444 0ustar travistravis00000000000000language: generic sudo: required dist: trusty env: global: # used by ci-helpers - CONDA_CHANNELS=conda-forge SETUP_XVFB=true DEPS="pytest tox coveralls six" matrix: - PYTEST_QT_API=pyqt4 PYQT_PACKAGE="pyqt=4.*" PYTHON_VERSION=2.7 - PYTEST_QT_API=pyqt4v2 PYQT_PACKAGE="pyqt=4.*" PYTHON_VERSION=2.7 - PYTEST_QT_API=pyside PYQT_PACKAGE="pyside=1.*" PYTHON_VERSION=2.7 - PYTEST_QT_API=pyqt4 PYQT_PACKAGE="pyqt=4.*" PYTHON_VERSION=3.4 - PYTEST_QT_API=pyqt4v2 PYQT_PACKAGE="pyqt=4.*" PYTHON_VERSION=3.4 - PYTEST_QT_API=pyside PYQT_PACKAGE="pyside=1.*" PYTHON_VERSION=3.4 - PYTEST_QT_API=pyqt5 PYQT_PACKAGE="pyqt=5.*" PYTHON_VERSION=3.5 - PYTEST_QT_API=pyside2 PYQT_PACKAGE="pyside2=2.*" PYTHON_VERSION=3.5 - PYTEST_QT_API=pyqt5 PYQT_PACKAGE="pyqt=5.*" PYTHON_VERSION=3.6 - PYTEST_QT_API=pyside2 PYQT_PACKAGE="pyside2=2.*" PYTHON_VERSION=3.6 install: - sudo apt-get update # Xvfb / window manager - sudo apt-get install -y xvfb herbstluftwm # Setup miniconda - git clone --depth 1 git://github.com/astropy/ci-helpers.git - CONDA_DEPENDENCIES="${DEPS} ${PYQT_PACKAGE}" source ci-helpers/travis/setup_conda.sh - source activate test && pip install -e . before_script: - "herbstluftwm &" - sleep 1 script: - source activate test && catchsegv coverage run --source=pytestqt -m pytest -v tests # for some reason tox doesn't get installed with a u+x flag - | chmod u+x /home/travis/miniconda/envs/test/bin/tox /home/travis/miniconda/envs/test/bin/tox -e lint after_success: - coveralls deploy: provider: pypi skip_upload_docs: true user: nicoddemus distributions: sdist bdist_wheel password: secure: Fql0uLFWz+D6p36BUm+2WB5tjUiuCkOFpU68Dnycn5cIRqZQuBzfNNZPVGHgIUGNZ5rC+rJC+edEvJoF2AB9cQ7kz86SzZuQHdEOwsnR4KQw2tkD5fw2BqH4bQ56SQ+UHAsyQAuDkjShHTrwSTJrzShIE5Poma5BqmqC4mRSgbI= on: tags: true repo: pytest-dev/pytest-qt condition: $PYTEST_QT_API = pyqt5 pytest-qt-2.3.1/setup.cfg0000664000372000037200000000010313223543656016155 0ustar travistravis00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 pytest-qt-2.3.1/tests/0000775000372000037200000000000013223543656015504 5ustar travistravis00000000000000pytest-qt-2.3.1/tests/test_wait_signal.py0000664000372000037200000013106613223543324021415 0ustar travistravis00000000000000import functools import fnmatch import pytest import sys from pytestqt.qt_compat import qt_api from pytestqt.wait_signal import SignalEmittedError, TimeoutError, SignalAndArgs def test_signal_blocker_exception(qtbot): """ Make sure waitSignal without signals and timeout doesn't hang, but raises ValueError instead. """ with pytest.raises(ValueError): qtbot.waitSignal(None, None).wait() with pytest.raises(ValueError): qtbot.waitSignals([], None).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, 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, raising=raising) as blocker: pass else: with func(signal, 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), ]) ) 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, 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('configval, raises', [ ('false', False), ('true', True), (None, True), ]) def test_raising(qtbot, testdir, configval, raises): if configval is not None: testdir.makeini(""" [pytest] qt_wait_signal_raising = {} """.format(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_wait_signal_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), ]) ) 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, 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('multiple', [True, False]) @pytest.mark.parametrize('raising', [True, False]) def test_wait_signals_handles_exceptions(qtbot, multiple, raising, signaller): """ Make sure waitSignal handles exceptions correctly. """ class TestException(Exception): pass if multiple: func = qtbot.waitSignals arg = [signaller.signal, signaller.signal_2] else: func = qtbot.waitSignal arg = signaller.signal with pytest.raises(TestException): with func(arg, 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). For some reason, this crashes PySide although it seems perfectly fine code. """ if qt_api.pytest_qt_api.startswith('pyside'): pytest.skip('test crashes PySide and PySide2') import sip class Obj(qt_api.QtCore.QObject): pass obj = Obj() with qtbot.waitSignal(obj.destroyed): obj.deleteLater() assert sip.isdeleted(obj) 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): """If there's a timeout, the args attribute is None.""" with qtbot.waitSignal(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). Some Qt frameworks, such as PyQt4 or PyQt5, have 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 PySide, where the signal names cannot be determined at run-time). """ if qt_api.pytest_qt_api.startswith('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 != 'pyside': pytest.skip("test only makes sense for PySide, 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)) ] PY_2 = sys.version_info[0] == 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.pytest_qt_api.startswith('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_unable_to_get_callback_name(self, qtbot, signaller): """ Test that for complicated callbacks which aren't callables, but e.g. double-wrapped partials, the test code is sometimes unable to determine the name of the callback. Note that this behavior changes with Python 3.5, where a functools.partial() is smart enough to detect wrapped calls. """ if sys.version_info >= (3,5): pytest.skip("Only on Python 3.4 and lower double-wrapped functools.partial callbacks are a problem") if qt_api.pytest_qt_api.startswith('pyside'): signal = (signaller.signal_single_arg, "signal_single_arg(int)") else: signal = signaller.signal_single_arg def callback(int_param, unused_param1, unused_param2): return int_param == 1337 wrapped_callback = functools.partial(callback, unused_param2=1) double_wrapped_callback = functools.partial(wrapped_callback, unused_param1=1) with pytest.raises(TimeoutError) as excinfo: with qtbot.waitSignal(signal=signal, timeout=200, raising=True, check_params_cb=double_wrapped_callback): signaller.signal_single_arg.emit(1) ex_msg = TestWaitSignalsTimeoutErrorMessage.get_exception_message(excinfo) assert ex_msg == ("Signal signal_single_arg(int) emitted with parameters [1] within 200 ms, " "but did not satisfy the callback") 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.pytest_qt_api.startswith('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.pytest_qt_api.startswith('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)]" if PY_2: parameters = "[(u'1', 1), (u'2', 2)]" if qt_api.pytest_qt_api == 'pyqt4': parameters = "[(PyQt4.QtCore.QString(u'1'), 1), (PyQt4.QtCore.QString(u'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" if PY_2: signal_args = "u'1', 1" if qt_api.pytest_qt_api == 'pyqt4': signal_args = "PyQt4.QtCore.QString(u'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" if PY_2: signal_args = "u'1', 1" if qt_api.pytest_qt_api == 'pyqt4': signal_args = "PyQt4.QtCore.QString(u'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" if PY_2: signal_args = "u'1', 1" if qt_api.pytest_qt_api == 'pyqt4': signal_args = "PyQt4.QtCore.QString(u'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 PySide 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 not qt_api.pytest_qt_api.startswith('pyside'): 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() pytest-qt-2.3.1/tests/test_wait_until.py0000664000372000037200000000314513223543324021267 0ustar travistravis00000000000000import pytest def test_wait_until(qtbot, wait_4_ticks_callback, tick_counter): tick_counter.start(100) qtbot.waitUntil(wait_4_ticks_callback, 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(AssertionError): qtbot.waitUntil(wait_4_ticks_callback, 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.yield_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-2.3.1/tests/test_qtest_proxies.py0000664000372000037200000000132013223543324022012 0ustar travistravis00000000000000import pytest # noinspection PyUnresolvedReferences from pytestqt.qt_compat import qt_api fails_on_pyqt = pytest.mark.xfail('not qt_api.pytest_qt_api.startswith("pyside")') @pytest.mark.parametrize('expected_method', [ 'keyPress', 'keyClick', 'keyClicks', 'keyEvent', 'keyPress', 'keyRelease', fails_on_pyqt('keyToAscii'), 'mouseClick', 'mouseDClick', 'mouseEvent', 'mouseMove', 'mousePress', 'mouseRelease', ], ) def test_expected_qtest_proxies(qtbot, expected_method): """ Ensure that we are exporting expected QTest API methods. """ assert hasattr(qtbot, expected_method) assert getattr(qtbot, expected_method).__name__ == expected_method pytest-qt-2.3.1/tests/test_logging.py0000664000372000037200000003467113223543324020546 0ustar travistravis00000000000000import datetime import pytest from pytestqt.qt_compat import qt_api pytestmark = pytest.mark.skipif(qt_api.pytest_qt_api == 'pyside2', reason="https://bugreports.qt.io/browse/PYSIDE-435") @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 if qt_api.qInstallMessageHandler: def print_msg(msg_type, context, message): sys.stderr.write(to_unicode(message) + '\\n') qt_api.qInstallMessageHandler(print_msg) else: def print_msg(msg_type, message): sys.stderr.write(to_unicode(message) + '\\n') qt_api.qInstallMsgHandler(print_msg) def test_types(): 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 {0} """.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 --*', '*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 --*', 'this is a DEBUG message*', 'this is a WARNING message*', 'this is a CRITICAL message*', ]) def test_qtlog_fixture(qtlog): """ Test qtlog fixture. """ 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.QtDebugMsg, 'this is a DEBUG message'), (qt_api.QtWarningMsg, 'this is a WARNING message'), (qt_api.QtCriticalMsg, 'this is a CRITICAL message'), ] # `records` attribute is read-only with pytest.raises(AttributeError): qtlog.records = [] def test_fixture_with_logging_disabled(testdir): """ 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('--no-qt-log') 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('--qt-log-format={0}'.format(f)) today = '{0:%Y-%m-%d}'.format(datetime.datetime.now()) res.stdout.fnmatch_lines([ '*-- Captured Qt messages --*', 'QtWarningMsg WARNING {0}: this is a WARNING message*'.format(today), ]) @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 {0} or above emitted*'.format( level.upper()), '*-- Captured Qt messages --*', ]) lines.append('*{0} passed*'.format(expect_passes)) 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.pytest_qt_api == 'pyqt5': 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*') def test_context_none(testdir): """ Sometimes PyQt5 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. :type testdir: _pytest.pytester.TmpTestdir """ if qt_api.pytest_qt_api != 'pyqt5': pytest.skip('Context information only available in PyQt5') 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, None) log_capture._handle_with_context(qt_api.QtWarningMsg, context, "WARNING message") assert 0 """ ) res = testdir.runpytest() res.stdout.fnmatch_lines([ '*None:None:None:*', '* 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-2.3.1/tests/test_modeltest.py0000664000372000037200000002016113223543324021105 0ustar travistravis00000000000000import pytest from pytestqt.qt_compat import qt_api pytestmark = pytest.mark.usefixtures('qtbot') class BasicModel(qt_api.QtCore.QAbstractItemModel): def data(self, index, role=qt_api.QtCore.Qt.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.QStandardItemModel. """ model = qt_api.QStandardItemModel() items = [qt_api.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) def test_string_list_model(qtmodeltester): model = qt_api.QStringListModel() model.setStringList(['hello', 'world']) qtmodeltester.check(model) def test_sort_filter_proxy_model(qtmodeltester): model = qt_api.QStringListModel() model.setStringList(['hello', 'world']) proxy = qt_api.QSortFilterProxyModel() proxy.setSourceModel(model) qtmodeltester.check(proxy) @pytest.mark.parametrize('broken_role', [ qt_api.QtCore.Qt.ToolTipRole, qt_api.QtCore.Qt.StatusTipRole, qt_api.QtCore.Qt.WhatsThisRole, qt_api.QtCore.Qt.SizeHintRole, qt_api.QtCore.Qt.FontRole, qt_api.QtCore.Qt.BackgroundColorRole, qt_api.QtCore.Qt.TextColorRole, qt_api.QtCore.Qt.TextAlignmentRole, qt_api.QtCore.Qt.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.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.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.AlignLeft, True), (qt_api.QtCore.Qt.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.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.DisplayRole): if role == qt_api.QtCore.Qt.TextAlignmentRole: return role_value elif role == qt_api.QtCore.Qt.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.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.Vertical, 0, 0) self.headerDataChanged.emit(qt_api.QtCore.Qt.Horizontal, 0, 0) def headerData(self, section, orientation, role=qt_api.QtCore.Qt.DisplayRole): return self._header_text def data(self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.DisplayRole): if role == qt_api.QtCore.Qt.DisplayRole and index == self.index(0, 0): return 'Contents' return None model = MyModel() model.set_header_text('Start Header') check_model(model, should_pass=1) 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) else: with pytest.raises(AssertionError): qtmodeltester.check(model) 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) def test_changing_model_insert(qtmodeltester): model = qt_api.QStandardItemModel() item = qt_api.QStandardItem('foo') qtmodeltester.check(model) model.insertRow(0, item) def test_changing_model_remove(qtmodeltester): model = qt_api.QStandardItemModel() item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.removeRow(0) def test_changing_model_data(qtmodeltester): model = qt_api.QStandardItemModel() item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.setData(model.index(0, 0), 'hello world') @pytest.mark.parametrize('orientation', [qt_api.QtCore.Qt.Horizontal, qt_api.QtCore.Qt.Vertical]) def test_changing_model_header_data(qtmodeltester, orientation): model = qt_api.QStandardItemModel() item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.setHeaderData(0, orientation, 'blah') def test_changing_model_sort(qtmodeltester): """Sorting emits layoutChanged""" model = qt_api.QStandardItemModel() item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) 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(Model, self).__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) assert model.row_count_did_run def test_fetch_more(qtmodeltester): class Model(qt_api.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.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) def test_invalid_parent(qtmodeltester): class Model(qt_api.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.QStandardItem('foo') item2 = qt_api.QStandardItem('bar') item3 = qt_api.QStandardItem('bar') model.setItem(0, 0, item) item.setChild(0, item2) item2.setChild(0, item3) with pytest.raises(AssertionError): qtmodeltester.check(model) pytest-qt-2.3.1/tests/test_exceptions.py0000664000372000037200000002153113223543324021270 0ustar travistravis00000000000000from pytestqt.exceptions import capture_exceptions, format_captured_exceptions import pytest import sys @pytest.mark.parametrize('raise_error', [False, True]) def test_catch_exceptions_in_virtual_methods(testdir, raise_error): """ Catch exceptions that happen inside Qt virtual methods 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}: raise ValueError('mistakes were made') return qt_api.QtCore.QObject.event(self, ev) def test_exceptions(qtbot): v = Receiver() app = qt_api.QApplication.instance() app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.User)) app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.User)) app.processEvents() '''.format(raise_error=raise_error)) result = testdir.runpytest() if raise_error: result.stdout.fnmatch_lines([ '*Qt exceptions in virtual methods:*', '*ValueError: mistakes were made*', '*1 failed*', ]) 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 'Qt exceptions in virtual methods:' in lines assert 'ValueError: errors were made' 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 QWidget = qt_api.QWidget QtCore = qt_api.QtCore # PyQt 5.5+ will crash if there's no custom exception handler installed sys.excepthook = lambda *args: None class MyWidget(QWidget): def mouseReleaseEvent(self, ev): raise RuntimeError {marker_code} def test_widget(qtbot): w = MyWidget() qtbot.addWidget(w) qtbot.mouseClick(w, QtCore.Qt.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 QWidget = qt_api.QWidget QtCore = qt_api.QtCore 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 QWidget = qt_api.QWidget QtCore = qt_api.QtCore QEvent = qt_api.QtCore.QEvent class MyWidget(QWidget): def event(self, ev): raise RuntimeError('event processed') def test_widget(qtbot, qapp): w = MyWidget() qapp.postEvent(w, QEvent(QEvent.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 QWidget = qt_api.QWidget QtCore = qt_api.QtCore QEvent = qt_api.QtCore.QEvent class MyWidget(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 QWidget = qt_api.QWidget QtCore = qt_api.QtCore QEvent = qt_api.QtCore.QEvent QApplication = qt_api.QApplication class MyWidget(QWidget): def event(self, ev): if ev.type() == QEvent.User: raise RuntimeError('event processed') return True @pytest.yield_fixture def widget(qapp): w = MyWidget() {setup_code} yield w {teardown_code} def send_event(w, qapp): qapp.postEvent(w, QEvent(QEvent.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.plugin 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.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 QWidget = qt_api.QWidget Signal = qt_api.Signal class MyWidget(QWidget): on_event = 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.QWidget): def event(self, ev): called.append(1) raise RuntimeError('event processed') w = MyWidget() with capture_exceptions() as exceptions: qapp.postEvent(w, qt_api.QEvent(qt_api.QEvent.User)) qapp.processEvents() assert called del exceptions[:] _out, err = capsys.readouterr() assert "raise RuntimeError('event processed')" in err pytest-qt-2.3.1/tests/test_basics.py0000664000372000037200000003150313223543324020353 0ustar travistravis00000000000000import os import sys import weakref import pytest 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.QApplication.instance() is not None widget = qt_api.QWidget() qtbot.addWidget(widget) widget.setWindowTitle('W1') widget.show() assert widget.isVisible() assert widget.windowTitle() == 'W1' def test_key_events(qtbot, event_recorder): """ Basic key events test. """ def extract(key_event): return ( key_event.type(), 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.QEvent.KeyPress, int(qt_api.Qt.Key_A), 'a') qtbot.keyRelease(event_recorder, 'a') assert event_recorder.event_data == (qt_api.QEvent.KeyRelease, int(qt_api.Qt.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.Qt.LeftButton) assert event_recorder.event_data == (qt_api.QEvent.MouseButtonPress, qt_api.Qt.LeftButton, qt_api.Qt.NoModifier) qtbot.mousePress(event_recorder, qt_api.Qt.RightButton, qt_api.Qt.AltModifier) assert event_recorder.event_data == (qt_api.QEvent.MouseButtonPress, qt_api.Qt.RightButton, qt_api.Qt.AltModifier) def test_stop_for_interaction(qtbot, timer): """ Test qtbot.stopForInteraction() """ widget = qt_api.QWidget() qtbot.addWidget(widget) qtbot.waitForWindowShown(widget) timer.single_shot_callback(widget.close, 0) qtbot.stopForInteraction() @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) if qt_api.pytest_qt_api != 'pyqt5': with pytest.raises(RuntimeError) as exc_info: with method(None, None): pass assert str(exc_info.value) == 'Available in PyQt5 only' else: widget = qt_api.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('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. """ if qt_api.pytest_qt_api != 'pyqt5': pytest.skip('Available in PyQt5 only') method = getattr(qtbot, method_name) widget = qt_api.QWidget() qtbot.add_widget(widget) with pytest.raises(ValueError) as exc_info: with method(widget, timeout=100): widget.show() raise ValueError('some other error') assert str(exc_info.value) == '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.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.QEvent(qt_api.QEvent.User)) def event(self, ev): if ev.type() == qt_api.QEvent.User: self.events.pop(-1) return qt_api.QtCore.QObject.event(self, ev) return EventsQueue() @pytest.yield_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_public_api_backward_compatibility(): """ Test backward compatibility for version 1.6.0: since then symbols that were available from pytestqt.plugin have been moved to other modules to enhance navigation and maintainability, this test ensures the same symbols are still available from the same imports. (#90) """ import pytestqt.plugin assert pytestqt.plugin.QtBot assert pytestqt.plugin.SignalBlocker assert pytestqt.plugin.MultiSignalBlocker assert pytestqt.plugin.SignalTimeoutError assert pytestqt.plugin.format_captured_exceptions assert pytestqt.plugin.capture_exceptions assert pytestqt.plugin.QtLoggingPlugin assert pytestqt.plugin.Record def test_qvariant(tmpdir): """Test that make_variant and extract_from_variant work in the same way across all supported Qt bindings. """ settings = qt_api.QtCore.QSettings(str(tmpdir / 'foo.ini'), qt_api.QtCore.QSettings.IniFormat) settings.setValue('int', qt_api.make_variant(42)) settings.setValue('str', qt_api.make_variant('Hello')) settings.setValue('empty', qt_api.make_variant()) assert qt_api.extract_from_variant(settings.value('int')) == 42 assert qt_api.extract_from_variant(settings.value('str')) == 'Hello' assert qt_api.extract_from_variant(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.QWidget): closed = False def closeEvent(self, e): e.accept() self.closed = True @pytest.yield_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.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.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', ['pyqt4', 'pyqt5', 'pyside', 'pyside2']) 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") 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.replace('v2', '') == option_api: # handle pyqt4v2 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', ['pyqt4', 'pyqt5', 'pyside', 'pyside2']) 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.replace('v2', '') == 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']) @pytest.mark.skipif(qt_api.pytest_qt_api in ['pyqt4', 'pyqt4v2', 'pyside'], reason="QApplication.arguments() doesn't return custom arguments with Qt4 and Windows") 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 *' ]) pytest-qt-2.3.1/tests/conftest.py0000664000372000037200000000463713223543324017705 0ustar travistravis00000000000000import functools import sys 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. """ # time.clock() is more accurate on Windows get_time = time.clock if sys.platform.startswith('win') else time.time class StopWatch: def __init__(self): self._start_time = None self.elapsed = None def start(self): self._start_time = get_time() def stop(self): self.elapsed = (get_time() - 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.yield_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-2.3.1/CHANGELOG.rst0000664000372000037200000003766313223543324016372 0ustar travistravis000000000000002.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 .. _@mochick: https://github.com/mochick 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 .. _@p0las: https://github.com/p0las 2.2.0 ----- - ``pytest-qt`` now supports `PySide2`_ thanks to `@rth`_! .. _PySide2: https://wiki.qt.io/PySide2 .. _@rth: https://github.com/rth 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 .. _@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 .. _@montefra: https://github.com/montefra .. _@MShekow: https://github.com/MShekow .. _@Sheeo: https://github.com/Sheeo .. _@The-Compiler: https://github.com/The-Compiler pytest-qt-2.3.1/requirements.txt0000664000372000037200000000000613223543324017612 0ustar travistravis00000000000000pytestpytest-qt-2.3.1/PKG-INFO0000664000372000037200000002006613223543656015443 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: pytest-qt Version: 2.3.1 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 Description-Content-Type: UNKNOWN Description: ========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests for `PySide`_, `PySide2` and `PyQt`_ 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!' .. _PySide: https://pypi.python.org/pypi/PySide .. _PySide2: https://wiki.qt.io/PySide2 .. _PyQt: http://www.riverbankcomputing.com/software/pyqt .. _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 .. |anaconda| image:: https://anaconda.org/conda-forge/pytest-qt/badges/version.svg :target: https://anaconda.org/conda-forge/pytest-qt .. |travis| image:: https://img.shields.io/travis/pytest-dev/pytest-qt/master.svg :target: https://travis-ci.org/pytest-dev/pytest-qt .. |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 .. |appveyor| image:: https://img.shields.io/appveyor/ci/pytest-dev/pytest-qt/master.svg :target: https://ci.appveyor.com/project/nicoddemus/pytest-qt .. |python| image:: https://img.shields.io/pypi/pyversions/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt/ :alt: Supported Python versions |python| |version| |anaconda| |travis| |appveyor| |coverage| |docs| 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 ============ Works with either PySide_, PySide2_ or PyQt_ (``PyQt5`` and ``PyQt4``) picking whichever is available on the system, giving preference to the first one installed in this order: - ``PySide2`` - ``PyQt5`` - ``PySide`` - ``PyQt4`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to ``pyqt5``, ``pyside``, ``pyside2``, ``pyqt4`` or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` API to `version 2`_. .. 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). .. _version 2: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html 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. :) Running tests ------------- Tests are run using `tox`_. The simplest way to test is with `PySide`_, as it is available on pip and can be installed by ``tox`` automatically:: $ tox -e py34-pyside,py27-pyside,docs If you want to test against `PyQt`_, install it into your global python installation and use the ``py27-pyqt4``, ``py34-pyqt4`` or ``py34-pyqt5`` testing environments, and ``tox`` will copy the appropriate files into its virtual environments to ensure isolation. 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 `_) **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 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 :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Desktop Environment :: Window Managers Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: User Interfaces pytest-qt-2.3.1/scripts/0000775000372000037200000000000013223543656016031 5ustar travistravis00000000000000pytest-qt-2.3.1/scripts/link_pyqt.py0000664000372000037200000001574513223543324020421 0ustar travistravis00000000000000#!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2014-2015 Florian Bruhin (The Compiler) # This file was originally part of qutebrowser, copied to pytest-qt with # permission from the author, by Bruno Oliveira (nicoddemus@gmail.com). The # file also went through some small modifications so it could run in Python 2.7 # and link PyQt4 files. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3 of the License, or (at your option) any later version. # # pytest-qt is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with pytest-qt. If not, see . """Symlink PyQt into a given virtualenv.""" from __future__ import print_function import os import argparse import shutil import os.path import sys import subprocess import tempfile import filecmp import io class Error(Exception): """Exception raised when linking fails.""" pass def run_py(executable, *code): """Run the given python code with the given executable.""" if os.name == 'nt' and len(code) > 1: # Windows can't do newlines in arguments... oshandle, filename = tempfile.mkstemp() with os.fdopen(oshandle, 'w') as f: f.write('\n'.join(code)) cmd = [executable, filename] try: ret = subprocess.check_output(cmd, universal_newlines=True, stderr=None).rstrip() finally: os.remove(filename) else: cmd = [executable, '-c', '\n'.join(code)] ret = subprocess.check_output(cmd, universal_newlines=True, stderr=None).rstrip() return ret def get_ignored_files(directory, files): """Get the files which should be ignored for link_pyqt() on Windows.""" needed_exts = ('.py', '.dll', '.pyd', '.so') ignored_dirs = ('examples', 'doc') filtered = [] for f in files: ext = os.path.splitext(f)[1] full_path = os.path.join(directory, f) if os.path.isdir(full_path) and f in ignored_dirs: filtered.append(f) elif (ext not in needed_exts) and os.path.isfile(full_path): filtered.append(f) return filtered def needs_update(source, dest): """Check if a file to be linked/copied needs to be updated.""" if os.path.islink(dest): # No need to delete a link and relink -> skip this return False elif os.path.isdir(dest): diffs = filecmp.dircmp(source, dest) ignored = get_ignored_files(source, diffs.left_only) has_new_files = set(ignored) != set(diffs.left_only) return (has_new_files or diffs.right_only or diffs.common_funny or diffs.diff_files or diffs.funny_files) else: return not filecmp.cmp(source, dest) def get_lib_path(executable, name, required=True): """Get the path of a python library. Args: executable: The Python executable to use. name: The name of the library to get the path for. required: Whether Error should be raised if the lib was not found. """ code = [ 'try:', ' import {}'.format(name), 'except ImportError as e:', ' print("ImportError: " + str(e))', 'else:', ' print("path: " + {}.__file__)'.format(name) ] output = run_py(executable, *code) try: prefix, data = output.split(': ') except ValueError: raise ValueError("Unexpected output: {!r}".format(output)) if prefix == 'path': return data elif prefix == 'ImportError': if required: raise Error("Could not import {} with {}: {}!".format( name, executable, data)) else: return None else: raise ValueError("Unexpected output: {!r}".format(output)) def link_pyqt(executable, venv_path, qt_version): """Symlink the systemwide PyQt/sip into the venv. Args: executable: The python executable where the source files are present. venv_path: The path to the virtualenv site-packages. """ sip_file = get_lib_path(executable, 'sip') sipconfig_file = get_lib_path(executable, 'sipconfig', required=False) pyqt_dir = os.path.dirname(get_lib_path(executable, 'PyQt%d' % qt_version)) for path in [sip_file, sipconfig_file, pyqt_dir]: if path is None: continue fn = os.path.basename(path) dest = os.path.join(venv_path, fn) if os.path.exists(dest): if needs_update(path, dest): remove(dest) else: continue copy_or_link(path, dest) def copy_or_link(source, dest): """Copy or symlink source to dest.""" if os.name == 'nt': if os.path.isdir(source): print('{} -> {}'.format(source, dest)) shutil.copytree(source, dest, ignore=get_ignored_files) else: print('{} -> {}'.format(source, dest)) shutil.copy(source, dest) else: print('{} -> {}'.format(source, dest)) os.symlink(source, dest) def remove(filename): """Remove a given filename, regardless of whether it's a file or dir.""" if os.path.isdir(filename): shutil.rmtree(filename) else: os.unlink(filename) def get_venv_lib_path(path): """Get the library path of a virtualenv.""" subdir = 'Scripts' if os.name == 'nt' else 'bin' executable = os.path.join(path, subdir, 'python') return run_py(executable, 'from distutils.sysconfig import get_python_lib', 'print(get_python_lib())') def get_tox_syspython(tox_path): """Get the system python based on a virtualenv created by tox.""" path = os.path.join(tox_path, '.tox-config1') with io.open(path, encoding='ascii') as f: line = f.readline() _md5, sys_python = line.rstrip().split(' ') return sys_python def main(): """Main entry point.""" parser = argparse.ArgumentParser() parser.add_argument('path', help="Base path to the venv.") parser.add_argument('qt_version', type=int, help="Major Qt version (5 for PyQt5, etc).") parser.add_argument('--tox', help="Add when called via tox.", action='store_true') args = parser.parse_args() if args.tox: executable = get_tox_syspython(args.path) else: executable = sys.executable venv_path = get_venv_lib_path(args.path) link_pyqt(executable, venv_path, args.qt_version) if __name__ == '__main__': try: main() except Error as e: print(str(e), file=sys.stderr) sys.exit(1)