Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Remove figure from Gcf when it is closed #1683

Closed
wants to merge 11 commits into from

4 participants

@tacaswell
Owner

Re-wired signal/slot connections so that the figure in removed from Gcf when it is closed.

In PR #1498 the attribute WA_DeleteOnClose was no longer set on the
QtMainWindow object in QtFigureManager. The signal connection that
was being used to remove the figure from Gcf when the window was closed
was tied to the destroyed() signal of QtMainWindow, which is no
longer being destroyed. Thus, gca and gcf would return references
to no-longer visible figures/axes. _widgetclosed is now called when
MainWindow emits 'closing()'.

Thomas A Caswell Re-wired signal/slot connections so that the figure in removed from
Gcf when it is closed.

In PR #1498 the attribute WA_DeleteOnClose was no longer set on the
QtMainWindow object in QtFigureManager.  The signal connection that
was being used to remove the figure from Gcf when the window was closed
was tied to the `destroyed()` signal of QtMainWindow, which is no
longer being destroyed.  Thus, gca and gcf would return references
to no-longer visible figures/axes.  _widgetclosed is now called when
MainWindow emits 'closing()'.
121acdc
@dmcdougall
Collaborator

How do we test these Qt4-based issues? I merged #1678 because there appeared to be no succinct way to test it, but perhaps that was an oversight on my part.

