Skip to content

Commit fe24d4f

Browse files
authored
Merge pull request #108 from dstansby/leaks
Fix leaky QT widgets
2 parents 9c21e12 + d067f83 commit fe24d4f

File tree

7 files changed

+97
-62
lines changed

7 files changed

+97
-62
lines changed

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ fix = true
3636
convention = "numpy"
3737

3838
[tool.mypy]
39+
python_version = "3.8"
3940
# Block below are checks that form part of mypy 'strict' mode
4041
warn_unused_configs = true
4142
warn_redundant_casts = true
@@ -51,6 +52,7 @@ disallow_incomplete_defs = true
5152
disallow_untyped_defs = true
5253
no_implicit_reexport = true
5354
warn_return_any = false # TODO: fix
55+
ignore_missing_imports = true
5456

5557
[[tool.mypy.overrides]]
5658
module = [

src/napari_matplotlib/base.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
from pathlib import Path
3-
from typing import List, Tuple
3+
from typing import List, Optional, Tuple
44

55
import napari
66
from matplotlib.axes import Axes
@@ -43,16 +43,20 @@ class NapariMPLWidget(QWidget):
4343
List of currently selected napari layers.
4444
"""
4545

46-
def __init__(self, napari_viewer: napari.viewer.Viewer):
47-
super().__init__()
46+
def __init__(
47+
self,
48+
napari_viewer: napari.viewer.Viewer,
49+
parent: Optional[QWidget] = None,
50+
):
51+
super().__init__(parent=parent)
4852

4953
self.viewer = napari_viewer
5054
self.canvas = FigureCanvas()
5155

5256
self.canvas.figure.patch.set_facecolor("none")
5357
self.canvas.figure.set_layout_engine("constrained")
5458
self.toolbar = NapariNavigationToolbar(
55-
self.canvas, self
59+
self.canvas, parent=self
5660
) # type: ignore[no-untyped-call]
5761
self._replace_toolbar_icons()
5862

src/napari_matplotlib/histogram.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
from typing import Optional
2+
3+
import napari
14
import numpy as np
5+
from qtpy.QtWidgets import QWidget
26

37
from .base import NapariMPLWidget
8+
from .util import Interval
49

510
__all__ = ["HistogramWidget"]
611

7-
import napari
8-
9-
from .util import Interval
10-
1112
_COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"}
1213

1314

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

22-
def __init__(self, napari_viewer: napari.viewer.Viewer):
23-
super().__init__(napari_viewer)
23+
def __init__(
24+
self,
25+
napari_viewer: napari.viewer.Viewer,
26+
parent: Optional[QWidget] = None,
27+
):
28+
super().__init__(napari_viewer, parent=parent)
2429
self.add_single_axes()
2530
self.update_layers(None)
2631

src/napari_matplotlib/scatter.py

+50-46
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from typing import Any, List, Optional, Tuple
1+
from typing import Any, Dict, List, Optional, Tuple, Union
22

33
import napari
44
import numpy.typing as npt
5-
from magicgui import magicgui
6-
from magicgui.widgets import ComboBox
5+
from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget
76

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

23-
def __init__(self, napari_viewer: napari.viewer.Viewer):
24-
super().__init__(napari_viewer)
25-
22+
def __init__(
23+
self,
24+
napari_viewer: napari.viewer.Viewer,
25+
parent: Optional[QWidget] = None,
26+
):
27+
super().__init__(napari_viewer, parent=parent)
2628
self.add_single_axes()
27-
self.update_layers(None)
2829

2930
def clear(self) -> None:
3031
"""
@@ -113,55 +114,57 @@ class FeaturesScatterWidget(ScatterBaseWidget):
113114
napari.layers.Vectors,
114115
)
115116

116-
def __init__(self, napari_viewer: napari.viewer.Viewer):
117-
super().__init__(napari_viewer)
118-
self._key_selection_widget = magicgui(
119-
self._set_axis_keys,
120-
x_axis_key={"choices": self._get_valid_axis_keys},
121-
y_axis_key={"choices": self._get_valid_axis_keys},
122-
call_button="plot",
123-
)
117+
def __init__(
118+
self,
119+
napari_viewer: napari.viewer.Viewer,
120+
parent: Optional[QWidget] = None,
121+
):
122+
super().__init__(napari_viewer, parent=parent)
123+
124+
self.layout().addLayout(QVBoxLayout())
124125

125-
self.layout().addWidget(self._key_selection_widget.native)
126+
self._selectors: Dict[str, QComboBox] = {}
127+
for dim in ["x", "y"]:
128+
self._selectors[dim] = QComboBox()
129+
# Re-draw when combo boxes are updated
130+
self._selectors[dim].currentTextChanged.connect(self._draw)
131+
132+
self.layout().addWidget(QLabel(f"{dim}-axis:"))
133+
self.layout().addWidget(self._selectors[dim])
134+
135+
self.update_layers(None)
126136

127137
@property
128-
def x_axis_key(self) -> Optional[str]:
138+
def x_axis_key(self) -> Union[str, None]:
129139
"""
130140
Key to access x axis data from the FeaturesTable.
131141
"""
132-
return self._x_axis_key
142+
if self._selectors["x"].count() == 0:
143+
return None
144+
else:
145+
return self._selectors["x"].currentText()
133146

