1
0

test_backend_qt.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import copy
  2. import sys
  3. from unittest import mock
  4. import matplotlib
  5. from matplotlib import pyplot as plt
  6. from matplotlib import rcParams
  7. from matplotlib._pylab_helpers import Gcf
  8. import pytest
  9. @pytest.fixture(autouse=True)
  10. def mpl_test_settings(qt_core, mpl_test_settings):
  11. """
  12. Ensure qt_core fixture is *first* fixture.
  13. We override the `mpl_test_settings` fixture and depend on the `qt_core`
  14. fixture first. It is very important that it is first, because it skips
  15. tests when Qt is not available, and if not, then the main
  16. `mpl_test_settings` fixture will try to switch backends before the skip can
  17. be triggered.
  18. """
  19. @pytest.fixture
  20. def qt_core(request):
  21. backend, = request.node.get_closest_marker('backend').args
  22. if backend == 'Qt4Agg':
  23. if any(k in sys.modules for k in ('PyQt5', 'PySide2')):
  24. pytest.skip('Qt5 binding already imported')
  25. try:
  26. import PyQt4
  27. # RuntimeError if PyQt5 already imported.
  28. except (ImportError, RuntimeError):
  29. try:
  30. import PySide
  31. except ImportError:
  32. pytest.skip("Failed to import a Qt4 binding.")
  33. elif backend == 'Qt5Agg':
  34. if any(k in sys.modules for k in ('PyQt4', 'PySide')):
  35. pytest.skip('Qt4 binding already imported')
  36. try:
  37. import PyQt5
  38. # RuntimeError if PyQt4 already imported.
  39. except (ImportError, RuntimeError):
  40. try:
  41. import PySide2
  42. except ImportError:
  43. pytest.skip("Failed to import a Qt5 binding.")
  44. else:
  45. raise ValueError('Backend marker has unknown value: ' + backend)
  46. qt_compat = pytest.importorskip('matplotlib.backends.qt_compat')
  47. QtCore = qt_compat.QtCore
  48. if backend == 'Qt4Agg':
  49. try:
  50. py_qt_ver = int(QtCore.PYQT_VERSION_STR.split('.')[0])
  51. except AttributeError:
  52. py_qt_ver = QtCore.__version_info__[0]
  53. if py_qt_ver != 4:
  54. pytest.skip('Qt4 is not available')
  55. return QtCore
  56. @pytest.mark.parametrize('backend', [
  57. # Note: the value is irrelevant; the important part is the marker.
  58. pytest.param('Qt4Agg', marks=pytest.mark.backend('Qt4Agg')),
  59. pytest.param('Qt5Agg', marks=pytest.mark.backend('Qt5Agg')),
  60. ])
  61. def test_fig_close(backend):
  62. # save the state of Gcf.figs
  63. init_figs = copy.copy(Gcf.figs)
  64. # make a figure using pyplot interface
  65. fig = plt.figure()
  66. # simulate user clicking the close button by reaching in
  67. # and calling close on the underlying Qt object
  68. fig.canvas.manager.window.close()
  69. # assert that we have removed the reference to the FigureManager
  70. # that got added by plt.figure()
  71. assert init_figs == Gcf.figs
  72. @pytest.mark.backend('Qt5Agg')
  73. def test_fig_signals(qt_core):
  74. # Create a figure
  75. plt.figure()
  76. # Access signals
  77. import signal
  78. event_loop_signal = None
  79. # Callback to fire during event loop: save SIGINT handler, then exit
  80. def fire_signal_and_quit():
  81. # Save event loop signal
  82. nonlocal event_loop_signal
  83. event_loop_signal = signal.getsignal(signal.SIGINT)
  84. # Request event loop exit
  85. qt_core.QCoreApplication.exit()
  86. # Timer to exit event loop
  87. qt_core.QTimer.singleShot(0, fire_signal_and_quit)
  88. # Save original SIGINT handler
  89. original_signal = signal.getsignal(signal.SIGINT)
  90. # Use our own SIGINT handler to be 100% sure this is working
  91. def CustomHandler(signum, frame):
  92. pass
  93. signal.signal(signal.SIGINT, CustomHandler)
  94. # mainloop() sets SIGINT, starts Qt event loop (which triggers timer and
  95. # exits) and then mainloop() resets SIGINT
  96. matplotlib.backends.backend_qt5._BackendQT5.mainloop()
  97. # Assert: signal handler during loop execution is signal.SIG_DFL
  98. assert event_loop_signal == signal.SIG_DFL
  99. # Assert: current signal handler is the same as the one we set before
  100. assert CustomHandler == signal.getsignal(signal.SIGINT)
  101. # Reset SIGINT handler to what it was before the test
  102. signal.signal(signal.SIGINT, original_signal)
  103. @pytest.mark.parametrize(
  104. 'qt_key, qt_mods, answer',
  105. [
  106. ('Key_A', ['ShiftModifier'], 'A'),
  107. ('Key_A', [], 'a'),
  108. ('Key_A', ['ControlModifier'], 'ctrl+a'),
  109. ('Key_Aacute', ['ShiftModifier'],
  110. '\N{LATIN CAPITAL LETTER A WITH ACUTE}'),
  111. ('Key_Aacute', [],
  112. '\N{LATIN SMALL LETTER A WITH ACUTE}'),
  113. ('Key_Control', ['AltModifier'], 'alt+control'),
  114. ('Key_Alt', ['ControlModifier'], 'ctrl+alt'),
  115. ('Key_Aacute', ['ControlModifier', 'AltModifier', 'MetaModifier'],
  116. 'ctrl+alt+super+\N{LATIN SMALL LETTER A WITH ACUTE}'),
  117. ('Key_Backspace', [], 'backspace'),
  118. ('Key_Backspace', ['ControlModifier'], 'ctrl+backspace'),
  119. ('Key_Play', [], None),
  120. ],
  121. ids=[
  122. 'shift',
  123. 'lower',
  124. 'control',
  125. 'unicode_upper',
  126. 'unicode_lower',
  127. 'alt_control',
  128. 'control_alt',
  129. 'modifier_order',
  130. 'backspace',
  131. 'backspace_mod',
  132. 'non_unicode_key',
  133. ]
  134. )
  135. @pytest.mark.parametrize('backend', [
  136. # Note: the value is irrelevant; the important part is the marker.
  137. pytest.param('Qt4Agg', marks=pytest.mark.backend('Qt4Agg')),
  138. pytest.param('Qt5Agg', marks=pytest.mark.backend('Qt5Agg')),
  139. ])
  140. def test_correct_key(backend, qt_core, qt_key, qt_mods, answer):
  141. """
  142. Make a figure.
  143. Send a key_press_event event (using non-public, qtX backend specific api).
  144. Catch the event.
  145. Assert sent and caught keys are the same.
  146. """
  147. qt_mod = qt_core.Qt.NoModifier
  148. for mod in qt_mods:
  149. qt_mod |= getattr(qt_core.Qt, mod)
  150. class _Event:
  151. def isAutoRepeat(self): return False
  152. def key(self): return getattr(qt_core.Qt, qt_key)
  153. def modifiers(self): return qt_mod
  154. def receive(event):
  155. assert event.key == answer
  156. qt_canvas = plt.figure().canvas
  157. qt_canvas.mpl_connect('key_press_event', receive)
  158. qt_canvas.keyPressEvent(_Event())
  159. @pytest.mark.backend('Qt5Agg')
  160. def test_dpi_ratio_change():
  161. """
  162. Make sure that if _dpi_ratio changes, the figure dpi changes but the
  163. widget remains the same physical size.
  164. """
  165. prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT._dpi_ratio'
  166. with mock.patch(prop, new_callable=mock.PropertyMock) as p:
  167. p.return_value = 3
  168. fig = plt.figure(figsize=(5, 2), dpi=120)
  169. qt_canvas = fig.canvas
  170. qt_canvas.show()
  171. from matplotlib.backends.backend_qt5 import qApp
  172. # Make sure the mocking worked
  173. assert qt_canvas._dpi_ratio == 3
  174. size = qt_canvas.size()
  175. qt_canvas.manager.show()
  176. qt_canvas.draw()
  177. qApp.processEvents()
  178. # The DPI and the renderer width/height change
  179. assert fig.dpi == 360
  180. assert qt_canvas.renderer.width == 1800
  181. assert qt_canvas.renderer.height == 720
  182. # The actual widget size and figure physical size don't change
  183. assert size.width() == 600
  184. assert size.height() == 240
  185. assert qt_canvas.get_width_height() == (600, 240)
  186. assert (fig.get_size_inches() == (5, 2)).all()
  187. p.return_value = 2
  188. assert qt_canvas._dpi_ratio == 2
  189. qt_canvas.draw()
  190. qApp.processEvents()
  191. # this second processEvents is required to fully run the draw.
  192. # On `update` we notice the DPI has changed and trigger a
  193. # resize event to refresh, the second processEvents is
  194. # required to process that and fully update the window sizes.
  195. qApp.processEvents()
  196. # The DPI and the renderer width/height change
  197. assert fig.dpi == 240
  198. assert qt_canvas.renderer.width == 1200
  199. assert qt_canvas.renderer.height == 480
  200. # The actual widget size and figure physical size don't change
  201. assert size.width() == 600
  202. assert size.height() == 240
  203. assert qt_canvas.get_width_height() == (600, 240)
  204. assert (fig.get_size_inches() == (5, 2)).all()
  205. @pytest.mark.backend('Qt5Agg')
  206. def test_subplottool():
  207. fig, ax = plt.subplots()
  208. with mock.patch(
  209. "matplotlib.backends.backend_qt5.SubplotToolQt.exec_",
  210. lambda self: None):
  211. fig.canvas.manager.toolbar.configure_subplots()
  212. @pytest.mark.backend('Qt5Agg')
  213. def test_figureoptions():
  214. fig, ax = plt.subplots()
  215. ax.plot([1, 2])
  216. ax.imshow([[1]])
  217. ax.scatter(range(3), range(3), c=range(3))
  218. with mock.patch(
  219. "matplotlib.backends.qt_editor._formlayout.FormDialog.exec_",
  220. lambda self: None):
  221. fig.canvas.manager.toolbar.edit_parameters()
  222. @pytest.mark.backend('Qt5Agg')
  223. def test_double_resize():
  224. # Check that resizing a figure twice keeps the same window size
  225. fig, ax = plt.subplots()
  226. fig.canvas.draw()
  227. window = fig.canvas.manager.window
  228. w, h = 3, 2
  229. fig.set_size_inches(w, h)
  230. assert fig.canvas.width() == w * rcParams['figure.dpi']
  231. assert fig.canvas.height() == h * rcParams['figure.dpi']
  232. old_width = window.width()
  233. old_height = window.height()
  234. fig.set_size_inches(w, h)
  235. assert window.width() == old_width
  236. assert window.height() == old_height
  237. @pytest.mark.backend("Qt5Agg")
  238. def test_canvas_reinit():
  239. import matplotlib.pyplot as plt
  240. from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
  241. from functools import partial
  242. called = False
  243. def crashing_callback(fig, stale):
  244. nonlocal called
  245. fig.canvas.draw_idle()
  246. called = True
  247. fig, ax = plt.subplots()
  248. fig.stale_callback = crashing_callback
  249. # this should not raise
  250. canvas = FigureCanvasQTAgg(fig)
  251. assert called