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

pantsbuild / pants / 26260209689

21 May 2026 11:59PM UTC coverage: 75.453% (-15.7%) from 91.156%
26260209689

Pull #23365

github

web-flow
Merge 5fe873b58 into 7ea655ba0
Pull Request #23365: uv.lock -> pex optimization

5 of 16 new or added lines in 1 file covered. (31.25%)

10118 existing lines in 378 files now uncovered.

54669 of 72454 relevant lines covered (75.45%)

2.31 hits per line

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

79.57
/src/python/pants/backend/python/lint/ruff/check/rules.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
3✔
5

6
import itertools
3✔
7
from dataclasses import dataclass
3✔
8
from pathlib import PurePath
3✔
9
from typing import AbstractSet, Any
3✔
10

11
from pants.backend.python.lint.ruff.check.skip_field import SkipRuffCheckField
3✔
12
from pants.backend.python.lint.ruff.common import RunRuffRequest, run_ruff
3✔
13
from pants.backend.python.lint.ruff.skip_field import SkipRuffField
3✔
14
from pants.backend.python.lint.ruff.subsystem import Ruff, RuffMode
3✔
15
from pants.backend.python.target_types import (
3✔
16
    InterpreterConstraintsField,
17
    PythonResolveField,
18
    PythonSourceField,
19
)
20
from pants.backend.python.util_rules import pex
3✔
21
from pants.core.goals.fix import FixResult, FixTargetsRequest
3✔
22
from pants.core.goals.lint import REPORT_DIR, LintResult, LintTargetsRequest
3✔
23
from pants.core.util_rules.partitions import Partition, PartitionerType, Partitions
3✔
24
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
3✔
25
from pants.engine.fs import (
3✔
26
    CreateDigest,
27
    DigestSubset,
28
    FileContent,
29
    MergeDigests,
30
    PathGlobs,
31
    RemovePrefix,
32
)
33
from pants.engine.internals.graph import resolve_source_paths
3✔
34
from pants.engine.intrinsics import (
3✔
35
    create_digest,
36
    digest_subset_to_digest,
37
    digest_to_snapshot,
38
    merge_digests,
39
    remove_prefix,
40
)
41
from pants.engine.platform import Platform
3✔
42
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
3✔
43
from pants.engine.target import FieldSet, SourcesPathsRequest, Target
3✔
44
from pants.util.logging import LogLevel
3✔
45
from pants.util.meta import classproperty
3✔
46

47

48
@dataclass(frozen=True)
3✔
49
class RuffCheckFieldSet(FieldSet):
3✔
50
    required_fields = (PythonSourceField,)
3✔
51

52
    source: PythonSourceField
3✔
53
    resolve: PythonResolveField
3✔
54
    interpreter_constraints: InterpreterConstraintsField
3✔
55

56
    @classmethod
3✔
57
    def opt_out(cls, tgt: Target) -> bool:
3✔
58
        return tgt.get(SkipRuffCheckField).value or tgt.get(SkipRuffField).value
2✔
59

60

61
class RuffLintRequest(LintTargetsRequest):
3✔
62
    field_set_type = RuffCheckFieldSet
3✔
63
    tool_subsystem = Ruff  # type: ignore[assignment]
3✔
64
    partitioner_type = PartitionerType.DEFAULT_SINGLE_PARTITION
3✔
65

66
    @classproperty
3✔
67
    def tool_name(cls) -> str:
3✔
68
        return "ruff check"
×
69

70
    @classproperty
3✔
71
    def tool_id(cls) -> str:
3✔
72
        return "ruff-check"
×
73

74

75
class RuffFixRequest(FixTargetsRequest):
3✔
76
    field_set_type = RuffCheckFieldSet
3✔
77
    tool_subsystem = Ruff  # type: ignore[assignment]
3✔
78
    partitioner_type = PartitionerType.CUSTOM
3✔
79

80
    # We don't need to include automatically added lint rules for this RuffFixRequest,
81
    # because these lint rules are already checked by RuffLintRequest.
82
    enable_lint_rules = False
3✔
83

84
    @classproperty
3✔
85
    def tool_name(cls) -> str:
3✔
86
        return "ruff check --fix"
×
87

88
    @classproperty
3✔
89
    def tool_id(cls) -> str:
3✔
90
        return RuffLintRequest.tool_id
×
91

92

93
@dataclass(frozen=True)
3✔
94
class RuffFixPartitionMetadata:
3✔
95
    init_files: tuple[str, ...]
