1"""
2A module that implements tooling to enable easy warnings about deprecations.
3"""
4
5from __future__ import annotations
6
7import logging
8import os
9import warnings
10from typing import Any, TextIO
11
12from pip._vendor.packaging.version import parse
13
14from pip import __version__ as current_version # NOTE: tests patch this name.
15
16DEPRECATION_MSG_PREFIX = "DEPRECATION: "
17
18
19class PipDeprecationWarning(Warning):
20 include_source: bool = False
21
22
23_original_showwarning: Any = None
24
25
26# Warnings <-> Logging Integration
27def _showwarning(
28 message: Warning | str,
29 category: type[Warning],
30 filename: str,
31 lineno: int,
32 file: TextIO | None = None,
33 line: str | None = None,
34) -> None:
35 if file is not None:
36 if _original_showwarning is not None:
37 _original_showwarning(message, category, filename, lineno, file, line)
38 elif issubclass(category, PipDeprecationWarning):
39 # We use a specially named logger which will handle all of the
40 # deprecation messages for pip.
41 logger = logging.getLogger("pip._internal.deprecations")
42 if isinstance(message, PipDeprecationWarning) and message.include_source:
43 logger.warning("%s (%s:%s)", message, filename, lineno)
44 else:
45 logger.warning(message)
46 else:
47 _original_showwarning(message, category, filename, lineno, file, line)
48
49
50def install_warning_logger() -> None:
51 # Enable our Deprecation Warnings
52 # If we're running pip test suite, promote the PipDeprecationWarning into errors.
53 if os.environ.get("_PIP_TEST_ENV", None):
54 warnings.simplefilter("error", PipDeprecationWarning)
55 else:
56 warnings.simplefilter("default", PipDeprecationWarning, append=True)
57
58 global _original_showwarning
59
60 if _original_showwarning is None:
61 _original_showwarning = warnings.showwarning
62 warnings.showwarning = _showwarning
63
64
65def deprecated(
66 *,
67 reason: str,
68 replacement: str | None,
69 gone_in: str | None,
70 feature_flag: str | None = None,
71 issue: int | None = None,
72 stacklevel: int = 2,
73 include_source: bool = False,
74) -> None:
75 """Helper to deprecate existing functionality.
76
77 reason:
78 Textual reason shown to the user about why this functionality has
79 been deprecated. Should be a complete sentence.
80 replacement:
81 Textual suggestion shown to the user about what alternative
82 functionality they can use.
83 gone_in:
84 The version of pip does this functionality should get removed in.
85 Raises an error if pip's current version is greater than or equal to
86 this.
87 feature_flag:
88 Command-line flag of the form --use-feature={feature_flag} for testing
89 upcoming functionality.
90 issue:
91 Issue number on the tracker that would serve as a useful place for
92 users to find related discussion and provide feedback.
93 stacklevel:
94 How many frames up the call stack to attribute the warning to.
95 Defaults to 2 (the caller of deprecated()).
96 include_source:
97 If True, include the source filename and line number in the warning
98 output. Useful when the warning originates from external code.
99 """
100
101 # Determine whether or not the feature is already gone in this version.
102 is_gone = gone_in is not None and parse(current_version) >= parse(gone_in)
103
104 message_parts = [
105 (reason, f"{DEPRECATION_MSG_PREFIX}{{}}"),
106 (
107 gone_in,
108 (
109 "pip {} will enforce this behaviour change."
110 if not is_gone
111 else "Since pip {}, this is no longer supported."
112 ),
113 ),
114 (
115 replacement,
116 "A possible replacement is {}.",
117 ),
118 (
119 feature_flag,
120 (
121 "You can use the flag --use-feature={} to test the upcoming behaviour."
122 if not is_gone
123 else None
124 ),
125 ),
126 (
127 issue,
128 "Discussion can be found at https://github.com/pypa/pip/issues/{}",
129 ),
130 ]
131
132 message = " ".join(
133 format_str.format(value)
134 for value, format_str in message_parts
135 if format_str is not None and value is not None
136 )
137
138 # Raise as an error if this behaviour is deprecated.
139 if is_gone:
140 raise PipDeprecationWarning(message)
141
142 warning = PipDeprecationWarning(message)
143 warning.include_source = include_source
144 warnings.warn(warning, stacklevel=stacklevel)