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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 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:
×
UNCOV
155
    address: str
×
156

157

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

169

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

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

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

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

202

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

209

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

214
    input_digest = await merge_digests(MergeDigests((runner.digest, batch.sources_snapshot.digest)))
×
215

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

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

238

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

252

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

255

UNCOV
256
class CodeQualityToolUnsupportedGoalError(Exception):
×
257
    """Raised when a rule builder is instantiated for an unrecognized or unsupported goal."""
258

259

UNCOV
260
@dataclass
×
UNCOV
261
class CodeQualityToolRuleBuilder:
×
UNCOV
262
    goal: str
×
UNCOV
263
    target: str
×
UNCOV
264
    name: str
×
UNCOV
265
    scope: str
×
266

UNCOV
267
    @property
×
UNCOV
268
    def goal_type(self) -> type[Goal]:
×
UNCOV
269
        return _name_to_supported_goal[self.goal]
×
270

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

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

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

UNCOV
293
            skip = SkipOption("lint")
×
294

UNCOV
295
        class CodeQualityProcessingRequest(LintFilesRequest):
×
UNCOV
296
            tool_subsystem = CodeQualityToolInstance  # type: ignore[assignment]
×
297

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

306
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
307

308
            matching_filepaths = FilespecMatcher(
×
309
                includes=cqt.file_glob_include,
310
                excludes=cqt.file_glob_exclude,
311
            ).matches(request.files)
312

313
            return Partitions.single_partition(sorted(matching_filepaths))
×
314

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

322
            proc_result = await process_files(
×
323
                CodeQualityToolBatch(
324
                    runner=code_quality_tool_runner,
325
                    sources_snapshot=sources_snapshot,
326
                    output_files=(),
327
                ),
328
            )
329

330
            return LintResult.create(request, process_result=proc_result)
×
331

UNCOV
332
        namespace = dict(locals())
×
333

UNCOV
334
        return [
×
335
            *collect_rules(namespace),
336
            *CodeQualityProcessingRequest.rules(),
337
        ]
338

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

UNCOV
345
            skip = SkipOption("lint", "fmt")
×
346

UNCOV
347
        class CodeQualityProcessingRequest(FmtFilesRequest):
×
UNCOV
348
            tool_subsystem = CodeQualityToolInstance  # type: ignore[assignment]
×
349

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

358
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
359

360
            matching_filepaths = FilespecMatcher(
×
361
                includes=cqt.file_glob_include,
362
                excludes=cqt.file_glob_exclude,
363
            ).matches(request.files)
364

365
            return Partitions.single_partition(sorted(matching_filepaths))
×
366

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

371
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
372
            code_quality_tool_runner_request = await runner_request_for_code_quality_tool(cqt)
×
373
            code_quality_tool_runner = await create_tool_runner(code_quality_tool_runner_request)
×
374

375
            proc_result = await process_files(
×
376
                CodeQualityToolBatch(
377
                    runner=code_quality_tool_runner,
378
                    sources_snapshot=sources_snapshot,
379
                    output_files=request.files,
380
                ),
381
            )
382

383
            output = await digest_to_snapshot(proc_result.output_digest)
×
384

385
            return FmtResult(
×
386
                input=request.snapshot,
387
                output=output,
388
                stdout=Simplifier().simplify(proc_result.stdout),
389
                stderr=Simplifier().simplify(proc_result.stderr),
390
                tool_name=request.tool_name,
391
            )
392

UNCOV
393
        namespace = dict(locals())
×
394

UNCOV
395
        return [
×
396
            *collect_rules(namespace),
397
            *CodeQualityProcessingRequest.rules(),
398
        ]
399

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

UNCOV
406
            skip = SkipOption("lint", "fmt", "fix")
×
407

UNCOV
408
        class CodeQualityProcessingRequest(FixFilesRequest):
×
UNCOV
409
            tool_subsystem = CodeQualityToolInstance  # type: ignore[assignment]
×
410

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

419
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
420

421
            matching_filepaths = FilespecMatcher(
×
422
                includes=cqt.file_glob_include,
423
                excludes=cqt.file_glob_exclude,
424
            ).matches(request.files)
425

426
            return Partitions.single_partition(sorted(matching_filepaths))
×
427

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

432
            cqt = await find_code_quality_tool(CodeQualityToolAddressString(address=self.target))
×
433
            code_quality_tool_runner_request = await runner_request_for_code_quality_tool(cqt)
×
434
            code_quality_tool_runner = await create_tool_runner(code_quality_tool_runner_request)
×
435

436
            proc_result = await process_files(
×
437
                CodeQualityToolBatch(
438
                    runner=code_quality_tool_runner,
439
                    sources_snapshot=sources_snapshot,
440
                    output_files=request.files,
441
                ),
442
            )
443

444
            output = await digest_to_snapshot(proc_result.output_digest)
×
445

446
            return FixResult(
×
447
                input=request.snapshot,
448
                output=output,
449
                stdout=Simplifier().simplify(proc_result.stdout),
450
                stderr=Simplifier().simplify(proc_result.stderr),
451
                tool_name=request.tool_name,
452
            )
453

UNCOV
454
        namespace = dict(locals())
×
455

UNCOV
456
        return [
×
457
            *collect_rules(namespace),
458
            *CodeQualityProcessingRequest.rules(),
459
        ]
460

461

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