diff -Nru pytest-qt-2.3.1/appveyor.yml pytest-qt-3.2.2/appveyor.yml --- pytest-qt-2.3.1/appveyor.yml 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/appveyor.yml 2018-12-13 17:55:12.000000000 +0000 @@ -20,7 +20,7 @@ - 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.*" @@ -45,6 +45,11 @@ PYTEST_QT_API: "pyside2" CONDA_DEPENDENCIES: "pytest pyside2=2.*" +matrix: + exclude: + # PySide2 crashes: #202 + - PYTEST_QT_API: "pyside2" + platform: -x64 @@ -69,3 +74,5 @@ test_script: - "%CMD_IN_ENV% python -m pytest -v tests/" + +skip_tags: true diff -Nru pytest-qt-2.3.1/CHANGELOG.rst pytest-qt-3.2.2/CHANGELOG.rst --- pytest-qt-2.3.1/CHANGELOG.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/CHANGELOG.rst 2018-12-13 17:55:12.000000000 +0000 @@ -1,11 +1,121 @@ +3.2.2 (2018-12-13) +------------------ + +- Fix Off-by-one error in ``modeltester`` (`#249`_). Thanks `@ext-jmmugnes`_ for the PR. + +.. _#249: https://github.com/pytest-dev/pytest-qt/pull/249 + + +3.2.1 (2018-10-01) +------------------ + +- Fixed compatibility with PyQt5 5.11.3 + +3.2.0 (2018-09-26) +------------------ + +- The ``CallbackBlocker`` returned by ``qtbot.waitCallback()`` now has a new + ``assert_called_with(...)`` convenience method. + +3.1.0 (2018-09-23) +------------------ + +- If Qt's model tester implemented in C++ is available (PyQt5 5.11 or newer), + the ``qtmodeltester`` fixture now uses that instead of the Python + implementation. This can be turned off by passing ``force_py=True`` to + ``qtmodeltester.check()``. + +- The Python code used by ``qtmodeltester`` is now based on the latest Qt + modeltester. This also means that the ``data_display_may_return_none`` + attribute for ``qtmodeltester`` isn't used anymore. + +- New ``qtbot.waitCallback()`` method that returns a ``CallbackBlocker``, which + can be used to wait for a callback to be called. + +- ``qtbot.assertNotEmitted`` now has a new ``wait`` parameter which can be used + to make sure asynchronous signals aren't emitted by waiting after the code in + the ``with`` block finished. + +- The ``qt_wait_signal_raising`` option was renamed to ``qt_default_raising``. + The old name continues to work, but is deprecated. + +- The docs still referred to ``SignalTimeoutError`` in some places, despite it + being renamed to ``TimeoutError`` in the 2.1 release. This is now corrected. + +- Improve debugging output when no Qt wrapper was found. + +- When no context is available for warnings on Qt 5, no ``None:None:0`` line is + shown anymore. + +- The ``no_qt_log`` marker is now registered with pytest so ``--strict`` can be + used. + +- ``qtbot.waitSignal`` with timeout ``0`` now expects the signal to arrive + directly in the code enclosed by it. + +Thanks `@The-Compiler`_ for the PRs. + +3.0.2 (2018-08-31) +------------------ + +- Another fix related to ``QtInfoMsg`` objects during logging (`#225`_). + + +3.0.1 (2018-08-30) +------------------ + +- Fix handling of ``QtInfoMsg`` objects during logging (`#225`_). + Thanks `@willsALMANJ`_ for the report. + +.. _#225: https://github.com/pytest-dev/pytest-qt/issues/225 + + +3.0.0 (2018-07-12) +------------------ + +- Removed ``qtbot.mouseEvent`` proxy, it was an internal Qt function which has + now been removed in PyQt 5.11 (`#219`_). Thanks `@mitya57`_ for the PR. + +- Fix memory leak when tests raised an exception inside Qt virtual methods (`#187`_). + Thanks `@fabioz`_ for the report and PR. + +.. _#187: https://github.com/pytest-dev/pytest-qt/issues/187 +.. _#219: https://github.com/pytest-dev/pytest-qt/pull/219 + + +2.4.1 (2018-06-14) +------------------ + +- Properly handle chained exceptions when capturing them inside + virtual methods (`#215`_). Thanks `@fabioz`_ for the report and sample + code with the fix. + +.. _#215: https://github.com/pytest-dev/pytest-qt/pull/215 + + +2.4.0 +----- + +- Use new pytest 3.6 marker API when possible (`#212`_). Thanks `@The-Compiler`_ for the PR. + +.. _#212: https://github.com/pytest-dev/pytest-qt/pull/212 + +2.3.2 +----- + +- Fix ``QStringListModel`` import when using ``PySide2`` (`#209`_). Thanks `@rth`_ for the PR. + +.. _#209: https://github.com/pytest-dev/pytest-qt/pull/209 + + 2.3.1 ----- - ``PYTEST_QT_API`` environment variable correctly wins over ``qt_api`` - ini variable if both are set at the same time (`#196`_). Thanks `@mochick` for the PR. + ini variable if both are set at the same time (`#196`_). Thanks `@mochick`_ for the PR. .. _#196: https://github.com/pytest-dev/pytest-qt/pull/196 -.. _@mochick: https://github.com/mochick + 2.3.0 ----- @@ -21,7 +131,7 @@ Thanks `@p0las`_ for the PR. .. _#189: https://github.com/pytest-dev/pytest-qt/issues/189 -.. _@p0las: https://github.com/p0las + 2.2.0 @@ -30,7 +140,7 @@ - ``pytest-qt`` now supports `PySide2`_ thanks to `@rth`_! .. _PySide2: https://wiki.qt.io/PySide2 -.. _@rth: https://github.com/rth + 2.1.2 ----- @@ -193,7 +303,7 @@ `@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 + 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`_). @@ -445,12 +555,18 @@ .. _@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 diff -Nru pytest-qt-2.3.1/debian/changelog pytest-qt-3.2.2/debian/changelog --- pytest-qt-2.3.1/debian/changelog 2018-09-27 10:59:50.000000000 +0000 +++ pytest-qt-3.2.2/debian/changelog 2019-01-06 20:12:33.000000000 +0000 @@ -1,3 +1,17 @@ +pytest-qt (3.2.2-1) unstable; urgency=medium + + * Team upload. + * New upstream release (Closes: #917597) + * d/patches/Remove-mouseEvent-proxy-method.patch: Dropped, not needed + anymore + * d/patches/Skip-tests-requiring-a-window-manager.patch: Rebased + * Enable autopkgtest-pkg-python testsuite + * Bump debhelper compat level to 12 + * Bump Standards-Version to 4.3.0 (no changes) + * d/copyright: Update for new upstream release + + -- Ondřej Nový Sun, 06 Jan 2019 21:12:33 +0100 + pytest-qt (2.3.1-2) unstable; urgency=medium * Team upload. diff -Nru pytest-qt-2.3.1/debian/control pytest-qt-3.2.2/debian/control --- pytest-qt-2.3.1/debian/control 2018-09-27 10:59:11.000000000 +0000 +++ pytest-qt-3.2.2/debian/control 2019-01-06 20:02:45.000000000 +0000 @@ -3,7 +3,7 @@ Uploaders: Ghislain Antony Vaillant Section: python Priority: optional -Build-Depends: debhelper-compat (= 11), +Build-Depends: debhelper-compat (= 12), dh-python, python3-all, python3-pyqt5 , @@ -15,11 +15,12 @@ sphinx-common, xauth , xvfb -Standards-Version: 4.2.1 +Standards-Version: 4.3.0 Rules-Requires-Root: no Vcs-Browser: https://salsa.debian.org/python-team/modules/pytest-qt Vcs-Git: https://salsa.debian.org/python-team/modules/pytest-qt.git Homepage: https://github.com/pytest-dev/pytest-qt +Testsuite: autopkgtest-pkg-python Package: python3-pytestqt Architecture: all diff -Nru pytest-qt-2.3.1/debian/copyright pytest-qt-3.2.2/debian/copyright --- pytest-qt-2.3.1/debian/copyright 2018-09-27 10:50:35.000000000 +0000 +++ pytest-qt-3.2.2/debian/copyright 2019-01-06 20:11:14.000000000 +0000 @@ -12,13 +12,11 @@ License: Expat Files: pytestqt/modeltest.py -Copyright: 2015 The Qt Company Ltd. +Copyright: 2016 The Qt Company Ltd. + 2017 Klarälvdalens Datakonsult AB, a KDAB Group company + author Giuseppe D'Angelo License: LGPL-2.1 or LGPL-3 -Files: scripts/link_pyqt.py -Copyright: 2014-2015 Florian Bruhin (The Compiler) -License: LGPL-3+ - License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -Nru pytest-qt-2.3.1/debian/patches/Remove-mouseEvent-proxy-method.patch pytest-qt-3.2.2/debian/patches/Remove-mouseEvent-proxy-method.patch --- pytest-qt-2.3.1/debian/patches/Remove-mouseEvent-proxy-method.patch 2018-09-27 10:57:14.000000000 +0000 +++ pytest-qt-3.2.2/debian/patches/Remove-mouseEvent-proxy-method.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -From: Dmitry Shachnev -Date: Thu, 12 Jul 2018 15:59:53 +0300 -Subject: Remove mouseEvent proxy method - -QTest::mouseEvent is an internal Qt function, and it was removed in -PyQt v5.11 which is causing test failures. - -Forwarded: https://github.com/pytest-dev/pytest-qt/pull/219 ---- - pytestqt/qtbot.py | 2 -- - tests/test_qtest_proxies.py | 1 - - 2 files changed, 3 deletions(-) - -diff --git a/pytestqt/qtbot.py b/pytestqt/qtbot.py -index 7333f93..080dbc6 100644 ---- a/pytestqt/qtbot.py -+++ b/pytestqt/qtbot.py -@@ -96,7 +96,6 @@ class QtBot(object): - - .. staticmethod:: mouseClick (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) - .. staticmethod:: mouseDClick (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) -- .. staticmethod:: mouseEvent (action, widget, button, stateKey, pos[, delay=-1]) - .. staticmethod:: mouseMove (widget[, pos=QPoint()[, delay=-1]]) - .. staticmethod:: mousePress (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) - .. staticmethod:: mouseRelease (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) -@@ -549,7 +548,6 @@ class QtBot(object): - - 'mouseClick', - 'mouseDClick', -- 'mouseEvent', - 'mouseMove', - 'mousePress', - 'mouseRelease', -diff --git a/tests/test_qtest_proxies.py b/tests/test_qtest_proxies.py -index 715f20f..980841f 100644 ---- a/tests/test_qtest_proxies.py -+++ b/tests/test_qtest_proxies.py -@@ -18,7 +18,6 @@ fails_on_pyqt = pytest.mark.xfail('not qt_api.pytest_qt_api.startswith("pyside") - - 'mouseClick', - 'mouseDClick', -- 'mouseEvent', - 'mouseMove', - 'mousePress', - 'mouseRelease', diff -Nru pytest-qt-2.3.1/debian/patches/series pytest-qt-3.2.2/debian/patches/series --- pytest-qt-2.3.1/debian/patches/series 2018-09-27 10:57:14.000000000 +0000 +++ pytest-qt-3.2.2/debian/patches/series 2019-01-06 19:35:34.000000000 +0000 @@ -1,2 +1 @@ Skip-tests-requiring-a-window-manager.patch -Remove-mouseEvent-proxy-method.patch diff -Nru pytest-qt-2.3.1/debian/patches/Skip-tests-requiring-a-window-manager.patch pytest-qt-3.2.2/debian/patches/Skip-tests-requiring-a-window-manager.patch --- pytest-qt-2.3.1/debian/patches/Skip-tests-requiring-a-window-manager.patch 2018-09-27 10:50:35.000000000 +0000 +++ pytest-qt-3.2.2/debian/patches/Skip-tests-requiring-a-window-manager.patch 2019-01-06 19:34:56.000000000 +0000 @@ -6,16 +6,14 @@ tests/test_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) -diff --git a/tests/test_basics.py b/tests/test_basics.py -index 9c2229b..6584bb1 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py -@@ -71,7 +71,7 @@ def test_stop_for_interaction(qtbot, timer): +@@ -82,7 +82,7 @@ qtbot.stopForInteraction() --@pytest.mark.parametrize('show', [True, False]) +-@pytest.mark.parametrize("show", [True, False]) +@pytest.mark.parametrize('show', [False]) - @pytest.mark.parametrize('method_name', ['waitExposed', 'waitActive']) + @pytest.mark.parametrize("method_name", ["waitExposed", "waitActive"]) def test_wait_window(show, method_name, qtbot): """ diff -Nru pytest-qt-2.3.1/docs/app_exit.rst pytest-qt-3.2.2/docs/app_exit.rst --- pytest-qt-2.3.1/docs/app_exit.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/app_exit.rst 2018-12-13 17:55:12.000000000 +0000 @@ -17,7 +17,7 @@ def test_exit_button(qtbot, monkeypatch): exit_calls = [] - monkeypatch.setattr(QApplication, 'exit', lambda: exit_calls.append(1)) + monkeypatch.setattr(QApplication, "exit", lambda: exit_calls.append(1)) button = get_app_exit_button() qtbot.click(button) assert exit_calls == [1] @@ -28,8 +28,7 @@ .. code-block:: python def test_exit_button(qtbot): - with mock.patch.object(QApplication, 'exit'): + with mock.patch.object(QApplication, "exit"): button = get_app_exit_button() qtbot.click(button) assert QApplication.exit.call_count == 1 - diff -Nru pytest-qt-2.3.1/docs/conf.py pytest-qt-3.2.2/docs/conf.py --- pytest-qt-2.3.1/docs/conf.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/conf.py 2018-12-13 17:55:12.000000000 +0000 @@ -11,213 +11,207 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +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('..')) +sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# 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'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'pytest-qt' -copyright = u'2013, Bruno Oliveira' +project = u"pytest-qt" +copyright = u"2013, Bruno Oliveira" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -from pytestqt import version # The short X.Y version. # The full version, including alpha/beta/rc tags. # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# 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'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# 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 +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# 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' +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 = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# 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 +# 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 +# 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'] +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' +# 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 +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = 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 = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'pytest-qtdoc' +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': '', + # 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'), + ("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 +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# 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) -] +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 +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -226,16 +220,22 @@ # (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'), + ( + "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 = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff -Nru pytest-qt-2.3.1/docs/index.rst pytest-qt-3.2.2/docs/index.rst --- pytest-qt-2.3.1/docs/index.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/index.rst 2018-12-13 17:55:12.000000000 +0000 @@ -1,9 +1,3 @@ -.. pytest-qt documentation master file, created by - sphinx-quickstart on Mon Mar 04 22:54:36 2013. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - - ========= pytest-qt ========= @@ -21,12 +15,12 @@ logging signals wait_until + wait_callback virtual_methods modeltester app_exit note_pyqt4v2 + note_dialogs + troubleshooting reference changelog - - - diff -Nru pytest-qt-2.3.1/docs/logging.rst pytest-qt-3.2.2/docs/logging.rst --- pytest-qt-2.3.1/docs/logging.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/logging.rst 2018-12-13 17:55:12.000000000 +0000 @@ -17,8 +17,10 @@ from pytestqt.qt_compat import qWarning + def do_something(): - qWarning('this is a WARNING message') + qWarning("this is a WARNING message") + def test_foo(): do_something() @@ -96,6 +98,7 @@ # logging is disabled within the context manager do_something() + @pytest.mark.no_qt_log def test_bar(): # logging is disabled for the entire test @@ -152,8 +155,10 @@ from pytestqt.qt_compat import qCritical + def do_something(): - qCritical('WM_PAINT failed') + qCritical("WM_PAINT failed") + def test_foo(qtlog): do_something() @@ -201,8 +206,9 @@ .. code-block:: python def do_something(): - qCritical('WM_PAINT not handled') - qCritical('QObject: widget destroyed in another thread') + qCritical("WM_PAINT not handled") + qCritical("QObject: widget destroyed in another thread") + def test_foo(qtlog): do_something() @@ -226,11 +232,12 @@ .. code-block:: python def do_something(): - qCritical('WM_PAINT not handled') - qCritical('QObject: widget destroyed in another thread') + 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') + @pytest.mark.qt_log_level_fail("CRITICAL") + @pytest.mark.qt_log_ignore("WM_DESTROY.*sent", "WM_PAINT failed") def test_foo(qtlog): do_something() @@ -239,6 +246,6 @@ .. code-block:: python - @pytest.mark.qt_log_ignore('WM_DESTROY.*sent', extend=False) + @pytest.mark.qt_log_ignore("WM_DESTROY.*sent", extend=False) def test_foo(qtlog): do_something() diff -Nru pytest-qt-2.3.1/docs/modeltester.rst pytest-qt-3.2.2/docs/modeltester.rst --- pytest-qt-2.3.1/docs/modeltester.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/modeltester.rst 2018-12-13 17:55:12.000000000 +0000 @@ -36,19 +36,24 @@ If the tester finds a problem the test will fail with an assert pinpointing the issue. -The following attribute may influence the outcome of the check depending -on your model implementation: +Qt/Python tester +---------------- -* ``data_display_may_return_none`` (default: ``False``): While you can - technically return ``None`` (or an invalid ``QVariant``) from ``data()`` - for ``QtCore.Qt.DisplayRole``, this usually is a sign of - a bug in your implementation. Set this variable to ``True`` if this really - is OK in your model. +Starting with PyQt5 5.11, Qt's ``QAbstractItemModelTester`` is exposed to Python. -The source code was ported from `modeltest.cpp`_ by `Florian Bruhin`_, many -thanks! +If it's available, by default, ``qtmodeltester.check`` will use the C++ +implementation and fail tests if it emits any warnings. -.. _modeltest.cpp: http://code.qt.io/cgit/qt/qtbase.git/tree/tests/auto/other/modeltest/modeltest.cpp +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 diff -Nru pytest-qt-2.3.1/docs/note_dialogs.rst pytest-qt-3.2.2/docs/note_dialogs.rst --- pytest-qt-2.3.1/docs/note_dialogs.rst 1970-01-01 00:00:00.000000000 +0000 +++ pytest-qt-3.2.2/docs/note_dialogs.rst 2018-12-13 17:55:12.000000000 +0000 @@ -0,0 +1,60 @@ +A 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 diff -Nru pytest-qt-2.3.1/docs/signals.rst pytest-qt-3.2.2/docs/signals.rst --- pytest-qt-2.3.1/docs/signals.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/signals.rst 2018-12-13 17:55:12.000000000 +0000 @@ -8,7 +8,9 @@ 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:: +ensuring the results are correct: + +.. code-block:: python def test_long_computation(qtbot): app = Application() @@ -19,7 +21,7 @@ app.worker.start() # Test will block at this point until either the "finished" or the # "failed" signal is emitted. If 10 seconds passed without a signal, - # SignalTimeoutError will be raised. + # TimeoutError will be raised. assert_application_results(app) @@ -29,10 +31,10 @@ ----------------- .. versionadded:: 1.4 -.. versionchanged:: 2.0 +.. versionchanged:: 2.0 You can pass ``raising=False`` to avoid raising a -:class:`qtbot.SignalTimeoutError ` if the timeout is +:class:`qtbot.TimeoutError ` if the timeout is reached before the signal is triggered: .. code-block:: python @@ -44,36 +46,38 @@ assert_application_results(app) - # qtbot.SignalTimeoutError is not raised, but you can still manually + # qtbot.TimeoutError is not raised, but you can still manually # check whether the signal was triggered: assert blocker.signal_triggered, "process timed-out" -.. _qt_wait_signal_raising: +.. _qt_default_raising: -qt_wait_signal_raising ini option ---------------------------------- +qt_default_raising ini option +----------------------------- .. versionadded:: 1.11 -.. versionchanged:: 2.0 +.. versionchanged:: 2.0 +.. versionchanged:: 3.1 -The ``qt_wait_signal_raising`` ini option can be used to override the default +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_wait_signal_raising = false + 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 +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. @@ -84,15 +88,29 @@ """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 + + # 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: + 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 --------------------------------------- @@ -108,7 +126,7 @@ ... with qtbot.waitSignal(app.got_cmd) as blocker: app.listen() - assert blocker.args == ['test'] + assert blocker.args == ["test"] Signals without arguments will set ``args`` to an empty list. If the time out @@ -142,7 +160,7 @@ w.start() # this will be reached after all workers emit their "finished" - # signal or a qtbot.SignalTimeoutError will be raised + # signal or a qtbot.TimeoutError will be raised assert_application_results(app) check_params_cbs parameter @@ -170,16 +188,20 @@ """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: + with qtbot.waitSignals( + signals, raising=True, check_params_cbs=callbacks + ) as blocker: app.worker.start() @@ -188,10 +210,10 @@ .. versionadded:: 2.0 -By default a test using ``qtbot.waitSignals`` completes successfully if *all* signals in ``signals`` +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 +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:: @@ -201,7 +223,7 @@ 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 +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 @@ -230,3 +252,14 @@ ... 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()") diff -Nru pytest-qt-2.3.1/docs/troubleshooting.rst pytest-qt-3.2.2/docs/troubleshooting.rst --- pytest-qt-2.3.1/docs/troubleshooting.rst 1970-01-01 00:00:00.000000000 +0000 +++ pytest-qt-3.2.2/docs/troubleshooting.rst 2018-12-13 17:55:12.000000000 +0000 @@ -0,0 +1,62 @@ +Troubleshooting +=============== + + +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 diff -Nru pytest-qt-2.3.1/docs/tutorial.rst pytest-qt-3.2.2/docs/tutorial.rst --- pytest-qt-2.3.1/docs/tutorial.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/tutorial.rst 2018-12-13 17:55:12.000000000 +0000 @@ -66,4 +66,3 @@ assert window.filesTable.rowCount() == 2 assert window.filesTable.item(0, 0).text() == 'video1.avi' assert window.filesTable.item(1, 0).text() == 'video2.avi' - diff -Nru pytest-qt-2.3.1/docs/wait_callback.rst pytest-qt-3.2.2/docs/wait_callback.rst --- pytest-qt-2.3.1/docs/wait_callback.rst 1970-01-01 00:00:00.000000000 +0000 +++ pytest-qt-3.2.2/docs/wait_callback.rst 2018-12-13 17:55:12.000000000 +0000 @@ -0,0 +1,57 @@ +waitCallback: 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) diff -Nru pytest-qt-2.3.1/docs/wait_until.rst pytest-qt-3.2.2/docs/wait_until.rst --- pytest-qt-2.3.1/docs/wait_until.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/docs/wait_until.rst 2018-12-13 17:55:12.000000000 +0000 @@ -17,10 +17,10 @@ def test_validate(qtbot): window = MyWindow() - window.edit.setText('not a number') + window.edit.setText("not a number") # after focusing, should update status label window.edit.setFocus() - assert window.status.text() == 'Please input a number' + assert window.status.text() == "Please input a number" The ``window.edit.setFocus()`` may not be processed immediately, only in a future event loop, which @@ -34,11 +34,13 @@ def test_validate(qtbot): window = MyWindow() - window.edit.setText('not a number') + 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' + assert window.status.text() == "Please input a number" + qtbot.waitUntil(check_label) @@ -65,8 +67,8 @@ def test_validate(qtbot): window = MyWindow() - window.edit.setText('not a number') + 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' \ No newline at end of file + assert window.status.text() == "Please input a number" diff -Nru pytest-qt-2.3.1/.gitignore pytest-qt-3.2.2/.gitignore --- pytest-qt-2.3.1/.gitignore 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/.gitignore 2018-12-13 17:55:12.000000000 +0000 @@ -12,7 +12,7 @@ /.cache /.venv /.eggs - +/.pytest_cache # auto-generated by setuptools_scm /pytestqt/_version.py diff -Nru pytest-qt-2.3.1/HOWTORELEASE.rst pytest-qt-3.2.2/HOWTORELEASE.rst --- pytest-qt-2.3.1/HOWTORELEASE.rst 1970-01-01 00:00:00.000000000 +0000 +++ pytest-qt-3.2.2/HOWTORELEASE.rst 2018-12-13 17:55:12.000000000 +0000 @@ -0,0 +1,7 @@ +Here 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. diff -Nru pytest-qt-2.3.1/PKG-INFO pytest-qt-3.2.2/PKG-INFO --- pytest-qt-2.3.1/PKG-INFO 2018-01-05 00:32:46.000000000 +0000 +++ pytest-qt-3.2.2/PKG-INFO 2018-12-13 17:57:40.000000000 +0000 @@ -1,21 +1,20 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: pytest-qt -Version: 2.3.1 +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-Content-Type: UNKNOWN Description: ========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests - for `PySide`_, `PySide2` and `PyQt`_ applications. + 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, + 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: @@ -24,11 +23,11 @@ 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!' + + assert widget.greet_label.text() == "Hello!" .. _PySide: https://pypi.python.org/pypi/PySide @@ -40,10 +39,10 @@ .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt - - .. |anaconda| image:: https://anaconda.org/conda-forge/pytest-qt/badges/version.svg + + .. |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 @@ -53,14 +52,17 @@ .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io - .. |appveyor| image:: https://img.shields.io/appveyor/ci/pytest-dev/pytest-qt/master.svg + .. |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 - |python| |version| |anaconda| |travis| |appveyor| |coverage| |docs| + .. |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 @@ -142,15 +144,18 @@ Running tests ------------- - Tests are run using `tox`_. The simplest way to test is with `PySide`_, as it - is available on pip and can be installed by ``tox`` automatically:: + 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 py34-pyside,py27-pyside,docs + $ 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 - If you want to test against `PyQt`_, install it into your global python - installation and use the ``py27-pyqt4``, ``py34-pyqt4`` or ``py34-pyqt5`` - testing environments, and ``tox`` will copy the appropriate files into - its virtual environments to ensure isolation. Contributors ------------ @@ -172,13 +177,13 @@ .. |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| + |pycharm| + + |pydev| .. _tox: https://tox.readthedocs.io @@ -194,7 +199,10 @@ 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 diff -Nru pytest-qt-2.3.1/.pre-commit-config.yaml pytest-qt-3.2.2/.pre-commit-config.yaml --- pytest-qt-2.3.1/.pre-commit-config.yaml 1970-01-01 00:00:00.000000000 +0000 +++ pytest-qt-3.2.2/.pre-commit-config.yaml 2018-12-13 17:55:12.000000000 +0000 @@ -0,0 +1,37 @@ +repos: +- 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] diff -Nru pytest-qt-2.3.1/pytestqt/exceptions.py pytest-qt-3.2.2/pytestqt/exceptions.py --- pytest-qt-2.3.1/pytestqt/exceptions.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/exceptions.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,7 +1,10 @@ -from contextlib import contextmanager +import functools import sys import traceback +from contextlib import contextmanager + import pytest +from pytestqt.utils import get_marker @contextmanager @@ -19,6 +22,12 @@ 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. @@ -32,12 +41,8 @@ """Start exception capturing by installing a hook into sys.excepthook that records exceptions received into ``self.exceptions``. """ - def hook(type_, value, tback): - self.exceptions.append((type_, value, tback)) - sys.stderr.write(format_captured_exceptions([(type_, value, tback)])) - self.old_hook = sys.excepthook - sys.excepthook = hook + sys.excepthook = functools.partial(_except_hook, exceptions=self.exceptions) def finish(self): """Stop exception capturing, restoring the original hook. @@ -57,9 +62,10 @@ self.finish() exceptions = self.exceptions self.exceptions = [] - prefix = '%s ERROR: ' % when - pytest.fail(prefix + format_captured_exceptions(exceptions), - pytrace=False) + 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): @@ -67,19 +73,27 @@ Formats exceptions given as (type, value, traceback) into a string suitable to display as a test failure. """ - message = 'Qt exceptions in virtual methods:\n' - message += '_' * 80 + '\n' + 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: - message += ''.join(traceback.format_exception(exc_type, value, tback)) + '\n' - message += '_' * 80 + '\n' - return message + 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 = item.get_marker('qt_no_exception_capture') or \ - item.config.getini('qt_no_exception_capture') + disabled = get_marker(item, "qt_no_exception_capture") or item.config.getini( + "qt_no_exception_capture" + ) return not disabled @@ -93,6 +107,7 @@ In versions prior to ``2.1``, this exception was called ``SignalTimeoutError``. An alias is kept for backward compatibility. """ + pass diff -Nru pytest-qt-2.3.1/pytestqt/__init__.py pytest-qt-3.2.2/pytestqt/__init__.py --- pytest-qt-2.3.1/pytestqt/__init__.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/__init__.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,3 +1,4 @@ # _version is automatically generated by setuptools_scm from pytestqt._version import version + __version__ = version diff -Nru pytest-qt-2.3.1/pytestqt/logging.py pytest-qt-3.2.2/pytestqt/logging.py --- pytest-qt-2.3.1/pytestqt/logging.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/logging.py 2018-12-13 17:55:12.000000000 +0000 @@ -5,6 +5,7 @@ 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): @@ -13,26 +14,28 @@ test and augment reporting if the test failed with the messages captured. """ - LOG_FAIL_OPTIONS = ['NO', 'CRITICAL', 'WARNING', 'DEBUG'] + LOG_FAIL_OPTIONS = ["NO", "CRITICAL", "WARNING", "DEBUG", "INFO"] def __init__(self, config): self.config = config def pytest_runtest_setup(self, item): - if item.get_marker('no_qt_log'): + if get_marker(item, "no_qt_log"): return - m = item.get_marker('qt_log_ignore') + m = get_marker(item, "qt_log_ignore") if m: - if not set(m.kwargs).issubset(set(['extend'])): - raise ValueError("Invalid keyword arguments in {0!r} for " - "qt_log_ignore mark.".format(m.kwargs)) - if m.kwargs.get('extend', True): - config_regexes = self.config.getini('qt_log_ignore') + 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') + ignore_regexes = self.config.getini("qt_log_ignore") item.qt_log_capture = _QtMessageCapture(ignore_regexes) item.qt_log_capture._start() @@ -40,50 +43,72 @@ 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'): + if not hasattr(item, "qt_log_capture"): return - if call.when == 'call': + if call.when == "call": report = outcome.get_result() - m = item.get_marker('qt_log_level_fail') + 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') + log_fail_level = self.config.getini("qt_log_level_fail") assert log_fail_level in QtLoggingPlugin.LOG_FAIL_OPTIONS # make test fail if any records were captured which match # log_fail_level - if log_fail_level != 'NO' and report.outcome != 'failed': + if report.outcome != "failed": for rec in item.qt_log_capture.records: - if rec.matches_level(log_fail_level) and not rec.ignored: - report.outcome = 'failed' + 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) + 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') + 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': - log_format = '{rec.context.file}:{rec.context.function}:' \ - '{rec.context.line}:\n {rec.type_name}: {rec.message}' + 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: - log_format = '{rec.type_name}: {rec.message}' + 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 '' + 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)) + long_repr.addsection("Captured Qt messages", "\n".join(lines)) item.qt_log_capture._stop() del item.qt_log_capture @@ -138,7 +163,7 @@ finally: self._start() - _Context = namedtuple('_Context', 'file function line') + _Context = namedtuple("_Context", "file function line category") def _append_new_record(self, msg_type, message, context): """ @@ -148,9 +173,10 @@ :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') + s = s.decode("utf-8", "replace") return s message = to_unicode(message) @@ -166,6 +192,7 @@ to_unicode(context.file), to_unicode(context.function), context.line, + to_unicode(context.category), ) self._records.append(Record(msg_type, message, ignored, context)) @@ -198,10 +225,10 @@ :ivar str message: message contents. :ivar Qt.QtMsgType type: enum that identifies message type - :ivar str type_name: ``type`` as string: ``"QtDebugMsg"``, + :ivar str type_name: ``type`` as string: ``"QtInfoMsg"``, ``"QtDebugMsg"``, ``"QtWarningMsg"`` or ``"QtCriticalMsg"``. :ivar str log_type_name: - type name similar to the logging package: ``DEBUG``, + 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" @@ -233,13 +260,15 @@ Return a string representation of the given QtMsgType enum value. """ - if not getattr(cls, '_type_name_map', None): + 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', + 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 @@ -248,25 +277,31 @@ 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): + 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', + 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 == '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',) + 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: {0}'.format(level)) + raise ValueError("log_fail_level unknown: {}".format(level)) class _QtLogLevelErrorRepr(TerminalRepr): @@ -275,11 +310,15 @@ messages at or above the allowed level. """ - def __init__(self, item, level): - msg = 'Failure: Qt messages with level {0} or above emitted' + 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.fileloc = ReprFileLocation( + path, lineno=line_index + 1, message=msg.format(level.upper()) + ) self.sections = [] def addsection(self, name, content, sep="-"): diff -Nru pytest-qt-2.3.1/pytestqt/modeltest.py pytest-qt-3.2.2/pytestqt/modeltest.py --- pytest-qt-2.3.1/pytestqt/modeltest.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/modeltest.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,52 +1,63 @@ -# This file is based on the original C++ modeltest.cpp from: -# http://code.qt.io/cgit/qt/qtbase.git/tree/tests/auto/other/modeltest/modeltest.cpp +# 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) 2015 The Qt Company Ltd. -# Contact: http://www.qt.io/licensing/ +# 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 test suite of the Qt Toolkit. +# This file is part of the QtTest module of the Qt Toolkit. # -# $QT_BEGIN_LICENSE:LGPL21$ +# $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 http://www.qt.io/terms-conditions. For further -# information use the contact form at http://www.qt.io/contact-us. +# 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 2.1 or version 3 as published by the Free -# Software Foundation and appearing in the file LICENSE.LGPLv21 and -# LICENSE.LGPLv3 included in the packaging of this file. Please review the -# following information to ensure the GNU Lesser General Public License -# requirements will be met: https://www.gnu.org/licenses/lgpl.html and -# http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +# 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. # -# As a special exception, The Qt Company gives you certain additional -# rights. These rights are described in The Qt Company LGPL Exception -# version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +# 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') +_Changing = collections.namedtuple("_Changing", "parent, old_size, last, next") -class ModelTester: +HAS_QT_TESTER = hasattr(qt_api.QtTest, "QAbstractItemModelTester") - """A tester for Qt's QAbstractItemModels. - :ivar bool data_display_may_return_none: if the model implementation is - allowed to return None from data() for DisplayRole. - """ +class ModelTester: + + """A tester for Qt's QAbstractItemModels.""" def __init__(self, config): self._model = None @@ -54,30 +65,52 @@ self._insert = None self._remove = None self._changing = [] - self.data_display_may_return_none = False + self._qt_tester = None def _debug(self, text): - print('modeltest: ' + text) + print("modeltest: " + text) def _modelindex_debug(self, index): """Get a string for debug output for a QModelIndex.""" - if not index.isValid(): - return ' (0x{:x})'.format(id(index)) + 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(), + return "{}/{} {!r} (0x{:x})".format( + index.row(), + index.column(), qt_api.extract_from_variant(data), - id(index)) + id(index), + ) - def check(self, model): + 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 = [] @@ -99,13 +132,10 @@ self._model.rowsRemoved.connect(self._run) # Special checks for changes - self._model.layoutAboutToBeChanged.connect( - self._on_layout_about_to_be_changed) + 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.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) @@ -133,12 +163,11 @@ self._model.rowsRemoved.disconnect(self._run) self._model.layoutAboutToBeChanged.disconnect( - self._on_layout_about_to_be_changed) + 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.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) @@ -152,8 +181,7 @@ if self._fetching_more: return self._test_basic() - self._test_row_count() - self._test_column_count() + self._test_row_count_and_column_count() self._test_has_index() self._test_index() self._test_parent() @@ -165,35 +193,30 @@ Make sure the model doesn't outright segfault, testing the functions which make sense. """ - assert self._model.buddy(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() + 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 - display_data = self._model.data(qt_api.QtCore.QModelIndex(), - qt_api.QtCore.Qt.DisplayRole) - - assert qt_api.extract_from_variant(display_data) is None self._fetch_more(qt_api.QtCore.QModelIndex()) flags = self._model.flags(qt_api.QtCore.QModelIndex()) assert flags == qt_api.QtCore.Qt.ItemIsDropEnabled or not flags self._has_children(qt_api.QtCore.QModelIndex()) - self._model.hasIndex(0, 0) - self._model.headerData(0, qt_api.QtCore.Qt.Horizontal) - self._model.index(0, 0) - self._model.itemData(qt_api.QtCore.QModelIndex()) - cache = None - self._model.match(qt_api.QtCore.QModelIndex(), -1, cache) + + 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 self._parent(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() + assert not self._parent(qt_api.QtCore.QModelIndex()).isValid() assert self._model.rowCount() >= 0 - self._model.setData(qt_api.QtCore.QModelIndex(), None, -1) - self._model.setHeaderData(-1, qt_api.QtCore.Qt.Horizontal, None) - self._model.setHeaderData(999999, qt_api.QtCore.Qt.Horizontal, None) - self._model.sibling(0, 0, qt_api.QtCore.QModelIndex()) self._model.span(qt_api.QtCore.QModelIndex()) + self._model.supportedDropActions() + self._model.roleNames() - def _test_row_count(self): - """Test model's implementation of rowCount() and hasChildren(). + 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. @@ -202,33 +225,31 @@ """ # check top row top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) + rows = self._model.rowCount(top_index) assert rows >= 0 - if rows > 0: - assert self._has_children(top_index) + + 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) - if second_level_index.isValid(): # not the top level - # check a row count where parent is valid - rows = self._model.rowCount(second_level_index) - assert rows >= 0 - if rows > 0: - assert self._has_children(second_level_index) + assert second_level_index.isValid() - def _test_column_count(self): - """Test model's implementation of columnCount() and hasChildren(). + rows = self._model.rowCount(second_level_index) + assert rows >= 0 - columnCount() is tested more extensively in _check_children(), - but this catches the big mistakes. - """ - # check top row - top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) - assert self._column_count(top_index) >= 0 + columns = self._column_count(second_level_index) + assert columns >= 0 - # check a column count where parent is valid - child_index = self._model.index(0, 0, top_index) - if child_index.isValid(): - assert self._column_count(child_index) >= 0 + 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(). @@ -248,7 +269,7 @@ assert not self._model.hasIndex(rows, columns) assert not self._model.hasIndex(rows + 1, columns + 1) - if rows > 0: + if rows > 0 and columns > 0: assert self._model.hasIndex(0, 0) def _test_index(self): @@ -257,33 +278,25 @@ index() is tested more extensively in _check_children(), but this catches the big mistakes. """ - # Make sure that invalid values return an invalid index - assert self._model.index(-2, -2) == qt_api.QtCore.QModelIndex() - assert self._model.index(-2, 0) == qt_api.QtCore.QModelIndex() - assert self._model.index(0, -2) == qt_api.QtCore.QModelIndex() - rows = self._model.rowCount() columns = self._column_count() - if rows == 0: - return - - # Catch off by one errors - assert self._model.index(rows, columns) == qt_api.QtCore.QModelIndex() - assert self._model.index(0, 0).isValid() - - # Make sure that the same index is *always* returned - a = self._model.index(0, 0) - b = self._model.index(0, 0) - assert a == b + 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 self._parent(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() + assert not self._parent(qt_api.QtCore.QModelIndex()).isValid() - if self._model.rowCount() == 0: + if not self._has_children(): return # Column 0 | Column 1 | @@ -294,22 +307,25 @@ # Common error test #1, make sure that a top level index has a parent # that is a invalid QModelIndex. top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) - assert self._parent(top_index) == qt_api.QtCore.QModelIndex() + 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._model.rowCount(top_index) > 0: + 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. - top_index_1 = self._model.index(0, 1, qt_api.QtCore.QModelIndex()) - if self._model.rowCount(top_index_1) > 0: - child_index = self._model.index(0, 0, top_index) - child_index_1 = self._model.index(0, 0, top_index_1) - assert child_index != child_index_1 + 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. @@ -349,24 +365,33 @@ # Some further testing against rows(), columns(), and hasChildren() assert rows >= 0 assert columns >= 0 - if rows > 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)) + 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): - if self._model.canFetchMore(parent): - self._fetch_more(parent) + 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 @@ -374,37 +399,34 @@ modified_index = self._model.index(r, c, parent) assert index == modified_index - # Make sure we get the same index if we request it twice in a - # row - a = self._model.index(r, c, parent) - b = self._model.index(r, c, parent) - assert a == b - sibling = self._model.sibling(r, c, top_left_child) assert index == sibling - sibling = top_left_child.sibling(r, c) - assert index == sibling + 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 - data = self._model.data(index, qt_api.QtCore.Qt.DisplayRole) - if not self.data_display_may_return_none: - assert qt_api.extract_from_variant(data) is not None - # If the next test fails here is some somewhat useful debug you # play with. - if self._parent(index) != parent: self._debug( - "parent-check failed for index {}:\n" - " parent {} != expected {}".format( + "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)), - self._modelindex_debug(parent) + r, + c, + current_depth, + self._model.data(index), + self._model.data(parent), ) ) @@ -413,10 +435,11 @@ # 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._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 @@ -427,54 +450,80 @@ def _test_data(self): """Test model's implementation of data()""" - # Invalid index should return an invalid qvariant - value = self._model.data(qt_api.QtCore.QModelIndex(), qt_api.QtCore.Qt.DisplayRole) - assert qt_api.extract_from_variant(value) is None - - if self._model.rowCount() == 0: + if not self._has_children(): return # A valid index should have a valid QVariant data assert self._model.index(0, 0).isValid() - # shouldn't be able to set data on an invalid index - ok = self._model.setData(qt_api.QtCore.QModelIndex(), "foo", - qt_api.QtCore.Qt.DisplayRole) - assert not ok + 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.ToolTipRole, str), - (qt_api.QtCore.Qt.StatusTipRole, str), - (qt_api.QtCore.Qt.WhatsThisRole, str), + (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.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) - assert data == None or isinstance(data, typ), 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 = 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 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] + 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. @@ -485,51 +534,65 @@ 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), - ) + self._debug( + "rows about to be inserted: start {}, end {}, parent {}, " + "parent row count {}, last item {}, next item {}".format( + start, + end, + self._modelindex_debug(parent), + parent_rowcount, + self._modelindex_debug(last_index), + self._modelindex_debug(next_index), + ) ) - last_data = self._model.data(last_index) - next_data = self._model.data(next_index) - c = _Changing(parent=parent, old_size=parent_rowcount, - last=last_data, next=next_data) + 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)) - next_data = self._model.data(self._model.index(end + 1, 0, c.parent)) + 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( + " 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) - ) + 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.'): + 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), [...] @@ -542,13 +605,14 @@ 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('') + self._debug(" item {} inserted: {}".format(ii, self._modelindex_debug(idx))) + self._debug("") assert current_size == expected_size - assert c.last == last_data - assert c.next == next_data + 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)): @@ -565,62 +629,85 @@ This gets stored to make sure it actually happens in rowsRemoved. """ - last_index = self._model.index(start - 1, 0, parent) - next_index = self._model.index(end + 1, 0, parent) parent_rowcount = self._model.rowCount(parent) + 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), - ) + self._debug( + "rows about to be removed: start {}, end {}, parent {}, " + "parent row count {}, last item {}, next item {}".format( + start, + end, + self._modelindex_debug(parent), + parent_rowcount, + self._modelindex_debug(last_index), + self._modelindex_debug(next_index), + ) ) - last_data = self._model.data(last_index) - next_data = self._model.data(next_index) - c = _Changing(parent=parent, old_size=parent_rowcount, - last=last_data, next=next_data) + 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)) - next_data = self._model.data(self._model.index(start, 0, c.parent)) + 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( + " 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) - ) + 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.'): + if not qt_api.QtCore.qVersion().startswith("4."): # Skipping this on Qt4 as the parent changes for some reason # see _on_rows_inserted for details assert c.parent == parent assert current_size == expected_size - assert c.last == last_data - assert c.next == next_data + 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() diff -Nru pytest-qt-2.3.1/pytestqt/plugin.py pytest-qt-3.2.2/pytestqt/plugin.py --- pytest-qt-2.3.1/pytestqt/plugin.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/plugin.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,7 +1,14 @@ +import warnings + import pytest -from pytestqt.exceptions import capture_exceptions, format_captured_exceptions, \ - _is_exception_capture_enabled, _QtExceptionCaptureManager, SignalTimeoutError +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 @@ -18,7 +25,7 @@ assert format_captured_exceptions -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def qapp_args(): """ Fixture that provides QApplication arguments to use. @@ -35,7 +42,7 @@ return [] -@pytest.yield_fixture(scope='session') +@pytest.yield_fixture(scope="session") def qapp(qapp_args): """ Fixture that instantiates the QApplication instance that will be used by @@ -52,6 +59,7 @@ 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 @@ -72,7 +80,7 @@ @pytest.fixture def qtlog(request): """Fixture that can access messages captured during testing""" - if hasattr(request._pyfuncitem, 'qt_log_capture'): + if hasattr(request._pyfuncitem, "qt_log_capture"): return request._pyfuncitem.qt_log_capture else: return _QtMessageCapture([]) # pragma: no cover @@ -84,33 +92,54 @@ Fixture used to create a ModelTester instance to test models. """ from pytestqt.modeltest import ModelTester + tester = ModelTester(request.config) yield tester tester._cleanup() def pytest_addoption(parser): - parser.addini('qt_api', - 'Qt api version to use: "pyside", "pyqt4", "pyqt4v2", "pyqt5"') - parser.addini('qt_no_exception_capture', - 'disable automatic exception capture') - parser.addini('qt_wait_signal_raising', - 'Default value for the raising parameter of qtbot.waitSignal') + 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: {0} (default: "{1}")' - .format(QtLoggingPlugin.LOG_FAIL_OPTIONS, default_log_fail), - default=default_log_fail) - parser.addini('qt_log_ignore', - 'list of regexes for messages that should not cause a tests ' - 'to fails', type='linelist') - - group = parser.getgroup('qt', 'qt testing') - group.addoption('--no-qt-log', dest='qt_log', action='store_false', - default=True, help='disable pytest-qt logging capture') - group.addoption('--qt-log-format', dest='qt_log_format', default=None, - help='defines how qt log messages are displayed.') + 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 @@ -127,7 +156,7 @@ yield _process_events() if capture_enabled: - item.qt_exception_capture_manager.fail_if_exceptions_occurred('SETUP') + item.qt_exception_capture_manager.fail_if_exceptions_occurred("SETUP") @pytest.mark.hookwrapper @@ -137,7 +166,7 @@ _process_events() capture_enabled = _is_exception_capture_enabled(item) if capture_enabled: - item.qt_exception_capture_manager.fail_if_exceptions_occurred('CALL') + item.qt_exception_capture_manager.fail_if_exceptions_occurred("CALL") @pytest.mark.hookwrapper @@ -155,7 +184,7 @@ _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.fail_if_exceptions_occurred("TEARDOWN") item.qt_exception_capture_manager.finish() @@ -170,37 +199,43 @@ def pytest_configure(config): config.addinivalue_line( - 'markers', + "markers", "qt_no_exception_capture: Disables pytest-qt's automatic exception " - 'capture for just one test item.') + "capture for just one test item.", + ) config.addinivalue_line( - 'markers', - 'qt_log_level_fail: overrides qt_log_level_fail ini option.') + "markers", "qt_log_level_fail: overrides qt_log_level_fail ini option." + ) config.addinivalue_line( - 'markers', - 'qt_log_ignore: overrides qt_log_ignore ini option.') - - if config.getoption('qt_log'): - config.pluginmanager.register(QtLoggingPlugin(config), '_qt_logging') - - qt_api.set_qt_api(config.getini('qt_api')) + "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, + "%s %s" % (v.qt_api, v.qt_api_version), + "Qt runtime %s" % v.runtime, + "Qt compiled %s" % v.compiled, ] - version_line = ' -- '.join(fields) + version_line = " -- ".join(fields) return [version_line] - - - - diff -Nru pytest-qt-2.3.1/pytestqt/qtbot.py pytest-qt-3.2.2/pytestqt/qtbot.py --- pytest-qt-2.3.1/pytestqt/qtbot.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/qtbot.py 2018-12-13 17:55:12.000000000 +0000 @@ -4,16 +4,23 @@ from pytestqt.exceptions import SignalTimeoutError, TimeoutError from pytestqt.qt_compat import qt_api -from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalEmittedSpy, SignalEmittedError +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()] + return {"true": True, "false": False}[value.lower()] except KeyError: - raise ValueError('unknown string for bool: %r' % value) + raise ValueError("unknown string for bool: %r" % value) class QtBot(object): @@ -96,7 +103,6 @@ .. staticmethod:: mouseClick (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) .. staticmethod:: mouseDClick (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) - .. staticmethod:: mouseEvent (action, widget, button, stateKey, pos[, delay=-1]) .. staticmethod:: mouseMove (widget[, pos=QPoint()[, delay=-1]]) .. staticmethod:: mousePress (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) .. staticmethod:: mouseRelease (widget, button[, stateKey=0[, pos=QPoint()[, delay=-1]]]) @@ -108,8 +114,10 @@ :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.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. @@ -131,6 +139,19 @@ 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 @@ -171,7 +192,9 @@ .. note:: This method is also available as ``wait_active`` (pep-8 alias) """ __tracebackhide__ = True - return _WaitWidgetContextManager('qWaitForWindowActive', 'activated', widget, timeout) + return _WaitWidgetContextManager( + "qWaitForWindowActive", "activated", widget, timeout + ) wait_active = waitActive # pep-8 alias @@ -201,7 +224,9 @@ .. note:: This method is also available as ``wait_exposed`` (pep-8 alias) """ __tracebackhide__ = True - return _WaitWidgetContextManager('qWaitForWindowExposed', 'exposed', widget, timeout) + return _WaitWidgetContextManager( + "qWaitForWindowExposed", "exposed", widget, timeout + ) wait_exposed = waitExposed # pep-8 alias @@ -218,7 +243,7 @@ .. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias) """ - if hasattr(qt_api.QtTest.QTest, 'qWaitForWindowExposed'): + if hasattr(qt_api.QtTest.QTest, "qWaitForWindowExposed"): return qt_api.QtTest.QTest.qWaitForWindowExposed(widget) else: return qt_api.QtTest.QTest.qWaitForWindowShown(widget) @@ -288,7 +313,7 @@ :param bool raising: If :class:`QtBot.TimeoutError ` should be raised if a timeout occurred. - This defaults to ``True`` unless ``qt_wait_signal_raising = false`` + 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. @@ -304,20 +329,24 @@ .. note:: This method is also available as ``wait_signal`` (pep-8 alias) """ - if raising is None: - raising_val = self._request.config.getini('qt_wait_signal_raising') - if not raising_val: - raising = True - else: - raising = _parse_ini_boolean(raising_val) - blocker = SignalBlocker(timeout=timeout, raising=raising, check_params_cb=check_params_cb) + 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"): + def waitSignals( + self, + signals=None, + timeout=1000, + raising=None, + check_params_cbs=None, + order="none", + ): """ .. versionadded:: 1.4 @@ -348,7 +377,7 @@ :param bool raising: If :class:`QtBot.TimeoutError ` should be raised if a timeout occurred. - This defaults to ``True`` unless ``qt_wait_signal_raising = false`` + 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. @@ -380,14 +409,22 @@ if order not in ["none", "simple", "strict"]: raise ValueError("order has to be set to 'none', 'simple' or 'strict'") - if raising is None: - raising = self._request.config.getini('qt_wait_signal_raising') + 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) + 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 @@ -407,19 +444,24 @@ blocker.wait() @contextlib.contextmanager - def assertNotEmitted(self, signal): + 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: + with spy, self.waitSignal(signal, timeout=wait, raising=False): yield spy.assert_not_emitted() @@ -462,6 +504,7 @@ """ __tracebackhide__ = True import time + start = time.time() def timed_out(): @@ -477,7 +520,7 @@ raise else: if result not in (None, True, False): - msg = 'waitUntil() callback must return None, True or False, returned %r' + msg = "waitUntil() callback must return None, True or False, returned %r" raise ValueError(msg % result) # 'assert' form @@ -488,11 +531,56 @@ if result: return else: - assert not timed_out(), 'waitUntil timed out in %s miliseconds' % timeout + 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): """ @@ -512,6 +600,7 @@ .. 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 @@ -539,20 +628,18 @@ # inject methods from QTest into QtBot method_names = [ - 'keyPress', - 'keyClick', - 'keyClicks', - 'keyEvent', - 'keyPress', - 'keyRelease', - 'keyToAscii', - - 'mouseClick', - 'mouseDClick', - 'mouseEvent', - 'mouseMove', - 'mousePress', - 'mouseRelease', + "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) @@ -564,13 +651,14 @@ 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 = getattr(item, "qt_widgets", []) qt_widgets.append(weakref.ref(widget)) item.qt_widgets = qt_widgets @@ -579,7 +667,7 @@ """ Close all widgets registered in the pytest item. """ - widgets = getattr(item, 'qt_widgets', None) + widgets = getattr(item, "qt_widgets", None) if widgets: for w in item.qt_widgets: w = w() @@ -593,7 +681,7 @@ """ Iterates over widgets registered in the given pytest item. """ - return iter(getattr(item, 'qt_widgets', [])) + return iter(getattr(item, "qt_widgets", [])) class _WaitWidgetContextManager(object): @@ -615,8 +703,8 @@ def __enter__(self): __tracebackhide__ = True - if qt_api.pytest_qt_api != 'pyqt5': - raise RuntimeError('Available in PyQt5 only') + if qt_api.pytest_qt_api != "pyqt5": + raise RuntimeError("Available in PyQt5 only") return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -626,8 +714,9 @@ 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) + msg = "widget {} not {} in {} ms.".format( + self._widget, self._adjective_name, self._timeout + ) raise TimeoutError(msg) finally: self._widget = None - diff -Nru pytest-qt-2.3.1/pytestqt/qt_compat.py pytest-qt-3.2.2/pytestqt/qt_compat.py --- pytest-qt-2.3.1/pytestqt/qt_compat.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/qt_compat.py 2018-12-13 17:55:12.000000000 +0000 @@ -9,11 +9,17 @@ """ from __future__ import with_statement, division + +import sys from collections import namedtuple import os -VersionTuple = namedtuple('VersionTuple', 'qt_api, qt_api_version, runtime, compiled') +VersionTuple = namedtuple("VersionTuple", "qt_api, qt_api_version, runtime, compiled") + + +def _import(name): + __import__(name) class _QtApi: @@ -24,47 +30,64 @@ 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') + 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' + 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) + _import(name) return True - except ImportError: + 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' + if _can_import("PySide2.QtCore"): + return "pyside2" + elif _can_import("PyQt5.QtCore"): + return "pyqt5" + elif _can_import("PySide.QtCore"): + return "pyside" + elif _can_import("PyQt4.QtCore"): + return "pyqt4" return None def set_qt_api(self, api): self.pytest_qt_api = self._get_qt_api_from_env() or api or self._guess_qt_api() if not self.pytest_qt_api: # pragma: no cover - msg = 'pytest-qt requires either PySide, PySide2, PyQt4 or PyQt5 to be installed' + 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', + "pyside": "PySide", + "pyside2": "PySide2", + "pyqt4": "PyQt4", + "pyqt4v2": "PyQt4", + "pyqt5": "PyQt5", } _root_module = _root_modules[self.pytest_qt_api] @@ -72,10 +95,11 @@ m = __import__(_root_module, globals(), locals(), [module_name], 0) return getattr(m, module_name) - if self.pytest_qt_api == 'pyqt4v2': # pragma: no cover + 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) @@ -84,16 +108,24 @@ 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.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 @@ -104,20 +136,22 @@ self.qInstallMsgHandler = None self.qInstallMessageHandler = None - if self.pytest_qt_api.startswith('pyside'): + if self.pytest_qt_api.startswith("pyside"): self.Signal = QtCore.Signal self.Slot = QtCore.Slot self.Property = QtCore.Property - self.QStringListModel = QtGui.QStringListModel + 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 - self.QStringListModel = QtGui.QStringListModel - if self.pytest_qt_api == 'pyside2': - _QtWidgets = _import_module('QtWidgets') + if self.pytest_qt_api == "pyside2": + _QtWidgets = _import_module("QtWidgets") self.QApplication = _QtWidgets.QApplication self.QWidget = _QtWidgets.QWidget self.QLineEdit = _QtWidgets.QLineEdit @@ -143,13 +177,16 @@ self.extract_from_variant = extract_from_variant self.make_variant = make_variant - elif self.pytest_qt_api in ('pyqt4', 'pyqt4v2', 'pyqt5'): + # 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') + if self.pytest_qt_api == "pyqt5": + _QtWidgets = _import_module("QtWidgets") self.QApplication = _QtWidgets.QApplication self.QWidget = _QtWidgets.QWidget self.qInstallMessageHandler = QtCore.qInstallMessageHandler @@ -195,21 +232,36 @@ 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': + 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__) + 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_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() diff -Nru pytest-qt-2.3.1/pytestqt/utils.py pytest-qt-3.2.2/pytestqt/utils.py --- pytest-qt-2.3.1/pytestqt/utils.py 1970-01-01 00:00:00.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/utils.py 2018-12-13 17:55:12.000000000 +0000 @@ -0,0 +1,11 @@ +def 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) diff -Nru pytest-qt-2.3.1/pytestqt/_version.py pytest-qt-3.2.2/pytestqt/_version.py --- pytest-qt-2.3.1/pytestqt/_version.py 2018-01-05 00:32:46.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/_version.py 2018-12-13 17:57:40.000000000 +0000 @@ -1,4 +1,4 @@ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -version = '2.3.1' +version = '3.2.2' diff -Nru pytest-qt-2.3.1/pytestqt/wait_signal.py pytest-qt-3.2.2/pytestqt/wait_signal.py --- pytest-qt-2.3.1/pytestqt/wait_signal.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/pytestqt/wait_signal.py 2018-12-13 17:55:12.000000000 +0000 @@ -24,7 +24,7 @@ self.raising = raising self._signals = None # will be initialized by inheriting implementations self._timeout_message = "" - if timeout is None: + if timeout is None or timeout == 0: self._timer = None else: self._timer = qt_api.QtCore.QTimer(self._loop) @@ -46,7 +46,10 @@ if self._timer is not None: self._timer.timeout.connect(self._quit_loop_by_timeout) self._timer.start() - self._loop.exec_() + + if self.timeout != 0: + self._loop.exec_() + if not self.signal_triggered and self.raising: raise TimeoutError(self._timeout_message) @@ -71,22 +74,28 @@ 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))) + 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') + 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).") + 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))) + 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 @@ -119,7 +128,9 @@ name = callback.__name__ except AttributeError: try: - name = callback.func.__name__ # e.g. for callbacks wrapped with functools.partial() + name = ( + callback.func.__name__ + ) # e.g. for callbacks wrapped with functools.partial() except AttributeError: name = "" return name @@ -157,7 +168,7 @@ .. note:: contrary to the parameter of same name in :meth:`pytestqt.qtbot.QtBot.waitSignal`, this parameter does not - consider the :ref:`qt_wait_signal_raising`. + consider the :ref:`qt_default_raising` option. :ivar list args: The arguments which were emitted by the signal, or None if the signal @@ -230,14 +241,20 @@ 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)) + 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) + return "Signal {signal_name} not emitted after {timeout} ms".format( + signal_name=self.signal_name, timeout=self.timeout + ) class SignalAndArgs: @@ -251,7 +268,7 @@ # 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] + signal_name = signal_name.partition("(")[0] return signal_name + args @@ -266,7 +283,8 @@ # Returns e.g. "3rd" for 3, or "21st" for 21 -get_ordinal_str = lambda n: "%d%s" % (n, {1: "st", 2: "nd", 3: "rd"}.get(n if n < 20 else n % 10, "th")) +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): @@ -324,8 +342,12 @@ def _determine_unique_signals(self, signals): # create a map that maps from a unique signal to a list of indices # (positions) where this signal is expected (in case order matters) - signals_as_str = [str(self.get_signal_from_potential_signal_tuple(signal)) for signal in signals] - signal_str_to_unique_signal = {} # maps from a signal-string to one of the signal instances (the first one found) + 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] @@ -344,7 +366,9 @@ 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) + 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): @@ -379,7 +403,8 @@ 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)) + SignalAndArgs(signal_name=self._signal_names[unique_signal], args=args) + ) def _check_signal_match(self, unique_signal, *args): if self._order == "none": @@ -396,15 +421,20 @@ 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 + 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! + 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) + 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) @@ -427,7 +457,9 @@ 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): + if not self._violates_callback_at_index( + self._signal_expected_index, *args + ): return True return False @@ -446,7 +478,11 @@ def _get_unemitted_signal_indices(self, signal): """Returns the indices for the provided signal for which NO signal instance has been emitted yet.""" - return [index for index in self._signals_map[signal] if self._signals_emitted[index] == False] + 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: @@ -456,9 +492,11 @@ 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) + 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: @@ -469,25 +507,39 @@ 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_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) + return "Emitted signals: {}. Missing: {}".format( + emitted_signals, missing_signals + ) @staticmethod def _format_as_array(list_of_strings): - return "[{}]".format(', '.join(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) + 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) + 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]] + 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: )""" @@ -546,12 +598,114 @@ 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))) + raise SignalEmittedError( + "Signal %r unexpectedly emitted with " + "arguments %r" % (self.signal, list(self.args)) + ) else: - raise SignalEmittedError("Signal %r unexpectedly emitted" % - (self.signal,)) + 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): @@ -561,6 +715,18 @@ 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 diff -Nru pytest-qt-2.3.1/pytest_qt.egg-info/PKG-INFO pytest-qt-3.2.2/pytest_qt.egg-info/PKG-INFO --- pytest-qt-2.3.1/pytest_qt.egg-info/PKG-INFO 2018-01-05 00:32:46.000000000 +0000 +++ pytest-qt-3.2.2/pytest_qt.egg-info/PKG-INFO 2018-12-13 17:57:40.000000000 +0000 @@ -1,21 +1,20 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: pytest-qt -Version: 2.3.1 +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-Content-Type: UNKNOWN Description: ========= pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests - for `PySide`_, `PySide2` and `PyQt`_ applications. + 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, + 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: @@ -24,11 +23,11 @@ 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!' + + assert widget.greet_label.text() == "Hello!" .. _PySide: https://pypi.python.org/pypi/PySide @@ -40,10 +39,10 @@ .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt - - .. |anaconda| image:: https://anaconda.org/conda-forge/pytest-qt/badges/version.svg + + .. |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 @@ -53,14 +52,17 @@ .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io - .. |appveyor| image:: https://img.shields.io/appveyor/ci/pytest-dev/pytest-qt/master.svg + .. |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 - |python| |version| |anaconda| |travis| |appveyor| |coverage| |docs| + .. |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 @@ -142,15 +144,18 @@ Running tests ------------- - Tests are run using `tox`_. The simplest way to test is with `PySide`_, as it - is available on pip and can be installed by ``tox`` automatically:: + 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 py34-pyside,py27-pyside,docs + $ 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 - If you want to test against `PyQt`_, install it into your global python - installation and use the ``py27-pyqt4``, ``py34-pyqt4`` or ``py34-pyqt5`` - testing environments, and ``tox`` will copy the appropriate files into - its virtual environments to ensure isolation. Contributors ------------ @@ -172,13 +177,13 @@ .. |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| + |pycharm| + + |pydev| .. _tox: https://tox.readthedocs.io @@ -194,7 +199,10 @@ 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 diff -Nru pytest-qt-2.3.1/pytest_qt.egg-info/SOURCES.txt pytest-qt-3.2.2/pytest_qt.egg-info/SOURCES.txt --- pytest-qt-2.3.1/pytest_qt.egg-info/SOURCES.txt 2018-01-05 00:32:46.000000000 +0000 +++ pytest-qt-3.2.2/pytest_qt.egg-info/SOURCES.txt 2018-12-13 17:57:40.000000000 +0000 @@ -1,9 +1,11 @@ .gitattributes .gitignore +.pre-commit-config.yaml .project .pydevproject .travis.yml CHANGELOG.rst +HOWTORELEASE.rst LICENSE README.rst appveyor.yml @@ -21,11 +23,14 @@ 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 @@ -42,8 +47,8 @@ pytestqt/plugin.py pytestqt/qt_compat.py pytestqt/qtbot.py +pytestqt/utils.py pytestqt/wait_signal.py -scripts/link_pyqt.py tests/conftest.py tests/test_basics.py tests/test_exceptions.py diff -Nru pytest-qt-2.3.1/README.rst pytest-qt-3.2.2/README.rst --- pytest-qt-2.3.1/README.rst 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/README.rst 2018-12-13 17:55:12.000000000 +0000 @@ -3,10 +3,10 @@ ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests -for `PySide`_, `PySide2` and `PyQt`_ applications. +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, +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: @@ -15,11 +15,11 @@ 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!' + + assert widget.greet_label.text() == "Hello!" .. _PySide: https://pypi.python.org/pypi/PySide @@ -31,10 +31,10 @@ .. |version| image:: http://img.shields.io/pypi/v/pytest-qt.svg :target: https://pypi.python.org/pypi/pytest-qt - -.. |anaconda| image:: https://anaconda.org/conda-forge/pytest-qt/badges/version.svg + +.. |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 @@ -44,14 +44,17 @@ .. |docs| image:: https://readthedocs.org/projects/pytest-qt/badge/?version=latest :target: https://pytest-qt.readthedocs.io -.. |appveyor| image:: https://img.shields.io/appveyor/ci/pytest-dev/pytest-qt/master.svg +.. |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 -|python| |version| |anaconda| |travis| |appveyor| |coverage| |docs| +.. |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 @@ -133,15 +136,18 @@ Running tests ------------- -Tests are run using `tox`_. The simplest way to test is with `PySide`_, as it -is available on pip and can be installed by ``tox`` automatically:: +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 py34-pyside,py27-pyside,docs + $ 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 -If you want to test against `PyQt`_, install it into your global python -installation and use the ``py27-pyqt4``, ``py34-pyqt4`` or ``py34-pyqt5`` -testing environments, and ``tox`` will copy the appropriate files into -its virtual environments to ensure isolation. Contributors ------------ @@ -163,12 +169,12 @@ .. |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| +|pycharm| + +|pydev| .. _tox: https://tox.readthedocs.io diff -Nru pytest-qt-2.3.1/requirements.txt pytest-qt-3.2.2/requirements.txt --- pytest-qt-2.3.1/requirements.txt 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/requirements.txt 2018-12-13 17:55:12.000000000 +0000 @@ -1 +1 @@ -pytest \ No newline at end of file +pytest diff -Nru pytest-qt-2.3.1/scripts/link_pyqt.py pytest-qt-3.2.2/scripts/link_pyqt.py --- pytest-qt-2.3.1/scripts/link_pyqt.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/scripts/link_pyqt.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2015 Florian Bruhin (The Compiler) - -# This file was originally part of qutebrowser, copied to pytest-qt with -# permission from the author, by Bruno Oliveira (nicoddemus@gmail.com). The -# file also went through some small modifications so it could run in Python 2.7 -# and link PyQt4 files. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# pytest-qt is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with pytest-qt. If not, see . - -"""Symlink PyQt into a given virtualenv.""" -from __future__ import print_function -import os -import argparse -import shutil -import os.path -import sys -import subprocess -import tempfile -import filecmp -import io - - -class Error(Exception): - - """Exception raised when linking fails.""" - - pass - - -def run_py(executable, *code): - """Run the given python code with the given executable.""" - if os.name == 'nt' and len(code) > 1: - # Windows can't do newlines in arguments... - oshandle, filename = tempfile.mkstemp() - with os.fdopen(oshandle, 'w') as f: - f.write('\n'.join(code)) - cmd = [executable, filename] - try: - ret = subprocess.check_output(cmd, universal_newlines=True, - stderr=None).rstrip() - finally: - os.remove(filename) - else: - cmd = [executable, '-c', '\n'.join(code)] - ret = subprocess.check_output(cmd, universal_newlines=True, - stderr=None).rstrip() - return ret - - -def get_ignored_files(directory, files): - """Get the files which should be ignored for link_pyqt() on Windows.""" - needed_exts = ('.py', '.dll', '.pyd', '.so') - ignored_dirs = ('examples', 'doc') - filtered = [] - for f in files: - ext = os.path.splitext(f)[1] - full_path = os.path.join(directory, f) - if os.path.isdir(full_path) and f in ignored_dirs: - filtered.append(f) - elif (ext not in needed_exts) and os.path.isfile(full_path): - filtered.append(f) - return filtered - - -def needs_update(source, dest): - """Check if a file to be linked/copied needs to be updated.""" - if os.path.islink(dest): - # No need to delete a link and relink -> skip this - return False - elif os.path.isdir(dest): - diffs = filecmp.dircmp(source, dest) - ignored = get_ignored_files(source, diffs.left_only) - has_new_files = set(ignored) != set(diffs.left_only) - return (has_new_files or diffs.right_only or - diffs.common_funny or diffs.diff_files or - diffs.funny_files) - else: - return not filecmp.cmp(source, dest) - - -def get_lib_path(executable, name, required=True): - """Get the path of a python library. - - Args: - executable: The Python executable to use. - name: The name of the library to get the path for. - required: Whether Error should be raised if the lib was not found. - """ - code = [ - 'try:', - ' import {}'.format(name), - 'except ImportError as e:', - ' print("ImportError: " + str(e))', - 'else:', - ' print("path: " + {}.__file__)'.format(name) - ] - output = run_py(executable, *code) - - try: - prefix, data = output.split(': ') - except ValueError: - raise ValueError("Unexpected output: {!r}".format(output)) - - if prefix == 'path': - return data - elif prefix == 'ImportError': - if required: - raise Error("Could not import {} with {}: {}!".format( - name, executable, data)) - else: - return None - else: - raise ValueError("Unexpected output: {!r}".format(output)) - - -def link_pyqt(executable, venv_path, qt_version): - """Symlink the systemwide PyQt/sip into the venv. - - Args: - executable: The python executable where the source files are present. - venv_path: The path to the virtualenv site-packages. - """ - sip_file = get_lib_path(executable, 'sip') - sipconfig_file = get_lib_path(executable, 'sipconfig', required=False) - pyqt_dir = os.path.dirname(get_lib_path(executable, 'PyQt%d' % qt_version)) - - for path in [sip_file, sipconfig_file, pyqt_dir]: - if path is None: - continue - - fn = os.path.basename(path) - dest = os.path.join(venv_path, fn) - - if os.path.exists(dest): - if needs_update(path, dest): - remove(dest) - else: - continue - - copy_or_link(path, dest) - - -def copy_or_link(source, dest): - """Copy or symlink source to dest.""" - if os.name == 'nt': - if os.path.isdir(source): - print('{} -> {}'.format(source, dest)) - shutil.copytree(source, dest, ignore=get_ignored_files) - else: - print('{} -> {}'.format(source, dest)) - shutil.copy(source, dest) - else: - print('{} -> {}'.format(source, dest)) - os.symlink(source, dest) - - -def remove(filename): - """Remove a given filename, regardless of whether it's a file or dir.""" - if os.path.isdir(filename): - shutil.rmtree(filename) - else: - os.unlink(filename) - - -def get_venv_lib_path(path): - """Get the library path of a virtualenv.""" - subdir = 'Scripts' if os.name == 'nt' else 'bin' - executable = os.path.join(path, subdir, 'python') - return run_py(executable, - 'from distutils.sysconfig import get_python_lib', - 'print(get_python_lib())') - - -def get_tox_syspython(tox_path): - """Get the system python based on a virtualenv created by tox.""" - path = os.path.join(tox_path, '.tox-config1') - with io.open(path, encoding='ascii') as f: - line = f.readline() - _md5, sys_python = line.rstrip().split(' ') - return sys_python - - -def main(): - """Main entry point.""" - parser = argparse.ArgumentParser() - parser.add_argument('path', help="Base path to the venv.") - parser.add_argument('qt_version', type=int, - help="Major Qt version (5 for PyQt5, etc).") - parser.add_argument('--tox', help="Add when called via tox.", - action='store_true') - args = parser.parse_args() - - if args.tox: - executable = get_tox_syspython(args.path) - else: - executable = sys.executable - - venv_path = get_venv_lib_path(args.path) - link_pyqt(executable, venv_path, args.qt_version) - - -if __name__ == '__main__': - try: - main() - except Error as e: - print(str(e), file=sys.stderr) - sys.exit(1) diff -Nru pytest-qt-2.3.1/setup.cfg pytest-qt-3.2.2/setup.cfg --- pytest-qt-2.3.1/setup.cfg 2018-01-05 00:32:46.000000000 +0000 +++ pytest-qt-3.2.2/setup.cfg 2018-12-13 17:57:40.000000000 +0000 @@ -1,6 +1,13 @@ [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 diff -Nru pytest-qt-2.3.1/setup.py pytest-qt-3.2.2/setup.py --- pytest-qt-2.3.1/setup.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/setup.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,5 +1,4 @@ import sys -import re from setuptools import setup from setuptools.command.test import test as TestCommand @@ -23,41 +22,41 @@ 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']}, - + 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(), + 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'], + 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', - 'Topic :: Desktop Environment :: Window Managers', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: User Interfaces', + "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}, + tests_require=["pytest"], + cmdclass={"test": PyTest}, ) diff -Nru pytest-qt-2.3.1/tests/conftest.py pytest-qt-3.2.2/tests/conftest.py --- pytest-qt-2.3.1/tests/conftest.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/conftest.py 2018-12-13 17:55:12.000000000 +0000 @@ -5,7 +5,7 @@ import pytest from pytestqt.qt_compat import qt_api -pytest_plugins = 'pytester' +pytest_plugins = "pytester" @pytest.fixture @@ -15,10 +15,9 @@ timeouts are being respected. """ # time.clock() is more accurate on Windows - get_time = time.clock if sys.platform.startswith('win') else time.time + get_time = time.clock if sys.platform.startswith("win") else time.time class StopWatch: - def __init__(self): self._start_time = None self.elapsed = None diff -Nru pytest-qt-2.3.1/tests/test_basics.py pytest-qt-3.2.2/tests/test_basics.py --- pytest-qt-2.3.1/tests/test_basics.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_basics.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,7 +1,8 @@ -import os -import sys import weakref + import pytest + +from pytestqt import qt_compat from pytestqt.qt_compat import qt_api @@ -13,51 +14,61 @@ assert qt_api.QApplication.instance() is not None widget = qt_api.QWidget() qtbot.addWidget(widget) - widget.setWindowTitle('W1') + widget.setWindowTitle("W1") widget.show() assert widget.isVisible() - assert widget.windowTitle() == 'W1' + 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(), - ) + 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.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') + 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(), - ) + 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) + 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) + assert event_recorder.event_data == ( + qt_api.QEvent.MouseButtonPress, + qt_api.Qt.RightButton, + qt_api.Qt.AltModifier, + ) def test_stop_for_interaction(qtbot, timer): @@ -71,19 +82,19 @@ qtbot.stopForInteraction() -@pytest.mark.parametrize('show', [True, False]) -@pytest.mark.parametrize('method_name', ['waitExposed', 'waitActive']) +@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': + 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' + assert str(exc_info.value) == "Available in PyQt5 only" else: widget = qt_api.QWidget() qtbot.add_widget(widget) @@ -96,14 +107,14 @@ pass -@pytest.mark.parametrize('method_name', ['waitExposed', 'waitActive']) +@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') + if qt_api.pytest_qt_api != "pyqt5": + pytest.skip("Available in PyQt5 only") method = getattr(qtbot, method_name) widget = qt_api.QWidget() @@ -111,8 +122,8 @@ 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' + raise ValueError("some other error") + assert str(exc_info.value) == "some other error" def test_widget_kept_as_weakref(qtbot): @@ -137,7 +148,7 @@ https://github.com/pytest-dev/pytest-qt/issues/67 """ testdir.makepyfile( - ''' + """ from pytestqt.qt_compat import qt_api import pytest @@ -172,17 +183,15 @@ assert events_queue.events == [] events_queue.events.append('test event') events_queue.pop_later() - ''' + """ ) res = testdir.runpytest() - res.stdout.fnmatch_lines([ - '*3 passed in*', - ]) + 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 @@ -191,13 +200,12 @@ 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', - ]) + res.stdout.fnmatch_lines( + ["*test session starts*", "PyQtAPI 1.0 -- Qt runtime 2.5 -- Qt compiled 3.5"] + ) def test_public_api_backward_compatibility(): @@ -207,6 +215,7 @@ 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 @@ -221,15 +230,16 @@ """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 + 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): @@ -237,7 +247,8 @@ Ensure widgets added by "qtbot.add_widget" are closed before all other fixtures are teardown. (#106). """ - testdir.makepyfile(''' + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api @@ -258,11 +269,10 @@ def test_foo(widget): pass - ''') + """ + ) result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '*= 1 passed in *' - ]) + result.stdout.fnmatch_lines(["*= 1 passed in *"]) def test_qtbot_wait(qtbot, stop_watch): @@ -274,7 +284,6 @@ @pytest.fixture def event_recorder(qtbot): - class EventRecorder(qt_api.QWidget): """ @@ -306,139 +315,167 @@ return widget -@pytest.mark.parametrize('value, expected', [ - (True, True), - (False, False), - ('True', True), - ('False', False), - ('true', True), - ('false', False), -]) +@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') + pytestqt.qtbot._parse_ini_boolean("foo") -@pytest.mark.parametrize('option_api', ['pyqt4', 'pyqt5', 'pyside', 'pyside2']) +@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") + monkeypatch.delenv("PYTEST_QT_API", raising=False) - testdir.makeini(""" + testdir.makeini( + """ [pytest] qt_api={option_api} - """.format(option_api=option_api)) + """.format( + option_api=option_api + ) + ) - testdir.makepyfile(''' + 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 *' - ]) + 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:*' - ]) + result.stderr.fnmatch_lines(["*ImportError:*"]) else: # Python >= 3.6 - result.stderr.fnmatch_lines([ - '*ModuleNotFoundError:*' - ]) + result.stderr.fnmatch_lines(["*ModuleNotFoundError:*"]) -@pytest.mark.parametrize('envvar', ['pyqt4', 'pyqt5', 'pyside', 'pyside2']) +@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(""" + testdir.makeini( + """ [pytest] qt_api={option_api} - """.format(option_api='piecute')) + """.format( + option_api="piecute" + ) + ) - monkeypatch.setenv('PYTEST_QT_API', envvar) + monkeypatch.setenv("PYTEST_QT_API", envvar) - testdir.makepyfile(''' + 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 *' - ]) + 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:*' - ]) + result.stderr.fnmatch_lines(["*ImportError:*"]) else: # Python >= 3.6 - result.stderr.fnmatch_lines([ - '*ModuleNotFoundError:*' - ]) + 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(''' + testdir.makepyfile( + """ import pytest def test_foo(qtbot): pass - ''') - monkeypatch.setenv('PYTEST_QT_API', 'piecute') + """ + ) + monkeypatch.setenv("PYTEST_QT_API", "piecute") result = testdir.runpytest_subprocess() - result.stderr.fnmatch_lines(['* Invalid value for $PYTEST_QT_API: piecute']) + 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") +@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(''' + testdir.makepyfile( + """ def test_args(qapp): assert '--test-arg' in list(qapp.arguments()) - ''') + """ + ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines([ - '*= 1 passed in *' - ]) + 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) diff -Nru pytest-qt-2.3.1/tests/test_exceptions.py pytest-qt-3.2.2/tests/test_exceptions.py --- pytest-qt-2.3.1/tests/test_exceptions.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_exceptions.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,9 +1,11 @@ -from pytestqt.exceptions import capture_exceptions, format_captured_exceptions -import pytest import sys +import pytest + +from pytestqt.exceptions import capture_exceptions, format_captured_exceptions + -@pytest.mark.parametrize('raise_error', [False, True]) +@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 @@ -11,14 +13,19 @@ :type testdir: _pytest.pytester.TmpTestdir """ - testdir.makepyfile(''' + testdir.makepyfile( + """ from pytestqt.qt_compat import qt_api class Receiver(qt_api.QtCore.QObject): def event(self, ev): if {raise_error}: - raise ValueError('mistakes were made') + try: + raise RuntimeError('original error') + except RuntimeError: + raise ValueError('mistakes were made') + return qt_api.QtCore.QObject.event(self, ev) @@ -29,33 +36,54 @@ app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.User)) app.processEvents() - '''.format(raise_error=raise_error)) + """.format( + raise_error=raise_error + ) + ) result = testdir.runpytest() if raise_error: - result.stdout.fnmatch_lines([ - '*Qt exceptions in virtual methods:*', - '*ValueError: mistakes were made*', - '*1 failed*', - ]) - assert 'pytest.fail' not in '\n'.join(result.outlines) + 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*') + result.stdout.fnmatch_lines("*1 passed*") def test_format_captured_exceptions(): try: - raise ValueError('errors were made') + 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 + 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()] -@pytest.mark.parametrize('no_capture_by_marker', [True, False]) + 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 @@ -64,14 +92,17 @@ :type testdir: TmpTestdir """ if no_capture_by_marker: - marker_code = '@pytest.mark.qt_no_exception_capture' + marker_code = "@pytest.mark.qt_no_exception_capture" else: - marker_code = '' - testdir.makeini(''' + marker_code = "" + testdir.makeini( + """ [pytest] qt_no_exception_capture = 1 - ''') - testdir.makepyfile(''' + """ + ) + testdir.makepyfile( + """ import pytest import sys from pytestqt.qt_compat import qt_api @@ -91,9 +122,12 @@ w = MyWidget() qtbot.addWidget(w) qtbot.mouseClick(w, QtCore.Qt.LeftButton) - '''.format(marker_code=marker_code)) + """.format( + marker_code=marker_code + ) + ) res = testdir.runpytest() - res.stdout.fnmatch_lines(['*1 passed*']) + res.stdout.fnmatch_lines(["*1 passed*"]) def test_no_capture_preserves_custom_excepthook(testdir): @@ -102,7 +136,8 @@ :type testdir: TmpTestdir """ - testdir.makepyfile(''' + testdir.makepyfile( + """ import pytest import sys from pytestqt.qt_compat import qt_api @@ -120,9 +155,10 @@ def test_capture(qtbot): assert sys.excepthook is not custom_excepthook - ''') + """ + ) res = testdir.runpytest() - res.stdout.fnmatch_lines(['*2 passed*']) + res.stdout.fnmatch_lines(["*2 passed*"]) def test_exception_capture_on_call(testdir): @@ -131,7 +167,8 @@ :type testdir: TmpTestdir """ - testdir.makepyfile(''' + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api QWidget = qt_api.QWidget @@ -147,12 +184,10 @@ w = MyWidget() qapp.postEvent(w, QEvent(QEvent.User)) qapp.processEvents() - ''') - res = testdir.runpytest('-s') - res.stdout.fnmatch_lines([ - "*RuntimeError('event processed')*", - '*1 failed*', - ]) + """ + ) + res = testdir.runpytest("-s") + res.stdout.fnmatch_lines(["*RuntimeError('event processed')*", "*1 failed*"]) def test_exception_capture_on_widget_close(testdir): @@ -161,7 +196,8 @@ :type testdir: TmpTestdir """ - testdir.makepyfile(''' + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api QWidget = qt_api.QWidget @@ -177,15 +213,13 @@ 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*', - ]) + """ + ) + res = testdir.runpytest("-s") + res.stdout.fnmatch_lines(["*RuntimeError('close error')*", "*1 error*"]) -@pytest.mark.parametrize('mode', ['setup', 'teardown']) +@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 @@ -193,14 +227,15 @@ :type testdir: TmpTestdir """ - if mode == 'setup': - setup_code = 'send_event(w, qapp)' - teardown_code = '' + if mode == "setup": + setup_code = "send_event(w, qapp)" + teardown_code = "" else: - setup_code = '' - teardown_code = 'send_event(w, qapp)' + setup_code = "" + teardown_code = "send_event(w, qapp)" - testdir.makepyfile(''' + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api QWidget = qt_api.QWidget @@ -228,13 +263,18 @@ 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*', - ]) + """.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 @@ -248,23 +288,23 @@ from pytestqt.plugin import capture_exceptions class Receiver(qt_api.QtCore.QObject): - def event(self, ev): - raise ValueError('mistakes were made') + 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'] + 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(''' + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api QWidget = qt_api.QWidget @@ -288,9 +328,10 @@ assert len(exceptions) == 1 assert str(exceptions[0][1]) == "error" - ''') + """ + ) result = testdir.runpytest() - result.stdout.fnmatch_lines(['*1 passed*']) + result.stdout.fnmatch_lines(["*1 passed*"]) def test_exceptions_to_stderr(qapp, capsys): @@ -301,10 +342,9 @@ from pytestqt.qt_compat import qt_api class MyWidget(qt_api.QWidget): - def event(self, ev): called.append(1) - raise RuntimeError('event processed') + raise RuntimeError("event processed") w = MyWidget() with capture_exceptions() as exceptions: @@ -313,4 +353,44 @@ assert called del exceptions[:] _out, err = capsys.readouterr() - assert "raise RuntimeError('event processed')" in err + 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*"]) diff -Nru pytest-qt-2.3.1/tests/test_logging.py pytest-qt-3.2.2/tests/test_logging.py --- pytest-qt-2.3.1/tests/test_logging.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_logging.py 2018-12-13 17:55:12.000000000 +0000 @@ -4,11 +4,9 @@ from pytestqt.qt_compat import qt_api -pytestmark = pytest.mark.skipif(qt_api.pytest_qt_api == 'pyside2', reason="https://bugreports.qt.io/browse/PYSIDE-435") - -@pytest.mark.parametrize('test_succeeds', [True, False]) -@pytest.mark.parametrize('qt_log', [True, False]) +@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. @@ -33,45 +31,78 @@ 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 {0} - """.format(test_succeeds) + assert {} + """.format( + test_succeeds + ) ) - res = testdir.runpytest(*(['--no-qt-log'] if not qt_log else [])) + 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() + assert "Captured Qt messages" not in res.stdout.str() + assert "Captured stderr call" not in res.stdout.str() else: if qt_log: - res.stdout.fnmatch_lines([ - '*-- Captured Qt messages --*', - '*QtDebugMsg: this is a DEBUG message*', - '*QtWarningMsg: this is a WARNING message*', - '*QtCriticalMsg: this is a CRITICAL message*', - ]) + 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 --*', - 'this is a DEBUG message*', - 'this is a WARNING message*', - 'this is a CRITICAL message*', - ]) + 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. """ - qt_api.qDebug('this is a DEBUG message') - qt_api.qWarning('this is a WARNING message') - qt_api.qCritical('this is a CRITICAL message') + # 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'), + (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): @@ -94,11 +125,11 @@ assert qtlog.records == [] """ ) - res = testdir.runpytest('--no-qt-log') - res.stdout.fnmatch_lines('*1 passed*') + res = testdir.runpytest("--no-qt-log") + res.stdout.fnmatch_lines("*1 passed*") -@pytest.mark.parametrize('use_context_manager', [True, False]) +@pytest.mark.parametrize("use_context_manager", [True, False]) def test_disable_qtlog_context_manager(testdir, use_context_manager): """ Test qtlog.disabled() context manager. @@ -113,9 +144,9 @@ ) if use_context_manager: - code = 'with qtlog.disabled():' + code = "with qtlog.disabled():" else: - code = 'if 1:' + code = "if 1:" testdir.makepyfile( """ @@ -123,14 +154,16 @@ def test_1(qtlog): {code} qt_api.qCritical('message') - """.format(code=code) + """.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]) +@pytest.mark.parametrize("use_mark", [True, False]) def test_disable_qtlog_mark(testdir, use_mark): """ Test mark which disables logging capture for a test. @@ -143,7 +176,7 @@ qt_log_level_fail = CRITICAL """ ) - mark = '@pytest.mark.no_qt_log' if use_mark else '' + mark = "@pytest.mark.no_qt_log" if use_mark else "" testdir.makepyfile( """ @@ -152,7 +185,9 @@ {mark} def test_1(): qt_api.qCritical('message') - """.format(mark=mark) + """.format( + mark=mark + ) ) res = testdir.inline_run() passed = 1 if use_mark else 0 @@ -173,19 +208,20 @@ assert 0 """ ) - f = '{rec.type_name} {rec.log_type_name} {rec.when:%Y-%m-%d}: {rec.message}' - res = testdir.runpytest('--qt-log-format={0}'.format(f)) - today = '{0:%Y-%m-%d}'.format(datetime.datetime.now()) - res.stdout.fnmatch_lines([ - '*-- Captured Qt messages --*', - 'QtWarningMsg WARNING {0}: this is a WARNING message*'.format(today), - ]) + 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)], - ) +@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. @@ -196,7 +232,9 @@ """ [pytest] qt_log_level_fail = {level} - """.format(level=level) + """.format( + level=level + ) ) testdir.makepyfile( """ @@ -213,13 +251,16 @@ ) res = testdir.runpytest() lines = [] - if level != 'NO': - lines.extend([ - '*Failure: Qt messages with level {0} or above emitted*'.format( - level.upper()), - '*-- Captured Qt messages --*', - ]) - lines.append('*{0} passed*'.format(expect_passes)) + 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) @@ -284,35 +325,28 @@ 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*', - + "*_ 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)*', - + "*_ 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*', - + "*_ 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*', + "*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'" -]) +@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. @@ -334,16 +368,17 @@ @pytest.mark.qt_log_ignore({marker_args}) def test1(): qt_api.qCritical('{message}') - """.format(message=message, marker_args=marker_args) + """.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), -]) +@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. @@ -365,7 +400,9 @@ @pytest.mark.qt_log_ignore('match-mark', extend=False) def test1(): qt_api.qCritical('{message}') - """.format(message=message) + """.format( + message=message + ) ) res = testdir.inline_run() @@ -392,18 +429,17 @@ ) res = testdir.runpytest() lines = [ - '*= ERRORS =*', - '*_ ERROR at setup of test1 _*', + "*= ERRORS =*", + "*_ ERROR at setup of test1 _*", "*ValueError: Invalid keyword arguments in {'does_not_exist': True} " - "for qt_log_ignore mark.", - + "for qt_log_ignore mark.", # summary - '*= 1 error in*', + "*= 1 error in*", ] res.stdout.fnmatch_lines(lines) -@pytest.mark.parametrize('apply_mark', [True, False]) +@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. @@ -413,7 +449,7 @@ if apply_mark: mark = '@pytest.mark.qt_log_ignore("WM_DESTROY", "WM_PAINT")' else: - mark = '' + mark = "" testdir.makepyfile( """ from pytestqt.qt_compat import qt_api @@ -422,7 +458,9 @@ {mark} def test1(): qt_api.qCritical('WM_PAINT was sent') - """.format(mark=mark) + """.format( + mark=mark + ) ) res = testdir.inline_run() passed = 1 if apply_mark else 0 @@ -453,14 +491,16 @@ """ ) 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', - ]) + 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*') + res.stdout.fnmatch_lines("*test_lineno_failure.py:2: Failure*") def test_context_none(testdir): @@ -468,29 +508,28 @@ 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. + 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') + if qt_api.pytest_qt_api != "pyqt5": + pytest.skip("Context information only available in PyQt5") testdir.makepyfile( """ from pytestqt.qt_compat import qt_api def test_foo(request): log_capture = request.node.qt_log_capture - context = log_capture._Context(None, None, None) + context = log_capture._Context(None, None, 0, None) log_capture._handle_with_context(qt_api.QtWarningMsg, context, "WARNING message") assert 0 """ ) res = testdir.runpytest() - res.stdout.fnmatch_lines([ - '*None:None:None:*', - '* QtWarningMsg: WARNING message*', - ]) + assert "*None:None:0:*" not in str(res.stdout) + res.stdout.fnmatch_lines(["* QtWarningMsg: WARNING message*"]) def test_logging_broken_makereport(testdir): @@ -501,7 +540,8 @@ :type testdir: _pytest.pytester.TmpTestdir """ - testdir.makepyfile(conftest=""" + testdir.makepyfile( + conftest=""" import pytest @pytest.mark.hookwrapper(tryfirst=True) @@ -509,7 +549,8 @@ if call.when == 'call': raise Exception("This should not be hidden") yield - """) + """ + ) p = testdir.makepyfile( """ def test_foo(): @@ -517,6 +558,4 @@ """ ) res = testdir.runpytest_subprocess(p) - res.stdout.fnmatch_lines([ - '*This should not be hidden*', - ]) + res.stdout.fnmatch_lines(["*This should not be hidden*"]) diff -Nru pytest-qt-2.3.1/tests/test_modeltest.py pytest-qt-3.2.2/tests/test_modeltest.py --- pytest-qt-2.3.1/tests/test_modeltest.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_modeltest.py 2018-12-13 17:55:12.000000000 +0000 @@ -1,12 +1,12 @@ import pytest from pytestqt.qt_compat import qt_api +from pytestqt import modeltest -pytestmark = pytest.mark.usefixtures('qtbot') +pytestmark = pytest.mark.usefixtures("qtbot") class BasicModel(qt_api.QtCore.QAbstractItemModel): - def data(self, index, role=qt_api.QtCore.Qt.DisplayRole): return None @@ -37,45 +37,53 @@ items[0].setChild(0, items[4]) items[4].setChild(0, items[5]) - qtmodeltester.check(model) + qtmodeltester.check(model, force_py=True) def test_string_list_model(qtmodeltester): model = qt_api.QStringListModel() - model.setStringList(['hello', 'world']) - qtmodeltester.check(model) + 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']) + model.setStringList(["hello", "world"]) proxy = qt_api.QSortFilterProxyModel() proxy.setSourceModel(model) - qtmodeltester.check(proxy) + 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, -]) +@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): + 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): + 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: @@ -84,37 +92,40 @@ 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), -]) +@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): + 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): + 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 "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 @@ -126,15 +137,17 @@ 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): + 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 "Contents" return None model = MyModel() - model.set_header_text('Start Header') - check_model(model, should_pass=1) - model.set_header_text('New Header') + model.set_header_text("Start Header") + check_model(model, should_pass=True) + model.set_header_text("New Header") @pytest.fixture @@ -144,17 +157,20 @@ 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) + qtmodeltester.check(model, force_py=True) else: with pytest.raises(AssertionError): - qtmodeltester.check(model) + 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 @@ -162,48 +178,49 @@ model = Model() with pytest.raises(AssertionError): - qtmodeltester.check(model) + qtmodeltester.check(model, force_py=True) def test_changing_model_insert(qtmodeltester): model = qt_api.QStandardItemModel() - item = qt_api.QStandardItem('foo') - qtmodeltester.check(model) + 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') + item = qt_api.QStandardItem("foo") model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, force_py=True) model.removeRow(0) def test_changing_model_data(qtmodeltester): model = qt_api.QStandardItemModel() - item = qt_api.QStandardItem('foo') + item = qt_api.QStandardItem("foo") model.setItem(0, 0, item) - qtmodeltester.check(model) - model.setData(model.index(0, 0), 'hello world') + 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]) +@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') + item = qt_api.QStandardItem("foo") model.setItem(0, 0, item) - qtmodeltester.check(model) - model.setHeaderData(0, orientation, 'blah') + 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') + item = qt_api.QStandardItem("foo") model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, force_py=True) model.sort(0) @@ -218,8 +235,8 @@ With a previous implementation of the modeltester using sip.cast, the custom implementations did never actually run. """ - class Model(BasicModel): + class Model(BasicModel): def __init__(self, parent=None): super(Model, self).__init__(parent) self.row_count_did_run = False @@ -230,30 +247,27 @@ model = Model() assert not model.row_count_did_run - qtmodeltester.check(model) + 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') + self.setData(self.index(0, 0), "bar") model = Model() - item = qt_api.QStandardItem('foo') + item = qt_api.QStandardItem("foo") model.setItem(0, 0, item) - qtmodeltester.check(model) + 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) @@ -261,12 +275,84 @@ return qt_api.QtCore.QModelIndex() model = Model() - item = qt_api.QStandardItem('foo') - item2 = qt_api.QStandardItem('bar') - item3 = qt_api.QStandardItem('bar') + 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) + 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 * ==*", + ] + ) diff -Nru pytest-qt-2.3.1/tests/test_qtest_proxies.py pytest-qt-3.2.2/tests/test_qtest_proxies.py --- pytest-qt-2.3.1/tests/test_qtest_proxies.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_qtest_proxies.py 2018-12-13 17:55:12.000000000 +0000 @@ -4,25 +4,27 @@ from pytestqt.qt_compat import qt_api -fails_on_pyqt = pytest.mark.xfail('not qt_api.pytest_qt_api.startswith("pyside")') - +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', - fails_on_pyqt('keyToAscii'), - 'mouseClick', - 'mouseDClick', - 'mouseEvent', - 'mouseMove', - 'mousePress', - 'mouseRelease', -], +@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): """ diff -Nru pytest-qt-2.3.1/tests/test_wait_signal.py pytest-qt-3.2.2/tests/test_wait_signal.py --- pytest-qt-2.3.1/tests/test_wait_signal.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_wait_signal.py 2018-12-13 17:55:12.000000000 +0000 @@ -5,7 +5,12 @@ import sys from pytestqt.qt_compat import qt_api -from pytestqt.wait_signal import SignalEmittedError, TimeoutError, SignalAndArgs +from pytestqt.wait_signal import ( + SignalEmittedError, + TimeoutError, + SignalAndArgs, + CallbackCalledTwiceError, +) def test_signal_blocker_exception(qtbot): @@ -34,8 +39,7 @@ return blocker -def context_manager_wait(qtbot, signal, timeout, multiple, raising, - should_raise): +def context_manager_wait(qtbot, signal, timeout, multiple, raising, should_raise): """ Waiting for signal using context manager API. """ @@ -67,18 +71,27 @@ @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), - ]) + ("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): +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. @@ -88,8 +101,14 @@ 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) + 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 @@ -97,19 +116,38 @@ stop_watch.check(timeout, delay) -@pytest.mark.parametrize('configval, raises', [ - ('false', False), - ('true', True), - (None, True), -]) -def test_raising(qtbot, testdir, configval, raises): +@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(""" + testdir.makeini( + """ [pytest] - qt_wait_signal_raising = {} - """.format(configval)) + {} = {} + """.format( + configkey, configval + ) + ) - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api @@ -121,22 +159,35 @@ with qtbot.waitSignal(signaller.signal, timeout=10): pass - """) - res = testdir.runpytest() + """ + ) + + 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*']) + res.stdout.fnmatch_lines(["*1 failed*"]) else: - res.stdout.fnmatch_lines(['*1 passed*']) + res.stdout.fnmatch_lines(["*1 passed*"]) -def test_raising_by_default_overridden(qtbot, testdir): - testdir.makeini(""" +@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] - qt_wait_signal_raising = false - """) + {} = false + """.format( + configkey + ) + ) - testdir.makepyfile(""" + testdir.makepyfile( + """ import pytest from pytestqt.qt_compat import qt_api @@ -149,28 +200,46 @@ with qtbot.waitSignal(signal, raising=True, timeout=10) as blocker: pass - """) + """ + ) res = testdir.runpytest() - res.stdout.fnmatch_lines(['*1 failed*']) + 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), - ]) + ( + "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): +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. @@ -181,9 +250,14 @@ 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) + 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 @@ -205,8 +279,9 @@ """ Make sure an explicit emit() inside a waitSignal block works. """ - with qtbot.waitSignals([signaller.signal, signaller.signal_2], - timeout=5000) as waiting: + with qtbot.waitSignals( + [signaller.signal, signaller.signal_2], timeout=5000 + ) as waiting: signaller.signal.emit() signaller.signal_2.emit() @@ -236,30 +311,35 @@ return Signaller() -@pytest.mark.parametrize('multiple', [True, False]) -@pytest.mark.parametrize('raising', [True, False]) -def test_wait_signals_handles_exceptions(qtbot, multiple, raising, 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 waitSignal handles exceptions correctly. + Make sure blockers handle exceptions correctly. """ class TestException(Exception): pass - if multiple: + if blocker == "multiple": func = qtbot.waitSignals - arg = [signaller.signal, signaller.signal_2] - else: + args = [[signaller.signal, signaller.signal_2]] + elif blocker == "single": func = qtbot.waitSignal - arg = signaller.signal + args = [signaller.signal] + elif blocker == "callback": + func = qtbot.waitCallback + args = [] + else: + assert False with pytest.raises(TestException): - with func(arg, timeout=10, raising=raising): + with func(*args, timeout=10, raising=raising): raise TestException -@pytest.mark.parametrize('multiple', [True, False]) -@pytest.mark.parametrize('do_timeout', [True, False]) +@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 @@ -285,7 +365,7 @@ def test_wait_signals_invalid_strict_parameter(qtbot, signaller): with pytest.raises(ValueError): - qtbot.waitSignals([signaller.signal], order='invalid') + qtbot.waitSignals([signaller.signal], order="invalid") def test_destroyed(qtbot): @@ -293,8 +373,8 @@ 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') + if qt_api.pytest_qt_api.startswith("pyside"): + pytest.skip("test crashes PySide and PySide2") import sip @@ -314,8 +394,8 @@ 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] + 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.""" @@ -340,8 +420,8 @@ """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] + signaller.signal_args_2.emit("foo", 2342) + assert blocker.args == ["foo", 2342] def test_signal_identity(signaller): @@ -387,7 +467,11 @@ as signal parameter. """ with pytest.raises(ValueError): - signal_tuple_with_invalid_length = (signaller.signal, "signal()", "does not belong here") + signal_tuple_with_invalid_length = ( + signaller.signal, + "signal()", + "does not belong here", + ) with qtbot.waitSignal(signal=signal_tuple_with_invalid_length, raising=False): pass @@ -409,13 +493,13 @@ def test_signalandargs_equality(): - signal_args1 = SignalAndArgs(signal_name="signal", args=(1,2)) + 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_1 = SignalAndArgs(signal_name="signal", args=(1, 2)) signal_args1_2 = "foo" assert signal_args1_1 != signal_args1_2 @@ -443,71 +527,83 @@ 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), - ]) + 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), + (("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), - ]) + 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), - ]) + 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), + (("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), - ]) + cases.extend( + [ + # don't allow in-between signals + (("A1", "A1", "A2", "B1"), ("A1", "A2", "B1"), False) + ] + ) return cases @@ -524,7 +620,7 @@ 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 + signal = signaller.signal_args if code[0] == "A" else signaller.signal_args_2 return signal @staticmethod @@ -533,12 +629,16 @@ 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" + 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): + 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). @@ -571,74 +671,137 @@ 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) + 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'), + ("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), + (("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), - ] + (("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): + 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: + 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") + ("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): + 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") + 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") + ("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): + 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") + 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") + ("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): + 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") + 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: + 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 @@ -648,12 +811,18 @@ 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) + 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): + with qtbot.waitSignals( + signals=signals_to_expect, + order="none", + check_params_cbs=callbacks, + raising=False, + ): pass @@ -665,13 +834,17 @@ 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: + 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: + with qtbot.waitSignal( + signal=signaller.signal, timeout=200, check_params_cb=None, raising=False + ) as blocker: signaller.signal.emit() assert blocker.all_args == [] @@ -684,9 +857,11 @@ 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)] + 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): """ @@ -695,12 +870,14 @@ """ def cb(str_param, int_param): - return str_param == '1' and int_param == 1 + 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)] + 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): @@ -708,9 +885,12 @@ 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)")] + 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 @@ -725,8 +905,13 @@ 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: + with qtbot.waitSignals( + signals=signals, + timeout=200, + check_params_cbs=None, + order="none", + raising=False, + ) as blocker: pass assert blocker.all_signals_and_args == [] @@ -735,13 +920,20 @@ 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: + 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) + signaller.signal_args.emit("1", 1) assert blocker.all_signals_and_args == [] def test_non_empty_on_timeout_no_cb(self, qtbot, signaller): @@ -750,14 +942,19 @@ 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) + 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=()) + SignalAndArgs(signal_name="signal_args(QString,int)", args=("1", 1)), + SignalAndArgs(signal_name="signal()", args=()), ] def test_non_empty_no_cb(self, qtbot, signaller): @@ -766,16 +963,21 @@ 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: + 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) + 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)) + 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)), ] @@ -790,13 +992,15 @@ 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'): + 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): + 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" @@ -808,10 +1012,12 @@ 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 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'): + if qt_api.pytest_qt_api.startswith("pyside"): signal = (signaller.signal_single_arg, "signal_single_arg(int)") else: signal = signaller.signal_single_arg @@ -823,12 +1029,18 @@ 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): + 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") + 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): """ @@ -836,7 +1048,7 @@ 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'): + if qt_api.pytest_qt_api.startswith("pyside"): signal = (signaller.signal_single_arg, "signal_single_arg(int)") else: signal = signaller.signal_single_arg @@ -845,12 +1057,16 @@ return int_param == 1337 with pytest.raises(TimeoutError) as excinfo: - with qtbot.waitSignal(signal=signal, timeout=200, check_params_cb=arg_validator, raising=True): + 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") + 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): """ @@ -858,7 +1074,7 @@ 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'): + if qt_api.pytest_qt_api.startswith("pyside"): signal = (signaller.signal_args, "signal_args(QString,int)") else: signal = signaller.signal_args @@ -867,17 +1083,23 @@ 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): + 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) + 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) + 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: @@ -894,12 +1116,19 @@ 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): + 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)]") + 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): @@ -908,12 +1137,19 @@ 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): + 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)]") + 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): """ @@ -928,17 +1164,24 @@ 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): + 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': + 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) + 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): """ @@ -946,12 +1189,19 @@ 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): + 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)]") + 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): """ @@ -959,17 +1209,24 @@ 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): + 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': + 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) + 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): """ @@ -978,20 +1235,26 @@ 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): + 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': + 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) + 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): """ @@ -999,18 +1262,30 @@ 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!") + 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): + 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.") + 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): """ @@ -1022,14 +1297,24 @@ return True with pytest.raises(TimeoutError) as excinfo: - signals = [(signaller.signal, "signal_without_args"), (signaller.signal_args, "signal_with_args")] + 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): + 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)]") + assert ex_msg == ( + "Emitted signals: None. " + "Missing: [signal_without_args, signal_with_args (callback: my_cb)]" + ) @staticmethod def get_exception_message(excinfo): @@ -1048,19 +1333,94 @@ with qtbot.assertNotEmitted(signaller.signal): signaller.signal.emit() - fnmatch.fnmatchcase(str(excinfo.value), - "Signal * unexpectedly emitted.") + 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) + signaller.signal_args.emit("foo", 123) - fnmatch.fnmatchcase(str(excinfo.value), - "Signal * unexpectedly emitted with arguments " - "['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 diff -Nru pytest-qt-2.3.1/tests/test_wait_until.py pytest-qt-3.2.2/tests/test_wait_until.py --- pytest-qt-2.3.1/tests/test_wait_until.py 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tests/test_wait_until.py 2018-12-13 17:55:12.000000000 +0000 @@ -23,16 +23,18 @@ qtbot.wait_until -@pytest.fixture(params=['predicate', 'assert']) +@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': + if request.param == "predicate": return lambda: tick_counter.ticks >= 4 else: + def check_ticks(): assert tick_counter.ticks >= 4 + return check_ticks @@ -44,7 +46,6 @@ from pytestqt.qt_compat import qt_api class Counter: - def __init__(self): self._ticks = 0 self.timer = qt_api.QtCore.QTimer() diff -Nru pytest-qt-2.3.1/tox.ini pytest-qt-3.2.2/tox.ini --- pytest-qt-2.3.1/tox.ini 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/tox.ini 2018-12-13 17:55:12.000000000 +0000 @@ -1,35 +1,23 @@ [tox] -# note that tox expects interpreters to be found at C:\PythonXY, -# with XY being python version ("27" or "34") for instance -envlist = py{27,34}-pyqt4, py{34,35}-pyqt5, py{27,34,35}-pyside, lint +envlist = py{36,37}-pyqt5, py{36,37}-pyside2, linting [testenv] -deps=pytest - pyside: pyside -changedir=tests +deps= + pytest + pyside2: pyside2 + pyqt5: pyqt5 commands= - pyqt5: {envpython} ../scripts/link_pyqt.py --tox {envdir} 5 - pyqt4: {envpython} ../scripts/link_pyqt.py --tox {envdir} 4 - {envpython} -m pytest {posargs} + pytest {posargs} setenv= - pyside: PYTEST_QT_API=pyside - pyqt4: PYTEST_QT_API=pyqt4 - pyqt4v2: PYTEST_QT_API=pyqt4v2 + pyside2: PYTEST_QT_API=pyside2 pyqt5: PYTEST_QT_API=pyqt5 - pyqt5: QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms passenv=DISPLAY XAUTHORITY USER USERNAME -[testenv:lint] -basepython=python3.4 -usedevelop=true -deps= - pytest - sphinx - sphinx_rtd_theme - restructuredtext_lint -changedir=docs -setenv= - READTHEDOCS=True -commands= - rst-lint {toxinidir}/CHANGELOG.rst {toxinidir}/README.rst - sphinx-build -q -E -W -b html . _build +[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 diff -Nru pytest-qt-2.3.1/.travis.yml pytest-qt-3.2.2/.travis.yml --- pytest-qt-2.3.1/.travis.yml 2018-01-05 00:29:08.000000000 +0000 +++ pytest-qt-3.2.2/.travis.yml 2018-12-13 17:55:12.000000000 +0000 @@ -1,13 +1,17 @@ -language: generic +language: python +python: "3.6" sudo: required dist: trusty env: global: # used by ci-helpers - - CONDA_CHANNELS=conda-forge SETUP_XVFB=true DEPS="pytest tox coveralls six" + - 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 @@ -22,28 +26,46 @@ - PYTEST_QT_API=pyqt5 PYQT_PACKAGE="pyqt=5.*" PYTHON_VERSION=3.6 - PYTEST_QT_API=pyside2 PYQT_PACKAGE="pyside2=2.*" PYTHON_VERSION=3.6 -install: - - sudo apt-get update - - # Xvfb / window manager - - sudo apt-get install -y xvfb herbstluftwm - - # Setup miniconda - - git clone --depth 1 git://github.com/astropy/ci-helpers.git - - CONDA_DEPENDENCIES="${DEPS} ${PYQT_PACKAGE}" source ci-helpers/travis/setup_conda.sh - - source activate test && pip install -e . +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: - - source activate test && catchsegv coverage run --source=pytestqt -m pytest -v tests - # for some reason tox doesn't get installed with a u+x flag - - | - chmod u+x /home/travis/miniconda/envs/test/bin/tox - /home/travis/miniconda/envs/test/bin/tox -e lint + - | + if [ $LINTING == "1" ]; then + tox -e linting + else + source activate test && catchsegv coverage run --source=pytestqt -m pytest -v tests + fi after_success: - coveralls @@ -59,4 +81,3 @@ tags: true repo: pytest-dev/pytest-qt condition: $PYTEST_QT_API = pyqt5 -