Repository: pypa/pip

Issue:
pip `--install-option` leaks across lines
* Pip version: 9.0.2
* Python version: 3.6.4
* Operating system: Ubuntu 16.04.4 LTS (Xenial)

### Description:

Installing two packages using a `requirements.txt` file, where one package requires `--install-option` and the second doesn't, the `--install-option` leaks from the first package to the second, unless there's an intervening blank line.

### What I've run:

```
% pyenv virtualenv 3.6.4 test-before-bugreport
% pyenv shell test-before-bugreport
% pip install -U pip setuptools
% pip list
Package    Version
---------- -------
pip        9.0.2
setuptools 39.0.1
% grep -E 'pygraphviz|M2Crypto' requirements.txt > new-req
% cat -nv new-req
     1	pygraphviz --install-option="--include-path=/usr/include/graphviz" --install-option="--library-path=/usr/lib/graphviz/"
     2	M2Crypto
% pip install -r new-req
/home/dnsviz/.pyenv/versions/3.6.4/envs/test-before-bugreport/lib/python3.6/site-packages/pip/req/req_file.py:150: UserWarning: Disabling all use of wheels due to the use of --build-options / --global-options / --install-options.
  cmdoptions.check_install_build_global(options, opts)
Collecting pygraphviz (from -r new-req (line 1))
  Using cached pygraphviz-1.3.1.zip
Collecting M2Crypto (from -r new-req (line 2))
  Using cached M2Crypto-0.29.0.tar.gz
Installing collected packages: pygraphviz, M2Crypto
  Running setup.py install for pygraphviz ... done
  Running setup.py install for M2Crypto ... error
    Complete output from command /home/dnsviz/.pyenv/versions/3.6.4/envs/test-before-bugreport/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-ikgbiv_3/M2Crypto/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-6fj6jp4d-record/install-record.txt --single-version-externally-managed --compile --install-headers /home/dnsviz/.pyenv/versions/3.6.4/envs/test-before-bugreport/include/site/python3.6/M2Crypto --include-path=/usr/include/graphviz --library-path=/usr/lib/graphviz/:
    usage: -c [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
       or: -c --help [cmd1 cmd2 ...]
       or: -c --help-commands
       or: -c cmd --help

    error: option --include-path not recognized

    ----------------------------------------
Command "/home/dnsviz/.pyenv/versions/3.6.4/envs/test-before-bugreport/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-ikgbiv_3/M2Crypto/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-6fj6jp4d-record/install-record.txt --single-version-externally-managed --compile --install-headers /home/dnsviz/.pyenv/versions/3.6.4/envs/test-before-bugreport/include/site/python3.6/M2Crypto --include-path=/usr/include/graphviz --library-path=/usr/lib/graphviz/" failed with error code 1 in /tmp/pip-build-ikgbiv_3/M2Crypto/
```

Repeating the invocation, `pygraphviz` is already installed, `M2Crypto` then installs fine.

Blank lines etc don't help.  It looks as though the list of `--install-option` options is not being reset to empty at the start of the next entry.


Discussion:
Duplicate of issue #4453 which already duplicates issue #3763
Sorry for the noise; I did search but I spotted nothing obviously came up in the first few results pages for my search text and I couldn't find search text which helped.

Resolving patch:
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -771,11 +771,13 @@ def install(self, install_options, global_options=None, root=None,
         # Options specified in requirements file override those
         # specified on the command line, since the last option given
         # to setup.py is the one that is used.
-        global_options += self.options.get('global_options', [])
-        install_options += self.options.get('install_options', [])
+        global_options = list(global_options) + \
+            self.options.get('global_options', [])
+        install_options = list(install_options) + \
+            self.options.get('install_options', [])
 
         if self.isolated:
-            global_options = list(global_options) + ["--no-user-cfg"]
+            global_options = global_options + ["--no-user-cfg"]
 
         with TempDirectory(kind="record") as temp_dir:
             record_filename = os.path.join(temp_dir.path, 'install-record.txt')


Test patch:
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -513,3 +513,37 @@ def test_install_unsupported_wheel_file(script, data):
     assert ("simple.dist-0.1-py1-none-invalid.whl is not a supported " +
             "wheel on this platform" in result.stderr)
     assert len(result.files_created) == 0
+
+
+def test_install_options_local_to_package(script, data):
+    """Make sure --install-options does not leak across packages.
+
+    A requirements.txt file can have per-package --install-options; these
+    should be isolated to just the package instead of leaking to subsequent
+    packages.  This needs to be a functional test because the bug was around
+    cross-contamination at install time.
+    """
+    home_simple = script.scratch_path.join("for-simple")
+    test_simple = script.scratch.join("for-simple")
+    home_simple.mkdir()
+    reqs_file = script.scratch_path.join("reqs.txt")
+    reqs_file.write(
+        textwrap.dedent("""
+            simple --install-option='--home=%s'
+            INITools
+            """ % home_simple))
+    result = script.pip(
+        'install',
+        '--no-index', '-f', data.find_links,
+        '-r', reqs_file,
+        expect_error=True,
+    )
+
+    simple = test_simple / 'lib' / 'python' / 'simple'
+    bad = test_simple / 'lib' / 'python' / 'initools'
+    good = script.site_packages / 'initools'
+    assert simple in result.files_created
+    assert result.files_created[simple].dir
+    assert bad not in result.files_created
+    assert good in result.files_created
+    assert result.files_created[good].dir