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