3✔
96

97
    @property
3✔
98
    def description(self) -> None:
3✔
99
        return None
×
100

101

102
def _ancestor_init_files(
3✔
103
    files: tuple[str, ...], candidate_init_files: AbstractSet[str]
104
) -> tuple[str, ...]:
UNCOV
105
    init_files = set[str]()
×
UNCOV
106
    for file in files:
×
UNCOV
107
        for directory in [parent for parent in PurePath(file).parents if str(parent) != "."]:
×
UNCOV
108
            init_file = str(directory / "__init__.py")
×
UNCOV
109
            if init_file in candidate_init_files:
×
UNCOV
110
                init_files.add(init_file)
×
UNCOV
111
    return tuple(sorted(init_files))
×
112

113

114
@rule
3✔
115
async def partition_ruff_fix(
3✔
116
    request: RuffFixRequest.PartitionRequest, ruff: Ruff
117
) -> Partitions[str, RuffFixPartitionMetadata]:
UNCOV
118
    if ruff.skip:
×
119
        return Partitions()
×
120

UNCOV
121
    all_sources_paths = await concurrently(
×
122
        resolve_source_paths(SourcesPathsRequest(field_set.source), **implicitly())
123
        for field_set in request.field_sets
124
    )
UNCOV
125
    files = tuple(
×
126
        sorted(
127
            itertools.chain.from_iterable(
128
                sources_paths.files for sources_paths in all_sources_paths
129
            )
130
        )
131
    )
UNCOV
132
    selected_init_files = {file for file in files if PurePath(file).name == "__init__.py"}
×
UNCOV
133
    metadata = RuffFixPartitionMetadata(_ancestor_init_files(files, selected_init_files))
×
134

UNCOV
135
    return Partitions([Partition(files, metadata)])
×
136

137

138
@rule(desc="Fix with `ruff check --fix`", level=LogLevel.DEBUG)
3✔
139
async def ruff_fix(request: RuffFixRequest.Batch, ruff: Ruff, platform: Platform) -> FixResult:
3✔
140
    # Ruff's isort rules use package marker files to classify imports. The `fix` goal may split
141
    # editable files into smaller batches, so reconstruct selected ancestor `__init__.py` files as
142
    # read-only context while still asking Ruff to edit only the current batch.
143
    init_files = (
2✔
144
        request.partition_metadata.init_files
145
        if isinstance(request.partition_metadata, RuffFixPartitionMetadata)
146
        else ()
147
    )
148
    missing_init_files = tuple(file for file in init_files if file not in request.snapshot.files)
2✔
149
    init_digest = await create_digest(
2✔
150
        CreateDigest(FileContent(file, b"") for file in missing_init_files)
151
    )
152
    snapshot = await digest_to_snapshot(
2✔
153
        await merge_digests(MergeDigests((request.snapshot.digest, init_digest)))
154
    )
155
    result = await run_ruff(
2✔
156
        RunRuffRequest(snapshot=snapshot, files=request.files, mode=RuffMode.FIX),
157
        ruff,
158
        platform,
159
    )
160
    return await FixResult.create(request, result)
2✔
161

162

163
@rule(desc="Lint with `ruff check`", level=LogLevel.DEBUG)
3✔
164
async def ruff_lint(
3✔
165
    request: RuffLintRequest.Batch[RuffCheckFieldSet, Any],
166
    ruff: Ruff,
167
    platform: Platform,
168
) -> LintResult:
169
    source_files = await determine_source_files(
2✔
170
        SourceFilesRequest(field_set.source for field_set in request.elements)
171
    )
172
    result = await run_ruff(
2✔
173
        RunRuffRequest(
174
            snapshot=source_files.snapshot,
175
            files=source_files.snapshot.files,
176
            mode=RuffMode.LINT,
177
        ),
178
        ruff,
179
        platform,
180
    )
181
    report_digest = await digest_subset_to_digest(
2✔
182
        DigestSubset(result.output_digest, PathGlobs([f"{REPORT_DIR}/**"]))
183
    )
184
    report = await remove_prefix(RemovePrefix(report_digest, REPORT_DIR))
2✔
185
    return LintResult.create(request, result, report=report)
2✔
186

187

188
def rules():
3✔
189
    return (
3✔
190
        *collect_rules(),
191
        *RuffFixRequest.rules(),
192
        *RuffLintRequest.rules(),
193
        *pex.rules(),
194
    )
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