Repository: vega/altair

Issue:
save PNG/SVG in Windows
I encountered a problem saving PNG/SVG in Windows. I installed Selenium by 
`
conda install selenium
`
then downloaded chromedriver and put it under the system PATH variable (I think this is working). When I do chart.savechart('test.png'), there's problem message as follows:

```python-traceback
---------------------------------------------------------------------------
PermissionError                           Traceback (most recent call last)
<ipython-input-16-c16ecf6573c1> in <module>()
----> 1 chart.savechart('test.png')

G:\Software\Anaconda2\envs\py36\lib\site-packages\altair\vegalite\v2\api.py in savechart(self, fp, format, **kwargs)
    331             utils.write_file_or_filename(fp, spec_html, mode='w')
    332         elif format in ['png', 'svg']:
--> 333             utils.save_spec(self.to_dict(), fp, format=format, **kwargs)
    334         else:
    335             raise ValueError("unrecognized format: '{0}'".format(format))

G:\Software\Anaconda2\envs\py36\lib\site-packages\altair\utils\headless.py in save_spec(spec, fp, mode, format, driver_timeout)
    136                                                  spec, mode)
    137         finally:
--> 138             os.remove(htmlfile)
    139     finally:
    140         driver.close()

PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\\Users\\PAGPI_~1\\AppData\\Local\\Temp\\tmpbyp9uhvp.html'
```

I suppose the temp html file is somehow being used in Windows so it's better to remove it after the chromedriver is closed? Let me know if you guys need further testing under Win environment


Discussion:
Ah, I vaguely remember running into this problem several years ago when trying to use ``NamedTemporaryFile`` with windows in a similar context in some other project... it's something to do with Windows having some sort of lock on temporary files when they're used with a context manager. I think I ended up getting around it by writing a custom named temporary file routine from scratch.

Windows... always trying to be special.
In the meantime, you can open figures in the browser and click "save to png" on the rendering.
I don't have access to a windows machine to debug this, so if anyone else has time to take a look I'd appreciate it!
Found it... this is a "fix" for essentially the same bug when it came up in another project: https://github.com/jakevdp/JSAnimation/commit/7eab0c23e237df707f02cfff3606eee8867e8665

We'd want to do something more sophisticated than that, though (there's a reason I never released that other package :smile: )
And here's the slightly more sophisticated version: https://github.com/jakevdp/JSAnimation/commit/2edd36c8d0eac6508e1dd887fb142e689a08b202
And just for good measure, here's the relevant issue in CPython: https://bugs.python.org/issue14243 (looks like this won't be a problem any more in Python 3.7)
Thanks Jake! I tried your 'more sophisticated version' and it now worked. Do you want to adopt this solution? If so, I can do a PR

Resolving patch:
diff --git a/altair/utils/headless.py b/altair/utils/headless.py
--- a/altair/utils/headless.py
+++ b/altair/utils/headless.py
@@ -3,15 +3,10 @@
 """
 
 import base64
-import io
-import json
+import contextlib
 import os
 import tempfile
 
-import six
-
-from .core import write_file_or_filename
-
 try:
     from selenium import webdriver
 except ImportError:
@@ -23,6 +18,25 @@
     ChromeOptions = None
 
 
+@contextlib.contextmanager
+def temporary_filename(**kwargs):
+    """Create and clean-up a temporary file
+
+    Arguments are the same as those passed to tempfile.mkstemp
+
+    We could use tempfile.NamedTemporaryFile here, but that causes issues on
+    windows (see https://bugs.python.org/issue14243).
+    """
+    filedescriptor, filename = tempfile.mkstemp(**kwargs)
+    os.close(filedescriptor)
+
+    try:
+        yield filename
+    finally:
+        if os.path.exists(filename):
+            os.remove(filename)
+
+
 HTML_TEMPLATE = """
 <!DOCTYPE html>
 <html>
@@ -83,6 +97,7 @@
             .catch(function(err) { console.error(err); });
         """}
 
+
 def spec_to_image_mimebundle(spec, format, mode,
                              vega_version,
                              vegaembed_version,
@@ -129,9 +144,11 @@ def spec_to_image_mimebundle(spec, format, mode,
         raise ValueError("must specify vega-lite version")
 
     if webdriver is None:
-        raise ImportError("selenium package is required for saving chart as {0}".format(format))
+        raise ImportError("selenium package is required "
+                          "for saving chart as {0}".format(format))
     if ChromeOptions is None:
-        raise ImportError("chromedriver is required for saving chart as {0}".format(format))
+        raise ImportError("chromedriver is required "
+                          "for saving chart as {0}".format(format))
 
     html = HTML_TEMPLATE.format(vega_version=vega_version,
                                 vegalite_version=vegalite_version,
@@ -146,18 +163,16 @@ def spec_to_image_mimebundle(spec, format, mode,
         driver = webdriver.Chrome(chrome_options=chrome_options)
         driver.set_page_load_timeout(driver_timeout)
 
-        try:
-            fd, htmlfile = tempfile.mkstemp(suffix='.html', text=True)
+        with temporary_filename(suffix='.html') as htmlfile:
             with open(htmlfile, 'w') as f:
                 f.write(html)
             driver.get("file://" + htmlfile)
             online = driver.execute_script("return navigator.onLine")
             if not online:
-                raise ValueError("Internet connection required for saving chart as {0}".format(format))
+                raise ValueError("Internet connection required for saving "
+                                 "chart as {0}".format(format))
             render = driver.execute_async_script(EXTRACT_CODE[format],
                                                  spec, mode)
-        finally:
-            os.remove(htmlfile)
     finally:
         driver.close()
 


Test patch: