Repository: pypa/pip

Issue:
Improve error message for invalid requirements files
A `pip3 install` command is failing with esoteric error message. I am running as the root user on Ubuntu.

* pip version: pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6)
* Python version: Python 3.6.7
* OS: Ubuntu


I have this:

```Dockerfile
WORKDIR /apps/datavana/moodwire/mwapi
RUN mkdir -p /pth/to/downloaded/files
RUN mkdir -p /interos/temp
ARG commit

ADD https://bitbucket.org/interos/datavana/raw/"$commit"/moodwire/mwapi/requirements.txt  '/interos/temp/requirements.txt'
ADD https://bitbucket.org/interos/datavana/raw/"$commit"/moodwire/mwapi/constraints.txt '/interos/temp/constraints.txt'

# the ADD commands succeed

RUN pip3 install --cache-dir="/pth/to/downloaded/files" -r /interos/temp/requirements.txt -c /interos/temp/constraints.txt
```

I get this error:

```
Step 28/92 : RUN pip3 install --cache-dir="/pth/to/downloaded/files" -r /interos/temp/requirements.txt -c /interos/temp/constraints.txt
 ---> Running in ac48ca1a7f93
Invalid requirement: '<html>'
Traceback (most recent call last):
  File "/usr/share/python-wheels/packaging-17.1-py2.py3-none-any.whl/packaging/requirements.py", line 93, in __init__
    req = REQUIREMENT.parseString(requirement_string)
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1632, in parseString
    raise exc
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1622, in parseString
    loc, tokens = self._parse( instring, 0 )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1379, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 3395, in parseImpl
    loc, exprtokens = e._parse( instring, loc, doActions )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1379, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 3717, in parseImpl
    return self.expr._parse( instring, loc, doActions, callPreParse=False )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1379, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 3378, in parseImpl
    loc, resultlist = self.exprs[0]._parse( instring, loc, doActions, callPreParse=False )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1383, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 2689, in parseImpl
    raise ParseException(instring, loc, self.errmsg, self)
pyparsing.ParseException: Expected W:(abcd...) (at char 0), (line:1, col:1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/pip/req/req_install.py", line 82, in __init__
    req = Requirement(req)
  File "/usr/share/python-wheels/packaging-17.1-py2.py3-none-any.whl/packaging/requirements.py", line 97, in __init__
    requirement_string[e.loc:e.loc + 8]))
pip._vendor.packaging.requirements.InvalidRequirement: Invalid requirement, parse error at "'<html>'"

The command '/bin/sh -c pip3 install --cache-dir="/pth/to/downloaded/files" -r /interos/temp/requirements.txt -c /interos/temp/constraints.txt' returned a non-zero code: 1

```

the error message is pretty bad - no matter what's going on.

