Repository: ipython/ipython
Pull request: Fix/mpl integration
- Closes https://github.com/matplotlib/matplotlib/issues/17454
- fixes qt5/qt6 confusion

Problem statement:
%matplotlib inline called from IPython turns interactive mode off, forever and only for scripts
### Bug report

**Bug summary**

After calling `%matplotlib inline` from IPython shell, matplotlib interactive mode is turned off for  commands inside scripts and cannot be turned on again, irrespectively from the interactive status in the console.

**Code for reproduction**

```python
# <this is file test_plot3.py>
from matplotlib import pyplot as plt
from matplotlib import get_backend
print('interactive: ',plt.isinteractive())
print('backend: ',get_backend())
plt.plot([1,2,3],[2,3,4])

```

**Actual outcome**

```
<Running the shell commands from ipython console:>
In [1]: %run test_plot3.py
interactive:  False
backend:  Qt5Agg

In [2]: plt.ion()

In [3]: %matplotlib inline

In [4]: %matplotlib
Using matplotlib backend: Qt5Agg

In [5]: plt.ion()

In [6]: plt.isinteractive()
Out[6]: True

In [7]: %run test_plot3.py
interactive:  False
backend:  Qt5Agg
```


**Matplotlib version**
<!--Please specify your platform and versions of the relevant libraries you are using:-->
  * Operating system: Windows 10 Home 64 bit
  * Matplotlib version: 
In [9]: matplotlib.__version__
Out[9]: '3.2.1'
  * Python version:
Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.13.0 -- An enhanced Interactive Python. Type '?' for help.

<!--Please tell us how you installed matplotlib and python e.g., from source, pip, conda-->
<!--If you installed from conda, please specify which channel you used if not the default-->

conda with default

Hints from issue discussion:
attn @Carreau as this may be an IPython issue (as that is where the both the `%matplotlib` and `%run` magics are defined).
Thanks. 
If it can be interesting, I am finding some other similar odd behavior.

For example this script contains three version of same functions, two of which fail on second execution if the script is launched from IPython console with `%run`. All functions work if they are imported and called explicitly from console. 

**Code for reproduction**

```
<this is file demo.py>
import matplotlib
import matplotlib.pyplot as plt
from IPython import get_ipython

#all three work if run called from console, but 2 and 3 fail on second run if called from __main__

def demo_display():

    '''works:
    %matplotlib
    matplotlib.use()
    '''

    #save current status
    be = matplotlib.get_backend()    

    #wrap and execute plot
    get_ipython().magic('matplotlib') 
    plt.plot([1,2,3],[1,-1,1])

    #restore status
    matplotlib.use(be) #this works with `matplotlib`, but not with 'matplotlib inline'

def demo_display2():
    '''Fails on second run:
        %matplotlib
        plt.switch_backend()
        '''
    
    #save current status
    be = matplotlib.get_backend()    

    #wrap and execute plot
    get_ipython().magic('matplotlib') 
    plt.plot([1,2,3],[1,-1,1])

    #restore status
    plt.switch_backend(be)  #this fails on second execution

def demo_display3():
    '''temporarily modify  a test function with plotting commmands'''
    '''Fails on second run:
        %matplotlib inline
        plt.switch_backend()
        '''
        
    #save current status
    be = matplotlib.get_backend()    

    #wrap and execute plot
    get_ipython().magic('matplotlib inline') #this works well in notebook, not in ipython shell
    plt.plot([1,2,3],[1,-1,1])

    #restore status
    matplotlib.use(be) #this works with `matplotlib`, but not with 'matplotlib inline'
        
if __name__ == "__main__":

    demo_display()   #runs from console and from __main__
    #demo_display2()   #runs from __main__. runs only once from console
    #demo_display3()   #runs from __main__. runs only once from console

```

**Actual outcome**
Importing and running any of the functions in the scripts, they are executed.
If called from __main__, functions 2 and 3 are executed a first time, but fails on second execution. Error message is:

`---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-2-c34c2ff19293> in <module>
----> 1 get_ipython().run_line_magic('run', 'demo_display')

~\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py in run_line_magic(self, magic_name, line, _stack_depth)
   2315                 kwargs['local_ns'] = sys._getframe(stack_depth).f_locals
   2316             with self.builtin_trap:
-> 2317                 result = fn(*args, **kwargs)
   2318             return result
   2319

<decorator-gen-60> in run(self, parameter_s, runner, file_finder)

~\Anaconda3\lib\site-packages\IPython\core\magic.py in <lambda>(f, *a, **k)
    185     # but it's overkill for just that one bit of state.
    186     def magic_deco(arg):
--> 187         call = lambda f, *a, **k: f(*a, **k)
    188
    189         if callable(arg):

~\Anaconda3\lib\site-packages\IPython\core\magics\execution.py in run(self, parameter_s, runner, file_finder)
    825                     else:
    826                         # regular execution
--> 827                         run()
    828
    829             if 'i' in opts:

~\Anaconda3\lib\site-packages\IPython\core\magics\execution.py in run()
    811                         def run():
    812                             runner(filename, prog_ns, prog_ns,
--> 813                                     exit_ignore=exit_ignore)
    814
    815                     if 't' in opts:

~\Anaconda3\lib\site-packages\IPython\core\pylabtools.py in mpl_execfile(fname, *where, **kw)
    182         matplotlib.interactive(is_interactive)
    183         # make rendering call now, if the user tried to do it
--> 184         if plt.draw_if_interactive.called:
    185             plt.draw()
    186             plt.draw_if_interactive.called = False

AttributeError: 'function' object has no attribute 'called'`