lib/matplotlib/backends/backend_qt4.py
@@ -389,7 +389,9 @@ def __init__( self, canvas, num ):
self.window = MainWindow()
self.window.connect(self.window, QtCore.SIGNAL('closing()'),
canvas.close_event)
-
+ self.window.connect( self.window, QtCore.SIGNAL( 'closing()' ),
@pelson Collaborator
pelson added a note

Extra whitespace has found its way in here. would you mind removing it? (Is it an automatic editor thing?)

@tacaswell Owner

sorry, on my to-do list this weekend is to get my editor set up to highlight pep8 issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@tacaswell
Owner

This is not directly related to #1678 , it's fixing a bug from #1498

I don't know how to test this stuff in general. As near as I can tell, there is no UI testing at all.

This particular bug should be straight forward to test (make a figure, call close() on it, look at the state of Gcf before and after), but I don't know enough about nose to write that test from scratch (if you can point me at a similar test, I can take a crack at it).

@dmcdougall
Collaborator

@tacaswell I made a pull request against your branch to add a skeleton test for you. See tacaswell/matplotlib#1.

@dmcdougall
Collaborator

@tacaswell Now you need to add the Gcf check in that test file.

@tacaswell
Owner

I think all of the test failures are false-negatives

5201======================================================================
5202FAIL: matplotlib.tests.test_bbox_tight.test_bbox_inches_tight.test
5203----------------------------------------------------------------------
lib/matplotlib/tests/test_backend_qt4.py
@@ -0,0 +1,25 @@
+from matplotlib import rcParams
@dmcdougall Collaborator

Looks like you moved from rcParams to switch_backend, so you can remove this line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dmcdougall
Collaborator

I think all of the test failures are false-negatives

5201======================================================================
5202FAIL: matplotlib.tests.test_bbox_tight.test_bbox_inches_tight.test
5203----------------------------------------------------------------------

I think you are right. The test_bbox_tight test passes locally for me.

@tacaswell
Owner

It is the same failure as this one which you pointed out in this thread:
#1671 (comment)

@dmcdougall
Collaborator

Yep. I no longer get that failure locally, but it doesn't seem to sit well with Travis. You clearly didn't cause the error.

lib/matplotlib/tests/test_backend_qt4.py
@@ -0,0 +1,24 @@
+from matplotlib import pyplot as plt
+from matplotlib.testing.decorators import cleanup
+from matplotlib._pylab_helpers import Gcf
+import copy
+
+
+@cleanup
+def test_fig_close():
+ # force switch to the Qt4 backend
+ plt.switch_backend('Qt4Agg')
@mdboom Owner
mdboom added a note

We should probably mark this test as knownfail or skip if Qt4/PySide are not installed.

@tacaswell Owner

What is the correct way to test if Qt4/PySide is available?

@mdboom Owner
mdboom added a note

Something like:

try:
    import Qt
    HAS_PYQT = True
except ImportError:
    HAS_PYQT = False

(And obviously extend to also check for PySide -- it should be good enough to have one or the other).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@tacaswell tacaswell added knownfailureif to test based on if qt4_compat could be imported,
as this should hit either PySide or PyQt4, depending on which is
installed.  (I have faith that the setup code that will make sure that
if only one of them is installed, it defaults to using that one)
def3327
@tacaswell
Owner

It looks like travis does not have Qt, but yet this test seems to pass. I am deeply confused why this test did not fail on travis before adding the guard.

@dmcdougall
Collaborator

@tacaswell To investigate, you can simulate a missing dependency locally:

sys.modules['Qt'] = None
try:
    import Qt  # will fail
    HAS_PYQT = True
except ImportError:
    HAS_PYQT = False

P.S.: I thought it was PyQt4, not Qt? My knowledge of Qt4 is limited.

@mdboom
Owner

Ah -- I think Travis is using matplotlib.test() to run the tests, which uses the default test categories listed in lib/matplotlib/__init__.py... test_backend_qt4.py is not among them, so it simply doesn't get run. I think the right thing to do there is add it to the list and then put the import check as described here.

And, yes, I misremembered the name of the module. Indeed it is PyQt4 (as I said, I hadn't tested my code... :smile:)

@dmcdougall
Collaborator

And, yes, I misremembered the name of the module. Indeed it is PyQt4 (as I said, I hadn't tested my code... :smile:)

Not tested; merely proven to be correct.

@dmcdougall
Collaborator

@tacaswell That's my fault. I should have added that when I forked your branch. Apologies for the wild goose chase.

@tacaswell
Owner

@dmcdougall no worries. I will deal with this tonight.

@tacaswell
Owner

I also checked that it does indeed fail (gracefully) if you hide both PyQt4 and PySide, but didn't include that in the commits.

@dmcdougall
Collaborator

+1

@tacaswell
Owner

can this get tagged to 1.2.x? I think it should have the same milestone as the commit that introduced the bug.

Sorry if I am mis-understanding the meaning of milestones/the release process (as none of the commits related to this are on the v1.2.x branch).

@dmcdougall
Collaborator

can this get tagged to 1.2.x? I think it should have the same milestone as the commit that introduced the bug.

I agree.

Sorry if I am mis-understanding the meaning of milestones/the release process (as none of the commits related to this are on the v1.2.x branch).

I just noticed this wasn't targeted to the v1.2.x. Would you be able to transfer your commits over to the v1.2.x branch as I think this change is big enough to be worried about git cherry-picking after the fact.

@mdboom Was #1498 cherry-picked to 1.2?

@tacaswell tacaswell referenced this pull request from a commit in tacaswell/matplotlib
@tacaswell tacaswell test for issue #1683 75a92df
@mdboom
Owner

Yes: #1498 was cherry-picked to v1.2.x. This should be as well.

@mdboom
Owner

Superceded by #1705. Closing.

@mdboom mdboom closed this
@tacaswell tacaswell deleted the tacaswell:qt4_closeevent_Gcf branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 18, 2013
  1. Re-wired signal/slot connections so that the figure in removed from

    Thomas A Caswell authored
    Gcf when it is closed.
    
    In PR #1498 the attribute WA_DeleteOnClose was no longer set on the
    QtMainWindow object in QtFigureManager.  The signal connection that
    was being used to remove the figure from Gcf when the window was closed
    was tied to the `destroyed()` signal of QtMainWindow, which is no
    longer being destroyed.  Thus, gca and gcf would return references
    to no-longer visible figures/axes.  _widgetclosed is now called when
    MainWindow emits 'closing()'.
  2. PEP8 removed trailing whitespace

    Thomas A Caswell authored
  3. more white space corrections

    Thomas A Caswell authored
  4. @dmcdougall
  5. @tacaswell

    Merge pull request #1 from dmcdougall/qt4_closeevent_Gcf

    tacaswell authored
    Add figure close test for qt4 backend
Commits on Jan 19, 2013
  1. @tacaswell
  2. @tacaswell

    test for issue #1683

    tacaswell authored
  3. @tacaswell

    fixed link rot

    tacaswell authored
  4. @tacaswell

    removed unused import

    tacaswell authored
Commits on Jan 23, 2013
  1. @tacaswell

    added knownfailureif to test based on if qt4_compat could be imported,

    tacaswell authored
    as this should hit either PySide or PyQt4, depending on which is
    installed.  (I have faith that the setup code that will make sure that
    if only one of them is installed, it defaults to using that one)
Commits on Jan 25, 2013
  1. added test_backend_qt4 to test list

    Thomas A Caswell authored
This page is out of date. Refresh to see the latest.
View
1  lib/matplotlib/__init__.py
@@ -1128,6 +1128,7 @@ def tk_window_focus():
'matplotlib.tests.test_triangulation',
'matplotlib.tests.test_transforms',
'matplotlib.tests.test_arrow_patches',
+ 'matplotlib.tests.test_backend_qt4',
]
View
57 lib/matplotlib/backends/backend_qt4.py
@@ -367,12 +367,14 @@ def idle_draw(*args):
self._idle = True
if d: QtCore.QTimer.singleShot(0, idle_draw)
+
class MainWindow(QtGui.QMainWindow):
def closeEvent(self, event):
self.emit(QtCore.SIGNAL('closing()'))
QtGui.QMainWindow.closeEvent(self, event)
-class FigureManagerQT( FigureManagerBase ):
+
+class FigureManagerQT(FigureManagerBase):
"""
Public attributes
@@ -382,29 +384,31 @@ class FigureManagerQT( FigureManagerBase ):
window : The qt.QMainWindow
"""
- def __init__( self, canvas, num ):
- if DEBUG: print('FigureManagerQT.%s' % fn_name())
- FigureManagerBase.__init__( self, canvas, num )
+ def __init__(self, canvas, num):
+ if DEBUG:
+ print('FigureManagerQT.%s' % fn_name())
+ FigureManagerBase.__init__(self, canvas, num)
self.canvas = canvas
self.window = MainWindow()
self.window.connect(self.window, QtCore.SIGNAL('closing()'),
- canvas.close_event)
+ canvas.close_event)
+ self.window.connect(self.window, QtCore.SIGNAL('closing()'),
+ self._widgetclosed)
self.window.setWindowTitle("Figure %d" % num)
- image = os.path.join( matplotlib.rcParams['datapath'],'images','matplotlib.png' )
- self.window.setWindowIcon(QtGui.QIcon( image ))
+ image = os.path.join(matplotlib.rcParams['datapath'], 'images', 'matplotlib.png')
+ self.window.setWindowIcon(QtGui.QIcon(image))
# Give the keyboard focus to the figure instead of the
# manager; StrongFocus accepts both tab and click to focus and
# will enable the canvas to process event w/o clicking.
# ClickFocus only takes the focus is the window has been
# clicked
- # on. http://developer.qt.nokia.com/doc/qt-4.8/qt.html#FocusPolicy-enum
- self.canvas.setFocusPolicy( QtCore.Qt.StrongFocus )
+ # on. http://qt-project.org/doc/qt-4.8/qt.html#FocusPolicy-enum or
+ # http://doc.qt.digia.com/qt/qt.html#FocusPolicy-enum
+ self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus)
self.canvas.setFocus()
- QtCore.QObject.connect( self.window, QtCore.SIGNAL( 'destroyed()' ),
- self._widgetclosed )
self.window._destroying = False
self.toolbar = self._get_toolbar(self.canvas, self.window)
@@ -420,7 +424,7 @@ def __init__( self, canvas, num ):
# requested size:
cs = canvas.sizeHint()
sbs = self.window.statusBar().sizeHint()
- self._status_and_tool_height = tbs_height+sbs.height()
+ self._status_and_tool_height = tbs_height + sbs.height()
height = cs.height() + self._status_and_tool_height
self.window.resize(cs.width(), height)
@@ -429,14 +433,14 @@ def __init__( self, canvas, num ):
if matplotlib.is_interactive():
self.window.show()
- def notify_axes_change( fig ):
+ def notify_axes_change(fig):
# This will be called whenever the current axes is changed
if self.toolbar is not None:
self.toolbar.update()
- self.canvas.figure.add_axobserver( notify_axes_change )
+ self.canvas.figure.add_axobserver(notify_axes_change)
@QtCore.Slot()
- def _show_message(self,s):
+ def _show_message(self, s):
# Fixes a PySide segfault.
self.window.statusBar().showMessage(s)
@@ -446,8 +450,9 @@ def full_screen_toggle(self):
else:
self.window.showFullScreen()
- def _widgetclosed( self ):
- if self.window._destroying: return
+ def _widgetclosed(self):
+ if self.window._destroying:
+ return
self.window._destroying = True
try:
Gcf.destroy(self.num)
@@ -475,15 +480,19 @@ def resize(self, width, height):
def show(self):
self.window.show()
- def destroy( self, *args ):
+ def destroy(self, *args):
# check for qApp first, as PySide deletes it in its atexit handler
- if QtGui.QApplication.instance() is None: return
- if self.window._destroying: return
+ if QtGui.QApplication.instance() is None:
+ return
+ if self.window._destroying:
+ return
self.window._destroying = True
- QtCore.QObject.disconnect( self.window, QtCore.SIGNAL( 'destroyed()' ),
- self._widgetclosed )
- if self.toolbar: self.toolbar.destroy()
- if DEBUG: print("destroy figure manager")
+ QtCore.QObject.disconnect(self.window, QtCore.SIGNAL('destroyed()'),
+ self._widgetclosed)
+ if self.toolbar:
+ self.toolbar.destroy()
+ if DEBUG:
+ print("destroy figure manager")
self.window.close()
def get_window_title(self):
View
32 lib/matplotlib/tests/test_backend_qt4.py
@@ -0,0 +1,32 @@
+from matplotlib import pyplot as plt
+from matplotlib.testing.decorators import cleanup
+from matplotlib.testing.decorators import knownfailureif
+from matplotlib._pylab_helpers import Gcf
+import copy
+
+try:
+ import matplotlib.backends.qt4_compat
+ HAS_QT = True
+except ImportError:
+ HAS_QT = False
+
+
+@cleanup
+@knownfailureif(not HAS_QT)
+def test_fig_close():
+ # force switch to the Qt4 backend
+ plt.switch_backend('Qt4Agg')
+
+ #save the state of Gcf.figs
+ init_figs = copy.copy(Gcf.figs)
+
+ # make a figure using pyplot interface
+ fig = plt.figure()
+
+ # simulate user clicking the close button by reaching in
+ # and calling close on the underlying Qt object
+ fig.canvas.manager.window.close()
+
+ # assert that we have removed the reference to the FigureManager
+ # that got added by plt.figure()
+ assert(init_figs == Gcf.figs)
Something went wrong with that request. Please try again.