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

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

0.0
/src/python/pants/backend/python/typecheck/pyright/rules.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
×
5

6
import json
×
7
import logging
×
8
import os
×
9
from collections.abc import Iterable
×
10
from dataclasses import dataclass, replace
×
11

12
import toml
×
13

14
from pants.backend.javascript.subsystems import nodejs_tool
×
15
from pants.backend.javascript.subsystems.nodejs import NodeJS
×
16
from pants.backend.javascript.subsystems.nodejs_tool import prepare_tool_process
×
17
from pants.backend.python.subsystems.setup import PythonSetup
×
18
from pants.backend.python.target_types import (
×
19
    InterpreterConstraintsField,
20
    PythonResolveField,
21
    PythonSourceField,
22
)
23
from pants.backend.python.typecheck.pyright.skip_field import SkipPyrightField
×
24
from pants.backend.python.typecheck.pyright.subsystem import Pyright
×
25
from pants.backend.python.util_rules import pex_from_targets
×
26
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
27
from pants.backend.python.util_rules.partition import (
×
28
    _partition_by_interpreter_constraints_and_resolve,
29
)
30
from pants.backend.python.util_rules.pex import (
×
31
    PexRequest,
32
    VenvPexProcess,
33
    VenvPexRequest,
34
    create_pex,
35
    create_venv_pex,
36
)
37
from pants.backend.python.util_rules.pex_environment import PexEnvironment
×
38
from pants.backend.python.util_rules.pex_from_targets import RequirementsPexRequest
×
39
from pants.backend.python.util_rules.python_sources import (
×
40
    PythonSourceFilesRequest,
41
    prepare_python_sources,
42
)
43
from pants.core.goals.check import CheckRequest, CheckResult, CheckResults
×
44
from pants.core.util_rules import config_files
×
45
from pants.core.util_rules.config_files import ConfigFiles, find_config_file
×
46
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
×
47
from pants.engine.collection import Collection
×
48
from pants.engine.fs import CreateDigest, FileContent
×
49
from pants.engine.internals.graph import resolve_coarsened_targets as coarsened_targets_get
×
50
from pants.engine.internals.native_engine import Digest, MergeDigests
×
51
from pants.engine.internals.selectors import concurrently
×
52
from pants.engine.intrinsics import (
×
53
    create_digest,
54
    execute_process,
55
    get_digest_contents,
56
    merge_digests,
57
)
58
from pants.engine.process import ProcessCacheScope, execute_process_or_raise
×
59
from pants.engine.rules import Rule, collect_rules, implicitly, rule
×
60
from pants.engine.target import CoarsenedTargets, CoarsenedTargetsRequest, FieldSet, Target
×
61
from pants.engine.unions import UnionRule
×
62
from pants.util.logging import LogLevel
×
63
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
×
64
from pants.util.strutil import pluralize
×
65

66
logger = logging.getLogger(__name__)
×
67

68

69
@dataclass(frozen=True)
×
70
class PyrightFieldSet(FieldSet):
×
71
    required_fields = (PythonSourceField,)
×
72

73
    sources: PythonSourceField
×
74
    resolve: PythonResolveField
×
75
    interpreter_constraints: InterpreterConstraintsField
×
76

77
    @classmethod
×
78
    def opt_out(cls, tgt: Target) -> bool:
×
79
        return tgt.get(SkipPyrightField).value
×
80

81

82
class PyrightRequest(CheckRequest):
×
83
    field_set_type = PyrightFieldSet
×
84
    tool_name = Pyright.options_scope
×
85

86

87
@dataclass(frozen=True)
×
88
class PyrightPartition:
×
89
    field_sets: FrozenOrderedSet[PyrightFieldSet]
×
90
    root_targets: CoarsenedTargets
×
91
    resolve_description: str | None
×
92
    interpreter_constraints: InterpreterConstraints
×
93

94
    def description(self) -> str:
×
95
        ics = str(sorted(str(c) for c in self.interpreter_constraints))
×
96
        return f"{self.resolve_description}, {ics}" if self.resolve_description else ics
×
97

98

99
class PyrightPartitions(Collection[PyrightPartition]):
×
100
    pass
×
101

102

103
async def _patch_config_file(
×
104
    config_files: ConfigFiles, venv_dir: str, source_roots: Iterable[str]
105
) -> Digest:
106
    """Patch the Pyright config file to use the incoming venv directory (from
107
    requirements_venv_pex). If there is no config file, create a dummy pyrightconfig.json with the
108
    `venv` key populated.
109

110
    The incoming venv directory works alongside the `--venvpath` CLI argument.
111

112
    Additionally, add source roots to the `extraPaths` key in the config file.
113
    """
114

115
    source_roots_list = list(source_roots)
×
116
    if not config_files.snapshot.files:
×
117
        # venv workaround as per: https://github.com/microsoft/pyright/issues/4051
118
        generated_config: dict[str, str | list[str]] = {
×
119
            "venv": venv_dir,
120
            "extraPaths": source_roots_list,
121
        }
122
        return await create_digest(
×
123
            CreateDigest(
124
                [
125
                    FileContent(
126
                        "pyrightconfig.json",
127
                        json.dumps(generated_config).encode(),
128
                    )
129
                ]
130
            )
131
        )
132

133
    config_contents = await get_digest_contents(config_files.snapshot.digest)
×
134
    new_files: list[FileContent] = []
×
135
    for file in config_contents:
×
136
        # This only supports a single json config file in the root of the project
137
        # https://github.com/pantsbuild/pants/issues/17816 tracks supporting multiple config files and workspaces
138
        if file.path == "pyrightconfig.json":
×
139
            json_config = json.loads(file.content)
×
140
            json_config["venv"] = venv_dir
×
141
            json_extra_paths: list[str] = json_config.get("extraPaths", [])
×
142
            json_config["extraPaths"] = list(OrderedSet(json_extra_paths + source_roots_list))
×
143
            new_content = json.dumps(json_config).encode()
×
144
            new_files.append(replace(file, content=new_content))
×
145

146
        # This only supports a single pyproject.toml file in the root of the project
147
        # https://github.com/pantsbuild/pants/issues/17816 tracks supporting multiple config files and workspaces
148
        elif file.path == "pyproject.toml":
×
149
            toml_config = toml.loads(file.content.decode())
×
150
            pyright_config = toml_config["tool"]["pyright"]
×
151
            pyright_config["venv"] = venv_dir
×
152
            toml_extra_paths: list[str] = pyright_config.get("extraPaths", [])
×
153
            pyright_config["extraPaths"] = list(OrderedSet(toml_extra_paths + source_roots_list))
×
154
            new_content = toml.dumps(toml_config).encode()
×
155
            new_files.append(replace(file, content=new_content))
×
156

157
    return await create_digest(CreateDigest(new_files))
×
158

159

160
@rule(
×
161
    desc="Pyright typecheck each partition based on its interpreter_constraints",
162
    level=LogLevel.DEBUG,
163
)
164
async def pyright_typecheck_partition(
×
165
    partition: PyrightPartition,
166
    pyright: Pyright,
167
    pex_environment: PexEnvironment,
168
    nodejs: NodeJS,
169
) -> CheckResult:
170
    root_sources_get = determine_source_files(
×
171
        SourceFilesRequest(fs.sources for fs in partition.field_sets)
172
    )
173

174
    # Grab the closure of the root source files to be typechecked
175
    transitive_sources_get = prepare_python_sources(
×
176
        PythonSourceFilesRequest(partition.root_targets.closure()), **implicitly()
177
    )
178

179
    # See `requirements_venv_pex` for how this will get wrapped in a `VenvPex`.
180
    requirements_pex_get = create_pex(
×
181
        **implicitly(
182
            RequirementsPexRequest(
183
                (fs.address for fs in partition.field_sets),
184
                hardcoded_interpreter_constraints=partition.interpreter_constraints,
185
            )
186
        )
187
    )
188

189
    # Look for any/all of the Pyright configuration files (the config is modified below
190
    # for the `venv` workaround)
191
    config_files_get = find_config_file(pyright.config_request())
×
192

193
    root_sources, transitive_sources, requirements_pex, config_files = await concurrently(
×
194
        root_sources_get,
195
        transitive_sources_get,
196
        requirements_pex_get,
197
        config_files_get,
198
    )
199

200
    # This is a workaround for https://github.com/pantsbuild/pants/issues/19946.
201
    # complete_pex_env needs to be created here so that the test `test_passing_cache_clear`
202
    # test can pass using the appropriate caching directory.
203
    # See https://github.com/pantsbuild/pants/pull/19430#discussion_r1337851780
204
    # for more discussion.
205
    complete_pex_env = pex_environment.in_workspace()
×
206
    requirements_pex_request = PexRequest(
×
207
        output_filename="requirements_venv.pex",
208
        internal_only=True,
209
        pex_path=[requirements_pex],
210
        interpreter_constraints=partition.interpreter_constraints,
211
    )
212
    requirements_venv_pex = await create_venv_pex(
×
213
        VenvPexRequest(requirements_pex_request, complete_pex_env), **implicitly()
214
    )
215

216
    # Force the requirements venv to materialize always by running a no-op.
217
    # This operation must be called with `ProcessCacheScope.SESSION`
218
    # so that it runs every time.
219
    _ = await execute_process_or_raise(
×
220
        **implicitly(
221
            VenvPexProcess(
222
                requirements_venv_pex,
223
                description="Force venv to materialize",
224
                argv=["-c", "''"],
225
                cache_scope=ProcessCacheScope.PER_SESSION,
226
            )
227
        )
228
    )
229

230
    # Patch the config file to use the venv directory from the requirements pex,
231
    # and add source roots to the `extraPaths` key in the config file.
232
    patched_config_digest = await _patch_config_file(
×
233
        config_files, requirements_venv_pex.venv_rel_dir, transitive_sources.source_roots
234
    )
235

236
    input_digest = await merge_digests(
×
237
        MergeDigests(
238
            [
239
                transitive_sources.source_files.snapshot.digest,
240
                requirements_venv_pex.digest,
241
                patched_config_digest,
242
            ]
243
        )
244
    )
245

246
    process = await prepare_tool_process(
×
247
        pyright.request(
248
            args=(
249
                f"--venvpath={complete_pex_env.pex_root}",  # Used with `venv` in config
250
                *pyright.args,  # User-added arguments
251
                *(os.path.join("{chroot}", file) for file in root_sources.snapshot.files),
252
            ),
253
            input_digest=input_digest,
254
            description=f"Run Pyright on {pluralize(len(root_sources.snapshot.files), 'file')}.",
255
            level=LogLevel.DEBUG,
256
        ),
257
        **implicitly(),
258
    )
259
    result = await execute_process(process, **implicitly())
×
260
    return CheckResult.from_fallible_process_result(
×
261
        result,
262
        partition_description=partition.description(),
263
    )
264

265

266
@rule(
×
267
    desc="Determine if it is necessary to partition Pyright's input (interpreter_constraints and resolves)",
268
    level=LogLevel.DEBUG,
269
)
270
async def pyright_determine_partitions(
×
271
    request: PyrightRequest,
272
    pyright: Pyright,
273
    python_setup: PythonSetup,
274
) -> PyrightPartitions:
275
    resolve_and_interpreter_constraints_to_field_sets = (
×
276
        _partition_by_interpreter_constraints_and_resolve(request.field_sets, python_setup)
277
    )
278

279
    coarsened_targets = await coarsened_targets_get(
×
280
        CoarsenedTargetsRequest(field_set.address for field_set in request.field_sets),
281
        **implicitly(),
282
    )
283
    coarsened_targets_by_address = coarsened_targets.by_address()
×
284

285
    return PyrightPartitions(
×
286
        PyrightPartition(
287
            FrozenOrderedSet(field_sets),
288
            CoarsenedTargets(
289
                OrderedSet(
290
                    coarsened_targets_by_address[field_set.address] for field_set in field_sets
291
                )
292
            ),
293
            resolve if len(python_setup.resolves) > 1 else None,
294
            interpreter_constraints or pyright.interpreter_constraints,
295
        )
296
        for (resolve, interpreter_constraints), field_sets in sorted(
297
            resolve_and_interpreter_constraints_to_field_sets.items()
298
        )
299
    )
300

301

302
@rule(desc="Typecheck using Pyright", level=LogLevel.DEBUG)
×
303
async def pyright_typecheck(
×
304
    request: PyrightRequest,
305
    pyright: Pyright,
306
) -> CheckResults:
307
    if pyright.skip:
×
308
        return CheckResults([], checker_name=request.tool_name)
×
309

310
    # Explicitly excluding `pyright` as a function argument to `pyright_determine_partitions` and `pyright_typecheck_partition`
311
    # as it throws "TypeError: unhashable type: 'Pyright'"
312
    partitions = await pyright_determine_partitions(request, **implicitly())
×
313
    partitioned_results = await concurrently(
×
314
        pyright_typecheck_partition(partition, **implicitly()) for partition in partitions
315
    )
316
    return CheckResults(
×
317
        partitioned_results,
318
        checker_name=request.tool_name,
319
    )
320

321

322
def rules() -> Iterable[Rule | UnionRule]:
×
323
    return (
×
324
        *collect_rules(),
325
        *config_files.rules(),
326
        *pex_from_targets.rules(),
327
        *nodejs_tool.rules(),
328
        UnionRule(CheckRequest, PyrightRequest),
329
    )
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