Repository: pypa/pip

Issue:
Update pip's deprecation helpers to mention version
> The deprecation messages should've mentioned the release in which the functionality will be removed.

I have a PR in the works; I'll post it soon.

_Originally posted by @pradyunsg in https://github.com/pypa/pip/issues/6162#issuecomment-456662154_

Discussion:

Patch:
diff --git a/news/5082.feature b/news/5082.feature
deleted file mode 100644
index 17c678764..000000000
--- a/news/5082.feature
+++ /dev/null
@@ -1,2 +0,0 @@
-Improve the error message when ``METADATA`` or ``PKG-INFO`` is None when
-accessing metadata.
diff --git a/news/6121.feature b/news/6121.feature
deleted file mode 100644
index afe1f3857..000000000
--- a/news/6121.feature
+++ /dev/null
@@ -1,2 +0,0 @@
-Include the wheel's tags in the log message explanation when a candidate
-wheel link is found incompatible.
diff --git a/news/6386.bugfix b/news/6386.bugfix
deleted file mode 100644
index 366ec2862..000000000
--- a/news/6386.bugfix
+++ /dev/null
@@ -1,2 +0,0 @@
-Extend to Subversion 1.8+ the behavior of calling Subversion in
-interactive mode when pip is run interactively.
diff --git a/news/6513.bugfix b/news/6513.bugfix
deleted file mode 100644
index 0e1757ae7..000000000
--- a/news/6513.bugfix
+++ /dev/null
@@ -1,2 +0,0 @@
-Include more details in the log message if ``pip freeze`` can't generate a
-requirement string for a particular distribution.
diff --git a/news/6549.feature b/news/6549.feature
new file mode 100644
index 000000000..d4970e39a
--- /dev/null
+++ b/news/6549.feature
@@ -0,0 +1,1 @@
+Improve deprecation messages to include the version in which the functionality will be removed.
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index 096adcd6c..7b291a1e4 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -9,7 +9,6 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
 if MYPY_CHECK_RUNNING:
     from typing import Optional
-    from pip._vendor.pkg_resources import Distribution
     from pip._internal.req.req_install import InstallRequirement
 
 
@@ -29,36 +28,6 @@ class UninstallationError(PipError):
     """General exception during uninstallation"""
 
 
-class NoneMetadataError(PipError):
-    """
-    Raised when accessing "METADATA" or "PKG-INFO" metadata for a
-    pip._vendor.pkg_resources.Distribution object and
-    `dist.has_metadata('METADATA')` returns True but
-    `dist.get_metadata('METADATA')` returns None (and similarly for
-    "PKG-INFO").
-    """
-
-    def __init__(self, dist, metadata_name):
-        # type: (Distribution, str) -> None
-        """
-        :param dist: A Distribution object.
-        :param metadata_name: The name of the metadata being accessed
-            (can be "METADATA" or "PKG-INFO").
-        """
-        self.dist = dist
-        self.metadata_name = metadata_name
-
-    def __str__(self):
-        # type: () -> str
-        # Use `dist` in the error message because its stringification
-        # includes more information, like the version and location.
-        return (
-            'None {} metadata found for distribution: {}'.format(
-                self.metadata_name, self.dist,
-            )
-        )
-
-
 class DistributionNotFound(InstallationError):
     """Raised when a distribution cannot be found to satisfy a requirement"""
 
diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py
index 5b42a7c72..62845b657 100644
--- a/src/pip/_internal/index.py
+++ b/src/pip/_internal/index.py
@@ -384,15 +384,7 @@ class CandidateEvaluator(object):
                     return (False, reason)
 
                 if not self._is_wheel_supported(wheel):
-                    # Include the wheel's tags in the reason string to
-                    # simplify troubleshooting compatibility issues.
-                    file_tags = wheel.get_formatted_file_tags()
-                    reason = (
-                        "none of the wheel's tags match: {}".format(
-                            ', '.join(file_tags)
-                        )
-                    )
-                    return (False, reason)
+                    return (False, 'it is not compatible with this Python')
 
                 version = wheel.version
 