134147
@x_axis_key.setter
135-
def x_axis_key(self, key: Optional[str]) -> None:
136-
self._x_axis_key = key
148+
def x_axis_key(self, key: str) -> None:
149+
self._selectors["x"].setCurrentText(key)
137150
self._draw()
138151

139152
@property
140-
def y_axis_key(self) -> Optional[str]:
153+
def y_axis_key(self) -> Union[str, None]:
141154
"""
142155
Key to access y axis data from the FeaturesTable.
143156
"""
144-
return self._y_axis_key
157+
if self._selectors["y"].count() == 0:
158+
return None
159+
else:
160+
return self._selectors["y"].currentText()
145161

146162
@y_axis_key.setter
147-
def y_axis_key(self, key: Optional[str]) -> None:
148-
"""
149-
Set the y-axis key.
150-
"""
151-
self._y_axis_key = key
152-
self._draw()
153-
154-
def _set_axis_keys(self, x_axis_key: str, y_axis_key: str) -> None:
155-
"""
156-
Set both axis keys and then redraw the plot.
157-
"""
158-
self._x_axis_key = x_axis_key
159-
self._y_axis_key = y_axis_key
163+
def y_axis_key(self, key: str) -> None:
164+
self._selectors["y"].setCurrentText(key)
160165
self._draw()
161166

162-
def _get_valid_axis_keys(
163-
self, combo_widget: Optional[ComboBox] = None
164-
) -> List[str]:
167+
def _get_valid_axis_keys(self) -> List[str]:
165168
"""
166169
Get the valid axis keys from the layer FeatureTable.
167170
@@ -185,11 +188,12 @@ def _ready_to_scatter(self) -> bool:
185188
return False
186189

187190
feature_table = self.layers[0].features
191+
valid_keys = self._get_valid_axis_keys()
188192
return (
189193
feature_table is not None
190194
and len(feature_table) > 0
191-
and self.x_axis_key is not None
192-
and self.y_axis_key is not None
195+
and self.x_axis_key in valid_keys
196+
and self.y_axis_key in valid_keys
193197
)
194198

195199
def draw(self) -> None:
@@ -230,9 +234,9 @@ def _on_update_layers(self) -> None:
230234
"""
231235
Called when the layer selection changes by ``self.update_layers()``.
232236
"""
233-
if hasattr(self, "_key_selection_widget"):
234-
self._key_selection_widget.reset_choices()
235-
236-
# reset the axis keys
237-
self._x_axis_key = None
238-
self._y_axis_key = None
237+
# Clear combobox
238+
for dim in ["x", "y"]:
239+
while self._selectors[dim].count() > 0:
240+
self._selectors[dim].removeItem(0)
241+
# Add keys for newly selected layer
242+
self._selectors[dim].addItems(self._get_valid_axis_keys())

src/napari_matplotlib/slice.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from typing import Any, Dict, Tuple
1+
from typing import Any, Dict, Optional, Tuple
22

33
import napari
44
import numpy as np
55
import numpy.typing as npt
6-
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QSpinBox
6+
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QSpinBox, QWidget
77

88
from .base import NapariMPLWidget
99
from .util import Interval
@@ -22,9 +22,13 @@ class SliceWidget(NapariMPLWidget):
2222
n_layers_input = Interval(1, 1)
2323
input_layer_types = (napari.layers.Image,)
2424

25-
def __init__(self, napari_viewer: napari.viewer.Viewer):
25+
def __init__(
26+
self,
27+
napari_viewer: napari.viewer.Viewer,
28+
parent: Optional[QWidget] = None,
29+
):
2630
# Setup figure/axes
27-
super().__init__(napari_viewer)
31+
super().__init__(napari_viewer, parent=parent)
2832
self.add_single_axes()
2933

3034
button_layout = QHBoxLayout()

src/napari_matplotlib/tests/conftest.py

+16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import numpy as np
24
import pytest
35
from skimage import data
@@ -22,3 +24,17 @@ def astronaut_data():
2224
@pytest.fixture
2325
def brain_data():
2426
return data.brain(), {"rgb": False}
27+
28+
29+
@pytest.fixture(autouse=True, scope="session")
30+
def set_strict_qt():
31+
env_var = "NAPARI_STRICT_QT"
32+
old_val = os.environ.get(env_var)
33+
os.environ[env_var] = "1"
34+
# Run tests
35+
yield
36+
# Reset to original value
37+
if old_val is not None:
38+
os.environ[env_var] = old_val
39+
else:
40+
del os.environ[env_var]

tox.ini

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ python =
1212
[testenv]
1313
extras = testing
1414
commands =
15-
- python -c 'from skimage import data; data.brain()'
16-
- python -m pytest --mpl -v --color=yes --cov=napari_matplotlib --cov-report=xml
15+
python -c 'from skimage import data; data.brain()'
16+
python -m pytest --mpl -v --color=yes --cov=napari_matplotlib --cov-report=xml

0 commit comments

Comments
 (0)