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

pantsbuild / pants / 19381742489

15 Nov 2025 12:52AM UTC coverage: 49.706% (-30.6%) from 80.29%
19381742489

Pull #22890

github

web-flow
Merge d961abf79 into 42e1ebd41
Pull Request #22890: Updated all python subsystem constraints to 3.14

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

14659 existing lines in 485 files now uncovered.

31583 of 63540 relevant lines covered (49.71%)

0.79 hits per line

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

0.0
/src/python/pants/core/goals/update_build_files.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import dataclasses
×
UNCOV
7
import logging
×
UNCOV
8
import os.path
×
UNCOV
9
import tokenize
×
UNCOV
10
from collections import defaultdict
×
UNCOV
11
from dataclasses import dataclass
×
UNCOV
12
from enum import Enum
×
UNCOV
13
from io import BytesIO
×
UNCOV
14
from typing import DefaultDict, cast
×
15

UNCOV
16
from colors import green, red
×
17

UNCOV
18
from pants.backend.build_files.fix.deprecations import renamed_fields_rules, renamed_targets_rules
×
UNCOV
19
from pants.backend.build_files.fmt.black.register import BlackRequest
×
UNCOV
20
from pants.backend.build_files.fmt.buildifier.rules import BuildifierRequest, _run_buildifier_fmt
×
UNCOV
21
from pants.backend.build_files.fmt.buildifier.subsystem import Buildifier
×
UNCOV
22
from pants.backend.build_files.fmt.ruff.register import RuffRequest
×
UNCOV
23
from pants.backend.build_files.fmt.yapf.register import YapfRequest
×
UNCOV
24
from pants.backend.python.goals import lockfile
×
UNCOV
25
from pants.backend.python.lint.black.rules import _run_black
×
UNCOV
26
from pants.backend.python.lint.black.subsystem import Black
×
UNCOV
27
from pants.backend.python.lint.ruff.format.rules import _run_ruff_fmt
×
UNCOV
28
from pants.backend.python.lint.ruff.subsystem import Ruff
×
UNCOV
29
from pants.backend.python.lint.yapf.rules import _run_yapf
×
UNCOV
30
from pants.backend.python.lint.yapf.subsystem import Yapf
×
UNCOV
31
from pants.backend.python.subsystems.python_tool_base import get_lockfile_interpreter_constraints
×
UNCOV
32
from pants.backend.python.util_rules import pex
×
UNCOV
33
from pants.base.specs import Specs
×
UNCOV
34
from pants.engine.console import Console
×
UNCOV
35
from pants.engine.engine_aware import EngineAwareParameter
×
UNCOV
36
from pants.engine.environment import EnvironmentName
×
UNCOV
37
from pants.engine.fs import CreateDigest, FileContent, PathGlobs, Workspace
×
UNCOV
38
from pants.engine.goal import Goal, GoalSubsystem
×
UNCOV
39
from pants.engine.internals.build_files import BuildFileOptions
×
UNCOV
40
from pants.engine.internals.parser import ParseError
×
UNCOV
41
from pants.engine.internals.specs_rules import resolve_specs_paths
×
UNCOV
42
from pants.engine.intrinsics import (
×
43
    create_digest,
44
    digest_to_snapshot,
45
    get_digest_contents,
46
    path_globs_to_paths,
47
)
UNCOV
48
from pants.engine.platform import Platform
×
UNCOV
49
from pants.engine.rules import collect_rules, concurrently, goal_rule, implicitly, rule
×
UNCOV
50
from pants.engine.unions import UnionMembership, UnionRule, union
×
UNCOV
51
from pants.option.option_types import BoolOption, EnumOption
×
UNCOV
52
from pants.util.docutil import bin_name, doc_url
×
UNCOV
53
from pants.util.logging import LogLevel
×
UNCOV
54
from pants.util.memo import memoized
×
UNCOV
55
from pants.util.strutil import help_text, softwrap
×
56

UNCOV
57
logger = logging.getLogger(__name__)
×
58

59
# ------------------------------------------------------------------------------------------
60
# Generic goal
61
# ------------------------------------------------------------------------------------------
62

63

UNCOV
64
@dataclass(frozen=True)
×
UNCOV
65
class RewrittenBuildFile:
×
UNCOV
66
    path: str
×
UNCOV
67
    lines: tuple[str, ...]
×
UNCOV
68
    change_descriptions: tuple[str, ...]
×
69

70

UNCOV
71
class Formatter(Enum):
×
UNCOV
72
    YAPF = "yapf"
×
UNCOV
73
    BLACK = "black"
×
UNCOV
74
    RUFF = "ruff"
×
UNCOV
75
    BUILDIFIER = "buildifier"
×
76

77

UNCOV
78
@union(in_scope_types=[EnvironmentName])
×
UNCOV
79
@dataclass(frozen=True)
×
UNCOV
80
class RewrittenBuildFileRequest(EngineAwareParameter):
×
UNCOV
81
    path: str
×
UNCOV
82
    lines: tuple[str, ...]
×
UNCOV
83
    colors_enabled: bool = dataclasses.field(compare=False)
×
84

UNCOV
85
    def debug_hint(self) -> str:
×
86
        return self.path
×
87

UNCOV
88
    def to_file_content(self) -> FileContent:
×
89
        lines = "\n".join(self.lines) + "\n"
×
90
        return FileContent(self.path, lines.encode("utf-8"))
×
91

UNCOV
92
    @memoized
×
UNCOV
93
    def tokenize(self) -> list[tokenize.TokenInfo]:
×
94
        _bytes_stream = BytesIO("\n".join(self.lines).encode("utf-8"))
×
95
        try:
×
96
            return list(tokenize.tokenize(_bytes_stream.readline))
×
97
        except tokenize.TokenError as e:
×
98
            raise ParseError(f"Failed to parse {self.path}: {e}")
×
99

UNCOV
100
    def red(self, s: str) -> str:
×
101
        return cast(str, red(s)) if self.colors_enabled else s
×
102

UNCOV
103
    def green(self, s: str) -> str:
×
104
        return cast(str, green(s)) if self.colors_enabled else s
×
105

106

UNCOV
107
@rule(polymorphic=True)
×
UNCOV
108
async def rewrite_build_file(
×
109
    req: RewrittenBuildFileRequest, env_name: EnvironmentName
110
) -> RewrittenBuildFile:
111
    raise NotImplementedError()
×
112

113

UNCOV
114
class DeprecationFixerRequest(RewrittenBuildFileRequest):
×
115
    """A fixer for deprecations.
116

117
    These can be disabled by the user with `--no-fix-safe-deprecations`.
118
    """
119

120

UNCOV
121
class UpdateBuildFilesSubsystem(GoalSubsystem):
×
UNCOV
122
    name = "update-build-files"
×
UNCOV
123
    help = help_text(
×
124
        f"""
125
        Format and fix safe deprecations in BUILD files.
126

127
        This does not handle the full Pants upgrade. You must still manually change
128
        `pants_version` in `pants.toml` and you may need to manually address some deprecations.
129
        See {doc_url("docs/releases/upgrade-tips")} for upgrade tips.
130
        """
131
    )
132

UNCOV
133
    @classmethod
×
UNCOV
134
    def activated(cls, union_membership: UnionMembership) -> bool:
×
135
        return RewrittenBuildFileRequest in union_membership
×
136

UNCOV
137
    check = BoolOption(
×
138
        default=False,
139
        help=softwrap(
140
            """
141
            Do not write changes to disk, only write back what would change. Return code
142
            0 means there would be no changes, and 1 means that there would be.
143
            """
144
        ),
145
    )
UNCOV
146
    fmt = BoolOption(
×
147
        default=True,
148
        help=softwrap(
149
            """
150
            Format BUILD files using Black, Ruff or Yapf.
151

152
            Set `[black].args` / `[ruff].args` / `[yapf].args`, `[black].config` / `[ruff].config`, `[yapf].config` ,
153
            and `[black].config_discovery` / `[ruff].config_discovery`, `[yapf].config_discovery` to change
154
            Black's, Ruff's, or Yapf's behavior. Set
155
            `[black].interpreter_constraints` / `[ruff].interpreter_constraints` / `[yapf].interpreter_constraints`
156
            and `[python].interpreter_search_path` to change which interpreter is
157
            used to run the formatter.
158
            """
159
        ),
160
    )
