From faeeaa838a30666e312178ffbc3729d9f62169c0 Mon Sep 17 00:00:00 2001 From: "Hendry, Adam" Date: Sat, 25 Jun 2022 17:26:34 -0700 Subject: [PATCH] fix(typehint): use redundant alias imports for mypy This commit also better aligns the repo with `pyvista/CONTRIBUTING.md`. Fixes Issue #163 --- .codespellrc | 4 + .flake8 | 21 ++ .gitignore | 8 + .isort.cfg | 6 - .pre-commit-config.yaml | 51 +++++ .pylintrc | 82 +------- .vscode/settings.json | 141 +++++++++++++ docs/conf.py | 119 ++++++----- mypy.ini | 17 -- pyproject.toml | 21 ++ pyvistaqt/__init__.py | 57 +++--- pyvistaqt/_version.py | 4 +- pyvistaqt/counter.py | 6 +- pyvistaqt/dialog.py | 53 ++--- pyvistaqt/editor.py | 12 +- pyvistaqt/plotting.py | 221 +++++++++----------- pyvistaqt/rwi.py | 436 +++++++++++++++++++++++----------------- pyvistaqt/utils.py | 16 +- setup.cfg | 39 ++++ setup.py | 63 +++--- tests/conftest.py | 9 +- tests/test_plotting.py | 203 ++++++++++--------- tests/test_qt.py | 1 + 23 files changed, 915 insertions(+), 675 deletions(-) create mode 100644 .codespellrc create mode 100644 .flake8 delete mode 100644 .isort.cfg create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json delete mode 100644 mypy.ini create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..14ceed54 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ +[codespell] +skip = *.pyc,*.txt,*.gif,*.png,*.jpg,*.ply,*.vtk,*.vti,*.vtu,*.js,*.html,*.doctree,*.ttf,*.woff,*.woff2,*.eot,*.mp4,*.inv,*.pickle,*.ipynb,flycheck*,./.git/*,./.hypothesis/*,*.yml,doc/_build/*,./doc/images/*,./dist/*,*~,.hypothesis*,./doc/examples/*,*.mypy_cache/*,*cover,./tests/tinypages/_build/*,*/_autosummary/* +ignore-words-list = lod,byteorder,flem,parm,doubleclick,revered +quiet-level = 3 \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..2dbf0e1b --- /dev/null +++ b/.flake8 @@ -0,0 +1,21 @@ +[flake8] +exclude = .git,__pycache__,build,dist,doc/examples,doc/_build,pyvista/**/__init__.py,pyvista/__init__.py,.venv,.venv/**/*.py, .venv/**/*.pyi +ignore = + # whitespace before ':' + E203, + # line break before binary operator + W503, + # line length too long + E501, + # do not assign a lambda expression, use a def + E731, + # too many leading '#' for block comment + E266, + # ambiguous variable name + E741, + # module level import not at top of file + E402, + # Quotes (temporary) + Q0, + # bare excepts (temporary) + B001, E722 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ef242522..adeccc48 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,11 @@ docs/external_examples.rst # vim *.swp + +<<<<<<< Updated upstream +# venv +.venv +======= +# virtual environments +.venv/ +>>>>>>> Stashed changes diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index ba2778dc..00000000 --- a/.isort.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[settings] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..7b5f1517 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,51 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + +- repo: https://gitlab.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: [ + "flake8-black==0.3.2", + "flake8-isort==4.1.1", + "flake8-quotes==3.3.1", + ] + +- repo: https://github.com/codespell-project/codespell + rev: v2.1.0 + hooks: + - id: codespell + args: [ + "doc examples examples_flask pyvista tests", + "*.py *.rst *.md", + ] + +- repo: https://github.com/pycqa/pydocstyle + rev: 6.1.1 + hooks: + - id: pydocstyle + additional_dependencies: [toml==0.10.2] + files: ^(pyvista/|other/) + exclude: ^pyvista/ext/ + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: no-commit-to-branch + args: [--branch, main] + +# this validates our github workflow files +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.15.1 + hooks: + - id: check-github-workflows \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index a0629152..f75ec6eb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,17 +60,8 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, +# Many previous options removed in pylint PR4942 +disable=raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, @@ -78,68 +69,6 @@ disable=print-statement, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - bad-continuation, arguments-differ, no-name-in-module, no-member @@ -442,13 +371,6 @@ max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..38909b12 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,141 @@ +{ + "[markdown]": { + "editor.codeActionsOnSave": { + "source.fixAll.markdownlint": true + }, + "editor.defaultFormatter": null, + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.wordWrap": "bounded", + "editor.wordWrapColumn": 90 + }, + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "editor.defaultFormatter": "ms-python.python", + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true + }, + "cSpell.allowCompoundWords": true, + "cSpell.dictionaries": [ + "python" + ], + "cSpell.ignorePaths": [ + "*.dll", + "*.html", + "*.css", + "*.json", + "*.git/**", + "**/build/**", + ".venv/", + "*.bat" + ], + "cSpell.words": [ + "autocrlf", + "bdist", + "CICD", + "cobertura", + "deps", + "DICOM", + "docparams", + "docstrings", + "docutils", + "fspath", + "genindex", + "gitlabci", + "GLFM", + "guiqtbot", + "Hendry", + "ipynb", + "iscc", + "isort", + "Kaszynski", + "Kubernetes", + "modindex", + "mypy", + "NSIS", + "nsist", + "numfig", + "pwsh", + "pyinstaller", + "pylint", + "pylintrc", + "pynsist", + "pyproject", + "pyqt", + "pyside", + "pytest", + "pytestqt", + "PYTHONIOENCODING", + "pyvista", + "pyvistaqt", + "pyvistaqtdoc", + "qapp", + "qtbot", + "QTBUG", + "qtpy", + "rcfile", + "recommonmark", + "rgba", + "rsrc", + "sphinxcontrib", + "tada", + "tldr", + "toctree", + "todos", + "venv", + "virtualenv", + "vmtk" + ], + "editor.acceptSuggestionOnEnter": "off", + "editor.formatOnSave": false, + "editor.rulers": [ + 90 + ], + "editor.tabCompletion": "on", + "esbonio.sphinx.buildDir": "${workspaceFolder}/docs/_build", + "esbonio.sphinx.confDir": "${workspaceFolder}/docs", + "esbonio.sphinx.forceFullBuild": false, + "explorer.compactFolders": false, + "files.exclude": { + "**/.git": false + }, + "git-graph.referenceLabels.alignment": "Branches (aligned to the graph) & Tags (on the right)", + "grammarly.files.include": [ + "**/README.rst", + "**/CODE_OF_CONDUCT.md", + "**/CONTRIBUTING.md" + ], + "markdownlint.config": { + "MD026": false, // Trailing punctuation okay in headers (e.g. emojis) + "MD033": false, // Some HTML is fine + "MD041": false, // Allow non-headings for first line in file + "MD046": false // Allow indented code blocks (M029 errors otherwise) + }, + "mypy.dmypyExecutable": "${workspaceFolder}/.venv/Scripts/dmypy.exe", + "powershell.integratedConsole.showOnStartup": false, + "powershell.powerShellDefaultVersion": "PowerShell Core 7 (x64)", + "python.formatting.provider": "black", + "python.linting.flake8Enabled": true, + "python.linting.flake8Path": "${workspaceFolder}/.venv/Scripts/flake8.exe", + "python.linting.mypyArgs": [ + "--config-file", + "mypy.ini" + ], + "python.linting.mypyEnabled": false, + "python.linting.mypyPath": "${workspaceFolder}/.venv/Scripts/mypy.exe", + "python.linting.pylintEnabled": true, + "python.linting.pylintPath": "${workspaceFolder}/.venv/Scripts/pylint.exe", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "python.testing.pytestArgs": [ + "${workspaceFolder}/tests" + ], + "python.testing.pytestEnabled": true, + "python.testing.pytestPath": "${workspaceFolder}/.venv/Scripts/pytest.exe", + "restructuredtext.linter.rstcheck.executablePath": "${workspaceFolder}/.venv/Scripts/rstcheck.exe", + "trailing-spaces.syntaxIgnore": [ + "markdown" + ] +} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 443edd55..bb31f6a5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,24 +1,36 @@ +"""Pytest configuration settings for ``pyvistaqt``.""" +# pylint: disable=invalid-name # Sphinx uses variables named here (cannot make UPPER) +from __future__ import annotations + import datetime import os import sys +from typing import Optional +import warnings + +from docutils.parsers.rst import directives +import numpy as np +import pyvista +from sphinx.ext.autosummary import Autosummary, get_documenter +from sphinx.util.inspect import safe_getattr +import sphinx_rtd_theme + +import pyvistaqt if sys.version_info >= (3, 0): import faulthandler + faulthandler.enable() sys.path.insert(0, os.path.abspath('.')) - -import numpy as np # -- pyvista configuration --------------------------------------------------- -import pyvista -import pyvistaqt # Manage errors pyvista.set_error_output_file('errors.txt') # Ensure that offscreen rendering is used for docs generation -pyvista.OFF_SCREEN = True # Not necessary - simply an insurance policy +pyvista.OFF_SCREEN = True # Not necessary - simply an insurance policy pyvista.BUILDING_GALLERY = True # Preferred plotting style for documentation pyvista.set_plot_theme('document') @@ -29,12 +41,13 @@ os.makedirs(pyvista.FIGURE_PATH) # SG warnings -import warnings + warnings.filterwarnings( - "ignore", + 'ignore', category=UserWarning, - message='Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.', + message='Matplotlib is currently using agg, which is a non-GUI backend, so cannot' + 'show the figure.', ) # -- General configuration ------------------------------------------------ @@ -45,16 +58,17 @@ # 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.napoleon', - 'sphinx.ext.doctest', - 'sphinx.ext.autosummary', - 'notfound.extension', - 'sphinx_copybutton', - 'sphinx.ext.extlinks', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx' - ] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.doctest', + 'sphinx.ext.autosummary', + 'notfound.extension', + 'sphinx_copybutton', + 'sphinx.ext.extlinks', + 'sphinx.ext.coverage', + 'sphinx.ext.intersphinx', +] linkcheck_retries = 3 @@ -70,10 +84,10 @@ master_doc = 'index' # General information about the project. -project = u'PyVistaQt' +project = 'PyVistaQt' year = datetime.date.today().year -copyright = u'2017-{}, The PyVista Developers'.format(year) -author = u'Alex Kaszynski and Bane Sullivan' +copyright = f'2017-{year}, The PyVista Developers' # pylint: disable=redefined-builtin +author = 'Alex Kaszynski and Bane Sullivan' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -89,8 +103,8 @@ # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None +# Usually you set 'language' from the command line for these cases. +language: Optional[str] = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -112,24 +126,36 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_context = { - # Enable the "Edit in GitHub link within the header of each page. + # Enable the 'Edit in GitHub link within the header of each page. 'display_github': False, # Set the following variables to generate the resulting github URL for each page. - # Format Template: https://{{ github_host|default("github.com") }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} + # Format Template: https://{{ github_host|default('github.com') }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} # pylint: disable=line-too-long 'github_user': 'pyvista', 'github_repo': 'pyvistaqt', 'github_version': 'main/docs/', 'menu_links_name': 'Getting Connected', 'menu_links': [ - (' Slack Community', 'http://slack.pyvista.org'), - (' Support', 'https://github.com/pyvista/pyvista-support'), - (' Source Code', 'https://github.com/pyvista/pyvistaqt'), - (' Contributing', 'https://github.com/pyvista/pyvistaqt/blob/main/CONTRIBUTING.md'), + ( + ' Slack Community', + 'http://slack.pyvista.org', + ), + ( + ' Support', + 'https://github.com/pyvista/pyvista-support', + ), + ( + ' Source Code', + 'https://github.com/pyvista/pyvistaqt', + ), + ( + ' Contributing', + 'https://github.com/pyvista/pyvistaqt/blob/main/CONTRIBUTING.md', + ), ], } @@ -143,7 +169,7 @@ # 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". +# so a file named 'default.css' will overwrite the builtin 'default.css'. html_static_path = ['_static'] @@ -154,25 +180,21 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None, - 'https://docs.pyvista.org/': None, +intersphinx_mapping = { + 'https://docs.python.org/': None, + 'https://docs.pyvista.org/': None, } # -- Custom 404 page notfound_context = { - 'body': '