@@ -1074,9 +1066,7 @@ class PackageFinder(object):
     def _log_skipped_link(self, link, reason):
         # type: (Link, str) -> None
         if link not in self._logged_links:
-            # Put the link at the end so the reason is more visible and
-            # because the link string is usually very long.
-            logger.debug('Skipping link: %s: %s', reason, link)
+            logger.debug('Skipping link %s; %s', link, reason)
             self._logged_links.add(link)
 
     def get_install_candidate(self, link, search):
diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py
index 03e8696bb..c79a785c5 100644
--- a/src/pip/_internal/operations/freeze.py
+++ b/src/pip/_internal/operations/freeze.py
@@ -60,14 +60,10 @@ def freeze(
                                             user_only=user_only):
         try:
             req = FrozenRequirement.from_dist(dist)
-        except RequirementParseError as exc:
-            # We include dist rather than dist.project_name because the
-            # dist string includes more information, like the version and
-            # location. We also include the exception message to aid
-            # troubleshooting.
+        except RequirementParseError:
             logger.warning(
-                'Could not generate requirement for distribution %r: %s',
-                dist, exc
+                "Could not parse requirement: %s",
+                dist.project_name
             )
             continue
         if exclude_editable and req.editable:
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index 8c896f8ce..b9359bddc 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -78,16 +78,23 @@ def deprecated(reason, replacement, gone_in, issue=None):
     """
 
     # Construct a nice message.
-    # This is purposely eagerly formatted as we want it to appear as if someone
-    # typed this entire message out.
-    message = DEPRECATION_MSG_PREFIX + reason
-    if replacement is not None:
-        message += " A possible replacement is {}.".format(replacement)
-    if issue is not None:
-        url = "https://github.com/pypa/pip/issues/" + str(issue)
-        message += " You can find discussion regarding this at {}.".format(url)
+    #   This is eagerly formatted as we want it to get logged as if someone
+    #   typed this entire message out.
+    sentences = [
+        (reason, DEPRECATION_MSG_PREFIX + "{}"),
+        (gone_in, "pip {} will remove support for this functionality."),
+        (replacement, "A possible replacement is {}."),
+        (issue, (
+            "You can find discussion regarding this at "
+            "https://github.com/pypa/pip/issues/{}."
+        )),
+    ]
+    message = " ".join(
+        template.format(val) for val, template in sentences if val is not None
+    )
 
     # Raise as an error if it has to be removed.
     if gone_in is not None and parse(current_version) >= parse(gone_in):
         raise PipDeprecationWarning(message)
+
     warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py
index 2d80920df..ac27e5600 100644
--- a/src/pip/_internal/utils/packaging.py
+++ b/src/pip/_internal/utils/packaging.py
@@ -6,7 +6,6 @@ from email.parser import FeedParser
 from pip._vendor import pkg_resources
 from pip._vendor.packaging import specifiers, version
 
-from pip._internal.exceptions import NoneMetadataError
 from pip._internal.utils.misc import display_path
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
@@ -44,27 +43,16 @@ def check_requires_python(requires_python, version_info):
 
 def get_metadata(dist):
     # type: (Distribution) -> Message
-    """
-    :raises NoneMetadataError: if the distribution reports `has_metadata()`
-        True but `get_metadata()` returns None.
-    """
-    metadata_name = 'METADATA'
     if (isinstance(dist, pkg_resources.DistInfoDistribution) and
-            dist.has_metadata(metadata_name)):
-        metadata = dist.get_metadata(metadata_name)
+            dist.has_metadata('METADATA')):
+        metadata = dist.get_metadata('METADATA')
     elif dist.has_metadata('PKG-INFO'):
-        metadata_name = 'PKG-INFO'
-        metadata = dist.get_metadata(metadata_name)
+        metadata = dist.get_metadata('PKG-INFO')
     else:
         logger.warning("No metadata found in %s", display_path(dist.location))
         metadata = ''
 
-    if metadata is None:
-        raise NoneMetadataError(dist, metadata_name)
-
     feed_parser = FeedParser()
-    # The following line errors out if with a "NoneType" TypeError if
-    # passed metadata=None.
     feed_parser.feed(metadata)
     return feed_parser.close()
 
diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py
index 0d787009f..7ab03a277 100644
--- a/src/pip/_internal/vcs/subversion.py
+++ b/src/pip/_internal/vcs/subversion.py
@@ -20,7 +20,6 @@ _svn_info_xml_url_re = re.compile(r'<url>(.*)</url>')
 
 if MYPY_CHECK_RUNNING:
     from typing import List, Optional, Tuple
-    from pip._internal.vcs import RevOptions
 
 logger = logging.getLogger(__name__)
 
@@ -49,12 +48,10 @@ class Subversion(VersionControl):
                 # Subversion doesn't like to check out over an existing
                 # directory --force fixes this, but was only added in svn 1.5
                 rmtree(location)
-            cmd_args = (['export'] + self.get_remote_call_options() +
-                        rev_options.to_args() + [url, location])
+            cmd_args = ['export'] + rev_options.to_args() + [url, location]
             self.run_command(cmd_args, show_stdout=False)
 
     def fetch_new(self, dest, url, rev_options):
-        # type: (str, str, RevOptions) -> None
         rev_display = rev_options.to_display()
         logger.info(
             'Checking out %s%s to %s',
@@ -62,21 +59,15 @@ class Subversion(VersionControl):
             rev_display,
             display_path(dest),
         )
-        cmd_args = (['checkout', '-q'] +
-                    self.get_remote_call_options() +
-                    rev_options.to_args() + [url, dest])
+        cmd_args = ['checkout', '-q'] + rev_options.to_args() + [url, dest]
         self.run_command(cmd_args)
 
     def switch(self, dest, url, rev_options):
-        # type: (str, str, RevOptions) -> None
-        cmd_args = (['switch'] + self.get_remote_call_options() +
-                    rev_options.to_args() + [url, dest])
+        cmd_args = ['switch'] + rev_options.to_args() + [url, dest]
         self.run_command(cmd_args)
 
     def update(self, dest, url, rev_options):
-        # type: (str, str, RevOptions) -> None
-        cmd_args = (['update'] + self.get_remote_call_options() +
-                    rev_options.to_args() + [dest])
+        cmd_args = ['update'] + rev_options.to_args() + [dest]
         self.run_command(cmd_args)
 
     @classmethod
@@ -186,11 +177,6 @@ class Subversion(VersionControl):
         else:
             try:
                 # subversion >= 1.7
-                # Note that using get_remote_call_options is not necessary here
-                # because `svn info` is being run against a local directory.
-                # We don't need to worry about making sure interactive mode
-                # is being used to prompt for passwords, because passwords
-                # are only potentially needed for remote server requests.
                 xml = cls.run_command(
                     ['info', '--xml', location],
                     show_stdout=False,
diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py
index 970dadf8f..18f25df4a 100644
--- a/src/pip/_internal/wheel.py
+++ b/src/pip/_internal/wheel.py
@@ -663,16 +663,6 @@ def check_compatibility(version, name):
         )
 
 
-def format_tag(file_tag):
-    # type: (Tuple[str, ...]) -> str
-    """
-    Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>".
-
-    :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag).
-    """
-    return '-'.join(file_tag)
-
-
 class Wheel(object):
     """A wheel file"""
 
@@ -712,13 +702,6 @@ class Wheel(object):
             for y in self.abis for z in self.plats
         }
 
-    def get_formatted_file_tags(self):
-        # type: () -> List[str]
-        """
-        Return the wheel's tags as a sorted list of strings.
-        """
-        return sorted(format_tag(tag) for tag in self.file_tags)
-
     def support_index_min(self, tags=None):
         # type: (Optional[List[Pep425Tag]]) -> Optional[int]
         """


