Repository: bokeh/bokeh

Issue:
Permission Denied error when invoking export_png() or export_svgs from inside Jupyter Notebook
# READ AND FOLLOW THESE INSTRUCTIONS CAREFULLY

*ISSUES THAT DO NOT CONTAIN NECESSARY INFORMATION MAY BE CLOSED, IMMEDIATELY*

The issue tracker is NOT the place for general support. For questions and
technical assistance, come ask the [Bokeh mailing list](https://groups.google.com/a/continuum.io/forum/#!forum/bokeh) or join the chat on [Gitter](https://gitter.im/bokeh/bokeh). For feature requests, please provide a detailed description or proposal of the new capability or behavior.

For defects or deficiencies, please provide ALL OF THE FOLLOWING:

#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)
bokeh==0.12.15
jupyter==1.0.0
jupyter-client==5.1.0
jupyter-console==5.2.0
jupyter-contrib-core==0.3.1
jupyter-contrib-nbextensions==0.2.8
jupyter-core==4.3.0
jupyter-highlight-selected-word==0.0.11
jupyter-latex-envs==1.3.8.4
jupyter-nbextensions-configurator==0.2.5
python-dateutil==2.6.1
selenium==3.11.0

How jupyter notebook is launched: C:\Users\<username>\AppData\Local\Programs\Python\Python36-32\Scripts\jupyter.exe notebook
OS: Windows 7 64 bit
Browser: Chrome Version 66.0.3359.66 (Official Build) beta (64-bit)
Notebook is set to trusted.
#### Description of expected behavior and the observed behavior
Expected behavior is that the line: 
export_svgs(p, filename="%s/"%script_outdir +str(1).zfill(3) + "_" +"output.svg")
will save 001_output.svg in the same directory that the notebook is.

#### Complete, minimal, self-contained example code that reproduces the issue

```
# code goes here between backticks
from bokeh.plotting import figure, show, save, output_notebook, output_file, reset_output
from bokeh.io import export_svgs,export_png
from bokeh.models import HoverTool
hover = HoverTool(tooltips=[("data (x,y)", "($x, $y)")])
TOOLS=[hover]
p = figure(width=1400, height=700,tools=TOOLS)
p.line([1, 2, 3], [5, 3, 4])
reset_output()
output_notebook()
show(p)
#reset_output()
#output_file("%s/"%"." +str(1).zfill(3) + "_" +"output.svg")
#save(p)
export_svgs(p, filename="%s/"%"." +str(1).zfill(3) + "_" +"filename.svg")
#export_png(p,filename="plot.png")

```

#### Stack traceback and/or browser JavaScript console output
PermissionError                           Traceback (most recent call last)
<ipython-input-42-55a2a21e9ee8> in <module>()
     17 #p.border_fill_color = None
     18 #p.output_backend = "svg"
---> 19 export_svgs(p, filename="%s/"%"." +str(1).zfill(3) + "_" +"filename.svg")
     20 #export_png(p,filename="plot.png")

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\util\api.py in wrapper(*args, **kw)
    188         @wraps(obj)
    189         def wrapper(*args, **kw):
--> 190             return obj(*args, **kw)
    191 
    192         wrapper.__bkversion__ = version

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\io\export.py in export_svgs(obj, filename, height, width, webdriver)
    125 
    126     '''
--> 127     svgs = get_svgs(obj, height=height, width=width, driver=webdriver)
    128 
    129     if len(svgs) == 0:

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\util\api.py in wrapper(*args, **kw)
    188         @wraps(obj)
    189         def wrapper(*args, **kw):
--> 190             return obj(*args, **kw)
    191 
    192         wrapper.__bkversion__ = version

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\io\export.py in get_svgs(obj, driver, **kwargs)
    197         html_path = tmp.name
    198 
--> 199         save_layout_html(obj, html_path, **kwargs)
    200 
    201         web_driver = driver if driver is not None else _create_default_webdriver()

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\util\api.py in wrapper(*args, **kw)
    188         @wraps(obj)
    189         def wrapper(*args, **kw):
--> 190             return obj(*args, **kw)
    191 
    192         wrapper.__bkversion__ = version

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\io\export.py in save_layout_html(obj, html_path, resources, **kwargs)
    229             obj.plot_width = kwargs.get('width', old_width)
    230 
--> 231     save(obj, filename=html_path, resources=resources, title="")
    232 
    233     if resize:

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\util\api.py in wrapper(*args, **kw)
    188         @wraps(obj)
    189         def wrapper(*args, **kw):
--> 190             return obj(*args, **kw)
    191 
    192         wrapper.__bkversion__ = version

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\io\saving.py in save(obj, filename, resources, title, state, **kwargs)
     84 
     85     filename, resources, title = _get_save_args(state, filename, resources, title)
---> 86     _save_helper(obj, filename, resources, title)
     87     return abspath(filename)
     88 

c:\users\<username>\appdata\local\programs\python\python36-32\lib\site-packages\bokeh\io\saving.py in _save_helper(obj, filename, resources, title)
    148     html = file_html(obj, resources, title=title)
    149 
--> 150     with io.open(filename, mode="w", encoding="utf-8") as f:
    151         f.write(decode_utf8(html))
    152 

PermissionError: [Errno 13] Permission denied: 'C:\\Users\\<username>\\AppData\\Local\\Temp\\tmpc8jqeews.html'
#### Screenshots or screencasts of the bug in action



Discussion:
@mattpap evidently some platforms will not let you open `NamedTemporaryFile` objects a second time:

https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file

we will have to make it simple use the returned file object for writing directly. 
I was actually wondering when fixing #7741, if this could be a problem. Well, here we are.

Resolving patch:
diff --git a/bokeh/io/export.py b/bokeh/io/export.py
--- a/bokeh/io/export.py
+++ b/bokeh/io/export.py
@@ -13,7 +13,7 @@
 # Boilerplate
 #-----------------------------------------------------------------------------
 from __future__ import absolute_import, division, print_function, unicode_literals
-from six import raise_from
+from six import raise_from, b
 
 import logging
 log = logging.getLogger(__name__)
@@ -34,9 +34,9 @@
 # External imports
 
 # Bokeh imports
+from ..embed import file_html
 from ..resources import INLINE
 from ..util.dependencies import import_required, detect_phantomjs
-from .saving import save
 from .util import default_filename
 
 #-----------------------------------------------------------------------------
@@ -163,7 +163,7 @@ def get_screenshot_as_png(obj, driver=None, **kwargs):
                             '("conda install pillow" or "pip install pillow")')
 
     with _tmp_html() as tmp:
-        save_layout_html(obj, tmp.name, **kwargs)
+        save_layout_html(obj, tmp.file, **kwargs)
 
         web_driver = driver if driver is not None else _create_default_webdriver()
         web_driver.get("file:///" + tmp.name)
@@ -192,7 +192,7 @@ def get_svgs(obj, driver=None, **kwargs):
 
     '''
     with _tmp_html() as tmp:
-        save_layout_html(obj, tmp.name, **kwargs)
+        save_layout_html(obj, tmp.file, **kwargs)
 
         web_driver = driver if driver is not None else _create_default_webdriver()
         web_driver.get("file:///" + tmp.name)
@@ -207,7 +207,7 @@ def get_svgs(obj, driver=None, **kwargs):
     return svgs
 
 @dev((1,0,0))
-def save_layout_html(obj, html_path, resources=INLINE, **kwargs):
+def save_layout_html(obj, file, resources=INLINE, **kwargs):
     '''
 
     '''
@@ -224,7 +224,9 @@ def save_layout_html(obj, html_path, resources=INLINE, **kwargs):
             obj.plot_height = kwargs.get('height', old_height)
             obj.plot_width = kwargs.get('width', old_width)
 
-    save(obj, filename=html_path, resources=resources, title="")
+    html = file_html(obj, resources, title="")
+    file.write(b(html))
+    file.flush()
 
     if resize:
         obj.plot_height = old_height
@@ -325,7 +327,7 @@ def _create_default_webdriver():
     return webdriver.PhantomJS(executable_path=phantomjs_path, service_log_path=devnull)
 
 def _tmp_html():
-    return NamedTemporaryFile(prefix=".bokeh", suffix=".html")
+    return NamedTemporaryFile(mode="wb", prefix="bokeh", suffix=".html")
 
 #-----------------------------------------------------------------------------
 # Code


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
@@ -185,8 +185,8 @@ def test_save_layout_html_resets_plot_dims():
     layout = Plot(x_range=Range1d(), y_range=Range1d(),
                   plot_height=initial_height, plot_width=initial_width)
 
-    with NamedTemporaryFile() as tmp:
-        bie.save_layout_html(layout, tmp.name, height=100, width=100)
+    with NamedTemporaryFile(mode="wb") as tmp:
+        bie.save_layout_html(layout, tmp.file, height=100, width=100)
 
     assert layout.plot_height == initial_height
     assert layout.plot_width == initial_width