• 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/python/lint/pylint/rules.py
1
# Copyright 2020 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
from dataclasses import dataclass
×
7

UNCOV
8
import packaging
×
9

UNCOV
10
from pants.backend.python.lint.pylint.subsystem import (
×
11
    Pylint,
12
    PylintFieldSet,
13
    PylintFirstPartyPlugins,
14
)
UNCOV
15
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
16
from pants.backend.python.util_rules import pex_from_targets
×
UNCOV
17
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
18
from pants.backend.python.util_rules.partition import (
×
19
    _partition_by_interpreter_constraints_and_resolve,
20
)
UNCOV
21
from pants.backend.python.util_rules.pex import (
×
22
    Pex,
23
    PexRequest,
24
    VenvPexProcess,
25
    VenvPexRequest,
26
    create_pex,
27
    create_venv_pex,
28
    determine_pex_resolve_info,
29
)
UNCOV
30
from pants.backend.python.util_rules.pex_environment import PexEnvironment
×
UNCOV
31
from pants.backend.python.util_rules.pex_from_targets import RequirementsPexRequest
×
UNCOV
32
from pants.backend.python.util_rules.python_sources import (
×
33
    PythonSourceFilesRequest,
34
    prepare_python_sources,
35
)
UNCOV
36
from pants.core.goals.lint import REPORT_DIR, LintResult, LintTargetsRequest, Partitions
×
UNCOV
37
from pants.core.util_rules.config_files import find_config_file
×
UNCOV
38
from pants.core.util_rules.partitions import Partition
×
UNCOV
39
from pants.engine.fs import CreateDigest, Directory, MergeDigests, RemovePrefix
×
UNCOV
40
from pants.engine.internals.graph import resolve_coarsened_targets as coarsened_targets_get
×
UNCOV
41
from pants.engine.intrinsics import create_digest, execute_process, merge_digests, remove_prefix
×
UNCOV
42
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
43
from pants.engine.target import CoarsenedTargets, CoarsenedTargetsRequest
×
UNCOV
44
from pants.util.logging import LogLevel
×
UNCOV
45
from pants.util.strutil import pluralize
×
46

47

UNCOV
48
@dataclass(frozen=True)
×
UNCOV
49
class PartitionMetadata:
×
UNCOV
50
    coarsened_targets: CoarsenedTargets
×
51
    # NB: These are the same across every element in a partition
UNCOV
52
    resolve_description: str | None
×
UNCOV
53
    interpreter_constraints: InterpreterConstraints
×
54

UNCOV
55
    @property
×
UNCOV
56
    def description(self) -> str:
×
UNCOV
57
        ics = str(sorted(str(c) for c in self.interpreter_constraints))
×
UNCOV
58
        return f"{self.resolve_description}, {ics}" if self.resolve_description else ics
×
59

60

UNCOV
61
class PylintRequest(LintTargetsRequest):
×
UNCOV
62
    field_set_type = PylintFieldSet
×
UNCOV
63
    tool_subsystem = Pylint  # type: ignore[assignment]
×
64

65

UNCOV
66
def generate_argv(field_sets: tuple[PylintFieldSet, ...], pylint: Pylint) -> tuple[str, ...]:
×
67
    args = []
×
68
    if pylint.config is not None:
×
69
        args.append(f"--rcfile={pylint.config}")
×
70
    args.append("--jobs={pants_concurrency}")
×
71
    args.extend(pylint.args)
×
72
    args.extend(field_set.source.file_path for field_set in field_sets)
×
73
    return tuple(args)
×
74

75

UNCOV
76
@rule(desc="Determine if necessary to partition Pylint input", level=LogLevel.DEBUG)
×
UNCOV
77
async def partition_pylint(
×
78
    request: PylintRequest.PartitionRequest[PylintFieldSet],
79
    pylint: Pylint,
80
    python_setup: PythonSetup,
81
    first_party_plugins: PylintFirstPartyPlugins,
82
) -> Partitions[PylintFieldSet, PartitionMetadata]:
83
    if pylint.skip:
×
84
        return Partitions()
×
85

86
    first_party_ics = InterpreterConstraints.create_from_compatibility_fields(
×
87
        first_party_plugins.interpreter_constraints_and_resolve_fields, python_setup
88
    )
89

90
    resolve_and_interpreter_constraints_to_field_sets = (
×
91
        _partition_by_interpreter_constraints_and_resolve(request.field_sets, python_setup)
92
    )
93

94
    coarsened_targets = await coarsened_targets_get(
×
95
        CoarsenedTargetsRequest(field_set.address for field_set in request.field_sets),
96
        **implicitly(),
97
    )
98
    coarsened_targets_by_address = coarsened_targets.by_address()
×
99

100
    return Partitions(
×
101
        Partition(
102
            tuple(field_sets),
103
            PartitionMetadata(
104
                CoarsenedTargets(
105
                    coarsened_targets_by_address[field_set.address] for field_set in field_sets
106
                ),
107
                resolve if len(python_setup.resolves) > 1 else None,
108
                InterpreterConstraints.merge((interpreter_constraints, first_party_ics)),
109
            ),
110
        )
111
        for (
112
            resolve,
113
            interpreter_constraints,
114
        ), field_sets in resolve_and_interpreter_constraints_to_field_sets.items()
115
    )
116

117

UNCOV
118
@rule(desc="Lint using Pylint", level=LogLevel.DEBUG)
×
UNCOV
119
async def run_pylint(
×
120
    request: PylintRequest.Batch[PylintFieldSet, PartitionMetadata],
121
    pylint: Pylint,
122
    first_party_plugins: PylintFirstPartyPlugins,
123
    pex_environment: PexEnvironment,
124
) -> LintResult:
125
    assert request.partition_metadata is not None
×
126

127
    # The coarsened targets in the incoming request are for all targets in the request's original
128
    # partition. Since the core `lint` logic re-batches inputs according to `[lint].batch_size`,
129
    # this could be many more targets than are actually needed to lint the specific batch of files
130
    # received by this rule. Subset the CTs one more time here to only those that are relevant.
131
    all_coarsened_targets_by_address = request.partition_metadata.coarsened_targets.by_address()
×
132
    coarsened_targets = CoarsenedTargets(
×
133
        all_coarsened_targets_by_address[field_set.address] for field_set in request.elements
134
    )
135
    coarsened_closure = tuple(coarsened_targets.closure())
×
136

137
    requirements_pex_get = create_pex(
×
138
        **implicitly(
139
            RequirementsPexRequest(
140
                (target.address for target in coarsened_closure),
141
                # NB: These constraints must be identical to the other PEXes. Otherwise, we risk using
142
                # a different version for the requirements than the other two PEXes, which can result
143
                # in a PEX runtime error about missing dependencies.
144
                hardcoded_interpreter_constraints=request.partition_metadata.interpreter_constraints,
145
            )
146
        )
147
    )
148

149
    pylint_pex_get = create_pex(
×
150
        pylint.to_pex_request(
151
            interpreter_constraints=request.partition_metadata.interpreter_constraints,
152
            extra_requirements=first_party_plugins.requirement_strings,
153
        )
154
    )
155

156
    sources_get = prepare_python_sources(
×
157
        PythonSourceFilesRequest(coarsened_closure), **implicitly()
158
    )
159
    # Ensure that the empty report dir exists.
160
    report_directory_digest_get = create_digest(CreateDigest([Directory(REPORT_DIR)]))
×
161

162
    (
×
163
        pylint_pex,
164
        requirements_pex,
165
        sources,
166
        report_directory,
167
    ) = await concurrently(
168
        pylint_pex_get,
169
        requirements_pex_get,
170
        sources_get,
171
        report_directory_digest_get,
172
    )
173

174
    pylint_pex_info = await determine_pex_resolve_info(**implicitly({pylint_pex: Pex}))
×
175
    astroid_info = pylint_pex_info.find("astroid")
×
176
    # Astroid is a transitive dependency of pylint and should always be available in the pex.
177
    assert astroid_info
×
178

179
    pylint_runner_pex, config_files = await concurrently(
×
180
        create_venv_pex(
181
            VenvPexRequest(
182
                PexRequest(
183
                    output_filename="pylint_runner.pex",
184
                    interpreter_constraints=request.partition_metadata.interpreter_constraints,
185
                    main=pylint.main,
186
                    internal_only=True,
187
                    pex_path=[pylint_pex, requirements_pex],
188
                ),
189
                pex_environment.in_sandbox(working_directory=None),
190
                # Astroid < 2.9.1 had a regression that prevented the use of symlinks:
191
                # https://github.com/PyCQA/pylint/issues/1470
192
                site_packages_copies=(astroid_info.version < packaging.version.Version("2.9.1")),
193
            ),
194
            **implicitly(),
195
        ),
196
        find_config_file(pylint.config_request(sources.source_files.snapshot.dirs)),
197
    )
198

199
    pythonpath = list(sources.source_roots)
×
200
    if first_party_plugins:
×
201
        pythonpath.append(first_party_plugins.PREFIX)
×
202

203
    input_digest = await merge_digests(
×
204
        MergeDigests(
205
            (
206
                config_files.snapshot.digest,
207
                first_party_plugins.sources_digest,
208
                sources.source_files.snapshot.digest,
209
                report_directory,
210
            )
211
        )
212
    )
213

214
    result = await execute_process(
×
215
        **implicitly(
216
            VenvPexProcess(
217
                pylint_runner_pex,
218
                argv=generate_argv(request.elements, pylint),
219
                input_digest=input_digest,
220
                output_directories=(REPORT_DIR,),
221
                extra_env={"PEX_EXTRA_SYS_PATH": ":".join(pythonpath)},
222
                concurrency_available=len(request.elements),
223
                description=f"Run Pylint on {pluralize(len(request.elements), 'target')}.",
224
                level=LogLevel.DEBUG,
225
            )
226
        )
227
    )
228
    report = await remove_prefix(RemovePrefix(result.output_digest, REPORT_DIR))
×
229
    return LintResult.create(request, result, report=report)
×
230

231

UNCOV
232
def rules():
×
UNCOV
233
    return (
×
234
        *collect_rules(),
235
        *PylintRequest.rules(),
236
        *pex_from_targets.rules(),
237
    )
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