Repository: streamlit/streamlit

Issue:
matplotlib.pyplot crash on OSX
# Summary
Using Streamlit on OSX without matplotlib installed:
```python
import streamlit as st

st.pyplot()
```

# Steps to reproduce
1. Run the previous code with streamlit

## Expected behavior:
Python should not crash

## Actual behavior:
Streamlit and Python is crashing

## Is this a regression?
no

# Debug info
- Streamlit version: 0.46.0
- Python version: 3.7.4
- PipEnv
- OS version: macOS Mojave 10.14.6
- Browser version: Chrome 76.0.3809.132

# Additional information
```bash
(lib) bash-3.2$ streamlit run examples/example.py 

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:3000
  Network URL: http://192.168.0.6:3000

2019-09-23 19:04:48.506 python[12881:14857510] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff4acde2fd __exceptionPreprocess + 256
	1   libobjc.A.dylib                     0x00007fff753b2a17 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff4acf7e59 -[NSException raise] + 9
	3   AppKit                              0x00007fff4829e5ca -[NSWindow(NSWindow_Theme) _postWindowNeedsToResetDragMarginsUnlessPostingDisabled] + 317
	4   AppKit                              0x00007fff4829b9f7 -[NSWindow _initContent:styleMask:backing:defer:contentView:] + 1479
	5   AppKit                              0x00007fff4829b42a -[NSWindow initWithContentRect:styleMask:backing:defer:] + 45
	6   _macosx.cpython-37m-darwin.so       0x0000000117def6b0 -[Window initWithContentRect:styleMask:backing:defer:withManager:] + 80
	7   _macosx.cpython-37m-darwin.so       0x0000000117df2bc7 FigureManager_init + 327
	8   Python                              0x00000001072e000f wrap_init + 12
	9   Python                              0x00000001072a75f9 wrapperdescr_call + 337
	10  Python                              0x00000001072a1918 _PyObject_FastCallKeywords + 358
	11  Python                              0x0000000107336ef0 call_function + 746
	12  Python                              0x000000010732fc70 _PyEval_EvalFrameDefault + 6421
	13  Python                              0x00000001072a1e8c function_code_fastcall + 112
	14  Python                              0x00000001072a2829 _PyObject_Call_Prepend + 150
	15  Python                              0x00000001072dff7d slot_tp_init + 80
	16  Python                              0x00000001072dcc20 type_call + 172
	17  Python                              0x00000001072a1918 _PyObject_FastCallKeywords + 358
	18  Python                              0x0000000107336ef0 call_function + 746
	19  Python                              0x000000010732fc70 _PyEval_EvalFrameDefault + 6421
	20  Python                              0x00000001072a1e8c function_code_fastcall + 112
	21  Python                              0x0000000107336ef7 call_function + 753
	22  Python                              0x000000010732fc70 _PyEval_EvalFrameDefault + 6421
	23  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	24  Python                              0x00000001072a16e5 _PyFunction_FastCallDict + 441
	25  Python                              0x00000001072a2829 _PyObject_Call_Prepend + 150
	26  Python                              0x00000001072a1bbf PyObject_Call + 136
	27  Python                              0x000000010732ff43 _PyEval_EvalFrameDefault + 7144
	28  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	29  Python                              0x00000001072a1a77 _PyFunction_FastCallKeywords + 225
	30  Python                              0x0000000107336ef7 call_function + 753
	31  Python                              0x000000010732fd0b _PyEval_EvalFrameDefault + 6576
	32  Python                              0x00000001072a1e8c function_code_fastcall + 112
	33  Python                              0x0000000107336ef7 call_function + 753
	34  Python                              0x000000010732fd0b _PyEval_EvalFrameDefault + 6576
	35  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	36  Python                              0x00000001072a16e5 _PyFunction_FastCallDict + 441
	37  Python                              0x000000010732ff43 _PyEval_EvalFrameDefault + 7144
	38  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	39  Python                              0x00000001072a16e5 _PyFunction_FastCallDict + 441
	40  Python                              0x000000010732ff43 _PyEval_EvalFrameDefault + 7144
	41  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	42  Python                              0x00000001072a16e5 _PyFunction_FastCallDict + 441
	43  Python                              0x000000010732ff43 _PyEval_EvalFrameDefault + 7144
	44  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	45  Python                              0x00000001072a1a77 _PyFunction_FastCallKeywords + 225
	46  Python                              0x0000000107336ef7 call_function + 753
	47  Python                              0x000000010732fd0b _PyEval_EvalFrameDefault + 6576
	48  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	49  Python                              0x00000001072a1a77 _PyFunction_FastCallKeywords + 225
	50  Python                              0x0000000107336ef7 call_function + 753
	51  Python                              0x000000010732fc70 _PyEval_EvalFrameDefault + 6421
	52  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	53  Python                              0x00000001072a16e5 _PyFunction_FastCallDict + 441
	54  Python                              0x000000010732ff43 _PyEval_EvalFrameDefault + 7144
	55  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	56  Python                              0x00000001072a1a77 _PyFunction_FastCallKeywords + 225
	57  Python                              0x0000000107336ef7 call_function + 753
	58  Python                              0x000000010732fc70 _PyEval_EvalFrameDefault + 6421
	59  Python                              0x00000001073377a6 _PyEval_EvalCodeWithName + 1870
	60  Python                              0x000000010732e2b8 PyEval_EvalCode + 51
	61  Python                              0x000000010732bd1e builtin_exec + 554
	62  Python                              0x00000001072a252f _PyMethodDef_RawFastCallKeywords + 495
	63  Python                              0x00000001072a1ab2 _PyCFunction_FastCallKeywords + 44
	64  Python                              0x0000000107336e82 call_function + 636
	65  Python                              0x000000010732fd0b _PyEval_EvalFrameDefault + 6576
	66  Python                              0x00000001072a1e8c function_code_fastcall + 112
	67  Python                              0x0000000107336ef7 call_function + 753
	68  Python                              0x000000010732fc57 _PyEval_EvalFrameDefault + 6396
	69  Python                              0x00000001072a1e8c function_code_fastcall + 112
	70  Python                              0x00000001072a2829 _PyObject_Call_Prepend + 150
	71  Python                              0x00000001072a1bbf PyObject_Call + 136
	72  Python                              0x000000010732ff43 _PyEval_EvalFrameDefault + 7144
	73  Python                              0x00000001072a1e8c function_code_fastcall + 112
	74  Python                              0x0000000107336ef7 call_function + 753
	75  Python                              0x000000010732fc57 _PyEval_EvalFrameDefault + 6396
	76  Python                              0x00000001072a1e8c function_code_fastcall + 112
	77  Python                              0x0000000107336ef7 call_function + 753
	78  Python                              0x000000010732fc57 _PyEval_EvalFrameDefault + 6396
	79  Python                              0x00000001072a1e8c function_code_fastcall + 112
	80  Python                              0x00000001072a2829 _PyObject_Call_Prepend + 150
	81  Python                              0x00000001072a1bbf PyObject_Call + 136
	82  Python                              0x000000010739e261 t_bootstrap + 71
	83  Python                              0x0000000107364e95 pythread_wrapper + 25
	84  libsystem_pthread.dylib             0x00007fff76d742eb _pthread_body + 126
	85  libsystem_pthread.dylib             0x00007fff76d77249 _pthread_start + 66
	86  libsystem_pthread.dylib             0x00007fff76d7340d thread_start + 13
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Abort trap: 6
```



