• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

31.46
/src/python/pants/base/deprecated.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
1✔
5

6
import inspect
1✔
7
import logging
1✔
8
from collections.abc import Callable
1✔
9
from functools import wraps
1✔
10
from typing import Any, TypeVar
1✔
11

12
from packaging.version import InvalidVersion, Version
1✔
13

14
from pants.util.memo import memoized, memoized_method
1✔
15
from pants.version import PANTS_SEMVER
1✔
16

17
logger = logging.getLogger(__name__)
1✔
18

19

20
class DeprecationError(Exception):
1✔
21
    """The base exception type thrown for any form of @deprecation application error."""
22

23

24
class MissingSemanticVersionError(DeprecationError):
1✔
25
    """Indicates the required removal_version was not supplied."""
26

27

28
class BadSemanticVersionError(DeprecationError):
1✔
29
    """Indicates the supplied removal_version was not a valid semver string."""
30

31

32
class NonDevSemanticVersionError(DeprecationError):
1✔
33
    """Indicates the supplied removal_version was not a pre-release version."""
34

35

36
class InvalidSemanticVersionOrderingError(DeprecationError):
1✔
37
    """Indicates that multiple semantic version strings were provided in an inconsistent
38
    ordering."""
39

40

41
class BadDecoratorNestingError(DeprecationError):
1✔
42
    """Indicates the @deprecated decorator was innermost in a sequence of layered decorators."""
43

44

45
class CodeRemovedError(Exception):
1✔
46
    """Indicates that the removal_version is not in the future.
47

48
    I.e., that the option/function/module with that removal_version has already been removed.
49

50
    Note that the code in question may not actually have been excised from the codebase yet, but
51
    it may be at any time.
52
    """
53

54

55
def is_deprecation_active(start_version: str | None) -> bool:
1✔
UNCOV
56
    return start_version is None or Version(start_version) <= PANTS_SEMVER
×
57

58

59
def get_deprecated_tense(removal_version: str) -> str:
1✔
60
    """Provides the grammatical tense for a given deprecated version vs the current version."""
UNCOV
61
    return "is scheduled to be" if (Version(removal_version) >= PANTS_SEMVER) else "was"
×
62

63

64
@memoized_method
1✔
65
def validate_deprecation_semver(version_string: str, version_description: str) -> Version:
1✔
66
    """Validates that version_string is a valid semver.
67

68
    If so, returns that semver. Raises an error otherwise.
69

70
    :param version_string: A pantsbuild.pants version which affects some deprecated entity.
71
    :param version_description: A string used in exception messages to describe what the
72
        `version_string` represents.
73
    :raises DeprecationError: if the version_string parameter is invalid.
74
    """
UNCOV
75
    if version_string is None:
×
UNCOV
76
        raise MissingSemanticVersionError(f"The {version_description} must be provided.")
×
UNCOV
77
    if not isinstance(version_string, str):
×
UNCOV
78
        raise BadSemanticVersionError(
×
79
            f"The {version_description} must be a version string but was {version_string} with "
80
            f"type {type(version_string)}."
81
        )
82

UNCOV
83
    try:
×
UNCOV
84
        v = Version(version_string)
×
UNCOV
85
    except InvalidVersion as e:
×
UNCOV
86
        raise BadSemanticVersionError(
×
87
            f"The given {version_description} {version_string} is not a valid version: {repr(e)}"
88
        )
89

90
    # NB: packaging.Version will see versions like 1.a.0 as 1a0 and as "valid".
91
    # We explicitly want our versions to be of the form x.y.z.
UNCOV
92
    if len(v.base_version.split(".")) != 3:
×
UNCOV
93
        raise BadSemanticVersionError(
×
94
            f"The given {version_description} is not a valid version: "
95
            f"{version_description}. Expecting the format `x.y.z.dev0`"
96
        )
UNCOV
97
    if not v.is_prerelease:
×
UNCOV
98
        raise NonDevSemanticVersionError(
×
99
            f"The given {version_description} is not a dev version: {version_string}\n"
100
            "Features should generally be removed in the first `dev` release of a release "
101
            "cycle."
102
        )
UNCOV
103
    return v
×
104

105

106
@memoized
1✔
107
def warn_or_error(
1✔
108
    removal_version: str,
109
    entity: str,
110
    hint: str | None,
111
    *,
112
    start_version: str | None = None,
113
    print_warning: bool = True,
114
    stacklevel: int = 0,
115
    context: int = 1,
116
) -> None:
117
    """Check the removal_version against the current Pants version.
118

119
    When choosing a removal version, there is a natural tension between the code-base, which benefits
120
    from short deprecation cycles, and the user-base which may prefer to deal with deprecations less
121
    frequently. As a rule of thumb, if the hint message can fully convey corrective action
122
    succinctly and you judge the impact to be on the small side (effects custom tasks as opposed to
123
    effecting BUILD files), lean towards the next release version as the removal version; otherwise,
124
    consider initiating a discussion to win consensus on a reasonable removal version.
125

126
    Issues a warning if the removal version is > current Pants version or an error otherwise.
127

128
    :param removal_version: The pantsbuild.pants version at which the deprecated entity will
129
        be/was removed.
130
    :param entity: A short description of the deprecated entity, e.g. "using an INI config file".
131
    :param hint: How to migrate.
132
    :param start_version: The pantsbuild.pants version at which the entity will
133
        begin to display a deprecation warning. This must be less than the `removal_version`. If
134
        not provided, the deprecation warning is always displayed.
135
    :param print_warning: Whether to print a warning for deprecations *before* their removal.
136
        If this flag is off, an exception will still be raised for options past their deprecation
137
        date.
138
    :param stacklevel: How far up the call stack to go for blame. Use 0 to disable.
139
    :param context: How many lines of source context to include.
140
    :raises DeprecationError: if the removal_version parameter is invalid.
141
    :raises CodeRemovedError: if the current version is later than the version marked for removal.
142
    """