Test patch:
diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py
index 68625ff43..4016b5c86 100644
--- a/tests/functional/test_freeze.py
+++ b/tests/functional/test_freeze.py
@@ -108,19 +108,12 @@ def test_freeze_with_invalid_names(script):
             '...{}==1.0...'.format(pkgname.replace('_', '-'))
         )
     for pkgname in invalid_pkgnames:
-        # Check that the full distribution repr is present.
-        dist_repr = '{} 1.0 ('.format(pkgname.replace('_', '-'))
-        expected = (
-            '...Could not generate requirement for '
-            'distribution {}...'.format(dist_repr)
+        _check_output(
+            result.stderr,
+            '...Could not parse requirement: {}\n...'.format(
+                pkgname.replace('_', '-')
+            )
         )
-        _check_output(result.stderr, expected)
-
-    # Also check that the parse error details occur at least once.
-    # We only need to find one occurrence to know that exception details
-    # are logged.
-    expected = '...site-packages): Parse error at "...'
-    _check_output(result.stderr, expected)
 
 
 @pytest.mark.git
diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py
index a5d842c1e..cf405ca3f 100644
--- a/tests/functional/test_install_config.py
+++ b/tests/functional/test_install_config.py
@@ -105,10 +105,7 @@ def test_command_line_append_flags(script, virtualenv, data):
         "Analyzing links from page https://test.pypi.org"
         in result.stdout
     )
