Repository: bokeh/bokeh

Issue:
[BUG] Selenium required to build extensions
After trying to compile panel with bokeh 2.0 I encounter this problem:

https://github.com/holoviz/panel/pull/905#issuecomment-586711398


Discussion:
We should add selenium to the min deps tests

Resolving patch:
diff --git a/bokeh/io/export.py b/bokeh/io/export.py
--- a/bokeh/io/export.py
+++ b/bokeh/io/export.py
@@ -24,7 +24,7 @@
 import warnings
 from os.path import abspath
 from tempfile import mkstemp
-from typing import Any, List, Optional, Tuple, Union, cast
+from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, cast
 
 # External imports
 from PIL import Image
@@ -35,7 +35,6 @@
 from ..models.layouts import LayoutDOM
 from ..resources import INLINE, Resources
 from .util import default_filename
-from .webdriver import WebDriver, webdriver_control
 
 #-----------------------------------------------------------------------------
 # Globals and constants
@@ -49,12 +48,15 @@
     'get_svgs',
 )
 
+if TYPE_CHECKING:
+    from selenium.webdriver.remote.webdriver import WebDriver
+
 #-----------------------------------------------------------------------------
 # General API
 #-----------------------------------------------------------------------------
 
 def export_png(obj: Union[LayoutDOM, Document], *, filename: Optional[str] = None, width: Optional[int] = None,
-        height: Optional[int] = None, webdriver: Optional[WebDriver] = None, timeout: int = 5) -> str:
+        height: Optional[int] = None, webdriver: "Optional[WebDriver]" = None, timeout: int = 5) -> str:
     ''' Export the ``LayoutDOM`` object or document as a PNG.
 
     If the filename is not given, it is derived from the script name (e.g.
@@ -105,7 +107,7 @@ def export_png(obj: Union[LayoutDOM, Document], *, filename: Optional[str] = Non
     return abspath(filename)
 
 def export_svgs(obj: Union[LayoutDOM, Document], *, filename: Optional[str] = None, width: Optional[int] = None,
-        height: Optional[int] = None, webdriver: Optional[WebDriver] = None, timeout: int = 5) -> List[str]:
+        height: Optional[int] = None, webdriver: "Optional[WebDriver]" = None, timeout: int = 5) -> List[str]:
     ''' Export the SVG-enabled plots within a layout. Each plot will result
     in a distinct SVG file.
 
