Skip to content
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

[Bug]: plt.show(block=True) when a wx.App is already running #23072

Open
Yves33 opened this issue May 19, 2022 · 3 comments
Open

[Bug]: plt.show(block=True) when a wx.App is already running #23072

Yves33 opened this issue May 19, 2022 · 3 comments
Labels

Comments

@Yves33
Copy link

Yves33 commented May 19, 2022

Bug summary

When trying to use pyplot interface from wx.App, pyplot.show() does not block and does not start an event loop

Code for reproduction

#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('WXAgg')
import numpy as np
import wx

class TestFrame(wx.Frame):
    def __init__(self, parent, title):
        super(TestFrame, self).__init__(parent, title=title,size=(350, 250))
        self.sz=wx.BoxSizer()
        self.button=wx.Button(self,-1,"click")
        self.button.Bind(wx.EVT_BUTTON, self.onClick)   
        self.sz.Add(self.button,wx.EXPAND)
        self.SetSizer(self.sz)
        
    def onClick(self,evt):
        plt.plot(np.arange(0,6.28,0.1),np.sin(np.arange(0,6.28,0.1)))
        plt.show(block=True)
        ## as wx.App mainloop is running, matplotlib won't create an event loop. 
        ## we therefore need to manually create one!
        #plt.gcf().canvas.start_event_loop() #!magick line
        print("This shouldn't get printed before the closing the pyplot window!")

def main():
    app = wx.App()
    tf = TestFrame(None, title='Test app')
    tf.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()

Actual outcome

With the above code, the line

print("This shouldn't get printed before the closing the pyplot window!")

gets executed even though the pyplot window is not closed.

Expected outcome

The line should not be executed before the pyplot window is closed

Additional information

In backend_wx.py, mainloop() is testing for wx.App.IsMainloopRunning(), which is True, and does not go any further.
Not a fix, but calling

plt.gcf().canvas.start_event_loop()

immediately after plt.show()
restores the blocking behaviour. Wondering if it is reliable or if this trick may break in a near future?

Operating system

Windows10

Matplotlib Version

3.4.3

Matplotlib Backend

WX; WXAgg

Python version

3.8.8

Jupyter version

not applicable

Installation

conda

@tacaswell
Copy link
Member

I will defer to wx experts on if this is a good idea or not technically, I have grave concerns about this conceptually.

pyplot has roughly two roles:

  • provide the MATLAB like global state implicit plotting
  • stand in for an application because once you show a GUI window you are a GUI application

With plt.show(block=False) we assume that someone somewhere else is running the event loop (typically the the command prompt) so the figures are live when the user is not typing or actively running code.

With plt.show(block=True) we are saying "no one else is running the GUI event loop so it might as well be us! We will stop when we run out of open windows no matter how long it takes!".

There is also plt.pause(N) which says "I'll show the windows and then wait at most N seconds before stopping the event loop and carrying on.

Taken together I think using plt.show(block=True) in a callback in a UI framework violates two conceptual aspects. First, plt.show(block=True) is assuming it is in charge of the event loop. the behavior of "oh, someone else has this, carry on" is more forgiving than "I'm inside an event loop I'M DOING TO RAISE". The second is that typically the body of a callback in a UI framework is expected to be fast (because it can not get to the next UI event until it finishes the last one and you do not want to lock up the UI).

See https://matplotlib.org/stable/users/explain/interactive_guide.html for more writing on this.

It is the case that Wx (and Qt) gives you a way to run a re-entrant event loop (so is the middle of processing a callback take a break and process other callbacks for a bit), I am not sure if this is general across all of the toolkits we support.

All of this taken together, I am not an favor of changing this behavior. Given the example, I assume this is part of a much bigger UI application I would strongly suggest you go down path of using the explicit API + full embedding, rather than pyplot, and manage your windows yourself.

I do not know what the other backends do in the analogous case. If they all do block in in the callback, then despite my concerns the behavior should match.

@Yves33
Copy link
Author

Yves33 commented May 20, 2022

Thanks !
Using plt.pause(-1) or plt.ginput(n=-1,timeout=-1,show_clicks=False)
instead of plt.show() works fine (at least in wxagg backend).


FYI, my typical use case is detailed below:
I have a script that does some interactive processing

def process_interactive(file):
	(put vlines and hlines that act as draggable cursors with appropriate callbacks)
	plt.plot(x,y)
	plt.show()
	return someresults

if __name__=="__main__":
	for f in sys.argv[1:]
		print(process_interactive(f))

However, because most users are not comfortable with command line,
I wrote a simple wx.gui with drag/drop zone and excel like result sheet.
I therefore have the following callback

OnDropFiles(self, x, y, filenames):
	for f in filenames:
		res=self.process_interactive(evt.files) ## this needs to block any other event processing by the app
	self.excel_like_control.putresults(res)

(the design may be bad and it would maybe be better to execute the first script through subprocess)

@tacaswell
Copy link
Member

I would suggest the small modification to be something like:

def process_interactive(file, *, blocking_function=None):
     if blocking_function is None:
          blocking_function = lambda : plt.show(block=True)
     ...
     # your code
    ...
    blocking_function()
    return stuff

so that way when you wrap it for the UI you can parameterize how to block to the context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants