pytest-qt-3.2.2/0000775000372000037200000000000013404516624014337 5ustar travistravis00000000000000pytest-qt-3.2.2/docs/0000775000372000037200000000000013404516624015267 5ustar travistravis00000000000000pytest-qt-3.2.2/docs/wait_until.rst0000664000372000037200000000524713404516400020200 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-3.2.2/docs/index.rst0000664000372000037200000000066613404516400017130 0ustar travistravis00000000000000========= pytest-qt ========= :Repository: `GitHub `_ :Version: |version| :License: `MIT `_ :Author: Bruno Oliveira .. toctree:: :maxdepth: 2 intro tutorial logging signals wait_until wait_callback virtual_methods modeltester app_exit note_pyqt4v2 note_dialogs troubleshooting reference changelog pytest-qt-3.2.2/docs/note_dialogs.rst0000664000372000037200000000345513404516400020467 0ustar travistravis00000000000000A note about Modal Dialogs ========================== Simple Dialogs -------------- For QMessageBox.question one approach is to mock the function using the `monkeypatch `_ fixture: .. code-block:: python def test_Qt(qtbot, monkeypatch): simple = Simple() qtbot.addWidget(simple) monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) simple.query() assert simple.answer Custom Dialogs -------------- Suppose you have a custom dialog that asks the user for their name and age, and a form that uses it. One approach is to add a convenience function that also has the nice benefit of making testing easier, like this: .. code-block:: python class AskNameAndAgeDialog(QDialog): @classmethod def ask(cls, text, parent): dialog = cls(parent) dialog.text.setText(text) if dialog.exec_() == QDialog.Accepted: return dialog.getName(), dialog.getAge() else: return None, None This allows clients of the dialog to use it this way: .. code-block:: python name, age = AskNameAndAgeDialog.ask("Enter name and age because of bananas:", parent) if name is not None: # use name and age for bananas ... And now it is also easy to mock ``AskNameAndAgeDialog.ask`` when testing the form: .. code-block:: python def test_form_registration(qtbot, monkeypatch): form = RegistrationForm() monkeypatch.setattr( AskNameAndAgeDialog, "ask", classmethod(lambda *args: ("Jonh", 30)) ) qtbot.click(form.enter_info()) # calls AskNameAndAgeDialog.ask # test that the rest of the form correctly behaves as if # user entered "Jonh" and 30 as name and age pytest-qt-3.2.2/docs/modeltester.rst0000664000372000037200000000425013404516400020341 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. Qt/Python tester ---------------- Starting with PyQt5 5.11, Qt's ``QAbstractItemModelTester`` is exposed to Python. If it's available, by default, ``qtmodeltester.check`` will use the C++ implementation and fail tests if it emits any warnings. To use the Python implementation instead, use ``qtmodeltester.check(model, force_py=True)``. Credits ------- The source code was ported from `qabstractitemmodeltester.cpp`_ by `Florian Bruhin`_, many thanks! .. _qabstractitemmodeltester.cpp: http://code.qt.io/cgit/qt/qtbase.git/tree/src/testlib/qabstractitemmodeltester.cpp .. _Florian Bruhin: https://github.com/The-Compiler .. _QAbstractItemModel: http://doc.qt.io/qt-5/qabstractitemmodel.html pytest-qt-3.2.2/docs/logging.rst0000664000372000037200000001610413404516400017441 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-3.2.2/docs/Makefile0000664000372000037200000001271013404516400016720 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-3.2.2/docs/troubleshooting.rst0000664000372000037200000000432213404516400021241 0ustar travistravis00000000000000Troubleshooting =============== tox: ``InvocationError`` without further information ---------------------------------------------------- It might happen that your ``tox`` run finishes abruptly without any useful information, e.g.:: ERROR: InvocationError: '/path/to/project/.tox/py36/bin/python setup.py test --addopts --doctest-modules' ___ summary _____ ERROR: py36: commands failed ``pytest-qt`` needs a ``DISPLAY`` to run, otherwise ``Qt`` calls ``abort()`` and the process crashes immediately. One solution is to use the `pytest-xvfb`_ plugin which takes care of the grifty details automatically, starting up a virtual framebuffer service, initializing variables, etc. This is the recommended solution if you are running in CI servers without a GUI, for example in Travis or CircleCI. Alternatively, ``tox`` users may edit ``tox.ini`` to allow the relevant variables to be passed to the underlying ``pytest`` invocation: .. code-block:: ini [testenv] passenv = DISPLAY XAUTHORITY Note that this solution will only work in boxes with a GUI. More details can be found in `issue #170`_. .. _pytest-xvfb: https://pypi.python.org/pypi/pytest-xvfb/ .. _issue #170: https://github.com/pytest-dev/pytest-qt/issues/170 xvfb: ``AssertionError``, ``TimeoutError`` when using ``waitUntil``, ``waitExposed`` and UI events. --------------------------------------------------------------------------------------------------- When using ``xvfb`` or equivalent make sure to have a window manager running otherwise UI events will not work properly. If you are running your code on Travis-CI make sure that your ``.travis.yml`` has the following content: .. code-block:: yaml sudo: required before_install: - sudo apt-get update - sudo apt-get install -y xvfb herbstluftwm install: - "export DISPLAY=:99.0" - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset" - sleep 3 before_script: - "herbstluftwm &" - sleep 1 More details can be found in `issue #206`_. .. _issue #206: https://github.com/pytest-dev/pytest-qt/issues/206 pytest-qt-3.2.2/docs/signals.rst0000664000372000037200000002161513404516400017456 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: .. code-block:: python def test_long_computation(qtbot): app = Application() # Watch for the app.worker.finished signal, then start the worker. with qtbot.waitSignal(app.worker.finished, timeout=10000) as blocker: blocker.connect(app.worker.failed) # Can add other signals to blocker app.worker.start() # Test will block at this point until either the "finished" or the # "failed" signal is emitted. If 10 seconds passed without a signal, # TimeoutError will be raised. assert_application_results(app) raising parameter ----------------- .. versionadded:: 1.4 .. versionchanged:: 2.0 You can pass ``raising=False`` to avoid raising a :class:`qtbot.TimeoutError ` if the timeout is reached before the signal is triggered: .. code-block:: python def test_long_computation(qtbot): ... with qtbot.waitSignal(app.worker.finished, raising=False) as blocker: app.worker.start() assert_application_results(app) # qtbot.TimeoutError is not raised, but you can still manually # check whether the signal was triggered: assert blocker.signal_triggered, "process timed-out" .. _qt_default_raising: qt_default_raising ini option ----------------------------- .. versionadded:: 1.11 .. versionchanged:: 2.0 .. versionchanged:: 3.1 The ``qt_default_raising`` ini option can be used to override the default value of the ``raising`` parameter of the ``qtbot.waitSignal`` and ``qtbot.waitSignals`` functions when omitted: .. code-block:: ini [pytest] qt_default_raising = false Calls which explicitly pass the ``raising`` parameter are not affected. This option was called ``qt_wait_signal_raising`` before 3.1.0. check_params_cb parameter ------------------------- .. versionadded:: 2.0 If the signal has parameters you want to compare with expected values, you can pass ``check_params_cb=some_callable`` that compares the provided signal parameters to some expected parameters. It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. .. code-block:: python def test_status_100(status): """Return true if status has reached 100%.""" return status == 100 def test_status_complete(qtbot): app = Application() # the following raises if the worker's status signal (which has an int parameter) wasn't raised # with value=100 within the default timeout with qtbot.waitSignal( app.worker.status, raising=True, check_params_cb=test_status_100 ) as blocker: app.worker.start() timeout parameter ---------------- The ``timeout`` parameter specifies how long ``waitSignal`` should wait for a signal to arrive. If the timeout is ``None``, there won't be any timeout, i.e. it'll wait indefinitely. If the timeout is set to ``0``, it's expected that the signal arrives directly in the code inside the ``with qtbot.waitSignal(...):`` block. Getting arguments of the emitted signal --------------------------------------- .. versionadded:: 1.10 The arguments emitted with the signal are available as the ``args`` attribute of the blocker: .. code-block:: python def test_signal(qtbot): ... with qtbot.waitSignal(app.got_cmd) as blocker: app.listen() assert blocker.args == ["test"] Signals without arguments will set ``args`` to an empty list. If the time out is reached instead, ``args`` will be ``None``. Getting all arguments of non-matching arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 2.1 When using the ``check_params_cb`` parameter, it may happen that the provided signal is received multiple times with different parameter values, which may or may not match the requirements of the callback. ``all_args`` then contains the list of signal parameters (as tuple) in the order they were received. waitSignals ----------- .. versionadded:: 1.4 If you have to wait until **all** signals in a list are triggered, use :meth:`qtbot.waitSignals `, which receives a list of signals instead of a single signal. As with :meth:`qtbot.waitSignal `, it also supports the ``raising`` parameter:: def test_workers(qtbot): workers = spawn_workers() with qtbot.waitSignals([w.finished for w in workers]): for w in workers: w.start() # this will be reached after all workers emit their "finished" # signal or a qtbot.TimeoutError will be raised assert_application_results(app) check_params_cbs parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 2.0 Corresponding to the ``check_params_cb`` parameter of ``waitSignal`` you can use the ``check_params_cbs`` parameter to check whether one or more of the provided signals are emitted with expected parameters. Provide a list of callables, each matching the signature of the corresponding signal in ``signals`` (just like a slot function would). Like for ``waitSignal``, each callable has to return ``True`` if parameters match, ``False`` otherwise. Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the corresponding signal. If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised. The following example shows that the ``app.worker.status`` signal has to be emitted with values 50 and 100, and the ``app.worker.finished`` signal has to be emitted too (for which no signal parameter evaluation takes place). .. code-block:: python def test_status_100(status): """Return true if status has reached 100%.""" return status == 100 def test_status_50(status): """Return true if status has reached 50%.""" return status == 50 def test_status_complete(qtbot): app = Application() signals = [app.worker.status, app.worker.status, app.worker.finished] callbacks = [test_status_50, test_status_100, None] with qtbot.waitSignals( signals, raising=True, check_params_cbs=callbacks ) as blocker: app.worker.start() order parameter ^^^^^^^^^^^^^^^ .. versionadded:: 2.0 By default a test using ``qtbot.waitSignals`` completes successfully if *all* signals in ``signals`` are emitted, irrespective of their exact order. The ``order`` parameter can be set to ``"strict"`` to enforce strict signal order. Exemplary, this means that ``blocker.signal_triggered`` will be ``False`` if ``waitSignals`` expects the signals ``[a, b]`` but the sender emitted signals ``[a, a, b]``. .. note:: The tested component can still emit signals unknown to the blocker. E.g. ``blocker.waitSignals([a, b], raising=True, order="strict")`` won't raise if the signal-sender emits signals ``[a, c, b]``, as ``c`` is not part of the observed signals. A third option is to set ``order="simple"`` which is like "strict", but signals may be emitted in-between the provided ones, e.g. if the expected signals are ``[a, b, c]`` and the sender actually emits ``[a, a, b, a, c]``, the test completes successfully (it would fail with ``order="strict"``). Getting emitted signals and arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. versionadded:: 2.1 To determine which of the expected signals were emitted during a ``wait()`` you can use ``blocker.all_signals_and_args`` which contains a list of :class:`wait_signal.SignalAndArgs ` objects, indicating the signals (and their arguments) in the order they were received. Making sure a given signal is not emitted ----------------------------------------- .. versionadded:: 1.11 If you want to ensure a signal is **not** emitted in a given block of code, use the :meth:`qtbot.assertNotEmitted ` context manager: .. code-block:: python def test_no_error(qtbot): ... with qtbot.assertNotEmitted(app.worker.error): app.worker.start() By default, this only catches signals emitted directly inside the block. You can pass ``wait=...`` to wait for a given duration (in milliseconds) for asynchronous signals to (not) arrive: .. code-block:: python def test_no_error(qtbot): ... with qtbot.assertNotEmitted(page.loadFinished, wait=100): page.runJavaScript("document.getElementById('not-a-link').click()") pytest-qt-3.2.2/docs/wait_callback.rst0000664000372000037200000000351313404516400020573 0ustar travistravis00000000000000waitCallback: Waiting for methods taking a callback =================================================== .. versionadded:: 3.1 Some methods in Qt (especially ``QtWebEngine``) take a callback as argument, which gets called by Qt once a given operation is done. To test such code, you can use :meth:`qtbot.waitCallback ` which waits until the callback has been called or a timeout is reached. The ``qtbot.waitCallback()`` method returns a callback which is callable directly. For example: .. code-block:: python def test_js(qtbot): page = QWebEnginePage() with qtbot.waitCallback() as cb: page.runJavaScript("1 + 1", cb) # After callback Anything following the ``with`` block will be run only after the callback has been called. If the callback doesn't get called during the given timeout, :class:`qtbot.TimeoutError ` is raised. If it is called more than once, :class:`qtbot.CallbackCalledTwiceError ` is raised. raising parameter ----------------- Similarly to ``qtbot.waitSignal``, you can pass a ``raising=False`` parameter (or set the ``qt_default_raising`` ini option) to avoid raising an exception on timeouts. See :doc:`signals` for details. Getting arguments the callback was called with ---------------------------------------------- After the callback is called, the arguments and keyword arguments passed to it are available via ``.args`` (as a list) and ``.kwargs`` (as a dict), respectively. In the example above, we could check the result via: .. code-block:: python assert cb.args == [2] assert cb.kwargs == {} Instead of checking the arguments by hand, you can use ``.assert_called_with()`` to make sure the callback was called with the given arguments: .. code-block:: python cb.assert_called_with(2) pytest-qt-3.2.2/docs/.gitignore0000664000372000037200000000001113404516400017237 0ustar travistravis00000000000000_build/ pytest-qt-3.2.2/docs/intro.rst0000664000372000037200000000510113404516400017141 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-3.2.2/docs/app_exit.rst0000664000372000037200000000222513404516400017623 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-3.2.2/docs/_static/0000775000372000037200000000000013404516624016715 5ustar travistravis00000000000000pytest-qt-3.2.2/docs/_static/find_files_dialog.png0000664000372000037200000007531513404516400023047 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-3.2.2/docs/tutorial.rst0000664000372000037200000000500013404516400017647 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-3.2.2/docs/reference.rst0000664000372000037200000000105013404516400017743 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-3.2.2/docs/make.bat0000664000372000037200000001175613404516400016676 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-3.2.2/docs/changelog.rst0000664000372000037200000000010413404516400017733 0ustar travistravis00000000000000 .. _changelog: Changelog ========= .. include:: ../CHANGELOG.rst pytest-qt-3.2.2/docs/virtual_methods.rst0000664000372000037200000000510013404516400021216 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-3.2.2/docs/note_pyqt4v2.rst0000664000372000037200000000221013404516400020362 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-3.2.2/docs/conf.py0000664000372000037200000001734213404516400016565 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 import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = u"pytest-qt" copyright = u"2013, Bruno Oliveira" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "pytest-qtdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "pytest-qt.tex", u"pytest-qt Documentation", u"Bruno Oliveira", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "pytest-qt", u"pytest-qt Documentation", [u"Bruno Oliveira"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "pytest-qt", u"pytest-qt Documentation", u"Bruno Oliveira", "pytest-qt", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' pytest-qt-3.2.2/README.rst0000664000372000037200000001345713404516400016030 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 .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.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/nicoddemus/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 .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |travis| |appveyor| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ 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`_. It is recommended to develop locally on Python 3 because ``PyQt5`` and ``PySide2`` are easily installable using ``pip``:: $ tox -e py37-pyside2,py37-pyqt5 ``pytest-qt`` is formatted using `black `_ and uses `pre-commit `_ for linting checks before commits. You can install ``pre-commit`` locally with:: $ pip install pre-commit $ pre-commit install 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-3.2.2/setup.py0000664000372000037200000000405513404516400016045 0ustar travistravis00000000000000import sys 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"], python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", 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", "Programming Language :: Python :: 3.7", "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-3.2.2/.gitattributes0000664000372000037200000000003413404516400017217 0ustar travistravis00000000000000CHANGELOG.md merge=union pytest-qt-3.2.2/.pydevproject0000664000372000037200000000073013404516400017046 0ustar travistravis00000000000000 /${PROJECT_DIR_NAME} /${PROJECT_DIR_NAME}/tests python 2.7 Default pytest-qt-3.2.2/pytest_qt.egg-info/0000775000372000037200000000000013404516624020065 5ustar travistravis00000000000000pytest-qt-3.2.2/pytest_qt.egg-info/top_level.txt0000664000372000037200000000001113404516624022607 0ustar travistravis00000000000000pytestqt pytest-qt-3.2.2/pytest_qt.egg-info/requires.txt0000664000372000037200000000005513404516624022465 0ustar travistravis00000000000000pytest>=2.7.0 [doc] sphinx sphinx_rtd_theme pytest-qt-3.2.2/pytest_qt.egg-info/PKG-INFO0000664000372000037200000002050413404516624021163 0ustar travistravis00000000000000Metadata-Version: 2.1 Name: pytest-qt Version: 3.2.2 Summary: pytest support for PyQt and PySide applications Home-page: http://github.com/pytest-dev/pytest-qt Author: Bruno Oliveira Author-email: nicoddemus@gmail.com License: MIT 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 .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.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/nicoddemus/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 .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |travis| |appveyor| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ 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`_. It is recommended to develop locally on Python 3 because ``PyQt5`` and ``PySide2`` are easily installable using ``pip``:: $ tox -e py37-pyside2,py37-pyqt5 ``pytest-qt`` is formatted using `black `_ and uses `pre-commit `_ for linting checks before commits. You can install ``pre-commit`` locally with:: $ pip install pre-commit $ pre-commit install 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: Programming Language :: Python :: 3.7 Classifier: Topic :: Desktop Environment :: Window Managers Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: User Interfaces Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Provides-Extra: doc pytest-qt-3.2.2/pytest_qt.egg-info/dependency_links.txt0000664000372000037200000000000113404516624024133 0ustar travistravis00000000000000 pytest-qt-3.2.2/pytest_qt.egg-info/entry_points.txt0000664000372000037200000000005013404516624023356 0ustar travistravis00000000000000[pytest11] pytest-qt = pytestqt.plugin pytest-qt-3.2.2/pytest_qt.egg-info/SOURCES.txt0000664000372000037200000000223013404516624021746 0ustar travistravis00000000000000.gitattributes .gitignore .pre-commit-config.yaml .project .pydevproject .travis.yml CHANGELOG.rst HOWTORELEASE.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_dialogs.rst docs/note_pyqt4v2.rst docs/reference.rst docs/signals.rst docs/troubleshooting.rst docs/tutorial.rst docs/virtual_methods.rst docs/wait_callback.rst docs/wait_until.rst docs/_static/find_files_dialog.png 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/utils.py pytestqt/wait_signal.py tests/conftest.py tests/test_basics.py tests/test_exceptions.py tests/test_logging.py tests/test_modeltest.py tests/test_qtest_proxies.py tests/test_wait_signal.py tests/test_wait_until.pypytest-qt-3.2.2/LICENSE0000664000372000037200000000213613404516400015336 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-3.2.2/pytestqt/0000775000372000037200000000000013404516624016234 5ustar travistravis00000000000000pytest-qt-3.2.2/pytestqt/wait_signal.py0000664000372000037200000006551713404516400021115 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 or timeout == 0: self._timer = None else: self._timer = qt_api.QtCore.QTimer(self._loop) self._timer.setSingleShot(True) self._timer.setInterval(timeout) def wait(self): """ Waits until either a connected signal is triggered or timeout is reached. :raise ValueError: if no signals are connected and timeout is None; in this case it would wait forever. """ __tracebackhide__ = True if self.signal_triggered: return if self.timeout is None and not self._signals: raise ValueError("No signals or timeout specified.") if self._timer is not None: self._timer.timeout.connect(self._quit_loop_by_timeout) self._timer.start() if self.timeout != 0: 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_default_raising` option. :ivar list args: The arguments which were emitted by the signal, or None if the signal wasn't emitted at all. .. versionadded:: 1.10 The *args* attribute. .. automethod:: wait .. automethod:: connect """ def __init__(self, timeout=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 def get_ordinal_str(n): return "%d%s" % (n, {1: "st", 2: "nd", 3: "rd"}.get(n if n < 20 else n % 10, "th")) class NoMatchingIndexFoundError(Exception): pass class MultiSignalBlocker(_AbstractSignalBlocker): """ Returned by :meth:`pytestqt.qtbot.QtBot.waitSignals` method, blocks until all signals connected to it are triggered or the timeout is reached. Variables identical to :class:`SignalBlocker`: - ``timeout`` - ``signal_triggered`` - ``raising`` .. automethod:: wait """ def __init__(self, timeout=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 ] # maps from a signal-string to one of the signal instances (the first one found) signal_str_to_unique_signal = {} for index, signal_str in enumerate(signals_as_str): signal = self.get_signal_from_potential_signal_tuple(signals[index]) potential_tuple = signals[index] if signal_str not in signal_str_to_unique_signal: unique_signal_tuple = potential_tuple signal_str_to_unique_signal[signal_str] = signal self._signals_map[signal] = [index] # create a new list else: # append to existing list unique_signal = signal_str_to_unique_signal[signal_str] self._signals_map[unique_signal].append(index) unique_signal_tuple = signals[index] self._determine_and_save_signal_name(unique_signal_tuple) def _determine_and_save_signal_name(self, unique_signal_tuple): signal_name = self.determine_signal_name(unique_signal_tuple) if signal_name: # might be an empty string if no name could be determined unique_signal = self.get_signal_from_potential_signal_tuple( unique_signal_tuple ) self._signal_names[unique_signal] = signal_name def _create_signal_emitted_indices(self, signals): for signal in signals: self._signals_emitted.append(False) def _connect_unique_signals(self): for unique_signal in self._signals_map: slot = functools.partial(self._unique_signal_emitted, unique_signal) self._slots.append(slot) unique_signal.connect(slot) self._signals.append(unique_signal) def _unique_signal_emitted(self, unique_signal, *args): """ Called when a given signal is emitted. If all expected signals have been emitted, quits the event loop and marks that we finished because signals. """ self._record_emitted_signal_if_possible(unique_signal, *args) self._check_signal_match(unique_signal, *args) if self._all_signals_emitted(): self.signal_triggered = True try: self._cleanup() finally: self._loop.quit() def _record_emitted_signal_if_possible(self, unique_signal, *args): if self._are_signal_names_available(): self.all_signals_and_args.append( SignalAndArgs(signal_name=self._signal_names[unique_signal], args=args) ) def _check_signal_match(self, unique_signal, *args): if self._order == "none": # perform the test for every matching index (stop after the first one that matches) try: successful_index = self._get_first_matching_index(unique_signal, *args) self._signals_emitted[successful_index] = True except NoMatchingIndexFoundError: # none found pass elif self._order == "simple": if self._check_signal_matches_expected_index(unique_signal, *args): self._signals_emitted[self._signal_expected_index] = True self._signal_expected_index += 1 else: # self.order == "strict" if not self._strict_order_violated: # only do the check if the strict order has not been violated yet self._strict_order_violated = ( True ) # assume the order has been violated this time if self._check_signal_matches_expected_index(unique_signal, *args): self._signals_emitted[self._signal_expected_index] = True self._signal_expected_index += 1 self._strict_order_violated = ( False ) # order has not been violated after all! else: if self._are_signal_names_available(): self._actual_signal_and_args_at_violation = SignalAndArgs( signal_name=self._signal_names[unique_signal], args=args ) def _all_signals_emitted(self): return not self._strict_order_violated and all(self._signals_emitted) def _get_first_matching_index(self, unique_signal, *args): successfully_emitted = False successful_index = -1 potential_indices = self._get_unemitted_signal_indices(unique_signal) for potential_index in potential_indices: if not self._violates_callback_at_index(potential_index, *args): successful_index = potential_index successfully_emitted = True break if not successfully_emitted: raise NoMatchingIndexFoundError return successful_index def _check_signal_matches_expected_index(self, unique_signal, *args): potential_indices = self._get_unemitted_signal_indices(unique_signal) if potential_indices: if self._signal_expected_index == potential_indices[0]: if not self._violates_callback_at_index( self._signal_expected_index, *args ): return True return False def _violates_callback_at_index(self, index, *args): """ Checks if there's a callback at the provided index that is violates due to invalid parameters. Returns False if there is no callback for that index, or if a callback exists but it wasn't violated (returned True). Returns True otherwise. """ if self._check_params_callbacks: callback_func = self._check_params_callbacks[index] if callback_func: if not callback_func(*args): return True return False def _get_unemitted_signal_indices(self, signal): """Returns the indices for the provided signal for which NO signal instance has been emitted yet.""" return [ index for index in self._signals_map[signal] if not self._signals_emitted[index] ] def _are_signal_names_available(self): if self._signal_names: return True return False def _get_degenerate_error_message(self): received_signals = sum(self._signals_emitted) total_signals = len(self._signals_emitted) return ( "Received {actual} of the {total} expected signals. " "To improve this error message, provide the names of the signals " "in the waitSignals() call." ).format(actual=received_signals, total=total_signals) def _get_expected_and_actual_signals_message(self): if not self.all_signals_and_args: emitted_signals = "None" else: emitted_signal_string_list = [str(_) for _ in self.all_signals_and_args] emitted_signals = self._format_as_array(emitted_signal_string_list) missing_signal_strings = [] for missing_signal_index in self._get_missing_signal_indices(): missing_signal_strings.append( self._get_signal_string_representation_for_index(missing_signal_index) ) missing_signals = self._format_as_array(missing_signal_strings) return "Emitted signals: {}. Missing: {}".format( emitted_signals, missing_signals ) @staticmethod def _format_as_array(list_of_strings): return "[{}]".format(", ".join(list_of_strings)) def _get_order_violation_message(self): expected_signal_as_str = self._get_signal_string_representation_for_index( self._signal_expected_index ) actual_signal_as_str = str(self._actual_signal_and_args_at_violation) return ( "Signal order violated! Expected {expected} as {ordinal} signal, " "but received {actual} instead. " ).format( expected=expected_signal_as_str, ordinal=get_ordinal_str(self._signal_expected_index + 1), actual=actual_signal_as_str, ) def _get_missing_signal_indices(self): return [ index for index, value in enumerate(self._signals_emitted) if not self._signals_emitted[index] ] def _get_signal_string_representation_for_index(self, index): """Returns something like or (callback: )""" signal = self._get_signal_for_index(index) signal_str_repr = self._signal_names[signal] if self._check_params_callbacks: potential_callback = self._check_params_callbacks[index] if potential_callback: callback_name = self.get_callback_name(potential_callback) if callback_name: signal_str_repr += " (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 CallbackBlocker(object): """ .. versionadded:: 3.1 An object which checks if the returned callback gets called. Intended to be used as a context manager. :ivar int timeout: maximum time to wait for the callback to be called. :ivar bool raising: If :class:`TimeoutError` should be raised if a timeout occured. .. note:: contrary to the parameter of same name in :meth:`pytestqt.qtbot.QtBot.waitCallback`, this parameter does not consider the :ref:`qt_default_raising` option. :ivar list args: The arguments with which the callback was called, or None if the callback wasn't called at all. :ivar dict kwargs: The keyword arguments with which the callback was called, or None if the callback wasn't called at all. """ def __init__(self, timeout=1000, raising=True): self.timeout = timeout self.raising = raising self.args = None self.kwargs = None self.called = False self._loop = qt_api.QtCore.QEventLoop() if timeout is None: self._timer = None else: self._timer = qt_api.QtCore.QTimer(self._loop) self._timer.setSingleShot(True) self._timer.setInterval(timeout) def wait(self): """ Waits until either the returned callback is called or timeout is reached. """ __tracebackhide__ = True if self.called: return if self._timer is not None: self._timer.timeout.connect(self._quit_loop_by_timeout) self._timer.start() self._loop.exec_() if not self.called and self.raising: raise TimeoutError("Callback wasn't called after %sms." % self.timeout) def assert_called_with(self, *args, **kwargs): """ Check that the callback was called with the same arguments as this function. """ assert self.called assert self.args == list(args) assert self.kwargs == kwargs def _quit_loop_by_timeout(self): try: self._cleanup() finally: self._loop.quit() def _cleanup(self): if self._timer is not None: _silent_disconnect(self._timer.timeout, self._quit_loop_by_timeout) self._timer.stop() self._timer = None def __call__(self, *args, **kwargs): # Not inside the try: block, as if self.called is True, we did quit the # loop already. if self.called: raise CallbackCalledTwiceError("Callback called twice") try: self.args = list(args) self.kwargs = kwargs self.called = True self._cleanup() finally: self._loop.quit() def __enter__(self): return self def __exit__(self, type, value, traceback): __tracebackhide__ = True if value is None: # only wait if no exception happened inside the "with" block self.wait() class SignalEmittedError(Exception): """ .. versionadded:: 1.11 The exception thrown by :meth:`pytestqt.qtbot.QtBot.assertNotEmitted` if a signal was emitted unexpectedly. """ pass class CallbackCalledTwiceError(Exception): """ .. versionadded:: 3.1 The exception thrown by :meth:`pytestqt.qtbot.QtBot.waitCallback` if a callback was called twice. """ pass def _silent_disconnect(signal, slot): """Disconnects a signal from a slot, ignoring errors. Sometimes Qt might disconnect a signal automatically for unknown reasons. """ try: signal.disconnect(slot) except (TypeError, RuntimeError): # pragma: no cover pass pytest-qt-3.2.2/pytestqt/exceptions.py0000664000372000037200000000634713404516400020771 0ustar travistravis00000000000000import functools import sys import traceback from contextlib import contextmanager import pytest from pytestqt.utils import get_marker @contextmanager def capture_exceptions(): """ Context manager that captures exceptions that happen insides its context, and returns them as a list of (type, value, traceback) after the context ends. """ manager = _QtExceptionCaptureManager() manager.start() try: yield manager.exceptions finally: manager.finish() def _except_hook(type_, value, tback, exceptions=None): """Hook functions installed by _QtExceptionCaptureManager""" exceptions.append((type_, value, tback)) sys.stderr.write(format_captured_exceptions([(type_, value, tback)])) class _QtExceptionCaptureManager(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``. """ self.old_hook = sys.excepthook sys.excepthook = functools.partial(_except_hook, exceptions=self.exceptions) def finish(self): """Stop exception capturing, restoring the original hook. Can be called multiple times. """ if self.old_hook is not None: sys.excepthook = self.old_hook self.old_hook = None def fail_if_exceptions_occurred(self, when): """calls pytest.fail() with an informative message if exceptions have been captured so far. Before pytest.fail() is called, also finish capturing. """ if self.exceptions: self.finish() exceptions = self.exceptions self.exceptions = [] prefix = "%s ERROR: " % when msg = prefix + format_captured_exceptions(exceptions) del exceptions[:] # Don't keep exceptions alive longer. pytest.fail(msg, pytrace=False) def format_captured_exceptions(exceptions): """ Formats exceptions given as (type, value, traceback) into a string suitable to display as a test failure. """ if sys.version_info.major == 2: from StringIO import StringIO else: from io import StringIO stream = StringIO() stream.write("Qt exceptions in virtual methods:\n") sep = "_" * 80 + "\n" stream.write(sep) for (exc_type, value, tback) in exceptions: traceback.print_exception(exc_type, value, tback, file=stream) stream.write(sep) return stream.getvalue() def _is_exception_capture_enabled(item): """returns if exception capture is disabled for the given test item. """ disabled = get_marker(item, "qt_no_exception_capture") or item.config.getini( "qt_no_exception_capture" ) return not disabled class TimeoutError(Exception): """ .. versionadded:: 2.1 Exception thrown by :class:`pytestqt.qtbot.QtBot` methods. .. 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-3.2.2/pytestqt/qt_compat.py0000664000372000037200000002341013404516400020565 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 import sys from collections import namedtuple import os VersionTuple = namedtuple("VersionTuple", "qt_api, qt_api_version, runtime, compiled") def _import(name): __import__(name) class _QtApi: """ Interface to the underlying Qt API currently configured for pytest-qt. This object lazily loads all class references and other objects when the ``set_qt_api`` method gets called, providing a uniform way to access the Qt classes. """ def __init__(self): self._import_errors = {} def _get_qt_api_from_env(self): api = os.environ.get("PYTEST_QT_API") 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 as e: self._import_errors[name] = str(e) return False # Note, not importing only the root namespace because when uninstalling from conda, # the namespace can still be there. if _can_import("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 errors = "\n".join( " {}: {}".format(module, reason) for module, reason in sorted(self._import_errors.items()) ) msg = ( "pytest-qt requires either PySide, PySide2, PyQt4 or PyQt5 to be installed\n" + errors ) 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 # qInfo is not exposed in PySide2 (#232) if hasattr(QtCore, "QMessageLogger"): self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg) elif hasattr(QtCore, "qInfo"): self.qInfo = QtCore.qInfo else: self.qInfo = None self.qDebug = QtCore.qDebug self.qWarning = QtCore.qWarning self.qCritical = QtCore.qCritical self.qFatal = QtCore.qFatal self.QtInfoMsg = getattr(QtCore, "QtInfoMsg", None) 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 if hasattr(QtGui, "QStringListModel"): self.QStringListModel = QtGui.QStringListModel else: self.QStringListModel = QtCore.QStringListModel self.QStandardItem = QtGui.QStandardItem self.QStandardItemModel = QtGui.QStandardItemModel self.QAbstractListModel = QtCore.QAbstractListModel self.QAbstractTableModel = QtCore.QAbstractTableModel 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 # PySide never exposes QString self.QString = None 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 # QString exposed for our model tests if self.pytest_qt_api == "pyqt4" and sys.version_info.major == 2: self.QString = QtCore.QString else: # PyQt4 api v2 and pyqt5 only exposes native strings self.QString = None 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-3.2.2/pytestqt/__init__.py0000664000372000037200000000016513404516400020337 0ustar travistravis00000000000000# _version is automatically generated by setuptools_scm from pytestqt._version import version __version__ = version pytest-qt-3.2.2/pytestqt/qtbot.py0000664000372000037200000006306213404516400017736 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, CallbackBlocker, CallbackCalledTwiceError, ) def _parse_ini_boolean(value): if value in (True, False): return value try: return {"true": True, "false": False}[value.lower()] except KeyError: raise ValueError("unknown string for bool: %r" % value) class QtBot(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:: 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 _should_raise(self, raising_arg): ini_val = self._request.config.getini("qt_default_raising") legacy_ini_val = self._request.config.getini("qt_wait_signal_raising") if raising_arg is not None: return raising_arg elif legacy_ini_val: return _parse_ini_boolean(legacy_ini_val) elif ini_val: return _parse_ini_boolean(ini_val) else: return True 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_default_raising = false`` is set in the config. :param Callable check_params_cb: Optional ``callable`` that compares the provided signal parameters to some expected parameters. It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. :returns: ``SignalBlocker`` object. Call ``SignalBlocker.wait()`` to wait. .. note:: 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) """ raising = self._should_raise(raising) 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_default_raising = false`` is set in the config. :param list check_params_cbs: optional list of callables that compare the provided signal parameters to some expected parameters. Each callable has to match the signature of the corresponding signal in ``signals`` (just like a slot function would) and return ``True`` if parameters match, ``False`` otherwise. Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the corresponding signal. If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised. :param str order: Determines the order in which to expect signals: - ``"none"``: no order is enforced - ``"strict"``: signals have to be emitted strictly in the provided order (e.g. fails when expecting signals [a, b] and [a, a, b] is emitted) - ``"simple"``: like "strict", but signals may be emitted in-between the provided ones, e.g. expected ``signals == [a, b, c]`` and actually emitted ``signals = [a, a, b, a, c]`` works (would fail with ``"strict"``). :returns: ``MultiSignalBlocker`` object. Call ``MultiSignalBlocker.wait()`` to wait. .. note:: 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'") raising = self._should_raise(raising) if check_params_cbs: if len(check_params_cbs) != len(signals): raise ValueError( "Number of callbacks ({}) does not " "match number of signals ({})!".format( len(check_params_cbs), len(signals) ) ) blocker = MultiSignalBlocker( timeout=timeout, raising=raising, order=order, check_params_cbs=check_params_cbs, ) 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, wait=0): """ .. versionadded:: 1.11 Make sure the given ``signal`` doesn't get emitted. :param int wait: How many milliseconds to wait to make sure the signal isn't emitted asynchronously. By default, this method returns immediately and only catches signals emitted inside the ``with``-block. This is intended to be used as a context manager. .. note:: This method is also available as ``assert_not_emitted`` (pep-8 alias) """ spy = SignalEmittedSpy(signal) with spy, self.waitSignal(signal, timeout=wait, raising=False): yield spy.assert_not_emitted() 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 def waitCallback(self, timeout=1000, raising=None): """ .. versionadded:: 3.1 Stops current test until a callback is called. Used to stop the control flow of a test until the returned callback is called, or a number of milliseconds, specified by ``timeout``, has elapsed. Best used as a context manager:: with qtbot.waitCallback() as callback: function_taking_a_callback(callback) assert callback.args == [True] Also, you can use the :class:`CallbackBlocker` directly if the context manager form is not convenient:: blocker = qtbot.waitCallback(timeout=1000) function_calling_a_callback(blocker) blocker.wait() :param int timeout: How many milliseconds to wait before resuming control flow. :param bool raising: If :class:`QtBot.TimeoutError ` should be raised if a timeout occurred. This defaults to ``True`` unless ``qt_default_raising = false`` is set in the config. :returns: A ``CallbackBlocker`` object which can be used directly as a callback as it implements ``__call__``. .. note:: This method is also available as ``wait_callback`` (pep-8 alias) """ raising = self._should_raise(raising) blocker = CallbackBlocker(timeout=timeout, raising=raising) return blocker wait_callback = waitCallback # 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", "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 QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError 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-3.2.2/pytestqt/_version.py0000664000372000037200000000016413404516624020433 0ustar travistravis00000000000000# coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = '3.2.2' pytest-qt-3.2.2/pytestqt/utils.py0000664000372000037200000000052313404516400017736 0ustar travistravis00000000000000def get_marker(item, name): """Get a marker from a pytest item. This is here in order to stay compatible with pytest < 3.6 and not produce any deprecation warnings with >= 3.6. """ try: return item.get_closest_marker(name) except AttributeError: # pytest < 3.6 return item.get_marker(name) pytest-qt-3.2.2/pytestqt/plugin.py0000664000372000037200000001541713404516400020104 0ustar travistravis00000000000000import warnings import 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_default_raising", "Default value for the raising parameter of qtbot.waitSignal/waitCallback", ) parser.addini( "qt_wait_signal_raising", "Default value for the raising parameter of qtbot.waitSignal (legacy alias)", ) default_log_fail = QtLoggingPlugin.LOG_FAIL_OPTIONS[0] parser.addini( "qt_log_level_fail", 'log level in which tests can fail: {} (default: "{}")'.format( QtLoggingPlugin.LOG_FAIL_OPTIONS, default_log_fail ), default=default_log_fail, ) parser.addini( "qt_log_ignore", "list of regexes for messages that should not cause a tests " "to fails", type="linelist", ) group = parser.getgroup("qt", "qt testing") group.addoption( "--no-qt-log", dest="qt_log", action="store_false", default=True, help="disable pytest-qt logging capture", ) group.addoption( "--qt-log-format", dest="qt_log_format", default=None, help="defines how qt log messages are displayed.", ) @pytest.mark.hookwrapper @pytest.mark.tryfirst def pytest_runtest_setup(item): """ Hook called after before test setup starts, to start capturing exceptions as early as possible. """ capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: item.qt_exception_capture_manager = _QtExceptionCaptureManager() item.qt_exception_capture_manager.start() yield _process_events() if capture_enabled: item.qt_exception_capture_manager.fail_if_exceptions_occurred("SETUP") @pytest.mark.hookwrapper @pytest.mark.tryfirst def pytest_runtest_call(item): yield _process_events() capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: item.qt_exception_capture_manager.fail_if_exceptions_occurred("CALL") @pytest.mark.hookwrapper @pytest.mark.trylast def pytest_runtest_teardown(item): """ Hook called after each test tear down, to process any pending events and avoiding leaking events to the next test. Also, if exceptions have been captured during fixtures teardown, fail the test. """ _process_events() _close_widgets(item) _process_events() yield _process_events() capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: item.qt_exception_capture_manager.fail_if_exceptions_occurred("TEARDOWN") item.qt_exception_capture_manager.finish() def _process_events(): """Calls app.processEvents() while taking care of capturing exceptions or not based on the given item's configuration. """ app = qt_api.QApplication.instance() if app is not None: app.processEvents() def pytest_configure(config): config.addinivalue_line( "markers", "qt_no_exception_capture: Disables pytest-qt's automatic exception " "capture for just one test item.", ) config.addinivalue_line( "markers", "qt_log_level_fail: overrides qt_log_level_fail ini option." ) config.addinivalue_line( "markers", "qt_log_ignore: overrides qt_log_ignore ini option." ) config.addinivalue_line("markers", "no_qt_log: Turn off Qt logging capture.") if config.getoption("qt_log"): config.pluginmanager.register(QtLoggingPlugin(config), "_qt_logging") qt_api.set_qt_api(config.getini("qt_api")) if config.getini("qt_wait_signal_raising"): warnings.warn( "qt_wait_signal_raising is deprecated, use qt_default_raising instead.", DeprecationWarning, ) 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-3.2.2/pytestqt/logging.py0000664000372000037200000003011313404516400020222 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 from pytestqt.utils import get_marker 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", "INFO"] def __init__(self, config): self.config = config def pytest_runtest_setup(self, item): if get_marker(item, "no_qt_log"): return m = get_marker(item, "qt_log_ignore") if m: if not set(m.kwargs).issubset({"extend"}): raise ValueError( "Invalid keyword arguments in {!r} for " "qt_log_ignore mark.".format(m.kwargs) ) if m.kwargs.get("extend", True): config_regexes = self.config.getini("qt_log_ignore") ignore_regexes = config_regexes + list(m.args) else: ignore_regexes = m.args else: ignore_regexes = self.config.getini("qt_log_ignore") item.qt_log_capture = _QtMessageCapture(ignore_regexes) item.qt_log_capture._start() @pytest.mark.hookwrapper def pytest_runtest_makereport(self, item, call): """Add captured Qt messages to test item report if the call failed.""" outcome = yield if not hasattr(item, "qt_log_capture"): return if call.when == "call": report = outcome.get_result() m = get_marker(item, "qt_log_level_fail") if m: log_fail_level = m.args[0] else: log_fail_level = self.config.getini("qt_log_level_fail") assert log_fail_level in QtLoggingPlugin.LOG_FAIL_OPTIONS # make test fail if any records were captured which match # log_fail_level if report.outcome != "failed": for rec in item.qt_log_capture.records: is_modeltest_error = ( rec.context is not None and rec.context.category == "qt.modeltest" and rec.matches_level("WARNING") ) if ( rec.matches_level(log_fail_level) and not rec.ignored ) or is_modeltest_error: report.outcome = "failed" if report.longrepr is None: report.longrepr = _QtLogLevelErrorRepr( item, log_fail_level, is_modeltest_error ) break # if test has failed, add recorded messages to its terminal # representation if not report.passed: long_repr = getattr(report, "longrepr", None) if hasattr(long_repr, "addsection"): # pragma: no cover log_format = self.config.getoption("qt_log_format") context_format = None if log_format is None: if qt_api.pytest_qt_api == "pyqt5": context_format = "{rec.context.file}:{rec.context.function}:{rec.context.line}:\n" log_format = " {rec.type_name}: {rec.message}" else: context_format = None log_format = "{rec.type_name}: {rec.message}" lines = [] for rec in item.qt_log_capture.records: suffix = " (IGNORED)" if rec.ignored else "" if ( rec.context is not None and ( rec.context.file is not None or rec.context.function is not None or rec.context.line != 0 ) and context_format is not None ): context_line = context_format.format(rec=rec) lines.append(context_line) 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 category") def _append_new_record(self, msg_type, message, context): """ Creates a new Record instance and stores it. :param msg_type: Qt message typ :param message: message string, if bytes it will be converted to str. :param context: QMessageLogContext object or None """ def to_unicode(s): if isinstance(s, bytes): s = s.decode("utf-8", "replace") return s message = to_unicode(message) ignored = False for regex in self._ignore_regexes: if re.search(regex, message) is not None: ignored = True break if context is not None: context = self._Context( to_unicode(context.file), to_unicode(context.function), context.line, to_unicode(context.category), ) self._records.append(Record(msg_type, message, ignored, context)) def _handle_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: ``"QtInfoMsg"``, ``"QtDebugMsg"``, ``"QtWarningMsg"`` or ``"QtCriticalMsg"``. :ivar str log_type_name: type name similar to the logging package: ``INFO``, ``DEBUG``, ``WARNING`` and ``CRITICAL``. :ivar datetime.datetime when: when the message was captured :ivar bool ignored: If this record matches a regex from the "qt_log_ignore" option. :ivar context: a namedtuple containing the attributes ``file``, ``function``, ``line``. 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", } if qt_api.QtInfoMsg is not None: cls._type_name_map[qt_api.QtInfoMsg] = "QtInfoMsg" return cls._type_name_map[msg_type] @classmethod def _get_log_type_name(cls, msg_type): """ Return a string representation of the given QtMsgType enum value in the same style used by the builtin logging package. """ if not getattr(cls, "_log_type_name_map", None): cls._log_type_name_map = { qt_api.QtDebugMsg: "DEBUG", qt_api.QtWarningMsg: "WARNING", qt_api.QtCriticalMsg: "CRITICAL", qt_api.QtFatalMsg: "FATAL", } if qt_api.QtInfoMsg is not None: cls._log_type_name_map[qt_api.QtInfoMsg] = "INFO" return cls._log_type_name_map[msg_type] def matches_level(self, level): assert level in QtLoggingPlugin.LOG_FAIL_OPTIONS if level == "NO": return False elif level == "INFO": return self.log_type_name in ("INFO", "DEBUG", "WARNING", "CRITICAL") elif level == "DEBUG": return self.log_type_name in ("DEBUG", "WARNING", "CRITICAL") elif level == "WARNING": return self.log_type_name in ("WARNING", "CRITICAL") elif level == "CRITICAL": return self.log_type_name in ("CRITICAL",) else: # pragma: no cover raise ValueError("log_fail_level unknown: {}".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, is_modeltest_error): if is_modeltest_error: msg = "Qt modeltester errors" else: msg = "Failure: Qt messages with level {0} or above emitted" path, line_index, _ = item.location self.fileloc = ReprFileLocation( path, lineno=line_index + 1, message=msg.format(level.upper()) ) self.sections = [] def addsection(self, name, content, sep="-"): self.sections.append((name, content, sep)) def toterminal(self, out): self.fileloc.toterminal(out) for name, content, sep in self.sections: out.sep(sep, name) out.line(content) pytest-qt-3.2.2/pytestqt/modeltest.py0000664000372000037200000007271513404516400020612 0ustar travistravis00000000000000# encoding: UTF-8 # This file is based on the original C++ qabstractitemmodeltester.cpp from: # http://code.qt.io/cgit/qt/qtbase.git/tree/src/testlib/qabstractitemmodeltester.cpp # Commit 4af292fe5158c2d19e8ab1351c71c3940c7f1032 # # Licensed under the following terms: # # Copyright (C) 2016 The Qt Company Ltd. # Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, # info@kdab.com, author Giuseppe D'Angelo # Contact: https://www.qt.io/licensing/ # # This file is part of the QtTest module of the Qt Toolkit. # # $QT_BEGIN_LICENSE:LGPL$ # Commercial License Usage # Licensees holding valid commercial Qt licenses may use this file in # accordance with the commercial license agreement provided with the # Software or, alternatively, in accordance with the terms contained in # a written agreement between you and The Qt Company. For licensing terms # and conditions see https://www.qt.io/terms-conditions. For further # information use the contact form at https://www.qt.io/contact-us. # # GNU Lesser General Public License Usage # Alternatively, this file may be used under the terms of the GNU Lesser # General Public License version 3 as published by the Free Software # Foundation and appearing in the file LICENSE.LGPL3 included in the # packaging of this file. Please review the following information to # ensure the GNU Lesser General Public License version 3 requirements # will be met: https://www.gnu.org/licenses/lgpl-3.0.html. # # GNU General Public License Usage # Alternatively, this file may be used under the terms of the GNU # General Public License version 2.0 or (at your option) the GNU General # Public license version 3 or any later version approved by the KDE Free # Qt Foundation. The licenses are as published by the Free Software # Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 # included in the packaging of this file. Please review the following # information to ensure the GNU General Public License requirements will # be met: https://www.gnu.org/licenses/gpl-2.0.html and # https://www.gnu.org/licenses/gpl-3.0.html. # # $QT_END_LICENSE$ from __future__ import print_function import collections import sys from pytestqt.qt_compat import qt_api _Changing = collections.namedtuple("_Changing", "parent, old_size, last, next") HAS_QT_TESTER = hasattr(qt_api.QtTest, "QAbstractItemModelTester") class ModelTester: """A tester for Qt's QAbstractItemModels.""" def __init__(self, config): self._model = None self._fetching_more = None self._insert = None self._remove = None self._changing = [] self._qt_tester = None def _debug(self, text): print("modeltest: " + text) def _modelindex_debug(self, index): """Get a string for debug output for a QModelIndex.""" if index is None: return "" elif not index.isValid(): return " (0x{:x})".format(id(index)) else: data = self._model.data(index, qt_api.QtCore.Qt.DisplayRole) return "{}/{} {!r} (0x{:x})".format( index.row(), index.column(), qt_api.extract_from_variant(data), id(index), ) def check(self, model, force_py=False): """Runs a series of checks in the given model. Connect to all of the models signals. Whenever anything happens recheck everything. :param model: The ``QAbstractItemModel`` to test. :param force_py: Force using the Python implementation, even if the C++ implementation is available. """ assert model is not None if HAS_QT_TESTER and not force_py: reporting_mode = ( qt_api.QtTest.QAbstractItemModelTester.FailureReportingMode.Warning ) self._qt_tester = qt_api.QtTest.QAbstractItemModelTester( model, reporting_mode ) self._debug("Using Qt C++ tester") return self._debug("Using Python tester") self._model = model self._fetching_more = False self._insert = [] self._remove = [] self._changing = [] self._model.columnsAboutToBeInserted.connect(self._run) self._model.columnsAboutToBeRemoved.connect(self._run) self._model.columnsInserted.connect(self._run) self._model.columnsRemoved.connect(self._run) self._model.dataChanged.connect(self._run) self._model.headerDataChanged.connect(self._run) self._model.layoutAboutToBeChanged.connect(self._run) self._model.layoutChanged.connect(self._run) self._model.modelReset.connect(self._run) self._model.rowsAboutToBeInserted.connect(self._run) self._model.rowsAboutToBeRemoved.connect(self._run) self._model.rowsInserted.connect(self._run) self._model.rowsRemoved.connect(self._run) # Special checks for changes self._model.layoutAboutToBeChanged.connect(self._on_layout_about_to_be_changed) self._model.layoutChanged.connect(self._on_layout_changed) self._model.rowsAboutToBeInserted.connect(self._on_rows_about_to_be_inserted) self._model.rowsAboutToBeRemoved.connect(self._on_rows_about_to_be_removed) self._model.rowsInserted.connect(self._on_rows_inserted) self._model.rowsRemoved.connect(self._on_rows_removed) self._model.dataChanged.connect(self._on_data_changed) self._model.headerDataChanged.connect(self._on_header_data_changed) self._run() def _cleanup(self): """Not API intended for users, but called from the fixture function.""" if self._model is None: return self._model.columnsAboutToBeInserted.disconnect(self._run) self._model.columnsAboutToBeRemoved.disconnect(self._run) self._model.columnsInserted.disconnect(self._run) self._model.columnsRemoved.disconnect(self._run) self._model.dataChanged.disconnect(self._run) self._model.headerDataChanged.disconnect(self._run) self._model.layoutAboutToBeChanged.disconnect(self._run) self._model.layoutChanged.disconnect(self._run) self._model.modelReset.disconnect(self._run) self._model.rowsAboutToBeInserted.disconnect(self._run) self._model.rowsAboutToBeRemoved.disconnect(self._run) self._model.rowsInserted.disconnect(self._run) self._model.rowsRemoved.disconnect(self._run) self._model.layoutAboutToBeChanged.disconnect( self._on_layout_about_to_be_changed ) self._model.layoutChanged.disconnect(self._on_layout_changed) self._model.rowsAboutToBeInserted.disconnect(self._on_rows_about_to_be_inserted) self._model.rowsAboutToBeRemoved.disconnect(self._on_rows_about_to_be_removed) self._model.rowsInserted.disconnect(self._on_rows_inserted) self._model.rowsRemoved.disconnect(self._on_rows_removed) self._model.dataChanged.disconnect(self._on_data_changed) self._model.headerDataChanged.disconnect(self._on_header_data_changed) self._model = None def _run(self): assert self._model is not None assert self._fetching_more is not None if self._fetching_more: return self._test_basic() self._test_row_count_and_column_count() self._test_has_index() self._test_index() self._test_parent() self._test_data() def _test_basic(self): """Try to call a number of the basic functions (not all). Make sure the model doesn't outright segfault, testing the functions which make sense. """ assert not self._model.buddy(qt_api.QtCore.QModelIndex()).isValid() self._model.canFetchMore(qt_api.QtCore.QModelIndex()) assert self._column_count(qt_api.QtCore.QModelIndex()) >= 0 self._fetch_more(qt_api.QtCore.QModelIndex()) flags = self._model.flags(qt_api.QtCore.QModelIndex()) assert flags == qt_api.QtCore.Qt.ItemIsDropEnabled or not flags self._has_children(qt_api.QtCore.QModelIndex()) has_row = self._model.hasIndex(0, 0) if has_row: cache = None self._model.match(self._model.index(0, 0), -1, cache) self._model.mimeTypes() assert not self._parent(qt_api.QtCore.QModelIndex()).isValid() assert self._model.rowCount() >= 0 self._model.span(qt_api.QtCore.QModelIndex()) self._model.supportedDropActions() self._model.roleNames() def _test_row_count_and_column_count(self): """Test model's implementation of row/columnCount() and hasChildren(). Models that are dynamically populated are not as fully tested here. The models rowCount() is tested more extensively in _check_children(), but this catches the big mistakes. """ # check top row top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) rows = self._model.rowCount(top_index) assert rows >= 0 columns = self._column_count(top_index) assert columns >= 0 if rows == 0 or columns == 0: return assert self._has_children(top_index) second_level_index = self._model.index(0, 0, top_index) assert second_level_index.isValid() rows = self._model.rowCount(second_level_index) assert rows >= 0 columns = self._column_count(second_level_index) assert columns >= 0 if rows == 0 or columns == 0: return assert self._has_children(second_level_index) def _test_has_index(self): """Test model's implementation of hasIndex(). hasIndex() is tested more extensively in _check_children(), but this catches the big mistakes. """ # Make sure that invalid values return an invalid index assert not self._model.hasIndex(-2, -2) assert not self._model.hasIndex(-2, 0) assert not self._model.hasIndex(0, -2) rows = self._model.rowCount() columns = self._column_count() # check out of bounds assert not self._model.hasIndex(rows, columns) assert not self._model.hasIndex(rows + 1, columns + 1) if rows > 0 and columns > 0: assert self._model.hasIndex(0, 0) def _test_index(self): """Test model's implementation of index(). index() is tested more extensively in _check_children(), but this catches the big mistakes. """ rows = self._model.rowCount() columns = self._column_count() for row in range(rows): for column in range(columns): # Make sure that the same index is *always* returned a = self._model.index(row, column) b = self._model.index(row, column) assert a.isValid() assert b.isValid() assert a == b def _test_parent(self): """Tests model's implementation of QAbstractItemModel::parent().""" # Make sure the model won't crash and will return an invalid # QModelIndex when asked for the parent of an invalid index. assert not self._parent(qt_api.QtCore.QModelIndex()).isValid() if not self._has_children(): return # Column 0 | Column 1 | # QModelIndex() | | # \- top_index | top_index_1 | # \- child_index | child_index_1 | # Common error test #1, make sure that a top level index has a parent # that is a invalid QModelIndex. top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) assert not self._parent(top_index).isValid() # Common error test #2, make sure that a second level index has a # parent that is the first level index. if self._has_children(top_index): child_index = self._model.index(0, 0, top_index) assert self._parent(child_index) == top_index # Common error test #3, the second column should NOT have the same # children as the first column in a row. # Usually the second column shouldn't have children. if self._model.hasIndex(0, 1): top_index_1 = self._model.index(0, 1, qt_api.QtCore.QModelIndex()) if self._has_children(top_index) and self._has_children(top_index_1): child_index = self._model.index(0, 0, top_index) assert child_index.isValid() child_index_1 = self._model.index(0, 0, top_index_1) assert child_index_1.isValid() assert child_index != child_index_1 # Full test, walk n levels deep through the model making sure that all # parent's children correctly specify their parent. self._check_children(qt_api.QtCore.QModelIndex()) def _check_children(self, parent, current_depth=0): """Check parent/children relationships. Called from the parent() test. A model that returns an index of parent X should also return X when asking for the parent of the index. This recursive function does pretty extensive testing on the whole model in an effort to catch edge cases. This function assumes that rowCount(), columnCount() and index() already work. If they have a bug it will point it out, but the above tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one. """ # First just try walking back up the tree. p = parent while p.isValid(): p = p.parent() # For models that are dynamically populated if self._model.canFetchMore(parent): self._fetch_more(parent) rows = self._model.rowCount(parent) columns = self._column_count(parent) if rows > 0: assert self._has_children(parent) # Some further testing against rows(), columns(), and hasChildren() assert rows >= 0 assert columns >= 0 if rows > 0 and columns > 0: assert self._has_children(parent) self._debug( "Checking children of {} with depth {} " "({} rows, {} columns)".format( self._modelindex_debug(parent), current_depth, rows, columns ) ) top_left_child = self._model.index(0, 0, parent) assert not self._model.hasIndex(rows, 0, parent) assert not self._model.hasIndex(rows + 1, 0, parent) for r in range(rows): assert not self._model.hasIndex(r, columns, parent) assert not self._model.hasIndex(r, columns + 1, parent) for c in range(columns): assert self._model.hasIndex(r, c, parent) index = self._model.index(r, c, parent) # rowCount() and columnCount() said that it existed... if not index.isValid(): self._debug( "Got invalid index at row={} col={} parent={}".format( r, c, self._modelindex_debug(parent) ) ) assert index.isValid() # index() should always return the same index when called twice # in a row modified_index = self._model.index(r, c, parent) assert index == modified_index sibling = self._model.sibling(r, c, top_left_child) assert index == sibling sibling2 = top_left_child.sibling(r, c) assert index == sibling2 # Some basic checking on the index that is returned assert index.model() == self._model assert index.row() == r assert index.column() == c # If the next test fails here is some somewhat useful debug you # play with. if self._parent(index) != parent: self._debug( "Inconsistent parent() implementation detected\n" " index={} exp. parent={} act. parent={}\n" " row={} col={} depth={}\n" " data for child: {}\n" " data for parent: {}\n".format( self._modelindex_debug(index), self._modelindex_debug(parent), self._modelindex_debug(self._parent(index)), r, c, current_depth, self._model.data(index), self._model.data(parent), ) ) # Check that we can get back our real parent. assert self._parent(index) == parent # recursively go down the children if self._has_children(index) and current_depth < 10: self._debug( "{} has {} children".format( self._modelindex_debug(index), self._model.rowCount(index) ) ) self._check_children(index, current_depth + 1) # make sure that after testing the children that the index # doesn't change. newer_index = self._model.index(r, c, parent) assert index == newer_index self._debug("Children check for {} done".format(self._modelindex_debug(parent))) def _test_data(self): """Test model's implementation of data()""" if not self._has_children(): return # A valid index should have a valid QVariant data assert self._model.index(0, 0).isValid() string_types = [str] if sys.version_info.major == 2: string_types.append(unicode) # noqa if qt_api.QString is not None: string_types.append(qt_api.QString) string_types = tuple(string_types) types = [ (qt_api.QtCore.Qt.DisplayRole, string_types), (qt_api.QtCore.Qt.ToolTipRole, string_types), (qt_api.QtCore.Qt.StatusTipRole, string_types), (qt_api.QtCore.Qt.WhatsThisRole, string_types), (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), ), ( qt_api.QtCore.Qt.DecorationRole, ( qt_api.QtGui.QPixmap, qt_api.QtGui.QImage, qt_api.QtGui.QIcon, qt_api.QtGui.QColor, qt_api.QtGui.QBrush, ), ), ] # General purpose roles with a fixed expected type for role, typ in types: data = self._model.data(self._model.index(0, 0), role) if data is not None: data = qt_api.extract_from_variant(data) assert data == None or isinstance(data, typ), role # noqa # Check that the alignment is one we know about alignment = self._model.data( self._model.index(0, 0), qt_api.QtCore.Qt.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) if start > 0 else None next_data = self._model.data(next_index) if start < parent_rowcount else None c = _Changing( parent=parent, old_size=parent_rowcount, last=last_data, next=next_data ) self._insert.append(c) def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._insert.pop() last_data = ( self._model.data(self._model.index(start - 1, 0, parent)) if start - 1 >= 0 else None ) next_data = ( self._model.data(self._model.index(end + 1, 0, c.parent)) if end + 1 < self._model.rowCount(c.parent) else None ) expected_size = c.old_size + (end - start + 1) current_size = self._model.rowCount(parent) self._debug("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 if last_data is not None: assert c.last == last_data if next_data is not None: assert c.next == next_data def _on_layout_about_to_be_changed(self): for i in range(max(self._model.rowCount(), 100)): idx = qt_api.QtCore.QPersistentModelIndex(self._model.index(i, 0)) self._changing.append(idx) def _on_layout_changed(self): for p in self._changing: assert p == self._model.index(p.row(), p.column(), p.parent()) self._changing = [] def _on_rows_about_to_be_removed(self, parent, start, end): """Store what is about to be removed to make sure it actually happens. This gets stored to make sure it actually happens in rowsRemoved. """ parent_rowcount = self._model.rowCount(parent) last_index = self._model.index(start - 1, 0, parent) if start > 0 else None next_index = ( self._model.index(end + 1, 0, parent) if end < parent_rowcount - 1 else None ) self._debug( "rows about to be removed: start {}, end {}, parent {}, " "parent row count {}, last item {}, next item {}".format( start, end, self._modelindex_debug(parent), parent_rowcount, self._modelindex_debug(last_index), self._modelindex_debug(next_index), ) ) if last_index is not None: assert last_index.isValid() if next_index is not None: assert next_index.isValid() last_data = None if last_index is None else self._model.data(last_index) next_data = None if next_index is None else self._model.data(next_index) c = _Changing( parent=parent, old_size=parent_rowcount, last=last_data, next=next_data ) self._remove.append(c) def _on_rows_removed(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._remove.pop() last_data = ( self._model.data(self._model.index(start - 1, 0, c.parent)) if start > 0 else None ) next_data = ( self._model.data(self._model.index(start, 0, c.parent)) if end < c.old_size - 1 else None ) current_size = self._model.rowCount(parent) expected_size = c.old_size - (end - start + 1) self._debug("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 if last_data is not None: assert c.last == last_data if next_data is not None: assert c.next == next_data def _on_data_changed(self, top_left, bottom_right): assert top_left.isValid() assert bottom_right.isValid() common_parent = bottom_right.parent() assert top_left.parent() == common_parent assert top_left.row() <= bottom_right.row() assert top_left.column() <= bottom_right.column() row_count = self._model.rowCount(common_parent) column_count = self._column_count(common_parent) assert bottom_right.row() < row_count assert bottom_right.column() < column_count def _on_header_data_changed(self, orientation, start, end): assert orientation in [qt_api.QtCore.Qt.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-3.2.2/.project0000664000372000037200000000055313404516400016001 0ustar travistravis00000000000000 pytest-qt org.python.pydev.PyDevBuilder org.python.pydev.pythonNature pytest-qt-3.2.2/appveyor.yml0000664000372000037200000000524613404516400016726 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.*" matrix: exclude: # PySide2 crashes: #202 - PYTEST_QT_API: "pyside2" 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/" skip_tags: true pytest-qt-3.2.2/tox.ini0000664000372000037200000000067513404516400015652 0ustar travistravis00000000000000[tox] envlist = py{36,37}-pyqt5, py{36,37}-pyside2, linting [testenv] deps= pytest pyside2: pyside2 pyqt5: pyqt5 commands= pytest {posargs} setenv= pyside2: PYTEST_QT_API=pyside2 pyqt5: PYTEST_QT_API=pyqt5 passenv=DISPLAY XAUTHORITY USER USERNAME [testenv:linting] skip_install = True basepython = python3.6 deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [flake8] max-line-length = 120 pytest-qt-3.2.2/.gitignore0000664000372000037200000000034513404516400016321 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 /.pytest_cache # auto-generated by setuptools_scm /pytestqt/_version.py pytest-qt-3.2.2/.travis.yml0000664000372000037200000000536713404516400016453 0ustar travistravis00000000000000language: python python: "3.6" sudo: required dist: trusty env: global: # used by ci-helpers - DEPS="pytest tox coveralls six" - MINICONDA_VERSION=latest - DISPLAY=":99.0" matrix: - LINTING=1 - 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 matrix: # PySide2 crashes: #202 allow_failures: - env: PYTEST_QT_API=pyside2 PYQT_PACKAGE="pyside2=2.*" PYTHON_VERSION=3.5 - env: PYTEST_QT_API=pyside2 PYQT_PACKAGE="pyside2=2.*" PYTHON_VERSION=3.6 install: - | if [ $LINTING == "1" ]; then pip install -U pip pip install tox else # Xvfb / window manager sudo apt-get update sudo apt-get install -y xvfb herbstluftwm # courtesy of https://github.com/astropy/ci-helpers/blob/master/travis/setup_conda_linux.sh /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset # Setup miniconda wget https://repo.continuum.io/miniconda/Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh -O miniconda.sh bash miniconda.sh -b -p $HOME/miniconda export PATH="$HOME/miniconda/bin:$PATH" conda config --add channels conda-forge conda config --set always_yes yes --set changeps1 no conda create -n test --quiet python=${PYTHON_VERSION} ${DEPS} ${PYQT_PACKAGE} source activate test && pip install -e . fi before_script: - "herbstluftwm &" - sleep 1 script: - | if [ $LINTING == "1" ]; then tox -e linting else source activate test && catchsegv coverage run --source=pytestqt -m pytest -v tests fi 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-3.2.2/HOWTORELEASE.rst0000664000372000037200000000045513404516400016766 0ustar travistravis00000000000000Here are the steps on how to make a new release. 1. Create a ``release-VERSION`` branch from ``upstream/master``. 2. Update ``CHANGELOG.rst``. 3. Push a branch with the changes. 4. Once all builds pass, push a tag named ``VERSION`` to ``upstream``. 5. After the deployment is complete, merge the PR. pytest-qt-3.2.2/setup.cfg0000664000372000037200000000030613404516624016157 0ustar travistravis00000000000000[bdist_wheel] universal = 1 [tool:pytest] testpaths = tests addopts = --strict xfail_strict = true markers = filterwarnings: pytest's filterwarnings marker [egg_info] tag_build = tag_date = 0 pytest-qt-3.2.2/tests/0000775000372000037200000000000013404516624015501 5ustar travistravis00000000000000pytest-qt-3.2.2/tests/test_wait_signal.py0000664000372000037200000014371313404516400021414 0ustar travistravis00000000000000import functools import fnmatch import pytest import sys from pytestqt.qt_compat import qt_api from pytestqt.wait_signal import ( SignalEmittedError, TimeoutError, SignalAndArgs, CallbackCalledTwiceError, ) 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("delayed", [True, False]) def test_zero_timeout(qtbot, timer, delayed, signaller): """ With a zero timeout, we don't run a main loop, so only immediate signals are processed. """ with qtbot.waitSignal(signaller.signal, raising=False, timeout=0) as blocker: if delayed: timer.single_shot(signaller.signal, 0) else: signaller.signal.emit() assert blocker.signal_triggered != delayed @pytest.mark.parametrize( "configval, raises", [("false", False), ("true", True), (None, True)] ) @pytest.mark.parametrize("configkey", ["qt_wait_signal_raising", "qt_default_raising"]) def test_raising(qtbot, testdir, configkey, configval, raises): if configval is not None: testdir.makeini( """ [pytest] {} = {} """.format( configkey, 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 """ ) if configkey == "qt_wait_signal_raising" and configval is not None: with pytest.warns(DeprecationWarning): res = testdir.runpytest() else: res = testdir.runpytest() if raises: res.stdout.fnmatch_lines(["*1 failed*"]) else: res.stdout.fnmatch_lines(["*1 passed*"]) @pytest.mark.filterwarnings("ignore:qt_wait_signal_raising is deprecated") @pytest.mark.parametrize("configkey", ["qt_wait_signal_raising", "qt_default_raising"]) def test_raising_by_default_overridden(qtbot, testdir, configkey): testdir.makeini( """ [pytest] {} = false """.format( configkey ) ) 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("blocker", ["single", "multiple", "callback"]) @pytest.mark.parametrize("raising", [True, False]) def test_blockers_handle_exceptions(qtbot, blocker, raising, signaller): """ Make sure blockers handle exceptions correctly. """ class TestException(Exception): pass if blocker == "multiple": func = qtbot.waitSignals args = [[signaller.signal, signaller.signal_2]] elif blocker == "single": func = qtbot.waitSignal args = [signaller.signal] elif blocker == "callback": func = qtbot.waitCallback args = [] else: assert False with pytest.raises(TestException): with func(*args, timeout=10, raising=raising): raise TestException @pytest.mark.parametrize("multiple", [True, False]) @pytest.mark.parametrize("do_timeout", [True, False]) def test_wait_twice(qtbot, timer, multiple, do_timeout, signaller): """ https://github.com/pytest-dev/pytest-qt/issues/69 """ if multiple: func = qtbot.waitSignals arg = [signaller.signal] else: func = qtbot.waitSignal arg = signaller.signal if do_timeout: with func(arg, timeout=100, raising=False): timer.single_shot(signaller.signal, 200) with func(arg, timeout=100, raising=False): timer.single_shot(signaller.signal, 200) else: with func(arg): signaller.signal.emit() with func(arg): signaller.signal.emit() def test_wait_signals_invalid_strict_parameter(qtbot, signaller): with pytest.raises(ValueError): qtbot.waitSignals([signaller.signal], order="invalid") def test_destroyed(qtbot): """Test that waitSignal works with the destroyed signal (#82). 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() def test_emitted_late(self, qtbot, signaller, timer): with pytest.raises(SignalEmittedError): with qtbot.assertNotEmitted(signaller.signal, wait=100): timer.single_shot(signaller.signal, 10) def test_continues_when_emitted(self, qtbot, signaller, stop_watch): stop_watch.start() with pytest.raises(SignalEmittedError): with qtbot.assertNotEmitted(signaller.signal, wait=5000): signaller.signal.emit() stop_watch.check(4000) class TestWaitCallback: def test_immediate(self, qtbot): with qtbot.waitCallback() as callback: assert not callback.called callback() assert callback.called def test_later(self, qtbot): t = qt_api.QtCore.QTimer() t.setSingleShot(True) t.setInterval(50) with qtbot.waitCallback() as callback: t.timeout.connect(callback) t.start() assert callback.called def test_args(self, qtbot): with qtbot.waitCallback() as callback: callback(23, answer=42) assert callback.args == [23] assert callback.kwargs == {"answer": 42} def test_assert_called_with(self, qtbot): with qtbot.waitCallback() as callback: callback(23, answer=42) callback.assert_called_with(23, answer=42) def test_assert_called_with_wrong(self, qtbot): with qtbot.waitCallback() as callback: callback(23, answer=42) with pytest.raises(AssertionError): callback.assert_called_with(23) def test_explicit(self, qtbot): blocker = qtbot.waitCallback() assert not blocker.called blocker() blocker.wait() assert blocker.called def test_called_twice(self, qtbot): with pytest.raises(CallbackCalledTwiceError): with qtbot.waitCallback() as callback: callback() callback() def test_timeout_raising(self, qtbot): with pytest.raises(TimeoutError): with qtbot.waitCallback(timeout=10): pass def test_timeout_not_raising(self, qtbot): with qtbot.waitCallback(timeout=10, raising=False) as callback: pass assert not callback.called assert callback.args is None assert callback.kwargs is None pytest-qt-3.2.2/tests/test_wait_until.py0000664000372000037200000000314613404516400021265 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-3.2.2/tests/test_qtest_proxies.py0000664000372000037200000000145213404516400022015 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"), reason="fails on PyQt" ) @pytest.mark.parametrize( "expected_method", [ "keyPress", "keyClick", "keyClicks", "keyEvent", "keyPress", "keyRelease", pytest.param("keyToAscii", marks=fails_on_pyqt), "mouseClick", "mouseDClick", "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-3.2.2/tests/test_logging.py0000664000372000037200000003751313404516400020541 0ustar travistravis00000000000000import datetime import pytest from pytestqt.qt_compat import qt_api @pytest.mark.parametrize("test_succeeds", [True, False]) @pytest.mark.parametrize("qt_log", [True, False]) def test_basic_logging(testdir, test_succeeds, qt_log): """ Test Qt logging capture output. :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( """ import sys from pytestqt.qt_compat import qt_api def to_unicode(s): return s.decode('utf-8', 'replace') if isinstance(s, bytes) else s 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(): # qInfo is not exposed by the bindings yet (#225) # qt_api.qInfo('this is an INFO message') qt_api.qDebug('this is a DEBUG message') qt_api.qWarning('this is a WARNING message') qt_api.qCritical('this is a CRITICAL message') assert {} """.format( test_succeeds ) ) res = testdir.runpytest(*(["--no-qt-log"] if not qt_log else [])) if test_succeeds: assert "Captured Qt messages" not in res.stdout.str() assert "Captured stderr call" not in res.stdout.str() else: if qt_log: res.stdout.fnmatch_lines( [ "*-- Captured Qt messages --*", # qInfo is not exposed by the bindings yet (#232) # '*QtInfoMsg: this is an INFO message*', "*QtDebugMsg: this is a DEBUG message*", "*QtWarningMsg: this is a WARNING message*", "*QtCriticalMsg: this is a CRITICAL message*", ] ) else: res.stdout.fnmatch_lines( [ "*-- Captured stderr call --*", # qInfo is not exposed by the bindings yet (#232) # '*QtInfoMsg: this is an INFO message*', # 'this is an INFO message*', "this is a DEBUG message*", "this is a WARNING message*", "this is a CRITICAL message*", ] ) def test_qinfo(qtlog): """Test INFO messages when we have means to do so. Should be temporary until bindings catch up and expose qInfo (or at least QMessageLogger), then we should update the other logging tests properly. #232 """ if qt_api.pytest_qt_api.startswith("pyside"): assert ( qt_api.qInfo is None ), "pyside does not expose qInfo. If it does, update this test." return if qt_api.pytest_qt_api.startswith("pyqt4"): pytest.skip("qInfo and QtInfoMsg not supported in PyQt 4") qt_api.qInfo("this is an INFO message") records = [(m.type, m.message.strip()) for m in qtlog.records] assert records == [(qt_api.QtInfoMsg, "this is an INFO message")] def test_qtlog_fixture(qtlog): """ Test qtlog fixture. """ # qInfo is not exposed by the bindings yet (#232) qt_api.qDebug("this is a DEBUG message") qt_api.qWarning("this is a WARNING message") qt_api.qCritical("this is a CRITICAL message") records = [(m.type, m.message.strip()) for m in qtlog.records] assert records == [ (qt_api.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={}".format(f)) today = "{:%Y-%m-%d}".format(datetime.datetime.now()) res.stdout.fnmatch_lines( [ "*-- Captured Qt messages --*", "QtWarningMsg WARNING {}: 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 {} or above emitted*".format( level.upper() ), "*-- Captured Qt messages --*", ] ) lines.append("*{} 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, and it filters out the context information. :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, 0, None) log_capture._handle_with_context(qt_api.QtWarningMsg, context, "WARNING message") assert 0 """ ) res = testdir.runpytest() assert "*None:None:0:*" not in str(res.stdout) res.stdout.fnmatch_lines(["* QtWarningMsg: WARNING message*"]) def test_logging_broken_makereport(testdir): """ Make sure logging's makereport hookwrapper doesn't hide exceptions. See https://github.com/pytest-dev/pytest-qt/issues/98 :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( conftest=""" import pytest @pytest.mark.hookwrapper(tryfirst=True) def pytest_runtest_makereport(call): if call.when == 'call': raise Exception("This should not be hidden") yield """ ) p = testdir.makepyfile( """ def test_foo(): pass """ ) res = testdir.runpytest_subprocess(p) res.stdout.fnmatch_lines(["*This should not be hidden*"]) pytest-qt-3.2.2/tests/test_modeltest.py0000664000372000037200000002476713404516400021122 0ustar travistravis00000000000000import pytest from pytestqt.qt_compat import qt_api from pytestqt import modeltest pytestmark = pytest.mark.usefixtures("qtbot") class BasicModel(qt_api.QtCore.QAbstractItemModel): def data(self, index, role=qt_api.QtCore.Qt.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, force_py=True) def test_string_list_model(qtmodeltester): model = qt_api.QStringListModel() model.setStringList(["hello", "world"]) qtmodeltester.check(model, force_py=True) 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, force_py=True) @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=True) model.set_header_text("New Header") @pytest.fixture def check_model(qtmodeltester): """ Return a check_model(model, should_pass=True) function that uses qtmodeltester to check if the model is OK or not according to the ``should_pass`` parameter. """ def check(model, should_pass=True): if should_pass: qtmodeltester.check(model, force_py=True) else: with pytest.raises(AssertionError): qtmodeltester.check(model, force_py=True) return check def test_invalid_column_count(qtmodeltester): """Basic check with an invalid model.""" class Model(BasicModel): def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return -1 model = Model() with pytest.raises(AssertionError): qtmodeltester.check(model, force_py=True) def test_changing_model_insert(qtmodeltester): model = qt_api.QStandardItemModel() item = qt_api.QStandardItem("foo") qtmodeltester.check(model, force_py=True) 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, force_py=True) 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, force_py=True) 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, force_py=True) 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, force_py=True) model.sort(0) def test_nop(qtmodeltester): """We should not get a crash on cleanup with no model.""" pass def test_overridden_methods(qtmodeltester): """Make sure overriden methods of a model are actually run. With a previous implementation of the modeltester using sip.cast, the custom implementations did never actually run. """ class Model(BasicModel): def __init__(self, parent=None): super(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, force_py=True) 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, force_py=True) 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, force_py=True) @pytest.mark.skipif(not modeltest.HAS_QT_TESTER, reason="No Qt modeltester available") def test_qt_tester_valid(testdir): testdir.makepyfile( """ from pytestqt.qt_compat import qt_api from pytestqt import modeltest assert modeltest.HAS_QT_TESTER def test_ok(qtmodeltester): model = qt_api.QStandardItemModel() qtmodeltester.check(model) """ ) res = testdir.inline_run() res.assertoutcome(passed=1, failed=0) @pytest.mark.skipif(not modeltest.HAS_QT_TESTER, reason="No Qt modeltester available") def test_qt_tester_invalid(testdir): testdir.makeini( """ [pytest] qt_log_level_fail = NO """ ) testdir.makepyfile( """ from pytestqt.qt_compat import qt_api from pytestqt import modeltest assert modeltest.HAS_QT_TESTER class Model(qt_api.QtCore.QAbstractItemModel): def data(self, index, role=qt_api.QtCore.Qt.DisplayRole): return None def rowCount(self, parent=qt_api.QtCore.QModelIndex()): return 0 def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return -1 def index(self, row, column, parent=qt_api.QtCore.QModelIndex()): return qt_api.QtCore.QModelIndex() def parent(self, index): return qt_api.QtCore.QModelIndex() def test_ok(qtmodeltester): model = Model() qtmodeltester.check(model) """ ) res = testdir.runpytest() res.stdout.fnmatch_lines( [ "*__ test_ok __*", "test_qt_tester_invalid.py:*: Qt modeltester errors", "*-- Captured Qt messages --*", "* QtWarningMsg: FAIL! model->columnCount(QModelIndex()) >= 0 () returned FALSE " "(qabstractitemmodeltester.cpp:*)", "*-- Captured stdout call --*", "modeltest: Using Qt C++ tester", "*== 1 failed in * ==*", ] ) pytest-qt-3.2.2/tests/test_exceptions.py0000664000372000037200000002561713404516400021276 0ustar travistravis00000000000000import sys import pytest from pytestqt.exceptions import capture_exceptions, format_captured_exceptions @pytest.mark.parametrize("raise_error", [False, True]) def test_catch_exceptions_in_virtual_methods(testdir, raise_error): """ Catch exceptions that happen inside Qt 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}: try: raise RuntimeError('original error') except RuntimeError: raise ValueError('mistakes were made') return qt_api.QtCore.QObject.event(self, ev) def test_exceptions(qtbot): v = Receiver() app = qt_api.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: expected_lines = ["*Qt exceptions in virtual methods:*"] if sys.version_info.major == 3: expected_lines.append("RuntimeError: original error") expected_lines.extend(["*ValueError: mistakes were made*", "*1 failed*"]) result.stdout.fnmatch_lines(expected_lines) assert "pytest.fail" not in "\n".join(result.outlines) else: result.stdout.fnmatch_lines("*1 passed*") def test_format_captured_exceptions(): try: raise ValueError("errors were made") except ValueError: exceptions = [sys.exc_info()] obtained_text = format_captured_exceptions(exceptions) lines = obtained_text.splitlines() assert "Qt exceptions in virtual methods:" in lines assert "ValueError: errors were made" in lines @pytest.mark.skipif(sys.version_info.major == 2, reason="Python 3 only") def test_format_captured_exceptions_chained(): try: try: raise ValueError("errors were made") except ValueError: raise RuntimeError("error handling value error") except RuntimeError: exceptions = [sys.exc_info()] obtained_text = format_captured_exceptions(exceptions) lines = obtained_text.splitlines() assert "Qt exceptions in virtual methods:" in lines assert "ValueError: errors were made" in lines assert "RuntimeError: error handling value error" in lines @pytest.mark.parametrize("no_capture_by_marker", [True, False]) def test_no_capture(testdir, no_capture_by_marker): """ Make sure options that disable exception capture are working (either marker or ini configuration value). :type testdir: TmpTestdir """ if no_capture_by_marker: marker_code = "@pytest.mark.qt_no_exception_capture" else: marker_code = "" testdir.makeini( """ [pytest] qt_no_exception_capture = 1 """ ) testdir.makepyfile( """ import pytest import sys from pytestqt.qt_compat import qt_api 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.mark.xfail( condition=sys.version_info[:2] == (3, 4), reason="failing in Python 3.4, which is about to be dropped soon anyway", ) def test_exceptions_dont_leak(testdir): """ Ensure exceptions are cleared when an exception occurs and don't leak (#187). """ testdir.makepyfile( """ from pytestqt.qt_compat import qt_api import gc import weakref class MyWidget(qt_api.QWidget): def event(self, ev): called.append(1) raise RuntimeError('event processed') weak_ref = None called = [] def test_1(qapp): global weak_ref w = MyWidget() weak_ref = weakref.ref(w) qapp.postEvent(w, qt_api.QEvent(qt_api.QEvent.User)) qapp.processEvents() def test_2(qapp): assert called gc.collect() assert weak_ref() is None """ ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 failed, 1 passed*"]) pytest-qt-3.2.2/tests/test_basics.py0000664000372000037200000003275213404516400020357 0ustar travistravis00000000000000import weakref import pytest from pytestqt import qt_compat from pytestqt.qt_compat import qt_api def test_basics(qtbot): """ Basic test that works more like a sanity check to ensure we are setting up a QApplication properly and are able to display a simple event_recorder. """ assert qt_api.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", raising=False) testdir.makeini( """ [pytest] qt_api={option_api} """.format( option_api=option_api ) ) testdir.makepyfile( """ import pytest def test_foo(qtbot): pass """ ) result = testdir.runpytest_subprocess() if qt_api.pytest_qt_api.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 *"]) def test_importerror(monkeypatch): def _fake_import(name, *args): raise ImportError("Failed to import {}".format(name)) monkeypatch.delenv("PYTEST_QT_API", raising=False) monkeypatch.setattr(qt_compat, "_import", _fake_import) expected = ( "pytest-qt requires either PySide, PySide2, PyQt4 or PyQt5 to be installed\n" " PyQt4.QtCore: Failed to import PyQt4.QtCore\n" " PyQt5.QtCore: Failed to import PyQt5.QtCore\n" " PySide.QtCore: Failed to import PySide.QtCore\n" " PySide2.QtCore: Failed to import PySide2.QtCore" ) with pytest.raises(RuntimeError, match=expected): qt_api.set_qt_api(api=None) pytest-qt-3.2.2/tests/conftest.py0000664000372000037200000000463613404516400017701 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-3.2.2/CHANGELOG.rst0000664000372000037200000004646313404516400016365 0ustar travistravis000000000000003.2.2 (2018-12-13) ------------------ - Fix Off-by-one error in ``modeltester`` (`#249`_). Thanks `@ext-jmmugnes`_ for the PR. .. _#249: https://github.com/pytest-dev/pytest-qt/pull/249 3.2.1 (2018-10-01) ------------------ - Fixed compatibility with PyQt5 5.11.3 3.2.0 (2018-09-26) ------------------ - The ``CallbackBlocker`` returned by ``qtbot.waitCallback()`` now has a new ``assert_called_with(...)`` convenience method. 3.1.0 (2018-09-23) ------------------ - If Qt's model tester implemented in C++ is available (PyQt5 5.11 or newer), the ``qtmodeltester`` fixture now uses that instead of the Python implementation. This can be turned off by passing ``force_py=True`` to ``qtmodeltester.check()``. - The Python code used by ``qtmodeltester`` is now based on the latest Qt modeltester. This also means that the ``data_display_may_return_none`` attribute for ``qtmodeltester`` isn't used anymore. - New ``qtbot.waitCallback()`` method that returns a ``CallbackBlocker``, which can be used to wait for a callback to be called. - ``qtbot.assertNotEmitted`` now has a new ``wait`` parameter which can be used to make sure asynchronous signals aren't emitted by waiting after the code in the ``with`` block finished. - The ``qt_wait_signal_raising`` option was renamed to ``qt_default_raising``. The old name continues to work, but is deprecated. - The docs still referred to ``SignalTimeoutError`` in some places, despite it being renamed to ``TimeoutError`` in the 2.1 release. This is now corrected. - Improve debugging output when no Qt wrapper was found. - When no context is available for warnings on Qt 5, no ``None:None:0`` line is shown anymore. - The ``no_qt_log`` marker is now registered with pytest so ``--strict`` can be used. - ``qtbot.waitSignal`` with timeout ``0`` now expects the signal to arrive directly in the code enclosed by it. Thanks `@The-Compiler`_ for the PRs. 3.0.2 (2018-08-31) ------------------ - Another fix related to ``QtInfoMsg`` objects during logging (`#225`_). 3.0.1 (2018-08-30) ------------------ - Fix handling of ``QtInfoMsg`` objects during logging (`#225`_). Thanks `@willsALMANJ`_ for the report. .. _#225: https://github.com/pytest-dev/pytest-qt/issues/225 3.0.0 (2018-07-12) ------------------ - Removed ``qtbot.mouseEvent`` proxy, it was an internal Qt function which has now been removed in PyQt 5.11 (`#219`_). Thanks `@mitya57`_ for the PR. - Fix memory leak when tests raised an exception inside Qt virtual methods (`#187`_). Thanks `@fabioz`_ for the report and PR. .. _#187: https://github.com/pytest-dev/pytest-qt/issues/187 .. _#219: https://github.com/pytest-dev/pytest-qt/pull/219 2.4.1 (2018-06-14) ------------------ - Properly handle chained exceptions when capturing them inside virtual methods (`#215`_). Thanks `@fabioz`_ for the report and sample code with the fix. .. _#215: https://github.com/pytest-dev/pytest-qt/pull/215 2.4.0 ----- - Use new pytest 3.6 marker API when possible (`#212`_). Thanks `@The-Compiler`_ for the PR. .. _#212: https://github.com/pytest-dev/pytest-qt/pull/212 2.3.2 ----- - Fix ``QStringListModel`` import when using ``PySide2`` (`#209`_). Thanks `@rth`_ for the PR. .. _#209: https://github.com/pytest-dev/pytest-qt/pull/209 2.3.1 ----- - ``PYTEST_QT_API`` environment variable correctly wins over ``qt_api`` ini variable if both are set at the same time (`#196`_). Thanks `@mochick`_ for the PR. .. _#196: https://github.com/pytest-dev/pytest-qt/pull/196 2.3.0 ----- - New ``qapp_args`` fixture which can be used to pass custom arguments to ``QApplication``. Thanks `@The-Compiler`_ for the PR. 2.2.1 ----- - ``modeltester`` now accepts ``QBrush`` for ``BackgroundColorRole`` and ``TextColorRole`` (`#189`_). Thanks `@p0las`_ for the PR. .. _#189: https://github.com/pytest-dev/pytest-qt/issues/189 2.2.0 ----- - ``pytest-qt`` now supports `PySide2`_ thanks to `@rth`_! .. _PySide2: https://wiki.qt.io/PySide2 2.1.2 ----- - Fix issue where ``pytestqt`` was hiding the information when there's an exception raised from another exception on Python 3. 2.1.1 ----- - Fixed tests on Python 3.6. 2.1 --- - ``waitSignal`` and ``waitSignals`` now provide much more detailed messages when expected signals are not emitted. Many thanks to `@MShekow`_ for the PR (`#153`_). - ``qtbot`` fixture now can capture Qt virtual method exceptions in a block using ``captureExceptions`` (`#154`_). Thanks to `@fogo`_ for the PR. - New `qtbot.waitActive`_ and `qtbot.waitExposed`_ methods for PyQt5. Thanks `@The-Compiler`_ for the request (`#158`_). - ``SignalTimeoutError`` has been renamed to ``TimeoutError``. ``SignalTimeoutError`` is kept as a backward compatibility alias. .. _qtbot.waitActive: http://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.waitActive .. _qtbot.waitExposed: http://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.waitExposed .. _#153: https://github.com/pytest-dev/pytest-qt/issues/153 .. _#154: https://github.com/pytest-dev/pytest-qt/issues/154 .. _#158: https://github.com/pytest-dev/pytest-qt/issues/158 2.0 --- Breaking Changes ~~~~~~~~~~~~~~~~ With ``pytest-qt`` 2.0, we changed some defaults to values we think are much better, however this required some backwards-incompatible changes: - ``pytest-qt`` now defaults to using ``PyQt5`` if ``PYTEST_QT_API`` is not set. Before, it preferred ``PySide`` which is using the discontinued Qt4. - Python 3 versions prior to 3.4 are no longer supported. - The ``@pytest.mark.qt_log_ignore`` mark now defaults to ``extend=True``, i.e. extends the patterns defined in the config file rather than overriding them. You can pass ``extend=False`` to get the old behaviour of overriding the patterns. - ``qtbot.waitSignal`` now defaults to ``raising=True`` and raises an exception on timeouts. You can set ``qt_wait_signal_raising = false`` in your config to get back the old behaviour. - ``PYTEST_QT_FORCE_PYQT`` environment variable is no longer supported. Set ``PYTEST_QT_API`` to the appropriate value instead or the new ``qt_api`` configuration option in your ``pytest.ini`` file. New Features ~~~~~~~~~~~~ * From this version onward, ``pytest-qt`` is licensed under the MIT license (`#134`_). * New ``qtmodeltester`` fixture to test ``QAbstractItemModel`` subclasses. Thanks `@The-Compiler`_ for the initiative and port of the original C++ code for ModelTester (`#63`_). * New ``qtbot.waitUntil`` method, which continuously calls a callback until a condition is met or a timeout is reached. Useful for testing asynchronous features (like in X window environments for example). * ``waitSignal`` and ``waitSignals`` can receive an optional callback (or list of callbacks) that can evaluate if the arguments of emitted signals should resume execution or not. Additionally ``waitSignals`` has a new ``order`` parameter that allows to expect signals and their arguments in a strict, semi-strict or no specific order. Thanks `@MShekow`_ for the PR (`#141`_). * Now which Qt binding ``pytest-qt`` will use can be configured by the ``qt_api`` config option. Thanks `@The-Compiler`_ for the request (`#129`_). * While ``pytestqt.qt_compat`` is an internal module and shouldn't be imported directly, it is known that some test suites did import it. This module now uses a lazy-load mechanism to load Qt classes and objects, so the old symbols (``QtCore``, ``QApplication``, etc.) are no longer available from it. .. _#134: https://github.com/pytest-dev/pytest-qt/issues/134 .. _#141: https://github.com/pytest-dev/pytest-qt/pull/141 .. _#63: https://github.com/pytest-dev/pytest-qt/pull/63 .. _#129: https://github.com/pytest-dev/pytest-qt/issues/129 Other Changes ~~~~~~~~~~~~~ - Exceptions caught by ``pytest-qt`` in ``sys.excepthook`` are now also printed to ``stderr``, making debugging them easier from within an IDE. Thanks `@fabioz`_ for the PR (`126`_)! .. _126: https://github.com/pytest-dev/pytest-qt/pull/126 1.11.0 ------ .. note:: The default value for ``raising`` is planned to change to ``True`` starting in pytest-qt version ``1.12``. Users wishing to preserve the current behavior (``raising`` is ``False`` by default) should make use of the new ``qt_wait_signal_raising`` ini option below. - New ``qt_wait_signal_raising`` ini option can be used to override the default value of the ``raising`` parameter of the ``qtbot.waitSignal`` and ``qtbot.waitSignals`` functions when omitted: .. code-block:: ini [pytest] qt_wait_signal_raising = true Calls which explicitly pass the ``raising`` parameter are not affected. Thanks `@The-Compiler`_ for idea and initial work on a PR (`120`_). - ``qtbot`` now has a new ``assertNotEmitted`` context manager which can be used to ensure the given signal is not emitted (`92`_). Thanks `@The-Compiler`_ for the PR! .. _92: https://github.com/pytest-dev/pytest-qt/issues/92 .. _120: https://github.com/pytest-dev/pytest-qt/issues/120 1.10.0 ------ - ``SignalBlocker`` now has a ``args`` attribute with the arguments of the signal that triggered it, or ``None`` on a time out (`115`_). Thanks `@billyshambrook`_ for the request and `@The-Compiler`_ for the PR. - ``MultiSignalBlocker`` is now properly disconnects from signals upon exit. .. _115: https://github.com/pytest-dev/pytest-qt/issues/115 1.9.0 ----- - Exception capturing now happens as early/late as possible in order to catch all possible exceptions (including fixtures)(`105`_). Thanks `@The-Compiler`_ for the request. - Widgets registered by ``qtbot.addWidget`` are now closed before all other fixtures are tear down (`106`_). Thanks `@The-Compiler`_ for request. - ``qtbot`` now has a new ``wait`` method which does a blocking wait while the event loop continues to run, similar to ``QTest::qWait``. Thanks `@The-Compiler`_ for the PR (closes `107`_)! - raise ``RuntimeError`` instead of ``ImportError`` when failing to import any Qt binding: raising the latter causes ``pluggy`` in ``pytest-2.8`` to generate a subtle warning instead of a full blown error. Thanks `@Sheeo`_ for bringing this problem to attention (closes `109`_). .. _105: https://github.com/pytest-dev/pytest-qt/issues/105 .. _106: https://github.com/pytest-dev/pytest-qt/issues/106 .. _107: https://github.com/pytest-dev/pytest-qt/issues/107 .. _109: https://github.com/pytest-dev/pytest-qt/issues/109 1.8.0 ----- - ``pytest.mark.qt_log_ignore`` now supports an ``extend`` parameter that will extend the list of regexes used to ignore Qt messages (defaults to False). Thanks `@The-Compiler`_ for the PR (`99`_). - Fixed internal error when interacting with other plugins that raise an error, hiding the original exception (`98`_). Thanks `@The-Compiler`_ for the PR! - Now ``pytest-qt`` is properly tested with PyQt5 on Travis-CI. Many thanks to `@The-Compiler`_ for the PR! .. _99: https://github.com/pytest-dev/pytest-qt/issues/99 .. _98: https://github.com/pytest-dev/pytest-qt/issues/98 1.7.0 ----- - ``PYTEST_QT_API`` can now be set to ``pyqt4v2`` in order to use version 2 of the PyQt4 API. Thanks `@montefra`_ for the PR (`93`_)! .. _93: https://github.com/pytest-dev/pytest-qt/issues/93 1.6.0 ----- - Reduced verbosity when exceptions are captured in virtual methods (`77`_, thanks `@The-Compiler`_). - ``pytestqt.plugin`` has been split in several files (`74`_) and tests have been moved out of the ``pytestqt`` package. This should not affect users, but it is worth mentioning nonetheless. - ``QApplication.processEvents()`` is now called before and after other fixtures and teardown hooks, to better try to avoid non-processed events from leaking from one test to the next. (67_, thanks `@The-Compiler`_). - Show Qt/PyQt/PySide versions in pytest header (68_, thanks `@The-Compiler`_!). - Disconnect SignalBlocker functions after its loop exits to ensure second emissions that call the internal functions on the now-garbage-collected SignalBlocker instance (#69, thanks `@The-Compiler`_ for the PR). .. _77: https://github.com/pytest-dev/pytest-qt/issues/77 .. _74: https://github.com/pytest-dev/pytest-qt/issues/74 .. _67: https://github.com/pytest-dev/pytest-qt/issues/67 .. _68: https://github.com/pytest-dev/pytest-qt/issues/68 1.5.1 ----- - Exceptions are now captured also during test tear down, as delayed events will get processed then and might raise exceptions in virtual methods; this is specially problematic in ``PyQt5.5``, which `changed the behavior `_ to call ``abort`` by default, which will crash the interpreter. (65_, thanks `@The-Compiler`_). .. _65: https://github.com/pytest-dev/pytest-qt/issues/65 1.5.0 ----- - Fixed log line number in messages, and provide better contextual information in Qt5 (55_, thanks `@The-Compiler`_); - Fixed issue where exceptions inside a ``waitSignals`` or ``waitSignal`` with-statement block would be swallowed and a ``SignalTimeoutError`` would be raised instead. (59_, thanks `@The-Compiler`_ for bringing up the issue and providing a test case); - Fixed issue where the first usage of ``qapp`` fixture would return ``None``. Thanks to `@gqmelo`_ for noticing and providing a PR; - New ``qtlog`` now sports a context manager method, ``disabled`` (58_). Thanks `@The-Compiler`_ for the idea and testing; .. _55: https://github.com/pytest-dev/pytest-qt/issues/55 .. _58: https://github.com/pytest-dev/pytest-qt/issues/58 .. _59: https://github.com/pytest-dev/pytest-qt/issues/59 1.4.0 ----- - Messages sent by ``qDebug``, ``qWarning``, ``qCritical`` are captured and displayed when tests fail, similar to `pytest-catchlog`_. Also, tests can be configured to automatically fail if an unexpected message is generated. - New method ``waitSignals``: will block untill **all** signals given are triggered (thanks `@The-Compiler`_ for idea and complete PR). - New parameter ``raising`` to ``waitSignals`` and ``waitSignals``: when ``True`` will raise a ``qtbot.SignalTimeoutError`` exception when timeout is reached (defaults to ``False``). (thanks again to `@The-Compiler`_ for idea and complete PR). - ``pytest-qt`` now requires ``pytest`` version >= 2.7. .. _pytest-catchlog: https://pypi.python.org/pypi/pytest-catchlog Internal changes to improve memory management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``QApplication.exit()`` is no longer called at the end of the test session and the ``QApplication`` instance is not garbage collected anymore; - ``QtBot`` no longer receives a QApplication as a parameter in the constructor, always referencing ``QApplication.instance()`` now; this avoids keeping an extra reference in the ``qtbot`` instances. - ``deleteLater`` is called on widgets added in ``QtBot.addWidget`` at the end of each test; - ``QApplication.processEvents()`` is called at the end of each test to make sure widgets are cleaned up; 1.3.0 ----- - pytest-qt now supports `PyQt5`_! Which Qt api will be used is still detected automatically, but you can choose one using the ``PYTEST_QT_API`` environment variable (the old ``PYTEST_QT_FORCE_PYQT`` is still supported for backward compatibility). Many thanks to `@jdreaver`_ for helping to test this release! .. _PyQt5: http://pyqt.sourceforge.net/Docs/PyQt5/introduction.html 1.2.3 ----- - Now the module ````qt_compat```` no longer sets ``QString`` and ``QVariant`` APIs to ``2`` for PyQt, making it compatible for those still using version ``1`` of the API. 1.2.2 ----- - Now it is possible to disable automatic exception capture by using markers or a ``pytest.ini`` option. Consult the documentation for more information. (`26`_, thanks `@datalyze-solutions`_ for bringing this up). - ``QApplication`` instance is created only if it wasn't created yet (`21`_, thanks `@fabioz`_!) - ``addWidget`` now keeps a weak reference its widgets (`20`_, thanks `@fabioz`_) .. _26: https://github.com/pytest-dev/pytest-qt/issues/26 .. _21: https://github.com/pytest-dev/pytest-qt/issues/21 .. _20: https://github.com/pytest-dev/pytest-qt/issues/20 1.2.1 ----- - Fixed 16_: a signal emitted immediately inside a ``waitSignal`` block now works as expected (thanks `@baudren`_). .. _16: https://github.com/pytest-dev/pytest-qt/issues/16 1.2.0 ----- This version include the new ``waitSignal`` function, which makes it easy to write tests for long running computations that happen in other threads or processes: .. code-block:: python def test_long_computation(qtbot): app = Application() # Watch for the app.worker.finished signal, then start the worker. with qtbot.waitSignal(app.worker.finished, timeout=10000) as blocker: blocker.connect(app.worker.failed) # Can add other signals to blocker app.worker.start() # Test will wait here until either signal is emitted, or 10 seconds has elapsed assert blocker.signal_triggered # Assuming the work took less than 10 seconds assert_application_results(app) Many thanks to `@jdreaver`_ for discussion and complete PR! (`12`_, `13`_) .. _12: https://github.com/pytest-dev/pytest-qt/issues/12 .. _13: https://github.com/pytest-dev/pytest-qt/issues/13 1.1.1 ----- - Added ``stop`` as an alias for ``stopForInteraction`` (`10`_, thanks `@itghisi`_) - Now exceptions raised in virtual methods make tests fail, instead of silently passing (`11`_). If an exception is raised, the test will fail and it exceptions that happened inside virtual calls will be printed as such:: E Failed: Qt exceptions in virtual methods: E ________________________________________________________________________________ E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event E raise ValueError('mistakes were made') E E ValueError: mistakes were made E ________________________________________________________________________________ E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event E raise ValueError('mistakes were made') E E ValueError: mistakes were made E ________________________________________________________________________________ Thanks to `@jdreaver`_ for request and sample code! - Fixed documentation for ``QtBot``: it was not being rendered in the docs due to an import error. .. _10: https://github.com/pytest-dev/pytest-qt/issues/10 .. _11: https://github.com/pytest-dev/pytest-qt/issues/11 1.1.0 ----- Python 3 support. 1.0.2 ----- Minor documentation fixes. 1.0.1 ----- Small bug fix release. 1.0.0 ----- First working version. .. _@baudren: https://github.com/baudren .. _@billyshambrook: https://github.com/billyshambrook .. _@datalyze-solutions: https://github.com/datalyze-solutions .. _@ext-jmmugnes: https://github.com/ext-jmmugnes .. _@fabioz: https://github.com/fabioz .. _@fogo: https://github.com/fogo .. _@gqmelo: https://github.com/gqmelo .. _@itghisi: https://github.com/itghisi .. _@jdreaver: https://github.com/jdreaver .. _@mitya57: https://github.com/mitya57 .. _@mochick: https://github.com/mochick .. _@montefra: https://github.com/montefra .. _@MShekow: https://github.com/MShekow .. _@p0las: https://github.com/p0las .. _@rth: https://github.com/rth .. _@Sheeo: https://github.com/Sheeo .. _@The-Compiler: https://github.com/The-Compiler .. _@willsALMANJ: https://github.com/willsALMANJ pytest-qt-3.2.2/requirements.txt0000664000372000037200000000000713404516400017610 0ustar travistravis00000000000000pytest pytest-qt-3.2.2/PKG-INFO0000664000372000037200000002050413404516624015435 0ustar travistravis00000000000000Metadata-Version: 2.1 Name: pytest-qt Version: 3.2.2 Summary: pytest support for PyQt and PySide applications Home-page: http://github.com/pytest-dev/pytest-qt Author: Bruno Oliveira Author-email: nicoddemus@gmail.com License: MIT 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 .. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pytest-qt.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/nicoddemus/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 .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black |python| |version| |conda-forge| |travis| |appveyor| |coverage| |docs| |black| Features ======== - `qtbot`_ fixture to simulate user interaction with ``Qt`` widgets. - `Automatic capture`_ of ``qDebug``, ``qWarning`` and ``qCritical`` messages; - waitSignal_ and waitSignals_ functions to block test execution until specific signals are emitted. - `Exceptions in virtual methods and slots`_ are automatically captured and fail tests accordingly. .. _qtbot: https://pytest-qt.readthedocs.io/en/latest/reference.html#module-pytestqt.qtbot .. _Automatic capture: https://pytest-qt.readthedocs.io/en/latest/logging.html .. _waitSignal: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _waitSignals: https://pytest-qt.readthedocs.io/en/latest/signals.html .. _Exceptions in virtual methods and slots: https://pytest-qt.readthedocs.io/en/latest/virtual_methods.html Requirements ============ 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`_. It is recommended to develop locally on Python 3 because ``PyQt5`` and ``PySide2`` are easily installable using ``pip``:: $ tox -e py37-pyside2,py37-pyqt5 ``pytest-qt`` is formatted using `black `_ and uses `pre-commit `_ for linting checks before commits. You can install ``pre-commit`` locally with:: $ pip install pre-commit $ pre-commit install 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: Programming Language :: Python :: 3.7 Classifier: Topic :: Desktop Environment :: Window Managers Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: User Interfaces Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Provides-Extra: doc pytest-qt-3.2.2/.pre-commit-config.yaml0000664000372000037200000000177113404516400020616 0ustar travistravis00000000000000repos: - repo: https://github.com/ambv/black rev: 18.6b4 hooks: - id: black args: [--safe, --quiet] language_version: python3 - repo: https://github.com/asottile/blacken-docs rev: v0.2.0 hooks: - id: blacken-docs additional_dependencies: [black==18.6b4] language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements - id: flake8 - repo: https://github.com/asottile/pyupgrade rev: v1.2.0 hooks: - id: pyupgrade - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.0.0 hooks: - id: rst-backticks - repo: local hooks: - id: rst name: rst entry: rst-lint --encoding utf-8 files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst)$ language: python additional_dependencies: [pygments, restructuredtext_lint]