-    assert (
-        'Skipping link: not a file: {}'.format(data.find_links) in
-        result.stdout
-    ), 'stdout: {}'.format(result.stdout)
+    assert "Skipping link %s" % data.find_links in result.stdout
 
 
 @pytest.mark.network
@@ -130,10 +127,7 @@ def test_command_line_appends_correctly(script, data):
         "Analyzing links from page https://test.pypi.org"
         in result.stdout
     ), result.stdout
-    assert (
-        'Skipping link: not a file: {}'.format(data.find_links) in
-        result.stdout
-    ), 'stdout: {}'.format(result.stdout)
+    assert "Skipping link %s" % data.find_links in result.stdout
 
 
 def test_config_file_override_stack(script, virtualenv):
diff --git a/tests/functional/test_install_vcs_svn.py b/tests/functional/test_install_vcs_svn.py
index 8db3e8f50..6aea2737d 100644
--- a/tests/functional/test_install_vcs_svn.py
+++ b/tests/functional/test_install_vcs_svn.py
@@ -1,66 +1,30 @@
-from unittest import TestCase
-
+import pytest
 from mock import patch
 
-from pip._internal.vcs import RevOptions
 from pip._internal.vcs.subversion import Subversion
 
 
-class TestSubversionArgs(TestCase):
-    def setUp(self):
-        patcher = patch('pip._internal.vcs.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'])
+@patch('pip._internal.vcs.call_subprocess')
+@pytest.mark.network
+def test_obtain_should_recognize_auth_info_url(call_subprocess_mock, script):
+    url = 'svn+http://username:password@svn.example.com/'
+    svn = Subversion()
+    svn.obtain(script.scratch_path / 'test', url=url)
+    assert call_subprocess_mock.call_args[0][0] == [
+        svn.name, 'checkout', '-q', '--username', 'username', '--password',
+        'password', 'http://svn.example.com/',
+        script.scratch_path / 'test',
+    ]
+
+
+@patch('pip._internal.vcs.call_subprocess')
+@pytest.mark.network
+def test_export_should_recognize_auth_info_url(call_subprocess_mock, script):
+    url = 'svn+http://username:password@svn.example.com/'
+    svn = Subversion()
+    svn.export(script.scratch_path / 'test', url=url)
+    assert call_subprocess_mock.call_args[0][0] == [
+        svn.name, 'export', '--username', 'username', '--password',
+        'password', 'http://svn.example.com/',
+        script.scratch_path / 'test',
+    ]
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 3512b93a8..d8f40e0dd 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -133,7 +133,10 @@ class TestWheel:
         with pytest.raises(DistributionNotFound):
             finder.find_requirement(req, True)
 
