• 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

0.0
/src/python/pants/backend/adhoc/code_quality_tool.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from collections.abc import Iterable
×
UNCOV
4
from dataclasses import dataclass
×
UNCOV
5
from typing import ClassVar
×
6

UNCOV
7
from pants.core.goals.fix import Fix, FixFilesRequest, FixResult
×
UNCOV
8
from pants.core.goals.fmt import Fmt, FmtFilesRequest, FmtResult
×
UNCOV
9
from pants.core.goals.lint import Lint, LintFilesRequest, LintResult
×
UNCOV
10
from pants.core.util_rules.adhoc_process_support import (
×
11
    ToolRunner,
12
    ToolRunnerRequest,
13
    create_tool_runner,
14
    prepare_env_vars,
15
)
UNCOV
16
from pants.core.util_rules.adhoc_process_support import rules as adhoc_process_support_rules
×
UNCOV
17
from pants.core.util_rules.partitions import Partitions
×
UNCOV
18
from pants.engine.addresses import Addresses
×
UNCOV
19
from pants.engine.fs import PathGlobs
×
UNCOV
20
from pants.engine.goal import Goal
×
UNCOV
21
from pants.engine.internals.build_files import resolve_address
×
UNCOV
22
from pants.engine.internals.graph import resolve_targets
×
UNCOV
23
from pants.engine.internals.native_engine import (
×
24
    AddressInput,
25
    FilespecMatcher,
26
    MergeDigests,
27
    Snapshot,
28
)
UNCOV
29
from pants.engine.internals.selectors import concurrently
×
UNCOV
30
from pants.engine.intrinsics import digest_to_snapshot, execute_process, merge_digests
×
UNCOV
31
from pants.engine.process import FallibleProcessResult, Process
×
UNCOV
32
from pants.engine.rules import Rule, collect_rules, implicitly, rule
×
UNCOV
33
from pants.engine.target import (
×
34
    COMMON_TARGET_FIELDS,
35
    SpecialCasedDependencies,
36
    StringField,
37
    StringSequenceField,
38
    Target,
39
)
UNCOV
40
from pants.option.option_types import SkipOption
×
UNCOV
41
from pants.option.subsystem import Subsystem
×
UNCOV
42
from pants.util.frozendict import FrozenDict
×
UNCOV
43
from pants.util.strutil import Simplifier, help_text
×
44

45

UNCOV
46
class CodeQualityToolFileGlobIncludeField(StringSequenceField):
×
UNCOV
47
    alias: ClassVar[str] = "file_glob_include"
×
UNCOV
48
    required = True
×
UNCOV
49
    help = help_text(
×
50
        """
51
        Globs that identify files that can be processed by this tool
52

53
        A file matching any of the supplied globs is eligible for processing.
54
        Example: ["**/*.py"]
55
        """
56
    )
57

58

UNCOV
59
class CodeQualityToolFileGlobExcludeField(StringSequenceField):
×
UNCOV
60
    alias: ClassVar[str] = "file_glob_exclude"
×
UNCOV
61
    required = False
×
UNCOV
62
    default = ()
×
UNCOV
63
    help = help_text(
×
64
        """
65
        Globs matching files that should not be processed by this tool
66
        """
67
    )
68

69

UNCOV
70
class CodeQualityToolRunnableField(StringField):
×
UNCOV
71
    alias: ClassVar[str] = "runnable"
×
UNCOV
72
    required = True
×
UNCOV
73
    help = help_text(
×
74
        lambda: f"""
75
        Address to a target that can be invoked by the `run` goal (and does not set
76
        `run_in_sandbox_behavior=NOT_SUPPORTED`). This will be executed along with any arguments
77
        specified by `{CodeQualityToolArgumentsField.alias}`, in a sandbox with that target's transitive
78
        dependencies, along with the transitive dependencies specified by
79
        `{CodeQualityToolExecutionDependenciesField.alias}`.
80
        """
81
    )
82

83

