Skip to content

Commit 84efb18

Browse files
committed
unit tests and small fix
1 parent c65ba1e commit 84efb18

4 files changed

Lines changed: 359 additions & 4 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# guitest: show
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
import qtpy.QtCore as QC
8+
from guidata.qthelpers import exec_dialog, qt_app_context
9+
10+
from plotpy.tests import vistools as ptv
11+
from plotpy.tests.features.test_auto_curve_image import make_curve_image_legend
12+
from plotpy.tools.annotation import (
13+
AnnotatedCircleTool,
14+
AnnotatedEllipseTool,
15+
AnnotatedObliqueRectangleTool,
16+
AnnotatedPointTool,
17+
AnnotatedRectangleTool,
18+
AnnotatedSegmentTool,
19+
)
20+
from plotpy.tools.cross_section import (
21+
AverageCrossSectionTool,
22+
CrossSectionTool,
23+
ObliqueCrossSectionTool,
24+
)
25+
from plotpy.tools.image import ImageStatsTool
26+
from plotpy.tools.misc import SnapshotTool
27+
from plotpy.tools.shape import (
28+
CircleTool,
29+
EllipseTool,
30+
ObliqueRectangleTool,
31+
PointTool,
32+
RectangleTool,
33+
SegmentTool,
34+
)
35+
36+
if TYPE_CHECKING:
37+
from plotpy.plot.plotwidget import PlotDialog, PlotWindow
38+
from plotpy.tools.base import RectangularActionTool
39+
40+
P0 = QC.QPointF(10, 10)
41+
P1 = QC.QPointF(100, 100)
42+
43+
TOOLS = (
44+
AnnotatedCircleTool,
45+
AnnotatedEllipseTool,
46+
AnnotatedObliqueRectangleTool,
47+
AnnotatedPointTool,
48+
AnnotatedRectangleTool,
49+
AnnotatedSegmentTool,
50+
AverageCrossSectionTool,
51+
CrossSectionTool,
52+
ObliqueCrossSectionTool,
53+
SnapshotTool,
54+
ImageStatsTool,
55+
CircleTool,
56+
EllipseTool,
57+
ObliqueRectangleTool,
58+
PointTool,
59+
RectangleTool,
60+
SegmentTool,
61+
)
62+
63+
64+
def create_window(tool_classes: tuple[type[RectangularActionTool], ...]) -> PlotWindow:
65+
items = make_curve_image_legend()
66+
win = ptv.show_items(
67+
items, wintitle="Unit tests for RectangularActionTools", auto_tools=True
68+
)
69+
70+
for toolklass in tool_classes:
71+
tool = win.manager.add_tool(toolklass)
72+
73+
assert len(tool_classes) > 0
74+
win.manager.set_default_tool(tool)
75+
76+
return win
77+
78+
79+
def use_tool(win: PlotDialog, tool_class: type[RectangularActionTool]):
80+
filter_ = win.manager.get_plot().filter
81+
if (tool := win.manager.get_tool(tool_class)) is not None:
82+
tool.end_rect(filter_, P0, P1)
83+
84+
85+
def _test_annotation_tools(tool_classes: tuple[type[RectangularActionTool], ...]):
86+
with qt_app_context(exec_loop=False):
87+
win = create_window(tool_classes)
88+
for tool_class in tool_classes:
89+
use_tool(win, tool_class)
90+
exec_dialog(win)
91+
92+
93+
def test_annotated_circle_tool():
94+
_test_annotation_tools((AnnotatedCircleTool,))
95+
96+
97+
def test_annotated_ellipse_tool():
98+
_test_annotation_tools((AnnotatedEllipseTool,))
99+
100+
101+
def test_annotated_oblique_rectangle_tool():
102+
_test_annotation_tools((AnnotatedObliqueRectangleTool,))
103+
104+
105+
def test_annotated_point_tool():
106+
_test_annotation_tools((AnnotatedPointTool,))
107+
108+
109+
def test_annotated_rectangle_tool():
110+
_test_annotation_tools((AnnotatedRectangleTool,))
111+
112+
113+
def test_annotated_segment_tool():
114+
_test_annotation_tools((AnnotatedSegmentTool,))
115+
116+
117+
def test_avg_cross_section_tool():
118+
_test_annotation_tools((AverageCrossSectionTool,))
119+
120+
121+
def test_cross_section_tool():
122+
_test_annotation_tools((CrossSectionTool,))
123+
124+
125+
def test_oblique_cross_section_tool():
126+
_test_annotation_tools((ObliqueCrossSectionTool,))
127+
128+
129+
def test_snapshot_tool():
130+
_test_annotation_tools((SnapshotTool,))
131+
132+
133+
def test_image_stats_tool():
134+
_test_annotation_tools((ImageStatsTool,))
135+
136+
137+
def test_circle_tool():
138+
_test_annotation_tools((CircleTool,))
139+
140+
141+
def test_ellipse_tool():
142+
_test_annotation_tools((EllipseTool,))
143+
144+
145+
def test_oblique_rectangle_tool():
146+
_test_annotation_tools((ObliqueRectangleTool,))
147+
148+
149+
def test_point_tool():
150+
_test_annotation_tools((PointTool,))
151+
152+
153+
def test_rectangle_tool():
154+
_test_annotation_tools((RectangleTool,))
155+
156+
157+
def test_segment_tool():
158+
_test_annotation_tools((SegmentTool,))
159+
160+
161+
if __name__ == "__main__":
162+
_test_annotation_tools(TOOLS)
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# guitest: show
2+
3+
from typing import TYPE_CHECKING, TypeVar
4+
5+
import numpy as np
6+
import qtpy.QtCore as QC
7+
import qtpy.QtGui as QG
8+
import qtpy.QtWidgets as QW
9+
from guidata.qthelpers import exec_dialog, qt_app_context
10+
from numpy import linspace, sin
11+
12+
from plotpy.builder import make
13+
from plotpy.plot.plotwidget import PlotDialog, PlotWindow
14+
from plotpy.tests import vistools as ptv
15+
from plotpy.tools.base import InteractiveTool
16+
from plotpy.tools.curve import EditPointTool, SelectPointsTool, SelectPointTool
17+
18+
if TYPE_CHECKING:
19+
from plotpy.items.curve.base import CurveItem
20+
21+
CLICK = (QC.QEvent.Type.MouseButtonPress, QC.QEvent.Type.MouseButtonRelease)
22+
ToolT = TypeVar("ToolT", bound=InteractiveTool)
23+
24+
25+
def mouse_event_at_relative_plot_pos(
26+
win: PlotDialog,
27+
qapp: QW.QApplication,
28+
relative_xy: tuple[float, float],
29+
click_types: tuple[QC.QEvent.Type, ...] = (QC.QEvent.Type.MouseButtonPress,),
30+
mod=QC.Qt.KeyboardModifier.NoModifier,
31+
) -> None:
32+
"""Simulates a click on the plot at the given relative position.
33+
34+
Args:
35+
win: window containing the plot
36+
qapp: Main QApplication instance
37+
relative_xy: Relative position of the click on the plot
38+
click_types: Different mouse button event types to send.
39+
Defaults to (QC.QEvent.Type.MouseButtonPress,).
40+
mod: Keyboard modifier. Defaults to QC.Qt.KeyboardModifier.NoModifier.
41+
42+
Returns:
43+
None
44+
"""
45+
plot = win.manager.get_plot()
46+
47+
plot = win.manager.get_plot()
48+
canva: QW.QWidget = plot.canvas() # type: ignore
49+
size = canva.size()
50+
pos_x, pos_y = (
51+
relative_xy[0] * size.width(),
52+
size.height() - relative_xy[1] * size.height(),
53+
)
54+
# pos_x, pos_y = axes_to_canvas(plot.get_active_item(), x, y)
55+
canva_pos = QC.QPointF(pos_x, pos_y).toPoint()
56+
glob_pos = canva.mapToGlobal(canva_pos)
57+
# QTest.mouseClick(canva, QC.Qt.MouseButton.LeftButton, mod, canva_pos)
58+
59+
for type_ in click_types:
60+
mouse_event_press = QG.QMouseEvent(
61+
type_,
62+
canva_pos,
63+
glob_pos,
64+
QC.Qt.MouseButton.LeftButton,
65+
QC.Qt.MouseButton.LeftButton,
66+
mod,
67+
)
68+
qapp.sendEvent(canva, mouse_event_press)
69+
70+
71+
def drag_mouse(
72+
win: PlotDialog,
73+
qapp: QW.QApplication,
74+
x_path: np.ndarray,
75+
y_path: np.ndarray,
76+
mod=QC.Qt.KeyboardModifier.NoModifier,
77+
) -> None:
78+
x0, y0 = x_path[0], y_path[0]
79+
xn, yn = x_path[-1], y_path[-1]
80+
press = (QC.QEvent.Type.MouseButtonPress,)
81+
move = (QC.QEvent.Type.MouseMove,)
82+
release = (QC.QEvent.Type.MouseButtonRelease,)
83+
84+
mouse_event_at_relative_plot_pos(win, qapp, (x0, y0), press, mod)
85+
for x, y in zip(x_path, y_path):
86+
mouse_event_at_relative_plot_pos(win, qapp, (x, y), move, mod)
87+
mouse_event_at_relative_plot_pos(win, qapp, (xn, yn), release, mod)
88+
89+
90+
def create_window(tool_class: type[ToolT]) -> tuple[PlotWindow, ToolT]:
91+
n = 100
92+
x = linspace(
93+
0,
94+
n,
95+
)
96+
y = (sin(sin(sin(x / (n / 10)))) - 1) * -n
97+
curve = make.curve(x, y, color="b")
98+
win = ptv.show_items(
99+
[curve], wintitle="Unit tests for Point tools", auto_tools=False
100+
)
101+
plot = win.manager.get_plot()
102+
plot.set_active_item(curve) # type: ignore
103+
104+
tool = win.manager.add_tool(tool_class)
105+
tool.activate()
106+
return win, tool
107+
108+
109+
def test_select_point_tool_1():
110+
with qt_app_context(exec_loop=False) as qapp:
111+
win, tool = create_window(SelectPointTool)
112+
113+
mouse_event_at_relative_plot_pos(
114+
win,
115+
qapp,
116+
(0.5, 0.5),
117+
CLICK,
118+
)
119+
assert tool.get_coordinates() is not None
120+
exec_dialog(win)
121+
122+
123+
def test_select_point_tool_2():
124+
with qt_app_context(exec_loop=False) as qapp:
125+
win, tool = create_window(SelectPointTool)
126+
tool.on_active_item = True
127+
mouse_event_at_relative_plot_pos(
128+
win,
129+
qapp,
130+
(0.5, 0.5),
131+
CLICK,
132+
)
133+
assert tool.get_coordinates() is not None
134+
exec_dialog(win)
135+
136+
137+
def test_select_points_tool():
138+
with qt_app_context(exec_loop=False) as qapp:
139+
win, tool = create_window(tool_class=SelectPointsTool)
140+
mod = QC.Qt.KeyboardModifier.ControlModifier
141+
142+
mouse_event_at_relative_plot_pos(win, qapp, (0.4, 0.5), CLICK, mod)
143+
assert len(tool.get_coordinates() or ()) == 1
144+
145+
mouse_event_at_relative_plot_pos(win, qapp, (0.5, 0.5), CLICK, mod)
146+
mouse_event_at_relative_plot_pos(win, qapp, (0.6, 0.5), CLICK, mod)
147+
assert len(tool.get_coordinates() or ()) == 3
148+
149+
mouse_event_at_relative_plot_pos(win, qapp, (0.6, 0.5), CLICK, mod)
150+
assert len(tool.get_coordinates() or ()) == 2
151+
152+
mouse_event_at_relative_plot_pos(win, qapp, (0.7, 0.5), CLICK, mod)
153+
assert len(tool.get_coordinates() or ()) == 3
154+
155+
mouse_event_at_relative_plot_pos(win, qapp, (0.1, 0.1), CLICK)
156+
assert len(tool.get_coordinates() or ()) == 1
157+
158+
exec_dialog(win)
159+
160+
161+
def test_edit_point_tool():
162+
with qt_app_context(exec_loop=False) as qapp:
163+
win, tool = create_window(EditPointTool)
164+
assert tool is not None
165+
166+
n = 100
167+
min_v, max_v = 0, 1
168+
x_path = np.full(n, min_v)
169+
y_path = np.linspace(max_v, min_v, n)
170+
drag_mouse(win, qapp, x_path, y_path)
171+
172+
x_path = np.full(n, max_v)
173+
174+
drag_mouse(win, qapp, x_path, y_path)
175+
176+
curve_item: CurveItem = win.manager.get_plot().get_active_item() # type: ignore
177+
curve_changes = tool.get_changes()[curve_item]
178+
for i, (x, y) in curve_changes.items():
179+
x_arr, y_arr = curve_item.get_data()
180+
assert x == x_arr[i]
181+
assert y == y_arr[i]
182+
exec_dialog(win)
183+
184+
185+
if __name__ == "__main__":
186+
test_select_point_tool_1()
187+
test_select_point_tool_2()
188+
test_select_points_tool()
189+
test_edit_point_tool()

plotpy/tools/base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
from qtpy import QtWidgets as QW
1010

1111
from plotpy.constants import SHAPE_Z_OFFSET
12-
from plotpy.events import RectangularSelectionHandler, setup_standard_tool_filter
12+
from plotpy.events import (
13+
RectangularSelectionHandler,
14+
StatefulEventFilter,
15+
setup_standard_tool_filter,
16+
)
1317
from plotpy.items.shape.rectangle import RectangleShape
1418

1519

@@ -442,7 +446,7 @@ def setup_filter(self, baseplot):
442446
handler.SIG_END_RECT.connect(self.end_rect)
443447
return setup_standard_tool_filter(filter, start_state)
444448

445-
def end_rect(self, filter, p0, p1):
449+
def end_rect(self, filter: StatefulEventFilter, p0: QC.QPointF, p1: QC.QPointF):
446450
"""
447451
448452
:param filter:

plotpy/tools/curve.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
from plotpy.interfaces import ICurveItemType
2727
from plotpy.items import Marker, XRangeSelection
2828
from plotpy.items.curve.base import CurveItem
29+
from plotpy.plot.base import BasePlot
2930
from plotpy.tools.base import DefaultToolbarID, InteractiveTool, ToggleTool
3031
from plotpy.tools.cursor import BaseCursorTool
3132

3233
if TYPE_CHECKING:
33-
from plotpy.plot.base import BasePlot
3434
from plotpy.plot.manager import PlotManager
3535

3636

@@ -618,7 +618,7 @@ def label_cb(x, y):
618618

619619
return label_cb
620620

621-
def get_coordinates(self) -> tuple[tuple[float, float], ...] | None:
621+
def get_coordinates(self) -> tuple[tuple[float, float], ...]:
622622
"""Get all selected coordinates"""
623623
return tuple(self.markers.keys())
624624

0 commit comments

Comments
 (0)