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

pantsbuild / pants / 23710389144

29 Mar 2026 01:42PM UTC coverage: 92.849% (-0.07%) from 92.917%
23710389144

Pull #23200

github

web-flow
Merge 7a0639d44 into da60c6486
Pull Request #23200: perf: Port FrozenOrderedSet to rust

22 of 26 new or added lines in 6 files covered. (84.62%)

77 existing lines in 13 files now uncovered.

91400 of 98439 relevant lines covered (92.85%)

4.04 hits per line

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

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

4
from __future__ import annotations
1✔
5

6
import json
1✔
7
from collections.abc import Iterable
1✔
8
from textwrap import dedent
1✔
9

10
import pytest
1✔
11

12
from pants.backend.javascript.package_json import PackageJsonTarget
1✔
13
from pants.backend.python import target_types_rules
1✔
14
from pants.backend.python.target_types import (
1✔
15
    PythonRequirementTarget,
16
    PythonSourcesGeneratorTarget,
17
    PythonSourceTarget,
18
)
19
from pants.backend.python.typecheck.pyright.rules import (
1✔
20
    PyrightFieldSet,
21
    PyrightPartition,
22
    PyrightPartitions,
23
    PyrightRequest,
24
)
25
from pants.backend.python.typecheck.pyright.rules import rules as pyright_rules
1✔
26
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
1✔
27
from pants.core.goals.check import CheckResult, CheckResults
1✔
28
from pants.engine.addresses import Address
1✔
29
from pants.engine.fs import EMPTY_DIGEST
1✔
30
from pants.engine.rules import QueryRule
1✔
31
from pants.engine.target import Target
1✔
32
from pants.testutil.python_interpreter_selection import skip_unless_all_pythons_present
1✔
33
from pants.testutil.python_rule_runner import PythonRuleRunner
1✔
34
from pants.util.contextutil import temporary_dir
1✔
35
from pants.util.dirutil import safe_rmtree
1✔
36

37

38
@pytest.fixture
1✔
39
def rule_runner() -> PythonRuleRunner:
1✔
40
    return PythonRuleRunner(
1✔
41
        rules=[
42
            *pyright_rules(),
43
            *target_types_rules.rules(),
44
            QueryRule(CheckResults, (PyrightRequest,)),
45
            QueryRule(PyrightPartitions, (PyrightRequest,)),
46
        ],
47
        target_types=[
48
            PythonRequirementTarget,
49
            PythonSourcesGeneratorTarget,
50
            PythonSourceTarget,
51
            PackageJsonTarget,
52
        ],
53
    )
54

55

56
PACKAGE = "src/py/project"
1✔
57
GOOD_FILE = dedent(
1✔
58
    """\
59
    def add(x: int, y: int) -> int:
60
        return x + y
61

62
    result = add(3, 3)
63
    """
64
)
65
BAD_FILE = dedent(
1✔
66
    """\
67
    def add(x: int, y: int) -> int:
68
        return x + y
69

70
    result = add(2.0, 3.0)
71
    """
72
)
73
# This will fail if `reportUndefinedVariable` is enabled (default).
74
UNDEFINED_VARIABLE_FILE = dedent(
1✔
75
    """\
76
    print(foo)
77
    """
78
)
79

80
UNDEFINED_VARIABLE_JSON_CONFIG = dedent(
1✔
81
    """\
82
    {
83
        "reportUndefinedVariable": false
84
    }
85
    """
86
)
87

88
UNDEFINED_VARIABLE_TOML_CONFIG = dedent(
1✔
89
    """\
90
    [tool.pyright]
91
    reportUndefinedVariable = false
92
    """
93
)
94

95
PYRIGHT_VERSION = "1.1.396"
1✔
96
PYRIGHT_INTEGRITY_HASH = "sha512-+/8GN9ZRlqS/EFUjSW3yb2FN9XF7KjGpnLVYLtfTPDiiH+tfua898acKenUTGYbdfvSf7J0GD/g1b5RItnyYPw=="
1✔
97
PYRIGHT_LOCKFILE = json.dumps(
1✔
98
    {
99
        "name": "@the-company/project",
100
        "lockfileVersion": 2,
101
        "requires": True,
102
        "packages": {
103
            "": {"name": "@the-company/project", "devDependencies": {"pyright": PYRIGHT_VERSION}},
104
            "node_modules/fsevents": {
105
                "version": "2.3.3",
106
                "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
107
                "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
108
                "dev": True,
109
                "hasInstallScript": True,
110
                "optional": True,
111
                "engines": {"node": "^8.16.0 || ^10.6.0 || >=11.0.0"},
112
            },
113
            "node_modules/pyright": {
114
                "version": PYRIGHT_VERSION,
115
                "resolved": f"https://registry.npmjs.org/pyright/-/pyright-{PYRIGHT_VERSION}.tgz",
116
                "integrity": PYRIGHT_INTEGRITY_HASH,
117
                "dev": True,
118
                "bin": {"pyright": "index.js", "pyright-langserver": "langserver.index.js"},
119
                "engines": {"node": ">=14.0.0"},
120
                "optionalDependencies": {"fsevents": "~2.3.3"},
121
            },
122
        },
123
        "dependencies": {
124
            "fsevents": {
125
                "version": "2.3.3",
126
                "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
127
                "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
128
                "dev": True,
129
                "optional": True,
130
            },
131
            "pyright": {
132
                "version": PYRIGHT_VERSION,
133
                "resolved": f"https://registry.npmjs.org/pyright/-/pyright-{PYRIGHT_VERSION}.tgz",
134
                "integrity": PYRIGHT_INTEGRITY_HASH,
135
                "dev": True,
136
                "requires": {"fsevents": "~2.3.3"},
137
            },
138
        },
139
    }
140
)
141

142

143
def run_pyright(
1✔
144
    rule_runner: PythonRuleRunner, targets: list[Target], *, extra_args: Iterable[str] | None = None
145
) -> tuple[CheckResult, ...]:
146
    rule_runner.set_options(extra_args or (), env_inherit={"PATH", "PYENV_ROOT", "HOME"})
1✔
147
    result = rule_runner.request(
1✔
148
        CheckResults, [PyrightRequest(PyrightFieldSet.create(tgt) for tgt in targets)]
149
    )
150
    return result.results
1✔
151

152