UNCOV
84
class CodeQualityToolArgumentsField(StringSequenceField):
×
UNCOV
85
    alias: ClassVar[str] = "args"
×
UNCOV
86
    default = ()
×
UNCOV
87
    help = help_text(
×
88
        lambda: f"""
89
        Extra arguments to pass into the `{CodeQualityToolRunnableField.alias}` field
90
        before the list of source files
91
        """
92
    )
93

94

UNCOV
95
class CodeQualityToolExecutionDependenciesField(SpecialCasedDependencies):
×
UNCOV
96
    alias: ClassVar[str] = "execution_dependencies"
×
UNCOV
97
    required = False
×
UNCOV
98
    default = None
×
99

UNCOV
100
    help = help_text(
×
101
        """
102
        Additional dependencies that need to be available when running the tool.
103
        Typically used to point to config files.
104
        """
105
    )
106

107

UNCOV
108
class CodeQualityToolRunnableDependenciesField(SpecialCasedDependencies):
×
UNCOV
109
    alias: ClassVar[str] = "runnable_dependencies"
×
UNCOV
110
    required = False
×
UNCOV
111
    default = None
×
112

UNCOV
113
    help = help_text(
×
114
        lambda: f"""
115
        The runnable dependencies for this command.
116

117
        Dependencies specified here are those required to exist on the `PATH` to make the command
118
        complete successfully (interpreters specified in a `#!` command, etc). Note that these
119
        dependencies will be made available on the `PATH` with the name of the target.
120

121
        See also `{CodeQualityToolExecutionDependenciesField.alias}`.
122
        """
123
    )
124

125

UNCOV
126
class CodeQualityToolTarget(Target):
×
UNCOV
127
    alias: ClassVar[str] = "code_quality_tool"
×
UNCOV
128
    core_fields = (
×
129
        *COMMON_TARGET_FIELDS,
130
        CodeQualityToolRunnableField,
131
        CodeQualityToolArgumentsField,
132
        CodeQualityToolExecutionDependenciesField,
133
        CodeQualityToolRunnableDependenciesField,
134
        CodeQualityToolFileGlobIncludeField,
135
        CodeQualityToolFileGlobExcludeField,
136
    )
137

UNCOV
138
    help = help_text(
×
139
        lambda: f"""
140
        Configure a runnable to use as a linter, fixer or formatter
141

142
        Example BUILD file:
143

144
            {CodeQualityToolTarget.alias}(
145
                {CodeQualityToolRunnableField.alias}=":flake8_req",
146
                {CodeQualityToolExecutionDependenciesField.alias}=[":config_file"],
147
                {CodeQualityToolFileGlobIncludeField.alias}=["**/*.py"],
148
            )
149
        """
150
    )
151

152

UNCOV
153
@dataclass(frozen=True)
×
UNCOV
154
class CodeQualityToolAddressString:
×
155
    address: str
156

157

UNCOV
158
@dataclass(frozen=True)
×
UNCOV
159
class CodeQualityTool:
×
160
    runnable_address_str: str
161
    args: tuple[str, ...]
162
    execution_dependencies: tuple[str, ...]
163
    runnable_dependencies: tuple[str, ...]
164
    file_glob_include: tuple[str, ...]
165
    file_glob_exclude: tuple[str, ...]
166
    target: Target
167

168

UNCOV
169
@rule
×
UNCOV
170
async def find_code_quality_tool(request: CodeQualityToolAddressString) -> CodeQualityTool:
×
171
    tool_address = await resolve_address(
×
172
        **implicitly(
173
            {
174
                AddressInput.parse(
175
                    request.address, description_of_origin="code quality tool target"
176
                ): AddressInput
177
            }
178
        )
179
    )
180

181
    addresses = Addresses((tool_address,))
×
182
    addresses.expect_single()
×
183

184
    tool_targets = await resolve_targets(**implicitly({addresses: Addresses}))
×
185
    target = tool_targets[0]
×
186
    runnable_address_str = target[CodeQualityToolRunnableField].value
×
187
    if not runnable_address_str:
×
188
        raise Exception(f"Must supply a value for `runnable` for {request.address}.")
×
189

190
    return CodeQualityTool(
×
191
        runnable_address_str=runnable_address_str,
192
        execution_dependencies=target[CodeQualityToolExecutionDependenciesField].value or (),
193
        runnable_dependencies=target[CodeQualityToolRunnableDependenciesField].value or (),
194
        args=target[CodeQualityToolArgumentsField].value or (),
195
        file_glob_include=target[CodeQualityToolFileGlobIncludeField].value or (),
196
        file_glob_exclude=target[CodeQualityToolFileGlobExcludeField].value or (),
197
        target=target,
198
    )
199

200

UNCOV
201
@dataclass(frozen=True)
×
UNCOV
202
class CodeQualityToolBatch:
×
203
    runner: ToolRunner
204
    sources_snapshot: Snapshot
205
    output_files: tuple[str, ...]
206

207

UNCOV
208
@rule
×
UNCOV
209
async def process_files(batch: CodeQualityToolBatch) -> FallibleProcessResult:
×
210
    runner = batch.runner
×
211

212
    input_digest = await merge_digests(MergeDigests((runner.digest, batch.sources_snapshot.digest)))
×
213

214
    env_vars = await prepare_env_vars(
×
215
        runner.extra_env,
216
        (),
217
        extra_paths=runner.extra_paths,
218
        description_of_origin="code quality tool",
219
    )
220

221
    result = await execute_process(
×
222
        **implicitly(
223
            Process(
224
                argv=tuple(runner.args + batch.sources_snapshot.files),
225
                description="Running code quality tool",
226
                input_digest=input_digest,
227
                append_only_caches=runner.append_only_caches,
228
                immutable_input_digests=FrozenDict.frozen(runner.immutable_input_digests),
229
                env=env_vars,
230
                output_files=batch.output_files,
231
            )
232
        )
233
    )
234
    return result
×
235

236

UNCOV
237
@rule
×
UNCOV
238
async def runner_request_for_code_quality_tool(
×
239
    cqt: CodeQualityTool,
240
) -> ToolRunnerRequest:
241
    return ToolRunnerRequest(
×
242
        runnable_address_str=cqt.runnable_address_str,
243
        args=cqt.args,
244
        execution_dependencies=cqt.execution_dependencies,
245
        runnable_dependencies=cqt.runnable_dependencies,
246
        target=cqt.target,
247
    )
248

249

UNCOV
250
_name_to_supported_goal = {g.name: g for g in (Lint, Fmt, Fix)}
×
251

252

UNCOV
253
class CodeQualityToolUnsupportedGoalError(Exception):
×
254
    """Raised when a rule builder is instantiated for an unrecognized or unsupported goal."""
255

256

UNCOV
257
@dataclass
×
UNCOV
258
class CodeQualityToolRuleBuilder:
×
259
    goal: str
260
    target: str
261
    name: str
262
    scope: str
263

UNCOV
264
    @property
×
UNCOV
265
    def goal_type(self) -> type[Goal]:
×
UNCOV
266
        return _name_to_supported_goal[self.goal]
×
267

UNCOV
268
    def __post_init__(self):
×
UNCOV
269
        if self.goal not in _name_to_supported_goal:
×
UNCOV
270
            raise CodeQualityToolUnsupportedGoalError(
×
271
                f"""goal must be one of {sorted(_name_to_supported_goal)}"""
272
            )
273

UNCOV
274
    def rules(self) -> Iterable[Rule]:
×
UNCOV
275
        if self.goal_type is Fmt:
×
UNCOV
276
            return self._build_fmt_rules()
×
UNCOV
277
        elif self.goal_type is Fix:
×
UNCOV
278
            return self._build_fix_rules()
×
UNCOV
279
        elif self.goal_type is Lint:
×
UNCOV
280
            return self._build_lint_rules()
×
281
        else:
282
            raise ValueError(f"Unsupported goal for code quality tool: {self.goal}")
×
283

UNCOV
284
    def _build_lint_rules(self) -> Iterable[Rule]:
×
UNCOV
285
        class CodeQualityToolInstance(Subsystem):
×
UNCOV
286
            options_scope = self.scope
×
UNCOV
287
            name = self.name
×
UNCOV
288
            help = f"{self.goal.capitalize()} with {self.name}. Tool defined in {self.target}"
×
289

UNCOV
290
            skip = SkipOption("lint")
×
291

UNCOV
292
        class CodeQualityProcessingRequest(LintFilesRequest):
×
UNCOV
293
            tool_subsystem = CodeQualityToolInstance  # type: ignore[assignment]
×
294

UNCOV
295
        @rule(canonical_name_suffix=self.scope)
×
UNCOV
296
        async def partition_inputs(
×
297
            request: CodeQualityProcessingRequest.PartitionRequest,
298
            subsystem: CodeQualityToolInstance,
299
        ) -> Partitions:
300
            if subsystem.skip:
×
301
                return Partitions()
×
302

303
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
304

305
            matching_filepaths = FilespecMatcher(
×
306
                includes=cqt.file_glob_include,
307
                excludes=cqt.file_glob_exclude,
308
            ).matches(request.files)
309

310
            return Partitions.single_partition(sorted(matching_filepaths))
×
311

UNCOV
312
        @rule(canonical_name_suffix=self.scope)
×
UNCOV
313
        async def run_code_quality(request: CodeQualityProcessingRequest.Batch) -> LintResult:
×
314
            sources_snapshot, code_quality_tool_runner = await concurrently(
×
315
                digest_to_snapshot(**implicitly(PathGlobs(request.elements))),
316
                create_tool_runner(**implicitly(CodeQualityToolAddressString(address=self.target))),
317
            )
318

319
            proc_result = await process_files(
×
320
                CodeQualityToolBatch(
321
                    runner=code_quality_tool_runner,
322
                    sources_snapshot=sources_snapshot,
323
                    output_files=(),
324
                ),
325
            )
326

327
            return LintResult.create(request, process_result=proc_result)
×
328

UNCOV
329
        namespace = dict(locals())
×
330

UNCOV
331
        return [
×
332
            *collect_rules(namespace),
333
            *CodeQualityProcessingRequest.rules(),
334
        ]
335

UNCOV
336
    def _build_fmt_rules(self) -> Iterable[Rule]:
×
UNCOV
337
        class CodeQualityToolInstance(Subsystem):
×
UNCOV
338
            options_scope = self.scope
×
UNCOV
339
            name = self.name
×
UNCOV
340
            help = f"{self.goal.capitalize()} with {self.name}. Tool defined in {self.target}"
×
341

UNCOV
342
            skip = SkipOption("lint", "fmt")
×
343

UNCOV
344
        class CodeQualityProcessingRequest(FmtFilesRequest):
×
UNCOV
345
            tool_subsystem = CodeQualityToolInstance  # type: ignore[assignment]
×
346

UNCOV
347
        @rule(canonical_name_suffix=self.scope)
×
UNCOV
348
        async def partition_inputs(
×
349
            request: CodeQualityProcessingRequest.PartitionRequest,
350
            subsystem: CodeQualityToolInstance,
351
        ) -> Partitions:
352
            if subsystem.skip:
×
353
                return Partitions()
×
354

355
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
356

357
            matching_filepaths = FilespecMatcher(
×
358
                includes=cqt.file_glob_include,
359
                excludes=cqt.file_glob_exclude,
360
            ).matches(request.files)
361

362
            return Partitions.single_partition(sorted(matching_filepaths))
×
363

UNCOV
364
        @rule(canonical_name_suffix=self.scope)
×
UNCOV
365
        async def run_code_quality(request: CodeQualityProcessingRequest.Batch) -> FmtResult:
×
366
            sources_snapshot = request.snapshot
×
367

368
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
369
            code_quality_tool_runner_request = await runner_request_for_code_quality_tool(cqt)
×
370
            code_quality_tool_runner = await create_tool_runner(code_quality_tool_runner_request)
×
371

372
            proc_result = await process_files(
×
373
                CodeQualityToolBatch(
374
                    runner=code_quality_tool_runner,
375
                    sources_snapshot=sources_snapshot,
376
                    output_files=request.files,
377
                ),
378
            )
379

380
            output = await digest_to_snapshot(proc_result.output_digest)
×
381

382
            return FmtResult(
×
383
                input=request.snapshot,
384
                output=output,
385
                stdout=Simplifier().simplify(proc_result.stdout),
386
                stderr=Simplifier().simplify(proc_result.stderr),
387
                tool_name=request.tool_name,
388
            )
389

UNCOV
390
        namespace = dict(locals())
×
391

UNCOV
392
        return [
×
393
            *collect_rules(namespace),
394
            *CodeQualityProcessingRequest.rules(),
395
        ]
396

UNCOV
397
    def _build_fix_rules(self) -> Iterable[Rule]:
×
UNCOV
398
        class CodeQualityToolInstance(Subsystem):
×
UNCOV
399
            options_scope = self.scope
×
UNCOV
400
            name = self.name
×
UNCOV
401
            help = f"{self.goal.capitalize()} with {self.name}. Tool defined in {self.target}"
×
402

UNCOV
403
            skip = SkipOption("lint", "fmt", "fix")
×
404

UNCOV
405
        class CodeQualityProcessingRequest(FixFilesRequest):
×
UNCOV
406
            tool_subsystem = CodeQualityToolInstance  # type: ignore[assignment]
×
407

UNCOV
408
        @rule(canonical_name_suffix=self.scope)
×
UNCOV
409
        async def partition_inputs(
×
410
            request: CodeQualityProcessingRequest.PartitionRequest,
411
            subsystem: CodeQualityToolInstance,
412
        ) -> Partitions:
413
            if subsystem.skip:
×
414
                return Partitions()
×
415

416
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
417

418
            matching_filepaths = FilespecMatcher(
×
419
                includes=cqt.file_glob_include,
420
                excludes=cqt.file_glob_exclude,
421
            ).matches(request.files)
422

423
            return Partitions.single_partition(sorted(matching_filepaths))
×
424

UNCOV
425
        @rule(canonical_name_suffix=self.scope)
×
UNCOV
426
        async def run_code_quality(request: CodeQualityProcessingRequest.Batch) -> FixResult:
×
427
            sources_snapshot = request.snapshot
×
428

429
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
430
            code_quality_tool_runner_request = await runner_request_for_code_quality_tool(cqt)
×
431
            code_quality_tool_runner = await create_tool_runner(code_quality_tool_runner_request)
×
432

433
            proc_result = await process_files(
×
434
                CodeQualityToolBatch(
435
                    runner=code_quality_tool_runner,
436
                    sources_snapshot=sources_snapshot,
437
                    output_files=request.files,
438
                ),
439
            )
440

441
            output = await digest_to_snapshot(proc_result.output_digest)
×
442

443
            return FixResult(
×
444
                input=request.snapshot,
445
                output=output,
446
                stdout=Simplifier().simplify(proc_result.stdout),
447
                stderr=Simplifier().simplify(proc_result.stderr),
448
                tool_name=request.tool_name,
449
            )
450

UNCOV
451
        namespace = dict(locals())
×
452

UNCOV
453
        return [
×
454
            *collect_rules(namespace),
455
            *CodeQualityProcessingRequest.rules(),
456
        ]
457

458

UNCOV
459
def base_rules():
×
UNCOV
460
    return [
×
461
        *collect_rules(),
462
        *adhoc_process_support_rules(),
463
    ]
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