Repository: jazzband/pip-tools

Issue:
pip-sync broken on Windows
Calling pip-sync on Windows returns:
```
Could not open requirements file: [Errno 13] Permission denied: 'c:\\users\\<username>\\appdata\\local\\temp\\tmpe9jswo'
```

This was introduced in 3.3.1 by the change to using `tempfile.NamedTemporaryFile` in [sync.py](https://github.com/jazzband/pip-tools/blob/3.3.1/piptools/sync.py#L167) to create the requirements file before passing it to `pip install` via `subprocess.check_call`. This is caused by Windows requiring reopened temporary files to be opened with the `O_TEMPORARY`  flag. For context see https://stackoverflow.com/a/15235559. 

##### Environment Versions

1. OS Type: Windows 10
1. Python version: Python 2.7.12
1. pip version: pip 9.0.3
1. pip-tools version: pip-compile, version 3.3.1

##### Steps to replicate

1. `pip-sync -r <requirements_file>`

##### Expected result

Command should complete successfully.

##### Actual result

```
Could not open requirements file: [Errno 13] Permission denied: 'c:\\users\\<username>\\appdata\\local\\temp\\tmpe9jswo'
```



Discussion:
Yes, this is exactly why we do it [by leaving the file around](https://github.com/jazzband/pip-tools/blob/master/piptools/scripts/compile.py#L170) in pip-compile.
Hello @miuliano, thanks for the report, i'll try to fix it ASAP.

Thank you @davidovich for the info! It should have been done the same as in compile.


Resolving patch:
diff --git a/piptools/sync.py b/piptools/sync.py
--- a/piptools/sync.py
+++ b/piptools/sync.py
@@ -163,9 +163,13 @@ def sync(to_install, to_uninstall, verbose=False, dry_run=False, pip_flags=None,
                 req_lines.append(format_requirement(ireq, hashes=ireq_hashes))
 
             # save requirement lines to a temporary file
-            with tempfile.NamedTemporaryFile(mode='wt') as tmp_req_file:
-                tmp_req_file.write('\n'.join(req_lines))
-                tmp_req_file.flush()
+            tmp_req_file = tempfile.NamedTemporaryFile(mode='wt', delete=False)
+            tmp_req_file.write('\n'.join(req_lines))
+            tmp_req_file.close()
 
+            try:
                 check_call([pip, 'install', '-r', tmp_req_file.name] + pip_flags + install_flags)
+            finally:
+                os.unlink(tmp_req_file.name)
+
     return 0


Test patch:
diff --git a/tests/test_sync.py b/tests/test_sync.py
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -12,10 +12,16 @@
 
 
 @pytest.fixture
-def mocked_tmp_req_file():
+def mocked_tmp_file():
     with mock.patch.object(tempfile, 'NamedTemporaryFile') as m:
-        m.return_value.__enter__.return_value.name = 'requirements.txt'
-        yield m.return_value.__enter__.return_value
+        yield m.return_value
+
+
+@pytest.fixture
+def mocked_tmp_req_file(mocked_tmp_file):
+    with mock.patch('os.unlink'):
+        mocked_tmp_file.name = 'requirements.txt'
+        yield mocked_tmp_file
 
 
 @pytest.mark.parametrize(
@@ -227,6 +233,16 @@ def test_sync_install_temporary_requirement_file(from_line, from_editable, mocke
         check_call.assert_called_once_with(['pip', 'install', '-r', mocked_tmp_req_file.name, '-q'])
 
 
+def test_temporary_requirement_file_deleted(from_line, from_editable, mocked_tmp_file):
+    with mock.patch('piptools.sync.check_call'):
+        to_install = {from_line('django==1.8')}
+
+        with mock.patch('os.unlink') as unlink:
+            sync(to_install, set())
+
+            unlink.assert_called_once_with(mocked_tmp_file.name)
+
+
 def test_sync_requirement_file(from_line, from_editable, mocked_tmp_req_file):
     with mock.patch('piptools.sync.check_call'):
         to_install = {