UNCOV
161
    formatter = EnumOption(
×
162
        default=Formatter.BLACK,
163
        help="Which formatter Pants should use to format BUILD files.",
164
    )
UNCOV
165
    fix_safe_deprecations = BoolOption(
×
166
        default=True,
167
        help=softwrap(
168
            """
169
            Automatically fix deprecations, such as target type renames, that are safe
170
            because they do not change semantics.
171
            """
172
        ),
173
    )
174

175

UNCOV
176
class UpdateBuildFilesGoal(Goal):
×
UNCOV
177
    subsystem_cls = UpdateBuildFilesSubsystem
×
UNCOV
178
    environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
×
179

180

UNCOV
181
@goal_rule(desc="Update all BUILD files", level=LogLevel.DEBUG)
×
UNCOV
182
async def update_build_files(
×
183
    update_build_files_subsystem: UpdateBuildFilesSubsystem,
184
    build_file_options: BuildFileOptions,
185
    console: Console,
186
    workspace: Workspace,
187
    union_membership: UnionMembership,
188
    specs: Specs,
189
    env_name: EnvironmentName,
190
) -> UpdateBuildFilesGoal:
191
    if not specs:
×
192
        if not specs.includes.from_change_detection:
×
193
            logger.warning(
×
194
                softwrap(
195
                    f"""\
196
                    No arguments specified with `{bin_name()} update-build-files`, so the goal will
197
                    do nothing.
198

199
                    Instead, you should provide arguments like this:
200

201
                      * `{bin_name()} update-build-files ::` to run on everything
202
                      * `{bin_name()} update-build-files dir::` to run on `dir` and subdirs
203
                      * `{bin_name()} update-build-files dir` to run on `dir`
204
                      * `{bin_name()} update-build-files dir/BUILD` to run on that single BUILD file
205
                      * `{bin_name()} --changed-since=HEAD update-build-files` to run only on changed BUILD files
206
                    """
207
                )
208
            )
209
        return UpdateBuildFilesGoal(exit_code=0)
×
210

211
    all_build_file_paths, specs_paths = await concurrently(
×
212
        path_globs_to_paths(
213
            PathGlobs(
214
                globs=(
215
                    *(os.path.join("**", p) for p in build_file_options.patterns),
216
                    *(f"!{p}" for p in build_file_options.ignores),
217
                )
218
            )
219
        ),
220
        resolve_specs_paths(specs),
221
    )
222
    specified_paths = set(specs_paths.files)
×
223
    specified_build_files = await get_digest_contents(
×
224
        **implicitly(PathGlobs(fp for fp in all_build_file_paths.files if fp in specified_paths))
225
    )
226

227
    rewrite_request_classes = []
×
228
    formatter_to_request_class: dict[Formatter, type[RewrittenBuildFileRequest]] = {
×
229
        Formatter.BLACK: FormatWithBlackRequest,
230
        Formatter.YAPF: FormatWithYapfRequest,
231
        Formatter.RUFF: FormatWithRuffRequest,
232
        Formatter.BUILDIFIER: FormatWithBuildifierRequest,
233
    }
234
    chosen_formatter_request_class = formatter_to_request_class.get(
×
235
        update_build_files_subsystem.formatter
236
    )
237
    if not chosen_formatter_request_class:
×
238
        raise ValueError(f"Unrecognized formatter: {update_build_files_subsystem.formatter}")
×
239

240
    for request in union_membership[RewrittenBuildFileRequest]:
×
241
        if update_build_files_subsystem.fmt and request == chosen_formatter_request_class:
×
242
            rewrite_request_classes.append(request)
×
243

244
        if update_build_files_subsystem.fix_safe_deprecations and issubclass(
×
245
            request, DeprecationFixerRequest
246
        ):
247
            rewrite_request_classes.append(request)
×
248

249
        # If there are other types of requests that aren't the standard formatter
250
        # backends or deprecation fixers, add them here.
251
        if request not in formatter_to_request_class.values() and not issubclass(
×
252
            request, DeprecationFixerRequest
253
        ):
254
            rewrite_request_classes.append(request)
×
255

256
    build_file_to_lines = {
×
257
        build_file.path: tuple(build_file.content.decode("utf-8").splitlines())
258
        for build_file in specified_build_files
259
    }
260
    build_file_to_change_descriptions: DefaultDict[str, list[str]] = defaultdict(list)
×
261
    for rewrite_request_cls in rewrite_request_classes:
×
262
        all_rewritten_files = await concurrently(  # noqa: PNT30: this is inherently sequential
×
263
            rewrite_build_file(
264
                **implicitly(
265
                    {
266
                        rewrite_request_cls(
267
                            build_file, lines, colors_enabled=console._use_colors
268
                        ): RewrittenBuildFileRequest,
269
                        env_name: EnvironmentName,
270
                    }
271
                ),
272
            )
273
            for build_file, lines in build_file_to_lines.items()
274
        )
275
        for rewritten_file in all_rewritten_files:
×
276
            if not rewritten_file.change_descriptions:
×
277
                continue
×
278
            build_file_to_lines[rewritten_file.path] = rewritten_file.lines
×
279
            build_file_to_change_descriptions[rewritten_file.path].extend(
×
280
                rewritten_file.change_descriptions
281
            )
282

283
    changed_build_files = sorted(
×
284
        build_file
285
        for build_file, change_descriptions in build_file_to_change_descriptions.items()
286
        if change_descriptions
287
    )
288
    if not changed_build_files:
×
289
        parts = ["No required changes to BUILD files found."]
×
290
        if not update_build_files_subsystem.check:
×
291
            parts.append(
×
292
                softwrap(
293
                    f"""
294
                    However, there may still be deprecations that `update-build-files` doesn't know
295
                    how to fix. See {doc_url("docs/releases/upgrade-tips")} for upgrade tips.
296
                    """
297
                )
298
            )
299

300
        msg = " ".join(parts)
×
301
        logger.info(msg)
×
302
        return UpdateBuildFilesGoal(exit_code=0)
×
303

304
    if not update_build_files_subsystem.check:
×
305
        result = await create_digest(
×
306
            CreateDigest(
307
                FileContent(
308
                    build_file, ("\n".join(build_file_to_lines[build_file]) + "\n").encode("utf-8")
309
                )
310
                for build_file in changed_build_files
311
            )
312
        )
313
        workspace.write_digest(result)
×
314

315
    for build_file in changed_build_files:
×
316
        formatted_changes = "\n".join(
×
317
            f"  - {description}" for description in build_file_to_change_descriptions[build_file]
318
        )
319
        tense = "Would update" if update_build_files_subsystem.check else "Updated"
×
320
        console.print_stdout(f"{tense} {console.blue(build_file)}:\n{formatted_changes}")
×
321

322
    if update_build_files_subsystem.check:
×
323
        console.print_stdout(
×
324
            f"\nTo fix `update-build-files` failures, run `{bin_name()} update-build-files`."
325
        )
326

327
    return UpdateBuildFilesGoal(exit_code=1 if update_build_files_subsystem.check else 0)
×
328

329

330
# ------------------------------------------------------------------------------------------
331
# Yapf formatter fixer
332
# ------------------------------------------------------------------------------------------
333

334

UNCOV
335
class FormatWithYapfRequest(RewrittenBuildFileRequest):
×
UNCOV
336
    pass
×
337

338

UNCOV
339
@rule
×
UNCOV
340
async def format_build_file_with_yapf(
×
341
    request: FormatWithYapfRequest, yapf: Yapf
342
) -> RewrittenBuildFile:
343
    input_snapshot = await digest_to_snapshot(
×
344
        **implicitly(CreateDigest([request.to_file_content()]))
345
    )
346
    yapf_ics = await get_lockfile_interpreter_constraints(yapf)
×
347
    result = await _run_yapf(
×
348
        YapfRequest.Batch(
349
            Yapf.options_scope,
350
            input_snapshot.files,
351
            partition_metadata=None,
352
            snapshot=input_snapshot,
353
        ),
354
        yapf,
355
        yapf_ics,
356
    )