UNCOV
143
    removal_semver = validate_deprecation_semver(removal_version, "removal version")
×
UNCOV
144
    if start_version:
×
UNCOV
145
        start_semver = validate_deprecation_semver(start_version, "deprecation start version")
×
UNCOV
146
        if start_semver >= removal_semver:
×
UNCOV
147
            raise InvalidSemanticVersionOrderingError(
×
148
                f"The deprecation start version {start_version} must be less than "
149
                f"the end version {removal_version}."
150
            )
UNCOV
151
        elif PANTS_SEMVER < start_semver:
×
UNCOV
152
            return
×
153

UNCOV
154
    msg = (
×
155
        f"DEPRECATED: {entity} {get_deprecated_tense(removal_version)} removed in version "
156
        f"{removal_version}."
157
    )
UNCOV
158
    if stacklevel > 0:
×
159
        # Get stack frames, ignoring those for internal/builtin code.
UNCOV
160
        frames = [frame for frame in inspect.stack(context) if frame.index is not None]
×
UNCOV
161
        if stacklevel < len(frames):
×
UNCOV
162
            frame = frames[stacklevel]
×
UNCOV
163
            code_context = "    ".join(frame.code_context) if frame.code_context else ""
×
UNCOV
164
            msg += f"\n ==> {frame.filename}:{frame.lineno}\n    {code_context}"
×
UNCOV
165
    if hint:
×
UNCOV
166
        msg += f"\n\n{hint}"
×
167

UNCOV
168
    if removal_semver <= PANTS_SEMVER:
×
UNCOV
169
        raise CodeRemovedError(msg)
×
UNCOV
170
    if print_warning:
×
UNCOV
171
        logger.warning(msg)
×
172

173

174
def deprecated_conditional(
1✔
175
    predicate: Callable[[], bool],
176
    removal_version: str,
177
    entity: str,
178
    hint: str | None,
179
    *,
180
    start_version: str | None = None,
181
) -> None:
182
    """Mark something as deprecated if the predicate is true."""
UNCOV
183
    validate_deprecation_semver(removal_version, "removal version")
×
UNCOV
184
    if predicate():
×
UNCOV
185
        warn_or_error(removal_version, entity, hint, start_version=start_version)
×
186

187

188
ReturnType = TypeVar("ReturnType")
1✔
189

190

191
def deprecated(
1✔
192
    removal_version: str,
193
    hint: str | None = None,
194
    *,
195
    start_version: str | None = None,
196
) -> Callable[[Callable[..., ReturnType]], Callable[..., ReturnType]]:
197
    """Mark a function or method as deprecated."""
UNCOV
198
    validate_deprecation_semver(removal_version, "removal version")
×
199

UNCOV
200
    def decorator(func):
×
UNCOV
201
        if not inspect.isfunction(func):
×
UNCOV
202
            raise BadDecoratorNestingError(
×
203
                "The @deprecated decorator must be applied innermost of all decorators."
204
            )
205

UNCOV
206
        @wraps(func)
×
UNCOV
207
        def wrapper(*args, **kwargs):
×
UNCOV
208
            warn_or_error(
×
209
                removal_version,
210
                f"{func.__module__}.{func.__qualname__}()",
211
                hint,
212
                start_version=start_version,
213
                stacklevel=3,
214
                context=3,
215
            )
UNCOV
216
            return func(*args, **kwargs)
×
217

UNCOV
218
        return wrapper
×
219

UNCOV
220
    return decorator
×
221

222

223
def deprecated_module(
1✔
224
    removal_version: str, hint: str | None, *, start_version: str | None = None
225
) -> None:
226
    """Mark an entire module as deprecated.
227

228
    Add a call to this at the top of the deprecated module.
229
    """
UNCOV
230
    warn_or_error(removal_version, "module", hint, start_version=start_version)
×
231

232

233
# TODO: old_container and new_container are both `OptionValueContainer`, but that causes a dep
234
#  cycle.
235
def resolve_conflicting_options(
1✔
236
    *,
237
    old_option: str,
238
    new_option: str,
239
    old_scope: str,
240
    new_scope: str,
241
    old_container: Any,
242
    new_container: Any,
243
) -> Any:
244
    """Utility for resolving an option that's been migrated to a new location.
245

246
    This will check if either option was explicitly configured, and if so, use that. If both were
247
    configured, it will error. Otherwise, it will use the default value for the new, preferred
248
    option.
249

250
    The option names should use snake_case, rather than --kebab-case.
251
    """
UNCOV
252
    old_configured = not old_container.is_default(old_option)
×
UNCOV
253
    new_configured = not new_container.is_default(new_option)
×
UNCOV
254
    if old_configured and new_configured:
×
255

UNCOV
256
        def format_option(*, scope: str, option: str) -> str:
×
UNCOV
257
            scope_preamble = "--" if scope == "" else f"--{scope}-"
×
UNCOV
258
            return f"`{scope_preamble}{option}`".replace("_", "-")
×
259

UNCOV
260
        old_display = format_option(scope=old_scope, option=old_option)
×
UNCOV
261
        new_display = format_option(scope=new_scope, option=new_option)
×
UNCOV
262
        raise ValueError(
×
263
            f"Conflicting options used. You used the new, preferred {new_display}, but also "
264
            f"used the deprecated {old_display}.\n\nPlease use only one of these "
265
            f"(preferably {new_display})."
266
        )
UNCOV
267
    if old_configured:
×
UNCOV
268
        return old_container.get(old_option)
×
UNCOV
269
    return new_container.get(new_option)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc