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