Skip to content

Fix leaky QT widgets #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fix = true
convention = "numpy"

[tool.mypy]
python_version = "3.8"
# Block below are checks that form part of mypy 'strict' mode
warn_unused_configs = true
warn_redundant_casts = true
Expand All @@ -51,6 +52,7 @@ disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_reexport = true
warn_return_any = false # TODO: fix
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = [
Expand Down
12 changes: 8 additions & 4 deletions src/napari_matplotlib/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from pathlib import Path
from typing import List, Tuple
from typing import List, Optional, Tuple

import napari
from matplotlib.axes import Axes
Expand Down Expand Up @@ -43,16 +43,20 @@ class NapariMPLWidget(QWidget):
List of currently selected napari layers.
"""

def __init__(self, napari_viewer: napari.viewer.Viewer):
super().__init__()
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
parent: Optional[QWidget] = None,
):
super().__init__(parent=parent)

self.viewer = napari_viewer
self.canvas = FigureCanvas()

self.canvas.figure.patch.set_facecolor("none")
self.canvas.figure.set_layout_engine("constrained")
self.toolbar = NapariNavigationToolbar(
self.canvas, self
self.canvas, parent=self
) # type: ignore[no-untyped-call]
self._replace_toolbar_icons()

Expand Down
17 changes: 11 additions & 6 deletions src/napari_matplotlib/histogram.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from typing import Optional

import napari
import numpy as np
from qtpy.QtWidgets import QWidget

from .base import NapariMPLWidget
from .util import Interval

__all__ = ["HistogramWidget"]

import napari

from .util import Interval

_COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"}


Expand All @@ -19,8 +20,12 @@ class HistogramWidget(NapariMPLWidget):
n_layers_input = Interval(1, 1)
input_layer_types = (napari.layers.Image,)

def __init__(self, napari_viewer: napari.viewer.Viewer):
super().__init__(napari_viewer)
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
parent: Optional[QWidget] = None,
):
super().__init__(napari_viewer, parent=parent)
self.add_single_axes()
self.update_layers(None)

Expand Down
96 changes: 50 additions & 46 deletions src/napari_matplotlib/scatter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from typing import Any, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union

import napari
import numpy.typing as npt
from magicgui import magicgui
from magicgui.widgets import ComboBox
from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget

from .base import NapariMPLWidget
from .util import Interval
Expand All @@ -20,11 +19,13 @@ class ScatterBaseWidget(NapariMPLWidget):
# the scatter is plotted as a 2D histogram
_threshold_to_switch_to_histogram = 500

def __init__(self, napari_viewer: napari.viewer.Viewer):
super().__init__(napari_viewer)

def __init__(
self,
napari_viewer: napari.viewer.Viewer,
parent: Optional[QWidget] = None,
):
super().__init__(napari_viewer, parent=parent)
self.add_single_axes()
self.update_layers(None)

def clear(self) -> None:
"""
Expand Down Expand Up @@ -113,55 +114,57 @@ class FeaturesScatterWidget(ScatterBaseWidget):
napari.layers.Vectors,
)

def __init__(self, napari_viewer: napari.viewer.Viewer):
super().__init__(napari_viewer)
self._key_selection_widget = magicgui(
self._set_axis_keys,
x_axis_key={"choices": self._get_valid_axis_keys},
y_axis_key={"choices": self._get_valid_axis_keys},
call_button="plot",
)
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
parent: Optional[QWidget] = None,
):
super().__init__(napari_viewer, parent=parent)

self.layout().addLayout(QVBoxLayout())

self.layout().addWidget(self._key_selection_widget.native)
self._selectors: Dict[str, QComboBox] = {}
for dim in ["x", "y"]:
self._selectors[dim] = QComboBox()
# Re-draw when combo boxes are updated
self._selectors[dim].currentTextChanged.connect(self._draw)

self.layout().addWidget(QLabel(f"{dim}-axis:"))
self.layout().addWidget(self._selectors[dim])

self.update_layers(None)

@property
def x_axis_key(self) -> Optional[str]:
def x_axis_key(self) -> Union[str, None]:
"""
Key to access x axis data from the FeaturesTable.
"""
return self._x_axis_key
if self._selectors["x"].count() == 0:
return None
else:
return self._selectors["x"].currentText()