-        assert 'Skipping link: invalid wheel filename:' in caplog.text
+        assert (
+            "invalid.whl; invalid wheel filename"
+            in caplog.text
+        )
 
     def test_not_find_wheel_not_supported(self, data, monkeypatch):
         """
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 094c23af4..4323bc405 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -130,24 +130,6 @@ class TestCandidateEvaluator:
         actual = evaluator.evaluate_link(link, search=search)
         assert actual == expected
 
-    def test_evaluate_link__incompatible_wheel(self):
-        """
-        Test an incompatible wheel.
-        """
-        link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl')
-        search = Search(
-            supplied='sample', canonical='sample', formats=['binary'],
-        )
-        # Pass an empty list for the valid tags to make sure nothing matches.
-        evaluator = CandidateEvaluator(
-            [], py_version_info=(3, 6, 4),
-        )
-        actual = evaluator.evaluate_link(link, search=search)
-        expected = (
-            False, "none of the wheel's tags match: py2-none-any, py3-none-any"
-        )
-        assert actual == expected
-
 
 def test_sort_locations_file_expand_dir(data):
     """
diff --git a/tests/unit/test_legacy_resolve.py b/tests/unit/test_legacy_resolve.py
index 4d7f27b20..e384e2349 100644
--- a/tests/unit/test_legacy_resolve.py
+++ b/tests/unit/test_legacy_resolve.py
@@ -1,65 +1,33 @@
 import logging
 
 import pytest
-from pip._vendor import pkg_resources
+from mock import patch
 
-from pip._internal.exceptions import (
-    NoneMetadataError, UnsupportedPythonVersion,
-)
+from pip._internal.exceptions import UnsupportedPythonVersion
 from pip._internal.legacy_resolve import _check_dist_requires_python
-from pip._internal.utils.packaging import get_requires_python
 
 
-# We need to inherit from DistInfoDistribution for the `isinstance()`
-# check inside `packaging.get_metadata()` to work.
-class FakeDist(pkg_resources.DistInfoDistribution):
+class FakeDist(object):
 
-    def __init__(self, metadata, metadata_name=None):
-        """
-        :param metadata: The value that dist.get_metadata() should return
-            for the `metadata_name` metadata.
-        :param metadata_name: The name of the metadata to store
-            (can be "METADATA" or "PKG-INFO").  Defaults to "METADATA".
-        """
-        if metadata_name is None:
-            metadata_name = 'METADATA'
+    def __init__(self, project_name):
+        self.project_name = project_name
 
-        self.project_name = 'my-project'
-        self.metadata_name = metadata_name
-        self.metadata = metadata
 
-    def __str__(self):
-        return '<distribution {!r}>'.format(self.project_name)
-
-    def has_metadata(self, name):
-        return (name == self.metadata_name)
-
-    def get_metadata(self, name):
-        assert name == self.metadata_name
-        return self.metadata
-
-
-def make_fake_dist(requires_python=None, metadata_name=None):
-    metadata = 'Name: test\n'
-    if requires_python is not None:
-        metadata += 'Requires-Python:{}'.format(requires_python)
-
-    return FakeDist(metadata, metadata_name=metadata_name)
+@pytest.fixture
+def dist():
+    return FakeDist('my-project')
 
 
+@patch('pip._internal.legacy_resolve.get_requires_python')
 class TestCheckDistRequiresPython(object):
 
     """
     Test _check_dist_requires_python().
     """
 
-    def test_compatible(self, caplog):
-        """
-        Test a Python version compatible with the dist's Requires-Python.
-        """
+    def test_compatible(self, mock_get_requires, caplog, dist):
         caplog.set_level(logging.DEBUG)
-        dist = make_fake_dist('== 3.6.5')
-
+        mock_get_requires.return_value = '== 3.6.5'
         _check_dist_requires_python(
             dist,
             version_info=(3, 6, 5),
@@ -67,11 +35,24 @@ class TestCheckDistRequiresPython(object):
         )
         assert not len(caplog.records)
 
-    def test_incompatible(self):
-        """
-        Test a Python version incompatible with the dist's Requires-Python.
-        """
-        dist = make_fake_dist('== 3.6.4')
+    def test_invalid_specifier(self, mock_get_requires, caplog, dist):
+        caplog.set_level(logging.DEBUG)
+        mock_get_requires.return_value = 'invalid'
+        _check_dist_requires_python(
+            dist,
+            version_info=(3, 6, 5),
+            ignore_requires_python=False,
+        )
+        assert len(caplog.records) == 1
+        record = caplog.records[0]
+        assert record.levelname == 'WARNING'
+        assert record.message == (
+            "Package 'my-project' has an invalid Requires-Python: "
+            "Invalid specifier: 'invalid'"
+        )
+
+    def test_incompatible(self, mock_get_requires, dist):
+        mock_get_requires.return_value = '== 3.6.4'
         with pytest.raises(UnsupportedPythonVersion) as exc:
             _check_dist_requires_python(
                 dist,
@@ -83,13 +64,11 @@ class TestCheckDistRequiresPython(object):
             "3.6.5 not in '== 3.6.4'"
         )
 
-    def test_incompatible_with_ignore_requires(self, caplog):
-        """
-        Test a Python version incompatible with the dist's Requires-Python
-        while passing ignore_requires_python=True.
-        """
+    def test_incompatible_with_ignore_requires(
+        self, mock_get_requires, caplog, dist,
+    ):
         caplog.set_level(logging.DEBUG)
-        dist = make_fake_dist('== 3.6.4')
+        mock_get_requires.return_value = '== 3.6.4'
         _check_dist_requires_python(
             dist,
             version_info=(3, 6, 5),
@@ -102,67 +81,3 @@ class TestCheckDistRequiresPython(object):
             "Ignoring failed Requires-Python check for package 'my-project': "
             "3.6.5 not in '== 3.6.4'"
         )
-
-    def test_none_requires_python(self, caplog):
-        """
-        Test a dist with Requires-Python None.
-        """
-        caplog.set_level(logging.DEBUG)
-        dist = make_fake_dist()
-        # Make sure our test setup is correct.
-        assert get_requires_python(dist) is None
-        assert len(caplog.records) == 0
-
-        # Then there is no exception and no log message.
-        _check_dist_requires_python(
-            dist,
-            version_info=(3, 6, 5),
-            ignore_requires_python=False,
-        )
-        assert len(caplog.records) == 0
-
-    def test_invalid_requires_python(self, caplog):
-        """
-        Test a dist with an invalid Requires-Python.
-        """
-        caplog.set_level(logging.DEBUG)
-        dist = make_fake_dist('invalid')
-        _check_dist_requires_python(
-            dist,
-            version_info=(3, 6, 5),
-            ignore_requires_python=False,
-        )
-        assert len(caplog.records) == 1
-        record = caplog.records[0]
-        assert record.levelname == 'WARNING'
-        assert record.message == (
-            "Package 'my-project' has an invalid Requires-Python: "
-            "Invalid specifier: 'invalid'"
-        )
-
-    @pytest.mark.parametrize('metadata_name', [
-        'METADATA',
-        'PKG-INFO',
-    ])
-    def test_empty_metadata_error(self, caplog, metadata_name):
-        """
-        Test dist.has_metadata() returning True and dist.get_metadata()
-        returning None.
-        """
-        dist = make_fake_dist(metadata_name=metadata_name)
-        dist.metadata = None
-
-        # Make sure our test setup is correct.
-        assert dist.has_metadata(metadata_name)
-        assert dist.get_metadata(metadata_name) is None
-
-        with pytest.raises(NoneMetadataError) as exc:
-            _check_dist_requires_python(
-                dist,
-                version_info=(3, 6, 5),
-                ignore_requires_python=False,
-            )
-        assert str(exc.value) == (
-            "None {} metadata found for distribution: "
-            "<distribution 'my-project'>".format(metadata_name)
-        )
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 1d97e65a1..3e9087fb5 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -22,6 +22,7 @@ from mock import Mock, patch
 from pip._internal.exceptions import (
     HashMismatch, HashMissing, InstallationError,
 )