Discussion:

Resolving patch:
diff --git a/lib/streamlit/bootstrap.py b/lib/streamlit/bootstrap.py
--- a/lib/streamlit/bootstrap.py
+++ b/lib/streamlit/bootstrap.py
@@ -44,10 +44,10 @@ def signal_handler(signal_number, stack_frame):
 
     signal.signal(signal.SIGTERM, signal_handler)
     signal.signal(signal.SIGINT, signal_handler)
-    if sys.platform == 'win32':
-      signal.signal(signal.SIGBREAK, signal_handler)
+    if sys.platform == "win32":
+        signal.signal(signal.SIGBREAK, signal_handler)
     else:
-      signal.signal(signal.SIGQUIT, signal_handler)
+        signal.signal(signal.SIGQUIT, signal_handler)
 
 
 def _fix_sys_path(script_path):
@@ -62,15 +62,29 @@ def _fix_sys_path(script_path):
 def _fix_matplotlib_crash():
     """Set Matplotlib backend to avoid a crash.
 
-    The default Matplotlib backend crashes Python for most MacOS users.
-    So here we set a safer backend as a fix. Users can always disable this
-    behavior by setting the config runner.fixMatplotlib = false.
+    The default Matplotlib backend crashes Python on OSX when run on a thread
+    that's not the main thread, so here we set a safer backend as a fix.
+    Users can always disable this behavior by setting the config
+    runner.fixMatplotlib = false.
 
     This fix is OS-independent. We didn't see a good reason to make this
     Mac-only. Consistency within Streamlit seemed more important.
     """
-    if config.get_option("runner.fixMatplotlib"):
-        os.environ["MPLBACKEND"] = "Agg"
+    if sys.platform == "darwin" and config.get_option("runner.fixMatplotlib"):
+        try:
+            # TODO: a better option may be to set
+            #  os.environ["MPLBACKEND"] = "Agg". We'd need to do this towards
+            #  the top of __init__.py, before importing anything that imports
+            #  pandas (which imports matplotlib). Alternately, we could set
+            #  this environment variable in a new entrypoint defined in
+            #  setup.py. Both of these introduce additional trickiness: they
+            #  need to run without consulting streamlit.config.get_option,
+            #  because this would import streamlit, and therefore matplotlib.
+            import matplotlib
+
+            matplotlib.use("Agg")
+        except ImportError:
+            pass
 
 
 def _on_server_start(server):


Test patch:
diff --git a/lib/tests/streamlit/bootstrap_test.py b/lib/tests/streamlit/bootstrap_test.py
--- a/lib/tests/streamlit/bootstrap_test.py
+++ b/lib/tests/streamlit/bootstrap_test.py
@@ -15,6 +15,8 @@
 
 import sys
 import unittest
+
+import matplotlib
 from mock import patch
 
 try:
@@ -32,6 +34,38 @@
 report = Report("the/path", ["arg0", "arg1"])
 
 
+class BootstrapTest(unittest.TestCase):
+    @patch("streamlit.bootstrap.tornado.ioloop")
+    @patch("streamlit.bootstrap.Server")
+    def test_fix_matplotlib_crash(self, _1, _2):
+        """Test that bootstrap.run sets the matplotlib backend to
+        "Agg" if config.runner.fixMatplotlib=True.
+        """
+        # TODO: Find a proper way to mock sys.platform
+        ORIG_PLATFORM = sys.platform
+
+        for platform, do_fix in [("darwin", True), ("linux2", False)]:
+            sys.platform = platform
+
+            matplotlib.use("tkagg")
+
+            config._set_option("runner.fixMatplotlib", True, "test")
+            bootstrap.run("/not/a/script")
+            if do_fix:
+                self.assertEqual("agg", matplotlib.get_backend().lower())
+            else:
+                self.assertEqual("tkagg", matplotlib.get_backend().lower())
+
+            # Reset
+            matplotlib.use("tkagg")
+
+            config._set_option("runner.fixMatplotlib", False, "test")
+            bootstrap.run("/not/a/script")
+            self.assertEqual("tkagg", matplotlib.get_backend().lower())
+
+        sys.platform = ORIG_PLATFORM
+
+
 class BootstrapPrintTest(unittest.TestCase):
     """Test bootstrap.py's printing functions."""
 
diff --git a/scripts/run_bare_integration_tests.py b/scripts/run_bare_integration_tests.py
--- a/scripts/run_bare_integration_tests.py
+++ b/scripts/run_bare_integration_tests.py
@@ -35,6 +35,11 @@
 
 EXCLUDED_FILENAMES = set()
 
+# Since there is not DISPLAY set (and since Streamlit is not actually running
+# and fixing Matplotlib in these tests), we set the MPL backend to something
+# that doesn't require a display.
+os.environ['MPLBACKEND'] = 'Agg'
+
 # Scripts that rely on matplotlib can't be run in Python2. matplotlib
 # dropped Py2 support, and so we don't install it in our virtualenv.
 try: