Repository: bokeh/bokeh

Issue:
'export_png' from bokeh.io triggers 'deprecated' warning from Selenium using PhantomJS

#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)

'vanilla' Python installation.
No Anaconda, no virtualenv.
Packages installed with pip (9.0.1)

Python (3.5.0)

bokeh (0.12.13)

DateTime (4.2)
Jinja2  (2.10)
Pillow  (5.0.0)
PyYAML (3.12)
requests (2.18.4)
selenium (3.8.1)
six (1.11.0)
tornado (4.5.3)

PhantomJS (2.1.1) - only version that will work on my platform.

PuppyLinux 5.6.1 (32-bit, Ubuntu "Precise", kernel 3.2.44)

#### Description of expected behavior and the observed behavior

Expected:

Installation guide for bokeh should accurately reflect current versions of
dependencies.

Simple code should run without errors or warnings.


Observed:

Selenium prints warning that PhantomJS is deprecated when using 'export_png'.
Output file appears to be generated OK however.


#### Complete, minimal, self-contained example code that reproduces the issue

```
from bokeh.plotting import figure
from bokeh.io import export_png

p = figure(plot_width=400, plot_height=400)
p.vbar(x=[1, 2, 3], width=0.5, bottom=0,
       top=[1.2, 2.5, 3.7], color="firebrick")
       
export_png(p, filename="plot.png")

```


#### Stack traceback and/or browser JavaScript console output

/usr/local/lib/python3.5/site-packages/selenium/webdriver/phantomjs/webdriver.py:49:
UserWarning: Selenium support for PhantomJS has been deprecated, please use headless versions of Chrome or Firefox instead
  warnings.warn('Selenium support for PhantomJS has been deprecated, please use headless 


#### Screenshots or screencasts of the bug in action

None


Discussion:
It looks like we will have to move to headless chrome sooner than later.
ping @hsparra just an FYI, the project is going to have to migrate to Chrome headless as a default, though that really needs to wait for conda packages. Clients who need to stay on PhantomJS for reasons may have to manage their selenium version explicitly to avoid these warnings (or in the future, to have it work at all)
> (...) though that really needs to wait for conda packages.

Perhaps we should be more vocal for having those packages made. This is really the only obstacle from using headless chrome by default.
I have requested that Chrome Headless be added to Anaconda `defaults`: 

https://github.com/ContinuumIO/anaconda/issues/1279
As I don't use anaconda, is it feasible for me (as a very naive user) to change over to headless  chrome (or ff) now?

@RedactedCode This issue is just about making CH the default webdriver, which we would not want to do until CH is easily installable via conda. If you have installed it some other way, you can pass an explicit `webdriver` argument to the export functions. 
got a really nasty error when I tried to use export_png today. Would've been a lot nicer if it had told me to install phantomjs (it isn't included in selenium anymore). Instead I got this:
```
Traceback (most recent call last):
  File "generate_images.py", line 19, in <module>
    export_png(p)#, filename="./plot.png")
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/site-packages/bokeh/util/api.py", line 190, in wrapper
    return obj(*args, **kw)
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/site-packages/bokeh/io/export.py", line 83, in export_png
    image = get_screenshot_as_png(obj, height=height, width=width, driver=webdriver)
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/site-packages/bokeh/util/api.py", line 190, in wrapper
    return obj(*args, **kw)
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/site-packages/bokeh/io/export.py", line 165, in get_screenshot_as_png
    web_driver = driver if driver is not None else _create_default_webdriver()
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/site-packages/bokeh/io/export.py", line 303, in _create_default_webdriver
    phantomjs_path = detect_phantomjs()
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/site-packages/bokeh/util/dependencies.py", line 78, in detect_phantomjs
    proc = Popen([phantomjs_path, "--version"], stdout=PIPE, stderr=PIPE)
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/subprocess.py", line 709, in __init__
    restore_signals, start_new_session)
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/subprocess.py", line 1258, in _execute_child
    executable = os.fsencode(executable)
  File "/anaconda3/envs/graphing-web-project/lib/python3.6/os.py", line 800, in fsencode
    filename = fspath(filename)  # Does type-checking of `filename`.
TypeError: expected str, bytes or os.PathLike object, not NoneType
```
telling the user to install phantomjs is a bandaid until bokeh switches over to chrome headless, but it's better than this error right now
> it isn't included in selenium anymore

@grifball We don't have any control when things change out from under, and only have much bandwidth to react. As this is a pure python side concern, a PR to make a better error message would be a great task for a new contributor, and certainly appreciated. 
Sure! I'll try and do that after work
Great!

Resolving patch:
diff --git a/bokeh/command/subcommands/png.py b/bokeh/command/subcommands/png.py
--- a/bokeh/command/subcommands/png.py
+++ b/bokeh/command/subcommands/png.py
@@ -41,11 +41,10 @@
 from __future__ import absolute_import
 
 import io
-import os
 import sys
 import warnings
 
-from ...io.export import get_screenshot_as_png, terminate_web_driver
+from ...io.export import get_screenshot_as_png, create_webdriver, terminate_webdriver
 from ...models.plots import Plot
 from .file_output import FileOutputSubcommand
 
@@ -86,10 +85,11 @@ def invoke(self, args):
         '''
 
         '''
-        import selenium.webdriver as webdriver
-        self.driver = webdriver.PhantomJS(service_log_path=os.path.devnull)
-        super(PNG, self).invoke(args)
-        terminate_web_driver(self.driver)
+        self.driver = create_webdriver()
+        try:
+            super(PNG, self).invoke(args)
+        finally:
+            terminate_webdriver(self.driver)
 
     def write_file(self, args, filename, doc):
         '''
diff --git a/bokeh/command/subcommands/svg.py b/bokeh/command/subcommands/svg.py
--- a/bokeh/command/subcommands/svg.py
+++ b/bokeh/command/subcommands/svg.py
@@ -43,10 +43,9 @@
 from __future__ import absolute_import
 
 import io
-import os
 import warnings
 
-from ...io.export import get_svgs, terminate_web_driver
+from ...io.export import get_svgs, create_webdriver, terminate_webdriver
 from ...models.plots import Plot
 from ...util.string import decode_utf8
 from .file_output import FileOutputSubcommand
@@ -88,10 +87,11 @@ def invoke(self, args):
         '''
 
         '''
-        import selenium.webdriver as webdriver
-        self.driver = webdriver.PhantomJS(service_log_path=os.path.devnull)
-        super(SVG, self).invoke(args)
-        terminate_web_driver(self.driver)
+        self.driver = create_webdriver()
+        try:
+            super(SVG, self).invoke(args)
+        finally:
+            terminate_webdriver(self.driver)
 
     def write_file(self, args, filename, doc):
         '''
diff --git a/bokeh/io/export.py b/bokeh/io/export.py
--- a/bokeh/io/export.py
+++ b/bokeh/io/export.py
@@ -26,9 +26,9 @@
 import os
 import io
 import signal
+import warnings
 from os.path import abspath, devnull
 from tempfile import mkstemp
-from warnings import warn
 
 # External imports
 
@@ -162,21 +162,23 @@ def get_screenshot_as_png(obj, driver=None, **kwargs):
         with io.open(tmp.fd, mode="wb", closefd=False) as file:
             file.write(b(html))
 
-        web_driver = driver if driver is not None else _create_default_webdriver()
-        web_driver.get("file:///" + tmp.path)
-        web_driver.maximize_window()
+        web_driver = driver if driver is not None else create_webdriver()
 
-        ## resize for PhantomJS compat
-        web_driver.execute_script("document.body.style.width = '100%';")
+        try:
+            web_driver.get("file:///" + tmp.path)
+            web_driver.maximize_window()
 
-        wait_until_render_complete(web_driver)
+            ## resize for PhantomJS compat
+            web_driver.execute_script("document.body.style.width = '100%';")
 
-        png = web_driver.get_screenshot_as_png()
+            wait_until_render_complete(web_driver)
 
-        b_rect = web_driver.execute_script(_BOUNDING_RECT_SCRIPT)
+            png = web_driver.get_screenshot_as_png()
 
-        if driver is None: # only quit webdriver if not passed in as arg
-            terminate_web_driver(web_driver)
+            b_rect = web_driver.execute_script(_BOUNDING_RECT_SCRIPT)
+        finally:
+            if driver is None: # only quit webdriver if not passed in as arg
+                terminate_webdriver(web_driver)
 
     image = Image.open(io.BytesIO(png))
     cropped_image = _crop_image(image, **b_rect)
@@ -192,15 +194,17 @@ def get_svgs(obj, driver=None, **kwargs):
         with io.open(tmp.fd, mode="wb", closefd=False) as file:
             file.write(b(html))
 
-        web_driver = driver if driver is not None else _create_default_webdriver()
-        web_driver.get("file:///" + tmp.path)
+        web_driver = driver if driver is not None else create_webdriver()
 
-        wait_until_render_complete(web_driver)
+        try:
+            web_driver.get("file:///" + tmp.path)
 
-        svgs = web_driver.execute_script(_SVG_SCRIPT)
+            wait_until_render_complete(web_driver)
 
-        if driver is None: # only quit webdriver if not passed in as arg
-            terminate_web_driver(web_driver)
+            svgs = web_driver.execute_script(_SVG_SCRIPT)
+        finally:
+            if driver is None: # only quit webdriver if not passed in as arg
+                terminate_webdriver(web_driver)
 
     return svgs
 
@@ -213,7 +217,7 @@ def get_layout_html(obj, resources=INLINE, **kwargs):
         # Defer this import, it is expensive
         from ..models.plots import Plot
         if not isinstance(obj, Plot):
-            warn("Export method called with height or width kwargs on a non-Plot layout. The size values will be ignored.")
+            warnings.warn("Export method called with height or width kwargs on a non-Plot layout. The size values will be ignored.")
         else:
             resize = True
             old_height = obj.plot_height
@@ -264,7 +268,18 @@ def is_bokeh_render_complete(driver):
         if len(severe_errors) > 0:
             log.warn("There were severe browser errors that may have affected your export: {}".format(severe_errors))
 
-def terminate_web_driver(driver):
+def create_webdriver():
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", ".*", UserWarning, "selenium.webdriver.phantomjs.webdriver")
+
+        webdriver = import_required('selenium.webdriver',
+                                    'To use bokeh.io image export functions you need selenium ' +
+                                    '("conda install -c bokeh selenium" or "pip install selenium")')
+
+        phantomjs_path = detect_phantomjs()
+        return webdriver.PhantomJS(executable_path=phantomjs_path, service_log_path=devnull)
+
+def terminate_webdriver(driver):
     if driver.name == "phantomjs":
         # https://github.com/seleniumhq/selenium/issues/767
         driver.service.process.send_signal(signal.SIGTERM)
@@ -313,14 +328,6 @@ def _crop_image(image, left=0, top=0, right=0, bottom=0, **kwargs):
     '''
     return image.crop((left, top, right, bottom))
 
-def _create_default_webdriver():
-    webdriver = import_required('selenium.webdriver',
-                                'To use bokeh.io image export functions you need selenium ' +
-                                '("conda install -c bokeh selenium" or "pip install selenium")')
-
-    phantomjs_path = detect_phantomjs()
-    return webdriver.PhantomJS(executable_path=phantomjs_path, service_log_path=devnull)
-
 class _TempFile(object):
 
     def __init__(self, prefix="tmp", suffix=""):


Test patch:
diff --git a/bokeh/io/tests/test_export.py b/bokeh/io/tests/test_export.py
--- a/bokeh/io/tests/test_export.py
+++ b/bokeh/io/tests/test_export.py
@@ -19,17 +19,15 @@
 
 # Standard library imports
 from mock import patch
-import os
 import re
 
 # External imports
 from PIL import Image
-import selenium.webdriver as webdriver
 
 # Bokeh imports
 from bokeh.models.plots import Plot
 from bokeh.models.ranges import Range1d
-from bokeh.io.export import terminate_web_driver
+from bokeh.io.export import create_webdriver, terminate_webdriver
 
 # Module under test
 import bokeh.io.export as bie
@@ -67,12 +65,11 @@ def test_get_screenshot_as_png_with_driver():
                   outline_line_color=None, background_fill_color=None,
                   border_fill_color=None)
 
-    driver = webdriver.PhantomJS(service_log_path=os.path.devnull)
-
-    png = bie.get_screenshot_as_png(layout, driver=driver)
-
-    # Have to manually clean up the driver session
-    terminate_web_driver(driver)
+    driver = create_webdriver()
+    try:
+        png = bie.get_screenshot_as_png(layout, driver=driver)
+    finally:
+        terminate_webdriver(driver)
 
     assert png.size == (20, 20)
     # a 20x20px image of transparent pixels
@@ -86,17 +83,18 @@ def test_get_screenshot_as_png_large_plot():
                   outline_line_color=None, background_fill_color=None,
                   border_fill_color=None)
 
-    driver = webdriver.PhantomJS(service_log_path=os.path.devnull)
-    assert driver.get_window_size() == {'width': 400, 'height': 300}
-
-    bie.get_screenshot_as_png(layout, driver=driver)
+    driver = create_webdriver()
+    try:
+        assert driver.get_window_size() == {'width': 400, 'height': 300}
 
-    # LC: Although the window size doesn't match the plot dimensions (unclear
-    # why), the window resize allows for the whole plot to be captured
-    assert driver.get_window_size() == {'width': 1366, 'height': 768}
+        bie.get_screenshot_as_png(layout, driver=driver)
 
-    # Have to manually clean up the driver session
-    terminate_web_driver(driver)
+        # LC: Although the window size doesn't match the plot dimensions (unclear
+        # why), the window resize allows for the whole plot to be captured
+        assert driver.get_window_size() == {'width': 1366, 'height': 768}
+    finally:
+        # Have to manually clean up the driver session
+        terminate_webdriver(driver)
 
 @pytest.mark.unit
 @pytest.mark.selenium
@@ -123,9 +121,11 @@ def fix_ids(svg):
 
     svg0 = fix_ids(bie.get_svgs(layout)[0])
 
-    driver = webdriver.PhantomJS(service_log_path=os.path.devnull)
-    svg1 = fix_ids(bie.get_svgs(layout)[0])
-    terminate_web_driver(driver) # Have to manually clean up the driver session
+    driver = create_webdriver()
+    try:
+        svg1 = fix_ids(bie.get_svgs(layout)[0])
+    finally:
+        terminate_webdriver(driver) # Have to manually clean up the driver session
 
     svg2 = (
         '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" '