@x_axis_key.setter
def x_axis_key(self, key: Optional[str]) -> None:
self._x_axis_key = key
def x_axis_key(self, key: str) -> None:
self._selectors["x"].setCurrentText(key)
self._draw()

@property
def y_axis_key(self) -> Optional[str]:
def y_axis_key(self) -> Union[str, None]:
"""
Key to access y axis data from the FeaturesTable.
"""
return self._y_axis_key
if self._selectors["y"].count() == 0:
return None
else:
return self._selectors["y"].currentText()

@y_axis_key.setter
def y_axis_key(self, key: Optional[str]) -> None:
"""
Set the y-axis key.
"""
self._y_axis_key = key
self._draw()

def _set_axis_keys(self, x_axis_key: str, y_axis_key: str) -> None:
"""
Set both axis keys and then redraw the plot.
"""
self._x_axis_key = x_axis_key
self._y_axis_key = y_axis_key
def y_axis_key(self, key: str) -> None:
self._selectors["y"].setCurrentText(key)
self._draw()

def _get_valid_axis_keys(
self, combo_widget: Optional[ComboBox] = None
) -> List[str]:
def _get_valid_axis_keys(self) -> List[str]:
"""
Get the valid axis keys from the layer FeatureTable.

Expand All @@ -185,11 +188,12 @@ def _ready_to_scatter(self) -> bool:
return False

feature_table = self.layers[0].features
valid_keys = self._get_valid_axis_keys()
return (
feature_table is not None
and len(feature_table) > 0
and self.x_axis_key is not None
and self.y_axis_key is not None
and self.x_axis_key in valid_keys
and self.y_axis_key in valid_keys
)

def draw(self) -> None:
Expand Down Expand Up @@ -230,9 +234,9 @@ def _on_update_layers(self) -> None:
"""
Called when the layer selection changes by ``self.update_layers()``.
"""
if hasattr(self, "_key_selection_widget"):
self._key_selection_widget.reset_choices()

# reset the axis keys
self._x_axis_key = None
self._y_axis_key = None
# Clear combobox
for dim in ["x", "y"]:
while self._selectors[dim].count() > 0:
self._selectors[dim].removeItem(0)
# Add keys for newly selected layer
self._selectors[dim].addItems(self._get_valid_axis_keys())
12 changes: 8 additions & 4 deletions src/napari_matplotlib/slice.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Any, Dict, Tuple
from typing import Any, Dict, Optional, Tuple

import napari
import numpy as np
import numpy.typing as npt
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QSpinBox
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QSpinBox, QWidget

from .base import NapariMPLWidget
from .util import Interval
Expand All @@ -22,9 +22,13 @@ class SliceWidget(NapariMPLWidget):
n_layers_input = Interval(1, 1)
input_layer_types = (napari.layers.Image,)

def __init__(self, napari_viewer: napari.viewer.Viewer):
def __init__(
self,
napari_viewer: napari.viewer.Viewer,
parent: Optional[QWidget] = None,
):
# Setup figure/axes
super().__init__(napari_viewer)
super().__init__(napari_viewer, parent=parent)
self.add_single_axes()

button_layout = QHBoxLayout()
Expand Down
16 changes: 16 additions & 0 deletions src/napari_matplotlib/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

import numpy as np
import pytest
from skimage import data
Expand All @@ -22,3 +24,17 @@ def astronaut_data():
@pytest.fixture
def brain_data():
return data.brain(), {"rgb": False}


@pytest.fixture(autouse=True, scope="session")
def set_strict_qt():
env_var = "NAPARI_STRICT_QT"
old_val = os.environ.get(env_var)
os.environ[env_var] = "1"
# Run tests
yield
# Reset to original value
if old_val is not None:
os.environ[env_var] = old_val
else:
del os.environ[env_var]
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ python =
[testenv]
extras = testing
commands =
- python -c 'from skimage import data; data.brain()'
- python -m pytest --mpl -v --color=yes --cov=napari_matplotlib --cov-report=xml
python -c 'from skimage import data; data.brain()'
python -m pytest --mpl -v --color=yes --cov=napari_matplotlib --cov-report=xml