Repository: pypa/setuptools (python)

Issue:
Handling of find_links in setup.cfg in 42.0.0
Hi,

I have a couple of `setup_requires` dependencies in my setup.py and my setup.cfg looks like this:

```
[easy_install]
find_links = https://example.com
```

Looking at: https://github.com/pypa/setuptools/blob/e84f616a6507ec9115fad68b221cbf5333d9d2d9/setuptools/installer.py#L119

The `cmd` ends up being:

```python
['python', '-m', 'pip', '--disable-pip-version-check', 'wheel', '--no-deps', '-w', '/tmp/tmpWyNzjZ', '--quiet', '--find-links', 'h', '--find-links', 't', '--find-links', 't', '--find-links', 'p', '--find-links', 's', '--find-links', ':', '--find-links', '/', '--find-links', '/', '--find-links', 'e', '--find-links', 'x', '--find-links', 'a', '--find-links', 'm', '--find-links', 'p', '--find-links', 'l', '--find-links', 'e', '--find-links', '.', '--find-links', 'c', '--find-links', 'o', '--find-links', 'm', 'babel; extra == "i18n"']
```
It seems the assumption was that `find_links` is a list but it ends up being a string.

Additionally, since pip is unable to build/fetch a wheel, the following line throws an IndexError:
https://github.com/pypa/setuptools/blob/e84f616a6507ec9115fad68b221cbf5333d9d2d9/setuptools/installer.py#L122

Perhaps this could be caught and a more user-friendly error displayed instead.


Pull request description:
fix support for easy_install's find-links option in setup.cfg
###  Summary of changes

The easy_install's `find-links` option can sometime be a string (and not a list of string), e.g. when its value comes from `setup.cfg` (see handling in [easy_install own code](https://github.com/pypa/setuptools/blob/master/setuptools/command/easy_install.py#L345)).

Ensure it's similarly handled in `fetch_build_egg`.<!-- Summary goes here -->

Closes #1920, #1375, and probably #1880.

### Pull Request Checklist
- [x] Changes have tests
- [x] News fragment added in changelog.d. See [documentation](http://setuptools.readthedocs.io/en/latest/developer-guide.html#making-a-pull-request) for details


Patch:
diff --git a/changelog.d/1921.change.txt b/changelog.d/1921.change.txt
new file mode 100644
index 000000000..7c001eb8e
--- /dev/null
+++ b/changelog.d/1921.change.txt
@@ -0,0 +1,1 @@
+Fix support for easy_install's ``find-links`` option in ``setup.cfg``. 
diff --git a/setuptools/installer.py b/setuptools/installer.py
index 527b95ded..9f8be2ef8 100644
--- a/setuptools/installer.py
+++ b/setuptools/installer.py
@@ -7,11 +7,20 @@ from distutils.errors import DistutilsError
 
 import pkg_resources
 from setuptools.command.easy_install import easy_install
+from setuptools.extern import six
 from setuptools.wheel import Wheel
 
 from .py31compat import TemporaryDirectory
 
 
+def _fixup_find_links(find_links):
+    """Ensure find-links option end-up being a list of strings."""
+    if isinstance(find_links, six.string_types):
+        return find_links.split()
+    assert isinstance(find_links, (tuple, list))
+    return find_links
+
+
 def _legacy_fetch_build_egg(dist, req):
     """Fetch an egg needed for building.
 
@@ -31,7 +40,7 @@ def _legacy_fetch_build_egg(dist, req):
     if dist.dependency_links:
         links = dist.dependency_links[:]
         if 'find_links' in opts:
-            links = opts['find_links'][1] + links
+            links = _fixup_find_links(opts['find_links'][1]) + links
         opts['find_links'] = ('setup', links)
     install_dir = dist.get_egg_cache_dir()
     cmd = easy_install(
@@ -84,7 +93,7 @@ def fetch_build_egg(dist, req):
     else:
         index_url = None
     if 'find_links' in opts:
-        find_links = opts['find_links'][1][:]
+        find_links = _fixup_find_links(opts['find_links'][1])[:]
     else:
         find_links = []
     if dist.dependency_links:


Test patch:
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index f6da1b16c..2be1be47e 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -745,6 +745,44 @@ class TestSetupRequires:
         eggs = list(map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs'))))
         assert eggs == ['dep 1.0']
 
+    @pytest.mark.parametrize('use_legacy_installer,with_dependency_links_in_setup_py',
+                             itertools.product((False, True), (False, True)))
+    def test_setup_requires_with_find_links_in_setup_cfg(self, monkeypatch,
+                                                         use_legacy_installer,
+                                                         with_dependency_links_in_setup_py):
+        monkeypatch.setenv(str('PIP_RETRIES'), str('0'))
+        monkeypatch.setenv(str('PIP_TIMEOUT'), str('0'))
+        with contexts.save_pkg_resources_state():
+            with contexts.tempdir() as temp_dir:
+                make_trivial_sdist(os.path.join(temp_dir, 'python-xlib-42.tar.gz'), 'python-xlib', '42')
+                test_pkg = os.path.join(temp_dir, 'test_pkg')
+                test_setup_py = os.path.join(test_pkg, 'setup.py')
+                test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
+                os.mkdir(test_pkg)
+                with open(test_setup_py, 'w') as fp:
+                    if with_dependency_links_in_setup_py:
+                        dependency_links = [os.path.join(temp_dir, 'links')]
+                    else:
+                        dependency_links = []
+                    fp.write(DALS(
+                        '''
+                        from setuptools import installer, setup
+                        if {use_legacy_installer}:
+                            installer.fetch_build_egg = installer._legacy_fetch_build_egg
+                        setup(setup_requires='python-xlib==42',
+                        dependency_links={dependency_links!r})
+                        ''').format(use_legacy_installer=use_legacy_installer,
+                                    dependency_links=dependency_links))
+                with open(test_setup_cfg, 'w') as fp:
+                    fp.write(DALS(
+                        '''
+                        [easy_install]
+                        index_url = {index_url}
+                        find_links = {find_links}
+                        ''').format(index_url=os.path.join(temp_dir, 'index'),
+                                    find_links=temp_dir))
+                run_setup(test_setup_py, [str('--version')])
+
     def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch):
         # Use case: installing a package with a build dependency on
         # an already installed `dep[extra]`, which in turn depends