**Matplotlib version**
<!--Please specify your platform and versions of the relevant libraries you are using:-->
  * Operating system: Windows 10 Home 64 bit
  * Matplotlib version: 
In [9]: matplotlib.__version__
Out[9]: '3.2.1'
  * Python version:
Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.13.0 -- An enhanced Interactive Python. Type '?' for help.



This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear.  Thanks for your help!
I have a memory of doing work on this recently, but can not find where.

Commit message: STY: more style fixes

Patch to review:
diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py
index deadf038ea3..cfe83123d66 100644
--- a/IPython/core/pylabtools.py
+++ b/IPython/core/pylabtools.py
@@ -23,7 +23,7 @@
     "qt4": "Qt4Agg",
     "qt5": "Qt5Agg",
     "qt6": "QtAgg",
-    "qt": "Qt5Agg",
+    "qt": "QtAgg",
     "osx": "MacOSX",
     "nbagg": "nbAgg",
     "webagg": "WebAgg",
@@ -53,8 +53,8 @@
 # supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
 # and Qt6.
 backend2gui["QtAgg"] = "qt"
-backend2gui["Qt4Agg"] = "qt"
-backend2gui["Qt5Agg"] = "qt"
+backend2gui["Qt4Agg"] = "qt4"
+backend2gui["Qt5Agg"] = "qt5"
 
 # And some backends that don't need GUI integration
 del backend2gui["nbAgg"]
@@ -208,10 +208,12 @@ def mpl_execfile(fname,*where,**kw):
 
         #print '*** Matplotlib runner ***' # dbg
         # turn off rendering until end of script
-        is_interactive = matplotlib.rcParams['interactive']
-        matplotlib.interactive(False)
-        safe_execfile(fname,*where,**kw)
-        matplotlib.interactive(is_interactive)
+        with matplotlib.rc_context({"interactive": False}):
+            safe_execfile(fname, *where, **kw)
+
+        if matplotlib.is_interactive():
+            plt.show()
+
         # make rendering call now, if the user tried to do it
         if plt.draw_if_interactive.called:
             plt.draw()
@@ -317,9 +319,15 @@ def find_gui_and_backend(gui=None, gui_select=None):
 
     import matplotlib
 
+    has_unified_qt_backend = matplotlib.__version__[:3] >= "3.5"
+
+    backends_ = dict(backends)
+    if not has_unified_qt_backend:
+        backends_["qt"] = "qt5agg"
+
     if gui and gui != 'auto':
         # select backend based on requested gui
-        backend = backends[gui]
+        backend = backends_[gui]
         if gui == 'agg':
             gui = None
     else:
@@ -336,7 +344,7 @@ def find_gui_and_backend(gui=None, gui_select=None):
         # ones allowed.
         if gui_select and gui != gui_select:
             gui = gui_select
-            backend = backends[gui]
+            backend = backends_[gui]
 
     return gui, backend
 
diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py
index dd1a0ff58b5..a06ad487577 100644
--- a/IPython/core/tests/test_pylabtools.py
+++ b/IPython/core/tests/test_pylabtools.py
@@ -173,8 +173,8 @@ def act_mpl(backend):
         # Save rcParams since they get modified
         self._saved_rcParams = matplotlib.rcParams
         self._saved_rcParamsOrig = matplotlib.rcParamsOrig
-        matplotlib.rcParams = dict(backend='Qt4Agg')
-        matplotlib.rcParamsOrig = dict(backend='Qt4Agg')
+        matplotlib.rcParams = dict(backend="QtAgg")
+        matplotlib.rcParamsOrig = dict(backend="QtAgg")
 
         # Mock out functions
         self._save_am = pt.activate_matplotlib


Code review comments:
diff_hunk: @@ -317,9 +319,15 @@ def find_gui_and_backend(gui=None, gui_select=None):
 
     import matplotlib
 
+    has_unified_qt_backend = matplotlib.__version__[:3] >= "3.5"
path: IPython/core/pylabtools.py
text: @user1:
```suggestion
    has_unified_qt_backend = getattr(matplotlib, '__version_info__', (0,0)) >= (3, 5)
```

I think `__version_info__` was added in 3.5 so if it's there... But I prefer to not add a string comparison in case you ever get to matplotlib 3.60.+