diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c21e9bb..251538ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,4 @@ repos: - - repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black - args: ["--config=pyproject.toml"] - - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - args: ["--check", "--settings=.isort.cfg"] - - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.13.0 hooks: @@ -26,30 +14,12 @@ repos: pass_filenames: false args: ["--config-file", "mypy.ini", "@mypy_checklist.txt"] - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 - hooks: - - id: flake8 - additional_dependencies: - ["flake8-black==0.3.6", "flake8-isort==6.1.1", "flake8-quotes==3.3.2"] - args: ["--config=.flake8"] - - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell args: ["docs examples examples_flask pyvista tests", "*.py *.rst *.md"] - - repo: https://github.com/pycqa/pydocstyle - rev: 6.3.0 - hooks: - - id: pydocstyle - additional_dependencies: [toml==0.10.2] - # We use the 'match' and do not want pre-commit to pass - # globbed files - pass_filenames: false - args: ["--config=pyproject.toml"] - - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: @@ -64,30 +34,15 @@ repos: hooks: - id: check-github-workflows - - repo: local - hooks: - - id: pylint - name: pylint - entry: pylint - language: system - types: [python] - args: [ - "-rn", # Only display messages - "-sn", # Don't display the score - "--rcfile=.pylintrc", # Specify rc file - ] - - id: pycodestyle - name: pycodestyle - entry: pycodestyle - language: system - types: [python] - args: ["--config=./\\.pycodestyle"] - - repo: https://github.com/pre-commit/mirrors-prettier rev: "v4.0.0-alpha.8" hooks: - id: prettier types_or: [yaml, markdown, html, css, scss, javascript, json] -ci: - skip: [pylint, pycodestyle] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff + args: [--fix, --show-fixes] + - id: ruff-format diff --git a/docs/conf.py b/docs/conf.py index c8f3ff90..bd3491c9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,27 +1,29 @@ +from __future__ import annotations # noqa: INP001, D100 + import datetime +import faulthandler import os import sys -if sys.version_info >= (3, 0): - import faulthandler - faulthandler.enable() +faulthandler.enable() + +sys.path.insert(0, os.path.abspath(".")) # noqa: PTH100 -sys.path.insert(0, os.path.abspath('.')) +import numpy as np # noqa: E402 -import numpy as np # -- pyvista configuration --------------------------------------------------- -import pyvista +import pyvista # noqa: E402 -import pyvistaqt +import pyvistaqt # noqa: E402 # Manage errors -pyvista.set_error_output_file('errors.txt') +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') +pyvista.set_plot_theme("document") ws = np.array([1024, 768]) * 2 try: pyvista.global_theme.window_size = ws @@ -29,56 +31,57 @@ rc = pyvista.rcParams["window_size"] = ws del ws # Save figures in specified directory -pyvista.FIGURE_PATH = os.path.join(os.path.abspath('./images/'), 'auto-generated/') -if not os.path.exists(pyvista.FIGURE_PATH): - os.makedirs(pyvista.FIGURE_PATH) +pyvista.FIGURE_PATH = os.path.join(os.path.abspath("./images/"), "auto-generated/") # noqa: PTH100, PTH118 +if not os.path.exists(pyvista.FIGURE_PATH): # noqa: PTH110 + os.makedirs(pyvista.FIGURE_PATH) # noqa: PTH103 # SG warnings -import warnings +import warnings # noqa: E402 warnings.filterwarnings( "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 ------------------------------------------------ numfig = False html_show_sourcelink = False -html_logo = './_static/pyvista_logo.png' +html_logo = "./_static/pyvista_logo.png" # 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 linkcheck_timeout = 500 # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'PyVistaQt' -year = datetime.date.today().year -copyright = u'2017-{}, The PyVista Developers'.format(year) -author = u'Alex Kaszynski and Bane Sullivan' +project = "PyVistaQt" +year = datetime.date.today().year # noqa: DTZ011 +copyright = f"2017-{year}, The PyVista Developers" # noqa: A001 +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 @@ -95,29 +98,28 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'friendly' +pygments_style = "friendly" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Sphinx Gallery Options -# from sphinx_gallery.sorting import FileNameSortKey +# from sphinx_gallery.sorting import FileNameSortKey # noqa: ERA001 # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -import pydata_sphinx_theme html_theme = "pydata_sphinx_theme" html_context = { @@ -133,21 +135,21 @@ # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False -html_theme = 'pydata_sphinx_theme' +html_theme = "pydata_sphinx_theme" html_context = { # Enable the "Edit in GitHub link within the header of each page. - 'display_github': False, + "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 }} - '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'), + # Format Template: https://{{ github_host|default("github.com") }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} # noqa: E501 + "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"), ], } @@ -183,19 +185,19 @@ # 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"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'pyvistaqtdoc' +htmlhelp_basename = "pyvistaqtdoc" # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), - "pyvista": ('https://docs.pyvista.org/', None), + "pyvista": ("https://docs.pyvista.org/", None), "PySide6": ("https://doc.qt.io/qtforpython-6/", None), "PyQt6": ("https://www.riverbankcomputing.com/static/Docs/PyQt6/", None), "numpy": ("https://numpy.org/doc/stable", None), @@ -221,61 +223,63 @@ # -- 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 +from docutils.parsers.rst import directives # noqa: E402 +# -- Autosummary options +from sphinx.ext.autosummary import Autosummary # noqa: E402 +from sphinx.ext.autosummary import get_documenter # noqa: E402 +from sphinx.util.inspect import safe_getattr # noqa: E402 -class AutoAutoSummary(Autosummary): - option_spec = { - 'methods': directives.unchanged, - 'attributes': directives.unchanged, +class AutoAutoSummary(Autosummary): # noqa: D101 + option_spec = { # noqa: RUF012 + "methods": directives.unchanged, + "attributes": directives.unchanged, } required_arguments = 1 app = None @staticmethod - def get_members(obj, typ, include_public=None): + def get_members(obj, typ, include_public=None): # noqa: ANN001, ANN205, D102 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: continue if documenter.objtype in typ: items.append(name) - public = [x for x in items if x in include_public or not x.startswith('_')] + public = [x for x in items if x in include_public or not x.startswith("_")] return public, items - def run(self): + def run(self): # noqa: ANN201, D102 clazz = str(self.arguments[0]) try: - (module_name, class_name) = clazz.rsplit('.', 1) + (module_name, class_name) = clazz.rsplit(".", 1) m = __import__(module_name, globals(), locals(), [class_name]) 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('_')] - 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('_')] - except: - print('Something went wrong when autodocumenting {}'.format(clazz)) + if "methods" in self.options: + _, methods = self.get_members(c, ["method"], ["__init__"]) + self.content = [f"~{clazz}.{method}" for method in methods if not method.startswith("_")] + if "attributes" in self.options: + _, attribs = self.get_members(c, ["attribute", "property"]) + self.content = [f"~{clazz}.{attrib}" for attrib in attribs if not attrib.startswith("_")] + except: # noqa: S110, E722 + pass finally: - return super(AutoAutoSummary, self).run() + return super().run() # noqa: B012 + -def setup(app): +def setup(app) -> None: # noqa: ANN001, D103 AutoAutoSummary.app = app - app.add_directive('autoautosummary', AutoAutoSummary) + app.add_directive("autoautosummary", AutoAutoSummary) app.add_css_file("style.css") app.add_css_file("copybutton.css") diff --git a/pyproject.toml b/pyproject.toml index fcc8a4c1..0826ba20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,4 +25,35 @@ match = ''' | rwi ).*\.py ) -''' \ No newline at end of file +''' + +[tool.ruff] +line-length = 150 + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "COM812", + "D203", + "D212", + "ISC001" +] + +[tool.ruff.lint.isort] +# 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 +required-imports = ["from __future__ import annotations"] +force-single-line = true + +[tool.ruff.lint.per-file-ignores] +"doc/**" = ["INP001"] +"tests/**" = ["ANN001", "INP001", "S101"] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/pyvistaqt/__init__.py b/pyvistaqt/__init__.py index cdc9bb43..600f9c5f 100755 --- a/pyvistaqt/__init__.py +++ b/pyvistaqt/__init__.py @@ -1,43 +1,49 @@ -"""PyVista package for 3D plotting and mesh analysis.""" +"""PyVista package for 3D plotting and mesh analysis.""" # noqa: EXE002 + +from __future__ import annotations try: from importlib.metadata import version __version__ = version("pyvistaqt") -except Exception: # pragma: no cover # pylint: disable=broad-exception-caught +except Exception: # pragma: no cover # pylint: disable=broad-exception-caught # noqa: BLE001 try: from ._version import __version__ except ImportError: - __version__ = '0.0.0' + __version__ = "0.0.0" try: - from qtpy import QtCore # noqa -except Exception as exc: # pragma: no cover # pylint: disable=broad-except + from qtpy import QtCore # noqa: F401 +except Exception as exc: # pragma: no cover # pylint: disable=broad-except # noqa: BLE001 _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}") + def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003, ARG002 + msg = f"No Qt binding was found, got: {_exc_msg}" + raise RuntimeError(msg) # pylint: disable=too-few-public-methods - class BackgroundPlotter(_QtBindingError): + class BackgroundPlotter(_QtBindingError): # noqa: N818 """Handle Qt binding error for BackgroundPlotter.""" # pylint: disable=too-few-public-methods - class MainWindow(_QtBindingError): + class MainWindow(_QtBindingError): # noqa: N818 """Handle Qt binding error for MainWindow.""" # pylint: disable=too-few-public-methods - class MultiPlotter(_QtBindingError): + class MultiPlotter(_QtBindingError): # noqa: N818 """Handle Qt binding error for MultiPlotter.""" # pylint: disable=too-few-public-methods - class QtInteractor(_QtBindingError): + class QtInteractor(_QtBindingError): # noqa: N818 """Handle Qt binding error for QtInteractor.""" else: - from .plotting import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor + from .plotting import BackgroundPlotter + from .plotting import MainWindow + from .plotting import MultiPlotter + from .plotting import QtInteractor __all__ = [ diff --git a/pyvistaqt/counter.py b/pyvistaqt/counter.py index e31a7a64..943f5225 100644 --- a/pyvistaqt/counter.py +++ b/pyvistaqt/counter.py @@ -1,6 +1,10 @@ -"""This module contains a basic Qt-compatible counter class.""" +"""This module contains a basic Qt-compatible counter class.""" # noqa: D404 -from qtpy.QtCore import QObject, Signal, Slot +from __future__ import annotations + +from qtpy.QtCore import QObject +from qtpy.QtCore import Signal +from qtpy.QtCore import Slot class Counter(QObject): @@ -16,11 +20,11 @@ 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)}" - ) + msg = f"Expected type of `count` to be `int` but got: {type(count)}" + raise TypeError(msg) else: - raise ValueError("count is not strictly positive.") + msg = "count is not strictly positive." + raise ValueError(msg) @Slot() def decrease(self) -> None: diff --git a/pyvistaqt/dialog.py b/pyvistaqt/dialog.py index 0eaa3612..7228e553 100644 --- a/pyvistaqt/dialog.py +++ b/pyvistaqt/dialog.py @@ -1,26 +1,32 @@ -"""This module contains Qt dialog widgets.""" +"""This module contains Qt dialog widgets.""" # noqa: D404 + +from __future__ import annotations import os -from typing import Any, List +from typing import TYPE_CHECKING +from typing import Any +from typing import List +from typing import Optional -import numpy as np # type: ignore -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 +from qtpy.QtWidgets import QDoubleSpinBox +from qtpy.QtWidgets import QFileDialog +from qtpy.QtWidgets import QFormLayout +from qtpy.QtWidgets import QHBoxLayout +from qtpy.QtWidgets import QSlider + +if TYPE_CHECKING: + import numpy as np + import pyvista as pv -from .window import MainWindow + from .window import MainWindow class FileDialog(QFileDialog): - """Generic file query. + """ + Generic file query. It emits a signal when a file is selected and the dialog was property closed. @@ -31,14 +37,14 @@ class FileDialog(QFileDialog): dlg_accepted = Signal(str) # pylint: disable=too-many-arguments - def __init__( + def __init__( # noqa: PLR0913 self, parent: MainWindow = None, - filefilter: List[str] = None, - save_mode: bool = True, - show: bool = True, + filefilter: Optional[List[str]] = None, + save_mode: bool = True, # noqa: FBT001, FBT002 + show: bool = True, # noqa: FBT001, FBT002 callback: np.ndarray = None, - directory: bool = False, + directory: bool = False, # noqa: FBT001, FBT002 ) -> None: """Initialize the file dialog.""" super().__init__(parent) @@ -51,7 +57,7 @@ def __init__( if directory: self.FileMode(QFileDialog.Directory) - self.setOption(QFileDialog.ShowDirsOnly, True) + self.setOption(QFileDialog.ShowDirsOnly, True) # noqa: FBT003 if save_mode: self.setAcceptMode(QFileDialog.AcceptSave) @@ -63,7 +69,8 @@ def __init__( self.show() def emit_accepted(self) -> None: - """Send signal that the file dialog was closed properly. + """ + Send signal that the file dialog was closed properly. Sends: filename @@ -71,19 +78,20 @@ def emit_accepted(self) -> None: """ if self.result(): filename = self.selectedFiles()[0] - if os.path.isdir(os.path.dirname(filename)): + if os.path.isdir(os.path.dirname(filename)): # noqa: PTH112, PTH120 self.dlg_accepted.emit(filename) class DoubleSlider(QSlider): - """Double precision slider. + """ + Double precision slider. Reference: https://gist.github.com/dennis-tra/994a65d6165a328d4eabaadbaedac2cc """ - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 """Initialize the double slider.""" super().__init__(*args, **kwargs) self.decimals = 5 @@ -102,28 +110,26 @@ 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 + def setValue(self, value: float) -> None: # pylint: disable=invalid-name # noqa: N802 """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 + def setMinimum(self, value: float) -> None: # pylint: disable=invalid-name # noqa: N802 """Set the minimum value of the slider.""" if value > self._max_value: # pragma: no cover - raise ValueError("Minimum limit cannot be higher than maximum") + msg = "Minimum limit cannot be higher than maximum" + raise ValueError(msg) self._min_value = value self.setValue(self.value()) - def setMaximum(self, value: float) -> None: # pylint: disable=invalid-name + def setMaximum(self, value: float) -> None: # pylint: disable=invalid-name # noqa: N802 """Set the maximum value of the slider.""" if value < self._min_value: # pragma: no cover - raise ValueError("Minimum limit cannot be higher than maximum") + msg = "Minimum limit cannot be higher than maximum" + raise ValueError(msg) self._max_value = value self.setValue(self.value()) @@ -138,7 +144,7 @@ class RangeGroup(QHBoxLayout): def __init__( self, parent: MainWindow, - callback: Any, + callback: Any, # noqa: ANN401 minimum: float = 0.0, maximum: float = 20.0, value: float = 1.0, @@ -153,9 +159,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) @@ -165,22 +169,20 @@ def __init__( self.spinbox.valueChanged.connect(self.update_value) self.spinbox.valueChanged.connect(callback) - return None - - def update_spinbox(self, value: float) -> None: # pylint: disable=unused-argument + def update_spinbox(self, value: float) -> None: # pylint: disable=unused-argument # noqa: ARG002 """Set the value of the internal spinbox.""" self.spinbox.setValue(self.slider.value()) - def update_value(self, value: float) -> None: # pylint: disable=unused-argument + def update_value(self, value: float) -> None: # pylint: disable=unused-argument # noqa: ARG002 """Update the value of the internal slider.""" # if self.spinbox.value() < self.minimum: - # self.spinbox.setValue(self.minimum) - # elif self.spinbox.value() > self.maximum: - # self.spinbox.setValue(self.maximum) + # self.spinbox.setValue(self.minimum) # noqa: ERA001 + # elif self.spinbox.value() > self.maximum: # noqa: ERA001 + # self.spinbox.setValue(self.maximum) # noqa: ERA001 - self.slider.blockSignals(True) + self.slider.blockSignals(True) # noqa: FBT003 self.slider.setValue(self.spinbox.value()) - self.slider.blockSignals(False) + self.slider.blockSignals(False) # noqa: FBT003 @property def value(self) -> float: @@ -201,9 +203,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: # noqa: FBT001, FBT002 """Initialize the scaling dialog.""" super().__init__(parent) self.setGeometry(300, 300, 50, 50) @@ -212,15 +212,9 @@ 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) diff --git a/pyvistaqt/editor.py b/pyvistaqt/editor.py index 4bd24e36..5ffa0cf6 100644 --- a/pyvistaqt/editor.py +++ b/pyvistaqt/editor.py @@ -1,25 +1,28 @@ -"""This module contains the Qt scene editor.""" +"""This module contains the Qt scene editor.""" # noqa: D404 -import weakref +from __future__ import annotations + +from typing import TYPE_CHECKING from typing import List +import weakref -from pyvista import Renderer from qtpy.QtCore import Qt -from qtpy.QtWidgets import ( - QCheckBox, - QDialog, - QDoubleSpinBox, - QHBoxLayout, - QLabel, - QStackedWidget, - QTreeWidget, - QTreeWidgetItem, - QVBoxLayout, - QWidget, -) -from vtkmodules.vtkRenderingCore import vtkActor - -from .window import MainWindow +from qtpy.QtWidgets import QCheckBox +from qtpy.QtWidgets import QDialog +from qtpy.QtWidgets import QDoubleSpinBox +from qtpy.QtWidgets import QHBoxLayout +from qtpy.QtWidgets import QLabel +from qtpy.QtWidgets import QStackedWidget +from qtpy.QtWidgets import QTreeWidget +from qtpy.QtWidgets import QTreeWidgetItem +from qtpy.QtWidgets import QVBoxLayout +from qtpy.QtWidgets import QWidget + +if TYPE_CHECKING: + from pyvista import Renderer + from vtkmodules.vtkRenderingCore import vtkActor + + from .window import MainWindow class Editor(QDialog): @@ -60,7 +63,7 @@ def update(self) -> None: """Update the internal widget list.""" self.tree_widget.clear() for idx, renderer in enumerate(self.renderers): - actors = renderer._actors # pylint: disable=protected-access + actors = renderer._actors # pylint: disable=protected-access # noqa: SLF001 widget_idx = self.stacked_widget.addWidget(_get_renderer_widget(renderer)) top_item = QTreeWidgetItem(self.tree_widget, [f"Renderer {idx}"]) top_item.setData(0, Qt.ItemDataRole.UserRole, widget_idx) @@ -95,7 +98,7 @@ def _get_renderer_widget(renderer: Renderer) -> QWidget: del renderer # axes - def _axes_callback(state: bool) -> None: + def _axes_callback(state: bool) -> None: # noqa: FBT001 renderer = renderer_ref() if renderer is None or renderer.parent.iren is None: # pragma: no cover return @@ -120,7 +123,7 @@ def _get_actor_widget(actor: vtkActor) -> QWidget: # visibility set_vis_ref = weakref.ref(actor.SetVisibility) - def _set_vis(visibility: bool) -> None: # pragma: no cover + def _set_vis(visibility: bool) -> None: # pragma: no cover # noqa: FBT001 set_vis = set_vis_ref() if set_vis is not None: set_vis(visibility) diff --git a/pyvistaqt/plotting.py b/pyvistaqt/plotting.py index 7ceb849b..8fffc06c 100644 --- a/pyvistaqt/plotting.py +++ b/pyvistaqt/plotting.py @@ -38,17 +38,28 @@ probably entirely separate from the Python ``super()`` process. We fix this by internally by temporarily monkey-patching ``BasePlotter.__init__`` with a no-op ``__init__``. -""" +""" # noqa: D404 + +from __future__ import annotations + import contextlib +from functools import wraps import logging import os import platform import time +from typing import Any +from typing import Callable +from typing import Dict +from typing import Generator +from typing import List +from typing import Optional +from typing import Tuple +from typing import Type +from typing import Union import warnings -from functools import wraps -from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union -import numpy as np # type: ignore +import numpy as np # type: ignore # noqa: PGH003 import pyvista from pyvista import global_theme @@ -59,35 +70,37 @@ from pyvista.plotting.render_window_interactor import RenderWindowInteractor try: - from pyvista.core.utilities import conditional_decorator, threaded + from pyvista.core.utilities import conditional_decorator + from pyvista.core.utilities import threaded except ImportError: # PV < 0.40 - from pyvista.utilities import conditional_decorator, threaded -from qtpy import QtCore, QtGui -from qtpy.QtCore import QSize, QTimer, Signal -from qtpy.QtWidgets import ( - QAction, - QApplication, - QFrame, - QGestureEvent, - QGridLayout, - QMenuBar, - QToolBar, - QVBoxLayout, - QWidget, -) + from pyvista.utilities import conditional_decorator + from pyvista.utilities import threaded +from qtpy import QtCore +from qtpy import QtGui +from qtpy.QtCore import QSize +from qtpy.QtCore import QTimer +from qtpy.QtCore import Signal +from qtpy.QtWidgets import QAction +from qtpy.QtWidgets import QApplication +from qtpy.QtWidgets import QFrame +from qtpy.QtWidgets import QGestureEvent +from qtpy.QtWidgets import QGridLayout +from qtpy.QtWidgets import QMenuBar +from qtpy.QtWidgets import QToolBar +from qtpy.QtWidgets import QVBoxLayout +from qtpy.QtWidgets import QWidget from vtkmodules.vtkRenderingUI import vtkGenericRenderWindowInteractor from .counter import Counter -from .dialog import FileDialog, ScaleAxesDialog +from .dialog import FileDialog +from .dialog import ScaleAxesDialog from .editor import Editor from .rwi import QVTKRenderWindowInteractor -from .utils import ( - _check_type, - _create_menu_bar, - _setup_application, - _setup_ipython, - _setup_off_screen, -) +from .utils import _check_type +from .utils import _create_menu_bar +from .utils import _setup_application +from .utils import _setup_ipython +from .utils import _setup_off_screen from .window import MainWindow LOG = logging.getLogger("pyvistaqt") @@ -102,8 +115,8 @@ # See https://github.com/pyvista/pyvista/pull/693 # LOG is unused at the moment -# LOG = logging.getLogger(__name__) -# LOG.setLevel('DEBUG') +# LOG = logging.getLogger(__name__) # noqa: ERA001 +# LOG.setLevel('DEBUG') # noqa: ERA001 SAVE_CAM_BUTTON_TEXT = "Save Camera" CLEAR_CAMS_BUTTON_TEXT = "Clear Cameras" @@ -137,7 +150,7 @@ def pad_image(arr: np.ndarray, max_size: int = 400) -> np.ndarray: @contextlib.contextmanager def _no_base_plotter_init() -> Generator[None, None, None]: init = BasePlotter.__init__ - BasePlotter.__init__ = lambda *args, **kwargs: None + BasePlotter.__init__ = lambda *args, **kwargs: None # noqa: ARG005 try: yield finally: @@ -145,7 +158,8 @@ def _no_base_plotter_init() -> Generator[None, None, None]: class QtInteractor(QVTKRenderWindowInteractor, BasePlotter): - """Extend QVTKRenderWindowInteractor class. + """ + Extend QVTKRenderWindowInteractor class. This adds the methods available to pyvista.Plotter. @@ -175,6 +189,7 @@ class QtInteractor(QVTKRenderWindowInteractor, BasePlotter): Number of updates per second. Useful for automatically updating the render window when actors are change without being automatically ``Modified``. + """ # pylint: disable=too-many-instance-attributes @@ -185,17 +200,17 @@ class QtInteractor(QVTKRenderWindowInteractor, BasePlotter): key_press_event_signal = Signal(vtkGenericRenderWindowInteractor, str) # pylint: disable=too-many-arguments - def __init__( + def __init__( # noqa: C901, PLR0912, PLR0913 self, parent: MainWindow = None, - title: str = None, - off_screen: bool = None, - multi_samples: int = None, - line_smoothing: bool = False, - point_smoothing: bool = False, - polygon_smoothing: bool = False, + title: Optional[str] = None, + off_screen: Optional[bool] = None, + multi_samples: Optional[int] = None, + line_smoothing: bool = False, # noqa: FBT001, FBT002 + point_smoothing: bool = False, # noqa: FBT001, FBT002 + polygon_smoothing: bool = False, # noqa: FBT001, FBT002 auto_update: Union[float, bool] = 5.0, - **kwargs: Any, + **kwargs: Any, # noqa: ANN401 ) -> None: # pylint: disable=too-many-branches """Initialize Qt interactor.""" @@ -263,34 +278,27 @@ def __init__( self.render_timer.timeout.connect(self.render) self.render_timer.start(twait) - if global_theme.depth_peeling["enabled"]: - if self.enable_depth_peeling(): - for renderer in self.renderers: - renderer.enable_depth_peeling() + if global_theme.depth_peeling["enabled"] and self.enable_depth_peeling(): + for renderer in self.renderers: + renderer.enable_depth_peeling() # Set some private attributes that let BasePlotter know # that this is safely rendering self._first_time = False # Crucial! - # self._rendered = True # this is handled in render() + # self._rendered = True # this is handled in render() # noqa: ERA001 LOG.debug("QtInteractor init stop") - def _setup_interactor(self, off_screen: bool) -> None: + def _setup_interactor(self, off_screen: bool) -> None: # noqa: FBT001 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._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() @@ -303,12 +311,12 @@ def gesture_event(self, event: QGestureEvent) -> bool: self.update() return True - def key_press_event(self, obj: Any, event: Any) -> None: + def key_press_event(self, obj: Any, event: Any) -> None: # noqa: ANN401 """Call `key_press_event` using a signal.""" self.key_press_event_signal.emit(obj, event) @wraps(BasePlotter.render) - def _render(self, *args: Any, **kwargs: Any) -> BasePlotter.render: + def _render(self, *args: Any, **kwargs: Any) -> BasePlotter.render: # noqa: ANN401 """Wrap ``BasePlotter.render``.""" return BasePlotter.render(self, *args, **kwargs) @@ -333,10 +341,9 @@ def disable(self) -> None: self.setDisabled(True) return BasePlotter.disable(self) - def link_views_across_plotters( - self, other_plotter: Any, view: int = 0, other_views: Any = None - ) -> None: - """Link the views' cameras across two plotters. + def link_views_across_plotters(self, other_plotter: Any, view: int = 0, other_views: Any = None) -> None: # noqa: ANN401 + """ + Link the views' cameras across two plotters. Parameters ---------- @@ -363,18 +370,17 @@ def link_views_across_plotters( other_views = np.asarray(other_views) 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" - ) + msg = "Expected `other_views` type is int, or list or tuple of ints, " f"but {other_views.dtype} is given" + raise TypeError(msg) renderer = self.renderers[view] for view_index in other_views: other_plotter.renderers[view_index].camera = renderer.camera # pylint: disable=invalid-name - def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: - """Event is called when something is dropped onto the vtk window. + def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: # noqa: N802 + """ + 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 @@ -382,23 +388,23 @@ def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: """ try: for url in event.mimeData().urls(): - if os.path.isfile(url.path()): + if os.path.isfile(url.path()): # noqa: PTH113 # only call accept on files event.accept() - except IOError as exception: # pragma: no cover - warnings.warn(f"Exception when dragging files: {str(exception)}") + except OSError as exception: # pragma: no cover + warnings.warn(f"Exception when dragging files: {exception!s}") # noqa: B028 # pylint: disable=invalid-name,useless-return - def dropEvent(self, event: QtCore.QEvent) -> None: + def dropEvent(self, event: QtCore.QEvent) -> None: # noqa: N802 """Event is called after dragEnterEvent.""" try: for url in event.mimeData().urls(): self.url = url filename = self.url.path() - if os.path.isfile(filename): + if os.path.isfile(filename): # noqa: PTH113 self.add_mesh(pyvista.read(filename)) - except IOError as exception: # pragma: no cover - warnings.warn(f"Exception when dropping files: {str(exception)}") + except OSError as exception: # pragma: no cover + warnings.warn(f"Exception when dropping files: {exception!s}") # noqa: B028 def close(self) -> None: """Quit application.""" @@ -414,14 +420,13 @@ def close(self) -> None: _FakeEventHandler() ) for key in ("_RenderWindow", "renderer"): - try: + with contextlib.suppress(AttributeError): setattr(self, key, None) - except AttributeError: - pass class BackgroundPlotter(QtInteractor): - """Qt interactive plotter. + """ + Qt interactive plotter. Background plotter for pyvista that allows you to maintain an interactive plotting window without blocking the main python @@ -493,6 +498,7 @@ class BackgroundPlotter(QtInteractor): >>> from pyvistaqt import BackgroundPlotter >>> plotter = BackgroundPlotter() >>> _ = plotter.add_mesh(pv.Sphere()) + """ # pylint: disable=too-many-ancestors @@ -503,19 +509,19 @@ class BackgroundPlotter(QtInteractor): # pylint: disable=too-many-arguments # pylint: disable=too-many-locals - def __init__( + def __init__( # noqa: PLR0913, PLR0915 self, - show: bool = True, + show: bool = True, # noqa: FBT001, FBT002 app: Optional[QApplication] = None, window_size: Optional[Tuple[int, int]] = None, off_screen: Optional[bool] = None, - allow_quit_keypress: bool = True, - toolbar: bool = True, - menu_bar: bool = True, - editor: bool = True, + allow_quit_keypress: bool = True, # noqa: FBT001, FBT002 + toolbar: bool = True, # noqa: FBT001, FBT002 + menu_bar: bool = True, # noqa: FBT001, FBT002 + editor: bool = True, # noqa: FBT001, FBT002 update_app_icon: Optional[bool] = None, app_window_class: Optional[Type[MainWindow]] = None, - **kwargs: Any, + **kwargs: Any, # noqa: ANN401 ) -> None: # pylint: disable=too-many-branches """Initialize the qt plotter.""" @@ -563,14 +569,12 @@ def __init__( 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() super().__init__(parent=self.frame, off_screen=off_screen, **kwargs) - assert not self._closed + assert not self._closed # noqa: S101 vlayout.addWidget(self) self.frame.setLayout(vlayout) self.app_window.setCentralWidget(self.frame) @@ -596,13 +600,9 @@ def __init__( if update_app_icon: 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" - ) - ) + self.set_icon(os.path.join(os.path.dirname(__file__), "data", "pyvista_logo_square.png")) # noqa: PTH118, PTH120 else: - assert update_app_icon is False + assert update_app_icon is False # noqa: S101 # Keypress events if self.iren is not None: @@ -610,7 +610,8 @@ def __init__( LOG.debug("BackgroundPlotter init stop") def reset_key_events(self) -> None: - """Reset all of the key press events to their defaults. + """ + Reset all of the key press events to their defaults. Handles closing configuration for q-key. """ @@ -619,12 +620,13 @@ def reset_key_events(self) -> None: # pylint: disable=unnecessary-lambda self.add_key_event("q", lambda: self.close()) - def scale_axes_dialog(self, show: bool = True) -> ScaleAxesDialog: + def scale_axes_dialog(self, show: bool = True) -> ScaleAxesDialog: # noqa: FBT001, FBT002 """Open scale axes dialog.""" return ScaleAxesDialog(self.app_window, self, show=show) def close(self) -> None: - """Close the plotter. + """ + Close the plotter. This function closes the window which in turn will close the plotter through `signal_close`. @@ -637,9 +639,9 @@ def close(self) -> None: # been deleted # # So let's be safe and try/except this in case of a problem. - try: + try: # noqa: SIM105 self.app_window.close() - except Exception: # pragma: no cover # pylint: disable=broad-except + except Exception: # pragma: no cover # pylint: disable=broad-except # noqa: S110, BLE001 pass def _close(self) -> None: @@ -647,9 +649,7 @@ def _close(self) -> None: 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 + 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() @@ -657,9 +657,7 @@ def update_app_icon(self) -> None: # Window size hasn't remained constant since last render. # This means the user is resizing it so ignore update. pass - elif ( - cur_time - self._last_update_time > BackgroundPlotter.ICON_TIME_STEP - ) and self._last_camera_pos != self.camera_position: + elif (cur_time - self._last_update_time > BackgroundPlotter.ICON_TIME_STEP) and self._last_camera_pos != self.camera_position: # its been a while since last update OR # the camera position has changed and its been at least one second @@ -673,7 +671,8 @@ def update_app_icon(self) -> None: self._last_window_size = self.window_size def set_icon(self, img: Union[np.ndarray, str]) -> None: - """Set the icon image. + """ + Set the icon image. Parameters ---------- @@ -686,41 +685,35 @@ def set_icon(self, img: Union[np.ndarray, str]) -> None: ----- Currently string paths can silently fail, so make sure your path is something that produces a valid ``QIcon(img)``. + """ if not ( - isinstance(img, np.ndarray) - and img.ndim == 3 - and img.shape[0] == img.shape[1] - and img.dtype == np.uint8 - and img.shape[-1] in (3, 4) + isinstance(img, np.ndarray) and img.ndim == 3 and img.shape[0] == img.shape[1] and img.dtype == np.uint8 and img.shape[-1] in (3, 4) # noqa: PLR2004 ) 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" - ) + msg = "img must be 3D uint8 ndarray with shape[1] == shape[2] and " "shape[2] == 3 or 4, or str" + raise ValueError(msg) if isinstance(img, np.ndarray): fmt_str = "Format_RGB" - fmt_str += ("A8" if img.shape[2] == 4 else "") + "888" + fmt_str += ("A8" if img.shape[2] == 4 else "") + "888" # noqa: PLR2004 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. self.app.setWindowIcon(QtGui.QIcon(img)) - def _qt_screenshot(self, show: bool = True) -> FileDialog: + def _qt_screenshot(self, show: bool = True) -> FileDialog: # noqa: FBT001, FBT002 return FileDialog( self.app_window, filefilter=["Image File (*.png)", "JPEG (*.jpeg)"], show=show, - directory=bool(os.getcwd()), + directory=bool(os.getcwd()), # noqa: PTH109 callback=self.screenshot, ) - def _qt_export_vtkjs(self, show: bool = True) -> FileDialog: - """Spawn an save file dialog to export a vtksz file. + def _qt_export_vtkjs(self, show: bool = True) -> FileDialog: # noqa: FBT001, FBT002 + """ + Spawn an save file dialog to export a vtksz file. The exported file can be viewed with the OfflineLocalView viewer available at https://kitware.github.io/vtk-js/examples/OfflineLocalView.html @@ -728,16 +721,16 @@ def _qt_export_vtkjs(self, show: bool = True) -> FileDialog: """ try: callback = self.export_vtksz - ext = 'vtksz' + ext = "vtksz" except AttributeError: callback = self.export_vtkjs # pre-v0.40 - ext = 'vtkjs' + ext = "vtkjs" return FileDialog( self.app_window, filefilter=[f"VTK.js File(*.{ext})"], show=show, - directory=bool(os.getcwd()), + directory=bool(os.getcwd()), # noqa: PTH109 callback=callback, ) @@ -763,16 +756,15 @@ def window_size(self, window_size: QSize) -> None: self.app_window.setBaseSize(*window_size) self.app_window.resize(*window_size) # NOTE: setting BasePlotter is unnecessary and Segfaults CI - # BasePlotter.window_size.fset(self, window_size) + # BasePlotter.window_size.fset(self, window_size) # noqa: ERA001 def __del__(self) -> None: # pragma: no cover """Delete the qt plotter.""" self.close() - def add_callback( - self, func: Callable, interval: int = 1000, count: Optional[int] = None - ) -> None: - """Add a function that can update the scene in the background. + def add_callback(self, func: Callable, interval: int = 1000, count: Optional[int] = None) -> None: + """ + Add a function that can update the scene in the background. Parameters ---------- @@ -812,7 +804,7 @@ def load_camera_position() -> None: self.camera_position = camera_position self.saved_cameras_tool_bar.addAction(f"Cam {ncam}", load_camera_position) - if ncam < 10: + if ncam < 10: # noqa: PLR2004 self.add_key_event(str(ncam), load_camera_position) def clear_camera_positions(self) -> None: @@ -823,7 +815,7 @@ def clear_camera_positions(self) -> None: self.saved_cameras_tool_bar.removeAction(action) self.saved_camera_positions = [] - def _add_action(self, tool_bar: QToolBar, key: str, method: Any) -> QAction: + def _add_action(self, tool_bar: QToolBar, key: str, method: Any) -> QAction: # noqa: ANN401 action = QAction(key, self.app_window) action.triggered.connect(method) tool_bar.addAction(action) @@ -834,7 +826,7 @@ def add_toolbars(self) -> None: # Camera toolbar self.default_camera_tool_bar = self.app_window.addToolBar("Camera Position") - def _view_vector(*args: Any) -> None: + def _view_vector(*args: Any) -> None: # noqa: ANN401 return self.view_vector(*args) cvec_setters = { @@ -848,23 +840,15 @@ def _view_vector(*args: Any) -> None: "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 - ) + self._add_action(self.saved_cameras_tool_bar, SAVE_CAM_BUTTON_TEXT, self.save_camera_position) self._add_action( self.saved_cameras_tool_bar, CLEAR_CAMS_BUTTON_TEXT, @@ -884,9 +868,7 @@ def add_menu_bar(self) -> None: 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 - ) + 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) @@ -898,9 +880,7 @@ def add_menu_bar(self) -> None: ) cam_menu = view_menu.addMenu("Camera") - self._parallel_projection_action = cam_menu.addAction( - "Toggle Parallel Projection", self._toggle_parallel_projection - ) + self._parallel_projection_action = cam_menu.addAction("Toggle Parallel Projection", self._toggle_parallel_projection) view_menu.addSeparator() # Orientation marker @@ -927,7 +907,8 @@ def add_editor(self) -> None: class MultiPlotter: - """Qt interactive plotter. + """ + Qt interactive plotter. Multi plotter for pyvista that allows to maintain an interactive window with multiple plotters without @@ -956,21 +937,22 @@ class MultiPlotter: >>> from pyvistaqt import MultiPlotter >>> plotter = MultiPlotter() >>> _ = plotter[0, 0].add_mesh(pv.Sphere()) + """ # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments - def __init__( + def __init__( # noqa: PLR0913 self, app: Optional[QApplication] = None, nrows: int = 1, ncols: int = 1, - show: bool = True, + show: bool = True, # noqa: FBT001, FBT002 window_size: Optional[Tuple[int, int]] = None, title: Optional[str] = None, off_screen: Optional[bool] = None, - **kwargs: Any, + **kwargs: Any, # noqa: ANN401 ) -> None: """Initialize the multi plotter.""" _check_type(app, "app", [QApplication, type(None)]) @@ -1012,8 +994,9 @@ def close(self) -> None: """Close the multi plotter.""" self._window.close() - def __setitem__(self, idx: Tuple[int, int], plotter: Any) -> None: - """Set a valid plotter in the grid. + def __setitem__(self, idx: Tuple[int, int], plotter: Any) -> None: # noqa: ANN401 + """ + Set a valid plotter in the grid. Parameters ---------- @@ -1022,12 +1005,14 @@ def __setitem__(self, idx: Tuple[int, int], plotter: Any) -> None: be an integer or a tuple ``(row, col)``. plotter : BackgroundPlotter The plotter to set. + """ row, col = idx self._plotters[row * self._ncols + col] = plotter def __getitem__(self, idx: Tuple[int, int]) -> Optional[BackgroundPlotter]: - """Get a valid plotter in the grid. + """ + Get a valid plotter in the grid. Parameters ---------- @@ -1039,6 +1024,7 @@ def __getitem__(self, idx: Tuple[int, int]) -> Optional[BackgroundPlotter]: ------- plotter : BackgroundPlotter The selected plotter. + """ row, col = idx self._plotter = self._plotters[row * self._ncols + col] diff --git a/pyvistaqt/rwi.py b/pyvistaqt/rwi.py index 9730b8ab..7ed1ca70 100644 --- a/pyvistaqt/rwi.py +++ b/pyvistaqt/rwi.py @@ -1,6 +1,6 @@ # modified from: https://gitlab.kitware.com/vtk/vtk # under the OSI-approved BSD 3-clause License -# TODO: Mayavi has a potentially different version that might be better or worse +# TODO: Mayavi has a potentially different version that might be better or worse # noqa: FIX002, TD002, TD003 # coding=utf-8 """ A simple VTK widget for PyQt or PySide. @@ -54,11 +54,14 @@ Changes by Eric Larson and Guillaume Favelier, Apr. 2022 Support for PyQt6 -""" +""" # noqa: D205 # Check whether a specific PyQt implementation was chosen +from __future__ import annotations + try: import vtkmodules.qt + PyQtImpl = vtkmodules.qt.PyQtImpl except ImportError: pass @@ -70,46 +73,57 @@ QVTKRWIBase = "QWidget" try: import vtkmodules.qt + QVTKRWIBase = vtkmodules.qt.QVTKRWIBase except ImportError: pass -from vtkmodules.vtkRenderingCore import vtkRenderWindow -from vtkmodules.vtkRenderingUI import vtkGenericRenderWindowInteractor +from vtkmodules.vtkRenderingCore import vtkRenderWindow # noqa: E402 +from vtkmodules.vtkRenderingUI import vtkGenericRenderWindowInteractor # noqa: E402 if PyQtImpl is None: # Autodetect the PyQt implementation to use try: - import PyQt6 + import PyQt6 # noqa: F401 + PyQtImpl = "PyQt6" except ImportError: try: - import PySide6 + import PySide6 # noqa: F401 + PyQtImpl = "PySide6" except ImportError: try: - import PyQt5 + import PyQt5 # noqa: F401 + PyQtImpl = "PyQt5" except ImportError: try: - import PySide2 + import PySide2 # noqa: F401 + PyQtImpl = "PySide2" except ImportError: try: - import PyQt4 + import PyQt4 # noqa: F401 + PyQtImpl = "PyQt4" except ImportError: try: - import PySide + import PySide # noqa: F401 + PyQtImpl = "PySide" except ImportError: - raise ImportError("Cannot load either PyQt or PySide") + msg = "Cannot load either PyQt or PySide" + raise ImportError(msg) # noqa: B904 # Check the compatibility of PyQtImpl and QVTKRWIBase if QVTKRWIBase != "QWidget": - if PyQtImpl in ["PyQt6", "PySide6"] and QVTKRWIBase == "QOpenGLWidget": - pass # compatible - elif PyQtImpl in ["PyQt5", "PySide2","PyQt4", "PySide"] and QVTKRWIBase == "QGLWidget": + if ( + PyQtImpl in ["PyQt6", "PySide6"] + and QVTKRWIBase == "QOpenGLWidget" + or PyQtImpl in ["PyQt5", "PySide2", "PyQt4", "PySide"] + and QVTKRWIBase == "QGLWidget" + ): pass # compatible else: raise ImportError("Cannot load " + QVTKRWIBase + " from " + PyQtImpl) @@ -117,79 +131,73 @@ 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.QtGui import QCursor + from PyQt6.QtCore import QEvent + from PyQt6.QtCore import QSize 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 + from PyQt6.QtGui import QCursor + from PyQt6.QtWidgets import QApplication + from PyQt6.QtWidgets import QMainWindow + from PyQt6.QtWidgets import QSizePolicy + from PyQt6.QtWidgets import 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.QtGui import QCursor + from PySide6.QtCore import QEvent + from PySide6.QtCore import QSize 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 + from PySide6.QtGui import QCursor + from PySide6.QtWidgets import QApplication + from PySide6.QtWidgets import QMainWindow + from PySide6.QtWidgets import QSizePolicy + from PySide6.QtWidgets import 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.QtGui import QCursor + from PyQt5.QtCore import QEvent + from PyQt5.QtCore import QSize 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 + from PyQt5.QtGui import QCursor + from PyQt5.QtWidgets import QApplication + from PyQt5.QtWidgets import QMainWindow + from PyQt5.QtWidgets import QSizePolicy + from PyQt5.QtWidgets import 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.QtGui import QCursor + from PySide2.QtCore import QEvent + from PySide2.QtCore import QSize 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 + from PySide2.QtGui import QCursor + from PySide2.QtWidgets import QApplication + from PySide2.QtWidgets import QMainWindow + from PySide2.QtWidgets import QSizePolicy + from PySide2.QtWidgets import 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 QEvent + from PyQt4.QtCore import QSize 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 + from PyQt4.QtGui import QApplication + from PyQt4.QtGui import QMainWindow + from PyQt4.QtGui import QSizePolicy + from PyQt4.QtGui import 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 QEvent + from PySide.QtCore import QSize 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.QtGui import QApplication + from PySide.QtGui import QMainWindow + from PySide.QtGui import QSizePolicy + from PySide.QtGui import QWidget else: raise ImportError("Unknown PyQt implementation " + repr(PyQtImpl)) @@ -203,7 +211,7 @@ else: raise ImportError("Unknown base class for QVTKRenderWindowInteractor " + QVTKRWIBase) -if PyQtImpl == 'PyQt6': +if PyQtImpl == "PyQt6": CursorShape = Qt.CursorShape MouseButton = Qt.MouseButton WindowType = Qt.WindowType @@ -215,18 +223,14 @@ 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 -if PyQtImpl in ('PyQt4', 'PySide'): - MiddleButton = MouseButton.MidButton -else: - MiddleButton = MouseButton.MiddleButton +MiddleButton = MouseButton.MidButton if PyQtImpl in ("PyQt4", "PySide") else MouseButton.MiddleButton -def _get_event_pos(ev): +def _get_event_pos(ev): # noqa: ANN001, ANN202 try: # Qt6+ return ev.position().x(), ev.position().y() except AttributeError: # Qt5 @@ -234,8 +238,8 @@ 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. @@ -300,24 +304,24 @@ class QVTKRenderWindowInteractor(QVTKRWIBaseClass): - Keypress w: modify the representation of all actors so that they are wireframe. - """ + """ # noqa: D205 # 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 + _CURSOR_MAP = { # noqa: RUF012 + 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): + def __init__(self, parent=None, **kw) -> None: # noqa: ANN001, ANN003, C901, D107, PLR0915 # the current button self._ActiveButton = MouseButton.NoButton @@ -332,65 +336,67 @@ def __init__(self, parent=None, **kw): # stereo, rw try: - stereo = bool(kw['stereo']) + stereo = bool(kw["stereo"]) except KeyError: stereo = False try: - rw = kw['rw'] + rw = kw["rw"] except KeyError: rw = None # create base qt-level widget if QVTKRWIBase == "QWidget": - if "wflags" in kw: - wflags = kw['wflags'] - else: - wflags = Qt.WindowType.Widget + wflags = kw.get("wflags", Qt.WindowType.Widget) QWidget.__init__(self, parent, wflags | WindowType.MSWindowsOwnDC) elif QVTKRWIBase == "QGLWidget": QGLWidget.__init__(self, parent) 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() - WId = self.winId() + WId = self.winId() # noqa: N806 # Python2 - if type(WId).__name__ == 'PyCObject': - from ctypes import pythonapi, c_void_p, py_object + if type(WId).__name__ == "PyCObject": + from ctypes import c_void_p + from ctypes import py_object + from ctypes import 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) + WId = pythonapi.PyCObject_AsVoidPtr(WId) # noqa: N806 # Python3 - elif type(WId).__name__ == 'PyCapsule': - from ctypes import pythonapi, c_void_p, py_object, c_char_p + elif type(WId).__name__ == "PyCapsule": + from ctypes import c_char_p + from ctypes import c_void_p + from ctypes import py_object + from ctypes import 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) + WId = pythonapi.PyCapsule_GetPointer(WId, name) # noqa: N806 self._RenderWindow.SetWindowInfo(str(int(WId))) - if stereo: # stereo mode + if stereo: # stereo mode self._RenderWindow.StereoCapableWindowOn() self._RenderWindow.SetStereoTypeToCrystalEyes() try: - self._Iren = kw['iren'] + self._Iren = kw["iren"] except KeyError: self._Iren = vtkGenericRenderWindowInteractor() self._Iren.SetRenderWindow(self._RenderWindow) @@ -398,17 +404,16 @@ 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)) self._Timer = QTimer(self) self._Timer.timeout.connect(self.TimerEvent) - self._Iren.AddObserver('CreateTimerEvent', self.CreateTimer) - self._Iren.AddObserver('DestroyTimerEvent', self.DestroyTimer) - self._Iren.GetRenderWindow().AddObserver('CursorChangedEvent', - self.CursorChangedEvent) + self._Iren.AddObserver("CreateTimerEvent", self.CreateTimer) + self._Iren.AddObserver("DestroyTimerEvent", self.DestroyTimer) + 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 @@ -416,104 +421,101 @@ def __init__(self, parent=None, **kw): if self.parent(): self.parent().destroyed.connect(self.close, ConnectionType.DirectConnection) - def __getattr__(self, attr): - """Makes the object behave like a vtkGenericRenderWindowInteractor""" - if attr == '__vtk__': + def __getattr__(self, attr): # noqa: ANN001, ANN204 + """Makes the object behave like a vtkGenericRenderWindowInteractor.""" # noqa: D401 + if attr == "__vtk__": return lambda t=self._Iren: t - elif hasattr(self._Iren, attr): + if 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 - ''' + def Finalize(self) -> None: # noqa: N802 + """Call internal cleanup method on VTK objects.""" self._RenderWindow.Finalize() - def CreateTimer(self, obj, evt): + def CreateTimer(self, obj, evt) -> None: # noqa: ANN001, ARG002, N802, D102 self._Timer.start(10) - def DestroyTimer(self, obj, evt): + def DestroyTimer(self, obj, evt) -> int: # noqa: ANN001, ARG002, N802, D102 self._Timer.stop() return 1 - def TimerEvent(self): + def TimerEvent(self) -> None: # noqa: N802, D102 self._Iren.TimerEvent() - def CursorChangedEvent(self, obj, evt): - """Called when the CursorChangedEvent fires on the render window.""" + def CursorChangedEvent(self, obj, evt) -> None: # noqa: ANN001, ARG002, N802 + """Called when the CursorChangedEvent fires on the render window.""" # noqa: D401 # This indirection is needed since when the event fires, the current # cursor is not yet set so we defer this by which time the current # cursor should have been set. QTimer.singleShot(0, self.ShowCursor) - def HideCursor(self): + def HideCursor(self) -> None: # noqa: N802 """Hides the cursor.""" self.setCursor(Qt.BlankCursor) - def ShowCursor(self): - """Shows the cursor.""" + def ShowCursor(self) -> None: # noqa: N802 + """Shows the cursor.""" # noqa: D401 vtk_cursor = self._Iren.GetRenderWindow().GetCurrentCursor() qt_cursor = self._CURSOR_MAP.get(vtk_cursor, Qt.ArrowCursor) self.setCursor(qt_cursor) - def closeEvent(self, evt): + def closeEvent(self, evt) -> None: # noqa: ANN001, ARG002, N802, D102 self.Finalize() - def sizeHint(self): + def sizeHint(self): # noqa: ANN201, N802, D102 return QSize(400, 400) - def paintEngine(self): + def paintEngine(self) -> None: # noqa: N802, D102 return None - def paintEvent(self, ev): + def paintEvent(self, ev) -> None: # noqa: ANN001, ARG002, N802, D102 self._Iren.Render() - def resizeEvent(self, ev): + def resizeEvent(self, ev) -> None: # noqa: ANN001, ARG002, N802, D102 scale = self._getPixelRatio() - w = int(round(scale*self.width())) - h = int(round(scale*self.height())) + w = int(round(scale * self.width())) + h = int(round(scale * self.height())) if self._RenderWindow is None: return - self._RenderWindow.SetDPI(int(round(72*scale))) + 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. + def _GetKeyCharAndKeySym(self, ev): # noqa: ANN001, ANN202, N802 + """ + Convert a Qt key into a char and a vtk keysym. This is essentially copied from the c++ implementation in GUISupport/Qt/QVTKInteractorAdapter.cxx. """ # if there is a char, convert its ASCII code to a VTK keysym try: - keyChar = ev.text()[0] - keySym = _keysyms_for_ascii[ord(keyChar)] + keyChar = ev.text()[0] # noqa: N806 + keySym = _keysyms_for_ascii[ord(keyChar)] # noqa: N806 except IndexError: - keyChar = '\0' - keySym = None + keyChar = "\0" # noqa: N806 + keySym = None # noqa: N806 # next, try converting Qt key code to a VTK keysym if keySym is None: try: - keySym = _keysyms[ev.key()] + keySym = _keysyms[ev.key()] # noqa: N806 except KeyError: - keySym = None + keySym = None # noqa: N806 # use "None" as a fallback if keySym is None: - keySym = "None" + keySym = "None" # noqa: N806 return keyChar, keySym - def _GetCtrlShift(self, ev): + def _GetCtrlShift(self, ev): # noqa: ANN001, ANN202, N802 ctrl = shift = False - if hasattr(ev, 'modifiers'): + if hasattr(ev, "modifiers"): if ev.modifiers() & KeyboardModifier.ShiftModifier: shift = True if ev.modifiers() & KeyboardModifier.ControlModifier: @@ -527,48 +529,41 @@ def _GetCtrlShift(self, ev): return ctrl, shift @staticmethod - def _getPixelRatio(): + def _getPixelRatio(): # noqa: ANN205, N802 if PyQtImpl in ("PyQt4", "PySide"): # Qt4 seems not to provide any cross-platform means to get the # pixel ratio. - return 1. - else: - # Source: https://stackoverflow.com/a/40053864/3388962 - pos = QCursor.pos() - for screen in QApplication.screens(): - rect = screen.geometry() - if rect.contains(pos): - return screen.devicePixelRatio() - # Should never happen, but try to find a good fallback. - return QApplication.instance().devicePixelRatio() - - def _setEventInformation(self, x, y, ctrl, shift, - key, repeat=0, keysum=None): + return 1.0 + # Source: https://stackoverflow.com/a/40053864/3388962 + pos = QCursor.pos() + for screen in QApplication.screens(): + rect = screen.geometry() + if rect.contains(pos): + return screen.devicePixelRatio() + # Should never happen, but try to find a good fallback. + return QApplication.instance().devicePixelRatio() + + def _setEventInformation(self, x, y, ctrl, shift, key, repeat=0, keysum=None) -> None: # noqa: ANN001, N802, PLR0913 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): + def enterEvent(self, ev) -> None: # noqa: ANN001, N802, D102 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): + def leaveEvent(self, ev) -> None: # noqa: ANN001, N802, D102 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): + def mousePressEvent(self, ev) -> None: # noqa: ANN001, N802, D102 pos_x, pos_y = _get_event_pos(ev) ctrl, shift = self._GetCtrlShift(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() @@ -579,11 +574,10 @@ def mousePressEvent(self, ev): elif self._ActiveButton == MiddleButton: self._Iren.MiddleButtonPressEvent() - def mouseReleaseEvent(self, ev): + def mouseReleaseEvent(self, ev) -> None: # noqa: ANN001, N802, D102 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() @@ -592,7 +586,7 @@ def mouseReleaseEvent(self, ev): elif self._ActiveButton == MiddleButton: self._Iren.MiddleButtonReleaseEvent() - def mouseMoveEvent(self, ev): + def mouseMoveEvent(self, ev) -> None: # noqa: ANN001, N802, D102 pos_x, pos_y = _get_event_pos(ev) self.__saveModifiers = ev.modifiers() self.__saveButtons = ev.buttons() @@ -600,53 +594,50 @@ 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) + def keyPressEvent(self, ev) -> None: # noqa: ANN001, N802, D102 + key, keySym = self._GetKeyCharAndKeySym(ev) # noqa: N806 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) + def keyReleaseEvent(self, ev) -> None: # noqa: ANN001, N802, D102 + key, keySym = self._GetKeyCharAndKeySym(ev) # noqa: N806 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): - if hasattr(ev, 'delta'): + def wheelEvent(self, ev) -> None: # noqa: ANN001, N802, D102 + if hasattr(ev, "delta"): self.__wheelDelta += ev.delta() else: self.__wheelDelta += ev.angleDelta().y() - if self.__wheelDelta >= 120: + if self.__wheelDelta >= 120: # noqa: PLR2004 self._Iren.MouseWheelForwardEvent() self.__wheelDelta = 0 - elif self.__wheelDelta <= -120: + elif self.__wheelDelta <= -120: # noqa: PLR2004 self._Iren.MouseWheelBackwardEvent() self.__wheelDelta = 0 - def GetRenderWindow(self): + def GetRenderWindow(self): # noqa: ANN201, N802, D102 return self._RenderWindow - def Render(self): + def Render(self) -> None: # noqa: N802, D102 self.update() -def QVTKRenderWidgetConeExample(block=False): - """A simple example that uses the QVTKRenderWindowInteractor class.""" - +def QVTKRenderWidgetConeExample(block=False) -> None: # noqa: ANN001, FBT002, N802 + """A simple example that uses the QVTKRenderWindowInteractor class.""" # noqa: D401 from vtkmodules.vtkFiltersSources import vtkConeSource - from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper, vtkRenderer + from vtkmodules.vtkRenderingCore import vtkActor + from vtkmodules.vtkRenderingCore import vtkPolyDataMapper + from vtkmodules.vtkRenderingCore import vtkRenderer + # load implementations for rendering and interaction factory classes - import vtkmodules.vtkRenderingOpenGL2 - import vtkmodules.vtkInteractionStyle # every QT app needs an app app = QApplication.instance() @@ -659,7 +650,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()) # noqa: ARG005 ren = vtkRenderer() widget.GetRenderWindow().AddRenderer(ren) @@ -667,10 +658,10 @@ def QVTKRenderWidgetConeExample(block=False): cone = vtkConeSource() cone.SetResolution(8) - coneMapper = vtkPolyDataMapper() + coneMapper = vtkPolyDataMapper() # noqa: N806 coneMapper.SetInputConnection(cone.GetOutputPort()) - coneActor = vtkActor() + coneActor = vtkActor() # noqa: N806 coneActor.SetMapper(coneMapper) ren.AddActor(coneActor) @@ -693,123 +684,231 @@ 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', - Key.Key_Tab: 'Tab', - Key.Key_Backtab: 'Tab', + Key.Key_Backspace: "BackSpace", + Key.Key_Tab: "Tab", + Key.Key_Backtab: "Tab", # Key.Key_Clear : 'Clear', - Key.Key_Return: 'Return', - Key.Key_Enter: 'Return', - Key.Key_Shift: 'Shift_L', - Key.Key_Control: 'Control_L', - Key.Key_Alt: 'Alt_L', - Key.Key_Pause: 'Pause', - Key.Key_CapsLock: 'Caps_Lock', - Key.Key_Escape: 'Escape', - Key.Key_Space: 'space', + Key.Key_Return: "Return", + Key.Key_Enter: "Return", + Key.Key_Shift: "Shift_L", + Key.Key_Control: "Control_L", + Key.Key_Alt: "Alt_L", + Key.Key_Pause: "Pause", + Key.Key_CapsLock: "Caps_Lock", + Key.Key_Escape: "Escape", + Key.Key_Space: "space", # Key.Key_Prior : 'Prior', # Key.Key_Next : 'Next', - Key.Key_End: 'End', - Key.Key_Home: 'Home', - Key.Key_Left: 'Left', - Key.Key_Up: 'Up', - Key.Key_Right: 'Right', - Key.Key_Down: 'Down', - Key.Key_SysReq: 'Snapshot', - Key.Key_Insert: 'Insert', - Key.Key_Delete: 'Delete', - Key.Key_Help: 'Help', - Key.Key_0: '0', - Key.Key_1: '1', - Key.Key_2: '2', - Key.Key_3: '3', - Key.Key_4: '4', - Key.Key_5: '5', - Key.Key_6: '6', - Key.Key_7: '7', - Key.Key_8: '8', - Key.Key_9: '9', - Key.Key_A: 'a', - Key.Key_B: 'b', - Key.Key_C: 'c', - Key.Key_D: 'd', - Key.Key_E: 'e', - Key.Key_F: 'f', - Key.Key_G: 'g', - Key.Key_H: 'h', - Key.Key_I: 'i', - Key.Key_J: 'j', - Key.Key_K: 'k', - Key.Key_L: 'l', - Key.Key_M: 'm', - Key.Key_N: 'n', - Key.Key_O: 'o', - Key.Key_P: 'p', - Key.Key_Q: 'q', - Key.Key_R: 'r', - Key.Key_S: 's', - Key.Key_T: 't', - Key.Key_U: 'u', - Key.Key_V: 'v', - Key.Key_W: 'w', - Key.Key_X: 'x', - Key.Key_Y: 'y', - Key.Key_Z: 'z', - Key.Key_Asterisk: 'asterisk', - Key.Key_Plus: 'plus', - Key.Key_Minus: 'minus', - Key.Key_Period: 'period', - Key.Key_Slash: 'slash', - Key.Key_F1: 'F1', - Key.Key_F2: 'F2', - Key.Key_F3: 'F3', - Key.Key_F4: 'F4', - Key.Key_F5: 'F5', - Key.Key_F6: 'F6', - Key.Key_F7: 'F7', - Key.Key_F8: 'F8', - Key.Key_F9: 'F9', - Key.Key_F10: 'F10', - Key.Key_F11: 'F11', - Key.Key_F12: 'F12', - Key.Key_F13: 'F13', - Key.Key_F14: 'F14', - Key.Key_F15: 'F15', - Key.Key_F16: 'F16', - Key.Key_F17: 'F17', - Key.Key_F18: 'F18', - Key.Key_F19: 'F19', - Key.Key_F20: 'F20', - Key.Key_F21: 'F21', - Key.Key_F22: 'F22', - Key.Key_F23: 'F23', - Key.Key_F24: 'F24', - Key.Key_NumLock: 'Num_Lock', - Key.Key_ScrollLock: 'Scroll_Lock', - } + Key.Key_End: "End", + Key.Key_Home: "Home", + Key.Key_Left: "Left", + Key.Key_Up: "Up", + Key.Key_Right: "Right", + Key.Key_Down: "Down", + Key.Key_SysReq: "Snapshot", + Key.Key_Insert: "Insert", + Key.Key_Delete: "Delete", + Key.Key_Help: "Help", + Key.Key_0: "0", + Key.Key_1: "1", + Key.Key_2: "2", + Key.Key_3: "3", + Key.Key_4: "4", + Key.Key_5: "5", + Key.Key_6: "6", + Key.Key_7: "7", + Key.Key_8: "8", + Key.Key_9: "9", + Key.Key_A: "a", + Key.Key_B: "b", + Key.Key_C: "c", + Key.Key_D: "d", + Key.Key_E: "e", + Key.Key_F: "f", + Key.Key_G: "g", + Key.Key_H: "h", + Key.Key_I: "i", + Key.Key_J: "j", + Key.Key_K: "k", + Key.Key_L: "l", + Key.Key_M: "m", + Key.Key_N: "n", + Key.Key_O: "o", + Key.Key_P: "p", + Key.Key_Q: "q", + Key.Key_R: "r", + Key.Key_S: "s", + Key.Key_T: "t", + Key.Key_U: "u", + Key.Key_V: "v", + Key.Key_W: "w", + Key.Key_X: "x", + Key.Key_Y: "y", + Key.Key_Z: "z", + Key.Key_Asterisk: "asterisk", + Key.Key_Plus: "plus", + Key.Key_Minus: "minus", + Key.Key_Period: "period", + Key.Key_Slash: "slash", + Key.Key_F1: "F1", + Key.Key_F2: "F2", + Key.Key_F3: "F3", + Key.Key_F4: "F4", + Key.Key_F5: "F5", + Key.Key_F6: "F6", + Key.Key_F7: "F7", + Key.Key_F8: "F8", + Key.Key_F9: "F9", + Key.Key_F10: "F10", + Key.Key_F11: "F11", + Key.Key_F12: "F12", + Key.Key_F13: "F13", + Key.Key_F14: "F14", + Key.Key_F15: "F15", + Key.Key_F16: "F16", + Key.Key_F17: "F17", + Key.Key_F18: "F18", + Key.Key_F19: "F19", + Key.Key_F20: "F20", + Key.Key_F21: "F21", + Key.Key_F22: "F22", + Key.Key_F23: "F23", + Key.Key_F24: "F24", + Key.Key_NumLock: "Num_Lock", + Key.Key_ScrollLock: "Scroll_Lock", +} if __name__ == "__main__": - print(PyQtImpl) QVTKRenderWidgetConeExample() diff --git a/pyvistaqt/utils.py b/pyvistaqt/utils.py index f8c834a7..1b72fe98 100644 --- a/pyvistaqt/utils.py +++ b/pyvistaqt/utils.py @@ -1,23 +1,28 @@ -"""This module contains utilities routines.""" +"""This module contains utilities routines.""" # noqa: D404 -from typing import Any, List, Optional, Type +from __future__ import annotations + +from typing import Any +from typing import List +from typing import Optional +from typing import Type import pyvista -import scooby # type: ignore -from qtpy.QtWidgets import QApplication, QMenuBar +from qtpy.QtWidgets import QApplication +from qtpy.QtWidgets import QMenuBar +import scooby # type: ignore # noqa: PGH003 -def _check_type(var: Any, var_name: str, var_types: List[Type[Any]]) -> None: +def _check_type(var: Any, var_name: str, var_types: List[Type[Any]]) -> None: # noqa: ANN401 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." - ) + msg = f"Expected type for ``{var_name}`` is {types!s}" f" but {type(var)} was given." + raise TypeError(msg) -def _create_menu_bar(parent: Any) -> QMenuBar: - """Create a menu bar. +def _create_menu_bar(parent: Any) -> QMenuBar: # noqa: ANN401 + """ + Create a menu bar. The menu bar is expected to behave consistently for every operating system since `setNativeMenuBar(False)` @@ -31,7 +36,7 @@ def _create_menu_bar(parent: Any) -> QMenuBar: return menu_bar -def _setup_ipython(ipython: Any = None) -> Any: +def _setup_ipython(ipython: Any = None) -> Any: # noqa: ANN401 # ipython magic if scooby.in_ipython(): # pragma: no cover # pylint: disable=import-outside-toplevel diff --git a/pyvistaqt/window.py b/pyvistaqt/window.py index 02e94e5c..86c698d4 100644 --- a/pyvistaqt/window.py +++ b/pyvistaqt/window.py @@ -1,10 +1,14 @@ -"""This module contains a Qt-compatible MainWindow class.""" +"""This module contains a Qt-compatible MainWindow class.""" # noqa: D404 -from typing import Optional, Tuple +from __future__ import annotations + +from typing import Optional +from typing import Tuple from qtpy import QtCore from qtpy.QtCore import Signal -from qtpy.QtWidgets import QMainWindow, QWidget +from qtpy.QtWidgets import QMainWindow +from qtpy.QtWidgets import QWidget class MainWindow(QMainWindow): @@ -33,7 +37,7 @@ def event(self, event: QtCore.QEvent) -> bool: return True return super().event(event) - def closeEvent(self, event: QtCore.QEvent) -> None: # pylint: disable=invalid-name + def closeEvent(self, event: QtCore.QEvent) -> None: # pylint: disable=invalid-name # noqa: N802 """Manage the close event.""" self.signal_close.emit() event.accept() diff --git a/setup.py b/setup.py index eac04777..ee46a0ee 100644 --- a/setup.py +++ b/setup.py @@ -1,51 +1,51 @@ -""" -Installation file for python pyvistaqt module -""" +"""Installation file for python pyvistaqt module.""" + +from __future__ import annotations + import os from pathlib import Path -from io import open as io_open from setuptools import setup -package_name = 'pyvistaqt' -readme_file = Path(__file__).parent / 'README.rst' +package_name = "pyvistaqt" +readme_file = Path(__file__).parent / "README.rst" setup( name=package_name, packages=[package_name, package_name], - description='pyvista qt plotter', + description="pyvista qt plotter", long_description=readme_file.read_text(), - long_description_content_type='text/x-rst', - author='PyVista Developers', - author_email='info@pyvista.org', - license='MIT', + long_description_content_type="text/x-rst", + 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.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + "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.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], - - url='https://github.com/pyvista/pyvistaqt', - keywords='vtk numpy plotting mesh qt', - python_requires='>=3.7', + url="https://github.com/pyvista/pyvistaqt", + keywords="vtk numpy plotting mesh qt", + python_requires=">=3.7", setup_requires=["setuptools>=45", "setuptools_scm>=6.2"], use_scm_version={ "write_to": "pyvistaqt/_version.py", "version_scheme": "release-branch-semver", }, 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"), # noqa: PTH118 + ] + }, ) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..6e031999 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/tests/conftest.py b/tests/conftest.py index 3c3fdefe..274edc4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations # noqa: D100 + import gc import importlib import inspect @@ -6,117 +8,110 @@ import pytest import pyvista from pyvista.plotting import system_supports_plotting + import pyvistaqt NO_PLOTTING = not system_supports_plotting() -def pytest_configure(config): +def pytest_configure(config) -> None: """Configure pytest options.""" # Fixtures - for fixture in ('check_gc',): - config.addinivalue_line('usefixtures', fixture) + for fixture in ("check_gc",): + config.addinivalue_line("usefixtures", fixture) # Markers - for marker in ('allow_bad_gc', 'allow_bad_gc_pyside'): - config.addinivalue_line('markers', marker) + for marker in ("allow_bad_gc", "allow_bad_gc_pyside"): + config.addinivalue_line("markers", marker) # Adapted from PyVista -def _is_vtk(obj): +def _is_vtk(obj): # noqa: ANN202 try: - return obj.__class__.__name__.startswith('vtk') - except Exception: # old Python sometimes no __class__.__name__ + return obj.__class__.__name__.startswith("vtk") + except Exception: # old Python sometimes no __class__.__name__ # noqa: BLE001 return False -def _check_qt_installed(): +def _check_qt_installed() -> bool: try: - from qtpy import QtCore # noqa - except Exception: + from qtpy import QtCore # noqa: F401 + except Exception: # noqa: BLE001 return False else: return True @pytest.fixture(autouse=True) -def check_gc(request): +def check_gc(request): # noqa: ANN201, C901 """Ensure that all VTK objects are garbage-collected by Python.""" - if 'test_ipython' in request.node.name: # XXX this keeps a ref + if "test_ipython" in request.node.name: # XXX this keeps a ref # noqa: FIX003, TD001, TD002, TD003, TD004 yield return try: from qtpy import API_NAME - except Exception: - API_NAME = '' - marks = set(mark.name for mark in request.node.iter_markers()) - if 'allow_bad_gc' in marks: + except Exception: # noqa: BLE001 + API_NAME = "" # noqa: N806 + marks = {mark.name for mark in request.node.iter_markers()} + if "allow_bad_gc" in marks: yield return - if 'allow_bad_gc_pyside' in marks and API_NAME.lower().startswith('pyside'): + if "allow_bad_gc_pyside" in marks and API_NAME.lower().startswith("pyside"): yield return gc.collect() - before = set(id(o) for o in gc.get_objects() if _is_vtk(o)) + before = {id(o) for o in gc.get_objects() if _is_vtk(o)} yield pyvista.close_all() gc.collect() - after = [ - o - for o in gc.get_objects() - if _is_vtk(o) and id(o) not in before - ] - msg = 'Not all objects GCed:\n' + after = [o for o in gc.get_objects() if _is_vtk(o) and id(o) not in before] + msg = "Not all objects GCed:\n" for obj in after: cn = obj.__class__.__name__ cf = inspect.currentframe() - referrers = [ - v for v in gc.get_referrers(obj) - if v is not after and v is not cf - ] + referrers = [v for v in gc.get_referrers(obj) if v is not after and v is not cf] del cf for ri, referrer in enumerate(referrers): if isinstance(referrer, dict): for k, v in referrer.items(): if k is obj: - referrers[ri] = 'dict: d key' + referrers[ri] = "dict: d key" del k, v break elif v is obj: - referrers[ri] = f'dict: d[{k!r}]' - #raise RuntimeError(referrers[ri]) + referrers[ri] = f"dict: d[{k!r}]" + # raise RuntimeError(referrers[ri]) # noqa: ERA001 del k, v break del k, v else: - referrers[ri] = f'dict: len={len(referrer)}' + referrers[ri] = f"dict: len={len(referrer)}" else: referrers[ri] = repr(referrer) del ri, referrer - msg += f'{cn}: {referrers}\n' + msg += f"{cn}: {referrers}\n" del cn, referrers assert len(after) == 0, msg -@pytest.fixture() -def plotting(): +@pytest.fixture +def plotting() -> None: """Require plotting.""" if NO_PLOTTING: pytest.skip(NO_PLOTTING, reason="Requires system to support plotting") - yield -@pytest.fixture() -def no_qt(monkeypatch): +@pytest.fixture +def no_qt(monkeypatch): # noqa: ANN201 """Require plotting.""" 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 3614d796..f1b19009 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,42 +1,64 @@ +from __future__ import annotations # noqa: D100 + from contextlib import nullcontext import os import os.path as op -from packaging.version import Version import platform import re import sys import weakref 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.QtGui import QDragEnterEvent, QDropEvent -from qtpy.QtWidgets import (QTreeWidget, QStackedWidget, QCheckBox, - QGestureEvent, QPinchGesture) +from qtpy.QtCore import QMimeData +from qtpy.QtCore import QPoint +from qtpy.QtCore import QPointF +from qtpy.QtCore import Qt +from qtpy.QtCore import QUrl +from qtpy.QtGui import QDragEnterEvent +from qtpy.QtGui import QDropEvent +from qtpy.QtWidgets import QAction +from qtpy.QtWidgets import QCheckBox +from qtpy.QtWidgets import QFrame +from qtpy.QtWidgets import QGestureEvent +from qtpy.QtWidgets import QMenuBar +from qtpy.QtWidgets import QPinchGesture +from qtpy.QtWidgets import QStackedWidget +from qtpy.QtWidgets import QToolBar +from qtpy.QtWidgets import QTreeWidget +from qtpy.QtWidgets import QVBoxLayout +import vtk + from pyvistaqt.plotting import global_theme -from pyvista.plotting import Renderer + try: from pyvista.plotting.utilities import Scraper except ImportError: # PV < 0.40 from pyvista.utilities import Scraper 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 +from pyvistaqt import MainWindow +from pyvistaqt import MultiPlotter +from pyvistaqt import 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 +from pyvistaqt.plotting import QTimer +from pyvistaqt.plotting import QVTKRenderWindowInteractor +from pyvistaqt.utils import _check_type +from pyvistaqt.utils import _create_menu_bar +from pyvistaqt.utils import _setup_application PV_VERSION = Version(pyvista.__version__) -class TstWindow(MainWindow): - def __init__(self, parent=None, show=True, off_screen=True): +class TstWindow(MainWindow): # noqa: D101 + def __init__(self, parent=None, show=True, off_screen=True) -> None: # noqa: FBT002, D107 MainWindow.__init__(self, parent) self.frame = QFrame() @@ -51,17 +73,17 @@ def __init__(self, parent=None, show=True, off_screen=True): self.frame.setLayout(vlayout) self.setCentralWidget(self.frame) - mainMenu = _create_menu_bar(parent=self) + mainMenu = _create_menu_bar(parent=self) # noqa: N806 - fileMenu = mainMenu.addMenu('File') - self.exit_action = QAction('Exit', self) - self.exit_action.setShortcut('Ctrl+Q') + fileMenu = mainMenu.addMenu("File") # noqa: N806 + 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") # noqa: N806 + 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) @@ -70,25 +92,22 @@ def __init__(self, parent=None, show=True, off_screen=True): if show: self.show() - def add_sphere(self): - sphere = pyvista.Sphere( - phi_resolution=6, - theta_resolution=6 - ) + def add_sphere(self) -> None: # noqa: D102 + sphere = pyvista.Sphere(phi_resolution=6, theta_resolution=6) self.vtk_widget.add_mesh(sphere) self.vtk_widget.reset_camera() -def test_create_menu_bar(qtbot): +def test_create_menu_bar(qtbot) -> None: # noqa: D103 menu_bar = _create_menu_bar(parent=None) qtbot.addWidget(menu_bar) -def test_setup_application(qapp): +def test_setup_application(qapp) -> None: # noqa: D103 _setup_application(qapp) -def test_file_dialog(tmpdir, qtbot): +def test_file_dialog(tmpdir, qtbot) -> None: # noqa: D103 dialog = FileDialog( filefilter=None, directory=False, @@ -100,8 +119,8 @@ def test_file_dialog(tmpdir, qtbot): dialog.emit_accepted() # test no result p = tmpdir.mkdir("tmp").join("foo.png") - p.write('foo') - assert os.path.isfile(p) + p.write("foo") + assert os.path.isfile(p) # noqa: PTH113 filename = str(p) dialog.selectFile(filename) @@ -118,14 +137,14 @@ def test_file_dialog(tmpdir, qtbot): assert not dialog.isVisible() # dialog is closed after accept() -def test_check_type(): +def test_check_type() -> None: # noqa: D103 with pytest.raises(TypeError, match="Expected type"): _check_type(0, "foo", [str]) _check_type(0, "foo", [int, float]) _check_type("foo", "foo", [str]) -def test_mouse_interactions(qtbot): +def test_mouse_interactions(qtbot) -> None: # noqa: D103 plotter = BackgroundPlotter() window = plotter.app_window interactor = plotter.interactor @@ -136,19 +155,18 @@ def test_mouse_interactions(qtbot): plotter.close() -@pytest.mark.skipif(platform.system()=="Windows" and platform.python_version()[:-1]=="3.8.", reason="#51") -def test_ipython(qapp): - IPython = pytest.importorskip('IPython') - cmd = "from pyvistaqt import BackgroundPlotter as Plotter;" \ - "p = Plotter(show=False, off_screen=False); p.close(); exit()" +@pytest.mark.skipif(platform.system() == "Windows" and platform.python_version()[:-1] == "3.8.", reason="#51") +def test_ipython(qapp) -> None: # noqa: ARG001, D103 + IPython = pytest.importorskip("IPython") # noqa: N806 + cmd = "from pyvistaqt import BackgroundPlotter as Plotter;" "p = Plotter(show=False, off_screen=False); p.close(); exit()" IPython.start_ipython(argv=["-c", cmd]) -class SuperWindow(MainWindow): +class SuperWindow(MainWindow): # noqa: D101 pass -def test_depth_peeling(qtbot): +def test_depth_peeling(qtbot) -> None: # noqa: D103 plotter = BackgroundPlotter() qtbot.addWidget(plotter.app_window) assert not plotter.renderer.GetUseDepthPeeling() @@ -163,7 +181,7 @@ def test_depth_peeling(qtbot): global_theme.depth_peeling["enabled"] = False -def test_off_screen(qtbot): +def test_off_screen(qtbot) -> None: # noqa: D103 plotter = BackgroundPlotter(off_screen=False) qtbot.addWidget(plotter.app_window) assert not plotter.ren_win.GetOffScreenRendering() @@ -174,7 +192,7 @@ def test_off_screen(qtbot): plotter.close() -def test_smoothing(qtbot): +def test_smoothing(qtbot) -> None: # noqa: D103 plotter = BackgroundPlotter() qtbot.addWidget(plotter.app_window) assert not plotter.ren_win.GetPolygonSmoothing() @@ -193,10 +211,10 @@ def test_smoothing(qtbot): plotter.close() -def test_counter(qtbot): - with pytest.raises(TypeError, match='type of'): +def test_counter(qtbot) -> None: # noqa: D103 + 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) @@ -206,15 +224,15 @@ def test_counter(qtbot): assert counter.count == 0 -# TODO: Fix gc on PySide6 -@pytest.mark.parametrize('border', (True, False)) +# TODO: Fix gc on PySide6 # noqa: FIX002, TD002, TD003 +@pytest.mark.parametrize("border", [True, False]) @pytest.mark.allow_bad_gc_pyside -def test_subplot_gc(border): +def test_subplot_gc(border) -> None: # noqa: D103 BackgroundPlotter(shape=(2, 1), update_app_icon=False, border=border) @pytest.mark.allow_bad_gc_pyside -def test_editor(qtbot, plotting): +def test_editor(qtbot, plotting) -> None: # noqa: ARG001, D103 # test editor=False plotter = BackgroundPlotter(editor=False, off_screen=False) qtbot.addWidget(plotter.app_window) @@ -280,22 +298,23 @@ def test_editor(qtbot, plotting): plotter.close() -@pytest.fixture() -def ensure_closed(): +@pytest.fixture +def ensure_closed(): # noqa: ANN201 """Ensure all plotters are closed.""" try: from pyvista.plotting import close_all from pyvista.plotting.plotter import _ALL_PLOTTERS except ImportError: # PV < 0.40 - from pyvista.plotting.plotting import _ALL_PLOTTERS, close_all + from pyvista.plotting.plotting import _ALL_PLOTTERS + from pyvista.plotting.plotting import close_all close_all() # this is necessary to test _ALL_PLOTTERS assert len(_ALL_PLOTTERS) == 0 yield - WANT_AFTER = 0 if PV_VERSION >= Version('0.37') else 1 + WANT_AFTER = 0 if Version("0.37") <= PV_VERSION else 1 # noqa: N806 assert len(_ALL_PLOTTERS) == WANT_AFTER -def test_qt_interactor(qtbot, plotting, ensure_closed): +def test_qt_interactor(qtbot, plotting, ensure_closed) -> None: # noqa: ARG001, D103 window = TstWindow(show=False, off_screen=False) qtbot.addWidget(window) # register the main widget @@ -332,7 +351,7 @@ def test_qt_interactor(qtbot, plotting, ensure_closed): assert window.isVisible() assert interactor.isVisible() assert render_timer.isActive() - assert not vtk_widget._closed + assert not vtk_widget._closed # noqa: SLF001 # test enable/disable interactivity vtk_widget.disable() @@ -347,21 +366,20 @@ def test_qt_interactor(qtbot, plotting, ensure_closed): 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 - - -@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' - ) + assert vtk_widget._closed # noqa: SLF001 + + +@pytest.mark.parametrize( + "show_plotter", + [ + True, + False, + ], +) +def test_background_plotting_axes_scale(qtbot, show_plotter, plotting) -> None: # noqa: ARG001, D103 + 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 @@ -376,7 +394,7 @@ def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): plotter.add_mesh(pyvista.Sphere()) assert_hasattr(plotter, "renderer", Renderer) renderer = plotter.renderer - assert len(renderer._actors) == 1 + assert len(renderer._actors) == 1 # noqa: SLF001 assert np.any(plotter.mesh.points) dlg = plotter.scale_axes_dialog(show=False) # ScaleAxesDialog @@ -394,9 +412,9 @@ def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): dlg.x_slider_group.spinbox.setValue(-1) assert dlg.x_slider_group.value == 0 dlg.x_slider_group.spinbox.setValue(1000.0) - assert dlg.x_slider_group.value < 100 + assert dlg.x_slider_group.value < 100 # noqa: PLR2004 - plotter._last_update_time = 0.0 + plotter._last_update_time = 0.0 # noqa: SLF001 plotter.update() plotter.update_app_icon() plotter.close() @@ -404,8 +422,8 @@ def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): assert not dlg.isVisible() -def test_background_plotting_camera(qtbot, plotting): - plotter = BackgroundPlotter(off_screen=False, title='Testing Window') +def test_background_plotting_camera(qtbot, plotting) -> None: # noqa: ARG001, D103 + 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)] @@ -420,20 +438,19 @@ def test_background_plotting_camera(qtbot, plotting): plotter.clear_camera_positions() # 2 because the first two buttons are save and clear - assert len(plotter.saved_cameras_tool_bar.actions()) == 2 + assert len(plotter.saved_cameras_tool_bar.actions()) == 2 # noqa: PLR2004 plotter.close() -@pytest.mark.parametrize('other_views', [None, 0, [0]]) -def test_link_views_across_plotters(other_views): - - def _to_array(camera_position): +@pytest.mark.parametrize("other_views", [None, 0, [0]]) +def test_link_views_across_plotters(other_views) -> None: # noqa: D103 + def _to_array(camera_position): # noqa: ANN202 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) @@ -459,25 +476,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, - ]) -def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): +@pytest.mark.parametrize( + "show_plotter", + [ + True, + False, + ], +) +def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting) -> None: # noqa: ARG001, D103 # setup filesystem output_dir = str(tmpdir.mkdir("tmpdir")) - assert os.path.isdir(output_dir) + assert os.path.isdir(output_dir) # noqa: PTH112 - 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 @@ -492,13 +508,13 @@ def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): plotter.add_mesh(pyvista.Sphere()) assert_hasattr(plotter, "renderer", Renderer) renderer = plotter.renderer - assert len(renderer._actors) == 1 + assert len(renderer._actors) == 1 # noqa: SLF001 assert np.any(plotter.mesh.points) - dlg = plotter._qt_screenshot(show=False) # FileDialog + dlg = plotter._qt_screenshot(show=False) # FileDialog # noqa: SLF001 qtbot.addWidget(dlg) # register the dialog - filename = str(os.path.join(output_dir, "tmp.png")) + filename = str(os.path.join(output_dir, "tmp.png")) # noqa: PTH118 dlg.selectFile(filename) # show the dialog @@ -514,21 +530,17 @@ def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): plotter.close() assert not window.isVisible() - assert os.path.isfile(filename) + assert os.path.isfile(filename) # noqa: PTH113 @pytest.mark.skip @pytest.mark.allow_bad_gc -def test_background_plotter_export_vtkjs(qtbot, tmpdir, plotting): +def test_background_plotter_export_vtkjs(qtbot, tmpdir, plotting) -> None: # noqa: ARG001, D103 # setup filesystem output_dir = str(tmpdir.mkdir("tmpdir")) - assert os.path.isdir(output_dir) + assert os.path.isdir(output_dir) # noqa: PTH112 - plotter = BackgroundPlotter( - show=False, - off_screen=False, - title='Testing Window' - ) + plotter = BackgroundPlotter(show=False, off_screen=False, title="Testing Window") assert_hasattr(plotter, "app_window", MainWindow) window = plotter.app_window # MainWindow qtbot.addWidget(window) # register the window @@ -543,18 +555,18 @@ def test_background_plotter_export_vtkjs(qtbot, tmpdir, plotting): plotter.add_mesh(pyvista.Sphere()) assert_hasattr(plotter, "renderer", Renderer) renderer = plotter.renderer - assert len(renderer._actors) == 1 + assert len(renderer._actors) == 1 # noqa: SLF001 assert np.any(plotter.mesh.points) - dlg = plotter._qt_export_vtkjs(show=False) # FileDialog + dlg = plotter._qt_export_vtkjs(show=False) # FileDialog # noqa: SLF001 qtbot.addWidget(dlg) # register the dialog - if hasattr(plotter, 'export_vtksz'): - ext = '.vtksz' - filename = str(os.path.join(output_dir, f"tmp{ext}")) + if hasattr(plotter, "export_vtksz"): + ext = ".vtksz" + filename = str(os.path.join(output_dir, f"tmp{ext}")) # noqa: PTH118 else: - ext = '.vtkjs' - filename = str(os.path.join(output_dir, f"tmp")) + ext = ".vtkjs" + filename = str(os.path.join(output_dir, "tmp")) # noqa: PTH118 dlg.selectFile(filename) # show the dialog @@ -571,17 +583,17 @@ def test_background_plotter_export_vtkjs(qtbot, tmpdir, plotting): plotter.close() assert not window.isVisible() - if hasattr(plotter, 'export_vtksz'): - assert os.path.isfile(filename) + if hasattr(plotter, "export_vtksz"): + assert os.path.isfile(filename) # noqa: PTH113 else: - assert os.path.isfile(filename + ext) + assert os.path.isfile(filename + ext) # noqa: PTH113 # vtkWeakReference and vtkFloatArray, only sometimes -- usually PySide2 # but also sometimes macOS @pytest.mark.allow_bad_gc -def test_background_plotting_orbit(qtbot, plotting): - plotter = BackgroundPlotter(off_screen=False, title='Testing Window') +def test_background_plotting_orbit(qtbot, plotting) -> None: # noqa: ARG001, D103 + 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) @@ -589,8 +601,8 @@ def test_background_plotting_orbit(qtbot, plotting): @pytest.mark.skipif(sys.version_info < (3, 10), reason="#508") -def test_background_plotting_toolbar(qtbot, plotting): - with pytest.raises(TypeError, match='toolbar'): +def test_background_plotting_toolbar(qtbot, plotting) -> None: # noqa: ARG001, D103 + with pytest.raises(TypeError, match="toolbar"): # noqa: PT012 p = BackgroundPlotter(off_screen=False, toolbar="foo") p.close() @@ -618,26 +630,24 @@ def test_background_plotting_toolbar(qtbot, plotting): assert saved_cameras_tool_bar.isVisible() # triggering a view action - plotter._view_action.trigger() + plotter._view_action.trigger() # noqa: SLF001 plotter.close() -# TODO: _render_passes not GC'ed +# TODO: _render_passes not GC'ed # noqa: FIX002, TD002, TD003 @pytest.mark.allow_bad_gc_pyside -@pytest.mark.skipif( - platform.system() == 'Windows', reason='Segfaults on Windows') -def test_background_plotting_menu_bar(qtbot, plotting): - with pytest.raises(TypeError, match='menu_bar'): +@pytest.mark.skipif(platform.system() == "Windows", reason="Segfaults on Windows") +def test_background_plotting_menu_bar(qtbot, plotting) -> None: # noqa: ARG001, D103 + with pytest.raises(TypeError, match="menu_bar"): BackgroundPlotter(off_screen=False, menu_bar="foo") - plotter = BackgroundPlotter( - off_screen=False, menu_bar=False, update_app_icon=False) + plotter = BackgroundPlotter(off_screen=False, menu_bar=False, update_app_icon=False) assert plotter.main_menu is None - assert plotter._menu_close_action is None + assert plotter._menu_close_action is None # noqa: SLF001 plotter.close() - # menu_bar=True + # menu_bar=True # noqa: ERA001 plotter = BackgroundPlotter(off_screen=False, update_app_icon=False) assert_hasattr(plotter, "app_window", MainWindow) @@ -654,35 +664,35 @@ def test_background_plotting_menu_bar(qtbot, plotting): window.show() # EDL action - if hasattr(plotter.renderer, '_render_passes'): - obj, attr = plotter.renderer._render_passes, '_edl_pass' + if hasattr(plotter.renderer, "_render_passes"): + obj, attr = plotter.renderer._render_passes, "_edl_pass" # noqa: SLF001 else: - obj, attr = plotter.renderer, 'edl_pass' + obj, attr = plotter.renderer, "edl_pass" assert getattr(obj, attr, None) is None - plotter._edl_action.trigger() + plotter._edl_action.trigger() # noqa: SLF001 assert getattr(obj, attr, None) is not None # and now test reset - plotter._edl_action.trigger() + plotter._edl_action.trigger() # noqa: SLF001 # Parallel projection action assert not plotter.camera.GetParallelProjection() - plotter._parallel_projection_action.trigger() + plotter._parallel_projection_action.trigger() # noqa: SLF001 assert plotter.camera.GetParallelProjection() # and now test reset - plotter._parallel_projection_action.trigger() + plotter._parallel_projection_action.trigger() # noqa: SLF001 assert main_menu.isVisible() plotter.close() assert not main_menu.isVisible() - assert plotter._last_update_time == -np.inf + assert plotter._last_update_time == -np.inf # noqa: SLF001 -def test_drop_event(tmpdir, qtbot): +def test_drop_event(tmpdir, qtbot) -> None: # noqa: D103 output_dir = str(tmpdir.mkdir("tmpdir")) - filename = str(os.path.join(output_dir, "tmp.vtk")) + filename = str(os.path.join(output_dir, "tmp.vtk")) # noqa: PTH118 mesh = pyvista.Cone() mesh.save(filename) - assert os.path.isfile(filename) + assert os.path.isfile(filename) # noqa: PTH113 plotter = BackgroundPlotter(update_app_icon=False) with qtbot.wait_exposed(plotter.app_window, timeout=10000): plotter.app_window.show() @@ -700,12 +710,12 @@ def test_drop_event(tmpdir, qtbot): plotter.close() -def test_drag_event(tmpdir): +def test_drag_event(tmpdir) -> None: # noqa: D103 output_dir = str(tmpdir.mkdir("tmpdir")) - filename = str(os.path.join(output_dir, "tmp.vtk")) + filename = str(os.path.join(output_dir, "tmp.vtk")) # noqa: PTH118 mesh = pyvista.Cone() mesh.save(filename) - assert os.path.isfile(filename) + assert os.path.isfile(filename) # noqa: PTH113 plotter = BackgroundPlotter(update_app_icon=False) point = QPoint(0, 0) data = QMimeData() @@ -721,7 +731,7 @@ def test_drag_event(tmpdir): plotter.close() -def test_gesture_event(qtbot): +def test_gesture_event(qtbot) -> None: # noqa: D103 plotter = BackgroundPlotter(update_app_icon=False) with qtbot.wait_exposed(plotter.app_window, timeout=10000): plotter.app_window.show() @@ -731,38 +741,38 @@ def test_gesture_event(qtbot): plotter.close() -def test_background_plotting_add_callback(qtbot, monkeypatch, plotting): - class CallBack(object): - def __init__(self, sphere): +def test_background_plotting_add_callback(qtbot, monkeypatch, plotting) -> None: # noqa: ARG001, D103 + class CallBack: + def __init__(self, sphere) -> None: self.sphere = weakref.ref(sphere) - def __call__(self): + def __call__(self): # noqa: ANN204 self.sphere().points[:] = self.sphere().points * 0.5 update_count = [0] orig_update_app_icon = BackgroundPlotter.update_app_icon - def update_app_icon(slf): + def update_app_icon(slf): # noqa: ANN202 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) assert_hasattr(plotter, "_callback_timer", QTimer) assert_hasattr(plotter, "counters", list) - assert plotter._last_update_time == -np.inf + assert plotter._last_update_time == -np.inf # noqa: SLF001 sphere = pyvista.Sphere() plotter.add_mesh(sphere) mycallback = CallBack(sphere) window = plotter.app_window # MainWindow - callback_timer = plotter._callback_timer # QTimer + callback_timer = plotter._callback_timer # QTimer # noqa: SLF001 assert callback_timer.isActive() # ensure that the window is showed @@ -777,18 +787,16 @@ 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")) # noqa: PTH118, PTH120 callback_timer.stop() assert not callback_timer.isActive() # check that timers are set properly in add_callback() plotter.add_callback(mycallback, interval=200, count=3) - callback_timer = plotter._callback_timer # QTimer + callback_timer = plotter._callback_timer # QTimer # noqa: SLF001 assert callback_timer.isActive() counter = plotter.counters[-1] # Counter @@ -801,7 +809,7 @@ def update_app_icon(slf): assert not callback_timer.isActive() # counter stops the callback plotter.add_callback(mycallback, interval=200) - callback_timer = plotter._callback_timer # QTimer + callback_timer = plotter._callback_timer # QTimer # noqa: SLF001 assert callback_timer.isActive() # ensure that self.callback_timer send a signal @@ -812,32 +820,36 @@ def update_app_icon(slf): assert not callback_timer.isActive() # window stops the callback -def allow_bad_gc_old_pyvista(func): - if Version(pyvista.__version__) < Version('0.37'): +def allow_bad_gc_old_pyvista(func): # noqa: ANN201, D103 + if Version(pyvista.__version__) < Version("0.37"): return pytest.mark.allow_bad_gc(func) - else: - return func + return func -# TODO: Need to fix this allow_bad_gc: +# TODO: Need to fix this allow_bad_gc: # noqa: FIX002, TD002, TD003 # - the actors are not cleaned up in the non-empty scene case # - the q_key_press leaves a lingering vtkUnsignedCharArray referred to by # a "managedbuffer" object @allow_bad_gc_old_pyvista @pytest.mark.allow_bad_gc_pyside -@pytest.mark.parametrize('close_event', [ - "plotter_close", - "window_close", - pytest.param("q_key_press", marks=pytest.mark.allow_bad_gc), - "menu_exit", - "del_finalizer", - ]) -@pytest.mark.parametrize('empty_scene', [ - True, - False, - ]) -def test_background_plotting_close(qtbot, close_event, empty_scene, plotting, - ensure_closed): +@pytest.mark.parametrize( + "close_event", + [ + "plotter_close", + "window_close", + pytest.param("q_key_press", marks=pytest.mark.allow_bad_gc), + "menu_exit", + "del_finalizer", + ], +) +@pytest.mark.parametrize( + "empty_scene", + [ + True, + False, + ], +) +def test_background_plotting_close(qtbot, close_event, empty_scene, plotting, ensure_closed) -> None: # noqa: ARG001, D103 plotter = _create_testing_scene(empty_scene) # check that BackgroundPlotter.__init__() is called @@ -874,7 +886,7 @@ def test_background_plotting_close(qtbot, close_event, empty_scene, plotting, assert interactor.isVisible() assert main_menu.isVisible() assert render_timer.isActive() - assert not plotter._closed + assert not plotter._closed # noqa: SLF001 with qtbot.wait_signals([window.signal_close], timeout=500): if close_event == "plotter_close": @@ -884,7 +896,7 @@ def test_background_plotting_close(qtbot, close_event, empty_scene, plotting, elif close_event == "q_key_press": qtbot.keyClick(interactor, "q") elif close_event == "menu_exit": - plotter._menu_close_action.trigger() + plotter._menu_close_action.trigger() # noqa: SLF001 elif close_event == "del_finalizer": plotter.__del__() @@ -895,43 +907,43 @@ 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 + assert plotter._closed # noqa: SLF001 -def test_multiplotter(qtbot, plotting): +def test_multiplotter(qtbot, plotting) -> None: # noqa: ARG001, D103 mp = MultiPlotter( nrows=1, ncols=2, window_size=(300, 300), show=False, - title='Test', + title="Test", off_screen=False, ) - qtbot.addWidget(mp._window) + qtbot.addWidget(mp._window) # noqa: SLF001 mp[0, 0].add_mesh(pyvista.Cone()) mp[0, 1].add_mesh(pyvista.Box()) - assert not mp._window.isVisible() - with qtbot.wait_exposed(mp._window): + assert not mp._window.isVisible() # noqa: SLF001 + with qtbot.wait_exposed(mp._window): # noqa: SLF001 mp.show() - assert mp._window.isVisible() - for p in mp._plotters: - assert not p._closed - with qtbot.wait_signals([mp._window.signal_close], timeout=1000): + assert mp._window.isVisible() # noqa: SLF001 + for p in mp._plotters: # noqa: SLF001 + assert not p._closed # noqa: SLF001 + with qtbot.wait_signals([mp._window.signal_close], timeout=1000): # noqa: SLF001 mp.close() - for p in mp._plotters: - assert p._closed + for p in mp._plotters: # noqa: SLF001 + assert p._closed # noqa: SLF001 # cover default show=True mp = MultiPlotter(off_screen=False, menu_bar=False, toolbar=False) - qtbot.addWidget(mp._window) - with qtbot.wait_exposed(mp._window): - assert mp._window.isVisible() + qtbot.addWidget(mp._window) # noqa: SLF001 + with qtbot.wait_exposed(mp._window): # noqa: SLF001 + assert mp._window.isVisible() # noqa: SLF001 mp.close() -def _create_testing_scene(empty_scene, show=False, off_screen=False): +def _create_testing_scene(empty_scene, show=False, off_screen=False): # noqa: ANN202, FBT002 if empty_scene: plotter = BackgroundPlotter( show=show, @@ -943,92 +955,83 @@ 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 -def assert_hasattr(variable, attribute_name, variable_type): +def assert_hasattr(variable, attribute_name, variable_type) -> None: # noqa: D103 __tracebackhide__ = True assert hasattr(variable, attribute_name) assert isinstance(getattr(variable, attribute_name), variable_type) -@pytest.mark.parametrize('n_win', [1, 2]) -def test_sphinx_gallery_scraping(qtbot, monkeypatch, plotting, tmpdir, n_win): - pytest.importorskip('sphinx_gallery') - if Version('0.38.0') <= PV_VERSION <= Version('0.38.6'): - pytest.xfail('Scraping fails on PyVista 0.38.0 to 0.38.6') - monkeypatch.setattr(pyvista, 'BUILDING_GALLERY', True) +@pytest.mark.parametrize("n_win", [1, 2]) +def test_sphinx_gallery_scraping(qtbot, monkeypatch, plotting, tmpdir, n_win) -> None: # noqa: ARG001, D103 + pytest.importorskip("sphinx_gallery") + if Version("0.38.0") <= PV_VERSION <= Version("0.38.6"): + pytest.xfail("Scraping fails on PyVista 0.38.0 to 0.38.6") + monkeypatch.setattr(pyvista, "BUILDING_GALLERY", True) - plotters = [ - BackgroundPlotter(off_screen=False, editor=False, show=True) - for _ in range(n_win) - ] + plotters = [BackgroundPlotter(off_screen=False, editor=False, show=True) for _ in range(n_win)] # Adapted from pyvista/tests/test_scraper.py scraper = Scraper() src_dir = str(tmpdir) - out_dir = op.join(str(tmpdir), '_build', 'html') - img_fnames = [ - op.join(src_dir, 'auto_examples', 'images', f'sg_img_{n}.png') - for n in range(n_win) - ] + out_dir = op.join(str(tmpdir), "_build", "html") # noqa: PTH118 + img_fnames = [op.join(src_dir, "auto_examples", "images", f"sg_img_{n}.png") for n in range(n_win)] # noqa: PTH118 gallery_conf = {"src_dir": src_dir, "builder_name": "html"} - target_file = op.join(src_dir, 'auto_examples', 'sg.py') + target_file = op.join(src_dir, "auto_examples", "sg.py") # noqa: PTH118 block = None - block_vars = dict( - image_path_iterator=(img for img in img_fnames), - example_globals=dict(a=1), - target_file=target_file, - ) - os.makedirs(op.dirname(img_fnames[0])) + block_vars = { + "image_path_iterator": (img for img in img_fnames), + "example_globals": {"a": 1}, + "target_file": target_file, + } + os.makedirs(op.dirname(img_fnames[0])) # noqa: PTH103, PTH120 for img_fname in img_fnames: - assert not os.path.isfile(img_fname) - os.makedirs(out_dir) + assert not os.path.isfile(img_fname) # noqa: PTH113 + os.makedirs(out_dir) # noqa: PTH103 scraper(block, block_vars, gallery_conf) for img_fname in img_fnames: - assert os.path.isfile(img_fname) + assert os.path.isfile(img_fname) # noqa: PTH113 for plotter in plotters: plotter.close() @pytest.mark.skipif(sys.version_info < (3, 10), reason="#508") -@pytest.mark.parametrize("aa", [ - False, - "fxaa", - "msaa", - pytest.param( - "ssaa", - marks=pytest.mark.xfail( - reason="SSAA broken on multiple plots", - strict=True +@pytest.mark.parametrize( + "aa", + [ + False, + "fxaa", + "msaa", + pytest.param( + "ssaa", + marks=pytest.mark.xfail(reason="SSAA broken on multiple plots", strict=True), ), - ), -]) -def test_background_plotting_plots(qtbot, plotting, ensure_closed, aa): + ], +) +def test_background_plotting_plots(qtbot, plotting, ensure_closed, aa) -> None: # noqa: ARG001, C901, D103 plotter = BackgroundPlotter( show=True, off_screen=False, @@ -1041,7 +1044,7 @@ def test_background_plotting_plots(qtbot, plotting, ensure_closed, aa): ) skip_reason = None if aa == "fxaa": # Breaks on Windows and mesa - if platform.system()=="Windows": + if platform.system() == "Windows": skip_reason = "FXAA segfaults Windows" else: # Check if Mesa @@ -1053,9 +1056,8 @@ def test_background_plotting_plots(qtbot, plotting, ensure_closed, aa): skip_reason = "FXAA broken on Mesa" elif aa == "msaa": pytest.importorskip("pyvista", minversion="0.37") - elif aa == "ssaa": - if sys.platform == "darwin": - pytest.skip("Works sometimes on Darwin") + elif aa == "ssaa" and sys.platform == "darwin": + pytest.skip("Works sometimes on Darwin") if skip_reason: plotter.close() pytest.skip(skip_reason) @@ -1068,19 +1070,16 @@ def test_background_plotting_plots(qtbot, plotting, ensure_closed, aa): plotter.camera.zoom(5) # fill it if aa: plotter.enable_anti_aliasing(aa_type=aa) - if platform.system() != "macOS": - ctx = qtbot.wait_exposed(plotter) - else: - ctx = nullcontext() + ctx = qtbot.wait_exposed(plotter) if platform.system() != "macOS" else nullcontext() with ctx: plotter.window().show() img = np.array(plotter.image) non_black = img.any(-1).astype(bool).mean() del img - # TODO: This is possibly a bug indicative of the view being wrong + # TODO: This is possibly a bug indicative of the view being wrong # noqa: FIX002, TD002, TD003 if sys.platform == "darwin" and platform.machine() == "arm64": - ratio = 2. + ratio = 2.0 else: - ratio = 1. - assert 0.9 / ratio < non_black < 1. / ratio + ratio = 1.0 + assert 0.9 / ratio < non_black < 1.0 / ratio plotter.close() diff --git a/tests/test_qt.py b/tests/test_qt.py index abb74984..4577a011 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -1,8 +1,14 @@ +from __future__ import annotations # noqa: D100 + import pytest -def test_no_qt_binding(no_qt): - from pyvistaqt import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor +def test_no_qt_binding(no_qt) -> None: # noqa: ARG001, D103 + from pyvistaqt import BackgroundPlotter + from pyvistaqt import MainWindow + from pyvistaqt import MultiPlotter + from pyvistaqt import QtInteractor + with pytest.raises(RuntimeError, match="No Qt binding"): BackgroundPlotter() with pytest.raises(RuntimeError, match="No Qt binding"):