153
def test_passing(rule_runner: PythonRuleRunner) -> None:
1✔
154
    rule_runner.write_files({f"{PACKAGE}/f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
1✔
155
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
156
    result = run_pyright(rule_runner, [tgt])
1✔
157
    assert len(result) == 1
1✔
158
    assert result[0].exit_code == 0
1✔
159
    assert "0 errors" in result[0].stdout
1✔
160
    assert result[0].report == EMPTY_DIGEST
1✔
161

162

163
def test_failing(rule_runner: PythonRuleRunner) -> None:
1✔
164
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
1✔
165
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
166
    result = run_pyright(rule_runner, [tgt])
1✔
167
    assert len(result) == 1
1✔
168
    assert result[0].exit_code == 1
1✔
169
    assert f"{PACKAGE}/f.py:4" in result[0].stdout
1✔
170
    assert "2 errors" in result[0].stdout
1✔
171
    assert result[0].report == EMPTY_DIGEST
1✔
172

173

174
def test_spaces_in_filenames(rule_runner: PythonRuleRunner) -> None:
1✔
175
    rule_runner.write_files(
1✔
176
        {f"{PACKAGE}/f f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"}
177
    )
178
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f f.py"))
1✔
179
    result = run_pyright(rule_runner, [tgt])
1✔
180
    assert len(result) == 1
1✔
181
    assert result[0].exit_code == 0
1✔
182
    assert "0 errors" in result[0].stdout
1✔
183
    assert result[0].report == EMPTY_DIGEST
1✔
184

185

186
def test_multiple_targets(rule_runner: PythonRuleRunner) -> None:
1✔
187
    rule_runner.write_files(
1✔
188
        {
189
            f"{PACKAGE}/bad1.py": BAD_FILE,
190
            f"{PACKAGE}/bad2.py": BAD_FILE,
191
            f"{PACKAGE}/BUILD": "python_sources()",
192
        }
193
    )
194
    tgts = [
1✔
195
        rule_runner.get_target(Address(PACKAGE, relative_file_path="bad1.py")),
196
        rule_runner.get_target(Address(PACKAGE, relative_file_path="bad2.py")),
197
    ]
198
    result = run_pyright(rule_runner, tgts)
1✔
199
    assert len(result) == 1
1✔
200
    assert result[0].exit_code == 1
1✔
201
    assert f"{PACKAGE}/bad1.py:4" in result[0].stdout
1✔
202
    assert f"{PACKAGE}/bad2.py:4" in result[0].stdout
1✔
203
    assert "4 errors" in result[0].stdout
1✔
204
    assert result[0].report == EMPTY_DIGEST
1✔
205

206

207
@pytest.mark.parametrize(
1✔
208
    "config_filename,config_file,exit_code",
209
    (
210
        ("pyrightconfig.json", UNDEFINED_VARIABLE_JSON_CONFIG, 0),
211
        ("pyproject.toml", UNDEFINED_VARIABLE_TOML_CONFIG, 0),
212
        ("noconfig", "", 1),
213
    ),
214
)
215
def test_config_file(
1✔
216
    rule_runner: PythonRuleRunner, config_filename: str, config_file: str, exit_code: int
217
) -> None:
218
    rule_runner.write_files(
1✔
219
        {
220
            f"{PACKAGE}/f.py": UNDEFINED_VARIABLE_FILE,
221
            f"{PACKAGE}/BUILD": "python_sources()",
222
            f"{config_filename}": config_file,
223
        }
224
    )
225
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
226
    result = run_pyright(rule_runner, [tgt])
1✔
227
    assert len(result) == 1
1✔
228
    assert result[0].exit_code == exit_code
1✔
229

230

231
LIB_1_PACKAGE = f"{PACKAGE}/lib1"
1✔
232
LIB_2_PACKAGE = f"{PACKAGE}/lib2"
1✔
233

234

235
@pytest.mark.parametrize(
1✔
236
    "files, extra_args",
237
    [
238
        pytest.param(
239
            {
240
                f"{LIB_1_PACKAGE}/core/a.py": GOOD_FILE,
241
                f"{LIB_1_PACKAGE}/core/BUILD": "python_sources()",
242
                f"{LIB_2_PACKAGE}/core/b.py": "from core.a import add",
243
                f"{LIB_2_PACKAGE}/core/BUILD": "python_sources()",
244
            },
245
            (f"--source-root-patterns=['{LIB_1_PACKAGE}', '{LIB_2_PACKAGE}']",),
246
            id="from_version",
247
        ),
248
        pytest.param(
249
            {
250
                f"{LIB_1_PACKAGE}/core/a.py": GOOD_FILE,
251
                f"{LIB_1_PACKAGE}/core/BUILD": "python_sources()",
252
                f"{LIB_2_PACKAGE}/core/b.py": "from core.a import add",
253
                f"{LIB_2_PACKAGE}/core/BUILD": "python_sources()",
254
                "src/js/lib3/BUILD": "package_json()",
255
                "src/js/lib3/package.json": json.dumps(
256
                    {"name": "@the-company/project", "dependencies": {"pyright": PYRIGHT_VERSION}}
257
                ),
258
                "src/js/lib3/package-lock.json": PYRIGHT_LOCKFILE,
259
            },
260
            (
261
                f"--source-root-patterns=['{LIB_1_PACKAGE}', '{LIB_2_PACKAGE}', 'src/js']",
262
                "--pyright-install-from-resolve=lib3",
263
            ),
264
            id="from_resolve",
265
        ),
266
        pytest.param(
267
            {
268
                f"{LIB_1_PACKAGE}/core/a.py": GOOD_FILE,
269
                f"{LIB_1_PACKAGE}/core/BUILD": "python_sources()",
270
                f"{LIB_2_PACKAGE}/core/b.py": "from core.a import add",
271
                f"{LIB_2_PACKAGE}/core/BUILD": "python_sources()",
272
                "BUILD": "package_json(name='root_package')",
273
                "package.json": json.dumps(
274
                    {"name": "@the-company/project", "dependencies": {"pyright": PYRIGHT_VERSION}}
275
                ),
276
                "package-lock.json": PYRIGHT_LOCKFILE,
277
            },
278
            (
279
                f"--source-root-patterns=['{LIB_1_PACKAGE}', '{LIB_2_PACKAGE}', '/']",
280
                "--pyright-install-from-resolve=nodejs-default",
281
            ),
282
            id="from_resolve_at_root",
283
        ),
284
    ],
285
)
286
def test_additional_source_roots(
1✔
287
    files: dict[str, str], extra_args: tuple[str, ...], rule_runner: PythonRuleRunner
288
) -> None:
289
    rule_runner.write_files(files)
1✔
290
    tgts = [
1✔
291
        rule_runner.get_target(Address(f"{LIB_1_PACKAGE}/core", relative_file_path="a.py")),
292
        rule_runner.get_target(Address(f"{LIB_2_PACKAGE}/core", relative_file_path="b.py")),
293
    ]
294
    result = run_pyright(rule_runner, tgts)
1✔
295
    assert len(result) == 1
1✔
296
    assert result[0].exit_code == 1
1✔
297
    assert "reportMissingImports" in result[0].stdout
1✔
298

299
    result = run_pyright(
1✔
300
        rule_runner,
301
        tgts,
302
        extra_args=extra_args,
303
    )
304
    assert len(result) == 1
1✔
305
    assert result[0].exit_code == 0
1✔
306

307
    # When we run on just one target, Pyright should find its dependency in the other source root.
308
    result = run_pyright(
1✔
309
        rule_runner,
310
        tgts[1:],
311
        extra_args=extra_args,
312
    )
313
    assert len(result) == 1
1✔
314
    assert result[0].exit_code == 0
1✔
315

316

317
def test_skip(rule_runner: PythonRuleRunner) -> None:
1✔
318
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
1✔
319
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
320
    result = run_pyright(rule_runner, [tgt], extra_args=["--pyright-skip"])
1✔
321
    assert not result
1✔
322

323

324
def test_passing_cache_clear(rule_runner: PythonRuleRunner) -> None:
1✔
325
    # Ensure that the requirements venv must be created, by adding in a third-party
326
    # requirement to the test code.
327
    rule_runner.write_files(
1✔
328
        {
329
            "BUILD": "python_requirement(name='more-itertools', requirements=['more-itertools==8.4.0'])",
330
            f"{PACKAGE}/f.py": dedent(
331
                """\
332
            from more_itertools import is_sorted
333

334
            assert is_sorted([1, 2, 3]) is True
335
            """
336
            ),
337
            f"{PACKAGE}/BUILD": "python_sources()",
338
        }
339
    )
340
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
341

342
    with temporary_dir() as cache_dir:
1✔
343
        # On the first run, Pyright should work as the venv will be created from scratch.
344
        result = run_pyright(rule_runner, [tgt], extra_args=[f"--named-caches-dir={cache_dir}"])
1✔
345
        assert len(result) == 1
1✔
346
        assert result[0].exit_code == 0
1✔
347
        assert "0 errors" in result[0].stdout
1✔
348
        assert result[0].report == EMPTY_DIGEST
1✔
349

350
        # Delete the cache directory containing the venv
351
        safe_rmtree(cache_dir)
1✔
352

353
        # Run again - should work as the venv will be created again from scratch.
354
        result = run_pyright(rule_runner, [tgt], extra_args=[f"--named-caches-dir={cache_dir}"])
1✔
355
        assert len(result) == 1
1✔
356
        assert result[0].exit_code == 0
1✔
357
        assert "0 errors" in result[0].stdout
1✔
358
        assert result[0].report == EMPTY_DIGEST
1✔
359

360

361
@pytest.mark.parametrize(
1✔
362
    "files, extra_args",
363
    [
364
        pytest.param(
365
            {
366
                "BUILD": (
367
                    "python_requirement(name='more-itertools', requirements=['more-itertools==8.4.0'])"
368
                ),
369
                f"{PACKAGE}/f.py": dedent(
370
                    """\
371
            from more_itertools import flatten
372

373
            assert flatten(42) == [4, 2]
374
            """
375
                ),
376
                f"{PACKAGE}/BUILD": "python_sources()",
377
            },
378
            (),
379
            id="from_version",
380
        ),
381
        pytest.param(
382
            {
383
                "BUILD": (
384
                    "python_requirement(name='more-itertools', requirements=['more-itertools==8.4.0'])"
385
                ),
386
                f"{PACKAGE}/f.py": dedent(
387
                    """\
388
                from more_itertools import flatten
389

390
                assert flatten(42) == [4, 2]
391
                """
392
                ),
393
                f"{PACKAGE}/BUILD": "python_sources()",
394
                "src/js/BUILD": "package_json()",
395
                "src/js/package.json": json.dumps(
396
                    {"name": "@the-company/project", "dependencies": {"pyright": PYRIGHT_VERSION}}
397
                ),
398
                "src/js/package-lock.json": PYRIGHT_LOCKFILE,
399
            },
400
            ("--pyright-install-from-resolve=js",),
401
            id="from_resolve",
402
        ),
403
    ],
404
)
405
def test_thirdparty_dependency(
1✔
406
    rule_runner: PythonRuleRunner, files: dict[str, str], extra_args: tuple[str, ...]
407
) -> None:
408
    rule_runner.write_files(files)
1✔
409
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
410
    result = run_pyright(rule_runner, [tgt], extra_args=extra_args)
1✔
411
    assert len(result) == 1
1✔
412
    assert result[0].exit_code == 1
1✔
413
    assert f"{PACKAGE}/f.py:3" in result[0].stdout
1✔
414

415

416
@skip_unless_all_pythons_present("3.8", "3.9")
1✔
417
def test_partition_targets(rule_runner: PythonRuleRunner) -> None:
1✔
418
    def create_folder(folder: str, resolve: str, interpreter: str) -> dict[str, str]:
1✔
419
        return {
1✔
420
            f"{folder}/dep.py": "",
421
            f"{folder}/root.py": "",
422
            f"{folder}/BUILD": dedent(
423
                f"""\
424
                python_source(
425
                    name='dep',
426
                    source='dep.py',
427
                    resolve='{resolve}',
428
                    interpreter_constraints=['=={interpreter}.*'],
429
                )
430
                python_source(
431
                    name='root',
432
                    source='root.py',
433
                    resolve='{resolve}',
434
                    interpreter_constraints=['=={interpreter}.*'],
435
                    dependencies=[':dep'],
436
                )
437
                """
438
            ),
439
        }
440

441
    files = {
1✔
442
        **create_folder("resolveA_py38", "a", "3.8"),
443
        **create_folder("resolveA_py39", "a", "3.9"),
444
        **create_folder("resolveB_1", "b", "3.9"),
445
        **create_folder("resolveB_2", "b", "3.9"),
446
    }
447
    rule_runner.write_files(files)
1✔
448
    rule_runner.set_options(
1✔
449
        ["--python-resolves={'a': '', 'b': ''}", "--python-enable-resolves"],
450
        env_inherit={"PATH", "PYENV_ROOT", "HOME"},
451
    )
452

453
    resolve_a_py38_dep = rule_runner.get_target(Address("resolveA_py38", target_name="dep"))
1✔
454
    resolve_a_py38_root = rule_runner.get_target(Address("resolveA_py38", target_name="root"))
1✔
455
    resolve_a_py39_dep = rule_runner.get_target(Address("resolveA_py39", target_name="dep"))
1✔
456
    resolve_a_py39_root = rule_runner.get_target(Address("resolveA_py39", target_name="root"))
1✔
457
    resolve_b_dep1 = rule_runner.get_target(Address("resolveB_1", target_name="dep"))
1✔
458
    resolve_b_root1 = rule_runner.get_target(Address("resolveB_1", target_name="root"))
1✔
459
    resolve_b_dep2 = rule_runner.get_target(Address("resolveB_2", target_name="dep"))
1✔
460
    resolve_b_root2 = rule_runner.get_target(Address("resolveB_2", target_name="root"))
1✔
461
    request = PyrightRequest(
1✔
462
        PyrightFieldSet.create(t)
463
        for t in (
464
            resolve_a_py38_root,
465
            resolve_a_py39_root,
466
            resolve_b_root1,
467
            resolve_b_root2,
468
        )
469
    )
470

471
    partitions = rule_runner.request(PyrightPartitions, [request])
1✔
UNCOV
472
    assert len(partitions) == 3
×
473

UNCOV
474
    def assert_partition(
×
475
        partition: PyrightPartition,
476
        roots: list[Target],
477
        deps: list[Target],
478
        interpreter: str,
479
        resolve: str,
480
    ) -> None:
UNCOV
481
        root_addresses = {t.address for t in roots}
×
UNCOV
482
        assert {fs.address for fs in partition.field_sets} == root_addresses
×
UNCOV
483
        assert {t.address for t in partition.root_targets.closure()} == {
×
484
            *root_addresses,
485
            *(t.address for t in deps),
486
        }
UNCOV
487
        ics = [f"CPython=={interpreter}.*"]
×
UNCOV
488
        assert partition.interpreter_constraints == InterpreterConstraints(ics)
×
UNCOV
489
        assert partition.description() == f"{resolve}, {ics}"
×
490

UNCOV
491
    assert_partition(partitions[0], [resolve_a_py38_root], [resolve_a_py38_dep], "3.8", "a")
×
UNCOV
492
    assert_partition(partitions[1], [resolve_a_py39_root], [resolve_a_py39_dep], "3.9", "a")
×
UNCOV
493
    assert_partition(
×
494
        partitions[2],
495
        [resolve_b_root1, resolve_b_root2],
496
        [resolve_b_dep1, resolve_b_dep2],
497
        "3.9",
498
        "b",
499
    )
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