Discussion:

Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index fbccffdfd..4da3a9515 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -7,9 +7,6 @@ As a reminder, all contributors are expected to follow our [Code of Conduct][coc
 
 [coc]: https://www.pypa.io/en/latest/code-of-conduct/
 
-## Development Documentation
-
-Our [development documentation](https://pip.pypa.io/en/latest/development/) contains details on how to get started with contributing to pip, and details of our development processes.
 
 ## Bot Commands
 
diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst
index f0a5e075c..cc38f0bd7 100644
--- a/docs/html/reference/pip_install.rst
+++ b/docs/html/reference/pip_install.rst
@@ -153,7 +153,6 @@ The following options are supported:
   *  :ref:`--no-binary <install_--no-binary>`
   *  :ref:`--only-binary <install_--only-binary>`
   *  :ref:`--require-hashes <--require-hashes>`
-  *  :ref:`--trusted-host <--trusted-host>`
 
 For example, to specify :ref:`--no-index <--no-index>` and two
 :ref:`--find-links <--find-links>` locations:
diff --git a/news/3799.feature b/news/3799.feature
deleted file mode 100644
index e15c2a1da..000000000
--- a/news/3799.feature
+++ /dev/null
@@ -1,1 +0,0 @@
-Fully support using ``--trusted-host`` inside requirements files.
diff --git a/news/6527.bugfix b/news/6527.bugfix
new file mode 100644
index 000000000..92d29d9ff
--- /dev/null
+++ b/news/6527.bugfix
@@ -0,0 +1,2 @@
+Add the line number and file location to the error message when reading an
+invalid requirements file in certain situations.
diff --git a/news/6579.trivial b/news/6579.trivial
deleted file mode 100644
index 497dfaefd..000000000
--- a/news/6579.trivial
+++ /dev/null
@@ -1,1 +0,0 @@
-Link with developer documentation added in .github/CONTRIBUTING.md
\ No newline at end of file
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 4b766b918..dc00b9a1b 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -822,24 +822,6 @@ require_hashes = partial(
 )  # type: Callable[..., Option]
 
 
-list_path = partial(
-    Option,
-    '--path',
-    dest='path',
-    action='append',
-    help='Restrict to the specified installation path for listing '
-         'packages (can be used multiple times).'
-)  # type: Callable[..., Option]
-
-
-def check_list_path_option(options):
-    # type: (Values) -> None
-    if options.path and (options.user or options.local):
-        raise CommandError(
-            "Cannot combine '--path' with '--user' or '--local'"
-        )
-
-
 ##########
 # groups #
 ##########
diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py
index 9fc5b0469..47bd443a4 100644
--- a/src/pip/_internal/commands/freeze.py
+++ b/src/pip/_internal/commands/freeze.py
@@ -3,8 +3,8 @@ from __future__ import absolute_import
 import sys
 
 from pip._internal.cache import WheelCache
-from pip._internal.cli import cmdoptions
 from pip._internal.cli.base_command import Command
+from pip._internal.exceptions import CommandError
 from pip._internal.models.format_control import FormatControl
 from pip._internal.operations.freeze import freeze
 from pip._internal.utils.compat import stdlib_pkgs
@@ -57,7 +57,12 @@ class FreezeCommand(Command):
             action='store_true',
             default=False,
             help='Only output packages installed in user-site.')
-        self.cmd_opts.add_option(cmdoptions.list_path())
+        self.cmd_opts.add_option(
+            '--path',
+            dest='path',
+            action='append',
+            help='Restrict to the specified installation path for listing '
+                 'packages (can be used multiple times).')
         self.cmd_opts.add_option(
             '--all',
             dest='freeze_all',
@@ -79,7 +84,10 @@ class FreezeCommand(Command):
         if not options.freeze_all:
             skip.update(DEV_PKGS)
 
-        cmdoptions.check_list_path_option(options)
+        if options.path and (options.user or options.local):
+            raise CommandError(
+                "Cannot combine '--path' with '--user' or '--local'"
+            )
 
         freeze_kwargs = dict(
             requirement=options.requirements,
diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
index add31c0bf..2fbc31687 100644
--- a/src/pip/_internal/commands/list.py
+++ b/src/pip/_internal/commands/list.py
@@ -62,7 +62,12 @@ class ListCommand(Command):
             action='store_true',
             default=False,
             help='Only output packages installed in user-site.')
-        cmd_opts.add_option(cmdoptions.list_path())
+        self.cmd_opts.add_option(
+            '--path',
+            dest='path',
+            action='append',
+            help='Restrict to the specified installation path for listing '
+                 'packages (can be used multiple times).')
         cmd_opts.add_option(
             '--pre',
             action='store_true',
@@ -126,7 +131,10 @@ class ListCommand(Command):
             raise CommandError(
                 "Options --outdated and --uptodate cannot be combined.")
 
-        cmdoptions.check_list_path_option(options)
+        if options.path and (options.user or options.local):
+            raise CommandError(
+                "Cannot combine '--path' with '--user' or '--local'"
+            )
 
         packages = get_installed_distributions(
             local_only=options.local,
diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py
deleted file mode 100644
index 92ea47336..000000000
--- a/src/pip/_internal/distributions/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from pip._internal.distributions.source import SourceDistribution
-from pip._internal.distributions.wheel import WheelDistribution
-
-from pip._internal.utils.typing import MYPY_CHECK_RUNNING
-
-if MYPY_CHECK_RUNNING:
-    from pip._internal.distributions.base import AbstractDistribution
-    from pip._internal.req.req_install import InstallRequirement
-
-
-def make_abstract_dist(install_req):
-    # type: (InstallRequirement) -> AbstractDistribution
-    """Returns a Distribution for the given InstallRequirement
-    """
-    # If it's not an editable, is a wheel, it's a WheelDistribution
-    if install_req.editable:
-        return SourceDistribution(install_req)
-
-    if install_req.link and install_req.is_wheel:
-        return WheelDistribution(install_req)
-
-    # Otherwise, a SourceDistribution
-    return SourceDistribution(install_req)
diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py
deleted file mode 100644
index 710b98911..000000000
--- a/src/pip/_internal/distributions/base.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import abc
-
-from pip._vendor.six import add_metaclass
-
-
-@add_metaclass(abc.ABCMeta)
-class AbstractDistribution(object):
-    """A base class for handling installable artifacts.
-
-    The requirements for anything installable are as follows:
-
-     - we must be able to determine the requirement name
-       (or we can't correctly handle the non-upgrade case).
-
-     - for packages with setup requirements, we must also be able
-       to determine their requirements without installing additional
-       packages (for the same reason as run-time dependencies)
-
-     - we must be able to create a Distribution object exposing the
-       above metadata.
-    """
-
-    def __init__(self, req):
-        super(AbstractDistribution, self).__init__()
-        self.req = req
-
-    # TODO:  Give this a better name.
-    @abc.abstractmethod
-    def dist(self):
-        raise NotImplementedError()
-
-    # TODO:  Give this a better name.
-    @abc.abstractmethod
-    def prep_for_dist(self, finder, build_isolation):
-        raise NotImplementedError()
diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py
deleted file mode 100644
index c4dfaa565..000000000
--- a/src/pip/_internal/distributions/installed.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from pip._internal.distributions.base import AbstractDistribution
-
-
-class InstalledDistribution(AbstractDistribution):
-    """Represents an installed package.
-
-    This does not need any preparation as the required information has already
-    been computed.
-    """
-
-    def dist(self):
-        return self.req.satisfied_by
-
-    def prep_for_dist(self, finder, build_isolation):
-        pass
diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/source.py
deleted file mode 100644
index 4bec64a29..000000000
--- a/src/pip/_internal/distributions/source.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import logging
-
-from pip._internal.build_env import BuildEnvironment
-from pip._internal.distributions.base import AbstractDistribution
-from pip._internal.exceptions import InstallationError
-
-logger = logging.getLogger(__name__)
-
-
-class SourceDistribution(AbstractDistribution):
-    """Represents a source distribution.
-
-    The preparation step for these needs metadata for the packages to be
-    generated, either using PEP 517 or using the legacy `setup.py egg_info`.
-
-    NOTE from @pradyunsg (14 June 2019)
-    I expect SourceDistribution class will need to be split into
-    `legacy_source` (setup.py based) and `source` (PEP 517 based) when we start
-    bringing logic for preparation out of InstallRequirement into this class.
-    """
-
-    def dist(self):
-        return self.req.get_dist()
-
-    def prep_for_dist(self, finder, build_isolation):
-        # Prepare for building. We need to:
-        #   1. Load pyproject.toml (if it exists)
-        #   2. Set up the build environment
-
-        self.req.load_pyproject_toml()
-        should_isolate = self.req.use_pep517 and build_isolation
-
-        def _raise_conflicts(conflicting_with, conflicting_reqs):
-            raise InstallationError(
-                "Some build dependencies for %s conflict with %s: %s." % (
-                    self.req, conflicting_with, ', '.join(
-                        '%s is incompatible with %s' % (installed, wanted)
-                        for installed, wanted in sorted(conflicting))))
-
-        if should_isolate:
-            # Isolate in a BuildEnvironment and install the build-time
-            # requirements.
-            self.req.build_env = BuildEnvironment()
-            self.req.build_env.install_requirements(
-                finder, self.req.pyproject_requires, 'overlay',
-                "Installing build dependencies"
-            )
-            conflicting, missing = self.req.build_env.check_requirements(
-                self.req.requirements_to_check
-            )
-            if conflicting:
-                _raise_conflicts("PEP 517/518 supported requirements",
-                                 conflicting)
-            if missing:
-                logger.warning(
-                    "Missing build requirements in pyproject.toml for %s.",
-                    self.req,
-                )
-                logger.warning(
-                    "The project does not specify a build backend, and "
-                    "pip cannot fall back to setuptools without %s.",
-                    " and ".join(map(repr, sorted(missing)))
-                )
-            # Install any extra build dependencies that the backend requests.
-            # This must be done in a second pass, as the pyproject.toml
-            # dependencies must be installed before we can call the backend.
-            with self.req.build_env:
-                # We need to have the env active when calling the hook.
-                self.req.spin_message = "Getting requirements to build wheel"
-                reqs = self.req.pep517_backend.get_requires_for_build_wheel()
-            conflicting, missing = self.req.build_env.check_requirements(reqs)
-            if conflicting:
-                _raise_conflicts("the backend dependencies", conflicting)
-            self.req.build_env.install_requirements(
-                finder, missing, 'normal',
-                "Installing backend dependencies"
-            )
-
-        self.req.prepare_metadata()
-        self.req.assert_source_matches_version()
diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py
deleted file mode 100644
index 90883bc4d..000000000
--- a/src/pip/_internal/distributions/wheel.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from pip._vendor import pkg_resources
-
-from pip._internal.distributions.base import AbstractDistribution
-
-
-class WheelDistribution(AbstractDistribution):
-    """Represents a wheel distribution.
-
-    This does not need any preparation as wheels can be directly unpacked.
-    """
-
-    def dist(self):
-        return list(pkg_resources.find_distributions(
-                    self.req.source_dir))[0]
-
-    def prep_for_dist(self, finder, build_isolation):
-        pass
diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py
index 0aa692b15..6a54f8940 100644
--- a/src/pip/_internal/download.py
+++ b/src/pip/_internal/download.py
@@ -588,8 +588,6 @@ class PipSession(requests.Session):
         # well as any https:// host that we've marked as ignoring TLS errors
         # for.
         insecure_adapter = InsecureHTTPAdapter(max_retries=retries)
-        # Save this for later use in add_insecure_host().
-        self._insecure_adapter = insecure_adapter
 
         self.mount("https://", secure_adapter)
         self.mount("http://", insecure_adapter)
@@ -600,11 +598,7 @@ class PipSession(requests.Session):
         # We want to use a non-validating adapter for any requests which are
         # deemed insecure.
         for host in insecure_hosts:
-            self.add_insecure_host(host)
-
-    def add_insecure_host(self, host):
-        # type: (str) -> None
-        self.mount('https://{}/'.format(host), self._insecure_adapter)
+            self.mount("https://{}/".format(host), insecure_adapter)
 
     def request(self, method, url, *args, **kwargs):
         # Allow setting a default timeout on a session
diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py
index 1c8f1ecb1..c2a2415a2 100644
--- a/src/pip/_internal/index.py
+++ b/src/pip/_internal/index.py
@@ -608,8 +608,8 @@ class PackageFinder(object):
         # type: (...) -> PackageFinder
         """Create a PackageFinder.
 
-        :param trusted_hosts: Domains not to emit warnings for when not using
-            HTTPS.
+        :param trusted_hosts: Domains that we won't emit warnings for when
+            not using HTTPS.
         :param session: The Session to use to make requests.
         :param format_control: A FormatControl object or None. Used to control
             the selection of source packages / binary packages when consulting
@@ -676,23 +676,12 @@ class PackageFinder(object):
         # type: () -> None
         self.candidate_evaluator.allow_all_prereleases = True
 
-    def add_trusted_host(self, host, source=None):
-        # type: (str, Optional[str]) -> None
-        """
-        :param source: An optional source string, for logging where the host
-            string came from.
-        """
-        # It is okay to add a previously added host because PipSession stores
-        # the resulting prefixes in a dict.
-        msg = 'adding trusted host: {!r}'.format(host)
-        if source is not None:
-            msg += ' (from {})'.format(source)
-        logger.info(msg)
-        self.session.add_insecure_host(host)
-        if host in self.trusted_hosts:
-            return
-
-        self.trusted_hosts.append(host)
+    def extend_trusted_hosts(self, hosts):
+        # type: (List[str]) -> None
+        for host in hosts:
+            if host in self.trusted_hosts:
+                continue
+            self.trusted_hosts.append(host)
 
     def iter_secure_origins(self):
         # type: () -> Iterator[SecureOrigin]
diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py
index 08347b53e..1cc8531d1 100644
--- a/src/pip/_internal/legacy_resolve.py
+++ b/src/pip/_internal/legacy_resolve.py
@@ -34,14 +34,14 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 if MYPY_CHECK_RUNNING:
     from typing import DefaultDict, List, Optional, Set, Tuple
     from pip._vendor import pkg_resources
-
-    from pip._internal.cache import WheelCache
-    from pip._internal.distributions import AbstractDistribution
     from pip._internal.download import PipSession
-    from pip._internal.index import PackageFinder
-    from pip._internal.operations.prepare import RequirementPreparer
     from pip._internal.req.req_install import InstallRequirement
+    from pip._internal.index import PackageFinder
     from pip._internal.req.req_set import RequirementSet
+    from pip._internal.operations.prepare import (
+        DistAbstraction, RequirementPreparer
+    )
+    from pip._internal.cache import WheelCache
 
 logger = logging.getLogger(__name__)
 
@@ -277,7 +277,7 @@ class Resolver(object):
         return None
 
     def _get_abstract_dist_for(self, req):
-        # type: (InstallRequirement) -> AbstractDistribution
+        # type: (InstallRequirement) -> DistAbstraction
         """Takes a InstallRequirement and returns a single AbstractDist \
         representing a prepared variant of the same.
         """
diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py
index f8653c894..920df5d48 100644
--- a/src/pip/_internal/operations/check.py
+++ b/src/pip/_internal/operations/check.py
@@ -7,7 +7,7 @@ from collections import namedtuple
 from pip._vendor.packaging.utils import canonicalize_name
 from pip._vendor.pkg_resources import RequirementParseError
 
-from pip._internal.distributions import make_abstract_dist
+from pip._internal.operations.prepare import make_abstract_dist
 from pip._internal.utils.misc import get_installed_distributions
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index 3a4a70699..077a985ae 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -4,10 +4,9 @@
 import logging
 import os
 
-from pip._vendor import requests
+from pip._vendor import pkg_resources, requests
 
-from pip._internal.distributions import make_abstract_dist
-from pip._internal.distributions.installed import InstalledDistribution
+from pip._internal.build_env import BuildEnvironment
 from pip._internal.download import (
     is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path,
 )
@@ -22,17 +21,168 @@ from pip._internal.utils.misc import display_path, normalize_path
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
 if MYPY_CHECK_RUNNING:
-    from typing import Optional
-
-    from pip._internal.distributions import AbstractDistribution
-    from pip._internal.download import PipSession
-    from pip._internal.index import PackageFinder
+    from typing import Any, Optional
     from pip._internal.req.req_install import InstallRequirement
+    from pip._internal.index import PackageFinder
+    from pip._internal.download import PipSession
     from pip._internal.req.req_tracker import RequirementTracker
 
 logger = logging.getLogger(__name__)
 
 
+def make_abstract_dist(req):
+    # type: (InstallRequirement) -> DistAbstraction
+    """Factory to make an abstract dist object.
+
+    Preconditions: Either an editable req with a source_dir, or satisfied_by or
+    a wheel link, or a non-editable req with a source_dir.
+
+    :return: A concrete DistAbstraction.
+    """
+    if req.editable:
+        return IsSDist(req)
+    elif req.link and req.link.is_wheel:
+        return IsWheel(req)
+    else:
+        return IsSDist(req)
+
+
+class DistAbstraction(object):
+    """Abstracts out the wheel vs non-wheel Resolver.resolve() logic.
+
+    The requirements for anything installable are as follows:
+     - we must be able to determine the requirement name
+       (or we can't correctly handle the non-upgrade case).
+     - we must be able to generate a list of run-time dependencies
+       without installing any additional packages (or we would
+       have to either burn time by doing temporary isolated installs
+       or alternatively violate pips 'don't start installing unless
+       all requirements are available' rule - neither of which are
+       desirable).
+     - for packages with setup requirements, we must also be able
+       to determine their requirements without installing additional
+       packages (for the same reason as run-time dependencies)
+     - we must be able to create a Distribution object exposing the
+       above metadata.
+    """
+
+    def __init__(self, req):
+        # type: (InstallRequirement) -> None
+        self.req = req  # type: InstallRequirement
+
+    def dist(self):
+        # type: () -> Any
+        """Return a setuptools Dist object."""
+        raise NotImplementedError
+
+    def prep_for_dist(self, finder, build_isolation):
+        # type: (PackageFinder, bool) -> Any
+        """Ensure that we can get a Dist for this requirement."""
+        raise NotImplementedError
+
+
+class IsWheel(DistAbstraction):
+
+    def dist(self):
+        # type: () -> pkg_resources.Distribution
+        return list(pkg_resources.find_distributions(
+            self.req.source_dir))[0]
+
+    def prep_for_dist(self, finder, build_isolation):
+        # type: (PackageFinder, bool) -> Any
+        # FIXME:https://github.com/pypa/pip/issues/1112
+        pass
+
+
+class IsSDist(DistAbstraction):
+
+    def dist(self):
+        return self.req.get_dist()
+
+    def _raise_conflicts(self, conflicting_with, conflicting_reqs):
+        conflict_messages = [
+            '%s is incompatible with %s' % (installed, wanted)
+            for installed, wanted in sorted(conflicting_reqs)
+        ]
+        raise InstallationError(
+            "Some build dependencies for %s conflict with %s: %s." % (
+                self.req, conflicting_with, ', '.join(conflict_messages))
+        )
+
+    def install_backend_dependencies(self, finder):
+        # type: (PackageFinder) -> None
+        """
+        Install any extra build dependencies that the backend requests.
+
+        :param finder: a PackageFinder object.
+        """
+        req = self.req
+        with req.build_env:
+            # We need to have the env active when calling the hook.
+            req.spin_message = "Getting requirements to build wheel"
+            reqs = req.pep517_backend.get_requires_for_build_wheel()
+        conflicting, missing = req.build_env.check_requirements(reqs)
+        if conflicting:
+            self._raise_conflicts("the backend dependencies", conflicting)
+        req.build_env.install_requirements(
+            finder, missing, 'normal',
+            "Installing backend dependencies"
+        )
+
+    def prep_for_dist(self, finder, build_isolation):
+        # type: (PackageFinder, bool) -> None
+        # Prepare for building. We need to:
+        #   1. Load pyproject.toml (if it exists)
+        #   2. Set up the build environment
+
+        self.req.load_pyproject_toml()
+        should_isolate = self.req.use_pep517 and build_isolation
+
+        if should_isolate:
+            # Isolate in a BuildEnvironment and install the build-time
+            # requirements.
+            self.req.build_env = BuildEnvironment()
+            self.req.build_env.install_requirements(
+                finder, self.req.pyproject_requires, 'overlay',
+                "Installing build dependencies"
+            )
+            conflicting, missing = self.req.build_env.check_requirements(
+                self.req.requirements_to_check
+            )
+            if conflicting:
+                self._raise_conflicts("PEP 517/518 supported requirements",
+                                      conflicting)
+            if missing:
+                logger.warning(
+                    "Missing build requirements in pyproject.toml for %s.",
+                    self.req,
+                )
+                logger.warning(
+                    "The project does not specify a build backend, and "
+                    "pip cannot fall back to setuptools without %s.",
+                    " and ".join(map(repr, sorted(missing)))
+                )
+
+            # Install any extra build dependencies that the backend requests.
+            # This must be done in a second pass, as the pyproject.toml
+            # dependencies must be installed before we can call the backend.
+            self.install_backend_dependencies(finder=finder)
+
+        self.req.prepare_metadata()
+        self.req.assert_source_matches_version()
+
+
+class Installed(DistAbstraction):
+
+    def dist(self):
+        # type: () -> pkg_resources.Distribution
+        return self.req.satisfied_by
+
+    def prep_for_dist(self, finder, build_isolation):
+        # type: (PackageFinder, bool) -> Any
+        pass
+
+
 class RequirementPreparer(object):
     """Prepares a Requirement
     """
@@ -98,7 +248,7 @@ class RequirementPreparer(object):
         upgrade_allowed,  # type: bool
         require_hashes  # type: bool
     ):
-        # type: (...) -> AbstractDistribution
+        # type: (...) -> DistAbstraction
         """Prepare a requirement that would be obtained from req.link
         """
         # TODO: Breakup into smaller functions
@@ -223,7 +373,7 @@ class RequirementPreparer(object):
         use_user_site,  # type: bool
         finder  # type: PackageFinder
     ):
-        # type: (...) -> AbstractDistribution
+        # type: (...) -> DistAbstraction
         """Prepare an editable requirement
         """
         assert req.editable, "cannot prepare a non-editable req as editable"
@@ -250,13 +400,8 @@ class RequirementPreparer(object):
 
         return abstract_dist
 
-    def prepare_installed_requirement(
-        self,
-        req,  # type: InstallRequirement
-        require_hashes,  # type: bool
-        skip_reason  # type: str
-    ):
-        # type: (...) -> AbstractDistribution
+    def prepare_installed_requirement(self, req, require_hashes, skip_reason):
+        # type: (InstallRequirement, bool, Optional[str]) -> DistAbstraction
         """Prepare an already-installed requirement
         """
         assert req.satisfied_by, "req should have been satisfied but isn't"
@@ -276,6 +421,6 @@ class RequirementPreparer(object):
                     'completely repeatable environment, install into an '
                     'empty virtualenv.'
                 )
-            abstract_dist = InstalledDistribution(req)
+            abstract_dist = Installed(req)
 
         return abstract_dist
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index 99df626b7..d0b0c7153 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -205,11 +205,15 @@ def install_req_from_line(
     isolated=False,  # type: bool
     options=None,  # type: Optional[Dict[str, Any]]
     wheel_cache=None,  # type: Optional[WheelCache]
-    constraint=False  # type: bool
+    constraint=False,  # type: bool
+    line_source=None,  # type: Optional[str]
 ):
     # type: (...) -> InstallRequirement
     """Creates an InstallRequirement from a name, which might be a
     requirement, directory containing 'setup.py', filename, or URL.
+
+    :param line_source: An optional string describing where the line is from,
+        for logging purposes in case of an error.
     """
     if is_url(name):
         marker_sep = '; '
@@ -289,10 +293,17 @@ def install_req_from_line(
                   not any(op in req_as_string for op in operators)):
                 add_msg = "= is not a valid operator. Did you mean == ?"
             else:
-                add_msg = ""
-            raise InstallationError(
-                "Invalid requirement: '%s'\n%s" % (req_as_string, add_msg)
+                add_msg = ''
+            if line_source is None:
+                source = ''
+            else:
+                source = ' (from {})'.format(line_source)
+            msg = (
+                'Invalid requirement: {!r}{}'.format(req_as_string, source)
             )
+            if add_msg:
+                msg += '\nHint: {}'.format(add_msg)
+            raise InstallationError(msg)
     else:
         req = None
 
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index ee62171da..41fdd362f 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -138,7 +138,7 @@ def process_line(
     session=None,  # type: Optional[PipSession]
     wheel_cache=None,  # type: Optional[WheelCache]
     use_pep517=None,  # type: Optional[bool]
-    constraint=False  # type: bool
+    constraint=False,  # type: bool
 ):
     # type: (...) -> Iterator[InstallRequirement]
     """Process a single requirements line; This can result in creating/yielding
@@ -187,10 +187,16 @@ def process_line(
         for dest in SUPPORTED_OPTIONS_REQ_DEST:
             if dest in opts.__dict__ and opts.__dict__[dest]:
                 req_options[dest] = opts.__dict__[dest]
+        line_source = 'line {} of {}'.format(line_number, filename)
         yield install_req_from_line(
-            args_str, line_comes_from, constraint=constraint,
+            args_str,
+            comes_from=line_comes_from,
             use_pep517=use_pep517,
-            isolated=isolated, options=req_options, wheel_cache=wheel_cache
+            isolated=isolated,
+            options=req_options,
+            wheel_cache=wheel_cache,
+            constraint=constraint,
+            line_source=line_source,
         )
 
     # yield an editable requirement
@@ -250,9 +256,8 @@ def process_line(
             finder.find_links.append(value)
         if opts.pre:
             finder.set_allow_all_prereleases()
-        for host in opts.trusted_hosts or []:
-            source = 'line {} of {}'.format(line_number, filename)
-            finder.add_trusted_host(host, source=source)
+        if opts.trusted_hosts:
+            finder.extend_trusted_hosts(opts.trusted_hosts)
 
 
 def break_args_options(line):


Test patch:
diff --git a/tests/functional/test_install_vcs_svn.py b/tests/functional/test_install_vcs_svn.py
new file mode 100644
index 000000000..3034b0625
--- /dev/null
+++ b/tests/functional/test_install_vcs_svn.py
@@ -0,0 +1,66 @@
+from unittest import TestCase
+
+from mock import patch
+
+from pip._internal.vcs.subversion import Subversion
+from pip._internal.vcs.versioncontrol import RevOptions
+
+
+class TestSubversionArgs(TestCase):
+    def setUp(self):
+        patcher = patch('pip._internal.vcs.versioncontrol.call_subprocess')
+        self.addCleanup(patcher.stop)
+        self.call_subprocess_mock = patcher.start()
+
+        # Test Data.
+        self.url = 'svn+http://username:password@svn.example.com/'
+        # use_interactive is set to False to test that remote call options are
+        # properly added.
+        self.svn = Subversion(use_interactive=False)
+        self.rev_options = RevOptions(Subversion)
+        self.dest = '/tmp/test'
+
+    def assert_call_args(self, args):
+        assert self.call_subprocess_mock.call_args[0][0] == args
+
+    def test_obtain(self):
+        self.svn.obtain(self.dest, self.url)
+        self.assert_call_args(
+            ['svn', 'checkout', '-q', '--non-interactive', '--username',
+             'username', '--password', 'password',
+             'http://svn.example.com/', '/tmp/test'])
+
+    def test_export(self):
+        self.svn.export(self.dest, self.url)
+        self.assert_call_args(
+            ['svn', 'export', '--non-interactive', '--username', 'username',
+             '--password', 'password', 'http://svn.example.com/',
+             '/tmp/test'])
+
+    def test_fetch_new(self):
+        self.svn.fetch_new(self.dest, self.url, self.rev_options)
+        self.assert_call_args(
+            ['svn', 'checkout', '-q', '--non-interactive',
+             'svn+http://username:password@svn.example.com/',
+             '/tmp/test'])
+
+    def test_fetch_new_revision(self):
+        rev_options = RevOptions(Subversion, '123')
+        self.svn.fetch_new(self.dest, self.url, rev_options)
+        self.assert_call_args(
+            ['svn', 'checkout', '-q', '--non-interactive',
+             '-r', '123',
+             'svn+http://username:password@svn.example.com/',
+             '/tmp/test'])
+
+    def test_switch(self):
+        self.svn.switch(self.dest, self.url, self.rev_options)
+        self.assert_call_args(
+            ['svn', 'switch', '--non-interactive',
+             'svn+http://username:password@svn.example.com/',
+             '/tmp/test'])
+
+    def test_update(self):
+        self.svn.update(self.dest, self.url, self.rev_options)
+        self.assert_call_args(
+            ['svn', 'update', '--non-interactive', '/tmp/test'])
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 4ffc6475e..c7dc1715a 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -163,60 +163,15 @@ class TestPackageFinder:
         assert actual_target_python is target_python
         assert actual_target_python.py_version_info == (3, 7, 3)
 
-    def test_add_trusted_host(self):
-        # Leave a gap to test how the ordering is affected.
-        trusted_hosts = ['host1', 'host3']
-        session = PipSession(insecure_hosts=trusted_hosts)
-        finder = make_test_finder(
-            session=session,
-            trusted_hosts=trusted_hosts,
-        )
-        insecure_adapter = session._insecure_adapter
-        prefix2 = 'https://host2/'
-        prefix3 = 'https://host3/'
-
-        # Confirm some initial conditions as a baseline.
-        assert finder.trusted_hosts == ['host1', 'host3']
-        assert session.adapters[prefix3] is insecure_adapter
-        assert prefix2 not in session.adapters
-
-        # Test adding a new host.
-        finder.add_trusted_host('host2')
-        assert finder.trusted_hosts == ['host1', 'host3', 'host2']
-        # Check that prefix3 is still present.
-        assert session.adapters[prefix3] is insecure_adapter
-        assert session.adapters[prefix2] is insecure_adapter
-
-        # Test that adding the same host doesn't create a duplicate.
-        finder.add_trusted_host('host3')
-        assert finder.trusted_hosts == ['host1', 'host3', 'host2'], (
-            'actual: {}'.format(finder.trusted_hosts)
-        )
+    def test_extend_trusted_hosts(self):
+        trusted_hosts = ['host1', 'host2']
+        finder = make_test_finder(trusted_hosts=trusted_hosts)
 
-    def test_add_trusted_host__logging(self, caplog):
-        """
-        Test logging when add_trusted_host() is called.
-        """
-        trusted_hosts = ['host1']
-        session = PipSession(insecure_hosts=trusted_hosts)
-        finder = make_test_finder(
-            session=session,
-            trusted_hosts=trusted_hosts,
+        # Check that extend_trusted_hosts() prevents duplicates.
+        finder.extend_trusted_hosts(['host2', 'host3', 'host2'])
+        assert finder.trusted_hosts == ['host1', 'host2', 'host3'], (
+            'actual: {}'.format(finder.trusted_hosts)
         )
-        with caplog.at_level(logging.INFO):
-            # Test adding an existing host.
-            finder.add_trusted_host('host1', source='somewhere')
-            finder.add_trusted_host('host2')
-            # Test calling add_trusted_host() on the same host twice.
-            finder.add_trusted_host('host2')
-
-        actual = [(r.levelname, r.message) for r in caplog.records]
-        expected = [
-            ('INFO', "adding trusted host: 'host1' (from somewhere)"),
-            ('INFO', "adding trusted host: 'host2'"),
-            ('INFO', "adding trusted host: 'host2'"),
-        ]
-        assert actual == expected
 
     def test_iter_secure_origins(self):
         trusted_hosts = ['host1', 'host2']
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index 3f8c38ce5..a7f830475 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -530,7 +530,7 @@ class TestInstallRequirement(object):
         with pytest.raises(InstallationError) as e:
             install_req_from_line(test_name)
         err_msg = e.value.args[0]
-        assert ("Invalid requirement: '%s'\n" % test_name) == err_msg
+        assert "Invalid requirement: '{}'".format(test_name) == err_msg
 
     def test_requirement_file(self):
         req_file_path = os.path.join(self.tempdir, 'test.txt')
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 972d84684..2cd3e6a47 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -1,4 +1,3 @@
-import logging
 import os
 import subprocess
 import textwrap
@@ -191,6 +190,26 @@ class TestProcessLine(object):
         with pytest.raises(InstallationError):
             list(process_line("req1 req2", "file", 1))
 
+    def test_error_message(self):
+        """
+        Test the error message if a parsing error occurs (all of path,
+        line number, and hint).
+        """
+        iterator = process_line(
+            'my-package=1.0',
+            filename='path/requirements.txt',
+            line_number=3
+        )
+        with pytest.raises(InstallationError) as exc:
+            list(iterator)
+
+        expected = (
+            "Invalid requirement: 'my-package=1.0' "
+            '(from line 3 of path/requirements.txt)\n'
+            'Hint: = is not a valid operator. Did you mean == ?'
+        )
+        assert str(exc.value) == expected
+
     def test_yield_line_requirement(self):
         line = 'SomeProject'
         filename = 'filename'
@@ -315,21 +334,9 @@ class TestProcessLine(object):
         list(process_line("--extra-index-url=url", "file", 1, finder=finder))
         assert finder.index_urls == ['url']
 
-    def test_set_finder_trusted_host(self, caplog, finder):
-        with caplog.at_level(logging.INFO):
-            list(process_line(
-                "--trusted-host=host", "file.txt", 1, finder=finder,
-            ))
-        assert finder.trusted_hosts == ['host']
-        session = finder.session
-        assert session.adapters['https://host/'] is session._insecure_adapter
-
-        # Test the log message.
-        actual = [(r.levelname, r.message) for r in caplog.records]
-        expected = [
-            ('INFO', "adding trusted host: 'host' (from line 1 of file.txt)"),
-        ]
-        assert actual == expected
+    def test_set_finder_trusted_host(self, finder):
+        list(process_line("--trusted-host=url", "file", 1, finder=finder))
+        assert finder.trusted_hosts == ['url']
 
     def test_noop_always_unzip(self, finder):
         # noop, but confirm it can be set
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 36e80326e..95bce95ec 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -1,5 +1,4 @@
 import os
-from unittest import TestCase
 
 import pytest
 from mock import patch
@@ -495,63 +494,3 @@ def test_subversion__get_remote_call_options(
     svn = Subversion(use_interactive=use_interactive)
     svn._vcs_version = vcs_version
     assert svn.get_remote_call_options() == expected_options
-
-
-class TestSubversionArgs(TestCase):
-    def setUp(self):
-        patcher = patch('pip._internal.vcs.versioncontrol.call_subprocess')
-        self.addCleanup(patcher.stop)
-        self.call_subprocess_mock = patcher.start()
-
-        # Test Data.
-        self.url = 'svn+http://username:password@svn.example.com/'
-        # use_interactive is set to False to test that remote call options are
-        # properly added.
-        self.svn = Subversion(use_interactive=False)
-        self.rev_options = RevOptions(Subversion)
-        self.dest = '/tmp/test'
-
-    def assert_call_args(self, args):
-        assert self.call_subprocess_mock.call_args[0][0] == args
-
-    def test_obtain(self):
-        self.svn.obtain(self.dest, self.url)
-        self.assert_call_args(
-            ['svn', 'checkout', '-q', '--non-interactive', '--username',
-             'username', '--password', 'password',
-             'http://svn.example.com/', '/tmp/test'])
-
-    def test_export(self):
-        self.svn.export(self.dest, self.url)
-        self.assert_call_args(
-            ['svn', 'export', '--non-interactive', '--username', 'username',
-             '--password', 'password', 'http://svn.example.com/',
-             '/tmp/test'])
-
-    def test_fetch_new(self):
-        self.svn.fetch_new(self.dest, self.url, self.rev_options)
-        self.assert_call_args(
-            ['svn', 'checkout', '-q', '--non-interactive',
-             'svn+http://username:password@svn.example.com/',
-             '/tmp/test'])
-
-    def test_fetch_new_revision(self):
-        rev_options = RevOptions(Subversion, '123')
-        self.svn.fetch_new(self.dest, self.url, rev_options)
-        self.assert_call_args(
-            ['svn', 'checkout', '-q', '--non-interactive',
-             '-r', '123',
-             'svn+http://username:password@svn.example.com/',
-             '/tmp/test'])
-
-    def test_switch(self):
-        self.svn.switch(self.dest, self.url, self.rev_options)
-        self.assert_call_args(
-            ['svn', 'switch', '--non-interactive',
-             'svn+http://username:password@svn.example.com/',
-             '/tmp/test'])
-
-    def test_update(self):
-        self.svn.update(self.dest, self.url, self.rev_options)
-        self.assert_call_args(
-            ['svn', 'update', '--non-interactive', '/tmp/test'])