@@ -166,7 +168,7 @@ def export_svgs(obj: Union[LayoutDOM, Document], *, filename: Optional[str] = No
 # Dev API
 #-----------------------------------------------------------------------------
 
-def get_screenshot_as_png(obj: Union[LayoutDOM, Document], *, driver: Optional[WebDriver] = None, timeout: int = 5,
+def get_screenshot_as_png(obj: Union[LayoutDOM, Document], *, driver: "Optional[WebDriver]" = None, timeout: int = 5,
         resources: Resources = INLINE, width: Optional[int] = None, height: Optional[int] = None) -> Image:
     ''' Get a screenshot of a ``LayoutDOM`` object.
 
@@ -189,6 +191,8 @@ def get_screenshot_as_png(obj: Union[LayoutDOM, Document], *, driver: Optional[W
         aspect ratios. It is recommended to use the default ``fixed`` sizing mode.
 
     '''
+    from .webdriver import webdriver_control
+
     with _tmp_html() as tmp:
         html = get_layout_html(obj, resources=resources, width=width, height=height)
         with io.open(tmp.path, mode="w", encoding="utf-8") as file:
@@ -206,11 +210,13 @@ def get_screenshot_as_png(obj: Union[LayoutDOM, Document], *, driver: Optional[W
                  .crop((0, 0, width*dpr, height*dpr))
                  .resize((width, height)))
 
-def get_svgs(obj: Union[LayoutDOM, Document], *, driver: Optional[WebDriver] = None, timeout: int = 5,
+def get_svgs(obj: Union[LayoutDOM, Document], *, driver: "Optional[WebDriver]" = None, timeout: int = 5,
         resources: Resources = INLINE, width: Optional[int] = None, height: Optional[int] = None) -> List[str]:
     '''
 
     '''
+    from .webdriver import webdriver_control
+
     with _tmp_html() as tmp:
         html = get_layout_html(obj, resources=resources, width=width, height=height)
         with io.open(tmp.path, mode="w", encoding="utf-8") as file:
@@ -271,14 +277,14 @@ def get_layout_html(obj: Union[LayoutDOM, Document], *, resources: Resources = I
 
     return html
 
-def wait_until_render_complete(driver: WebDriver, timeout: int) -> None:
+def wait_until_render_complete(driver: "WebDriver", timeout: int) -> None:
     '''
 
     '''
     from selenium.webdriver.support.ui import WebDriverWait
     from selenium.common.exceptions import TimeoutException
 
-    def is_bokeh_loaded(driver: WebDriver) -> bool:
+    def is_bokeh_loaded(driver: "WebDriver") -> bool:
         return cast(bool, driver.execute_script('''
             return typeof Bokeh !== "undefined" && Bokeh.documents != null && Bokeh.documents.length != 0
         '''))
@@ -291,7 +297,7 @@ def is_bokeh_loaded(driver: WebDriver) -> bool:
 
     driver.execute_script(_WAIT_SCRIPT)
 
-    def is_bokeh_render_complete(driver: WebDriver) -> bool:
+    def is_bokeh_render_complete(driver: "WebDriver") -> bool:
         return cast(bool, driver.execute_script('return window._bokeh_render_complete;'))
 
     try:
@@ -307,7 +313,7 @@ def is_bokeh_render_complete(driver: WebDriver) -> bool:
 # Private API
 #-----------------------------------------------------------------------------
 
-def _log_console(driver: WebDriver) -> None:
+def _log_console(driver: "WebDriver") -> None:
     levels = {'WARNING', 'ERROR', 'SEVERE'}
     try:
         logs = driver.get_log('browser')
@@ -319,7 +325,7 @@ def _log_console(driver: WebDriver) -> None:
         for message in messages:
             log.warning(message)
 
-def _maximize_viewport(web_driver: WebDriver) -> Tuple[int, int, int]:
+def _maximize_viewport(web_driver: "WebDriver") -> Tuple[int, int, int]:
     calculate_viewport_size = """\
         const root = document.getElementsByClassName("bk-root")[0]
         const {width, height} = root.children[0].getBoundingClientRect()
diff --git a/bokeh/io/webdriver.py b/bokeh/io/webdriver.py
--- a/bokeh/io/webdriver.py
+++ b/bokeh/io/webdriver.py
@@ -14,15 +14,15 @@
 import logging # isort:skip
 log = logging.getLogger(__name__)
 
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
 from ..util.dependencies import import_required # isort:skip
 import_required("selenium.webdriver",
                 "To use bokeh.io image export functions you need selenium "
                 "('conda install selenium' or 'pip install selenium')")
 
-#-----------------------------------------------------------------------------
-# Imports
-#-----------------------------------------------------------------------------
-
 # Standard library imports
 import atexit
 import shutil


Test patch:
diff --git a/tests/codebase/test_no_selenium_common.py b/tests/codebase/test_no_selenium_common.py
new file mode 100644
--- /dev/null
+++ b/tests/codebase/test_no_selenium_common.py
@@ -0,0 +1,48 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2012 - 2020, Anaconda, Inc. All rights reserved.
+#
+# Powered by the Bokeh Development Team.
+#
+# The full license is in the file LICENSE.txt, distributed with this software.
+#-----------------------------------------------------------------------------
+
+#-----------------------------------------------------------------------------
+# Boilerplate
+#-----------------------------------------------------------------------------
+import pytest ; pytest
+
+#-----------------------------------------------------------------------------
+# Imports
+#-----------------------------------------------------------------------------
+
+# Standard library imports
+from subprocess import PIPE, Popen
+from sys import executable
+
+#-----------------------------------------------------------------------------
+# Tests
+#-----------------------------------------------------------------------------
+
+BASIC_IMPORTS = [
+    "import bokeh.application",
+    "import bokeh.client",
+    "import bokeh.embed",
+    "import bokeh.io",
+    "import bokeh.models",
+    "import bokeh.plotting",
+    "import bokeh.server",
+]
+
+def test_no_tornado_common() -> None:
+    ''' Basic usage of Bokeh should not result in any Selenium code being
+    imported. This test ensures that importing basic modules does not bring in
+    Tornado.
+
+    '''
+    proc = Popen([
+        executable, "-c", "import sys; %s; sys.exit(1 if any('selenium' in x for x in sys.modules.keys()) else 0)" % ";".join(BASIC_IMPORTS)
+    ],stdout=PIPE)
+    proc.communicate()
+    proc.wait()
+    if proc.returncode != 0:
+        assert False