+from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated
 from pip._internal.utils.encoding import BOMS, auto_decode
 from pip._internal.utils.glibc import check_glibc_version
 from pip._internal.utils.hashes import Hashes, MissingHashes
@@ -1068,3 +1069,79 @@ def test_remove_auth_from_url(auth_url, expected_url):
 def test_redact_password_from_url(auth_url, expected_url):
     url = redact_password_from_url(auth_url)
     assert url == expected_url
+
+
+@pytest.fixture()
+def patch_deprecation_check_version():
+    # We do this, so that the deprecation tests are easier to write.
+    import pip._internal.utils.deprecation as d
+    old_version = d.current_version
+    d.current_version = "1.0"
+    yield
+    d.current_version = old_version
+
+
+@pytest.mark.usefixtures("patch_deprecation_check_version")
+@pytest.mark.parametrize("replacement", [None, "a magic 8 ball"])
+@pytest.mark.parametrize("gone_in", [None, "2.0"])
+@pytest.mark.parametrize("issue", [None, 988])
+def test_deprecated_message_contains_information(gone_in, replacement, issue):
+    with pytest.warns(PipDeprecationWarning) as record:
+        deprecated(
+            "Stop doing this!",
+            replacement=replacement,
+            gone_in=gone_in,
+            issue=issue,
+        )
+
+    assert len(record) == 1
+    message = record[0].message.args[0]
+
+    assert "DEPRECATION: Stop doing this!" in message
+    # Ensure non-None values are mentioned.
+    for item in [gone_in, replacement, issue]:
+        if item is not None:
+            assert str(item) in message
+
+
+@pytest.mark.usefixtures("patch_deprecation_check_version")
+@pytest.mark.parametrize("replacement", [None, "a magic 8 ball"])
+@pytest.mark.parametrize("issue", [None, 988])
+def test_deprecated_raises_error_if_too_old(replacement, issue):
+    with pytest.raises(PipDeprecationWarning) as exception:
+        deprecated(
+            "Stop doing this!",
+            gone_in="1.0",  # this matches the patched version.
+            replacement=replacement,
+            issue=issue,
+        )
+
+    message = exception.value.args[0]
+
+    assert "DEPRECATION: Stop doing this!" in message
+    assert "1.0" in message
+    # Ensure non-None values are mentioned.
+    for item in [replacement, issue]:
+        if item is not None:
+            assert str(item) in message
+
+
+@pytest.mark.usefixtures("patch_deprecation_check_version")
+def test_deprecated_message_reads_well():
+    with pytest.raises(PipDeprecationWarning) as exception:
+        deprecated(
+            "Stop doing this!",
+            gone_in="1.0",  # this matches the patched version.
+            replacement="to be nicer",
+            issue="100000",  # I hope we never reach this number.
+        )
+
+    message = exception.value.args[0]
+
+    assert message == (
+        "DEPRECATION: Stop doing this! "
+        "pip 1.0 will remove support for this functionality. "
+        "A possible replacement is to be nicer. "
+        "You can find discussion regarding this at "
+        "https://github.com/pypa/pip/issues/100000."
+    )
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
index cae9ef35e..14cde104b 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -68,15 +68,6 @@ def make_test_install_req(base_name=None):
     return req
 
 
-@pytest.mark.parametrize('file_tag, expected', [
-    (('py27', 'none', 'any'), 'py27-none-any'),
-    (('cp33', 'cp32dmu', 'linux_x86_64'), 'cp33-cp32dmu-linux_x86_64'),
-])
-def test_format_tag(file_tag, expected):
-    actual = wheel.format_tag(file_tag)
-    assert actual == expected
-
-
 @pytest.mark.parametrize(
     "base_name, autobuilding, cache_available, expected",
     [