357
    output_content = await get_digest_contents(result.output.digest)
×
358

359
    formatted_build_file_content = next(fc for fc in output_content if fc.path == request.path)
×
360
    build_lines = tuple(formatted_build_file_content.content.decode("utf-8").splitlines())
×
361
    change_descriptions = ("Format with Yapf",) if result.did_change else ()
×
362

363
    return RewrittenBuildFile(request.path, build_lines, change_descriptions=change_descriptions)
×
364

365

366
# ------------------------------------------------------------------------------------------
367
# Black formatter fixer
368
# ------------------------------------------------------------------------------------------
369

370

UNCOV
371
class FormatWithBlackRequest(RewrittenBuildFileRequest):
×
UNCOV
372
    pass
×
373

374

UNCOV
375
@rule
×
UNCOV
376
async def format_build_file_with_black(
×
377
    request: FormatWithBlackRequest, black: Black
378
) -> RewrittenBuildFile:
379
    input_snapshot = await digest_to_snapshot(
×
380
        **implicitly(CreateDigest([request.to_file_content()]))
381
    )
382
    black_ics = await get_lockfile_interpreter_constraints(black)
×
383
    result = await _run_black(
×
384
        BlackRequest.Batch(
385
            Black.options_scope,
386
            input_snapshot.files,
387
            partition_metadata=None,
388
            snapshot=input_snapshot,
389
        ),
390
        black,
391
        black_ics,
392
    )
393
    output_content = await get_digest_contents(result.output.digest)
×
394

395
    formatted_build_file_content = next(fc for fc in output_content if fc.path == request.path)
×
396
    build_lines = tuple(formatted_build_file_content.content.decode("utf-8").splitlines())
×
397
    change_descriptions = ("Format with Black",) if result.did_change else ()
×
398

399
    return RewrittenBuildFile(request.path, build_lines, change_descriptions=change_descriptions)
×
400

401

402
# ------------------------------------------------------------------------------------------
403
# Ruff formatter fixer
404
# ------------------------------------------------------------------------------------------
405

406

UNCOV
407
class FormatWithRuffRequest(RewrittenBuildFileRequest):
×
UNCOV
408
    pass
×
409

410

UNCOV
411
@rule
×
UNCOV
412
async def format_build_file_with_ruff(
×
413
    request: FormatWithRuffRequest, ruff: Ruff, platform: Platform
414
) -> RewrittenBuildFile:
415
    input_snapshot = await digest_to_snapshot(
×
416
        **implicitly(CreateDigest([request.to_file_content()]))
417
    )
418
    result = await _run_ruff_fmt(
×
419
        RuffRequest.Batch(
420
            Ruff.options_scope,
421
            input_snapshot.files,
422
            partition_metadata=None,
423
            snapshot=input_snapshot,
424
        ),
425
        ruff,
426
        platform,
427
    )
428
    output_content = await get_digest_contents(result.output.digest)
×
429

430
    formatted_build_file_content = next(fc for fc in output_content if fc.path == request.path)
×
431
    build_lines = tuple(formatted_build_file_content.content.decode("utf-8").splitlines())
×
432
    change_descriptions = ("Format with Ruff",) if result.did_change else ()
×
433

434
    return RewrittenBuildFile(request.path, build_lines, change_descriptions=change_descriptions)
×
435

436

437
# ------------------------------------------------------------------------------------------
438
# Buildifier formatter fixer
439
# ------------------------------------------------------------------------------------------
440

441

UNCOV
442
class FormatWithBuildifierRequest(RewrittenBuildFileRequest):
×
UNCOV
443
    pass
×
444

445

UNCOV
446
@rule
×
UNCOV
447
async def format_build_file_with_buildifier(
×
448
    request: FormatWithBuildifierRequest, buildifier: Buildifier, platform: Platform
449
) -> RewrittenBuildFile:
450
    input_snapshot = await digest_to_snapshot(
×
451
        **implicitly(CreateDigest([request.to_file_content()]))
452
    )