Page not found.

\n\nPerhaps try the examples page.', + 'body': '

Page not found.

\n\nPerhaps try the ' + 'examples page.', } notfound_no_urls_prefix = True -from docutils.parsers.rst import directives -# -- Autosummary options -from sphinx.ext.autosummary import Autosummary, get_documenter -from sphinx.util.inspect import safe_getattr - - class AutoAutoSummary(Autosummary): option_spec = { @@ -188,7 +210,7 @@ def get_members(obj, typ, include_public=None): if not include_public: include_public = [] items = [] - for name in sorted(obj.__dict__.keys()):#dir(obj): + for name in sorted(obj.__dict__.keys()): # dir(obj): try: documenter = get_documenter(AutoAutoSummary.app, safe_getattr(obj, name), obj) except AttributeError: @@ -206,17 +228,22 @@ def run(self): c = getattr(m, class_name) if 'methods' in self.options: _, methods = self.get_members(c, ['method'], ['__init__']) - self.content = ["~%s.%s" % (clazz, method) for method in methods if not method.startswith('_')] + self.content = [ + '~%s.%s' % (clazz, method) for method in methods if not method.startswith('_') + ] if 'attributes' in self.options: _, attribs = self.get_members(c, ['attribute', 'property']) - self.content = ["~%s.%s" % (clazz, attrib) for attrib in attribs if not attrib.startswith('_')] + self.content = [ + '~%s.%s' % (clazz, attrib) for attrib in attribs if not attrib.startswith('_') + ] except: - print('Something went wrong when autodocumenting {}'.format(clazz)) + print(f'Something went wrong when autodocumenting {clazz}') finally: return super(AutoAutoSummary, self).run() + def setup(app): AutoAutoSummary.app = app app.add_directive('autoautosummary', AutoAutoSummary) - app.add_css_file("style.css") - app.add_css_file("copybutton.css") + app.add_css_file('style.css') + app.add_css_file('copybutton.css') diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 5fbf65b4..00000000 --- a/mypy.ini +++ /dev/null @@ -1,17 +0,0 @@ -[mypy] -disallow_untyped_defs = True - -[mypy-vtk.*] -ignore_missing_imports = True - -[mypy-vtkmodules.*] -ignore_missing_imports = True - -[mypy-pyvista.*] -ignore_missing_imports = True - -[mypy-qtpy.*] -ignore_missing_imports = True - -[mypy-IPython.*] -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d7bc5bf5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.isort] +profile = "black" +line_length = 100 +# Sort by name, don't cluster "from" vs "import" +force_sort_within_sections = true +# Combines "as" imports on the same line +combine_as_imports = true +skip_glob = "pyvistaqt/**/__init__.py,pyvistaqt/__init__.py" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true + +[tool.black] +line-length = 100 +skip-string-normalization = true +target-version = ["py39"] +exclude='\.eggs|\.git|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist|node_modules' + +[tool.pydocstyle] +match = '(?!coverage).*.py' \ No newline at end of file diff --git a/pyvistaqt/__init__.py b/pyvistaqt/__init__.py index 003c6288..afef228a 100755 --- a/pyvistaqt/__init__.py +++ b/pyvistaqt/__init__.py @@ -1,40 +1,35 @@ """PyVista package for 3D plotting and mesh analysis.""" +# pylint: disable=useless-import-alias # PEP 484 (mypy) requires redundant aliases +from __future__ import annotations + from ._version import __version__ +# mypy <= 0.961 does not support conditional imports +# See `Issue 1297`_ and `Issue 1393`_ +# +# .. _`Issue 1297`: +# https://github.com/python/mypy/issues/1297 +# .. _`Issue 1393`: +# https://github.com/python/mypy/issues/1393#issuecomment-1153228303 try: - from qtpy import QtCore # noqa -except Exception as exc: # pragma: no cover # pylint: disable=broad-except - _exc_msg = exc - - # pylint: disable=too-few-public-methods - class _QtBindingError: - def __init__(self, *args, **kwargs): - raise RuntimeError(f"No Qt binding was found, got: {_exc_msg}") - - # pylint: disable=too-few-public-methods - class BackgroundPlotter(_QtBindingError): - """Handle Qt binding error for BackgroundPlotter.""" - - # pylint: disable=too-few-public-methods - class MainWindow(_QtBindingError): - """Handle Qt binding error for MainWindow.""" - - # pylint: disable=too-few-public-methods - class MultiPlotter(_QtBindingError): - """Handle Qt binding error for MultiPlotter.""" - - # pylint: disable=too-few-public-methods - class QtInteractor(_QtBindingError): - """Handle Qt binding error for QtInteractor.""" - + from qtpy import QtCore as _QtCore +except Exception as exc: + QtCore = None # pylint: disable=invalid-name + raise RuntimeError(f'No Qt binding was found, got: {exc}') from exc else: - from .plotting import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor + QtCore = _QtCore + from .plotting import ( + BackgroundPlotter as BackgroundPlotter, + MultiPlotter as MultiPlotter, + QtInteractor as QtInteractor, + ) + from .window import MainWindow as MainWindow __all__ = [ - "__version__", - "BackgroundPlotter", - "MainWindow", - "MultiPlotter", - "QtInteractor", + '__version__', + 'BackgroundPlotter', + 'MainWindow', + 'MultiPlotter', + 'QtInteractor', ] diff --git a/pyvistaqt/_version.py b/pyvistaqt/_version.py index 41aab3cf..5098b481 100644 --- a/pyvistaqt/_version.py +++ b/pyvistaqt/_version.py @@ -1,6 +1,6 @@ """Version info for pyvistaqt.""" # major, minor, patch -VERSION_INFO = 0, 10, "dev0" +VERSION_INFO = 0, 10, 'dev0' # Nice string for the version -__version__ = ".".join(map(str, VERSION_INFO)) +__version__ = '.'.join(map(str, VERSION_INFO)) diff --git a/pyvistaqt/counter.py b/pyvistaqt/counter.py index e31a7a64..1fecd817 100644 --- a/pyvistaqt/counter.py +++ b/pyvistaqt/counter.py @@ -16,11 +16,9 @@ def __init__(self, count: int) -> None: if isinstance(count, int) and count > 0: self.count = count elif count > 0: - raise TypeError( - f"Expected type of `count` to be `int` but got: {type(count)}" - ) + raise TypeError(f'Expected type of `count` to be `int` but got: {type(count)}') else: - raise ValueError("count is not strictly positive.") + raise ValueError('count is not strictly positive.') @Slot() def decrease(self) -> None: diff --git a/pyvistaqt/dialog.py b/pyvistaqt/dialog.py index c531da14..8a3573e6 100644 --- a/pyvistaqt/dialog.py +++ b/pyvistaqt/dialog.py @@ -6,14 +6,7 @@ import pyvista as pv from qtpy import QtCore from qtpy.QtCore import Signal -from qtpy.QtWidgets import ( - QDialog, - QDoubleSpinBox, - QFileDialog, - QFormLayout, - QHBoxLayout, - QSlider, -) +from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QFileDialog, QFormLayout, QHBoxLayout, QSlider from .window import MainWindow @@ -21,8 +14,8 @@ class FileDialog(QFileDialog): """Generic file query. - It emits a signal when a file is selected and - the dialog was property closed. + It emits a signal when a file is selected and the dialog was + property closed. """ # pylint: disable=too-few-public-methods @@ -66,7 +59,6 @@ def emit_accepted(self) -> None: Sends: filename - """ if self.result(): filename = self.selectedFiles()[0] @@ -79,7 +71,6 @@ class DoubleSlider(QSlider): Reference: https://gist.github.com/dennis-tra/994a65d6165a328d4eabaadbaedac2cc - """ def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -101,20 +92,16 @@ def _value_range(self) -> float: def value(self) -> float: """Return the value of the slider.""" - return ( - float(super().value()) / self._max_int * self._value_range + self._min_value - ) + return float(super().value()) / self._max_int * self._value_range + self._min_value def setValue(self, value: float) -> None: # pylint: disable=invalid-name """Set the value of the slider.""" - super().setValue( - int((value - self._min_value) / self._value_range * self._max_int) - ) + super().setValue(int((value - self._min_value) / self._value_range * self._max_int)) def setMinimum(self, value: float) -> None: # pylint: disable=invalid-name """Set the minimum value of the slider.""" if value > self._max_value: # pragma: no cover - raise ValueError("Minimum limit cannot be higher than maximum") + raise ValueError('Minimum limit cannot be higher than maximum') self._min_value = value self.setValue(self.value()) @@ -122,7 +109,7 @@ def setMinimum(self, value: float) -> None: # pylint: disable=invalid-name def setMaximum(self, value: float) -> None: # pylint: disable=invalid-name """Set the maximum value of the slider.""" if value < self._min_value: # pragma: no cover - raise ValueError("Minimum limit cannot be higher than maximum") + raise ValueError('Minimum limit cannot be higher than maximum') self._max_value = value self.setValue(self.value()) @@ -152,9 +139,7 @@ def __init__( self.minimum = minimum self.maximum = maximum - self.spinbox = QDoubleSpinBox( - value=value, minimum=minimum, maximum=maximum, decimals=4 - ) + self.spinbox = QDoubleSpinBox(value=value, minimum=minimum, maximum=maximum, decimals=4) self.addWidget(self.slider) self.addWidget(self.spinbox) @@ -200,9 +185,7 @@ class ScaleAxesDialog(QDialog): accepted = Signal(float) signal_close = Signal() - def __init__( - self, parent: MainWindow, plotter: pv.Plotter, show: bool = True - ) -> None: + def __init__(self, parent: MainWindow, plotter: pv.Plotter, show: bool = True) -> None: """Initialize the scaling dialog.""" super().__init__(parent) self.setGeometry(300, 300, 50, 50) @@ -211,20 +194,14 @@ def __init__( self.plotter = plotter self.plotter.app_window.signal_close.connect(self.close) - self.x_slider_group = RangeGroup( - parent, self.update_scale, value=plotter.scale[0] - ) - self.y_slider_group = RangeGroup( - parent, self.update_scale, value=plotter.scale[1] - ) - self.z_slider_group = RangeGroup( - parent, self.update_scale, value=plotter.scale[2] - ) + self.x_slider_group = RangeGroup(parent, self.update_scale, value=plotter.scale[0]) + self.y_slider_group = RangeGroup(parent, self.update_scale, value=plotter.scale[1]) + self.z_slider_group = RangeGroup(parent, self.update_scale, value=plotter.scale[2]) form_layout = QFormLayout(self) - form_layout.addRow("X Scale", self.x_slider_group) - form_layout.addRow("Y Scale", self.y_slider_group) - form_layout.addRow("Z Scale", self.z_slider_group) + form_layout.addRow('X Scale', self.x_slider_group) + form_layout.addRow('Y Scale', self.y_slider_group) + form_layout.addRow('Z Scale', self.z_slider_group) self.setLayout(form_layout) diff --git a/pyvistaqt/editor.py b/pyvistaqt/editor.py index 96dabab9..bcf9398a 100644 --- a/pyvistaqt/editor.py +++ b/pyvistaqt/editor.py @@ -44,7 +44,7 @@ def _selection_callback() -> None: self.tree_widget.itemSelectionChanged.connect(_selection_callback) self.setLayout(self.layout) - self.setWindowTitle("Editor") + self.setWindowTitle('Editor') self.setModal(True) self.update() @@ -55,7 +55,7 @@ def update(self) -> None: for idx, renderer in enumerate(self.renderers): actors = renderer._actors # pylint: disable=protected-access widget_idx = self.stacked_widget.addWidget(_get_renderer_widget(renderer)) - top_item = QTreeWidgetItem(self.tree_widget, [f"Renderer {idx}"]) + top_item = QTreeWidgetItem(self.tree_widget, [f'Renderer {idx}']) top_item.setData(0, Qt.ItemDataRole.UserRole, widget_idx) self.tree_widget.addTopLevelItem(top_item) for name, actor in actors.items(): @@ -86,8 +86,8 @@ def _axes_callback(state: bool) -> None: else: renderer.hide_axes() - axes = QCheckBox("Axes") - if hasattr(renderer, "axes_widget"): + axes = QCheckBox('Axes') + if hasattr(renderer, 'axes_widget'): axes.setChecked(renderer.axes_widget.GetEnabled()) else: axes.setChecked(False) @@ -105,7 +105,7 @@ def _get_actor_widget(actor: vtkActor) -> QWidget: prop = actor.GetProperty() # visibility - visibility = QCheckBox("Visibility") + visibility = QCheckBox('Visibility') visibility.setChecked(actor.GetVisibility()) visibility.toggled.connect(actor.SetVisibility) layout.addWidget(visibility) @@ -117,7 +117,7 @@ def _get_actor_widget(actor: vtkActor) -> QWidget: opacity.setMaximum(1.0) opacity.setValue(prop.GetOpacity()) opacity.valueChanged.connect(prop.SetOpacity) - tmp_layout.addWidget(QLabel("Opacity")) + tmp_layout.addWidget(QLabel('Opacity')) tmp_layout.addWidget(opacity) layout.addLayout(tmp_layout) diff --git a/pyvistaqt/plotting.py b/pyvistaqt/plotting.py index 496ce88d..f5c96590 100644 --- a/pyvistaqt/plotting.py +++ b/pyvistaqt/plotting.py @@ -1,6 +1,5 @@ # pylint: disable=too-many-lines -""" -This module contains the QtInteractor and BackgroundPlotter. +"""This module contains the QtInteractor and BackgroundPlotter. Diagram ^^^^^^^ @@ -40,17 +39,16 @@ ``BasePlotter.__init__`` with a no-op ``__init__``. """ import contextlib +from functools import wraps import logging import os import platform import time -import warnings -from functools import wraps from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union +import warnings import numpy as np # type: ignore import pyvista -import scooby # type: ignore from pyvista import global_theme from pyvista.plotting.plotting import BasePlotter from pyvista.plotting.render_window_interactor import RenderWindowInteractor @@ -68,6 +66,7 @@ QVBoxLayout, QWidget, ) +import scooby # type: ignore from vtkmodules.vtkRenderingUI import vtkGenericRenderWindowInteractor from .counter import Counter @@ -89,7 +88,7 @@ else: from qtpy import QtGui # pylint: disable=ungrouped-imports -LOG = logging.getLogger("pyvistaqt") +LOG = logging.getLogger('pyvistaqt') LOG.setLevel(logging.CRITICAL) LOG.addHandler(logging.StreamHandler()) @@ -104,8 +103,8 @@ # LOG = logging.getLogger(__name__) # LOG.setLevel('DEBUG') -SAVE_CAM_BUTTON_TEXT = "Save Camera" -CLEAR_CAMS_BUTTON_TEXT = "Clear Cameras" +SAVE_CAM_BUTTON_TEXT = 'Save Camera' +CLEAR_CAMS_BUTTON_TEXT = 'Clear Cameras' def resample_image(arr: np.ndarray, max_size: int = 400) -> np.ndarray: @@ -198,14 +197,14 @@ def __init__( ) -> None: # pylint: disable=too-many-branches """Initialize Qt interactor.""" - LOG.debug("QtInteractor init start") + LOG.debug('QtInteractor init start') self.url: QtCore.QUrl = None # Cannot use super() here because # QVTKRenderWindowInteractor silently swallows all kwargs # because they use **kwargs in their constructor... qvtk_kwargs = dict(parent=parent) - for key in ("stereo", "iren", "rw", "wflags"): + for key in ('stereo', 'iren', 'rw', 'wflags'): if key in kwargs: qvtk_kwargs[key] = kwargs.pop(key) with _no_base_plotter_init(): @@ -262,32 +261,26 @@ def __init__( self.render_timer.timeout.connect(self.render) self.render_timer.start(twait) - if global_theme.depth_peeling["enabled"]: + if global_theme.depth_peeling['enabled']: if self.enable_depth_peeling(): for renderer in self.renderers: renderer.enable_depth_peeling() self._first_time = False # Crucial! - LOG.debug("QtInteractor init stop") + LOG.debug('QtInteractor init stop') def _setup_interactor(self, off_screen: bool) -> None: if off_screen: self.iren: Any = None else: - self.iren = RenderWindowInteractor( - self, interactor=self.ren_win.GetInteractor() - ) - self.iren.interactor.RemoveObservers( - "MouseMoveEvent" - ) # slows window update? + self.iren = RenderWindowInteractor(self, interactor=self.ren_win.GetInteractor()) + self.iren.interactor.RemoveObservers('MouseMoveEvent') # slows window update? self.iren.initialize() self.enable_trackball_style() def _setup_key_press(self) -> None: - self._observers: Dict[ - None, None - ] = {} # Map of events to observers of self.iren - self.iren.add_observer("KeyPressEvent", self.key_press_event) + self._observers: Dict[None, None] = {} # Map of events to observers of self.iren + self.iren.add_observer('KeyPressEvent', self.key_press_event) self.reset_key_events() def gesture_event(self, event: QGestureEvent) -> bool: @@ -308,7 +301,7 @@ def _render(self, *args: Any, **kwargs: Any) -> BasePlotter.render: """Wrap ``BasePlotter.render``.""" return BasePlotter.render(self, *args, **kwargs) - @conditional_decorator(threaded, platform.system() == "Darwin") + @conditional_decorator(threaded, platform.system() == 'Darwin') def render(self) -> None: """Override the ``render`` method to handle threading issues.""" return self.render_signal.emit() @@ -345,7 +338,6 @@ def link_views_across_plotters( ---- For linking views belonging to a single plotter, please use pyvista's `Plotter.link_views` method. - """ if other_views is None: other_views = np.arange(len(other_plotter.renderers)) @@ -356,8 +348,8 @@ def link_views_across_plotters( if not np.issubdtype(other_views.dtype, int): raise TypeError( - "Expected `other_views` type is int, or list or tuple of ints, " - f"but {other_views.dtype} is given" + 'Expected `other_views` type is int, or list or tuple of ints, ' + f'but {other_views.dtype} is given' ) renderer = self.renderers[view] @@ -368,9 +360,9 @@ def link_views_across_plotters( def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: """Event is called when something is dropped onto the vtk window. - Only triggers event when event contains file paths that - exist. User can drop anything in this window and we only want - to allow files. + Only triggers event when event contains file paths that exist. + User can drop anything in this window and we only want to allow + files. """ try: for url in event.mimeData().urls(): @@ -378,7 +370,7 @@ def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: # only call accept on files event.accept() except IOError as exception: # pragma: no cover - warnings.warn(f"Exception when dragging files: {str(exception)}") + warnings.warn(f'Exception when dragging files: {str(exception)}') # pylint: disable=invalid-name,useless-return def dropEvent(self, event: QtCore.QEvent) -> None: @@ -390,13 +382,13 @@ def dropEvent(self, event: QtCore.QEvent) -> None: if os.path.isfile(filename): self.add_mesh(pyvista.read(filename)) except IOError as exception: # pragma: no cover - warnings.warn(f"Exception when dropping files: {str(exception)}") + warnings.warn(f'Exception when dropping files: {str(exception)}') def close(self) -> None: """Quit application.""" if self._closed: return - if hasattr(self, "render_timer"): + if hasattr(self, 'render_timer'): self.render_timer.stop() BasePlotter.close(self) QVTKRenderWindowInteractor.close(self) @@ -426,7 +418,7 @@ class BackgroundPlotter(QtInteractor): screenshots or debug testing. allow_quit_keypress : - Allow user to exit by pressing ``"q"``. + Allow user to exit by pressing ``'q'``. toolbar : bool If True, display the default camera toolbar. Defaults to True. @@ -505,16 +497,16 @@ def __init__( # self._closed=True until the BasePlotter.__init__ # is called self._closed = True - LOG.debug("BackgroundPlotter init start") - _check_type(show, "show", [bool]) - _check_type(app, "app", [QApplication, type(None)]) - _check_type(window_size, "window_size", [tuple, type(None)]) - _check_type(off_screen, "off_screen", [bool, type(None)]) - _check_type(allow_quit_keypress, "allow_quit_keypress", [bool]) - _check_type(toolbar, "toolbar", [bool]) - _check_type(menu_bar, "menu_bar", [bool]) - _check_type(editor, "editor", [bool]) - _check_type(update_app_icon, "update_app_icon", [bool, type(None)]) + LOG.debug('BackgroundPlotter init start') + _check_type(show, 'show', [bool]) + _check_type(app, 'app', [QApplication, type(None)]) + _check_type(window_size, 'window_size', [tuple, type(None)]) + _check_type(off_screen, 'off_screen', [bool, type(None)]) + _check_type(allow_quit_keypress, 'allow_quit_keypress', [bool]) + _check_type(toolbar, 'toolbar', [bool]) + _check_type(menu_bar, 'menu_bar', [bool]) + _check_type(editor, 'editor', [bool]) + _check_type(update_app_icon, 'update_app_icon', [bool, type(None)]) # toolbar self._view_action: QAction = None @@ -538,16 +530,14 @@ def __init__( window_size = global_theme.window_size # Remove notebook argument in case user passed it - kwargs.pop("notebook", None) + kwargs.pop('notebook', None) self.ipython = _setup_ipython() self.app = _setup_application(app) self.off_screen = _setup_off_screen(off_screen) if app_window_class is None: app_window_class = MainWindow - self.app_window = app_window_class( - title=kwargs.get("title", global_theme.title) - ) + self.app_window = app_window_class(title=kwargs.get('title', global_theme.title)) self.frame = QFrame(parent=self.app_window) self.frame.setFrameStyle(QFrame.NoFrame) vlayout = QVBoxLayout() @@ -579,17 +569,15 @@ def __init__( self.add_callback(self.update_app_icon) elif update_app_icon is None: self.set_icon( - os.path.join( - os.path.dirname(__file__), "data", "pyvista_logo_square.png" - ) + os.path.join(os.path.dirname(__file__), 'data', 'pyvista_logo_square.png') ) else: assert update_app_icon is False # Keypress events if self.iren is not None: - self.add_key_event("S", self._qt_screenshot) # shift + s - LOG.debug("BackgroundPlotter init stop") + self.add_key_event('S', self._qt_screenshot) # shift + s + LOG.debug('BackgroundPlotter init stop') def reset_key_events(self) -> None: """Reset all of the key press events to their defaults. @@ -599,7 +587,7 @@ def reset_key_events(self) -> None: super().reset_key_events() if self.allow_quit_keypress: # pylint: disable=unnecessary-lambda - self.add_key_event("q", lambda: self.close()) + self.add_key_event('q', lambda: self.close()) def scale_axes_dialog(self, show: bool = True) -> ScaleAxesDialog: """Open scale axes dialog.""" @@ -608,9 +596,8 @@ def scale_axes_dialog(self, show: bool = True) -> ScaleAxesDialog: def close(self) -> None: """Close the plotter. - This function closes the window which in turn will - close the plotter through `signal_close`. - + This function closes the window which in turn will close the + plotter through `signal_close`. """ if not self._closed: # Can get: @@ -628,10 +615,9 @@ def _close(self) -> None: super().close() def update_app_icon(self) -> None: - """Update the app icon if the user is not trying to resize the window.""" - if os.name == "nt" or not hasattr( - self, "_last_window_size" - ): # pragma: no cover + """Update the app icon if the user is not trying to resize the + window.""" + if os.name == 'nt' or not hasattr(self, '_last_window_size'): # pragma: no cover # DO NOT EVEN ATTEMPT TO UPDATE ICON ON WINDOWS return cur_time = time.time() @@ -677,16 +663,14 @@ def set_icon(self, img: Union[np.ndarray, str]) -> None: and img.shape[-1] in (3, 4) ) and not isinstance(img, str): raise ValueError( - "img must be 3D uint8 ndarray with shape[1] == shape[2] and " - "shape[2] == 3 or 4, or str" + 'img must be 3D uint8 ndarray with shape[1] == shape[2] and ' + 'shape[2] == 3 or 4, or str' ) if isinstance(img, np.ndarray): - fmt_str = "Format_RGB" - fmt_str += ("A8" if img.shape[2] == 4 else "") + "888" + fmt_str = 'Format_RGB' + fmt_str += ('A8' if img.shape[2] == 4 else '') + '888' fmt = getattr(QtGui.QImage, fmt_str) - img = QtGui.QPixmap.fromImage( - QtGui.QImage(img.copy(), img.shape[1], img.shape[0], fmt) - ) + img = QtGui.QPixmap.fromImage(QtGui.QImage(img.copy(), img.shape[1], img.shape[0], fmt)) # Currently no way to check if str/path is actually correct (want to # allow resource paths and the like so os.path.isfile is no good) # and icon.isNull() returns False even if the path is bogus. @@ -695,7 +679,7 @@ def set_icon(self, img: Union[np.ndarray, str]) -> None: def _qt_screenshot(self, show: bool = True) -> FileDialog: return FileDialog( self.app_window, - filefilter=["Image File (*.png)", "JPEG (*.jpeg)"], + filefilter=['Image File (*.png)', 'JPEG (*.jpeg)'], show=show, directory=bool(os.getcwd()), callback=self.screenshot, @@ -705,14 +689,14 @@ def _qt_export_vtkjs(self, show: bool = True) -> FileDialog: """Spawn an save file dialog to export a vtkjs file.""" return FileDialog( self.app_window, - filefilter=["VTK JS File(*.vtkjs)"], + filefilter=['VTK JS File(*.vtkjs)'], show=show, directory=bool(os.getcwd()), callback=self.export_vtkjs, ) def _toggle_edl(self) -> None: - if hasattr(self.renderer, "edl_pass"): + if hasattr(self.renderer, 'edl_pass'): return self.renderer.disable_eye_dome_lighting() return self.renderer.enable_eye_dome_lighting() @@ -754,7 +738,6 @@ def add_callback( count : Number of times `func` will be called. If None, `func` will be called until the main window is closed. - """ self._callback_timer = QTimer(parent=self.app_window) self._callback_timer.timeout.connect(func) @@ -776,19 +759,19 @@ def save_camera_position(self) -> None: if self.camera_position is not None: camera_position: Any = self.camera_position[:] # py2.7 copy compatibility - if hasattr(self, "saved_cameras_tool_bar"): + if hasattr(self, 'saved_cameras_tool_bar'): def load_camera_position() -> None: # pylint: disable=attribute-defined-outside-init self.camera_position = camera_position - self.saved_cameras_tool_bar.addAction(f"Cam {ncam}", load_camera_position) + self.saved_cameras_tool_bar.addAction(f'Cam {ncam}', load_camera_position) if ncam < 10: self.add_key_event(str(ncam), load_camera_position) def clear_camera_positions(self) -> None: """Clear all camera positions.""" - if hasattr(self, "saved_cameras_tool_bar"): + if hasattr(self, 'saved_cameras_tool_bar'): for action in self.saved_cameras_tool_bar.actions(): if action.text() not in [SAVE_CAM_BUTTON_TEXT, CLEAR_CAMS_BUTTON_TEXT]: self.saved_cameras_tool_bar.removeAction(action) @@ -803,35 +786,29 @@ def _add_action(self, tool_bar: QToolBar, key: str, method: Any) -> QAction: def add_toolbars(self) -> None: """Add the toolbars.""" # Camera toolbar - self.default_camera_tool_bar = self.app_window.addToolBar("Camera Position") + self.default_camera_tool_bar = self.app_window.addToolBar('Camera Position') def _view_vector(*args: Any) -> None: return self.view_vector(*args) cvec_setters = { # Viewing vector then view up vector - "Top (-Z)": lambda: _view_vector((0, 0, 1), (0, 1, 0)), - "Bottom (+Z)": lambda: _view_vector((0, 0, -1), (0, 1, 0)), - "Front (-Y)": lambda: _view_vector((0, 1, 0), (0, 0, 1)), - "Back (+Y)": lambda: _view_vector((0, -1, 0), (0, 0, 1)), - "Left (-X)": lambda: _view_vector((1, 0, 0), (0, 0, 1)), - "Right (+X)": lambda: _view_vector((-1, 0, 0), (0, 0, 1)), - "Isometric": lambda: _view_vector((1, 1, 1), (0, 0, 1)), + 'Top (-Z)': lambda: _view_vector((0, 0, 1), (0, 1, 0)), + 'Bottom (+Z)': lambda: _view_vector((0, 0, -1), (0, 1, 0)), + 'Front (-Y)': lambda: _view_vector((0, 1, 0), (0, 0, 1)), + 'Back (+Y)': lambda: _view_vector((0, -1, 0), (0, 0, 1)), + 'Left (-X)': lambda: _view_vector((1, 0, 0), (0, 0, 1)), + 'Right (+X)': lambda: _view_vector((-1, 0, 0), (0, 0, 1)), + 'Isometric': lambda: _view_vector((1, 1, 1), (0, 0, 1)), } for key, method in cvec_setters.items(): - self._view_action = self._add_action( - self.default_camera_tool_bar, key, method - ) + self._view_action = self._add_action(self.default_camera_tool_bar, key, method) # pylint: disable=unnecessary-lambda - self._add_action( - self.default_camera_tool_bar, "Reset", lambda: self.reset_camera() - ) + self._add_action(self.default_camera_tool_bar, 'Reset', lambda: self.reset_camera()) # Saved camera locations toolbar self.saved_camera_positions = [] - self.saved_cameras_tool_bar = self.app_window.addToolBar( - "Saved Camera Positions" - ) + self.saved_cameras_tool_bar = self.app_window.addToolBar('Saved Camera Positions') self._add_action( self.saved_cameras_tool_bar, SAVE_CAM_BUTTON_TEXT, self.save_camera_position @@ -847,45 +824,43 @@ def add_menu_bar(self) -> None: self.main_menu = _create_menu_bar(parent=self.app_window) self.app_window.signal_close.connect(self.main_menu.clear) - file_menu = self.main_menu.addMenu("File") - file_menu.addAction("Take Screenshot", self._qt_screenshot) - file_menu.addAction("Export as VTKjs", self._qt_export_vtkjs) + file_menu = self.main_menu.addMenu('File') + file_menu.addAction('Take Screenshot', self._qt_screenshot) + file_menu.addAction('Export as VTKjs', self._qt_export_vtkjs) file_menu.addSeparator() # member variable for testing only - self._menu_close_action = file_menu.addAction("Exit", self.app_window.close) + self._menu_close_action = file_menu.addAction('Exit', self.app_window.close) - view_menu = self.main_menu.addMenu("View") - self._edl_action = view_menu.addAction( - "Toggle Eye Dome Lighting", self._toggle_edl - ) - view_menu.addAction("Scale Axes", self.scale_axes_dialog) - view_menu.addAction("Clear All", self.clear) + view_menu = self.main_menu.addMenu('View') + self._edl_action = view_menu.addAction('Toggle Eye Dome Lighting', self._toggle_edl) + view_menu.addAction('Scale Axes', self.scale_axes_dialog) + view_menu.addAction('Clear All', self.clear) - tool_menu = self.main_menu.addMenu("Tools") - tool_menu.addAction("Enable Cell Picking (through)", self.enable_cell_picking) + tool_menu = self.main_menu.addMenu('Tools') + tool_menu.addAction('Enable Cell Picking (through)', self.enable_cell_picking) tool_menu.addAction( - "Enable Cell Picking (visible)", + 'Enable Cell Picking (visible)', lambda: self.enable_cell_picking(through=False), ) - cam_menu = view_menu.addMenu("Camera") + cam_menu = view_menu.addMenu('Camera') self._parallel_projection_action = cam_menu.addAction( - "Toggle Parallel Projection", self._toggle_parallel_projection + 'Toggle Parallel Projection', self._toggle_parallel_projection ) view_menu.addSeparator() # Orientation marker - orien_menu = view_menu.addMenu("Orientation Marker") - orien_menu.addAction("Show All", self.show_axes_all) - orien_menu.addAction("Hide All", self.hide_axes_all) + orien_menu = view_menu.addMenu('Orientation Marker') + orien_menu.addAction('Show All', self.show_axes_all) + orien_menu.addAction('Hide All', self.hide_axes_all) # Bounds axes - axes_menu = view_menu.addMenu("Bounds Axes") - axes_menu.addAction("Add Bounds Axes (front)", self.show_bounds) - axes_menu.addAction("Add Bounds Grid (back)", self.show_grid) - axes_menu.addAction("Add Bounding Box", self.add_bounding_box) + axes_menu = view_menu.addMenu('Bounds Axes') + axes_menu.addAction('Add Bounds Axes (front)', self.show_bounds) + axes_menu.addAction('Add Bounds Grid (back)', self.show_grid) + axes_menu.addAction('Add Bounding Box', self.add_bounding_box) axes_menu.addSeparator() - axes_menu.addAction("Remove Bounding Box", self.remove_bounding_box) - axes_menu.addAction("Remove Bounds", self.remove_bounds_axes) + axes_menu.addAction('Remove Bounding Box', self.remove_bounding_box) + axes_menu.addAction('Remove Bounds', self.remove_bounds_axes) # A final separator to separate OS options view_menu.addSeparator() @@ -893,7 +868,7 @@ def add_menu_bar(self) -> None: def add_editor(self) -> None: """Add the editor.""" self.editor = Editor(parent=self.app_window, renderers=self.renderers) - self._editor_action = self.main_menu.addAction("Editor", self.editor.toggle) + self._editor_action = self.main_menu.addAction('Editor', self.editor.toggle) self.app_window.signal_close.connect(self.editor.close) @@ -944,13 +919,13 @@ def __init__( **kwargs: Any, ) -> None: """Initialize the multi plotter.""" - _check_type(app, "app", [QApplication, type(None)]) - _check_type(nrows, "nrows", [int]) - _check_type(ncols, "ncols", [int]) - _check_type(show, "show", [bool]) - _check_type(window_size, "window_size", [tuple, type(None)]) - _check_type(title, "title", [str, type(None)]) - _check_type(off_screen, "off_screen", [bool, type(None)]) + _check_type(app, 'app', [QApplication, type(None)]) + _check_type(nrows, 'nrows', [int]) + _check_type(ncols, 'ncols', [int]) + _check_type(show, 'show', [bool]) + _check_type(window_size, 'window_size', [tuple, type(None)]) + _check_type(title, 'title', [str, type(None)]) + _check_type(off_screen, 'off_screen', [bool, type(None)]) self.ipython = _setup_ipython() self.app = _setup_application(app) self.off_screen = _setup_off_screen(off_screen) diff --git a/pyvistaqt/rwi.py b/pyvistaqt/rwi.py index 3865bbfd..619af802 100644 --- a/pyvistaqt/rwi.py +++ b/pyvistaqt/rwi.py @@ -2,10 +2,8 @@ # under the OSI-approved BSD 3-clause License # TODO: Mayavi has a potentially different version that might be better or worse # coding=utf-8 -""" -A simple VTK widget for PyQt or PySide. -See http://www.trolltech.com for Qt documentation, -http://www.riverbankcomputing.co.uk for PyQt, and +"""A simple VTK widget for PyQt or PySide. See http://www.trolltech.com for Qt +documentation, http://www.riverbankcomputing.co.uk for PyQt, and http://pyside.github.io for PySide. This class is based on the vtkGenericRenderWindowInteractor and is @@ -55,21 +53,24 @@ Changes by Eric Larson and Guillaume Favelier, Apr. 2022 Support for PyQt6 """ +# noqa: F401 # Check whether a specific PyQt implementation was chosen try: import vtkmodules.qt + PyQtImpl = vtkmodules.qt.PyQtImpl except ImportError: pass # Check whether a specific QVTKRenderWindowInteractor base -# class was chosen, can be set to "QGLWidget" in +# class was chosen, can be set to 'QGLWidget' in # PyQt implementation version lower than Pyside6, -# or "QOpenGLWidget" in Pyside6 -QVTKRWIBase = "QWidget" +# or 'QOpenGLWidget' in Pyside6 +QVTKRWIBase = 'QWidget' try: import vtkmodules.qt + QVTKRWIBase = vtkmodules.qt.QVTKRWIBase except ImportError: pass @@ -80,128 +81,92 @@ if PyQtImpl is None: # Autodetect the PyQt implementation to use try: - import PyQt6 - PyQtImpl = "PyQt6" + import PyQt6 # noqa: F401 + + PyQtImpl = 'PyQt6' except ImportError: try: - import PySide6 - PyQtImpl = "PySide6" + import PySide6 # noqa: F401 + + PyQtImpl = 'PySide6' except ImportError: try: - import PyQt5 - PyQtImpl = "PyQt5" + import PyQt5 # noqa: F401 + + PyQtImpl = 'PyQt5' except ImportError: try: - import PySide2 - PyQtImpl = "PySide2" + import PySide2 # noqa: F401 + + PyQtImpl = 'PySide2' except ImportError: try: - import PyQt4 - PyQtImpl = "PyQt4" + import PyQt4 # noqa: F401 + + PyQtImpl = 'PyQt4' except ImportError: try: - import PySide - PyQtImpl = "PySide" + import PySide # noqa: F401 + + PyQtImpl = 'PySide' except ImportError: - raise ImportError("Cannot load either PyQt or PySide") + raise ImportError('Cannot load either PyQt or PySide') # Check the compatibility of PyQtImpl and QVTKRWIBase -if QVTKRWIBase != "QWidget": - if PyQtImpl in ["PyQt6", "PySide6"] and QVTKRWIBase == "QOpenGLWidget": +if QVTKRWIBase != 'QWidget': + if PyQtImpl in ['PyQt6', 'PySide6'] and QVTKRWIBase == 'QOpenGLWidget': pass # compatible - elif PyQtImpl in ["PyQt5", "PySide2","PyQt4", "PySide"] and QVTKRWIBase == "QGLWidget": + elif PyQtImpl in ['PyQt5', 'PySide2', 'PyQt4', 'PySide'] and QVTKRWIBase == 'QGLWidget': pass # compatible else: - raise ImportError("Cannot load " + QVTKRWIBase + " from " + PyQtImpl) + raise ImportError('Cannot load ' + QVTKRWIBase + ' from ' + PyQtImpl) -if PyQtImpl == "PyQt6": - if QVTKRWIBase == "QOpenGLWidget": +if PyQtImpl == 'PyQt6': + if QVTKRWIBase == 'QOpenGLWidget': from PyQt6.QtOpenGLWidgets import QOpenGLWidget - from PyQt6.QtWidgets import QWidget - from PyQt6.QtWidgets import QSizePolicy - from PyQt6.QtWidgets import QApplication - from PyQt6.QtWidgets import QMainWindow + from PyQt6.QtCore import QEvent, QObject, QSize, Qt, QTimer # noqa: F401 from PyQt6.QtGui import QCursor - from PyQt6.QtCore import Qt - from PyQt6.QtCore import QTimer - from PyQt6.QtCore import QObject - from PyQt6.QtCore import QSize - from PyQt6.QtCore import QEvent -elif PyQtImpl == "PySide6": - if QVTKRWIBase == "QOpenGLWidget": + from PyQt6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PySide6': + if QVTKRWIBase == 'QOpenGLWidget': from PySide6.QtOpenGLWidgets import QOpenGLWidget - from PySide6.QtWidgets import QWidget - from PySide6.QtWidgets import QSizePolicy - from PySide6.QtWidgets import QApplication - from PySide6.QtWidgets import QMainWindow + from PySide6.QtCore import QEvent, QObject, QSize, Qt, QTimer # noqa: F401 from PySide6.QtGui import QCursor - from PySide6.QtCore import Qt - from PySide6.QtCore import QTimer - from PySide6.QtCore import QObject - from PySide6.QtCore import QSize - from PySide6.QtCore import QEvent -elif PyQtImpl == "PyQt5": - if QVTKRWIBase == "QGLWidget": + from PySide6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PyQt5': + if QVTKRWIBase == 'QGLWidget': from PyQt5.QtOpenGL import QGLWidget - from PyQt5.QtWidgets import QWidget - from PyQt5.QtWidgets import QSizePolicy - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWidgets import QMainWindow + from PyQt5.QtCore import QEvent, QObject, QSize, Qt, QTimer # noqa: F401 from PyQt5.QtGui import QCursor - from PyQt5.QtCore import Qt - from PyQt5.QtCore import QTimer - from PyQt5.QtCore import QObject - from PyQt5.QtCore import QSize - from PyQt5.QtCore import QEvent -elif PyQtImpl == "PySide2": - if QVTKRWIBase == "QGLWidget": + from PyQt5.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PySide2': + if QVTKRWIBase == 'QGLWidget': from PySide2.QtOpenGL import QGLWidget - from PySide2.QtWidgets import QWidget - from PySide2.QtWidgets import QSizePolicy - from PySide2.QtWidgets import QApplication - from PySide2.QtWidgets import QMainWindow + from PySide2.QtCore import QEvent, QObject, QSize, Qt, QTimer # noqa: F401 from PySide2.QtGui import QCursor - from PySide2.QtCore import Qt - from PySide2.QtCore import QTimer - from PySide2.QtCore import QObject - from PySide2.QtCore import QSize - from PySide2.QtCore import QEvent -elif PyQtImpl == "PyQt4": - if QVTKRWIBase == "QGLWidget": + from PySide2.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PyQt4': + if QVTKRWIBase == 'QGLWidget': from PyQt4.QtOpenGL import QGLWidget - from PyQt4.QtGui import QWidget - from PyQt4.QtGui import QSizePolicy - from PyQt4.QtGui import QApplication - from PyQt4.QtGui import QMainWindow - from PyQt4.QtCore import Qt - from PyQt4.QtCore import QTimer - from PyQt4.QtCore import QObject - from PyQt4.QtCore import QSize - from PyQt4.QtCore import QEvent -elif PyQtImpl == "PySide": - if QVTKRWIBase == "QGLWidget": + from PyQt4.QtCore import QEvent, QObject, QSize, Qt, QTimer # noqa: F401 + from PyQt4.QtGui import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PySide': + if QVTKRWIBase == 'QGLWidget': from PySide.QtOpenGL import QGLWidget - from PySide.QtGui import QWidget - from PySide.QtGui import QSizePolicy - from PySide.QtGui import QApplication - from PySide.QtGui import QMainWindow - from PySide.QtCore import Qt - from PySide.QtCore import QTimer - from PySide.QtCore import QObject - from PySide.QtCore import QSize - from PySide.QtCore import QEvent + from PySide.QtCore import QEvent, QObject, QSize, Qt, QTimer # noqa: F401 + from PySide.QtGui import QApplication, QMainWindow, QSizePolicy, QWidget else: - raise ImportError("Unknown PyQt implementation " + repr(PyQtImpl)) + raise ImportError('Unknown PyQt implementation ' + repr(PyQtImpl)) # Define types for base class, based on string -if QVTKRWIBase == "QWidget": +if QVTKRWIBase == 'QWidget': QVTKRWIBaseClass = QWidget -elif QVTKRWIBase == "QGLWidget": +elif QVTKRWIBase == 'QGLWidget': QVTKRWIBaseClass = QGLWidget -elif QVTKRWIBase == "QOpenGLWidget": +elif QVTKRWIBase == 'QOpenGLWidget': QVTKRWIBaseClass = QOpenGLWidget else: - raise ImportError("Unknown base class for QVTKRenderWindowInteractor " + QVTKRWIBase) + raise ImportError('Unknown base class for QVTKRenderWindowInteractor ' + QVTKRWIBase) if PyQtImpl == 'PyQt6': CursorShape = Qt.CursorShape @@ -215,8 +180,9 @@ SizePolicy = QSizePolicy.Policy EventType = QEvent.Type else: - CursorShape = MouseButton = WindowType = WidgetAttribute = \ - KeyboardModifier = FocusPolicy = ConnectionType = Key = Qt + CursorShape = ( + MouseButton + ) = WindowType = WidgetAttribute = KeyboardModifier = FocusPolicy = ConnectionType = Key = Qt SizePolicy = QSizePolicy EventType = QEvent @@ -235,10 +201,10 @@ def _get_event_pos(ev): class QVTKRenderWindowInteractor(QVTKRWIBaseClass): - """ A QVTKRenderWindowInteractor for Python and Qt. Uses a + """A QVTKRenderWindowInteractor for Python and Qt. Uses a vtkGenericRenderWindowInteractor to handle the interactions. Use - GetRenderWindow() to get the vtkRenderWindow. Create with the - keyword stereo=1 in order to generate a stereo-capable window. + GetRenderWindow() to get the vtkRenderWindow. Create with the keyword + stereo=1 in order to generate a stereo-capable window. The user interface is summarized in vtkInteractorStyle.h: @@ -304,17 +270,17 @@ class QVTKRenderWindowInteractor(QVTKRWIBaseClass): # Map between VTK and Qt cursors. _CURSOR_MAP = { - 0: CursorShape.ArrowCursor, # VTK_CURSOR_DEFAULT - 1: CursorShape.ArrowCursor, # VTK_CURSOR_ARROW - 2: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZENE - 3: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZENWSE - 4: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZESW - 5: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZESE - 6: CursorShape.SizeVerCursor, # VTK_CURSOR_SIZENS - 7: CursorShape.SizeHorCursor, # VTK_CURSOR_SIZEWE - 8: CursorShape.SizeAllCursor, # VTK_CURSOR_SIZEALL - 9: CursorShape.PointingHandCursor, # VTK_CURSOR_HAND - 10: CursorShape.CrossCursor, # VTK_CURSOR_CROSSHAIR + 0: CursorShape.ArrowCursor, # VTK_CURSOR_DEFAULT + 1: CursorShape.ArrowCursor, # VTK_CURSOR_ARROW + 2: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZENE + 3: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZENWSE + 4: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZESW + 5: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZESE + 6: CursorShape.SizeVerCursor, # VTK_CURSOR_SIZENS + 7: CursorShape.SizeHorCursor, # VTK_CURSOR_SIZEWE + 8: CursorShape.SizeAllCursor, # VTK_CURSOR_SIZEALL + 9: CursorShape.PointingHandCursor, # VTK_CURSOR_HAND + 10: CursorShape.CrossCursor, # VTK_CURSOR_CROSSHAIR } def __init__(self, parent=None, **kw): @@ -342,18 +308,18 @@ def __init__(self, parent=None, **kw): rw = None # create base qt-level widget - if QVTKRWIBase == "QWidget": - if "wflags" in kw: + if QVTKRWIBase == 'QWidget': + if 'wflags' in kw: wflags = kw['wflags'] else: wflags = Qt.WindowType.Widget QWidget.__init__(self, parent, wflags | WindowType.MSWindowsOwnDC) - elif QVTKRWIBase == "QGLWidget": + elif QVTKRWIBase == 'QGLWidget': QGLWidget.__init__(self, parent) - elif QVTKRWIBase == "QOpenGLWidget": + elif QVTKRWIBase == 'QOpenGLWidget': QOpenGLWidget.__init__(self, parent) - if rw: # user-supplied render window + if rw: # user-supplied render window self._RenderWindow = rw else: self._RenderWindow = vtkRenderWindow() @@ -362,30 +328,30 @@ def __init__(self, parent=None, **kw): # Python2 if type(WId).__name__ == 'PyCObject': - from ctypes import pythonapi, c_void_p, py_object + from ctypes import c_void_p, py_object, pythonapi - pythonapi.PyCObject_AsVoidPtr.restype = c_void_p + pythonapi.PyCObject_AsVoidPtr.restype = c_void_p pythonapi.PyCObject_AsVoidPtr.argtypes = [py_object] WId = pythonapi.PyCObject_AsVoidPtr(WId) # Python3 elif type(WId).__name__ == 'PyCapsule': - from ctypes import pythonapi, c_void_p, py_object, c_char_p + from ctypes import c_char_p, c_void_p, py_object, pythonapi pythonapi.PyCapsule_GetName.restype = c_char_p pythonapi.PyCapsule_GetName.argtypes = [py_object] name = pythonapi.PyCapsule_GetName(WId) - pythonapi.PyCapsule_GetPointer.restype = c_void_p + pythonapi.PyCapsule_GetPointer.restype = c_void_p pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] WId = pythonapi.PyCapsule_GetPointer(WId, name) self._RenderWindow.SetWindowInfo(str(int(WId))) - if stereo: # stereo mode + if stereo: # stereo mode self._RenderWindow.StereoCapableWindowOn() self._RenderWindow.SetStereoTypeToCrystalEyes() @@ -398,7 +364,7 @@ def __init__(self, parent=None, **kw): # do all the necessary qt setup self.setAttribute(WidgetAttribute.WA_OpaquePaintEvent) self.setAttribute(WidgetAttribute.WA_PaintOnScreen) - self.setMouseTracking(True) # get all mouse events + self.setMouseTracking(True) # get all mouse events self.setFocusPolicy(FocusPolicy.WheelFocus) self.setSizePolicy(QSizePolicy(SizePolicy.Expanding, SizePolicy.Expanding)) @@ -407,8 +373,7 @@ def __init__(self, parent=None, **kw): self._Iren.AddObserver('CreateTimerEvent', self.CreateTimer) self._Iren.AddObserver('DestroyTimerEvent', self.DestroyTimer) - self._Iren.GetRenderWindow().AddObserver('CursorChangedEvent', - self.CursorChangedEvent) + self._Iren.GetRenderWindow().AddObserver('CursorChangedEvent', self.CursorChangedEvent) # If we've a parent, it does not close the child when closed. # Connect the parent's destroyed signal to this widget's close @@ -417,19 +382,16 @@ def __init__(self, parent=None, **kw): self.parent().destroyed.connect(self.close, ConnectionType.DirectConnection) def __getattr__(self, attr): - """Makes the object behave like a vtkGenericRenderWindowInteractor""" + """Makes the object behave like a vtkGenericRenderWindowInteractor.""" if attr == '__vtk__': return lambda t=self._Iren: t elif hasattr(self._Iren, attr): return getattr(self._Iren, attr) else: - raise AttributeError(self.__class__.__name__ + - " has no attribute named " + attr) + raise AttributeError(self.__class__.__name__ + ' has no attribute named ' + attr) def Finalize(self): - ''' - Call internal cleanup method on VTK objects - ''' + """Call internal cleanup method on VTK objects.""" self._RenderWindow.Finalize() def CreateTimer(self, obj, evt): @@ -473,16 +435,16 @@ def paintEvent(self, ev): def resizeEvent(self, ev): scale = self._getPixelRatio() - w = int(round(scale*self.width())) - h = int(round(scale*self.height())) - self._RenderWindow.SetDPI(int(round(72*scale))) + w = int(round(scale * self.width())) + h = int(round(scale * self.height())) + self._RenderWindow.SetDPI(int(round(72 * scale))) vtkRenderWindow.SetSize(self._RenderWindow, w, h) self._Iren.SetSize(w, h) self._Iren.ConfigureEvent() self.update() def _GetKeyCharAndKeySym(self, ev): - """ Convert a Qt key into a char and a vtk keysym. + """Convert a Qt key into a char and a vtk keysym. This is essentially copied from the c++ implementation in GUISupport/Qt/QVTKInteractorAdapter.cxx. @@ -502,9 +464,9 @@ def _GetKeyCharAndKeySym(self, ev): except KeyError: keySym = None - # use "None" as a fallback + # use 'None' as a fallback if keySym is None: - keySym = "None" + keySym = 'None' return keyChar, keySym @@ -526,7 +488,7 @@ def _GetCtrlShift(self, ev): @staticmethod def _getPixelRatio(): - if PyQtImpl in ["PyQt5", "PySide2", "PySide6"]: + if PyQtImpl in ['PyQt5', 'PySide2', 'PySide6']: # Source: https://stackoverflow.com/a/40053864/3388962 pos = QCursor.pos() for screen in QApplication.screens(): @@ -538,25 +500,28 @@ def _getPixelRatio(): else: # Qt4 seems not to provide any cross-platform means to get the # pixel ratio. - return 1. + return 1.0 - def _setEventInformation(self, x, y, ctrl, shift, - key, repeat=0, keysum=None): + def _setEventInformation(self, x, y, ctrl, shift, key, repeat=0, keysum=None): scale = self._getPixelRatio() - self._Iren.SetEventInformation(int(round(x*scale)), - int(round((self.height()-y-1)*scale)), - ctrl, shift, key, repeat, keysum) + self._Iren.SetEventInformation( + int(round(x * scale)), + int(round((self.height() - y - 1) * scale)), + ctrl, + shift, + key, + repeat, + keysum, + ) def enterEvent(self, ev): ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, chr(0), 0, None) self._Iren.EnterEvent() def leaveEvent(self, ev): ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, chr(0), 0, None) self._Iren.LeaveEvent() def mousePressEvent(self, ev): @@ -565,8 +530,7 @@ def mousePressEvent(self, ev): repeat = 0 if ev.type() == EventType.MouseButtonDblClick: repeat = 1 - self._setEventInformation(pos_x, pos_y, - ctrl, shift, chr(0), repeat, None) + self._setEventInformation(pos_x, pos_y, ctrl, shift, chr(0), repeat, None) self._ActiveButton = ev.button() @@ -580,8 +544,7 @@ def mousePressEvent(self, ev): def mouseReleaseEvent(self, ev): pos_x, pos_y = _get_event_pos(ev) ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(pos_x, pos_y, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(pos_x, pos_y, ctrl, shift, chr(0), 0, None) if self._ActiveButton == MouseButton.LeftButton: self._Iren.LeftButtonReleaseEvent() @@ -598,23 +561,20 @@ def mouseMoveEvent(self, ev): self.__saveY = pos_y ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(pos_x, pos_y, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(pos_x, pos_y, ctrl, shift, chr(0), 0, None) self._Iren.MouseMoveEvent() def keyPressEvent(self, ev): key, keySym = self._GetKeyCharAndKeySym(ev) ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, key, 0, keySym) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, key, 0, keySym) self._Iren.KeyPressEvent() self._Iren.CharEvent() def keyReleaseEvent(self, ev): key, keySym = self._GetKeyCharAndKeySym(ev) ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, key, 0, keySym) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, key, 0, keySym) self._Iren.KeyReleaseEvent() def wheelEvent(self, ev): @@ -641,15 +601,16 @@ def QVTKRenderWidgetConeExample(block=False): """A simple example that uses the QVTKRenderWindowInteractor class.""" from vtkmodules.vtkFiltersSources import vtkConeSource + import vtkmodules.vtkInteractionStyle from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper, vtkRenderer + # load implementations for rendering and interaction factory classes - import vtkmodules.vtkRenderingOpenGL2 - import vtkmodules.vtkInteractionStyle + import vtkmodules.vtkRenderingOpenGL2 # noqa: F401 # every QT app needs an app app = QApplication.instance() if not app: # pragma: no cover - app = QApplication(["PyVista"]) + app = QApplication(['PyVista']) window = QMainWindow() @@ -657,7 +618,7 @@ def QVTKRenderWidgetConeExample(block=False): widget = QVTKRenderWindowInteractor(window) window.setCentralWidget(widget) # if you don't want the 'q' key to exit comment this. - widget.AddObserver("ExitEvent", lambda o, e, a=app: a.quit()) + widget.AddObserver('ExitEvent', lambda o, e, a=app: a.quit()) ren = vtkRenderer() widget.GetRenderWindow().AddRenderer(ren) @@ -691,26 +652,135 @@ def QVTKRenderWidgetConeExample(block=False): _keysyms_for_ascii = ( - None, None, None, None, None, None, None, None, - None, "Tab", None, None, None, None, None, None, - None, None, None, None, None, None, None, None, - None, None, None, None, None, None, None, None, - "space", "exclam", "quotedbl", "numbersign", - "dollar", "percent", "ampersand", "quoteright", - "parenleft", "parenright", "asterisk", "plus", - "comma", "minus", "period", "slash", - "0", "1", "2", "3", "4", "5", "6", "7", - "8", "9", "colon", "semicolon", "less", "equal", "greater", "question", - "at", "A", "B", "C", "D", "E", "F", "G", - "H", "I", "J", "K", "L", "M", "N", "O", - "P", "Q", "R", "S", "T", "U", "V", "W", - "X", "Y", "Z", "bracketleft", - "backslash", "bracketright", "asciicircum", "underscore", - "quoteleft", "a", "b", "c", "d", "e", "f", "g", - "h", "i", "j", "k", "l", "m", "n", "o", - "p", "q", "r", "s", "t", "u", "v", "w", - "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Delete", - ) + None, + None, + None, + None, + None, + None, + None, + None, + None, + 'Tab', + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + 'space', + 'exclam', + 'quotedbl', + 'numbersign', + 'dollar', + 'percent', + 'ampersand', + 'quoteright', + 'parenleft', + 'parenright', + 'asterisk', + 'plus', + 'comma', + 'minus', + 'period', + 'slash', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'colon', + 'semicolon', + 'less', + 'equal', + 'greater', + 'question', + 'at', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'bracketleft', + 'backslash', + 'bracketright', + 'asciicircum', + 'underscore', + 'quoteleft', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + 'braceleft', + 'bar', + 'braceright', + 'asciitilde', + 'Delete', +) _keysyms = { Key.Key_Backspace: 'BackSpace', @@ -805,9 +875,9 @@ def QVTKRenderWidgetConeExample(block=False): Key.Key_F24: 'F24', Key.Key_NumLock: 'Num_Lock', Key.Key_ScrollLock: 'Scroll_Lock', - } +} -if __name__ == "__main__": +if __name__ == '__main__': print(PyQtImpl) QVTKRenderWidgetConeExample() diff --git a/pyvistaqt/utils.py b/pyvistaqt/utils.py index 497b3227..05b3bf24 100644 --- a/pyvistaqt/utils.py +++ b/pyvistaqt/utils.py @@ -2,26 +2,24 @@ from typing import Any, List, Optional, Type import pyvista -import scooby # type: ignore from qtpy.QtWidgets import QApplication, QMenuBar +import scooby # type: ignore def _check_type(var: Any, var_name: str, var_types: List[Type[Any]]) -> None: types = tuple(var_types) if not isinstance(var, types): raise TypeError( - f"Expected type for ``{var_name}`` is {str(types)}" - f" but {type(var)} was given." + f'Expected type for ``{var_name}`` is {str(types)}' f' but {type(var)} was given.' ) def _create_menu_bar(parent: Any) -> QMenuBar: """Create a menu bar. - The menu bar is expected to behave consistently - for every operating system since `setNativeMenuBar(False)` - is called by default and therefore lifetime and ownership can - be tested. + The menu bar is expected to behave consistently for every operating + system since `setNativeMenuBar(False)` is called by default and + therefore lifetime and ownership can be tested. """ menu_bar = QMenuBar(parent=parent) menu_bar.setNativeMenuBar(False) @@ -37,7 +35,7 @@ def _setup_ipython(ipython: Any = None) -> Any: from IPython import get_ipython ipython = get_ipython() - ipython.run_line_magic("gui", "qt") + ipython.run_line_magic('gui', 'qt') # pylint: disable=redefined-outer-name # pylint: disable=import-outside-toplevel @@ -52,7 +50,7 @@ def _setup_application(app: Optional[QApplication] = None) -> QApplication: if app is None: app = QApplication.instance() if not app: # pragma: no cover - app = QApplication(["PyVista"]) + app = QApplication(['PyVista']) return app diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..31a084b9 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name = pyvistaqt +version = attr: pyvistaqt._version.__version__ +description = pyvista qt plotter +long_description = file: README.rst, LICENSE +author = PyVista Developers +author_email = info@pyvista.org +license = MIT +classifiers = + Development Status :: 4 - Beta, + Intended Audience :: Science/Research, + Topic :: Scientific/Engineering :: Information Analysis, + License :: OSI Approved :: MIT License, + Operating System :: Microsoft :: Windows, + Operating System :: POSIX, + Operating System :: MacOS, + Programming Language :: Python :: 3.6, + Programming Language :: Python :: 3.7, + Programming Language :: Python :: 3.8 +url = https://github.com/pyvista/pyvistaqt +keywords = + vtk + numpy + plotting + mesh + qt + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = python>="3.6" +install_requires = + pyvista>="0.32.0" + QtPy>="1.9.0" + +[options.package_data] +pyvistaqt = + data/*.png \ No newline at end of file diff --git a/setup.py b/setup.py index 23a0a23d..ecd55757 100644 --- a/setup.py +++ b/setup.py @@ -1,52 +1,53 @@ -""" -Installation file for python pyvistaqt module -""" -import os +"""Installation file for python pyvistaqt module.""" +from __future__ import annotations + from io import open as io_open +import os +from typing import Optional from setuptools import setup package_name = 'pyvistaqt' -__version__ = None +__version__: Optional[str] = None filepath = os.path.dirname(__file__) version_file = os.path.join(filepath, package_name, '_version.py') -with io_open(version_file, mode='r') as fd: +with io_open(version_file, mode="r") as fd: exec(fd.read()) readme_file = os.path.join(filepath, 'README.rst') setup( name=package_name, - packages=[package_name, package_name], + packages=[package_name], version=__version__, - description='pyvista qt plotter', + description="pyvista qt plotter", long_description=io_open(readme_file, encoding="utf-8").read(), - author='PyVista Developers', - author_email='info@pyvista.org', - license='MIT', + author="PyVista Developers", + author_email="info@pyvista.org", + license="MIT", classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering :: Information Analysis', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Information Analysis", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], - - url='https://github.com/pyvista/pyvistaqt', - keywords='vtk numpy plotting mesh qt', - python_requires='>=3.6.*', + url="https://github.com/pyvista/pyvistaqt", + keywords="vtk numpy plotting mesh qt", + python_requires=">=3.6.*", install_requires=[ - 'pyvista>=0.32.0', - 'QtPy>=1.9.0', + "pyvista>=0.32.0", + "QtPy>=1.9.0", ], - package_data={'pyvistaqt': [ - os.path.join('data', '*.png'), - ]} - + package_data={ + "pyvistaqt": [ + os.path.join("data", "*.png"), + ] + }, ) diff --git a/tests/conftest.py b/tests/conftest.py index 56dae794..c2bb691a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import pytest from pyvista.plotting import system_supports_plotting + import pyvistaqt NO_PLOTTING = not system_supports_plotting() @@ -31,12 +32,12 @@ def no_qt(monkeypatch): need_reload = False if _check_qt_installed(): need_reload = True - monkeypatch.setenv('QT_API', 'bad_name') - sys.modules.pop('qtpy') + monkeypatch.setenv("QT_API", "bad_name") + sys.modules.pop("qtpy") importlib.reload(pyvistaqt) - assert 'qtpy' not in sys.modules + assert "qtpy" not in sys.modules yield monkeypatch.undo() if need_reload: importlib.reload(pyvistaqt) - assert 'qtpy' in sys.modules + assert "qtpy" in sys.modules diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d099b8a2..8e23ebe9 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,26 +1,34 @@ import os -from packaging.version import Version import platform import numpy as np +from packaging.version import Version import pytest import pyvista -import vtk -from qtpy.QtWidgets import QAction, QFrame, QMenuBar, QToolBar, QVBoxLayout +from pyvista.plotting import Renderer from qtpy import QtCore -from qtpy.QtCore import Qt, QPoint, QPointF, QMimeData, QUrl +from qtpy.QtCore import QMimeData, QPoint, QPointF, Qt, QUrl from qtpy.QtGui import QDragEnterEvent, QDropEvent -from qtpy.QtWidgets import (QTreeWidget, QStackedWidget, QCheckBox, - QGestureEvent, QPinchGesture) -from pyvistaqt.plotting import global_theme -from pyvista.plotting import Renderer +from qtpy.QtWidgets import ( + QAction, + QCheckBox, + QFrame, + QGestureEvent, + QMenuBar, + QPinchGesture, + QStackedWidget, + QToolBar, + QTreeWidget, + QVBoxLayout, +) +import vtk import pyvistaqt -from pyvistaqt import MultiPlotter, BackgroundPlotter, MainWindow, QtInteractor -from pyvistaqt.plotting import Counter, QTimer, QVTKRenderWindowInteractor -from pyvistaqt.editor import Editor +from pyvistaqt import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor from pyvistaqt.dialog import FileDialog -from pyvistaqt.utils import _setup_application, _create_menu_bar, _check_type +from pyvistaqt.editor import Editor +from pyvistaqt.plotting import Counter, QTimer, QVTKRenderWindowInteractor, global_theme +from pyvistaqt.utils import _check_type, _create_menu_bar, _setup_application class TstWindow(MainWindow): @@ -41,15 +49,15 @@ def __init__(self, parent=None, show=True, off_screen=True): mainMenu = _create_menu_bar(parent=self) - fileMenu = mainMenu.addMenu('File') - self.exit_action = QAction('Exit', self) - self.exit_action.setShortcut('Ctrl+Q') + fileMenu = mainMenu.addMenu("File") + self.exit_action = QAction("Exit", self) + self.exit_action.setShortcut("Ctrl+Q") self.exit_action.triggered.connect(self.close) fileMenu.addAction(self.exit_action) - meshMenu = mainMenu.addMenu('Mesh') - self.add_sphere_action = QAction('Add Sphere', self) - self.exit_action.setShortcut('Ctrl+A') + meshMenu = mainMenu.addMenu("Mesh") + self.add_sphere_action = QAction("Add Sphere", self) + self.exit_action.setShortcut("Ctrl+A") self.add_sphere_action.triggered.connect(self.add_sphere) meshMenu.addAction(self.add_sphere_action) @@ -59,10 +67,7 @@ def __init__(self, parent=None, show=True, off_screen=True): self.show() def add_sphere(self): - sphere = pyvista.Sphere( - phi_resolution=6, - theta_resolution=6 - ) + sphere = pyvista.Sphere(phi_resolution=6, theta_resolution=6) self.vtk_widget.add_mesh(sphere) self.vtk_widget.reset_camera() @@ -88,7 +93,7 @@ def test_file_dialog(tmpdir, qtbot): dialog.emit_accepted() # test no result p = tmpdir.mkdir("tmp").join("foo.png") - p.write('foo') + p.write("foo") assert os.path.isfile(p) filename = str(p) @@ -124,11 +129,17 @@ def test_mouse_interactions(qtbot): plotter.close() -@pytest.mark.skipif(platform.system()=="Windows" and platform.python_version()[:-1]=="3.8.", reason="#51") +@pytest.mark.skipif( + platform.system() == "Windows" and platform.python_version()[:-1] == "3.8.", + reason="#51", +) def test_ipython(qapp): import IPython - cmd = "from pyvistaqt import BackgroundPlotter as Plotter;" \ - "p = Plotter(show=False, off_screen=False); p.close(); exit()" + + cmd = ( + "from pyvistaqt import BackgroundPlotter as Plotter;" + "p = Plotter(show=False, off_screen=False); p.close(); exit()" + ) IPython.start_ipython(argv=["-c", cmd]) @@ -182,9 +193,9 @@ def test_smoothing(qtbot): def test_counter(qtbot): - with pytest.raises(TypeError, match='type of'): + with pytest.raises(TypeError, match="type of"): Counter(count=0.5) - with pytest.raises(ValueError, match='strictly positive'): + with pytest.raises(ValueError, match="strictly positive"): Counter(count=-1) counter = Counter(count=1) @@ -260,6 +271,7 @@ def test_editor(qtbot, plotting): def test_qt_interactor(qtbot, plotting): from pyvista.plotting.plotting import _ALL_PLOTTERS, close_all + close_all() # this is necessary to test _ALL_PLOTTERS assert len(_ALL_PLOTTERS) == 0 @@ -314,7 +326,7 @@ def test_qt_interactor(qtbot, plotting): assert not render_timer.isActive() # check that BasePlotter.close() is called - if Version(pyvista.__version__) < Version('0.27.0'): + if Version(pyvista.__version__) < Version("0.27.0"): assert not hasattr(vtk_widget, "iren") assert vtk_widget._closed @@ -322,16 +334,15 @@ def test_qt_interactor(qtbot, plotting): assert len(_ALL_PLOTTERS) == 1 -@pytest.mark.parametrize('show_plotter', [ - True, - False, - ]) +@pytest.mark.parametrize( + "show_plotter", + [ + True, + False, + ], +) def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): - plotter = BackgroundPlotter( - show=show_plotter, - off_screen=False, - title='Testing Window' - ) + plotter = BackgroundPlotter(show=show_plotter, off_screen=False, title="Testing Window") assert_hasattr(plotter, "app_window", MainWindow) window = plotter.app_window # MainWindow qtbot.addWidget(window) # register the window @@ -375,7 +386,7 @@ def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): def test_background_plotting_camera(qtbot, plotting): - plotter = BackgroundPlotter(off_screen=False, title='Testing Window') + plotter = BackgroundPlotter(off_screen=False, title="Testing Window") plotter.add_mesh(pyvista.Sphere()) cpos = [(0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)] @@ -394,16 +405,15 @@ def test_background_plotting_camera(qtbot, plotting): plotter.close() -@pytest.mark.parametrize('other_views', [None, 0, [0]]) +@pytest.mark.parametrize("other_views", [None, 0, [0]]) def test_link_views_across_plotters(other_views): - def _to_array(camera_position): return np.asarray([list(row) for row in camera_position]) - plotter_one = BackgroundPlotter(off_screen=True, title='Testing Window') + plotter_one = BackgroundPlotter(off_screen=True, title="Testing Window") plotter_one.add_mesh(pyvista.Sphere()) - plotter_two = BackgroundPlotter(off_screen=True, title='Testing Window') + plotter_two = BackgroundPlotter(off_screen=True, title="Testing Window") plotter_two.add_mesh(pyvista.Sphere()) plotter_one.link_views_across_plotters(plotter_two, other_views=other_views) @@ -429,24 +439,24 @@ def _to_array(camera_position): _to_array(plotter_two.camera_position), ) - match = 'Expected `other_views` type is int, or list or tuple of ints, but float64 is given' + match = "Expected `other_views` type is int, or list or tuple of ints, but float64 is given" with pytest.raises(TypeError, match=match): plotter_one.link_views_across_plotters(plotter_two, other_views=[0.0]) -@pytest.mark.parametrize('show_plotter', [ - True, - False, - ]) + +@pytest.mark.parametrize( + "show_plotter", + [ + True, + False, + ], +) def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): # setup filesystem output_dir = str(tmpdir.mkdir("tmpdir")) assert os.path.isdir(output_dir) - plotter = BackgroundPlotter( - show=show_plotter, - off_screen=False, - title='Testing Window' - ) + plotter = BackgroundPlotter(show=show_plotter, off_screen=False, title="Testing Window") assert_hasattr(plotter, "app_window", MainWindow) window = plotter.app_window # MainWindow qtbot.addWidget(window) # register the window @@ -486,20 +496,19 @@ def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): assert os.path.isfile(filename) -@pytest.mark.parametrize('show_plotter', [ - True, - False, - ]) +@pytest.mark.parametrize( + "show_plotter", + [ + True, + False, + ], +) def test_background_plotter_export_vtkjs(qtbot, tmpdir, show_plotter, plotting): # setup filesystem output_dir = str(tmpdir.mkdir("tmpdir")) assert os.path.isdir(output_dir) - plotter = BackgroundPlotter( - show=show_plotter, - off_screen=False, - title='Testing Window' - ) + plotter = BackgroundPlotter(show=show_plotter, off_screen=False, title="Testing Window") assert_hasattr(plotter, "app_window", MainWindow) window = plotter.app_window # MainWindow qtbot.addWidget(window) # register the window @@ -536,11 +545,11 @@ def test_background_plotter_export_vtkjs(qtbot, tmpdir, show_plotter, plotting): plotter.close() assert not window.isVisible() - assert os.path.isfile(filename + '.vtkjs') + assert os.path.isfile(filename + ".vtkjs") def test_background_plotting_orbit(qtbot, plotting): - plotter = BackgroundPlotter(off_screen=False, title='Testing Window') + plotter = BackgroundPlotter(off_screen=False, title="Testing Window") plotter.add_mesh(pyvista.Sphere()) # perform the orbit: plotter.orbit_on_path(threaded=True, step=0.0) @@ -548,7 +557,7 @@ def test_background_plotting_orbit(qtbot, plotting): def test_background_plotting_toolbar(qtbot, plotting): - with pytest.raises(TypeError, match='toolbar'): + with pytest.raises(TypeError, match="toolbar"): p = BackgroundPlotter(off_screen=False, toolbar="foo") p.close() @@ -582,7 +591,7 @@ def test_background_plotting_toolbar(qtbot, plotting): def test_background_plotting_menu_bar(qtbot, plotting): - with pytest.raises(TypeError, match='menu_bar'): + with pytest.raises(TypeError, match="menu_bar"): p = BackgroundPlotter(off_screen=False, menu_bar="foo") p.close() @@ -607,9 +616,9 @@ def test_background_plotting_menu_bar(qtbot, plotting): window.show() # EDL action - assert not hasattr(plotter.renderer, 'edl_pass') + assert not hasattr(plotter.renderer, "edl_pass") plotter._edl_action.trigger() - assert hasattr(plotter.renderer, 'edl_pass') + assert hasattr(plotter.renderer, "edl_pass") # and now test reset plotter._edl_action.trigger() @@ -691,11 +700,11 @@ def update_app_icon(slf): update_count[0] = update_count[0] + 1 return orig_update_app_icon(slf) - monkeypatch.setattr(BackgroundPlotter, 'update_app_icon', update_app_icon) + monkeypatch.setattr(BackgroundPlotter, "update_app_icon", update_app_icon) plotter = BackgroundPlotter( show=False, off_screen=False, - title='Testing Window', + title="Testing Window", update_app_icon=True, # also does add_callback ) assert_hasattr(plotter, "app_window", MainWindow) @@ -722,12 +731,12 @@ def update_app_icon(slf): plotter.update_app_icon() # should be a no-op assert update_count[0] in [2, 3] with pytest.raises(ValueError, match="ndarray with shape"): - plotter.set_icon(0.) + plotter.set_icon(0.0) # Maybe someday manually setting "set_icon" should disable update_app_icon? # Strings also supported directly by QIcon - plotter.set_icon(os.path.join( - os.path.dirname(pyvistaqt.__file__), "data", - "pyvista_logo_square.png")) + plotter.set_icon( + os.path.join(os.path.dirname(pyvistaqt.__file__), "data", "pyvista_logo_square.png") + ) callback_timer.stop() assert not callback_timer.isActive() @@ -757,19 +766,26 @@ def update_app_icon(slf): assert not callback_timer.isActive() # window stops the callback -@pytest.mark.parametrize('close_event', [ - "plotter_close", - "window_close", - "q_key_press", - "menu_exit", - "del_finalizer", - ]) -@pytest.mark.parametrize('empty_scene', [ - True, - False, - ]) +@pytest.mark.parametrize( + "close_event", + [ + "plotter_close", + "window_close", + "q_key_press", + "menu_exit", + "del_finalizer", + ], +) +@pytest.mark.parametrize( + "empty_scene", + [ + True, + False, + ], +) def test_background_plotting_close(qtbot, close_event, empty_scene, plotting): from pyvista.plotting.plotting import _ALL_PLOTTERS, close_all + close_all() # this is necessary to test _ALL_PLOTTERS assert len(_ALL_PLOTTERS) == 0 @@ -830,7 +846,7 @@ def test_background_plotting_close(qtbot, close_event, empty_scene, plotting): assert not render_timer.isActive() # check that BasePlotter.close() is called - if Version(pyvista.__version__) < Version('0.27.0'): + if Version(pyvista.__version__) < Version("0.27.0"): assert not hasattr(window.vtk_widget, "iren") assert plotter._closed @@ -844,7 +860,7 @@ def test_multiplotter(qtbot, plotting): ncols=2, window_size=(300, 300), show=False, - title='Test', + title="Test", off_screen=False, ) qtbot.addWidget(mp._window) @@ -881,28 +897,25 @@ def _create_testing_scene(empty_scene, show=False, off_screen=False): shape=(2, 2), border=True, border_width=10, - border_color='grey', + border_color="grey", show=show, off_screen=off_screen, update_app_icon=False, ) - plotter.set_background('black', top='blue') + plotter.set_background("black", top="blue") plotter.subplot(0, 0) cone = pyvista.Cone(resolution=4) actor = plotter.add_mesh(cone) plotter.remove_actor(actor) - plotter.add_text('Actor is removed') + plotter.add_text("Actor is removed") plotter.subplot(0, 1) - plotter.add_mesh(pyvista.Box(), color='green', opacity=0.8) + plotter.add_mesh(pyvista.Box(), color="green", opacity=0.8) plotter.subplot(1, 0) cylinder = pyvista.Cylinder(resolution=6) plotter.add_mesh(cylinder, smooth_shading=True) plotter.show_bounds() plotter.subplot(1, 1) - sphere = pyvista.Sphere( - phi_resolution=6, - theta_resolution=6 - ) + sphere = pyvista.Sphere(phi_resolution=6, theta_resolution=6) plotter.add_mesh(sphere) plotter.enable_cell_picking() return plotter diff --git a/tests/test_qt.py b/tests/test_qt.py index abb74984..904854d9 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -3,6 +3,7 @@ def test_no_qt_binding(no_qt): from pyvistaqt import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor + with pytest.raises(RuntimeError, match="No Qt binding"): BackgroundPlotter() with pytest.raises(RuntimeError, match="No Qt binding"):