Repository: matplotlib/matplotlib
Pull request: FIX: do not consider webagg and nbagg "interactive" for fallback
## PR Summary

There is logic in pyplot to determine if the user has is some way
selected a backend which is incompatible with the interactive
framework that we detect is running.  This helps prevent issues where
the user is already using one GUI frame work (ex tk) before Matplotlib
is imported and has Matplotlib configured to use Qt.  The other place
this behavior is desired is if Matplotlib is configured to use a GUI
framework by default, but is then imported on a headless server.  By
detecting there is no DISPLAY we fall back to Agg rather than failing
to import.

From the point of view of having UI events, webagg and nbagg are
interactive backends, however from the point of view of requiring
DISPLAY they are not.

This is the minimal fix, filtering out these two backends in pyplot
rather than changing this in rcsetup, incase anyone is relying on
those lists for other purposes.

closes #14903

This will backport badly due to the change in line light below this one but :woman_shrugging: 

Will work on a test tomorrow.

Problem statement:
'MPLBACKEND=webagg' is overwritten by agg when $DISPLAY is not set on Linux
## Bug report

* The environment variable `MPLBACKEND=webagg` is overwritten by 'agg' when $DISPLAY is not set on Linux.
* The behavior of  `backend:webagg` written in matplotlibrc was as same as `MPLBACKEND=webagg` (I tried).
* I want to avoid hard-coding the backend to .py file, because I use the same code on other environment.


**Code for reproduction**

```
# matplotlibrc
webagg.open_in_browser : False
webagg.address : 0.0.0.0
```

```python
# test.py
import matplotlib.pyplot as plt
print(plt.get_backend())
```

**Actual outcome**

When I type the shell command `MPLBACKEND=WebAgg DISPLAY= python test.py`,

```
agg
```

the backend is overwritten by agg.

When I type the shell command `MPLBACKEND=WebAgg DISPLAY=:0 python test.py` (although X11 is not installed),

```
WebAgg
```

I can use webagg.

**Expected outcome**

When I type the shell command `MPLBACKEND=WebAgg DISPLAY= python test.py`,

```
WebAgg
```

* I want to use webagg even when X11 is not installed.
* I want to use the environment variable MPLBACKEND to avoid hard-coding the backend.

**Question**

Does matplotlib overwrite the backend by 'agg', when `$DISPLAY` is empty?
I understand that the fallback behavior is useful.
However, is there any reason that webagg must be fallbacked to agg?

Or, is there a correct way (not setting the `$DISPLAY`) to avoid the fallback behavior?

**Matplotlib version**
  * Operating system: Debian 10
  * Matplotlib version:  3.1.0
  * Python version: 3.7.3 (anaconda)

Hints from issue discussion:
That is definitely a bug in the fallback logic to make sure we don't try to use a GUI backend on a headless server.  I suspect we have miss-categorized `webagg` as a GUI backend.
Thanks for your reply!
Also, if `webagg.open_in_browser` is automatically set to `False` when `$DISPLAY` is not set,
I think it is very convenient.
The short term fix is to add webagg to https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/rcsetup.py#L44.  The long term fix is... we need to figure out what rcParams["backend_fallback"] really means.  I have a PR that fixes that, but runs into other inconsistencies :(
Is the fallback behavior really needed even when users explicitly set the backend by MPLBACKEND or matplotlibrc?
this was requested many times when the fallback was not present (you can search the issue tracker), so heh.

Commit message: TST: skip the nbagg test if we don't have IPython installed

Patch to review:
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 378dfef082b6..3a08cec7ad1c 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -2244,7 +2244,8 @@ def getname_val(identifier):
 # requested, ignore rcParams['backend'] and force selection of a backend that
 # is compatible with the current running interactive framework.
 if (rcParams["backend_fallback"]
-        and dict.__getitem__(rcParams, "backend") in _interactive_bk
+        and dict.__getitem__(rcParams, "backend") in (
+            set(_interactive_bk) - {'WebAgg', 'nbAgg'})
         and cbook._get_running_interactive_framework()):
     dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
 # Set up the backend.
diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py
new file mode 100644
index 000000000000..b5d8528fc2fc
--- /dev/null
+++ b/lib/matplotlib/tests/test_backend_webagg.py
@@ -0,0 +1,22 @@
+import subprocess
+import sys
+import pytest
+
+
+@pytest.mark.parametrize("backend", ["webagg", "nbagg"])
+def test_webagg_fallback(backend):
+    if backend == "nbagg":
+        pytest.importorskip("IPython")
+    test_code = (
+        "import os;"
+        + f"os.environ['MPLBACKEND'] = '{backend}';"
+        + "import matplotlib.pyplot as plt; "
+        + "print(plt.get_backend());"
+        f"assert '{backend}' == plt.get_backend().lower();"
+    )
+    ret = subprocess.call(
+        [sys.executable, "-c", test_code],
+        env={"MPLBACKEND": backend, "DISPLAY": ""}
+    )
+
+    assert ret == 0


Code review comments:
diff_hunk: @@ -0,0 +1,22 @@
+import subprocess
+import sys
+import pytest
+
+
+@pytest.mark.parametrize("backend", ["webagg", "nbagg"])
+def test_webagg_fallback(backend):
+    if backend == "nbagg":
+        pytest.importorskip("IPython")
+    test_code = (
+        "import os;"
+        + f"os.environ['MPLBACKEND'] = '{backend}';"
+        + "import matplotlib.pyplot as plt; "
+        + "print(plt.get_backend());"
+        f"assert '{backend}' == plt.get_backend().lower();"
+    )
+    ret = subprocess.call(
+        [sys.executable, "-c", test_code],
+        env={"MPLBACKEND": backend, "DISPLAY": ""}
path: lib/matplotlib/tests/test_backend_webagg.py
text: @user1:
this line, even just `env={"MPLBACKEND": backend}` or `env={"DISPLAY": ""}` triggers the windows failures

from the Python docs:
>Note
>If specified, env must provide any variables required for the program to execute. On Windows, in order to run a side-by-side assembly the specified env must include a valid SystemRoot.