diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..11082d25 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ +[codespell] +skip = *.pyc,*.txt,*.gif,*.png,*.jpg,*.ply,*.vtk,*.vti,*.vtu,*.js,*.html,*.doctree,*.ttf,*.woff,*.woff2,*.eot,*.mp4,*.inv,*.pickle,*.ipynb,flycheck*,./.git/*,./.hypothesis/*,*.yml,doc/_build/*,./doc/images/*,./dist/*,*~,.hypothesis*,./doc/examples/*,*.mypy_cache/*,*cover,./tests/tinypages/_build/*,*/_autosummary/* +ignore-words-list = lod,byteorder,flem,parm,doubleclick,revered,PullRequest +quiet-level = 3 \ No newline at end of file diff --git a/.coveragerc b/.coveragerc index 5e6ab884..ef06fc87 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ source = pyvistaqt omit = */setup.py */pyvistaqt/rwi.py + */py.typed [report] exclude_lines = diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..3b5a96aa --- /dev/null +++ b/.flake8 @@ -0,0 +1,36 @@ +[flake8] +max-line-length = 100 +exclude = + __pycache__, + .venv, + .cache, + .eggs + .git, + .tox, + *.egg-info, + *.pyc, + *.pyi, + build, + dist, + # This is adopted from VTK + pyvistaqt/rwi.py, +max-complexity = 10 +doctests = true +extend-ignore = + # whitespace before ':' + E203, + # line break before binary operator + W503, + # line length too long + E501, + # do not assign a lambda expression, use a def + E731, + # missing class docstring; use ``__init__`` docstring instead + D101, + # missing docstring in magic method + D105, + # Qt uses camelCase + N802 +per-file-ignores = + # "Imported but unused: happens with packages + __init__.py:F401 \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index ba2778dc..00000000 --- a/.isort.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[settings] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..153d0aea --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,64 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + +- repo: https://gitlab.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: [ + "flake8-black==0.3.2", + "flake8-isort==4.1.1", + "flake8-quotes==3.3.1", + ] + +- repo: https://github.com/codespell-project/codespell + rev: v2.1.0 + hooks: + - id: codespell + args: [ + "doc examples examples_flask pyvista tests", + "*.py *.rst *.md", + ] + +- repo: https://github.com/pycqa/pydocstyle + rev: 6.1.1 + hooks: + - id: pydocstyle + additional_dependencies: [toml==0.10.2] + files: ^(pyvista/|other/) + exclude: ^pyvista/ext/ + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: no-commit-to-branch + args: [--branch, main] + +# this validates our github workflow files +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.16.2 + 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 + ] \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index a0629152..876e6f32 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,18 +3,24 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. -extension-pkg-whitelist=vtk +extension-pkg-whitelist=vtk, + PyQt5, + PyQt6, + PySide2, + PySide6 # Specify a score threshold to be exceeded before program exits with error. fail-under=10 # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS +ignore=rwi.py, + conf.py, + conftest.py # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns= +ignore-patterns=test_.*[.]py # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -60,17 +66,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, +disable=raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, @@ -78,71 +74,13 @@ disable=print-statement, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - bad-continuation, arguments-differ, no-name-in-module, - no-member + no-member, + # Redundant alias imports required for type hinting by PEP 484 + useless-import-alias, + # Qt uses PascalCase + invalid-name, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -446,8 +384,11 @@ max-module-lines=1000 # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator +# +# Removed in version 2.15 +# https://pylint.pycqa.org/en/latest/whatsnew/2/2.6/summary.html#summary-release-highlights +; no-space-check=trailing-comma, +; dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. diff --git a/docs/conf.py b/docs/conf.py index 443edd55..64b9850f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,38 +1,47 @@ +"""Configuration file for sphinx documentation.""" +from __future__ import annotations + import datetime import os import sys +import warnings +from typing import Optional + +import numpy as np +import pyvista +import sphinx_rtd_theme +from docutils.parsers.rst import directives +from sphinx.ext.autosummary import Autosummary, get_documenter +from sphinx.util.inspect import safe_getattr + +import pyvistaqt if sys.version_info >= (3, 0): import faulthandler + faulthandler.enable() sys.path.insert(0, os.path.abspath('.')) - -import numpy as np -# -- pyvista configuration --------------------------------------------------- -import pyvista - -import pyvistaqt - # Manage errors pyvista.set_error_output_file('errors.txt') + # Ensure that offscreen rendering is used for docs generation -pyvista.OFF_SCREEN = True # Not necessary - simply an insurance policy +pyvista.OFF_SCREEN = True # Not necessary - simply an insurance policy pyvista.BUILDING_GALLERY = True + # Preferred plotting style for documentation pyvista.set_plot_theme('document') pyvista.rcParams['window_size'] = np.array([1024, 768]) * 2 + # 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) -# SG warnings -import warnings warnings.filterwarnings( - "ignore", + 'ignore', category=UserWarning, message='Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.', ) @@ -45,16 +54,17 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.doctest', - 'sphinx.ext.autosummary', - 'notfound.extension', - 'sphinx_copybutton', - 'sphinx.ext.extlinks', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx' - ] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.doctest', + 'sphinx.ext.autosummary', + 'notfound.extension', + 'sphinx_copybutton', + 'sphinx.ext.extlinks', + 'sphinx.ext.coverage', + 'sphinx.ext.intersphinx', +] linkcheck_retries = 3 @@ -70,10 +80,14 @@ master_doc = 'index' # General information about the project. -project = u'PyVistaQt' +project = 'PyVistaQt' year = datetime.date.today().year -copyright = u'2017-{}, The PyVista Developers'.format(year) -author = u'Alex Kaszynski and Bane Sullivan' +copyright = ( + '2017-{}, The PyVista Developers'.format( # pylint: disable=redefined-builtin,line-too-long + year + ) +) +author = 'Alex Kaszynski and Bane Sullivan' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -89,13 +103,18 @@ # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None +# Usually you set 'language' from the command line for these cases. +language: Optional[str] = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # 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' @@ -112,24 +131,35 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_context = { - # Enable the "Edit in GitHub link within the header of each page. + # Enable the 'Edit in GitHub link within the header of each page. 'display_github': False, # Set the following variables to generate the resulting github URL for each page. - # Format Template: https://{{ github_host|default("github.com") }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} + # Format Template: https://{{ github_host|default('github.com') }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} 'github_user': 'pyvista', 'github_repo': 'pyvistaqt', 'github_version': 'main/docs/', 'menu_links_name': 'Getting Connected', 'menu_links': [ - (' Slack Community', 'http://slack.pyvista.org'), - (' Support', 'https://github.com/pyvista/pyvista-support'), - (' Source Code', 'https://github.com/pyvista/pyvistaqt'), - (' Contributing', 'https://github.com/pyvista/pyvistaqt/blob/main/CONTRIBUTING.md'), + ( + ' Slack Community', + 'http://slack.pyvista.org', + ), + ( + ' Support', + 'https://github.com/pyvista/pyvista-support', + ), + ( + ' Source Code', + 'https://github.com/pyvista/pyvistaqt', + ), + ( + ' Contributing', + 'https://github.com/pyvista/pyvistaqt/blob/main/CONTRIBUTING.md', + ), ], } @@ -143,7 +173,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". +# so a file named 'default.css' will overwrite the builtin 'default.css'. html_static_path = ['_static'] @@ -154,27 +184,21 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None, - 'https://docs.pyvista.org/': None, +intersphinx_mapping = { + 'https://docs.python.org/': None, + 'https://docs.pyvista.org/': None, } # -- Custom 404 page notfound_context = { - 'body': '

Page not found.

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

Page not found.

\n\nPerhaps try the examples page.', } notfound_no_urls_prefix = True -from docutils.parsers.rst import directives -# -- Autosummary options -from sphinx.ext.autosummary import Autosummary, get_documenter -from sphinx.util.inspect import safe_getattr - - class AutoAutoSummary(Autosummary): - option_spec = { 'methods': directives.unchanged, 'attributes': directives.unchanged, @@ -188,7 +212,7 @@ def get_members(obj, typ, include_public=None): if not include_public: include_public = [] items = [] - for name in sorted(obj.__dict__.keys()):#dir(obj): + for name in sorted(obj.__dict__.keys()): # dir(obj): try: documenter = get_documenter(AutoAutoSummary.app, safe_getattr(obj, name), obj) except AttributeError: @@ -206,17 +230,24 @@ def run(self): c = getattr(m, class_name) if 'methods' in self.options: _, methods = self.get_members(c, ['method'], ['__init__']) - self.content = ["~%s.%s" % (clazz, method) for method in methods if not method.startswith('_')] + self.content = [ + '~%s.%s' % (clazz, method) for method in methods if not method.startswith('_') + ] if 'attributes' in self.options: _, attribs = self.get_members(c, ['attribute', 'property']) - self.content = ["~%s.%s" % (clazz, attrib) for attrib in attribs if not attrib.startswith('_')] - except: + self.content = [ + '~%s.%s' % (clazz, attrib) for attrib in attribs if not attrib.startswith('_') + ] + except Exception: # pylint: disable=broad-except print('Something went wrong when autodocumenting {}'.format(clazz)) finally: - return super(AutoAutoSummary, self).run() + return super( + AutoAutoSummary, self # pylint: disable=lost-exception + ).run() # pylint: disable=lost-exception + def setup(app): AutoAutoSummary.app = app app.add_directive('autoautosummary', AutoAutoSummary) - app.add_css_file("style.css") - app.add_css_file("copybutton.css") + app.add_css_file('style.css') + app.add_css_file('copybutton.css') diff --git a/environment.yml b/environment.yml index c0999b85..2e7d2407 100644 --- a/environment.yml +++ b/environment.yml @@ -5,6 +5,7 @@ dependencies: - python<3.9 - codecov - ipython + - mypy - numpy - pytest - pytest-cov diff --git a/ignore_words.txt b/ignore_words.txt index 08703815..35430c91 100644 --- a/ignore_words.txt +++ b/ignore_words.txt @@ -1,3 +1,4 @@ lod byteorder flem +PullRequest \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6173c3c5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.isort] +profile = "black" +line_length = 100 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true + +[tool.black] +line-length = 100 +skip-string-normalization = true +target-version = ["py38"] +exclude='\.eggs|\.git|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist|node_modules' + +[tool.pydocstyle] +match = '(?!coverage).*.py' \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 50997d7b..533b2d34 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,4 +12,4 @@ filterwarnings = ignore:.*`np.float` is a deprecated alias.*:DeprecationWarning ignore:.*`np.object` is a deprecated alias.*:DeprecationWarning ignore:.*`np.long` is a deprecated alias:DeprecationWarning - ignore:.*Converting `np\.character` to a dtype is deprecated.*:DeprecationWarning + ignore:.*Converting `np\.character` to a dtype is deprecated.*:DeprecationWarning \ No newline at end of file diff --git a/pyvistaqt/__init__.py b/pyvistaqt/__init__.py index 003c6288..cb70dd9b 100755 --- a/pyvistaqt/__init__.py +++ b/pyvistaqt/__init__.py @@ -6,35 +6,19 @@ except Exception as exc: # pragma: no cover # pylint: disable=broad-except _exc_msg = exc - # pylint: disable=too-few-public-methods - class _QtBindingError: - def __init__(self, *args, **kwargs): - raise RuntimeError(f"No Qt binding was found, got: {_exc_msg}") - - # pylint: disable=too-few-public-methods - class BackgroundPlotter(_QtBindingError): - """Handle Qt binding error for BackgroundPlotter.""" - - # pylint: disable=too-few-public-methods - class MainWindow(_QtBindingError): - """Handle Qt binding error for MainWindow.""" - - # pylint: disable=too-few-public-methods - class MultiPlotter(_QtBindingError): - """Handle Qt binding error for MultiPlotter.""" - - # pylint: disable=too-few-public-methods - class QtInteractor(_QtBindingError): - """Handle Qt binding error for QtInteractor.""" + raise RuntimeError(f'No Qt binding was found, got: {_exc_msg}') from exc else: - from .plotting import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor + from .plotting import BackgroundPlotter as BackgroundPlotter + from .plotting import MultiPlotter as MultiPlotter + from .plotting import QtInteractor as QtInteractor + from .window import MainWindow as MainWindow __all__ = [ - "__version__", - "BackgroundPlotter", - "MainWindow", - "MultiPlotter", - "QtInteractor", + '__version__', + 'BackgroundPlotter', + 'MainWindow', + 'MultiPlotter', + 'QtInteractor', ] diff --git a/pyvistaqt/_version.py b/pyvistaqt/_version.py index 41aab3cf..5098b481 100644 --- a/pyvistaqt/_version.py +++ b/pyvistaqt/_version.py @@ -1,6 +1,6 @@ """Version info for pyvistaqt.""" # major, minor, patch -VERSION_INFO = 0, 10, "dev0" +VERSION_INFO = 0, 10, 'dev0' # Nice string for the version -__version__ = ".".join(map(str, VERSION_INFO)) +__version__ = '.'.join(map(str, VERSION_INFO)) diff --git a/pyvistaqt/counter.py b/pyvistaqt/counter.py index e31a7a64..1fecd817 100644 --- a/pyvistaqt/counter.py +++ b/pyvistaqt/counter.py @@ -16,11 +16,9 @@ def __init__(self, count: int) -> None: if isinstance(count, int) and count > 0: self.count = count elif count > 0: - raise TypeError( - f"Expected type of `count` to be `int` but got: {type(count)}" - ) + raise TypeError(f'Expected type of `count` to be `int` but got: {type(count)}') else: - raise ValueError("count is not strictly positive.") + raise ValueError('count is not strictly positive.') @Slot() def decrease(self) -> None: diff --git a/pyvistaqt/dialog.py b/pyvistaqt/dialog.py index c531da14..10d62b59 100644 --- a/pyvistaqt/dialog.py +++ b/pyvistaqt/dialog.py @@ -6,14 +6,7 @@ import pyvista as pv from qtpy import QtCore from qtpy.QtCore import Signal -from qtpy.QtWidgets import ( - QDialog, - QDoubleSpinBox, - QFileDialog, - QFormLayout, - QHBoxLayout, - QSlider, -) +from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QFileDialog, QFormLayout, QHBoxLayout, QSlider from .window import MainWindow @@ -101,20 +94,16 @@ def _value_range(self) -> float: def value(self) -> float: """Return the value of the slider.""" - return ( - float(super().value()) / self._max_int * self._value_range + self._min_value - ) + return float(super().value()) / self._max_int * self._value_range + self._min_value def setValue(self, value: float) -> None: # pylint: disable=invalid-name """Set the value of the slider.""" - super().setValue( - int((value - self._min_value) / self._value_range * self._max_int) - ) + super().setValue(int((value - self._min_value) / self._value_range * self._max_int)) def setMinimum(self, value: float) -> None: # pylint: disable=invalid-name """Set the minimum value of the slider.""" if value > self._max_value: # pragma: no cover - raise ValueError("Minimum limit cannot be higher than maximum") + raise ValueError('Minimum limit cannot be higher than maximum') self._min_value = value self.setValue(self.value()) @@ -122,7 +111,7 @@ def setMinimum(self, value: float) -> None: # pylint: disable=invalid-name def setMaximum(self, value: float) -> None: # pylint: disable=invalid-name """Set the maximum value of the slider.""" if value < self._min_value: # pragma: no cover - raise ValueError("Minimum limit cannot be higher than maximum") + raise ValueError('Minimum limit cannot be higher than maximum') self._max_value = value self.setValue(self.value()) @@ -152,9 +141,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) @@ -166,11 +153,17 @@ def __init__( return None - def update_spinbox(self, value: float) -> None: # pylint: disable=unused-argument + def update_spinbox( + self, + value: float, # pylint: disable=unused-argument + ) -> None: """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, # pylint: disable=unused-argument + ) -> None: """Update the value of the internal slider.""" # if self.spinbox.value() < self.minimum: # self.spinbox.setValue(self.minimum) @@ -200,9 +193,7 @@ class ScaleAxesDialog(QDialog): accepted = Signal(float) signal_close = Signal() - def __init__( - self, parent: MainWindow, plotter: pv.Plotter, show: bool = True - ) -> None: + def __init__(self, parent: MainWindow, plotter: pv.Plotter, show: bool = True) -> None: """Initialize the scaling dialog.""" super().__init__(parent) self.setGeometry(300, 300, 50, 50) @@ -211,20 +202,14 @@ def __init__( self.plotter = plotter self.plotter.app_window.signal_close.connect(self.close) - self.x_slider_group = RangeGroup( - parent, self.update_scale, value=plotter.scale[0] - ) - self.y_slider_group = RangeGroup( - parent, self.update_scale, value=plotter.scale[1] - ) - self.z_slider_group = RangeGroup( - parent, self.update_scale, value=plotter.scale[2] - ) + self.x_slider_group = RangeGroup(parent, self.update_scale, value=plotter.scale[0]) + self.y_slider_group = RangeGroup(parent, self.update_scale, value=plotter.scale[1]) + self.z_slider_group = RangeGroup(parent, self.update_scale, value=plotter.scale[2]) form_layout = QFormLayout(self) - form_layout.addRow("X Scale", self.x_slider_group) - form_layout.addRow("Y Scale", self.y_slider_group) - form_layout.addRow("Z Scale", self.z_slider_group) + form_layout.addRow('X Scale', self.x_slider_group) + form_layout.addRow('Y Scale', self.y_slider_group) + form_layout.addRow('Z Scale', self.z_slider_group) self.setLayout(form_layout) diff --git a/pyvistaqt/editor.py b/pyvistaqt/editor.py index 96dabab9..bcf9398a 100644 --- a/pyvistaqt/editor.py +++ b/pyvistaqt/editor.py @@ -44,7 +44,7 @@ def _selection_callback() -> None: self.tree_widget.itemSelectionChanged.connect(_selection_callback) self.setLayout(self.layout) - self.setWindowTitle("Editor") + self.setWindowTitle('Editor') self.setModal(True) self.update() @@ -55,7 +55,7 @@ def update(self) -> None: for idx, renderer in enumerate(self.renderers): actors = renderer._actors # pylint: disable=protected-access widget_idx = self.stacked_widget.addWidget(_get_renderer_widget(renderer)) - top_item = QTreeWidgetItem(self.tree_widget, [f"Renderer {idx}"]) + top_item = QTreeWidgetItem(self.tree_widget, [f'Renderer {idx}']) top_item.setData(0, Qt.ItemDataRole.UserRole, widget_idx) self.tree_widget.addTopLevelItem(top_item) for name, actor in actors.items(): @@ -86,8 +86,8 @@ def _axes_callback(state: bool) -> None: else: renderer.hide_axes() - axes = QCheckBox("Axes") - if hasattr(renderer, "axes_widget"): + axes = QCheckBox('Axes') + if hasattr(renderer, 'axes_widget'): axes.setChecked(renderer.axes_widget.GetEnabled()) else: axes.setChecked(False) @@ -105,7 +105,7 @@ def _get_actor_widget(actor: vtkActor) -> QWidget: prop = actor.GetProperty() # visibility - visibility = QCheckBox("Visibility") + visibility = QCheckBox('Visibility') visibility.setChecked(actor.GetVisibility()) visibility.toggled.connect(actor.SetVisibility) layout.addWidget(visibility) @@ -117,7 +117,7 @@ def _get_actor_widget(actor: vtkActor) -> QWidget: opacity.setMaximum(1.0) opacity.setValue(prop.GetOpacity()) opacity.valueChanged.connect(prop.SetOpacity) - tmp_layout.addWidget(QLabel("Opacity")) + tmp_layout.addWidget(QLabel('Opacity')) tmp_layout.addWidget(opacity) layout.addLayout(tmp_layout) diff --git a/pyvistaqt/plotting.py b/pyvistaqt/plotting.py index 496ce88d..ad21f8d0 100644 --- a/pyvistaqt/plotting.py +++ b/pyvistaqt/plotting.py @@ -89,11 +89,10 @@ else: from qtpy import QtGui # pylint: disable=ungrouped-imports -LOG = logging.getLogger("pyvistaqt") +LOG = logging.getLogger('pyvistaqt') LOG.setLevel(logging.CRITICAL) LOG.addHandler(logging.StreamHandler()) - # for display bugs due to older intel integrated GPUs, setting # vtkmodules.qt.QVTKRWIBase = 'QGLWidget' could help. However, its use # is discouraged and does not work well on VTK9+, so let's not bother @@ -104,8 +103,8 @@ # LOG = logging.getLogger(__name__) # LOG.setLevel('DEBUG') -SAVE_CAM_BUTTON_TEXT = "Save Camera" -CLEAR_CAMS_BUTTON_TEXT = "Clear Cameras" +SAVE_CAM_BUTTON_TEXT = 'Save Camera' +CLEAR_CAMS_BUTTON_TEXT = 'Clear Cameras' def resample_image(arr: np.ndarray, max_size: int = 400) -> np.ndarray: @@ -184,7 +183,7 @@ class QtInteractor(QVTKRenderWindowInteractor, BasePlotter): key_press_event_signal = Signal(vtkGenericRenderWindowInteractor, str) # pylint: disable=too-many-arguments - def __init__( + def __init__( # noqa: C901 self, parent: MainWindow = None, title: str = None, @@ -198,14 +197,14 @@ def __init__( ) -> None: # pylint: disable=too-many-branches """Initialize Qt interactor.""" - LOG.debug("QtInteractor init start") + LOG.debug('QtInteractor init start') self.url: QtCore.QUrl = None # Cannot use super() here because # QVTKRenderWindowInteractor silently swallows all kwargs # because they use **kwargs in their constructor... qvtk_kwargs = dict(parent=parent) - for key in ("stereo", "iren", "rw", "wflags"): + for key in ('stereo', 'iren', 'rw', 'wflags'): if key in kwargs: qvtk_kwargs[key] = kwargs.pop(key) with _no_base_plotter_init(): @@ -262,32 +261,26 @@ def __init__( self.render_timer.timeout.connect(self.render) self.render_timer.start(twait) - if global_theme.depth_peeling["enabled"]: + if global_theme.depth_peeling['enabled']: if self.enable_depth_peeling(): for renderer in self.renderers: renderer.enable_depth_peeling() self._first_time = False # Crucial! - LOG.debug("QtInteractor init stop") + LOG.debug('QtInteractor init stop') def _setup_interactor(self, off_screen: bool) -> None: if off_screen: self.iren: Any = None else: - self.iren = RenderWindowInteractor( - self, interactor=self.ren_win.GetInteractor() - ) - self.iren.interactor.RemoveObservers( - "MouseMoveEvent" - ) # slows window update? + self.iren = RenderWindowInteractor(self, interactor=self.ren_win.GetInteractor()) + self.iren.interactor.RemoveObservers('MouseMoveEvent') # slows window update? self.iren.initialize() self.enable_trackball_style() def _setup_key_press(self) -> None: - self._observers: Dict[ - None, None - ] = {} # Map of events to observers of self.iren - self.iren.add_observer("KeyPressEvent", self.key_press_event) + self._observers: Dict[None, None] = {} # Map of events to observers of self.iren + self.iren.add_observer('KeyPressEvent', self.key_press_event) self.reset_key_events() def gesture_event(self, event: QGestureEvent) -> bool: @@ -308,7 +301,7 @@ def _render(self, *args: Any, **kwargs: Any) -> BasePlotter.render: """Wrap ``BasePlotter.render``.""" return BasePlotter.render(self, *args, **kwargs) - @conditional_decorator(threaded, platform.system() == "Darwin") + @conditional_decorator(threaded, platform.system() == 'Darwin') def render(self) -> None: """Override the ``render`` method to handle threading issues.""" return self.render_signal.emit() @@ -356,8 +349,8 @@ def link_views_across_plotters( if not np.issubdtype(other_views.dtype, int): raise TypeError( - "Expected `other_views` type is int, or list or tuple of ints, " - f"but {other_views.dtype} is given" + 'Expected `other_views` type is int, or list or tuple of ints, ' + f'but {other_views.dtype} is given' ) renderer = self.renderers[view] @@ -378,7 +371,7 @@ def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None: # only call accept on files event.accept() except IOError as exception: # pragma: no cover - warnings.warn(f"Exception when dragging files: {str(exception)}") + warnings.warn(f'Exception when dragging files: {str(exception)}') # pylint: disable=invalid-name,useless-return def dropEvent(self, event: QtCore.QEvent) -> None: @@ -390,13 +383,13 @@ def dropEvent(self, event: QtCore.QEvent) -> None: if os.path.isfile(filename): self.add_mesh(pyvista.read(filename)) except IOError as exception: # pragma: no cover - warnings.warn(f"Exception when dropping files: {str(exception)}") + warnings.warn(f'Exception when dropping files: {str(exception)}') def close(self) -> None: """Quit application.""" if self._closed: return - if hasattr(self, "render_timer"): + if hasattr(self, 'render_timer'): self.render_timer.stop() BasePlotter.close(self) QVTKRenderWindowInteractor.close(self) @@ -426,7 +419,7 @@ class BackgroundPlotter(QtInteractor): screenshots or debug testing. allow_quit_keypress : - Allow user to exit by pressing ``"q"``. + Allow user to exit by pressing ``'q'``. toolbar : bool If True, display the default camera toolbar. Defaults to True. @@ -505,16 +498,16 @@ def __init__( # self._closed=True until the BasePlotter.__init__ # is called self._closed = True - LOG.debug("BackgroundPlotter init start") - _check_type(show, "show", [bool]) - _check_type(app, "app", [QApplication, type(None)]) - _check_type(window_size, "window_size", [tuple, type(None)]) - _check_type(off_screen, "off_screen", [bool, type(None)]) - _check_type(allow_quit_keypress, "allow_quit_keypress", [bool]) - _check_type(toolbar, "toolbar", [bool]) - _check_type(menu_bar, "menu_bar", [bool]) - _check_type(editor, "editor", [bool]) - _check_type(update_app_icon, "update_app_icon", [bool, type(None)]) + LOG.debug('BackgroundPlotter init start') + _check_type(show, 'show', [bool]) + _check_type(app, 'app', [QApplication, type(None)]) + _check_type(window_size, 'window_size', [tuple, type(None)]) + _check_type(off_screen, 'off_screen', [bool, type(None)]) + _check_type(allow_quit_keypress, 'allow_quit_keypress', [bool]) + _check_type(toolbar, 'toolbar', [bool]) + _check_type(menu_bar, 'menu_bar', [bool]) + _check_type(editor, 'editor', [bool]) + _check_type(update_app_icon, 'update_app_icon', [bool, type(None)]) # toolbar self._view_action: QAction = None @@ -538,16 +531,14 @@ def __init__( window_size = global_theme.window_size # Remove notebook argument in case user passed it - kwargs.pop("notebook", None) + kwargs.pop('notebook', None) self.ipython = _setup_ipython() self.app = _setup_application(app) self.off_screen = _setup_off_screen(off_screen) if app_window_class is None: app_window_class = MainWindow - self.app_window = app_window_class( - title=kwargs.get("title", global_theme.title) - ) + self.app_window = app_window_class(title=kwargs.get('title', global_theme.title)) self.frame = QFrame(parent=self.app_window) self.frame.setFrameStyle(QFrame.NoFrame) vlayout = QVBoxLayout() @@ -580,7 +571,9 @@ def __init__( elif update_app_icon is None: self.set_icon( os.path.join( - os.path.dirname(__file__), "data", "pyvista_logo_square.png" + os.path.dirname(__file__), + 'data', + 'pyvista_logo_square.png', ) ) else: @@ -588,8 +581,8 @@ def __init__( # Keypress events if self.iren is not None: - self.add_key_event("S", self._qt_screenshot) # shift + s - LOG.debug("BackgroundPlotter init stop") + self.add_key_event('S', self._qt_screenshot) # shift + s + LOG.debug('BackgroundPlotter init stop') def reset_key_events(self) -> None: """Reset all of the key press events to their defaults. @@ -599,7 +592,7 @@ def reset_key_events(self) -> None: super().reset_key_events() if self.allow_quit_keypress: # pylint: disable=unnecessary-lambda - self.add_key_event("q", lambda: self.close()) + self.add_key_event('q', lambda: self.close()) def scale_axes_dialog(self, show: bool = True) -> ScaleAxesDialog: """Open scale axes dialog.""" @@ -629,9 +622,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() @@ -677,16 +668,14 @@ def set_icon(self, img: Union[np.ndarray, str]) -> None: and img.shape[-1] in (3, 4) ) and not isinstance(img, str): raise ValueError( - "img must be 3D uint8 ndarray with shape[1] == shape[2] and " - "shape[2] == 3 or 4, or str" + 'img must be 3D uint8 ndarray with shape[1] == shape[2] and ' + 'shape[2] == 3 or 4, or str' ) if isinstance(img, np.ndarray): - fmt_str = "Format_RGB" - fmt_str += ("A8" if img.shape[2] == 4 else "") + "888" + fmt_str = 'Format_RGB' + fmt_str += ('A8' if img.shape[2] == 4 else '') + '888' fmt = getattr(QtGui.QImage, fmt_str) - img = QtGui.QPixmap.fromImage( - QtGui.QImage(img.copy(), img.shape[1], img.shape[0], fmt) - ) + img = QtGui.QPixmap.fromImage(QtGui.QImage(img.copy(), img.shape[1], img.shape[0], fmt)) # Currently no way to check if str/path is actually correct (want to # allow resource paths and the like so os.path.isfile is no good) # and icon.isNull() returns False even if the path is bogus. @@ -695,7 +684,7 @@ def set_icon(self, img: Union[np.ndarray, str]) -> None: def _qt_screenshot(self, show: bool = True) -> FileDialog: return FileDialog( self.app_window, - filefilter=["Image File (*.png)", "JPEG (*.jpeg)"], + filefilter=['Image File (*.png)', 'JPEG (*.jpeg)'], show=show, directory=bool(os.getcwd()), callback=self.screenshot, @@ -705,14 +694,14 @@ def _qt_export_vtkjs(self, show: bool = True) -> FileDialog: """Spawn an save file dialog to export a vtkjs file.""" return FileDialog( self.app_window, - filefilter=["VTK JS File(*.vtkjs)"], + filefilter=['VTK JS File(*.vtkjs)'], show=show, directory=bool(os.getcwd()), callback=self.export_vtkjs, ) def _toggle_edl(self) -> None: - if hasattr(self.renderer, "edl_pass"): + if hasattr(self.renderer, 'edl_pass'): return self.renderer.disable_eye_dome_lighting() return self.renderer.enable_eye_dome_lighting() @@ -741,7 +730,10 @@ def __del__(self) -> None: # pragma: no cover self.app_window.close() def add_callback( - self, func: Callable, interval: int = 1000, count: Optional[int] = None + self, + func: Callable, + interval: int = 1000, + count: Optional[int] = None, ) -> None: """Add a function that can update the scene in the background. @@ -776,21 +768,24 @@ def save_camera_position(self) -> None: if self.camera_position is not None: camera_position: Any = self.camera_position[:] # py2.7 copy compatibility - if hasattr(self, "saved_cameras_tool_bar"): + if hasattr(self, 'saved_cameras_tool_bar'): def load_camera_position() -> None: # pylint: disable=attribute-defined-outside-init self.camera_position = camera_position - self.saved_cameras_tool_bar.addAction(f"Cam {ncam}", load_camera_position) + self.saved_cameras_tool_bar.addAction(f'Cam {ncam}', load_camera_position) if ncam < 10: self.add_key_event(str(ncam), load_camera_position) def clear_camera_positions(self) -> None: """Clear all camera positions.""" - if hasattr(self, "saved_cameras_tool_bar"): + if hasattr(self, 'saved_cameras_tool_bar'): for action in self.saved_cameras_tool_bar.actions(): - if action.text() not in [SAVE_CAM_BUTTON_TEXT, CLEAR_CAMS_BUTTON_TEXT]: + if action.text() not in [ + SAVE_CAM_BUTTON_TEXT, + CLEAR_CAMS_BUTTON_TEXT, + ]: self.saved_cameras_tool_bar.removeAction(action) self.saved_camera_positions = [] @@ -803,38 +798,38 @@ def _add_action(self, tool_bar: QToolBar, key: str, method: Any) -> QAction: def add_toolbars(self) -> None: """Add the toolbars.""" # Camera toolbar - self.default_camera_tool_bar = self.app_window.addToolBar("Camera Position") + self.default_camera_tool_bar = self.app_window.addToolBar('Camera Position') def _view_vector(*args: Any) -> None: return self.view_vector(*args) cvec_setters = { # Viewing vector then view up vector - "Top (-Z)": lambda: _view_vector((0, 0, 1), (0, 1, 0)), - "Bottom (+Z)": lambda: _view_vector((0, 0, -1), (0, 1, 0)), - "Front (-Y)": lambda: _view_vector((0, 1, 0), (0, 0, 1)), - "Back (+Y)": lambda: _view_vector((0, -1, 0), (0, 0, 1)), - "Left (-X)": lambda: _view_vector((1, 0, 0), (0, 0, 1)), - "Right (+X)": lambda: _view_vector((-1, 0, 0), (0, 0, 1)), - "Isometric": lambda: _view_vector((1, 1, 1), (0, 0, 1)), + 'Top (-Z)': lambda: _view_vector((0, 0, 1), (0, 1, 0)), + 'Bottom (+Z)': lambda: _view_vector((0, 0, -1), (0, 1, 0)), + 'Front (-Y)': lambda: _view_vector((0, 1, 0), (0, 0, 1)), + 'Back (+Y)': lambda: _view_vector((0, -1, 0), (0, 0, 1)), + 'Left (-X)': lambda: _view_vector((1, 0, 0), (0, 0, 1)), + 'Right (+X)': lambda: _view_vector((-1, 0, 0), (0, 0, 1)), + 'Isometric': lambda: _view_vector((1, 1, 1), (0, 0, 1)), } for key, method in cvec_setters.items(): - self._view_action = self._add_action( - self.default_camera_tool_bar, key, method - ) + self._view_action = self._add_action(self.default_camera_tool_bar, key, method) # pylint: disable=unnecessary-lambda self._add_action( - self.default_camera_tool_bar, "Reset", lambda: self.reset_camera() + self.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.saved_cameras_tool_bar, + SAVE_CAM_BUTTON_TEXT, + self.save_camera_position, ) self._add_action( self.saved_cameras_tool_bar, @@ -847,45 +842,44 @@ def add_menu_bar(self) -> None: self.main_menu = _create_menu_bar(parent=self.app_window) self.app_window.signal_close.connect(self.main_menu.clear) - file_menu = self.main_menu.addMenu("File") - file_menu.addAction("Take Screenshot", self._qt_screenshot) - file_menu.addAction("Export as VTKjs", self._qt_export_vtkjs) + file_menu = self.main_menu.addMenu('File') + file_menu.addAction('Take Screenshot', self._qt_screenshot) + file_menu.addAction('Export as VTKjs', self._qt_export_vtkjs) file_menu.addSeparator() # member variable for testing only - self._menu_close_action = file_menu.addAction("Exit", self.app_window.close) + self._menu_close_action = file_menu.addAction('Exit', self.app_window.close) - view_menu = self.main_menu.addMenu("View") - self._edl_action = view_menu.addAction( - "Toggle Eye Dome Lighting", self._toggle_edl - ) - view_menu.addAction("Scale Axes", self.scale_axes_dialog) - view_menu.addAction("Clear All", self.clear) + view_menu = self.main_menu.addMenu('View') + self._edl_action = view_menu.addAction('Toggle Eye Dome Lighting', self._toggle_edl) + view_menu.addAction('Scale Axes', self.scale_axes_dialog) + view_menu.addAction('Clear All', self.clear) - tool_menu = self.main_menu.addMenu("Tools") - tool_menu.addAction("Enable Cell Picking (through)", self.enable_cell_picking) + tool_menu = self.main_menu.addMenu('Tools') + tool_menu.addAction('Enable Cell Picking (through)', self.enable_cell_picking) tool_menu.addAction( - "Enable Cell Picking (visible)", + 'Enable Cell Picking (visible)', lambda: self.enable_cell_picking(through=False), ) - cam_menu = view_menu.addMenu("Camera") + cam_menu = view_menu.addMenu('Camera') self._parallel_projection_action = cam_menu.addAction( - "Toggle Parallel Projection", self._toggle_parallel_projection + 'Toggle Parallel Projection', + self._toggle_parallel_projection, ) view_menu.addSeparator() # Orientation marker - orien_menu = view_menu.addMenu("Orientation Marker") - orien_menu.addAction("Show All", self.show_axes_all) - orien_menu.addAction("Hide All", self.hide_axes_all) + orien_menu = view_menu.addMenu('Orientation Marker') + orien_menu.addAction('Show All', self.show_axes_all) + orien_menu.addAction('Hide All', self.hide_axes_all) # Bounds axes - axes_menu = view_menu.addMenu("Bounds Axes") - axes_menu.addAction("Add Bounds Axes (front)", self.show_bounds) - axes_menu.addAction("Add Bounds Grid (back)", self.show_grid) - axes_menu.addAction("Add Bounding Box", self.add_bounding_box) + axes_menu = view_menu.addMenu('Bounds Axes') + axes_menu.addAction('Add Bounds Axes (front)', self.show_bounds) + axes_menu.addAction('Add Bounds Grid (back)', self.show_grid) + axes_menu.addAction('Add Bounding Box', self.add_bounding_box) axes_menu.addSeparator() - axes_menu.addAction("Remove Bounding Box", self.remove_bounding_box) - axes_menu.addAction("Remove Bounds", self.remove_bounds_axes) + axes_menu.addAction('Remove Bounding Box', self.remove_bounding_box) + axes_menu.addAction('Remove Bounds', self.remove_bounds_axes) # A final separator to separate OS options view_menu.addSeparator() @@ -893,7 +887,7 @@ def add_menu_bar(self) -> None: def add_editor(self) -> None: """Add the editor.""" self.editor = Editor(parent=self.app_window, renderers=self.renderers) - self._editor_action = self.main_menu.addAction("Editor", self.editor.toggle) + self._editor_action = self.main_menu.addAction('Editor', self.editor.toggle) self.app_window.signal_close.connect(self.editor.close) @@ -944,13 +938,13 @@ def __init__( **kwargs: Any, ) -> None: """Initialize the multi plotter.""" - _check_type(app, "app", [QApplication, type(None)]) - _check_type(nrows, "nrows", [int]) - _check_type(ncols, "ncols", [int]) - _check_type(show, "show", [bool]) - _check_type(window_size, "window_size", [tuple, type(None)]) - _check_type(title, "title", [str, type(None)]) - _check_type(off_screen, "off_screen", [bool, type(None)]) + _check_type(app, 'app', [QApplication, type(None)]) + _check_type(nrows, 'nrows', [int]) + _check_type(ncols, 'ncols', [int]) + _check_type(show, 'show', [bool]) + _check_type(window_size, 'window_size', [tuple, type(None)]) + _check_type(title, 'title', [str, type(None)]) + _check_type(off_screen, 'off_screen', [bool, type(None)]) self.ipython = _setup_ipython() self.app = _setup_application(app) self.off_screen = _setup_off_screen(off_screen) diff --git a/pyvistaqt/py.typed b/pyvistaqt/py.typed new file mode 100644 index 00000000..270b15e2 --- /dev/null +++ b/pyvistaqt/py.typed @@ -0,0 +1 @@ +partial\n \ No newline at end of file diff --git a/pyvistaqt/rwi.py b/pyvistaqt/rwi.py index 3865bbfd..078dae80 100644 --- a/pyvistaqt/rwi.py +++ b/pyvistaqt/rwi.py @@ -59,17 +59,19 @@ # Check whether a specific PyQt implementation was chosen try: import vtkmodules.qt + PyQtImpl = vtkmodules.qt.PyQtImpl except ImportError: pass # Check whether a specific QVTKRenderWindowInteractor base -# class was chosen, can be set to "QGLWidget" in +# class was chosen, can be set to 'QGLWidget' in # PyQt implementation version lower than Pyside6, -# or "QOpenGLWidget" in Pyside6 -QVTKRWIBase = "QWidget" +# or 'QOpenGLWidget' in Pyside6 +QVTKRWIBase = 'QWidget' try: import vtkmodules.qt + QVTKRWIBase = vtkmodules.qt.QVTKRWIBase except ImportError: pass @@ -81,127 +83,91 @@ # Autodetect the PyQt implementation to use try: import PyQt6 - PyQtImpl = "PyQt6" + + PyQtImpl = 'PyQt6' except ImportError: try: import PySide6 - PyQtImpl = "PySide6" + + PyQtImpl = 'PySide6' except ImportError: try: import PyQt5 - PyQtImpl = "PyQt5" + + PyQtImpl = 'PyQt5' except ImportError: try: import PySide2 - PyQtImpl = "PySide2" + + PyQtImpl = 'PySide2' except ImportError: try: import PyQt4 - PyQtImpl = "PyQt4" + + PyQtImpl = 'PyQt4' except ImportError: try: import PySide - PyQtImpl = "PySide" + + PyQtImpl = 'PySide' except ImportError: - raise ImportError("Cannot load either PyQt or PySide") + raise ImportError('Cannot load either PyQt or PySide') # Check the compatibility of PyQtImpl and QVTKRWIBase -if QVTKRWIBase != "QWidget": - if PyQtImpl in ["PyQt6", "PySide6"] and QVTKRWIBase == "QOpenGLWidget": +if QVTKRWIBase != 'QWidget': + if PyQtImpl in ['PyQt6', 'PySide6'] and QVTKRWIBase == 'QOpenGLWidget': pass # compatible - elif PyQtImpl in ["PyQt5", "PySide2","PyQt4", "PySide"] and QVTKRWIBase == "QGLWidget": + elif PyQtImpl in ['PyQt5', 'PySide2', 'PyQt4', 'PySide'] and QVTKRWIBase == 'QGLWidget': pass # compatible else: - raise ImportError("Cannot load " + QVTKRWIBase + " from " + PyQtImpl) + raise ImportError('Cannot load ' + QVTKRWIBase + ' from ' + PyQtImpl) -if PyQtImpl == "PyQt6": - if QVTKRWIBase == "QOpenGLWidget": +if PyQtImpl == 'PyQt6': + if QVTKRWIBase == 'QOpenGLWidget': from PyQt6.QtOpenGLWidgets import QOpenGLWidget - from PyQt6.QtWidgets import QWidget - from PyQt6.QtWidgets import QSizePolicy - from PyQt6.QtWidgets import QApplication - from PyQt6.QtWidgets import QMainWindow + from PyQt6.QtCore import QEvent, QObject, QSize, Qt, QTimer from PyQt6.QtGui import QCursor - from PyQt6.QtCore import Qt - from PyQt6.QtCore import QTimer - from PyQt6.QtCore import QObject - from PyQt6.QtCore import QSize - from PyQt6.QtCore import QEvent -elif PyQtImpl == "PySide6": - if QVTKRWIBase == "QOpenGLWidget": + from PyQt6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PySide6': + if QVTKRWIBase == 'QOpenGLWidget': from PySide6.QtOpenGLWidgets import QOpenGLWidget - from PySide6.QtWidgets import QWidget - from PySide6.QtWidgets import QSizePolicy - from PySide6.QtWidgets import QApplication - from PySide6.QtWidgets import QMainWindow + from PySide6.QtCore import QEvent, QObject, QSize, Qt, QTimer from PySide6.QtGui import QCursor - from PySide6.QtCore import Qt - from PySide6.QtCore import QTimer - from PySide6.QtCore import QObject - from PySide6.QtCore import QSize - from PySide6.QtCore import QEvent -elif PyQtImpl == "PyQt5": - if QVTKRWIBase == "QGLWidget": + from PySide6.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PyQt5': + if QVTKRWIBase == 'QGLWidget': from PyQt5.QtOpenGL import QGLWidget - from PyQt5.QtWidgets import QWidget - from PyQt5.QtWidgets import QSizePolicy - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWidgets import QMainWindow + from PyQt5.QtCore import QEvent, QObject, QSize, Qt, QTimer from PyQt5.QtGui import QCursor - from PyQt5.QtCore import Qt - from PyQt5.QtCore import QTimer - from PyQt5.QtCore import QObject - from PyQt5.QtCore import QSize - from PyQt5.QtCore import QEvent -elif PyQtImpl == "PySide2": - if QVTKRWIBase == "QGLWidget": + from PyQt5.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PySide2': + if QVTKRWIBase == 'QGLWidget': from PySide2.QtOpenGL import QGLWidget - from PySide2.QtWidgets import QWidget - from PySide2.QtWidgets import QSizePolicy - from PySide2.QtWidgets import QApplication - from PySide2.QtWidgets import QMainWindow + from PySide2.QtCore import QEvent, QObject, QSize, Qt, QTimer from PySide2.QtGui import QCursor - from PySide2.QtCore import Qt - from PySide2.QtCore import QTimer - from PySide2.QtCore import QObject - from PySide2.QtCore import QSize - from PySide2.QtCore import QEvent -elif PyQtImpl == "PyQt4": - if QVTKRWIBase == "QGLWidget": + from PySide2.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PyQt4': + if QVTKRWIBase == 'QGLWidget': from PyQt4.QtOpenGL import QGLWidget - from PyQt4.QtGui import QWidget - from PyQt4.QtGui import QSizePolicy - from PyQt4.QtGui import QApplication - from PyQt4.QtGui import QMainWindow - from PyQt4.QtCore import Qt - from PyQt4.QtCore import QTimer - from PyQt4.QtCore import QObject - from PyQt4.QtCore import QSize - from PyQt4.QtCore import QEvent -elif PyQtImpl == "PySide": - if QVTKRWIBase == "QGLWidget": + from PyQt4.QtCore import QEvent, QObject, QSize, Qt, QTimer + from PyQt4.QtGui import QApplication, QMainWindow, QSizePolicy, QWidget +elif PyQtImpl == 'PySide': + if QVTKRWIBase == 'QGLWidget': from PySide.QtOpenGL import QGLWidget - from PySide.QtGui import QWidget - from PySide.QtGui import QSizePolicy - from PySide.QtGui import QApplication - from PySide.QtGui import QMainWindow - from PySide.QtCore import Qt - from PySide.QtCore import QTimer - from PySide.QtCore import QObject - from PySide.QtCore import QSize - from PySide.QtCore import QEvent + from PySide.QtCore import QEvent, QObject, QSize, Qt, QTimer + from PySide.QtGui import QApplication, QMainWindow, QSizePolicy, QWidget else: - raise ImportError("Unknown PyQt implementation " + repr(PyQtImpl)) + raise ImportError('Unknown PyQt implementation ' + repr(PyQtImpl)) # Define types for base class, based on string -if QVTKRWIBase == "QWidget": +if QVTKRWIBase == 'QWidget': QVTKRWIBaseClass = QWidget -elif QVTKRWIBase == "QGLWidget": +elif QVTKRWIBase == 'QGLWidget': QVTKRWIBaseClass = QGLWidget -elif QVTKRWIBase == "QOpenGLWidget": +elif QVTKRWIBase == 'QOpenGLWidget': QVTKRWIBaseClass = QOpenGLWidget else: - raise ImportError("Unknown base class for QVTKRenderWindowInteractor " + QVTKRWIBase) + raise ImportError('Unknown base class for QVTKRenderWindowInteractor ' + QVTKRWIBase) if PyQtImpl == 'PyQt6': CursorShape = Qt.CursorShape @@ -215,8 +181,9 @@ SizePolicy = QSizePolicy.Policy EventType = QEvent.Type else: - CursorShape = MouseButton = WindowType = WidgetAttribute = \ - KeyboardModifier = FocusPolicy = ConnectionType = Key = Qt + CursorShape = ( + MouseButton + ) = WindowType = WidgetAttribute = KeyboardModifier = FocusPolicy = ConnectionType = Key = Qt SizePolicy = QSizePolicy EventType = QEvent @@ -235,7 +202,7 @@ 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. @@ -304,17 +271,17 @@ class QVTKRenderWindowInteractor(QVTKRWIBaseClass): # Map between VTK and Qt cursors. _CURSOR_MAP = { - 0: CursorShape.ArrowCursor, # VTK_CURSOR_DEFAULT - 1: CursorShape.ArrowCursor, # VTK_CURSOR_ARROW - 2: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZENE - 3: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZENWSE - 4: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZESW - 5: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZESE - 6: CursorShape.SizeVerCursor, # VTK_CURSOR_SIZENS - 7: CursorShape.SizeHorCursor, # VTK_CURSOR_SIZEWE - 8: CursorShape.SizeAllCursor, # VTK_CURSOR_SIZEALL - 9: CursorShape.PointingHandCursor, # VTK_CURSOR_HAND - 10: CursorShape.CrossCursor, # VTK_CURSOR_CROSSHAIR + 0: CursorShape.ArrowCursor, # VTK_CURSOR_DEFAULT + 1: CursorShape.ArrowCursor, # VTK_CURSOR_ARROW + 2: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZENE + 3: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZENWSE + 4: CursorShape.SizeBDiagCursor, # VTK_CURSOR_SIZESW + 5: CursorShape.SizeFDiagCursor, # VTK_CURSOR_SIZESE + 6: CursorShape.SizeVerCursor, # VTK_CURSOR_SIZENS + 7: CursorShape.SizeHorCursor, # VTK_CURSOR_SIZEWE + 8: CursorShape.SizeAllCursor, # VTK_CURSOR_SIZEALL + 9: CursorShape.PointingHandCursor, # VTK_CURSOR_HAND + 10: CursorShape.CrossCursor, # VTK_CURSOR_CROSSHAIR } def __init__(self, parent=None, **kw): @@ -342,18 +309,18 @@ def __init__(self, parent=None, **kw): rw = None # create base qt-level widget - if QVTKRWIBase == "QWidget": - if "wflags" in kw: + if QVTKRWIBase == 'QWidget': + if 'wflags' in kw: wflags = kw['wflags'] else: wflags = Qt.WindowType.Widget QWidget.__init__(self, parent, wflags | WindowType.MSWindowsOwnDC) - elif QVTKRWIBase == "QGLWidget": + elif QVTKRWIBase == 'QGLWidget': QGLWidget.__init__(self, parent) - elif QVTKRWIBase == "QOpenGLWidget": + elif QVTKRWIBase == 'QOpenGLWidget': QOpenGLWidget.__init__(self, parent) - if rw: # user-supplied render window + if rw: # user-supplied render window self._RenderWindow = rw else: self._RenderWindow = vtkRenderWindow() @@ -362,30 +329,33 @@ def __init__(self, parent=None, **kw): # Python2 if type(WId).__name__ == 'PyCObject': - from ctypes import pythonapi, c_void_p, py_object + from ctypes import c_void_p, py_object, pythonapi - pythonapi.PyCObject_AsVoidPtr.restype = c_void_p + pythonapi.PyCObject_AsVoidPtr.restype = c_void_p pythonapi.PyCObject_AsVoidPtr.argtypes = [py_object] WId = pythonapi.PyCObject_AsVoidPtr(WId) # Python3 elif type(WId).__name__ == 'PyCapsule': - from ctypes import pythonapi, c_void_p, py_object, c_char_p + from ctypes import c_char_p, c_void_p, py_object, pythonapi pythonapi.PyCapsule_GetName.restype = c_char_p pythonapi.PyCapsule_GetName.argtypes = [py_object] name = pythonapi.PyCapsule_GetName(WId) - pythonapi.PyCapsule_GetPointer.restype = c_void_p - pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_GetPointer.restype = c_void_p + pythonapi.PyCapsule_GetPointer.argtypes = [ + py_object, + c_char_p, + ] WId = pythonapi.PyCapsule_GetPointer(WId, name) self._RenderWindow.SetWindowInfo(str(int(WId))) - if stereo: # stereo mode + if stereo: # stereo mode self._RenderWindow.StereoCapableWindowOn() self._RenderWindow.SetStereoTypeToCrystalEyes() @@ -398,7 +368,7 @@ def __init__(self, parent=None, **kw): # do all the necessary qt setup self.setAttribute(WidgetAttribute.WA_OpaquePaintEvent) self.setAttribute(WidgetAttribute.WA_PaintOnScreen) - self.setMouseTracking(True) # get all mouse events + self.setMouseTracking(True) # get all mouse events self.setFocusPolicy(FocusPolicy.WheelFocus) self.setSizePolicy(QSizePolicy(SizePolicy.Expanding, SizePolicy.Expanding)) @@ -407,8 +377,7 @@ def __init__(self, parent=None, **kw): self._Iren.AddObserver('CreateTimerEvent', self.CreateTimer) self._Iren.AddObserver('DestroyTimerEvent', self.DestroyTimer) - self._Iren.GetRenderWindow().AddObserver('CursorChangedEvent', - self.CursorChangedEvent) + self._Iren.GetRenderWindow().AddObserver('CursorChangedEvent', self.CursorChangedEvent) # If we've a parent, it does not close the child when closed. # Connect the parent's destroyed signal to this widget's close @@ -423,13 +392,12 @@ def __getattr__(self, attr): elif hasattr(self._Iren, attr): return getattr(self._Iren, attr) else: - raise AttributeError(self.__class__.__name__ + - " has no attribute named " + attr) + raise AttributeError(self.__class__.__name__ + ' has no attribute named ' + attr) def Finalize(self): - ''' + """ Call internal cleanup method on VTK objects - ''' + """ self._RenderWindow.Finalize() def CreateTimer(self, obj, evt): @@ -473,16 +441,16 @@ def paintEvent(self, ev): def resizeEvent(self, ev): scale = self._getPixelRatio() - w = int(round(scale*self.width())) - h = int(round(scale*self.height())) - self._RenderWindow.SetDPI(int(round(72*scale))) + w = int(round(scale * self.width())) + h = int(round(scale * self.height())) + self._RenderWindow.SetDPI(int(round(72 * scale))) vtkRenderWindow.SetSize(self._RenderWindow, w, h) self._Iren.SetSize(w, h) self._Iren.ConfigureEvent() self.update() def _GetKeyCharAndKeySym(self, ev): - """ Convert a Qt key into a char and a vtk keysym. + """Convert a Qt key into a char and a vtk keysym. This is essentially copied from the c++ implementation in GUISupport/Qt/QVTKInteractorAdapter.cxx. @@ -502,9 +470,9 @@ def _GetKeyCharAndKeySym(self, ev): except KeyError: keySym = None - # use "None" as a fallback + # use 'None' as a fallback if keySym is None: - keySym = "None" + keySym = 'None' return keyChar, keySym @@ -526,7 +494,7 @@ def _GetCtrlShift(self, ev): @staticmethod def _getPixelRatio(): - if PyQtImpl in ["PyQt5", "PySide2", "PySide6"]: + if PyQtImpl in ['PyQt5', 'PySide2', 'PySide6']: # Source: https://stackoverflow.com/a/40053864/3388962 pos = QCursor.pos() for screen in QApplication.screens(): @@ -538,25 +506,28 @@ def _getPixelRatio(): else: # Qt4 seems not to provide any cross-platform means to get the # pixel ratio. - return 1. + return 1.0 - def _setEventInformation(self, x, y, ctrl, shift, - key, repeat=0, keysum=None): + def _setEventInformation(self, x, y, ctrl, shift, key, repeat=0, keysum=None): scale = self._getPixelRatio() - self._Iren.SetEventInformation(int(round(x*scale)), - int(round((self.height()-y-1)*scale)), - ctrl, shift, key, repeat, keysum) + self._Iren.SetEventInformation( + int(round(x * scale)), + int(round((self.height() - y - 1) * scale)), + ctrl, + shift, + key, + repeat, + keysum, + ) def enterEvent(self, ev): ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, chr(0), 0, None) self._Iren.EnterEvent() def leaveEvent(self, ev): ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, chr(0), 0, None) self._Iren.LeaveEvent() def mousePressEvent(self, ev): @@ -565,8 +536,7 @@ def mousePressEvent(self, ev): repeat = 0 if ev.type() == EventType.MouseButtonDblClick: repeat = 1 - self._setEventInformation(pos_x, pos_y, - ctrl, shift, chr(0), repeat, None) + self._setEventInformation(pos_x, pos_y, ctrl, shift, chr(0), repeat, None) self._ActiveButton = ev.button() @@ -580,8 +550,7 @@ def mousePressEvent(self, ev): def mouseReleaseEvent(self, ev): pos_x, pos_y = _get_event_pos(ev) ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(pos_x, pos_y, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(pos_x, pos_y, ctrl, shift, chr(0), 0, None) if self._ActiveButton == MouseButton.LeftButton: self._Iren.LeftButtonReleaseEvent() @@ -598,23 +567,20 @@ def mouseMoveEvent(self, ev): self.__saveY = pos_y ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(pos_x, pos_y, - ctrl, shift, chr(0), 0, None) + self._setEventInformation(pos_x, pos_y, ctrl, shift, chr(0), 0, None) self._Iren.MouseMoveEvent() def keyPressEvent(self, ev): key, keySym = self._GetKeyCharAndKeySym(ev) ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, key, 0, keySym) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, key, 0, keySym) self._Iren.KeyPressEvent() self._Iren.CharEvent() def keyReleaseEvent(self, ev): key, keySym = self._GetKeyCharAndKeySym(ev) ctrl, shift = self._GetCtrlShift(ev) - self._setEventInformation(self.__saveX, self.__saveY, - ctrl, shift, key, 0, keySym) + self._setEventInformation(self.__saveX, self.__saveY, ctrl, shift, key, 0, keySym) self._Iren.KeyReleaseEvent() def wheelEvent(self, ev): @@ -640,16 +606,17 @@ def Render(self): def QVTKRenderWidgetConeExample(block=False): """A simple example that uses the QVTKRenderWindowInteractor class.""" - from vtkmodules.vtkFiltersSources import vtkConeSource - from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper, vtkRenderer + import vtkmodules.vtkInteractionStyle + # load implementations for rendering and interaction factory classes import vtkmodules.vtkRenderingOpenGL2 - import vtkmodules.vtkInteractionStyle + from vtkmodules.vtkFiltersSources import vtkConeSource + from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper, vtkRenderer # every QT app needs an app app = QApplication.instance() if not app: # pragma: no cover - app = QApplication(["PyVista"]) + app = QApplication(['PyVista']) window = QMainWindow() @@ -657,7 +624,7 @@ def QVTKRenderWidgetConeExample(block=False): widget = QVTKRenderWindowInteractor(window) window.setCentralWidget(widget) # if you don't want the 'q' key to exit comment this. - widget.AddObserver("ExitEvent", lambda o, e, a=app: a.quit()) + widget.AddObserver('ExitEvent', lambda o, e, a=app: a.quit()) ren = vtkRenderer() widget.GetRenderWindow().AddRenderer(ren) @@ -691,26 +658,135 @@ def QVTKRenderWidgetConeExample(block=False): _keysyms_for_ascii = ( - None, None, None, None, None, None, None, None, - None, "Tab", None, None, None, None, None, None, - None, None, None, None, None, None, None, None, - None, None, None, None, None, None, None, None, - "space", "exclam", "quotedbl", "numbersign", - "dollar", "percent", "ampersand", "quoteright", - "parenleft", "parenright", "asterisk", "plus", - "comma", "minus", "period", "slash", - "0", "1", "2", "3", "4", "5", "6", "7", - "8", "9", "colon", "semicolon", "less", "equal", "greater", "question", - "at", "A", "B", "C", "D", "E", "F", "G", - "H", "I", "J", "K", "L", "M", "N", "O", - "P", "Q", "R", "S", "T", "U", "V", "W", - "X", "Y", "Z", "bracketleft", - "backslash", "bracketright", "asciicircum", "underscore", - "quoteleft", "a", "b", "c", "d", "e", "f", "g", - "h", "i", "j", "k", "l", "m", "n", "o", - "p", "q", "r", "s", "t", "u", "v", "w", - "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "Delete", - ) + None, + None, + None, + None, + None, + None, + None, + None, + None, + 'Tab', + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + 'space', + 'exclam', + 'quotedbl', + 'numbersign', + 'dollar', + 'percent', + 'ampersand', + 'quoteright', + 'parenleft', + 'parenright', + 'asterisk', + 'plus', + 'comma', + 'minus', + 'period', + 'slash', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'colon', + 'semicolon', + 'less', + 'equal', + 'greater', + 'question', + 'at', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'bracketleft', + 'backslash', + 'bracketright', + 'asciicircum', + 'underscore', + 'quoteleft', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + 'braceleft', + 'bar', + 'braceright', + 'asciitilde', + 'Delete', +) _keysyms = { Key.Key_Backspace: 'BackSpace', @@ -805,9 +881,9 @@ def QVTKRenderWidgetConeExample(block=False): Key.Key_F24: 'F24', Key.Key_NumLock: 'Num_Lock', Key.Key_ScrollLock: 'Scroll_Lock', - } +} -if __name__ == "__main__": +if __name__ == '__main__': print(PyQtImpl) QVTKRenderWidgetConeExample() diff --git a/pyvistaqt/utils.py b/pyvistaqt/utils.py index 497b3227..56d1d3ff 100644 --- a/pyvistaqt/utils.py +++ b/pyvistaqt/utils.py @@ -10,8 +10,7 @@ def _check_type(var: Any, var_name: str, var_types: List[Type[Any]]) -> None: types = tuple(var_types) if not isinstance(var, types): raise TypeError( - f"Expected type for ``{var_name}`` is {str(types)}" - f" but {type(var)} was given." + f'Expected type for ``{var_name}`` is {str(types)}' f' but {type(var)} was given.' ) @@ -37,7 +36,7 @@ def _setup_ipython(ipython: Any = None) -> Any: from IPython import get_ipython ipython = get_ipython() - ipython.run_line_magic("gui", "qt") + ipython.run_line_magic('gui', 'qt') # pylint: disable=redefined-outer-name # pylint: disable=import-outside-toplevel @@ -47,12 +46,14 @@ def _setup_ipython(ipython: Any = None) -> Any: return ipython -def _setup_application(app: Optional[QApplication] = None) -> QApplication: +def _setup_application( + app: Optional[QApplication] = None, +) -> QApplication: # run within python if app is None: app = QApplication.instance() if not app: # pragma: no cover - app = QApplication(["PyVista"]) + app = QApplication(['PyVista']) return app diff --git a/requirements_mypy.txt b/requirements_mypy.txt new file mode 100644 index 00000000..e0fef0cd --- /dev/null +++ b/requirements_mypy.txt @@ -0,0 +1,5 @@ +pytest-mypy-plugins +IceSpringPySideStubs-PySide6 +PyQt5-stubs +PySide2-stubs +types-setuptools \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index a61fb0a8..d9e25cc7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,3 +16,9 @@ pytest-qt==4.1.0 pyvista==0.34.1 QtPy==2.1.0 scooby==0.5.12 +flake8-black==0.3.2 +flake8-isort==4.1.1 +flake8-quotes==3.3.1 +check-jsonschema==0.16.2 +pre-commit==2.19.0 +pre-commit-hooks==4.3.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..203a3492 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name = pyvistaqt +version = attr: pyvistaqt._version.__version__ +description = pyvista qt plotter +long_description = file: README.rst, LICENSE +author = PyVista Developers +author_email = info@pyvista.org +license = MIT +classifiers = + Development Status :: 4 - Beta, + Intended Audience :: Science/Research, + Topic :: Scientific/Engineering :: Information Analysis, + License :: OSI Approved :: MIT License, + Operating System :: Microsoft :: Windows, + Operating System :: POSIX, + Operating System :: MacOS, + Programming Language :: Python :: 3.7, + Programming Language :: Python :: 3.8, + Programming Language :: Python :: 3.9 +url = https://github.com/pyvista/pyvistaqt +keywords = + vtk + numpy + plotting + mesh + qt + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = >=3.7 +install_requires = + pyvista>="0.32.0" + QtPy>="1.9.0" + +[options.package_data] +pyvistaqt = + data/*.png \ No newline at end of file diff --git a/setup.py b/setup.py index 23a0a23d..28478b53 100644 --- a/setup.py +++ b/setup.py @@ -1,52 +1,6 @@ -""" -Installation file for python pyvistaqt module -""" -import os -from io import open as io_open +"""Installation file for python pyvistaqt module.""" +from __future__ import annotations from setuptools import setup -package_name = 'pyvistaqt' - -__version__ = None -filepath = os.path.dirname(__file__) -version_file = os.path.join(filepath, package_name, '_version.py') -with io_open(version_file, mode='r') as fd: - exec(fd.read()) - -readme_file = os.path.join(filepath, 'README.rst') - -setup( - name=package_name, - packages=[package_name, package_name], - version=__version__, - description='pyvista qt plotter', - long_description=io_open(readme_file, encoding="utf-8").read(), - author='PyVista Developers', - author_email='info@pyvista.org', - license='MIT', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering :: Information Analysis', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ], - - url='https://github.com/pyvista/pyvistaqt', - keywords='vtk numpy plotting mesh qt', - python_requires='>=3.6.*', - install_requires=[ - 'pyvista>=0.32.0', - 'QtPy>=1.9.0', - ], - package_data={'pyvistaqt': [ - os.path.join('data', '*.png'), - ]} - -) +setup() diff --git a/tests/conftest.py b/tests/conftest.py index 56dae794..718f8741 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,42 +1,12 @@ -import importlib -import sys - import pytest from pyvista.plotting import system_supports_plotting -import pyvistaqt NO_PLOTTING = not system_supports_plotting() -def _check_qt_installed(): - try: - from qtpy import QtCore # noqa - except Exception: - return False - else: - return True - - @pytest.fixture() -def plotting(): +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): - """Require plotting.""" - need_reload = False - if _check_qt_installed(): - need_reload = True - monkeypatch.setenv('QT_API', 'bad_name') - sys.modules.pop('qtpy') - importlib.reload(pyvistaqt) - assert 'qtpy' not in sys.modules + pytest.skip(NO_PLOTTING, reason='Requires system to support plotting') yield - monkeypatch.undo() - if need_reload: - importlib.reload(pyvistaqt) - assert 'qtpy' in sys.modules diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 00000000..e82a674e --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,37 @@ +"""Tests for typehinting.""" +from __future__ import annotations + +import typing + +from mypy import api + +if typing.TYPE_CHECKING: # pragma: no cover + from qtpy import QtWidgets + + +def test_import( + qapp: QtWidgets.QApplication, # pylint: disable=unused-argument +) -> None: + """Regression test for `Issue #163`_. + + Args: + qapp (QtWidgets.QApplication): ``pytest-qt`` fixture for holding + the ``QApplication`` instance. + + A ``QApplication`` must exist before a ``QWidget`` can be + created. This fixture is used implicitly in this test. + + .. _`Issue #163`: + https://github.com/pyvista/pyvistaqt/issues/163 + """ + + src = """ +from pyvistaqt import MainWindow + +window = MainWindow() +window.setWindowTitle('window') +""" + + stdout, _, _ = api.run(['-c', src]) + + assert '"MainWindow" has no attribute "setWindowTitle"' not in stdout diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d099b8a2..9fcbfc9f 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,26 +1,41 @@ import os -from packaging.version import Version import platform import numpy as np import pytest import pyvista import vtk -from qtpy.QtWidgets import QAction, QFrame, QMenuBar, QToolBar, QVBoxLayout +from packaging.version import Version +from pyvista import BasePlotter +from pyvista.plotting import Renderer from qtpy import QtCore -from qtpy.QtCore import Qt, QPoint, QPointF, QMimeData, QUrl +from qtpy.QtCore import QMimeData, QPoint, QPointF, Qt, QUrl from qtpy.QtGui import QDragEnterEvent, QDropEvent -from qtpy.QtWidgets import (QTreeWidget, QStackedWidget, QCheckBox, - QGestureEvent, QPinchGesture) -from pyvistaqt.plotting import global_theme -from pyvista.plotting import Renderer +from qtpy.QtWidgets import ( + QAction, + QCheckBox, + QFrame, + QGestureEvent, + QMenuBar, + QPinchGesture, + QStackedWidget, + QToolBar, + QTreeWidget, + QVBoxLayout, +) import pyvistaqt -from pyvistaqt import MultiPlotter, BackgroundPlotter, MainWindow, QtInteractor -from pyvistaqt.plotting import Counter, QTimer, QVTKRenderWindowInteractor -from pyvistaqt.editor import Editor +from pyvistaqt import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor from pyvistaqt.dialog import FileDialog -from pyvistaqt.utils import _setup_application, _create_menu_bar, _check_type +from pyvistaqt.editor import Editor +from pyvistaqt.plotting import ( + Counter, + QTimer, + QVTKRenderWindowInteractor, + _no_base_plotter_init, + global_theme, +) +from pyvistaqt.utils import _check_type, _create_menu_bar, _setup_application class TstWindow(MainWindow): @@ -59,14 +74,18 @@ def __init__(self, parent=None, show=True, off_screen=True): self.show() def add_sphere(self): - sphere = pyvista.Sphere( - phi_resolution=6, - theta_resolution=6 - ) + sphere = pyvista.Sphere(phi_resolution=6, theta_resolution=6) self.vtk_widget.add_mesh(sphere) self.vtk_widget.reset_camera() +def test_base_plotter_noop(qtbot): + with _no_base_plotter_init(): + kwargs = {} + assert BasePlotter.__init__(kwargs) is None + assert BasePlotter.__init__ is not None + + def test_create_menu_bar(qtbot): menu_bar = _create_menu_bar(parent=None) qtbot.addWidget(menu_bar) @@ -87,7 +106,7 @@ def test_file_dialog(tmpdir, qtbot): dialog.emit_accepted() # test no result - p = tmpdir.mkdir("tmp").join("foo.png") + p = tmpdir.mkdir('tmp').join('foo.png') p.write('foo') assert os.path.isfile(p) @@ -107,10 +126,10 @@ def test_file_dialog(tmpdir, qtbot): def test_check_type(): - with pytest.raises(TypeError, match="Expected type"): - _check_type(0, "foo", [str]) - _check_type(0, "foo", [int, float]) - _check_type("foo", "foo", [str]) + 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): @@ -124,12 +143,18 @@ def test_mouse_interactions(qtbot): plotter.close() -@pytest.mark.skipif(platform.system()=="Windows" and platform.python_version()[:-1]=="3.8.", reason="#51") +@pytest.mark.skipif( + platform.system() == 'Windows' and platform.python_version()[:-1] == '3.8.', + reason='#51', +) def test_ipython(qapp): import IPython - cmd = "from pyvistaqt import BackgroundPlotter as Plotter;" \ - "p = Plotter(show=False, off_screen=False); p.close(); exit()" - IPython.start_ipython(argv=["-c", cmd]) + + 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): @@ -141,14 +166,14 @@ def test_depth_peeling(qtbot): qtbot.addWidget(plotter.app_window) assert not plotter.renderer.GetUseDepthPeeling() plotter.close() - global_theme.depth_peeling["enabled"] = True + global_theme.depth_peeling['enabled'] = True plotter = BackgroundPlotter(app_window_class=SuperWindow) assert isinstance(plotter.app_window, SuperWindow) assert isinstance(plotter.app_window, MainWindow) qtbot.addWidget(plotter.app_window) assert plotter.renderer.GetUseDepthPeeling() plotter.close() - global_theme.depth_peeling["enabled"] = False + global_theme.depth_peeling['enabled'] = False def test_off_screen(qtbot): @@ -204,7 +229,7 @@ def test_editor(qtbot, plotting): # test editor closing plotter = BackgroundPlotter(editor=True, off_screen=False) qtbot.addWidget(plotter.app_window) - assert_hasattr(plotter, "editor", Editor) + assert_hasattr(plotter, 'editor', Editor) editor = plotter.editor assert not editor.isVisible() with qtbot.wait_exposed(editor): @@ -227,7 +252,7 @@ def test_editor(qtbot, plotting): plotter.subplot(1, 0) plotter.show_axes() - assert_hasattr(editor, "tree_widget", QTreeWidget) + assert_hasattr(editor, 'tree_widget', QTreeWidget) tree_widget = editor.tree_widget top_item = tree_widget.topLevelItem(0) # any renderer will do assert top_item is not None @@ -238,7 +263,7 @@ def test_editor(qtbot, plotting): # toggle all the renderer-associated checkboxes twice # to ensure that slots are called for True and False - assert_hasattr(editor, "stacked_widget", QStackedWidget) + assert_hasattr(editor, 'stacked_widget', QStackedWidget) stacked_widget = editor.stacked_widget page_idx = top_item.data(0, Qt.ItemDataRole.UserRole) page_widget = stacked_widget.widget(page_idx) @@ -260,6 +285,7 @@ def test_editor(qtbot, plotting): def test_qt_interactor(qtbot, plotting): from pyvista.plotting.plotting import _ALL_PLOTTERS, close_all + close_all() # this is necessary to test _ALL_PLOTTERS assert len(_ALL_PLOTTERS) == 0 @@ -267,18 +293,18 @@ def test_qt_interactor(qtbot, plotting): qtbot.addWidget(window) # register the main widget # check that TstWindow.__init__() is called - assert_hasattr(window, "vtk_widget", QtInteractor) + assert_hasattr(window, 'vtk_widget', QtInteractor) vtk_widget = window.vtk_widget # QtInteractor # check that QtInteractor.__init__() is called - assert hasattr(vtk_widget, "iren") - assert_hasattr(vtk_widget, "render_timer", QTimer) + assert hasattr(vtk_widget, 'iren') + assert_hasattr(vtk_widget, 'render_timer', QTimer) # check that BasePlotter.__init__() is called - assert_hasattr(vtk_widget, "_closed", bool) - assert_hasattr(vtk_widget, "renderer", vtk.vtkRenderer) + assert_hasattr(vtk_widget, '_closed', bool) + assert_hasattr(vtk_widget, 'renderer', vtk.vtkRenderer) # check that QVTKRenderWindowInteractorAdapter.__init__() is called - assert_hasattr(vtk_widget, "interactor", QVTKRenderWindowInteractor) + assert_hasattr(vtk_widget, 'interactor', QVTKRenderWindowInteractor) interactor = vtk_widget.interactor # QVTKRenderWindowInteractor render_timer = vtk_widget.render_timer # QTimer @@ -315,24 +341,23 @@ def test_qt_interactor(qtbot, plotting): # check that BasePlotter.close() is called if Version(pyvista.__version__) < Version('0.27.0'): - assert not hasattr(vtk_widget, "iren") + assert not hasattr(vtk_widget, 'iren') assert vtk_widget._closed # check that BasePlotter.__init__() is called only once assert len(_ALL_PLOTTERS) == 1 -@pytest.mark.parametrize('show_plotter', [ - True, - False, - ]) +@pytest.mark.parametrize( + 'show_plotter', + [ + True, + False, + ], +) def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): - plotter = BackgroundPlotter( - show=show_plotter, - off_screen=False, - title='Testing Window' - ) - assert_hasattr(plotter, "app_window", MainWindow) + 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 @@ -344,7 +369,7 @@ def test_background_plotting_axes_scale(qtbot, show_plotter, plotting): assert window.isVisible() plotter.add_mesh(pyvista.Sphere()) - assert_hasattr(plotter, "renderer", Renderer) + assert_hasattr(plotter, 'renderer', Renderer) renderer = plotter.renderer assert len(renderer._actors) == 1 assert np.any(plotter.mesh.points) @@ -381,7 +406,11 @@ def test_background_plotting_camera(qtbot, plotting): cpos = [(0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)] plotter.camera_position = cpos plotter.save_camera_position() - plotter.camera_position = [(0.0, 0.0, 3.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + plotter.camera_position = [ + (0.0, 0.0, 3.0), + (0.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + ] # load existing position # NOTE: 2 because first two (0 and 1) buttons save and clear positions @@ -396,7 +425,6 @@ def test_background_plotting_camera(qtbot, plotting): @pytest.mark.parametrize('other_views', [None, 0, [0]]) def test_link_views_across_plotters(other_views): - def _to_array(camera_position): return np.asarray([list(row) for row in camera_position]) @@ -408,20 +436,32 @@ def _to_array(camera_position): plotter_one.link_views_across_plotters(plotter_two, other_views=other_views) - plotter_one.camera_position = [(0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + plotter_one.camera_position = [ + (0.0, 0.0, 1.0), + (0.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + ] np.testing.assert_allclose( _to_array(plotter_one.camera_position), _to_array(plotter_two.camera_position), ) - plotter_two.camera_position = [(0.0, 0.0, 3.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + plotter_two.camera_position = [ + (0.0, 0.0, 3.0), + (0.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + ] np.testing.assert_allclose( _to_array(plotter_one.camera_position), _to_array(plotter_two.camera_position), ) plotter_one.unlink_views() - plotter_one.camera_position = [(0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + plotter_one.camera_position = [ + (0.0, 0.0, 1.0), + (0.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + ] with pytest.raises(AssertionError): np.testing.assert_allclose( @@ -433,21 +473,21 @@ def _to_array(camera_position): with pytest.raises(TypeError, match=match): plotter_one.link_views_across_plotters(plotter_two, other_views=[0.0]) -@pytest.mark.parametrize('show_plotter', [ - True, - False, - ]) + +@pytest.mark.parametrize( + 'show_plotter', + [ + True, + False, + ], +) def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): # setup filesystem - output_dir = str(tmpdir.mkdir("tmpdir")) + output_dir = str(tmpdir.mkdir('tmpdir')) assert os.path.isdir(output_dir) - plotter = BackgroundPlotter( - show=show_plotter, - off_screen=False, - title='Testing Window' - ) - assert_hasattr(plotter, "app_window", MainWindow) + 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 @@ -459,7 +499,7 @@ def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): assert window.isVisible() plotter.add_mesh(pyvista.Sphere()) - assert_hasattr(plotter, "renderer", Renderer) + assert_hasattr(plotter, 'renderer', Renderer) renderer = plotter.renderer assert len(renderer._actors) == 1 assert np.any(plotter.mesh.points) @@ -467,7 +507,7 @@ def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): dlg = plotter._qt_screenshot(show=False) # FileDialog qtbot.addWidget(dlg) # register the dialog - filename = str(os.path.join(output_dir, "tmp.png")) + filename = str(os.path.join(output_dir, 'tmp.png')) dlg.selectFile(filename) # show the dialog @@ -486,21 +526,20 @@ def test_background_plotter_export_files(qtbot, tmpdir, show_plotter, plotting): assert os.path.isfile(filename) -@pytest.mark.parametrize('show_plotter', [ - True, - False, - ]) +@pytest.mark.parametrize( + 'show_plotter', + [ + True, + False, + ], +) def test_background_plotter_export_vtkjs(qtbot, tmpdir, show_plotter, plotting): # setup filesystem - output_dir = str(tmpdir.mkdir("tmpdir")) + output_dir = str(tmpdir.mkdir('tmpdir')) assert os.path.isdir(output_dir) - plotter = BackgroundPlotter( - show=show_plotter, - off_screen=False, - title='Testing Window' - ) - assert_hasattr(plotter, "app_window", MainWindow) + 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 @@ -512,7 +551,7 @@ def test_background_plotter_export_vtkjs(qtbot, tmpdir, show_plotter, plotting): assert window.isVisible() plotter.add_mesh(pyvista.Sphere()) - assert_hasattr(plotter, "renderer", Renderer) + assert_hasattr(plotter, 'renderer', Renderer) renderer = plotter.renderer assert len(renderer._actors) == 1 assert np.any(plotter.mesh.points) @@ -520,7 +559,7 @@ def test_background_plotter_export_vtkjs(qtbot, tmpdir, show_plotter, plotting): dlg = plotter._qt_export_vtkjs(show=False) # FileDialog qtbot.addWidget(dlg) # register the dialog - filename = str(os.path.join(output_dir, "tmp")) + filename = str(os.path.join(output_dir, 'tmp')) dlg.selectFile(filename) # show the dialog @@ -549,7 +588,7 @@ def test_background_plotting_orbit(qtbot, plotting): def test_background_plotting_toolbar(qtbot, plotting): with pytest.raises(TypeError, match='toolbar'): - p = BackgroundPlotter(off_screen=False, toolbar="foo") + p = BackgroundPlotter(off_screen=False, toolbar='foo') p.close() plotter = BackgroundPlotter(off_screen=False, toolbar=False) @@ -560,10 +599,10 @@ def test_background_plotting_toolbar(qtbot, plotting): plotter = BackgroundPlotter(off_screen=False) - assert_hasattr(plotter, "app_window", MainWindow) - assert_hasattr(plotter, "default_camera_tool_bar", QToolBar) - assert_hasattr(plotter, "saved_camera_positions", list) - assert_hasattr(plotter, "saved_cameras_tool_bar", QToolBar) + assert_hasattr(plotter, 'app_window', MainWindow) + assert_hasattr(plotter, 'default_camera_tool_bar', QToolBar) + assert_hasattr(plotter, 'saved_camera_positions', list) + assert_hasattr(plotter, 'saved_cameras_tool_bar', QToolBar) window = plotter.app_window default_camera_tool_bar = plotter.default_camera_tool_bar @@ -583,7 +622,7 @@ def test_background_plotting_toolbar(qtbot, plotting): def test_background_plotting_menu_bar(qtbot, plotting): with pytest.raises(TypeError, match='menu_bar'): - p = BackgroundPlotter(off_screen=False, menu_bar="foo") + p = BackgroundPlotter(off_screen=False, menu_bar='foo') p.close() plotter = BackgroundPlotter(off_screen=False, menu_bar=False) @@ -593,11 +632,11 @@ def test_background_plotting_menu_bar(qtbot, plotting): plotter = BackgroundPlotter(off_screen=False) # menu_bar=True - assert_hasattr(plotter, "app_window", MainWindow) - assert_hasattr(plotter, "main_menu", QMenuBar) - assert_hasattr(plotter, "_menu_close_action", QAction) - assert_hasattr(plotter, "_edl_action", QAction) - assert_hasattr(plotter, "_parallel_projection_action", QAction) + assert_hasattr(plotter, 'app_window', MainWindow) + assert_hasattr(plotter, 'main_menu', QMenuBar) + assert_hasattr(plotter, '_menu_close_action', QAction) + assert_hasattr(plotter, '_edl_action', QAction) + assert_hasattr(plotter, '_parallel_projection_action', QAction) window = plotter.app_window main_menu = plotter.main_menu @@ -627,8 +666,8 @@ def test_background_plotting_menu_bar(qtbot, plotting): def test_drop_event(tmpdir): - output_dir = str(tmpdir.mkdir("tmpdir")) - filename = str(os.path.join(output_dir, "tmp.vtk")) + output_dir = str(tmpdir.mkdir('tmpdir')) + filename = str(os.path.join(output_dir, 'tmp.vtk')) mesh = pyvista.Cone() mesh.save(filename) assert os.path.isfile(filename) @@ -648,8 +687,8 @@ def test_drop_event(tmpdir): def test_drag_event(tmpdir): - output_dir = str(tmpdir.mkdir("tmpdir")) - filename = str(os.path.join(output_dir, "tmp.vtk")) + output_dir = str(tmpdir.mkdir('tmpdir')) + filename = str(os.path.join(output_dir, 'tmp.vtk')) mesh = pyvista.Cone() mesh.save(filename) assert os.path.isfile(filename) @@ -698,9 +737,9 @@ def update_app_icon(slf): 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_hasattr(plotter, 'app_window', MainWindow) + assert_hasattr(plotter, '_callback_timer', QTimer) + assert_hasattr(plotter, 'counters', list) assert plotter._last_update_time == -np.inf sphere = pyvista.Sphere() @@ -721,13 +760,17 @@ def update_app_icon(slf): assert update_count[0] in [1, 2] 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.) - # Maybe someday manually setting "set_icon" should disable update_app_icon? + with pytest.raises(ValueError, match='ndarray with shape'): + plotter.set_icon(0.0) + # Maybe someday manually setting 'set_icon' should disable update_app_icon? # Strings also supported directly by QIcon - plotter.set_icon(os.path.join( - os.path.dirname(pyvistaqt.__file__), "data", - "pyvista_logo_square.png")) + plotter.set_icon( + os.path.join( + os.path.dirname(pyvistaqt.__file__), + 'data', + 'pyvista_logo_square.png', + ) + ) callback_timer.stop() assert not callback_timer.isActive() @@ -757,34 +800,41 @@ def update_app_icon(slf): assert not callback_timer.isActive() # window stops the callback -@pytest.mark.parametrize('close_event', [ - "plotter_close", - "window_close", - "q_key_press", - "menu_exit", - "del_finalizer", - ]) -@pytest.mark.parametrize('empty_scene', [ - True, - False, - ]) +@pytest.mark.parametrize( + 'close_event', + [ + 'plotter_close', + 'window_close', + 'q_key_press', + 'menu_exit', + 'del_finalizer', + ], +) +@pytest.mark.parametrize( + 'empty_scene', + [ + True, + False, + ], +) def test_background_plotting_close(qtbot, close_event, empty_scene, plotting): from pyvista.plotting.plotting import _ALL_PLOTTERS, close_all + close_all() # this is necessary to test _ALL_PLOTTERS assert len(_ALL_PLOTTERS) == 0 plotter = _create_testing_scene(empty_scene) # check that BackgroundPlotter.__init__() is called - assert_hasattr(plotter, "app_window", MainWindow) - assert_hasattr(plotter, "main_menu", QMenuBar) + assert_hasattr(plotter, 'app_window', MainWindow) + assert_hasattr(plotter, 'main_menu', QMenuBar) # check that QtInteractor.__init__() is called - assert hasattr(plotter, "iren") - assert_hasattr(plotter, "render_timer", QTimer) + assert hasattr(plotter, 'iren') + assert_hasattr(plotter, 'render_timer', QTimer) # check that BasePlotter.__init__() is called - assert_hasattr(plotter, "_closed", bool) + assert_hasattr(plotter, '_closed', bool) # check that QVTKRenderWindowInteractorAdapter._init__() is called - assert_hasattr(plotter, "interactor", QVTKRenderWindowInteractor) + assert_hasattr(plotter, 'interactor', QVTKRenderWindowInteractor) window = plotter.app_window # MainWindow main_menu = plotter.main_menu @@ -812,15 +862,15 @@ def test_background_plotting_close(qtbot, close_event, empty_scene, plotting): assert not plotter._closed with qtbot.wait_signals([window.signal_close], timeout=500): - if close_event == "plotter_close": + if close_event == 'plotter_close': plotter.close() - elif close_event == "window_close": + elif close_event == 'window_close': window.close() - elif close_event == "q_key_press": - qtbot.keyClick(interactor, "q") - elif close_event == "menu_exit": + elif close_event == 'q_key_press': + qtbot.keyClick(interactor, 'q') + elif close_event == 'menu_exit': plotter._menu_close_action.trigger() - elif close_event == "del_finalizer": + elif close_event == 'del_finalizer': plotter.__del__() # check that the widgets are closed @@ -831,7 +881,7 @@ def test_background_plotting_close(qtbot, close_event, empty_scene, plotting): # check that BasePlotter.close() is called if Version(pyvista.__version__) < Version('0.27.0'): - assert not hasattr(window.vtk_widget, "iren") + assert not hasattr(window.vtk_widget, 'iren') assert plotter._closed # check that BasePlotter.__init__() is called only once @@ -899,10 +949,7 @@ def _create_testing_scene(empty_scene, show=False, off_screen=False): plotter.add_mesh(cylinder, smooth_shading=True) plotter.show_bounds() plotter.subplot(1, 1) - sphere = pyvista.Sphere( - phi_resolution=6, - theta_resolution=6 - ) + sphere = pyvista.Sphere(phi_resolution=6, theta_resolution=6) plotter.add_mesh(sphere) plotter.enable_cell_picking() return plotter diff --git a/tests/test_qt.py b/tests/test_qt.py index abb74984..63de4de2 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -1,13 +1,30 @@ +import importlib +import sys + import pytest +import pyvistaqt + + +def _check_qt_installed(): + try: + from qtpy import QtCore # noqa + except Exception: + return False + else: + return True + -def test_no_qt_binding(no_qt): - from pyvistaqt import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor - with pytest.raises(RuntimeError, match="No Qt binding"): - BackgroundPlotter() - with pytest.raises(RuntimeError, match="No Qt binding"): - MainWindow() - with pytest.raises(RuntimeError, match="No Qt binding"): - MultiPlotter() - with pytest.raises(RuntimeError, match="No Qt binding"): - QtInteractor() +def test_no_qt_binding(monkeypatch): + need_reload = False + if _check_qt_installed(): + need_reload = True + monkeypatch.setenv('QT_API', 'bad_name') + sys.modules.pop('qtpy') + assert 'qtpy' not in sys.modules + with pytest.raises(RuntimeError, match='No Qt binding'): + importlib.reload(pyvistaqt) + monkeypatch.undo() + if need_reload: + importlib.reload(pyvistaqt) + assert 'qtpy' in sys.modules