Repository: bokeh/bokeh

Issue:
using ``export_png`` or ``save`` without filename from within jupyter notebook saves png file to lib/python
Using ``bokeh.io.export_png``, ``bokeh.io.export_svg`` or ``bokeh.io.save`` from within a jupyter notebook without specifying an output filename causes the file to be written to lib/python (on my machine it's '/Users/lcanavan/miniconda3/envs/bokeh/lib/python3.5/runpy.png'.

It's likely due to how if there's no filename arg, we infer the name of the script the document is in. A notebook is apparently executed by this ``runpy.py`` script in lib/python, so that's the generated name.

Is there a consistent way to get the path of a jupyter notebook from inside the session?

Bokeh 0.12.7dev3


Discussion:
I'm pretty sure a similar problem was already handled at some point. `runpy` in file name is familiar to me.
> Is there a consistent way to get the path of a jupyter notebook from inside the session?

Nop, because the kernel does not know anything about the server... by design.

You have **hacky** ways to do it, some useful references:

* https://github.com/jupyter/notebook/issues/1000
* https://stackoverflow.com/questions/12544056/how-do-i-get-the-current-ipython-notebook-name
* https://stackoverflow.com/questions/41782255/how-to-get-the-current-jupyter-notebook-servers-in-python
Personally I am fine with making filenames required at all times. 
I personally like this feature, though it needs some refinement in corner cases.
For automatically generated names, it would be nice to have the timestamp or something similar so it could be identified in what order the PNGs were generated, such as with a batch or unattended process.

Resolving patch:
diff --git a/bokeh/io.py b/bokeh/io.py
--- a/bokeh/io.py
+++ b/bokeh/io.py
@@ -489,25 +489,80 @@ def save(obj, filename=None, resources=None, title=None, state=None, **kwargs):
     _save_helper(obj, filename, resources, title)
     return os.path.abspath(filename)
 
-def _detect_filename(ext):
-    """ Detect filename from the name of the script being run. Returns
-    temporary file if the script could not be found or the location of the
-    script does not have write permission (e.g. interactive mode).
-    """
+def _no_access(basedir):
+    ''' Return True if the given base dir is not accessible or writeable
+
+    '''
+    return not os.access(basedir, os.W_OK | os.X_OK)
+
+def _shares_exec_prefix(basedir):
+    ''' Whether a give base directory is on the system exex prefix
+
+    '''
+    import sys
+    prefix = sys.exec_prefix
+    return (prefix is not None and basedir.startswith(prefix))
+
+def _temp_filename(ext):
+    ''' Generate a temporary, writable filename with the given extension
+
+    '''
+    return tempfile.NamedTemporaryFile(suffix="." + ext).name
+
+def _detect_current_filename():
+    ''' Attempt to return the filename of the currently running Python process
+
+    Returns None if the filename cannot be detected.
+    '''
     import inspect
-    from os.path import dirname, basename, splitext, join, curdir
 
+    filename = None
     frame = inspect.currentframe()
-    while frame.f_back and frame.f_globals.get('name') != '__main__':
-        frame = frame.f_back
+    try:
+        while frame.f_back and frame.f_globals.get('name') != '__main__':
+            frame = frame.f_back
+
+        filename = frame.f_globals.get('__file__')
+    finally:
+        del frame
+
+    return filename
+
+def default_filename(ext):
+    ''' Generate a default filename with a given extension, attempting to use
+    the filename of the currently running process, if possible.
+
+    If the filename of the current process is not available (or would not be
+    writable), then a temporary file with the given extension is returned.
+
+    Args:
+        ext (str) : the desired extension for the filename
+
+    Returns:
+        str
+
+    Raises:
+        RuntimeError
+            If the extensions requested is ".py"
+
+    '''
+    if ext == "py":
+        raise RuntimeError("asked for a default filename with 'py' extension")
+
+    from os.path import dirname, basename, splitext, join
+
+    filename = _detect_current_filename()
+
+    if filename is None:
+        return _temp_filename(ext)
 
-    filename = frame.f_globals.get('__file__')
+    basedir = dirname(filename) or os.getcwd()
 
-    if filename is None or not os.access(dirname(filename) or curdir, os.W_OK | os.X_OK):
-        return tempfile.NamedTemporaryFile(suffix="." + ext).name
+    if _no_access(basedir) or _shares_exec_prefix(basedir):
+        return _temp_filename(ext)
 
     name, _ = splitext(basename(filename))
-    return join(dirname(filename), name + "." + ext)
+    return join(basedir, name + "." + ext)
 
 def _get_save_args(state, filename, resources, title):
     warn = True
@@ -517,7 +572,7 @@ def _get_save_args(state, filename, resources, title):
 
     if filename is None:
         warn = False
-        filename = _detect_filename("html")
+        filename = default_filename("html")
 
     if resources is None and state.file:
         resources = state.file['resources']
@@ -804,7 +859,7 @@ def export_png(obj, filename=None, height=None, width=None, webdriver=None):
     image = _get_screenshot_as_png(obj, height=height, width=width, driver=webdriver)
 
     if filename is None:
-        filename = _detect_filename("png")
+        filename = default_filename("png")
 
     image.save(filename)
 
@@ -879,7 +934,7 @@ def export_svgs(obj, filename=None, height=None, width=None, webdriver=None):
         return
 
     if filename is None:
-        filename = _detect_filename("svg")
+        filename = default_filename("svg")
 
     filenames = []
 


Test patch:
diff --git a/bokeh/tests/test_io.py b/bokeh/tests/test_io.py
--- a/bokeh/tests/test_io.py
+++ b/bokeh/tests/test_io.py
@@ -411,3 +411,82 @@ def test__get_svgs_with_svg_present_with_driver():
     assert svgs[0] == ('<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" '
                        'width="20" height="20" style="width: 20px; height: 20px;"><defs/><g><g/><g transform="scale(1,1) '
                        'translate(0.5,0.5)"><rect fill="#FFFFFF" stroke="none" x="0" y="0" width="20" height="20"/><g/><g/><g/><g/></g></g></svg>')
+
+@patch('os.access')
+def test__no_access(mock_access):
+    import os
+    io._no_access("test")
+    assert mock_access.called
+    assert mock_access.call_args[0] == ("test", os.W_OK | os.X_OK)
+    assert mock_access.call_args[1] == {}
+
+@patch('tempfile.NamedTemporaryFile')
+def test__temp_filename(mock_tmp):
+    fn = Mock()
+    type(fn).name = PropertyMock(return_value="Junk.test")
+    mock_tmp.return_value = fn
+
+    r = io._temp_filename("test")
+    assert r == "Junk.test"
+    assert mock_tmp.called
+    assert mock_tmp.call_args[0] == ()
+    assert mock_tmp.call_args[1] == {'suffix': '.test'}
+
+def test__shares_exec_prefix():
+    import sys
+    old_ex = sys.exec_prefix
+    try:
+        sys.exec_prefix = "/foo/bar"
+        assert io._shares_exec_prefix("/foo/bar") == True
+        sys.exec_prefix = "/baz/bar"
+        assert io._shares_exec_prefix("/foo/bar") == False
+        sys.exec_prefix = None
+        assert io._shares_exec_prefix("/foo/bar") == False
+    finally:
+        sys.exec_prefix = old_ex
+
+def test__detect_current_filename():
+    assert io._detect_current_filename().endswith("py.test")
+
+def test_default_filename():
+    old__detect_current_filename = io._detect_current_filename
+    old__no_access = io._no_access
+    old__shares_exec_prefix = io._shares_exec_prefix
+
+    io._detect_current_filename = lambda : "/a/b/foo.py"
+
+    try:
+        # .py extension
+        with pytest.raises(RuntimeError):
+            io.default_filename("py")
+
+        # a current file, access, and no share exec
+        io._no_access = lambda x: False
+        r = io.default_filename("test")
+        assert r == "/a/b/foo.test"
+
+        # a current file, NO access, and no share exec
+        io._no_access = lambda x: True
+        r = io.default_filename("test")
+        assert r != "/a/b/foo.test"
+        assert r.endswith(".test")
+
+        # a current file, access, but WITH share exec
+        io._no_access = lambda x: False
+        io._shares_exec_prefix = lambda x: True
+        r = io.default_filename("test")
+        assert r != "/a/b/foo.test"
+        assert r.endswith(".test")
+
+        # no current file
+        io._detect_current_filename = lambda : None
+        io._no_access = lambda x: False
+        io._shares_exec_prefix = lambda x: False
+        r = io.default_filename("test")
+        assert r != "/a/b/foo.test"
+        assert r.endswith(".test")
+
+    finally:
+        io._detect_current_filename = old__detect_current_filename
+        io._no_access = old__no_access
+        io._shares_exec_prefix = old__shares_exec_prefix