From 95c5adb68ec638284e8e6f6110ce1c74e80b3b3f Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 26 Apr 2025 12:06:58 -0400 Subject: [PATCH 01/10] refactor: lots of typing updates --- .github/workflows/ci.yml | 9 ++++ .pre-commit-config.yaml | 3 ++ pyproject.toml | 26 +++++++--- src/ndv/controllers/_array_viewer.py | 4 +- src/ndv/data.py | 2 +- src/ndv/models/_array_display_model.py | 17 ++++--- src/ndv/models/_data_display_model.py | 2 +- src/ndv/models/_data_wrapper.py | 6 ++- src/ndv/models/_lut_model.py | 4 +- src/ndv/models/_mapping.py | 10 ++-- src/ndv/models/_reducer.py | 1 + src/ndv/views/_pygfx/_array_canvas.py | 53 +++++++++++--------- src/ndv/views/_pygfx/_histogram.py | 4 +- src/ndv/views/_pygfx/_util.py | 8 +-- src/ndv/views/_qt/_array_view.py | 31 ++++++------ src/ndv/views/_qt/_main_thread.py | 9 +++- src/ndv/views/_vispy/_array_canvas.py | 33 ++++++------- src/ndv/views/_vispy/_histogram.py | 26 ++++++---- src/ndv/views/_vispy/_plot_widget.py | 11 ++--- src/ndv/views/_wx/_app.py | 2 +- src/ndv/views/_wx/_array_view.py | 53 ++++++++++---------- src/ndv/views/_wx/_labeled_slider.py | 2 +- src/ndv/views/_wx/range_slider.py | 39 +++++++++------ src/ndv/views/bases/_app.py | 19 ++++--- uv.lock | 68 ++++++++++++++++++++++++++ 25 files changed, 289 insertions(+), 153 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feada717..eebe4fd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,15 @@ jobs: - uses: actions/checkout@v4 - run: pipx run check-manifest + pyright: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: uv run pyright + test: name: ${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.gui }} ${{ matrix.canvas }} runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b34772e..cf0e787b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,3 +32,6 @@ repos: - pydantic - psygnal - IPython + - types-wxpython + - qtpy + - git+https://github.com/tlambert03/PyQt6-stubs.git@v6.7.3 diff --git a/pyproject.toml b/pyproject.toml index cf954e61..a2bf964b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,15 +64,12 @@ pyside = [ "superqt[iconify,pyside6] >=0.7.2", # https://github.com/pyapp-kit/ndv/issues/59 "pyside6 ==6.6.3; sys_platform == 'win32'", - "numpy >=1.23,<2; sys_platform == 'win32'", # needed for pyside6.6 + "numpy >=1.23,<2; sys_platform == 'win32'", # needed for pyside6.6 "pyside6 >=6.4", "pyside6 >=6.6; python_version >= '3.12'", "qtpy >=2", ] -wxpython = [ - "pyconify>=0.2.1", - "wxpython >=4.2.2", -] +wxpython = ["pyconify>=0.2.1", "wxpython >=4.2.2"] # Supported Canavs backends vispy = ["vispy>=0.14.3", "pyopengl >=3.1"] @@ -110,7 +107,7 @@ dev = [ # omitting wxpython from dev env for now # because `uv sync && pytest hangs` on a wx test in the "full" env # use `make test extras=wx,[pygfx|vispy] isolated=1` to test - "ndv[vispy,pygfx,pyqt,jupyter]", + "ndv[vispy,pygfx,pyqt,jupyter]", "imageio[tifffile] >=2.20", "ipykernel>=6.29.5", "ipython>=8.18.1", @@ -119,6 +116,9 @@ dev = [ "pre-commit>=4.1.0", "rich>=13.9.4", "ruff>=0.9.4", + "pyright>=1.1.400", + "types-wxpython", + "pydevd", ] docs = [ "mkdocs >=1.6.1", @@ -189,6 +189,20 @@ plugins = ["pydantic.mypy"] module = ["jupyter_rfb.*", "vispy.*", "ipywidgets.*"] ignore_missing_imports = true +# https://github.com/microsoft/pyright/blob/main/docs/configuration.md +[tool.pyright] +include = ["src"] +pythonVersion = "3.9" +enableExperimentalFeatures = true +# reportMissingImports = false +reportOptionalMemberAccess = "none" +reportIncompatibleMethodOverride = "none" +reportAttributeAccessIssue = "none" +reportPrivateImportUsage = "none" +reportCallIssue = "none" +verboseOutput = true +venv = ".venv" + # https://docs.pytest.org/ [tool.pytest.ini_options] addopts = ["-v"] diff --git a/src/ndv/controllers/_array_viewer.py b/src/ndv/controllers/_array_viewer.py index 4e06fd4e..453b043a 100644 --- a/src/ndv/controllers/_array_viewer.py +++ b/src/ndv/controllers/_array_viewer.py @@ -19,6 +19,8 @@ from concurrent.futures import Future from typing import Any, Unpack + import numpy.typing as npt + from ndv._types import ChannelKey, MouseMoveEvent from ndv.models._array_display_model import ArrayDisplayModelKwargs from ndv.views.bases import HistogramCanvas @@ -269,7 +271,7 @@ def _add_histogram(self, channel: ChannelKey = None) -> None: self._histograms[channel] = hist def _update_channel_dtype( - self, channel: ChannelKey, dtype: np.typing.DTypeLike | None = None + self, channel: ChannelKey, dtype: npt.DTypeLike | None = None ) -> None: if not (ctrl := self._lut_controllers.get(channel, None)): return diff --git a/src/ndv/data.py b/src/ndv/data.py index f6c85082..d3d03ef6 100644 --- a/src/ndv/data.py +++ b/src/ndv/data.py @@ -1,5 +1,6 @@ """Sample data for testing and examples.""" +# pyright: reportMissingImports=none from __future__ import annotations from typing import Any @@ -41,7 +42,6 @@ def nd_sine_wave( angle = np.pi / angle_dim * angle_idx # Rotate x and y coordinates xr = np.cos(angle) * x - np.sin(angle) * y - np.sin(angle) * x + np.cos(angle) * y # Compute the sine wave sine_wave = (amplitude * 0.5) * np.sin(frequency * xr + phase) diff --git a/src/ndv/models/_array_display_model.py b/src/ndv/models/_array_display_model.py index 6bc65610..c261b48f 100644 --- a/src/ndv/models/_array_display_model.py +++ b/src/ndv/models/_array_display_model.py @@ -4,6 +4,7 @@ from enum import Enum from typing import TYPE_CHECKING, Literal, Optional, TypedDict, Union, cast +from cmap import Colormap from pydantic import Field, computed_field, model_validator from typing_extensions import Self, TypeAlias @@ -27,21 +28,21 @@ class LutModelKwargs(TypedDict, total=False): """Keyword arguments for `LUTModel`.""" visible: bool - cmap: cmap.Colormap | cmap._colormap.ColorStopsLike - clims: tuple[float, float] | None + cmap: "cmap.Colormap | cmap._colormap.ColorStopsLike" + clims: "tuple[float, float] | None" gamma: float autoscale: AutoscaleType class ArrayDisplayModelKwargs(TypedDict, total=False): """Keyword arguments for `ArrayDisplayModel`.""" - visible_axes: tuple[AxisKey, AxisKey, AxisKey] | tuple[AxisKey, AxisKey] + visible_axes: "tuple[AxisKey, AxisKey, AxisKey] | tuple[AxisKey, AxisKey]" current_index: Mapping[AxisKey, Union[int, slice]] - channel_mode: "ChannelMode" | Literal["grayscale", "composite", "color", "rgba"] + channel_mode: 'ChannelMode | Literal["grayscale", "composite", "color", "rgba"]' channel_axis: Optional[AxisKey] - reducers: Mapping[AxisKey | None, ReducerType] - luts: Mapping[int | None, LUTModel | LutModelKwargs] - default_lut: LUTModel | LutModelKwargs + reducers: Mapping["AxisKey | None", ReducerType] + luts: Mapping["int | None", "LUTModel | LutModelKwargs"] + default_lut: "LUTModel | LutModelKwargs" # map of axis to index/slice ... i.e. the current subset of data being displayed @@ -59,7 +60,7 @@ class ArrayDisplayModelKwargs(TypedDict, total=False): def _default_luts() -> LutMap: colors = ["green", "magenta", "cyan", "red", "blue", "yellow"] return ValidatedEventedDict( - (i, LUTModel(cmap=color)) for i, color in enumerate(colors) + (i, LUTModel(cmap=Colormap(color))) for i, color in enumerate(colors) ) diff --git a/src/ndv/models/_data_display_model.py b/src/ndv/models/_data_display_model.py index b8203b81..ad799705 100644 --- a/src/ndv/models/_data_display_model.py +++ b/src/ndv/models/_data_display_model.py @@ -77,7 +77,7 @@ class _ArrayDataDisplayModel(NDVModel): The data wrapper. Provides the actual data to be displayed """ - display: ArrayDisplayModel = Field(default_factory=ArrayDisplayModel) + display: ArrayDisplayModel = Field(default_factory=lambda: ArrayDisplayModel()) data_wrapper: Optional[DataWrapper] = None def model_post_init(self, __context: Any) -> None: diff --git a/src/ndv/models/_data_wrapper.py b/src/ndv/models/_data_wrapper.py index 640c1cfa..274ebf5d 100644 --- a/src/ndv/models/_data_wrapper.py +++ b/src/ndv/models/_data_wrapper.py @@ -1,5 +1,6 @@ """In this module, we provide built-in support for many array types.""" +# pyright: reportMissingImports=none from __future__ import annotations import json @@ -17,7 +18,7 @@ if TYPE_CHECKING: from collections.abc import Container, Iterator - from typing import Any, TypeAlias, TypeGuard + from typing import Any, Union import dask.array.core as da import numpy.typing as npt @@ -28,8 +29,9 @@ import torch import xarray as xr from pydantic import GetCoreSchemaHandler + from typing_extensions import TypeAlias, TypeGuard - Index: TypeAlias = int | slice + Index: TypeAlias = Union[int, slice] class SupportsIndexing(Protocol): diff --git a/src/ndv/models/_lut_model.py b/src/ndv/models/_lut_model.py index d3aa7447..2f5366f9 100644 --- a/src/ndv/models/_lut_model.py +++ b/src/ndv/models/_lut_model.py @@ -127,8 +127,8 @@ class ClimsStdDev(ClimPolicy): center: Optional[float] = None # None means center around the mean def get_limits(self, data: npt.NDArray) -> tuple[float, float]: - center = np.nanmean(data) if self.center is None else self.center - diff = self.n_stdev * np.nanstd(data) + center = float(np.nanmean(data) if self.center is None else self.center) + diff = float(self.n_stdev * np.nanstd(data)) return center - diff, center + diff def __eq__(self, other: object) -> bool: diff --git a/src/ndv/models/_mapping.py b/src/ndv/models/_mapping.py index 340f24f4..c4e87d0f 100644 --- a/src/ndv/models/_mapping.py +++ b/src/ndv/models/_mapping.py @@ -43,7 +43,7 @@ class ValidatedEventedDict(MutableMapping[_KT, _VT]): def __init__(self) -> None: ... @overload def __init__( # type: ignore[misc] - self: dict[str, _VT], + self: dict, key_validator: Callable[[Any], _KT] | None = None, value_validator: Callable[[Any], _VT] | None = None, **kwargs: _VT, @@ -58,10 +58,10 @@ def __init__( ) -> None: ... @overload def __init__( # type: ignore[misc] - self: dict[str, _VT], + self: dict, map: SupportsKeysAndGetItem[str, _VT], /, - key_validator: Callable[[Any], _KT] | None = ..., + key_validator: Callable[[Any], _KT] | None = None, value_validator: Callable[[Any], _VT] | None = ..., validate_lookup: bool = ..., **kwargs: _VT, @@ -77,10 +77,10 @@ def __init__( ) -> None: ... @overload def __init__( # type: ignore[misc] - self: dict[str, _VT], + self: dict, iterable: Iterable[tuple[str, _VT]], /, - key_validator: Callable[[Any], _KT] | None = ..., + key_validator: Callable[[Any], _KT] | None = None, value_validator: Callable[[Any], _VT] | None = ..., validate_lookup: bool = ..., **kwargs: _VT, diff --git a/src/ndv/models/_reducer.py b/src/ndv/models/_reducer.py index c374a7e8..aa0f2aef 100644 --- a/src/ndv/models/_reducer.py +++ b/src/ndv/models/_reducer.py @@ -18,6 +18,7 @@ class Reducer(Protocol): def __call__(self, a: npt.ArrayLike, axis: _ShapeLike = ...) -> npt.ArrayLike: """Reduce an array along an axis.""" + raise NotImplementedError() def _str_to_callable(obj: Any) -> Callable: diff --git a/src/ndv/views/_pygfx/_array_canvas.py b/src/ndv/views/_pygfx/_array_canvas.py index 290d5e30..dcff3a0b 100755 --- a/src/ndv/views/_pygfx/_array_canvas.py +++ b/src/ndv/views/_pygfx/_array_canvas.py @@ -25,15 +25,16 @@ if TYPE_CHECKING: from collections.abc import Sequence - from typing import TypeAlias + from typing import Union from pygfx.materials import ImageBasicMaterial from pygfx.resources import Texture + from typing_extensions import TypeAlias from wgpu.gui.jupyter import JupyterWgpuCanvas from wgpu.gui.qt import QWgpuCanvas from wgpu.gui.wx import WxWgpuCanvas - WgpuCanvas: TypeAlias = QWgpuCanvas | JupyterWgpuCanvas | WxWgpuCanvas + WgpuCanvas: TypeAlias = Union[QWgpuCanvas, JupyterWgpuCanvas, WxWgpuCanvas] def _is_inside(bounding_box: np.ndarray, pos: Sequence[float]) -> bool: @@ -58,7 +59,7 @@ def data(self) -> np.ndarray: def set_data(self, data: np.ndarray) -> None: # If dimensions are unchanged, reuse the buffer if data.shape == self._grid.data.shape: - self._grid.data[:] = data + self._grid.data[:] = data # pyright: ignore[reportOptionalSubscript] self._grid.update_range((0, 0, 0), self._grid.size) # Otherwise, the size (and maybe number of dimensions) changed # - we need a new buffer @@ -202,7 +203,7 @@ def set_handles(self, color: _cmap.Color) -> None: self._handles.material.color = color.rgba self._render() - def _create_fill(self) -> pygfx.Mesh | None: + def _create_fill(self) -> pygfx.Mesh: fill = pygfx.Mesh( geometry=pygfx.Geometry( positions=self._positions, @@ -212,7 +213,7 @@ def _create_fill(self) -> pygfx.Mesh | None: ) return fill - def _create_outline(self) -> pygfx.Line | None: + def _create_outline(self) -> pygfx.Line: outline = pygfx.Line( geometry=pygfx.Geometry( positions=self._positions, @@ -222,20 +223,24 @@ def _create_outline(self) -> pygfx.Line | None: ) return outline - def _create_handles(self) -> pygfx.Points | None: + def _create_handles(self) -> pygfx.Points: geometry = pygfx.Geometry(positions=self._positions[:-1]) handles = pygfx.Points( geometry=geometry, # FIXME Size in pixels is not ideal for selection. # TODO investigate what size_mode = vertex does... - material=pygfx.PointsMaterial(size=1.5 * self._handle_rad), + material=pygfx.PointsMaterial(size=1.5 * self._handle_rad), # pyright: ignore[reportArgumentType] ) # NB: Default bounding box for points does not consider the radius of # those points. We need to HACK it for handle selection - def get_handle_bb(old: Callable[[], np.ndarray]) -> Callable[[], np.ndarray]: - def new_get_bb() -> np.ndarray: - bb = old().copy() + def get_handle_bb( + old: Callable[[], np.ndarray | None], + ) -> Callable[[], np.ndarray | None]: + def new_get_bb() -> np.ndarray | None: + if (bb := old()) is None: + return None + bb = bb.copy() bb[0, :2] -= self._handle_rad bb[1, :2] += self._handle_rad return bb @@ -352,8 +357,9 @@ def get_cursor(self, mme: MouseMoveEvent) -> CursorType | None: return CursorType.BDIAG_ARROW # Step 2: Entire ROI if self._outline: - roi_bb = self._outline.get_bounding_box() - if _is_inside(roi_bb, world_pos): + if (roi_bb := self._outline.get_bounding_box()) and _is_inside( + roi_bb, world_pos + ): return CursorType.ALL_ARROW return None @@ -515,14 +521,15 @@ def set_range( cam = self._camera cam.show_object(self._scene) - width, height, depth = np.ptp(self._scene.get_world_bounding_box(), axis=0) - if width < 0.01: - width = 1 - if height < 0.01: - height = 1 - cam.width = width - cam.height = height - cam.zoom = 1 - margin + if bb := self._scene.get_world_bounding_box(): + width, height, depth = np.ptp(bb, axis=0) + if width < 0.01: + width = 1 + if height < 0.01: + height = 1 + cam.width = width + cam.height = height + cam.zoom = 1 - margin self.refresh() def refresh(self) -> None: @@ -531,7 +538,8 @@ def refresh(self) -> None: self._canvas.request_draw(self._animate) def _animate(self) -> None: - self._renderer.render(self._scene, self._camera) + if self._camera is not None: + self._renderer.render(self._scene, self._camera) def canvas_to_world( self, pos_xy: tuple[float, float] @@ -575,8 +583,7 @@ def elements_at(self, pos_xy: tuple[float, float]) -> list[CanvasElement]: elements: list[CanvasElement] = [] pos = self.canvas_to_world((pos_xy[0], pos_xy[1])) for c in self._scene.children: - bb = c.get_bounding_box() - if _is_inside(bb, pos): + if (bb := c.get_bounding_box()) and _is_inside(bb, pos): elements.append(self._elements[c]) return elements diff --git a/src/ndv/views/_pygfx/_histogram.py b/src/ndv/views/_pygfx/_histogram.py index 6ea347df..4bdb2309 100644 --- a/src/ndv/views/_pygfx/_histogram.py +++ b/src/ndv/views/_pygfx/_histogram.py @@ -24,7 +24,7 @@ from wgpu.gui.jupyter import JupyterWgpuCanvas from wgpu.gui.qt import QWgpuCanvas - WgpuCanvas: TypeAlias = QWgpuCanvas | JupyterWgpuCanvas + WgpuCanvas: TypeAlias = "QWgpuCanvas | JupyterWgpuCanvas" MIN_GAMMA: np.float64 = np.float64(1e-6) @@ -587,6 +587,8 @@ def on_mouse_move(self, event: MouseMoveEvent) -> bool: if self._bin_edges is not None: new_right = min(new_right, self._bin_edges[-1]) newlims = (self._clims[0], new_right) + else: + newlims = (self._clims[0], self._clims[1]) if self.model: self.model.clims = ClimsManual(min=newlims[0], max=newlims[1]) return False diff --git a/src/ndv/views/_pygfx/_util.py b/src/ndv/views/_pygfx/_util.py index b09ad258..80ec2066 100644 --- a/src/ndv/views/_pygfx/_util.py +++ b/src/ndv/views/_pygfx/_util.py @@ -4,7 +4,7 @@ from rendercanvas import BaseRenderCanvas -def rendercanvas_class() -> "BaseRenderCanvas": +def rendercanvas_class() -> "type[BaseRenderCanvas]": from ndv.views._app import GuiFrontend, gui_frontend frontend = gui_frontend() @@ -21,11 +21,13 @@ def sizeHint(self) -> QSize: if frontend == GuiFrontend.JUPYTER: import rendercanvas.jupyter - return rendercanvas.jupyter.JupyterRenderCanvas + return rendercanvas.jupyter.JupyterRenderCanvas # type: ignore[no-any-return] if frontend == GuiFrontend.WX: # ...still not working # import rendercanvas.wx # return rendercanvas.wx.WxRenderWidget from wgpu.gui.wx import WxWgpuCanvas - return WxWgpuCanvas + return WxWgpuCanvas # type: ignore[no-any-return] + + raise ValueError(f"Unsupported frontend: {frontend}") diff --git a/src/ndv/views/_qt/_array_view.py b/src/ndv/views/_qt/_array_view.py index 977e28a7..31b8c82d 100644 --- a/src/ndv/views/_qt/_array_view.py +++ b/src/ndv/views/_qt/_array_view.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, cast import psygnal -from qtpy.QtCore import QObject, QPoint, QSize, Qt, Signal +from qtpy.QtCore import QObject, QPoint, QSize, Qt, Signal # type: ignore from qtpy.QtGui import QCursor, QMouseEvent, QMovie from qtpy.QtWidgets import ( QCheckBox, @@ -105,17 +105,18 @@ def showPopup(self) -> None: popup.move(popup.x(), popup.y() - self.height() - popup.height()) # TODO: upstream me - def setCurrentColormap(self, cmap_: cmap.Colormap) -> None: + def setCurrentColormap(self, color: cmap.Colormap) -> None: """Adds the color to the QComboBox and selects it.""" + idx = 0 for idx in range(self.count()): if item := self.itemColormap(idx): - if item.name == cmap_.name: + if item.name == color.name: # cmap_ is already here - just select it self.setCurrentIndex(idx) return # cmap_ not in the combo box - add it! - self.addColormap(cmap_) + self.addColormap(color) # then, select it! # NB: "Add..." was at idx, now it's at idx+1 and cmap_ is at idx self.setCurrentIndex(idx) @@ -481,7 +482,7 @@ def set_animated(self, animate: bool) -> None: self._timer_id = None self.play_btn.setChecked(False) - def timerEvent(self, event: Any) -> None: + def timerEvent(self, a0: Any) -> None: """Handle timer event for play button, move to the next frame.""" # TODO # for now just increment the value by 1, but we should be able to @@ -797,9 +798,9 @@ def __init__( self._visible_axes: Sequence[AxisKey] = [] - def add_lut_view(self, channel: ChannelKey) -> QLutView: - view = QRGBView(channel) if channel == "RGB" else QLutView(channel) - self._luts[channel] = view + def add_lut_view(self, key: ChannelKey) -> QLutView: + view = QRGBView(key) if key == "RGB" else QLutView(key) + self._luts[key] = view view.histogramRequested.connect(self.histogramRequested) self._qwidget.luts.addWidget(view.frontend_widget()) @@ -811,9 +812,9 @@ def remove_lut_view(self, view: LutView) -> None: def _on_channel_mode_changed(self, text: str) -> None: self.channelModeChanged.emit(ChannelMode(text)) - def add_histogram(self, channel: ChannelKey, histogram: HistogramCanvas) -> None: + def add_histogram(self, channel: ChannelKey, widget: HistogramCanvas) -> None: if lut := self._luts.get(channel, None): - lut._add_histogram(histogram) + lut._add_histogram(widget) def remove_histogram(self, widget: QWidget) -> None: widget.setParent(None) @@ -824,7 +825,7 @@ def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: self._qwidget.dims_sliders.create_sliders(coords) def hide_sliders( - self, axes_to_hide: Container[Hashable], show_remainder: bool = True + self, axes_to_hide: Container[Hashable], *, show_remainder: bool = True ) -> None: """Hide sliders based on visible axes.""" self._qwidget.dims_sliders.hide_dimensions(axes_to_hide, show_remainder) @@ -862,13 +863,13 @@ def set_visible_axes(self, axes: Sequence[AxisKey]) -> None: self._visible_axes = tuple(axes) self._qwidget.ndims_btn.setChecked(len(axes) > 2) - def set_data_info(self, text: str) -> None: + def set_data_info(self, data_info: str) -> None: """Set the data info text, above the canvas.""" - self._qwidget.data_info_label.setText(text) + self._qwidget.data_info_label.setText(data_info) - def set_hover_info(self, text: str) -> None: + def set_hover_info(self, hover_info: str) -> None: """Set the hover info text, below the canvas.""" - self._qwidget.hover_info_label.setText(text) + self._qwidget.hover_info_label.setText(hover_info) def set_channel_mode(self, mode: ChannelMode) -> None: """Set the channel mode button text.""" diff --git a/src/ndv/views/_qt/_main_thread.py b/src/ndv/views/_qt/_main_thread.py index c9a0d655..062b9983 100644 --- a/src/ndv/views/_qt/_main_thread.py +++ b/src/ndv/views/_qt/_main_thread.py @@ -3,7 +3,14 @@ from concurrent.futures import Future from typing import TYPE_CHECKING, Callable -from qtpy.QtCore import QCoreApplication, QMetaObject, QObject, Qt, QThread, Slot +from qtpy.QtCore import ( # type: ignore + QCoreApplication, + QMetaObject, + QObject, + Qt, + QThread, + Slot, +) if TYPE_CHECKING: from typing_extensions import ParamSpec, TypeVar diff --git a/src/ndv/views/_vispy/_array_canvas.py b/src/ndv/views/_vispy/_array_canvas.py index 964e5a01..caa9121e 100755 --- a/src/ndv/views/_vispy/_array_canvas.py +++ b/src/ndv/views/_vispy/_array_canvas.py @@ -1,3 +1,4 @@ +# pyright: reportOptionalSubscript=none from __future__ import annotations import warnings @@ -8,11 +9,9 @@ import cmap as _cmap import numpy as np import vispy -import vispy.app import vispy.color import vispy.scene -import vispy.visuals -from vispy import scene +from vispy import scene, visuals from vispy.util.quaternion import Quaternion from ndv._types import ( @@ -41,9 +40,9 @@ class VispyImageHandle(ImageHandle): - def __init__(self, visual: scene.Image | scene.Volume) -> None: + def __init__(self, visual: visuals.ImageVisual | visuals.VolumeVisual) -> None: self._visual = visual - self._allowed_dims = {2, 3} if isinstance(visual, scene.visuals.Image) else {3} + self._allowed_dims = {2, 3} if isinstance(visual, visuals.ImageVisual) else {3} def data(self) -> np.ndarray: try: @@ -112,7 +111,7 @@ def move(self, pos: Sequence[float]) -> None: def remove(self) -> None: self._visual.parent = None - def get_cursor(self, mme: MouseMoveEvent) -> CursorType | None: + def get_cursor(self, event: MouseMoveEvent) -> CursorType | None: return None @@ -174,16 +173,16 @@ def set_border(self, color: _cmap.Color) -> None: def set_handles(self, color: _cmap.Color) -> None: _vis_color = vispy.color.Color(color.hex) _vis_color.alpha = color.alpha - self._handles.set_data(face_color=_vis_color) + self._handles.set_data(face_color=_vis_color) # pyright: ignore[reportArgumentType] def set_bounding_box( - self, mi: tuple[float, float], ma: tuple[float, float] + self, minimum: tuple[float, float], maximum: tuple[float, float] ) -> None: # NB: Support two diagonal points, not necessarily true min/max - x1 = float(min(mi[0], ma[0])) - y1 = float(min(mi[1], ma[1])) - x2 = float(max(mi[0], ma[0])) - y2 = float(max(mi[1], ma[1])) + x1 = float(min(minimum[0], maximum[0])) + y1 = float(min(minimum[1], maximum[1])) + x2 = float(max(minimum[0], maximum[0])) + y2 = float(max(minimum[1], maximum[1])) # Update rectangle self._rect.center = [(x1 + x2) / 2, (y1 + y2) / 2] @@ -246,8 +245,8 @@ def on_mouse_press(self, event: MousePressEvent) -> bool: def on_mouse_release(self, event: MouseReleaseEvent) -> bool: return False - def get_cursor(self, mme: MouseMoveEvent) -> CursorType | None: - canvas_pos = (mme.x, mme.y) + def get_cursor(self, event: MouseMoveEvent) -> CursorType | None: + canvas_pos = (event.x, event.y) pos = self._tform().map(canvas_pos)[:2] if self._handle_under(pos) is not None: center = self._rect.center @@ -511,11 +510,11 @@ def on_mouse_release(self, event: MouseReleaseEvent) -> bool: self._selection.on_mouse_release(event) return False - def get_cursor(self, mme: MouseMoveEvent) -> CursorType: + def get_cursor(self, event: MouseMoveEvent) -> CursorType: if self._viewer.interaction_mode == InteractionMode.CREATE_ROI: return CursorType.CROSS - for vis in self.elements_at((mme.x, mme.y)): - if cursor := vis.get_cursor(mme): + for vis in self.elements_at((event.x, event.y)): + if cursor := vis.get_cursor(event): return cursor return CursorType.DEFAULT diff --git a/src/ndv/views/_vispy/_histogram.py b/src/ndv/views/_vispy/_histogram.py index 6a483450..3fec06df 100644 --- a/src/ndv/views/_vispy/_histogram.py +++ b/src/ndv/views/_vispy/_histogram.py @@ -1,9 +1,13 @@ +# pyright: reportGeneralTypeIssues=none, reportOptionalSubscript=none +# pyright: reportOptionalMemberAccess=none, reportIndexIssue=none from __future__ import annotations from enum import Enum, auto -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import numpy as np +import vispy +import vispy.scene from vispy import scene from ndv._types import CursorType @@ -101,7 +105,9 @@ def __init__(self, *, vertical: bool = False) -> None: self.plot = PlotWidget() self.plot.lock_axis("y") self._canvas.central_widget.add_widget(self.plot) - self.node_tform = self.plot.node_transform(self.plot._view.scene) + self.node_tform = cast("vispy.scene.Node", self.plot).node_transform( + self.plot._view.scene + ) self.plot._view.add(self._hist_mesh) self.plot._view.add(self._lut_line) @@ -129,9 +135,9 @@ def set_channel_visible(self, visible: bool) -> None: self._lut_line.visible = visible self._gamma_handle.visible = visible - def set_colormap(self, lut: cmap.Colormap) -> None: + def set_colormap(self, cmap: cmap.Colormap) -> None: if self._hist_mesh is not None: - self._hist_mesh.color = lut.color_stops[-1].color.hex + self._hist_mesh.color = cmap.color_stops[-1].color.hex def set_gamma(self, gamma: float) -> None: if gamma < 0: @@ -177,15 +183,15 @@ def set_range( margin: float = 0, ) -> None: if x: - _x = (min(x), max(x)) + x = (min(x), max(x)) elif self._bin_edges is not None: - _x = self._bin_edges[0], self._bin_edges[-1] + x = self._bin_edges[0], self._bin_edges[-1] if y: - _y = (min(y), max(y)) + y = (min(y), max(y)) elif self._values is not None: - _y = (0, np.max(self._values)) - self._range = _y if y else None - self._domain = _x if x else None + y = (0, np.max(self._values)) + self._range = y + self._domain = x self._resize() def set_vertical(self, vertical: bool) -> None: diff --git a/src/ndv/views/_vispy/_plot_widget.py b/src/ndv/views/_vispy/_plot_widget.py index ba8ff01a..96daa5fc 100644 --- a/src/ndv/views/_vispy/_plot_widget.py +++ b/src/ndv/views/_vispy/_plot_widget.py @@ -6,7 +6,6 @@ from vispy import geometry, scene if TYPE_CHECKING: - from collections.abc import Sequence from typing import TypeVar from vispy.scene.events import SceneMouseEvent @@ -23,7 +22,7 @@ def add_view( col_span: int = 1, **kwargs: Any, ) -> scene.ViewBox: - super().add_view(...) + return super().add_view(...) # pyright: ignore[reportReturnType] def add_widget( self, @@ -34,7 +33,7 @@ def add_widget( col_span: int = 1, **kwargs: Any, ) -> scene.Widget: - super().add_widget(...) + return super().add_widget(...) def __getitem__(self, idxs: int | tuple[int, int]) -> T: return super().__getitem__(idxs) # type: ignore [no-any-return] @@ -331,7 +330,7 @@ def rect(self, args: Any) -> None: y = min(y, self.ybounds[1] - args.height) args.pos = (x, y) - super(PanZoom1DCamera, type(self)).rect.fset(self, args) + super(PanZoom1DCamera, type(self)).rect.fset(self, args) # pyright: ignore[reportAttributeAccessIssue] def zoom( self, @@ -349,7 +348,7 @@ def zoom( _factor[self.axis_index] = 1 super().zoom(_factor, center=center) - def pan(self, pan: Sequence[float]) -> None: + def pan(self, *pan: float) -> None: """Pan the camera by `pan`.""" if self.axis_index is None: super().pan(pan) @@ -375,7 +374,7 @@ def viewbox_mouse_event(self, event: SceneMouseEvent) -> None: if abs(dx) > abs(dy): # TODO: Can we do better here? Some sort of adaptive behavior? pan_dist = 0.1 * self.rect.width - self.pan([pan_dist if dx < 0 else -pan_dist, 0]) + self.pan(*[pan_dist if dx < 0 else -pan_dist, 0]) event.handled = True return super().viewbox_mouse_event(event) diff --git a/src/ndv/views/_wx/_app.py b/src/ndv/views/_wx/_app.py index 9dbfff49..01c1b53f 100644 --- a/src/ndv/views/_wx/_app.py +++ b/src/ndv/views/_wx/_app.py @@ -70,7 +70,7 @@ def filter_mouse_events( ) if hasattr(canvas, "_subwidget"): - canvas = canvas._subwidget + canvas = canvas._subwidget # pyright: ignore[reportAttributeAccessIssue] # TIP: event.Skip() allows the event to propagate to other handlers. diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index 1b8e82ae..77a3a958 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -5,11 +5,10 @@ from sys import version_info from typing import TYPE_CHECKING, cast +import cmap import psygnal import wx import wx.adv -import wx.lib.newevent -import wx.svg from psygnal import EmissionInfo, Signal from pyconify import svg_path @@ -24,13 +23,15 @@ if TYPE_CHECKING: from collections.abc import Container, Hashable, Mapping, Sequence - import cmap - from ndv._types import AxisKey, ChannelKey from ndv.models._data_display_model import _ArrayDataDisplayModel from ndv.views.bases._graphics._canvas import HistogramCanvas +ToggleBtnEvent = cast("int", wx.EVT_TOGGLEBUTTON.typeId) # type: ignore[attr-defined] +SliderEvent = cast("int", wx.EVT_SLIDER.typeId) # type: ignore[attr-defined] + + class _WxSpinner(wx.Panel): SPIN_GIF = str(Path(__file__).parent.parent / "_resources" / "spin.gif") @@ -184,7 +185,7 @@ def _on_visible_changed(self, event: wx.CommandEvent) -> None: def _on_cmap_changed(self, event: wx.CommandEvent) -> None: if self._model: - self._model.cmap = self._wxwidget.cmap.GetValue() + self._model.cmap = cmap.Colormap(self._wxwidget.cmap.GetValue()) def _on_clims_changed(self, event: wx.CommandEvent) -> None: if self._model: @@ -194,8 +195,8 @@ def _on_clims_changed(self, event: wx.CommandEvent) -> None: def _on_autoscale_rclick(self, event: wx.CommandEvent) -> None: btn = event.GetEventObject() pos = btn.ClientToScreen((0, 0)) - sz = btn.GetSize() - self._wxwidget.auto_popup.Position(pos, (0, sz[1])) + sz = cast("wx.Size", btn.GetSize()) + self._wxwidget.auto_popup.Position(pos, (0, sz.GetHeight())) self._wxwidget.auto_popup.Popup() def _on_autoscale_tail_changed(self, event: wx.CommandEvent) -> None: @@ -230,7 +231,7 @@ def _on_set_histogram_range_clicked(self, event: wx.CommandEvent) -> None: btn = self._wxwidget.log_btn if btn.GetValue(): btn.SetValue(False) - event = wx.PyCommandEvent(wx.EVT_TOGGLEBUTTON.typeId, btn.GetId()) + event = wx.PyCommandEvent(ToggleBtnEvent, btn.GetId()) # type: ignore event.SetEventObject(btn) wx.PostEvent(btn.GetEventHandler(), event) if hist := self.histogram: @@ -240,10 +241,10 @@ def _add_histogram(self, histogram: HistogramCanvas) -> None: widget = cast("wx.Window", histogram.frontend_widget()) # FIXME: pygfx backend needs this to be widget._subwidget if hasattr(widget, "_subwidget"): - widget = widget._subwidget + widget = widget._subwidget # pyright: ignore[reportAttributeAccessIssue] # FIXME: Rendercanvas may make this unnecessary - if (parent := widget.GetParent()) and parent is not self: + if (parent := widget.GetParent()) and parent is not self: # type: ignore widget.Reparent(self._wxwidget) # Reparent widget to this frame wx.CallAfter(parent.Destroy) widget.Show() @@ -292,7 +293,7 @@ def set_colormap(self, cmap: cmap.Colormap) -> None: def set_clims(self, clims: tuple[float, float]) -> None: # Block signals from changing clims - with wx.EventBlocker(self._wxwidget.clims, wx.EVT_SLIDER.typeId): + with wx.EventBlocker(self._wxwidget.clims, SliderEvent): self._wxwidget.clims.SetValue(*clims) wx.SafeYield() @@ -396,12 +397,12 @@ def _on_slider_changed(self, event: wx.CommandEvent) -> None: class _WxArrayViewer(wx.Frame): - def __init__(self, canvas_widget: wx.Window, parent: wx.Window = None): + def __init__(self, canvas_widget: wx.Window, parent: wx.Window | None = None): super().__init__(parent) # FIXME: pygfx backend needs this to be canvas_widget._subwidget if hasattr(canvas_widget, "_subwidget"): - canvas_widget = canvas_widget._subwidget + canvas_widget = canvas_widget._subwidget # pyright: ignore[reportAttributeAccessIssue] if (parent := canvas_widget.GetParent()) and parent is not self: canvas_widget.Reparent(self) # Reparent canvas_widget to this frame @@ -481,7 +482,7 @@ def __init__( canvas_widget: wx.Window, data_model: _ArrayDataDisplayModel, viewer_model: ArrayViewerModel, - parent: wx.Window = None, + parent: wx.Window | None = None, ) -> None: self._data_model = data_model self._viewer_model = viewer_model @@ -538,11 +539,11 @@ def set_visible_axes(self, axes: Sequence[AxisKey]) -> None: def frontend_widget(self) -> wx.Window: return self._wxwidget - def add_lut_view(self, channel: ChannelKey) -> WxLutView: + def add_lut_view(self, key: ChannelKey) -> WxLutView: wdg = self.frontend_widget() - view = WxRGBView(wdg, channel) if channel == "RGB" else WxLutView(wdg, channel) + view = WxRGBView(wdg, key) if key == "RGB" else WxLutView(wdg, key) self._wxwidget.luts.Add(view._wxwidget, 0, wx.EXPAND | wx.BOTTOM, 5) - self._luts[channel] = view + self._luts[key] = view # TODO: Reusable synchronization with ViewerModel view._wxwidget.histogram_btn.Show(self._viewer_model.show_histogram_button) view.histogramRequested.connect(self.histogramRequested) @@ -552,14 +553,14 @@ def add_lut_view(self, channel: ChannelKey) -> WxLutView: return view # TODO: Fix type - def add_histogram(self, channel: ChannelKey, canvas: HistogramCanvas) -> None: + def add_histogram(self, channel: ChannelKey, widget: HistogramCanvas) -> None: if lut := self._luts.get(channel, None): # Add the histogram widget on the LUT - lut._add_histogram(canvas) + lut._add_histogram(widget) self._wxwidget.Layout() - def remove_lut_view(self, lut: LutView) -> None: - wxwdg = cast("_WxLUTWidget", lut.frontend_widget()) + def remove_lut_view(self, view: LutView) -> None: + wxwdg = cast("_WxLUTWidget", view.frontend_widget()) self._wxwidget.luts.Detach(wxwdg) wxwdg.Destroy() self._wxwidget.Layout() @@ -569,7 +570,7 @@ def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None: self._wxwidget.Layout() def hide_sliders( - self, axes_to_hide: Container[Hashable], show_remainder: bool = True + self, axes_to_hide: Container[Hashable], *, show_remainder: bool = True ) -> None: self._wxwidget.dims_sliders.hide_dimensions(axes_to_hide, show_remainder) self._wxwidget.Layout() @@ -580,11 +581,11 @@ def current_index(self) -> Mapping[AxisKey, int | slice]: def set_current_index(self, value: Mapping[AxisKey, int | slice]) -> None: self._wxwidget.dims_sliders.set_current_index(value) - def set_data_info(self, text: str) -> None: - self._wxwidget._data_info_label.SetLabel(text) + def set_data_info(self, data_info: str) -> None: + self._wxwidget._data_info_label.SetLabel(data_info) - def set_hover_info(self, text: str) -> None: - self._wxwidget._hover_info_label.SetLabel(text) + def set_hover_info(self, hover_info: str) -> None: + self._wxwidget._hover_info_label.SetLabel(hover_info) def set_channel_mode(self, mode: ChannelMode) -> None: self._wxwidget.channel_mode_combo.SetValue(mode) diff --git a/src/ndv/views/_wx/_labeled_slider.py b/src/ndv/views/_wx/_labeled_slider.py index ccad7b14..05ed17b5 100644 --- a/src/ndv/views/_wx/_labeled_slider.py +++ b/src/ndv/views/_wx/_labeled_slider.py @@ -23,7 +23,7 @@ def setValue(self, value: int) -> None: self.slider.SetValue(value) def value(self) -> int: - return self.slider.GetValue() # type: ignore [no-any-return] + return self.slider.GetValue() def setSingleStep(self, step: int) -> None: self.slider.SetLineSize(step) diff --git a/src/ndv/views/_wx/range_slider.py b/src/ndv/views/_wx/range_slider.py index f453b2e7..059ce719 100644 --- a/src/ndv/views/_wx/range_slider.py +++ b/src/ndv/views/_wx/range_slider.py @@ -53,7 +53,7 @@ def GetPosition(self) -> tuple[int, int]: max_value = self.parent.GetMax() fraction = value_to_fraction(self.value, min_value, max_value) low = int(fraction_to_value(fraction, min_x, max_x)) - high = int(parent_size[1] / 2 + 1) + high = int(parent_size.GetHeight() / 2 + 1) # type: ignore [attr-defined] return low, high def SetPosition(self, pos: tuple[int, int]) -> None: @@ -88,7 +88,7 @@ def SetValue(self, value: int) -> None: self.value = value def PostEvent(self) -> None: - event = wx.PyCommandEvent(wx.EVT_SLIDER.typeId, self.parent.GetId()) + event = wx.PyCommandEvent(wx.EVT_SLIDER.typeId, self.parent.GetId()) # type: ignore event.SetEventObject(self.parent) wx.PostEvent(self.parent.GetEventHandler(), event) @@ -96,13 +96,13 @@ def GetMin(self) -> int: return self.parent.border_width + int(self.size[0] / 2) def GetMax(self) -> int: - parent_w = int(self.parent.GetSize()[0]) + parent_w = int(self.parent.GetSize().GetWidth()) # type: ignore [attr-defined] return parent_w - self.parent.border_width - int(self.size[0] / 2) def IsMouseOver(self, mouse_pos: wx.Point) -> bool: in_hitbox = True my_pos = self.GetPosition() - for i_coord, mouse_coord in enumerate(mouse_pos): + for i_coord, mouse_coord in enumerate((mouse_pos.x, mouse_pos.y)): boundary_low = my_pos[i_coord] - self.size[i_coord] / 2 boundary_high = my_pos[i_coord] + self.size[i_coord] / 2 in_hitbox = in_hitbox and (boundary_low <= mouse_coord <= boundary_high) @@ -144,6 +144,10 @@ def OnPaint(self, dc: wx.BufferedPaintDC) -> None: ) +DEFAULT_POSITION = wx.Point(-1, -1) +DEFAULT_SIZE = wx.Size(-1, -1) + + class RangeSlider(wx.Panel): def __init__( self, @@ -153,8 +157,8 @@ def __init__( highValue: int | None = None, minValue: int = 0, maxValue: int = 100, - pos: wx.Point = wx.DefaultPosition, - size: wx.Size = wx.DefaultSize, + pos: wx.Point = DEFAULT_POSITION, + size: wx.Size = DEFAULT_SIZE, style: int = wx.SL_HORIZONTAL, validator: wx.Validator = wx.DefaultValidator, name: str = "rangeSlider", @@ -164,7 +168,7 @@ def __init__( if validator != wx.DefaultValidator: raise NotImplementedError("Validator not implemented") super().__init__(parent=parent, id=id, pos=pos, size=size, name=name) - self.SetMinSize(size=(max(50, size[0]), max(26, size[1]))) + self.SetMinSize(size=(max(50, size.GetWidth()), max(26, size.GetHeight()))) if minValue > maxValue: minValue, maxValue = maxValue, minValue self.min_value = minValue @@ -203,20 +207,22 @@ def __init__( self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.Bind(wx.EVT_SIZE, self.OnResize) - def Enable(self, enable: bool = True) -> None: + def Enable(self, enable: bool = True) -> bool: super().Enable(enable) self.Refresh() + return True - def Disable(self) -> None: + def Disable(self) -> bool: super().Disable() self.Refresh() + return True def SetValueFromMousePosition(self, click_pos: wx.Point) -> None: for thumb in self.thumbs.values(): if thumb.dragged: - thumb.SetPosition(click_pos) + thumb.SetPosition((click_pos.x, click_pos.y)) - def OnMouseDown(self, evt: wx.Event) -> None: + def OnMouseDown(self, evt: wx.MouseEvent) -> None: if not self.IsEnabled(): return click_pos = evt.GetPosition() @@ -229,7 +235,7 @@ def OnMouseDown(self, evt: wx.Event) -> None: self.CaptureMouse() self.Refresh() - def OnMouseUp(self, evt: wx.Event) -> None: + def OnMouseUp(self, evt: wx.MouseEvent) -> None: if not self.IsEnabled(): return self.SetValueFromMousePosition(evt.GetPosition()) @@ -239,13 +245,13 @@ def OnMouseUp(self, evt: wx.Event) -> None: self.ReleaseMouse() self.Refresh() - def OnMouseLost(self, evt: wx.Event) -> None: + def OnMouseLost(self, evt: wx.MouseEvent) -> None: for thumb in self.thumbs.values(): thumb.dragged = False thumb.mouse_over = False self.Refresh() - def OnMouseMotion(self, evt: wx.Event) -> None: + def OnMouseMotion(self, evt: wx.MouseEvent) -> None: if not self.IsEnabled(): return refresh_needed = False @@ -262,7 +268,7 @@ def OnMouseMotion(self, evt: wx.Event) -> None: if refresh_needed: self.Refresh() - def OnMouseEnter(self, evt: wx.Event) -> None: + def OnMouseEnter(self, evt: wx.MouseEvent) -> None: if not self.IsEnabled(): return mouse_pos = evt.GetPosition() @@ -283,7 +289,8 @@ def OnResize(self, evt: wx.Event) -> None: self.Refresh() def OnPaint(self, evt: wx.Event) -> None: - w, h = self.GetSize() + sz = self.GetSize() + w, h = sz.GetWidth(), sz.GetHeight() # type: ignore [attr-defined] # BufferedPaintDC should reduce flickering dc = wx.BufferedPaintDC(self) background_brush = wx.Brush(self.GetBackgroundColour(), wx.SOLID) diff --git a/src/ndv/views/bases/_app.py b/src/ndv/views/bases/_app.py index 50de97c2..eb3e10af 100644 --- a/src/ndv/views/bases/_app.py +++ b/src/ndv/views/bases/_app.py @@ -2,15 +2,18 @@ import os import sys +import threading import traceback from concurrent.futures import Executor, Future, ThreadPoolExecutor from contextlib import suppress from functools import cache +from threading import Timer from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, cast if TYPE_CHECKING: from types import TracebackType + import pydevd from IPython.core.interactiveshell import InteractiveShell from typing_extensions import ParamSpec, TypeVar @@ -97,8 +100,6 @@ def call_later(self, msec: int, func: Callable[[], None]) -> None: """Call `func` after `msec` milliseconds.""" # generic implementation using python threading - from threading import Timer - Timer(msec / 1000, func).start() @@ -144,13 +145,9 @@ def ndv_excepthook( (debugpy := sys.modules.get("debugpy")) and debugpy.is_client_connected() and ("pydevd" in sys.modules) + and (py_db := _pydevd_debugger()) ): with suppress(Exception): - import threading - - import pydevd - - py_db = pydevd.get_global_debugger() thread = threading.current_thread() additional_info = py_db.set_additional_thread_info(thread) additional_info.is_tracing += 1 @@ -169,3 +166,11 @@ def ndv_excepthook( if os.getenv(EXIT_ON_EXCEPTION) in ("1", "true", "True"): print(f"\n{EXIT_ON_EXCEPTION} is set, exiting.") sys.exit(1) + + +def _pydevd_debugger() -> pydevd.PyDB | None: + with suppress(Exception): + import pydevd + + return pydevd.get_global_debugger() + return None diff --git a/uv.lock b/uv.lock index 5e9fdbf2..0c357391 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.13' and sys_platform == 'win32'", @@ -3224,11 +3225,14 @@ dev = [ { name = "ndv", extra = ["jupyter", "pygfx", "pyqt", "vispy"] }, { name = "pdbpp", marker = "sys_platform != 'win32'" }, { name = "pre-commit" }, + { name = "pydevd" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-qt" }, { name = "rich" }, { name = "ruff" }, + { name = "types-wxpython" }, ] docs = [ { name = "mike" }, @@ -3295,6 +3299,7 @@ requires-dist = [ { name = "wxpython", marker = "extra == 'wx'", specifier = ">=4.2.2" }, { name = "wxpython", marker = "extra == 'wxpython'", specifier = ">=4.2.2" }, ] +provides-extras = ["jup", "jupyter", "pygfx", "pyqt", "pyside", "qt", "vispy", "wx", "wxpython"] [package.metadata.requires-dev] array-libs = [ @@ -3319,11 +3324,14 @@ dev = [ { name = "ndv", extras = ["vispy", "pygfx", "pyqt", "jupyter"], editable = "." }, { name = "pdbpp", marker = "sys_platform != 'win32'", specifier = ">=0.10.3" }, { name = "pre-commit", specifier = ">=4.1.0" }, + { name = "pydevd" }, + { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8" }, { name = "pytest-cov", specifier = ">=6" }, { name = "pytest-qt", specifier = ">=4.4" }, { name = "rich", specifier = ">=13.9.4" }, { name = "ruff", specifier = ">=0.9.4" }, + { name = "types-wxpython" }, ] docs = [ { name = "mike", specifier = ">=2.1.3" }, @@ -4441,6 +4449,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/0c/c5c5cd3689c32ed1fe8c5d234b079c12c281c051759770c05b8bed6412b5/pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", size = 2004961 }, ] +[[package]] +name = "pydevd" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/27/eb05f8c3205f469e41172ae8b1c3a8be4c609b2bb60c037279bba542412b/pydevd-3.3.0.tar.gz", hash = "sha256:aa4bdb74c5e21bde8f396c5055f5e34b0a23a359ec1cc44c6b25282adc9c3f50", size = 1330240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/6e/06842c7fe76791b7baad693715b8ff45f59637f03c86428df25b2e42c3c6/pydevd-3.3.0-cp310-cp310-macosx_14_0_universal2.whl", hash = "sha256:5a3a6948d09db219754efdd254fb462aa68a76e635cabc9cb7e95669ce161b14", size = 2445279 }, + { url = "https://files.pythonhosted.org/packages/ea/4c/cccbe293312d355a8243aab7e9b032d3b3c6f8eecfde22a1772667723784/pydevd-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04fad1696d596e7bc0e938ca6a08bc0abcc9f6e10099b67148c9fe8abdddf36a", size = 3561262 }, + { url = "https://files.pythonhosted.org/packages/46/6f/6f0cfd095316987cd782e92b8f19d0367230d2ad11c26b276f4ddabaf01c/pydevd-3.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aadeec65b783264cc162426e97d3fe967ca2bf742b4cfef362562ca8cd75b829", size = 3660804 }, + { url = "https://files.pythonhosted.org/packages/93/a6/4b535afa71b42604644c58a9d211e5b74e311efd3a7f530421f1e1560af1/pydevd-3.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6221258670994bddbfcf1c878ee40485cdda56dc47f95024c0050248c0023e66", size = 4647264 }, + { url = "https://files.pythonhosted.org/packages/32/a9/da668fbfff7f998895e041ceb34ad2f9cba7b87c894bbcdc8c94a2967e99/pydevd-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65044faa27b8ce5f3166ad0bfcd080983aa5244af130f98aa81eab509b3a072d", size = 4614212 }, + { url = "https://files.pythonhosted.org/packages/a4/61/a32f0d4725b9609a873507ebae8ab1ec956bacc08a7263d0aeda66d8606e/pydevd-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fdfef7091745a495341d6fe3c74ff6c6031e1fde0495a49c6887600dddc80ab9", size = 2155743 }, + { url = "https://files.pythonhosted.org/packages/2f/ad/d9fa74ecf70328e5a79d1966a593e0b1e1a4a0c28626c5c72039bc6c6346/pydevd-3.3.0-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:46d7438e74c5903ae6ac1cc824cc7df1597f26929ee95379396aa0dd963625d0", size = 2157195 }, + { url = "https://files.pythonhosted.org/packages/e0/36/ef83b4a3f71212117399e3836cc547c59ae0332466cb83a8c900ee2df730/pydevd-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad52f71503825b571fdb00afbec6c9e9989c634b8a8d901e343b56f858b70a49", size = 3165156 }, + { url = "https://files.pythonhosted.org/packages/0a/60/1ad3bcd67cbc70c5323478bd575f20bc9baeadf80699ce9fda771dc2b349/pydevd-3.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925dc3f884832d58d241c6f9275cfaf5e8fd1f328a54642d2c601e2c106c1277", size = 3240649 }, + { url = "https://files.pythonhosted.org/packages/6b/3c/c15565be333e1cb895a22b448854de841f4778a4cbc4b47f6074a41cc178/pydevd-3.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:794cd4c20ff8d23c42e44ca69b038e60043d83d6b6cce2ff4e55dd3964679507", size = 4277074 }, + { url = "https://files.pythonhosted.org/packages/42/f8/b36b51964942db6c35fae95d7c1b15720cb84910503230d4f15681da021c/pydevd-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1472dd4ca78c2d5c3b0b165d7f0c9b00b523b3a1d059dbdfe22c75f1a42c34e5", size = 4241959 }, + { url = "https://files.pythonhosted.org/packages/7c/cd/76ac9f08f1e601a00c3034ff25bdcbc2503461e2eb542a92a57ce70519b0/pydevd-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cb190421435f56b8366a2757281962a8dca31c6ea480cd5e213e24d6418a809c", size = 1951893 }, + { url = "https://files.pythonhosted.org/packages/a6/a7/9d1ddefa77b7ef03c1f1e66a51f723be9085554b43fd43129606c8147a86/pydevd-3.3.0-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:2a47457fd0b45666fbe76b5037c4b4a2c5c9fc87755faf6c8d47accc7d0e4dc6", size = 2684408 }, + { url = "https://files.pythonhosted.org/packages/0c/1f/67ca15172e86877740af05fb840f1e3486ef996f906ff4c9b066faad0dd9/pydevd-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c67695b446014c3e893e2443dfc00abf1c1f25983148fa7899c21f32f70428", size = 4327759 }, + { url = "https://files.pythonhosted.org/packages/a2/21/f06498d8f037bb116387debf7ebd937399ac0f18a230c339cbf9b4064d1d/pydevd-3.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47737ab44b3365a741ee9e57d43951d7067938169411785cf2d7507cd049869a", size = 4486105 }, + { url = "https://files.pythonhosted.org/packages/49/9e/0de175ce93d8f08291aeaa52c914942fb963af0ee74efa9d52826fcfd18b/pydevd-3.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a2fa54ee2be7dc19c220e113551c92199c152a4ee348e7c3c105ebc7cff623c", size = 5435634 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/9180b86fe877c02472d71dc052f52cb7e48befee8e99c62890d31c378d71/pydevd-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0490b1d6aa50c0b0b54166ef9605c837411f0134b97e5afa6686f31eba1d830", size = 5446803 }, + { url = "https://files.pythonhosted.org/packages/6d/79/914d1252544d1f1866798cdfc892911496d25bcc7701ffbf8854572bf5dd/pydevd-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c027d2249478d127a146f245d50de9a211296467ec9d21f25febf3ac916623da", size = 2292336 }, + { url = "https://files.pythonhosted.org/packages/c9/9b/ac02fde7252570d7de383dd3d7ae9fe25341fdf3a6d5af7efb8cdb831a4e/pydevd-3.3.0-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:21506d009947e4766953ee80ae2b7806bb8144d9da2151408a60e727e19dcf24", size = 2670219 }, + { url = "https://files.pythonhosted.org/packages/69/e1/7fbd827ada3ca8e880cdf5e94ddbb5a09c99f437033306ef8d7becb44467/pydevd-3.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a779970f39580a84e48aec5ad1cd60ad59c5e7b820d31dae058427fb71a8747", size = 4325389 }, + { url = "https://files.pythonhosted.org/packages/a7/90/d24dd2287a4b71e64ecc15dab9559ad21900cf6d8ff209872f5a3bfa11b4/pydevd-3.3.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206232066a737bdc1ea27e46a61356a8b0cfdbfd3befe667aed2ba267102f72b", size = 4480684 }, + { url = "https://files.pythonhosted.org/packages/23/20/e6fd0dde2326d1093cd793403b6dd198c82dc4d95c40668b4de7d14d6052/pydevd-3.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8f61b3fbc1b1da52bd16289cf89f7b621c28787e3a6134285d85d79aa43d6fcb", size = 5434956 }, + { url = "https://files.pythonhosted.org/packages/b7/50/98ac5d8332051d34695b73bc4c01ba6b1ba5d39bcec9f9c185117bc137fb/pydevd-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cc982957d6c75fe42c14a15793ce812a5acc6a4f9baf88785d150a4cdc267b0", size = 5449451 }, + { url = "https://files.pythonhosted.org/packages/e6/65/23405d2590bf60e1b09bd979acc77a4da6f95c7879cbe1ada93f45fec9c4/pydevd-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:77d5e8688ddca31a8d342baf3d37b39db3c6c5e639f65f66ae58b9bc6dc47637", size = 2291685 }, + { url = "https://files.pythonhosted.org/packages/b6/ef/47533e3f064121930c483e9b0751892d7ed22a3e2de4b5dccae65bd3212d/pydevd-3.3.0-cp39-cp39-macosx_14_0_universal2.whl", hash = "sha256:79c4752d9794b583ee775c1e40d868b567bc79c05b89a58aefc9c8e5c3719976", size = 2447765 }, + { url = "https://files.pythonhosted.org/packages/14/0c/d2381dc80a7adf09c59730099392e0e501940e05d3741349a5441d6934b8/pydevd-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7c6b3e34e3b0d8952addc58dcb2aaeb9c8b92a51c7255d3e11356ac7d195594", size = 3561986 }, + { url = "https://files.pythonhosted.org/packages/91/e0/90eb431f0dfa5f4722259e04acb65b71aa1bd167b747d09fd4d0bf88e15f/pydevd-3.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15527e3b42a90d2b9ca568ef506ee3d0b0f3ebf5770bdc036916b61b2480f253", size = 3656147 }, + { url = "https://files.pythonhosted.org/packages/1c/da/7ce31076d4ff367002056b98eac1d535caf664efcc4ca7c6ceb737ff8596/pydevd-3.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:150ff02deac2a49a0f3b352766789fdf7313281aafdb78840b11413fbc2ac06e", size = 4640565 }, + { url = "https://files.pythonhosted.org/packages/ee/98/0938b4f4c23a6dc66fd0ba34e48cf73bf44b5fbddf8fbde6064c2fcb32b1/pydevd-3.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:512250f1100f361ca7e3a0b0da30b3f2876cb3ca1747deab32c0f5e4c3cd0df4", size = 4613405 }, + { url = "https://files.pythonhosted.org/packages/cc/07/942aa636ee088282f3604cd4fac12fdc9155670908364b5340f61632881d/pydevd-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:29ae2c91f5d9ebb080d64e7f2cb5d127ccbfbee2edd34cbce90db61ac07647b2", size = 2181136 }, +] + [[package]] name = "pyerfa" version = "2.0.1.5" @@ -4644,6 +4690,19 @@ version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/1b/ea40363be0056080454cdbabe880773c3c5bd66d7b13f0c8b8b8c8da1e0c/pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775", size = 48744 } +[[package]] +name = "pyright" +version = "1.1.400" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460 }, +] + [[package]] name = "pyside6" version = "6.6.3" @@ -5881,6 +5940,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 }, ] +[[package]] +name = "types-wxpython" +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/33/1a2cf4cc8c76e41a59499cc7b6eda388c65ca8e85a551dd7e594812dd156/types_wxpython-0.9.7.tar.gz", hash = "sha256:d9c09ded60cbe98ee44e8cb4cf46638ce4929e75e527e390d0db19808d71f6bd", size = 494676 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/18/9b740abeaae63cbd430aaf73dd54cbc415a30a1a77993134e68d0548dd0f/types_wxpython-0.9.7-py3-none-any.whl", hash = "sha256:2669fe4e064e8eed5fb163e8d3f7bbec65a62a5ea3ad6d43f9624d035cb3deb7", size = 530840 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" From 14c1b8c9f88cc931c41414479d32910c6e3dac87 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 19:32:14 -0400 Subject: [PATCH 02/10] fix typos --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0eb47c7..c34b651e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: validate-pyproject - repo: https://github.com/crate-ci/typos - rev: v1 + rev: v1.32.0 hooks: - id: typos args: [--force-exclude] # omitting --write-changes From e5bde277b479323b75d7363317f01af5acb7bea2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 19:45:31 -0400 Subject: [PATCH 03/10] fix typing add local pca --- .github_changelog_generator | 2 +- .pre-commit-config.yaml | 21 +++++++++++++++++++++ Makefile | 4 ++-- docs/css/material.css | 10 +++++----- examples/cookbook/microscope_dashboard.py | 2 ++ pyproject.toml | 3 +++ src/ndv/views/_wx/__init__.py | 1 - tests/conftest.py | 1 - tests/views/_vispy/test_histogram.py | 2 +- tests/views/_wx/test_lut_view.py | 4 ++-- 10 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index ae8408e8..0330aa95 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -3,4 +3,4 @@ project=ndv issues=false exclude-labels=duplicate,question,invalid,wontfix,hide add-sections={"tests":{"prefix":"**Tests & CI:**","labels":["tests"]}, "documentation":{"prefix":"**Documentation:**", "labels":["documentation"]}} -exclude-tags-regex=.*rc \ No newline at end of file +exclude-tags-regex=.*rc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c34b651e..df69f64b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,17 @@ ci: autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate" repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude: ".*\\.md" + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + args: ["--unsafe"] + - id: check-added-large-files + - repo: https://github.com/abravalheri/validate-pyproject rev: v0.24.1 hooks: @@ -35,3 +46,13 @@ repos: - types-wxpython - qtpy - git+https://github.com/tlambert03/PyQt6-stubs.git@v6.7.3 + + - repo: local + hooks: + - id: pyright + exclude: "^docs/" + name: pyright + language: system + types_or: [python, pyi] + require_serial: true + entry: uv run pyright diff --git a/Makefile b/Makefile index 7158e763..7ce107a3 100644 --- a/Makefile +++ b/Makefile @@ -40,5 +40,5 @@ docs: docs-serve: uv run --group docs mkdocs serve --no-strict -lint: - uv run pre-commit run --all-files \ No newline at end of file +lint: + uv run pre-commit run --all-files diff --git a/docs/css/material.css b/docs/css/material.css index d75b6280..e8c5b9a0 100644 --- a/docs/css/material.css +++ b/docs/css/material.css @@ -2,25 +2,25 @@ .md-main__inner { margin-bottom: 1.5rem; } - + /* Custom admonition: preview */ :root { --md-admonition-icon--preview: url('data:image/svg+xml;charset=utf-8,'); } - + .md-typeset .admonition.preview, .md-typeset details.preview { border-color: rgb(220, 139, 240); } - + .md-typeset .preview>.admonition-title, .md-typeset .preview>summary { background-color: rgba(142, 43, 155, 0.1); } - + .md-typeset .preview>.admonition-title::before, .md-typeset .preview>summary::before { background-color: rgb(220, 139, 240); -webkit-mask-image: var(--md-admonition-icon--preview); mask-image: var(--md-admonition-icon--preview); - } \ No newline at end of file + } diff --git a/examples/cookbook/microscope_dashboard.py b/examples/cookbook/microscope_dashboard.py index 55f1d49f..9511d8d3 100644 --- a/examples/cookbook/microscope_dashboard.py +++ b/examples/cookbook/microscope_dashboard.py @@ -14,6 +14,8 @@ - move the stage in the x and y directions. """ +from __future__ import annotations + from typing import Any, cast import astropy.units as u diff --git a/pyproject.toml b/pyproject.toml index a2bf964b..8b0e1a25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,7 +198,10 @@ enableExperimentalFeatures = true reportOptionalMemberAccess = "none" reportIncompatibleMethodOverride = "none" reportAttributeAccessIssue = "none" +reportMissingModuleSource = "none" reportPrivateImportUsage = "none" +reportMissingImports = "information" +reportArgumentType = "none" # because of pydantic reportCallIssue = "none" verboseOutput = true venv = ".venv" diff --git a/src/ndv/views/_wx/__init__.py b/src/ndv/views/_wx/__init__.py index 8b137891..e69de29b 100644 --- a/src/ndv/views/_wx/__init__.py +++ b/src/ndv/views/_wx/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/conftest.py b/tests/conftest.py index 50a55ed3..3c923111 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,7 +54,6 @@ def any_app(request: pytest.FixtureRequest) -> Iterator[Any]: # since it requires very little setup if importlib.util.find_spec("jupyter"): os.environ[GUI_ENV_VAR] = "jupyter" - gui_frontend.cache_clear() frontend = gui_frontend() diff --git a/tests/views/_vispy/test_histogram.py b/tests/views/_vispy/test_histogram.py index e63ed447..e17b4d94 100644 --- a/tests/views/_vispy/test_histogram.py +++ b/tests/views/_vispy/test_histogram.py @@ -61,7 +61,7 @@ def test_interaction(model: LUTModel, histogram: VispyHistogramCanvas) -> None: histogram.set_clims((left, right)) def world_to_canvas(x: float, y: float) -> tuple[float, float]: - return tuple(histogram.node_tform.imap((x, y))[:2]) + return tuple(histogram.node_tform.imap((x, y))[:2]) # type: ignore # Test cursors x, y = world_to_canvas((left + right) / 2, 0.5) diff --git a/tests/views/_wx/test_lut_view.py b/tests/views/_wx/test_lut_view.py index 9fcb85a0..800296a4 100644 --- a/tests/views/_wx/test_lut_view.py +++ b/tests/views/_wx/test_lut_view.py @@ -57,7 +57,7 @@ def test_WxLutView_update_model(model: LUTModel, view: WxLutView) -> None: def test_WxLutView_update_view(wxapp: wx.App, model: LUTModel, view: WxLutView) -> None: """Ensures the model updates when the view is changed.""" - def processEvent(evt: wx.PyEventBinder, wdg: wx.Control) -> None: + def processEvent(evt: wx.PyEventBinder | int, wdg: wx.Window) -> None: ev = wx.PyCommandEvent(evt.typeId, wdg.GetId()) wx.PostEvent(wdg.GetEventHandler(), ev) # Borrowed from: @@ -118,7 +118,7 @@ def processEvent(evt: wx.PyEventBinder, wdg: wx.Control) -> None: def test_WxLutView_histogram_controls(wxapp: wx.App, view: WxLutView) -> None: - def processEvent(evt: wx.PyEventBinder, wdg: wx.Control) -> None: + def processEvent(evt: wx.PyEventBinder | int, wdg: wx.Window) -> None: ev = wx.PyCommandEvent(evt.typeId, wdg.GetId()) wx.PostEvent(wdg.GetEventHandler(), ev) # Borrowed from: From 36231436c5f435c5bf47701f33dcfcb9540707e7 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 19:46:51 -0400 Subject: [PATCH 04/10] undo toml --- .pre-commit-config.yaml | 1 - pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df69f64b..e064a4ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,6 @@ repos: - id: trailing-whitespace exclude: ".*\\.md" - id: end-of-file-fixer - - id: check-toml - id: check-yaml args: ["--unsafe"] - id: check-added-large-files diff --git a/pyproject.toml b/pyproject.toml index 8b0e1a25..a83dd0c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -201,7 +201,7 @@ reportAttributeAccessIssue = "none" reportMissingModuleSource = "none" reportPrivateImportUsage = "none" reportMissingImports = "information" -reportArgumentType = "none" # because of pydantic +reportArgumentType = "none" # because of pydantic reportCallIssue = "none" verboseOutput = true venv = ".venv" From 6ef7c1e7715485df18381956e4abb2be7bcf3366 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 19:51:36 -0400 Subject: [PATCH 05/10] make local manual --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e064a4ca..c3617df0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,6 +49,7 @@ repos: - repo: local hooks: - id: pyright + stages: [manual] exclude: "^docs/" name: pyright language: system From 695ad2ca1b9ef1e61f49279d323df2eabe349a0d Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 20:01:17 -0400 Subject: [PATCH 06/10] bounding box is not none --- src/ndv/views/_pygfx/_array_canvas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ndv/views/_pygfx/_array_canvas.py b/src/ndv/views/_pygfx/_array_canvas.py index dcff3a0b..a9dfdab1 100755 --- a/src/ndv/views/_pygfx/_array_canvas.py +++ b/src/ndv/views/_pygfx/_array_canvas.py @@ -521,7 +521,7 @@ def set_range( cam = self._camera cam.show_object(self._scene) - if bb := self._scene.get_world_bounding_box(): + if (bb := self._scene.get_world_bounding_box()) is not None: width, height, depth = np.ptp(bb, axis=0) if width < 0.01: width = 1 From 5ecffd10963b464ff8ae1137daa968e4881772e5 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 20:05:00 -0400 Subject: [PATCH 07/10] more is not none --- src/ndv/views/_pygfx/_array_canvas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ndv/views/_pygfx/_array_canvas.py b/src/ndv/views/_pygfx/_array_canvas.py index a9dfdab1..1da8a530 100755 --- a/src/ndv/views/_pygfx/_array_canvas.py +++ b/src/ndv/views/_pygfx/_array_canvas.py @@ -357,7 +357,7 @@ def get_cursor(self, mme: MouseMoveEvent) -> CursorType | None: return CursorType.BDIAG_ARROW # Step 2: Entire ROI if self._outline: - if (roi_bb := self._outline.get_bounding_box()) and _is_inside( + if (roi_bb := self._outline.get_bounding_box()) is not None and _is_inside( roi_bb, world_pos ): return CursorType.ALL_ARROW @@ -583,7 +583,7 @@ def elements_at(self, pos_xy: tuple[float, float]) -> list[CanvasElement]: elements: list[CanvasElement] = [] pos = self.canvas_to_world((pos_xy[0], pos_xy[1])) for c in self._scene.children: - if (bb := c.get_bounding_box()) and _is_inside(bb, pos): + if (bb := c.get_bounding_box()) is not None and _is_inside(bb, pos): elements.append(self._elements[c]) return elements From dde602c91f096a2d69e27fe3be8d46f1b97d44ba Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 20:17:33 -0400 Subject: [PATCH 08/10] better syntax --- src/ndv/views/_pygfx/_array_canvas.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ndv/views/_pygfx/_array_canvas.py b/src/ndv/views/_pygfx/_array_canvas.py index 1da8a530..998a1c5c 100755 --- a/src/ndv/views/_pygfx/_array_canvas.py +++ b/src/ndv/views/_pygfx/_array_canvas.py @@ -37,7 +37,9 @@ WgpuCanvas: TypeAlias = Union[QWgpuCanvas, JupyterWgpuCanvas, WxWgpuCanvas] -def _is_inside(bounding_box: np.ndarray, pos: Sequence[float]) -> bool: +def _is_inside(bounding_box: np.ndarray | None, pos: Sequence[float]) -> bool: + if bounding_box is None: + return False return bool( bounding_box[0, 0] + 0.5 <= pos[0] and pos[0] <= bounding_box[1, 0] + 0.5 @@ -357,9 +359,8 @@ def get_cursor(self, mme: MouseMoveEvent) -> CursorType | None: return CursorType.BDIAG_ARROW # Step 2: Entire ROI if self._outline: - if (roi_bb := self._outline.get_bounding_box()) is not None and _is_inside( - roi_bb, world_pos - ): + roi_bb = self._outline.get_bounding_box() + if _is_inside(roi_bb, world_pos): return CursorType.ALL_ARROW return None @@ -583,7 +584,8 @@ def elements_at(self, pos_xy: tuple[float, float]) -> list[CanvasElement]: elements: list[CanvasElement] = [] pos = self.canvas_to_world((pos_xy[0], pos_xy[1])) for c in self._scene.children: - if (bb := c.get_bounding_box()) is not None and _is_inside(bb, pos): + bb = c.get_bounding_box() + if _is_inside(bb, pos): elements.append(self._elements[c]) return elements From f1b4a5bdc8b3ebf2e8bbdbf3ce07df0b4153d133 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 May 2025 20:18:27 -0400 Subject: [PATCH 09/10] pragma --- src/ndv/views/_pygfx/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ndv/views/_pygfx/_util.py b/src/ndv/views/_pygfx/_util.py index 80ec2066..9dabde2e 100644 --- a/src/ndv/views/_pygfx/_util.py +++ b/src/ndv/views/_pygfx/_util.py @@ -30,4 +30,4 @@ def sizeHint(self) -> QSize: return WxWgpuCanvas # type: ignore[no-any-return] - raise ValueError(f"Unsupported frontend: {frontend}") + raise ValueError(f"Unsupported frontend: {frontend}") # pragma: no cover From f12b213c5fe5960778fb87c6f30e041922637a37 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 19 May 2025 09:00:13 -0400 Subject: [PATCH 10/10] rename key --- src/ndv/views/_wx/_array_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ndv/views/_wx/_array_view.py b/src/ndv/views/_wx/_array_view.py index cd866da7..5c96aefc 100644 --- a/src/ndv/views/_wx/_array_view.py +++ b/src/ndv/views/_wx/_array_view.py @@ -546,9 +546,9 @@ def frontend_widget(self) -> wx.Window: def add_lut_view(self, key: ChannelKey) -> WxLutView: wdg = self.frontend_widget() view = ( - WxRGBView(wdg, channel) - if channel == "RGB" - else WxLutView(wdg, channel, self._viewer_model.default_luts) + WxRGBView(wdg, key) + if key == "RGB" + else WxLutView(wdg, key, self._viewer_model.default_luts) ) self._wxwidget.luts.Add(view._wxwidget, 0, wx.EXPAND | wx.BOTTOM, 5) self._luts[key] = view