Repository: bokeh/bokeh

Issue:
[BUG] Resource warning unclosed file... _sri.json possible Resource Leak
bokeh==2.0.2, 
Windows 10 Home Version10.0.18363 Build 18363 
PyCharm 2020.1.2 (Community Edition), Build #PC-201.7846.77, built on May 31, 2020, Runtime version: 11.0.7+10-b765.53 amd64 VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o., Windows 10 10.0, GC: ParNew, ConcurrentMarkSweep, Memory: 1979M, Cores: 12
Non-Bundled Plugins: org.exbin.deltahex.intellij, ru.meanmail.plugin.requirements

#### Description of expected behavior and the observed behavior
Expected html plot but addition got warning:
\venv\lib\site-packages\bokeh\resources.py:107: ResourceWarning: unclosed file <_io.TextIOWrapper name=‘C:\…\venv\lib\site-packages\bokeh\_sri.json’ mode=‘r’ encoding=‘cp1252’> _SRI_HASHES = json.load(open(join(ROOT_DIR, “_sri.json”)))

#### Complete, minimal, self-contained example code that reproduces the issue

```
df is a pandas dataframe 40,000 rows and 7 columns, float

    output_file(f'{data_path()}{plot_title}.html')
    if df.shape[1] > 2:
        colors = Category20[df.shape[1]]  # limited to 20 colors
    else:
        colors = ['#1f77b4', '#aec7e8']  # why do palettes have a min of 3?
    p = figure(title=plot_title, x_axis_label='Time(s)', y_axis_label=left_axis_label,
               x_range=(df.Time[0], df.Time.iloc[-1] * 1.3))
    for i, col in enumerate(df.columns):
        if col != 'Time': 
            p.line(x='Time', y=col, source=df, legend_label=col, color=colors[i])
    p.legend.click_policy = "hide"
    show(p)

```

#### Stack traceback and/or browser JavaScript console output

C:\Users\.....\GitHub\MultimodeEPGTest\venv\lib\site-packages\bokeh\resources.py:107:



Discussion:

Resolving patch:
diff --git a/bokeh/resources.py b/bokeh/resources.py
--- a/bokeh/resources.py
+++ b/bokeh/resources.py
@@ -104,7 +104,8 @@ def get_all_sri_hashes():
     global _SRI_HASHES
 
     if not _SRI_HASHES:
-        _SRI_HASHES = json.load(open(join(ROOT_DIR, "_sri.json")))
+        with open(join(ROOT_DIR, "_sri.json")) as f:
+            _SRI_HASHES = json.load(f)
 
     return dict(_SRI_HASHES)
 
diff --git a/bokeh/sphinxext/bokeh_releases.py b/bokeh/sphinxext/bokeh_releases.py
--- a/bokeh/sphinxext/bokeh_releases.py
+++ b/bokeh/sphinxext/bokeh_releases.py
@@ -65,11 +65,16 @@
 # Dev API
 # -----------------------------------------------------------------------------
 
-_SRI_SECTION = """
-Subresource Integrity Hashes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+_SRI_SECTION_PRE = """
 
-The SRI Hashes for version ``%s`` are given in the table below:
+**Sub-Resource Integrity Hashes**
+
+.. raw:: html
+
+    <div class="expander">
+        <a href="javascript:void(0)" class="expander-trigger expander-hidden">Table of SRI Hashes for version %s</a>
+        <div class="expander-content">
+            <div style="font-size: small;">
 
 .. csv-table::
     :widths: 20, 80
@@ -77,6 +82,15 @@
 
 """
 
+_SRI_SECTION_POST = """
+.. raw:: html
+
+            </div>
+        </div>
+   </div>
+
+"""
+
 
 class BokehReleases(BokehDirective):
     def run(self):
@@ -92,9 +106,10 @@ def run(self):
             rst_text = f".. include:: releases/{v}.rst"
             try:
                 hashes = get_sri_hashes_for_version(v)
-                rst_text += _SRI_SECTION % v
+                rst_text += _SRI_SECTION_PRE % v
                 for key, val in sorted(hashes.items()):
                     rst_text += f"    ``{key}``, ``{val}``\n"
+                rst_text += _SRI_SECTION_POST
             except KeyError:
                 if v == __version__:
                     raise RuntimeError(f"Missing SRI Hash for full release version {v!r}")


Test patch:
diff --git a/sphinx/source/docs/dev_guide/testing.rst b/sphinx/source/docs/dev_guide/testing.rst
--- a/sphinx/source/docs/dev_guide/testing.rst
+++ b/sphinx/source/docs/dev_guide/testing.rst
@@ -37,8 +37,9 @@ level of the repository:
 
     py.test tests/unit
 
-Note that this includes unit tests that require Selenium to be installed. To
-exclude those unit tests, you can run the command:
+Note that this includes unit tests that require Selenium as well as appropriate
+web drivers (e.g. chromedriver and geckodriver) to be installed. To exclude
+those unit tests, you can run the command:
 
 .. code-block:: sh
 
diff --git a/tests/unit/bokeh/io/test_util__io.py b/tests/unit/bokeh/io/test_util__io.py
--- a/tests/unit/bokeh/io/test_util__io.py
+++ b/tests/unit/bokeh/io/test_util__io.py
@@ -36,7 +36,7 @@
 #-----------------------------------------------------------------------------
 
 def test_detect_current_filename() -> None:
-    assert biu.detect_current_filename().endswith(("py.test", "pytest", "py.test-script.py"))
+    assert biu.detect_current_filename().endswith(("py.test", "pytest", "py.test-script.py", "pytest-script.py"))
 
 @patch('bokeh.io.util.NamedTemporaryFile')
 def test_temp_filename(mock_tmp) -> None:
diff --git a/tests/unit/bokeh/io/test_webdriver.py b/tests/unit/bokeh/io/test_webdriver.py
--- a/tests/unit/bokeh/io/test_webdriver.py
+++ b/tests/unit/bokeh/io/test_webdriver.py
@@ -33,6 +33,7 @@
 # Dev API
 #-----------------------------------------------------------------------------
 
+@pytest.mark.selenium
 def test_create_firefox_webdriver() -> None:
     d = biw.create_firefox_webdriver()
     try:
@@ -40,6 +41,7 @@ def test_create_firefox_webdriver() -> None:
     finally:
         d.quit()
 
+@pytest.mark.selenium
 def test_create_chromium_webdriver() -> None:
     d = biw.create_chromium_webdriver()
     try:
@@ -89,6 +91,7 @@ def test_get_without_reuse(self) -> None:
         biw.webdriver_control.reuse = True
         biw.webdriver_control.reset()
 
+    @pytest.mark.selenium
     @pytest.mark.parametrize('kind', ['firefox', 'chromium'])
     def test_create(self, kind) -> None:
         biw.webdriver_control.kind = kind