diff --git a/docs/development.rst b/docs/development.rst index bc1d36777..5b1ad1370 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -211,6 +211,12 @@ document existing behavior with the intention of covering that behavior with the above deprecation process are always acceptable, and will be considered on their merits. +.. note:: + + pip has a helper function for making deprecation easier for pip maintainers. + The supporting documentation can be found in the source code of + ``pip._internal.utils.deprecation.deprecated``. The function is not a part of + pip's public API. Release Process =============== diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 857486cc1..8c0ec82c0 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -9,7 +9,6 @@ import os import posixpath import re import sys -import warnings from collections import namedtuple from pip._vendor import html5lib, requests, six @@ -29,7 +28,7 @@ from pip._internal.exceptions import ( ) from pip._internal.models.index import PyPI from pip._internal.pep425tags import get_supported -from pip._internal.utils.deprecation import RemovedInPip12Warning +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, cached_property, normalize_path, @@ -212,10 +211,12 @@ class PackageFinder(object): # # dependency_links value # # FIXME: also, we should track comes_from (i.e., use Link) if self.process_dependency_links: - warnings.warn( + deprecated( "Dependency Links processing has been deprecated and will be " "removed in a future release.", - RemovedInPip12Warning, + replacement=None, + gone_in="18.2", + issue=4187, ) self.dependency_links.extend(links) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 82d0a8c4b..4bbc27b0a 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -4,7 +4,6 @@ import collections import logging import os import re -import warnings from pip._vendor import pkg_resources, six from pip._vendor.packaging.utils import canonicalize_name @@ -13,7 +12,7 @@ from pip._vendor.pkg_resources import RequirementParseError from pip._internal.exceptions import InstallationError from pip._internal.req import InstallRequirement from pip._internal.req.req_file import COMMENT_RE -from pip._internal.utils.deprecation import RemovedInPip12Warning +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, ) @@ -216,10 +215,12 @@ class FrozenRequirement(object): 'for this package:' ) else: - warnings.warn( + deprecated( "SVN editable detection based on dependency links " "will be dropped in the future.", - RemovedInPip12Warning, + replacement=None, + gone_in="18.2", + issue=4187, ) comments.append( '# Installing as editable to satisfy requirement %s:' % diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8838dd2eb..efe96a774 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -8,7 +8,6 @@ import shutil import sys import sysconfig import traceback -import warnings import zipfile from distutils.util import change_root from email.parser import FeedParser # type: ignore @@ -33,7 +32,7 @@ from pip._internal.locations import ( PIP_DELETE_MARKER_FILENAME, running_under_virtualenv, ) from pip._internal.req.req_uninstall import UninstallPathSet -from pip._internal.utils.deprecation import RemovedInPip12Warning +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( @@ -582,11 +581,13 @@ class InstallRequirement(object): if requires is None: logging.warn(template, self, "it is missing.") - warnings.warn( + deprecated( "Future versions of pip may reject packages with " "pyproject.toml files that do not contain the [build-system]" "table and the requires key, as specified in PEP 518.", - RemovedInPip12Warning, + replacement=None, + gone_in="18.2", + issue=5416, ) # Currently, we're isolating the build based on the presence of the diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index 89b66e9ad..bd744cf2c 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -6,68 +6,84 @@ from __future__ import absolute_import import logging import warnings +from pip._vendor.packaging.version import parse + +from pip import __version__ as current_version from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any # noqa: F401 + from typing import Any, Optional # noqa: F401 class PipDeprecationWarning(Warning): pass -class Pending(object): - pass - - -class RemovedInPip12Warning(PipDeprecationWarning, Pending): - pass +_original_showwarning = None # type: Any # Warnings <-> Logging Integration - - -_warnings_showwarning = None # type: Any - - def _showwarning(message, category, filename, lineno, file=None, line=None): if file is not None: - if _warnings_showwarning is not None: - _warnings_showwarning( + if _original_showwarning is not None: + _original_showwarning( message, category, filename, lineno, file, line, ) + elif issubclass(category, PipDeprecationWarning): + # We use a specially named logger which will handle all of the + # deprecation messages for pip. + logger = logging.getLogger("pip._internal.deprecations") + logger.warning(message) else: - if issubclass(category, PipDeprecationWarning): - # We use a specially named logger which will handle all of the - # deprecation messages for pip. - logger = logging.getLogger("pip._internal.deprecations") - - # This is purposely using the % formatter here instead of letting - # the logging module handle the interpolation. This is because we - # want it to appear as if someone typed this entire message out. - log_message = "DEPRECATION: %s" % message - - # PipDeprecationWarnings that are Pending still have at least 2 - # versions to go until they are removed so they can just be - # warnings. Otherwise, they will be removed in the very next - # version of pip. We want these to be more obvious so we use the - # ERROR logging level. - if issubclass(category, Pending): - logger.warning(log_message) - else: - logger.error(log_message) - else: - _warnings_showwarning( - message, category, filename, lineno, file, line, - ) + _original_showwarning( + message, category, filename, lineno, file, line, + ) def install_warning_logger(): # Enable our Deprecation Warnings warnings.simplefilter("default", PipDeprecationWarning, append=True) - global _warnings_showwarning + global _original_showwarning - if _warnings_showwarning is None: - _warnings_showwarning = warnings.showwarning + if _original_showwarning is None: + _original_showwarning = warnings.showwarning warnings.showwarning = _showwarning + + +def deprecated(reason, replacement, gone_in, issue=None): + # type: (str, Optional[str], Optional[str], Optional[int]) -> None + """Helper to deprecate existing functionality. + + reason: + Textual reason shown to the user about why this functionality has + been deprecated. + replacement: + Textual suggestion shown to the user about what alternative + functionality they can use. + gone_in: + The version of pip does this functionality should get removed in. + Raises errors if pip's current version is greater than or equal to + this. + issue: + Issue number on the tracker that would serve as a useful place for + users to find related discussion and provide feedback. + + Always pass replacement, gone_in and issue as keyword arguments for clarity + at the call site. + """ + + # 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: " + 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) + + # 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/tests/functional/test_warning.py b/tests/functional/test_warning.py index 20f246795..6c097c75b 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -1,21 +1,22 @@ +import textwrap + def test_environ(script, tmpdir): """$PYTHONWARNINGS was added in python2.7""" demo = tmpdir.join('warnings_demo.py') - demo.write(''' -from pip._internal.utils import deprecation -deprecation.install_warning_logger() + demo.write(textwrap.dedent(''' + from logging import basicConfig + from pip._internal.utils import deprecation -from logging import basicConfig -basicConfig() + deprecation.install_warning_logger() + basicConfig() -from warnings import warn -warn("deprecated!", deprecation.PipDeprecationWarning) -''') + deprecation.deprecated("deprecated!", replacement=None, gone_in=None) + ''')) result = script.run('python', demo, expect_stderr=True) - assert result.stderr == \ - 'ERROR:pip._internal.deprecations:DEPRECATION: deprecated!\n' + expected = 'WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n' + assert result.stderr == expected script.environ['PYTHONWARNINGS'] = 'ignore' result = script.run('python', demo)