453
    result = await _run_buildifier_fmt(
×
454
        request=BuildifierRequest.Batch(
455
            tool_name=Buildifier.options_scope,
456
            elements=input_snapshot.files,
457
            partition_metadata=None,
458
            snapshot=input_snapshot,
459
        ),
460
        buildifier=buildifier,
461
        platform=platform,
462
    )
463
    output_content = await get_digest_contents(result.output.digest)
×
464
    formatted_build_file_content = next(fc for fc in output_content if fc.path == request.path)
×
465
    build_lines = tuple(formatted_build_file_content.content.decode("utf-8").splitlines())
×
466
    change_descriptions = (f"Format with {Buildifier.name}",) if result.did_change else ()
×
467
    return RewrittenBuildFile(request.path, build_lines, change_descriptions=change_descriptions)
×
468

469

470
# ------------------------------------------------------------------------------------------
471
# Rename deprecated target types fixer
472
# ------------------------------------------------------------------------------------------
473

474

UNCOV
475
class RenameDeprecatedTargetsRequest(DeprecationFixerRequest):
×
UNCOV
476
    pass
×
477

478

UNCOV
479
@rule(desc="Check for deprecated target type names", level=LogLevel.DEBUG)
×
UNCOV
480
async def maybe_rename_deprecated_targets(
×
481
    request: RenameDeprecatedTargetsRequest,
482
) -> RewrittenBuildFile:
483
    old_bytes = "\n".join(request.lines).encode("utf-8")
×
484
    new_content = await renamed_targets_rules.fix_single(
×
485
        renamed_targets_rules.RenameTargetsInFileRequest(path=request.path, content=old_bytes),
486
        **implicitly(),
487
    )
488

489
    return RewrittenBuildFile(
×
490
        request.path,
491
        tuple(new_content.content.decode("utf-8").splitlines()),
492
        change_descriptions=(
493
            ("Renamed deprecated targets",) if old_bytes != new_content.content else ()
494
        ),
495
    )
496

497

498
# ------------------------------------------------------------------------------------------
499
# Rename deprecated field types fixer
500
# ------------------------------------------------------------------------------------------
501

502

UNCOV
503
class RenameDeprecatedFieldsRequest(DeprecationFixerRequest):
×
UNCOV
504
    pass
×
505

506

UNCOV
507
@rule(desc="Check for deprecated field type names", level=LogLevel.DEBUG)
×
UNCOV
508
async def maybe_rename_deprecated_fields(
×
509
    request: RenameDeprecatedFieldsRequest,
510
) -> RewrittenBuildFile:
511
    old_bytes = "\n".join(request.lines).encode("utf-8")
×
512
    new_content = await renamed_fields_rules.fix_single(
×
513
        renamed_fields_rules.RenameFieldsInFileRequest(path=request.path, content=old_bytes),
514
        **implicitly(),
515
    )
516

517
    return RewrittenBuildFile(
×
518
        request.path,
519
        tuple(new_content.content.decode("utf-8").splitlines()),
520
        change_descriptions=(
521
            ("Renamed deprecated fields",) if old_bytes != new_content.content else ()
522
        ),
523
    )
524

525

UNCOV
526
def rules():
×
UNCOV
527
    return (
×
528
        *collect_rules(),
529
        *collect_rules(renamed_fields_rules),
530
        *collect_rules(renamed_targets_rules),
531
        *pex.rules(),
532
        *lockfile.rules(),
533
        UnionRule(RewrittenBuildFileRequest, RenameDeprecatedTargetsRequest),
534
        UnionRule(RewrittenBuildFileRequest, RenameDeprecatedFieldsRequest),
535
        # NB: We want this to come at the end so that running Black or Yapf happens
536
        # after all our deprecation fixers.
537
        UnionRule(RewrittenBuildFileRequest, FormatWithBlackRequest),
538
        UnionRule(RewrittenBuildFileRequest, FormatWithYapfRequest),
539
        UnionRule(RewrittenBuildFileRequest, FormatWithRuffRequest),
540
        UnionRule(RewrittenBuildFileRequest, FormatWithBuildifierRequest),
541
    )
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

© 2026 Coveralls, Inc