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

pantsbuild / pants / 22740642519

05 Mar 2026 11:00PM UTC coverage: 52.677% (-40.3%) from 92.931%
22740642519

Pull #23157

github

web-flow
Merge 2aa18e6d4 into f0030f5e7
Pull Request #23157: [pants ng] Partition source files by config.

31678 of 60136 relevant lines covered (52.68%)

0.53 hits per line

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

34.31
/src/python/pants/backend/python/lint/pylint/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
from textwrap import dedent
1✔
7

8
import pytest
1✔
9

10
from pants.backend.python import target_types_rules
1✔
11
from pants.backend.python.lint.pylint import subsystem
1✔
12
from pants.backend.python.lint.pylint.rules import PartitionMetadata, PylintRequest
1✔
13
from pants.backend.python.lint.pylint.rules import rules as pylint_rules
1✔
14
from pants.backend.python.lint.pylint.subsystem import Pylint, PylintFieldSet
1✔
15
from pants.backend.python.target_types import (
1✔
16
    PythonRequirementTarget,
17
    PythonSourcesGeneratorTarget,
18
    PythonSourceTarget,
19
)
20
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
1✔
21
from pants.core.goals.lint import LintResult, Partitions
1✔
22
from pants.core.util_rules import config_files
1✔
23
from pants.core.util_rules.partitions import Partition
1✔
24
from pants.engine.addresses import Address
1✔
25
from pants.engine.fs import DigestContents
1✔
26
from pants.engine.internals.native_engine import EMPTY_DIGEST
1✔
27
from pants.engine.target import Target
1✔
28
from pants.testutil.python_interpreter_selection import (
1✔
29
    all_major_minor_python_versions,
30
    skip_unless_python39_present,
31
    skip_unless_python310_and_python311_present,
32
)
33
from pants.testutil.python_rule_runner import PythonRuleRunner
1✔
34
from pants.testutil.rule_runner import QueryRule
1✔
35
from pants.util.resources import read_resource, read_sibling_resource
1✔
36

37

38
@pytest.fixture
1✔
39
def rule_runner() -> PythonRuleRunner:
1✔
40
    return PythonRuleRunner(
1✔
41
        rules=[
42
            *pylint_rules(),
43
            *subsystem.rules(),
44
            *config_files.rules(),
45
            *target_types_rules.rules(),
46
            QueryRule(Partitions, [PylintRequest.PartitionRequest]),
47
            QueryRule(LintResult, [PylintRequest.Batch]),
48
        ],
49
        target_types=[PythonSourceTarget, PythonSourcesGeneratorTarget, PythonRequirementTarget],
50
    )
51

52

53
# See http://pylint.pycqa.org/en/latest/user_guide/run.html#exit-codes for exit codes.
54
PYLINT_ERROR_FAILURE_RETURN_CODE = 2
1✔
55
PYLINT_CONVENTION_FAILURE_RETURN_CODE = 16
1✔
56

57
PACKAGE = "src/python/project"
1✔
58
GOOD_FILE = "'''docstring'''\nUPPERCASE_CONSTANT = ''\n"
1✔
59
BAD_FILE = "'''docstring'''\nlowercase_constant = ''\n"
1✔
60

61

62
def run_pylint(
1✔
63
    rule_runner: PythonRuleRunner,
64
    targets: list[Target],
65
    *,
66
    python_ics: str | None = Pylint.default_interpreter_constraints[0],
67
    extra_args: list[str] | None = None,
68
) -> tuple[LintResult, ...]:
69
    args = extra_args or []
1✔
70
    if python_ics:
1✔
71
        args.extend([f"--python-interpreter-constraints=['{python_ics}']"])
1✔
72
    rule_runner.set_options(
1✔
73
        [
74
            "--backend-packages=pants.backend.python.lint.pylint",
75
            "--source-root-patterns=['src/python', 'tests/python']",
76
            *(args),
77
        ],
78
        env_inherit={"PATH", "PYENV_ROOT", "HOME"},
79
    )
80
    partitions = rule_runner.request(
1✔
81
        Partitions[PylintFieldSet, PartitionMetadata],
82
        [PylintRequest.PartitionRequest(tuple(PylintFieldSet.create(tgt) for tgt in targets))],
83
    )
84
    results: list[LintResult] = []
1✔
85
    for partition in partitions:
1✔
86
        result = rule_runner.request(
1✔
87
            LintResult,
88
            [PylintRequest.Batch("", partition.elements, partition.metadata)],
89
        )
90
        results.append(result)
1✔
91
    return tuple(results)
1✔
92

93

94
def assert_success(rule_runner: PythonRuleRunner, target: Target, *, extra_args: list[str]) -> None:
1✔
95
    result = run_pylint(rule_runner, [target], extra_args=extra_args)
1✔
96
    assert len(result) == 1
1✔
97
    assert "Your code has been rated at 10.00/10" in result[0].stdout
1✔
98
    assert result[0].exit_code == 0
1✔
99
    assert result[0].report == EMPTY_DIGEST
1✔
100

101

102
@pytest.mark.platform_specific_behavior
1✔
103
@pytest.mark.parametrize(
1✔
104
    "major_minor_interpreter",
105
    all_major_minor_python_versions(Pylint.default_interpreter_constraints),
106
)
107
def test_passing(rule_runner: PythonRuleRunner, major_minor_interpreter: str) -> None:
1✔
108
    rule_runner.write_files({f"{PACKAGE}/f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
1✔
109
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
1✔
110
    assert_success(
1✔
111
        rule_runner,
112
        tgt,
113
        extra_args=[f"--pylint-interpreter-constraints=['=={major_minor_interpreter}.*']"],
114
    )
115

116

117
def test_failing(rule_runner: PythonRuleRunner) -> None:
1✔
118
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
119
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
120
    result = run_pylint(rule_runner, [tgt])
×
121
    assert len(result) == 1
×
122
    assert result[0].exit_code == PYLINT_CONVENTION_FAILURE_RETURN_CODE
×
123
    assert f"{PACKAGE}/f.py:2:0: C0103" in result[0].stdout
×
124
    assert result[0].report == EMPTY_DIGEST
×
125

126

127
def test_report_file(rule_runner: PythonRuleRunner) -> None:
1✔
128
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
129
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
130
    result = run_pylint(
×
131
        rule_runner, [tgt], extra_args=["--pylint-args='--output=reports/output.txt'"]
132
    )
133
    assert len(result) == 1
×
134
    assert result[0].exit_code == PYLINT_CONVENTION_FAILURE_RETURN_CODE
×
135
    assert result[0].stdout.strip() == ""
×
136
    report_files = rule_runner.request(DigestContents, [result[0].report])
×
137
    assert len(report_files) == 1
×
138
    assert f"{PACKAGE}/f.py:2:0: C0103" in report_files[0].content.decode()
×
139

140

141
def test_multiple_targets(rule_runner: PythonRuleRunner) -> None:
1✔
142
    rule_runner.write_files(
×
143
        {
144
            f"{PACKAGE}/good.py": GOOD_FILE,
145
            f"{PACKAGE}/bad.py": BAD_FILE,
146
            f"{PACKAGE}/BUILD": "python_sources()",
147
        }
148
    )
149
    tgts = [
×
150
        rule_runner.get_target(Address(PACKAGE, relative_file_path="good.py")),
151
        rule_runner.get_target(Address(PACKAGE, relative_file_path="bad.py")),
152
    ]
153
    result = run_pylint(rule_runner, tgts)
×
154
    assert len(result) == 1
×
155
    assert result[0].exit_code == PYLINT_CONVENTION_FAILURE_RETURN_CODE
×
156
    assert f"{PACKAGE}/good.py" not in result[0].stdout
×
157
    assert f"{PACKAGE}/bad.py:2:0: C0103" in result[0].stdout
×
158
    assert result[0].report == EMPTY_DIGEST
×
159

160

161
@skip_unless_python310_and_python311_present
1✔
162
def test_uses_correct_python_version(rule_runner: PythonRuleRunner) -> None:
1✔
163
    rule_runner.write_files(
×
164
        {
165
            # ExceptionGroup was introduced in 3.11.
166
            f"{PACKAGE}/f.py": "'''docstring'''\neg = ExceptionGroup('', [Exception()])\n",
167
            f"{PACKAGE}/BUILD": dedent(
168
                """\
169
                python_sources(name='py310', interpreter_constraints=['CPython==3.10.*'])
170
                python_sources(name='py311', interpreter_constraints=['CPython==3.11.*'])
171
                """
172
            ),
173
        }
174
    )
175

176
    py310_tgt = rule_runner.get_target(
×
177
        Address(PACKAGE, target_name="py310", relative_file_path="f.py")
178
    )
179
    py310_result = run_pylint(rule_runner, [py310_tgt], python_ics=None)
×
180
    assert len(py310_result) == 1
×
181
    assert py310_result[0].exit_code == 2
×
182
    assert (
×
183
        "E0602: Undefined variable 'ExceptionGroup' (undefined-variable)" in py310_result[0].stdout
184
    )
185

186
    py311_tgt = rule_runner.get_target(
×
187
        Address(PACKAGE, target_name="py311", relative_file_path="f.py")
188
    )
189
    py311_result = run_pylint(rule_runner, [py311_tgt], python_ics=None)
×
190
    assert len(py311_result) == 1
×
191
    assert py311_result[0].exit_code == 0
×
192
    assert "Your code has been rated at 10.00/10" in py311_result[0].stdout.strip()
×
193

194
    combined_result = run_pylint(rule_runner, [py310_tgt, py311_tgt], python_ics=None)
×
195
    assert len(combined_result) == 2
×
196
    batched_py311_result, batched_py310_result = sorted(
×
197
        combined_result, key=lambda result: result.exit_code
198
    )
199

200
    assert batched_py310_result.exit_code == 2
×
201
    assert batched_py310_result.partition_description == "['CPython==3.10.*']"
×
202
    assert (
×
203
        "E0602: Undefined variable 'ExceptionGroup' (undefined-variable)"
204
        in batched_py310_result.stdout
205
    )
206

207
    assert batched_py311_result.exit_code == 0
×
208
    assert batched_py311_result.partition_description == "['CPython==3.11.*']"
×
209
    assert "Your code has been rated at 10.00/10" in batched_py311_result.stdout.strip()
×
210

211

212
@pytest.mark.parametrize(
1✔
213
    "config_path,extra_args",
214
    (["pylintrc", []], ["custom_config.ini", ["--pylint-config=custom_config.ini"]]),
215
)
216
def test_config_file(
1✔
217
    rule_runner: PythonRuleRunner, config_path: str, extra_args: list[str]
218
) -> None:
219
    rule_runner.write_files(
×
220
        {
221
            f"{PACKAGE}/f.py": BAD_FILE,
222
            f"{PACKAGE}/BUILD": "python_sources()",
223
            config_path: "[pylint]\ndisable = C0103",
224
        }
225
    )
226
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
227
    assert_success(rule_runner, tgt, extra_args=extra_args)
×
228

229

230
def test_passthrough_args(rule_runner: PythonRuleRunner) -> None:
1✔
231
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
232
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
233
    assert_success(rule_runner, tgt, extra_args=["--pylint-args='--disable=C0103'"])
×
234

235

236
def test_skip(rule_runner: PythonRuleRunner) -> None:
1✔
237
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
238
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
239
    result = run_pylint(rule_runner, [tgt], extra_args=["--pylint-skip"])
×
240
    assert not result
×
241

242

243
def test_includes_transitive_dependencies(rule_runner: PythonRuleRunner) -> None:
1✔
244
    rule_runner.write_files(
×
245
        {
246
            "BUILD": dedent(
247
                """\
248
                python_requirement(name='transitive_req', requirements=['freezegun'])
249
                python_requirement(name='direct_req', requirements=['ansicolors'])
250
                """
251
            ),
252
            f"{PACKAGE}/transitive_dep.py": dedent(
253
                """\
254
                import freezegun
255

256
                A = NotImplemented
257
                """
258
            ),
259
            f"{PACKAGE}/direct_dep.py": dedent(
260
                """\
261
                # No docstring - Pylint doesn't lint dependencies.
262

263
                from project.transitive_dep import A
264

265
                B = A
266
                """
267
            ),
268
            f"{PACKAGE}/f.py": dedent(
269
                """\
270
                '''Pylint should be upset about raising NotImplemented.'''
271
                from colors import green
272
                from project.direct_dep import B
273

274
                def i_just_raise():
275
                    '''A docstring.'''
276
                    print(green("hello"))
277
                    raise B  # pylint should error here
278
                """
279
            ),
280
            f"{PACKAGE}/BUILD": dedent(
281
                """\
282
                python_source(
283
                    name='transitive_dep',
284
                    source='transitive_dep.py',
285
                    dependencies=['//:transitive_req'],
286
                )
287
                python_source(
288
                    name='direct_dep',
289
                    source='direct_dep.py',
290
                    dependencies=[':transitive_dep']
291
                )
292
                python_source(
293
                    name="f",
294
                    source='f.py',
295
                    dependencies=['//:direct_req', ':direct_dep'],
296
                )
297
                """
298
            ),
299
        }
300
    )
301
    tgt = rule_runner.get_target(Address(PACKAGE, target_name="f"))
×
302
    result = run_pylint(rule_runner, [tgt])
×
303
    assert len(result) == 1
×
304
    assert result[0].exit_code == PYLINT_ERROR_FAILURE_RETURN_CODE
×
305
    assert f"{PACKAGE}/f.py:8:4: E0702" in result[0].stdout
×
306
    assert result[0].report == EMPTY_DIGEST
×
307

308

309
def test_pep420_namespace_packages(rule_runner: PythonRuleRunner) -> None:
1✔
310
    rule_runner.write_files(
×
311
        {
312
            f"{PACKAGE}/f.py": GOOD_FILE,
313
            f"{PACKAGE}/BUILD": "python_sources()",
314
            "tests/python/project/f2.py": dedent(
315
                """\
316
                '''Docstring.'''
317

318
                from project.f import UPPERCASE_CONSTANT
319

320
                CONSTANT2 = UPPERCASE_CONSTANT
321
                """
322
            ),
323
            "tests/python/project/BUILD": f"python_sources(dependencies=['{PACKAGE}'])",
324
        }
325
    )
326
    tgts = [
×
327
        rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py")),
328
        rule_runner.get_target(Address("tests/python/project", relative_file_path="f2.py")),
329
    ]
330
    result = run_pylint(rule_runner, tgts)
×
331
    assert len(result) == 1
×
332
    assert result[0].exit_code == 0
×
333
    assert "Your code has been rated at 10.00/10" in result[0].stdout.strip()
×
334
    assert result[0].report == EMPTY_DIGEST
×
335

336

337
def test_type_stubs(rule_runner: PythonRuleRunner) -> None:
1✔
338
    # If an implementation file shares the same name as a type stub, Pylint will only check the
339
    # implementation file. So, here, we only check running directly on a type stub.
340
    rule_runner.write_files({f"{PACKAGE}/f.pyi": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
341
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.pyi"))
×
342
    result = run_pylint(rule_runner, [tgt])
×
343
    assert len(result) == 1
×
344
    assert result[0].exit_code == PYLINT_CONVENTION_FAILURE_RETURN_CODE
×
345
    assert f"{PACKAGE}/f.pyi:2:0: C0103" in result[0].stdout
×
346
    assert result[0].report == EMPTY_DIGEST
×
347

348

349
def test_3rdparty_plugin(rule_runner: PythonRuleRunner) -> None:
1✔
350
    rule_runner.write_files(
×
351
        {
352
            f"{PACKAGE}/f.py": dedent(
353
                """\
354
                '''Docstring.'''
355

356
                import unittest
357

358
                class PluginTest(unittest.TestCase):
359
                    '''Docstring.'''
360

361
                    def test_plugin(self):
362
                        '''Docstring.'''
363
                        self.assertEqual(True, True)
364
                """
365
            ),
366
            f"{PACKAGE}/BUILD": "python_sources()",
367
            "pylint.lock": read_resource(
368
                "pants.backend.python.lint.pylint", "pylint_3rdparty_plugin_test.lock"
369
            ),
370
        }
371
    )
372
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
373
    result = run_pylint(
×
374
        rule_runner,
375
        [tgt],
376
        extra_args=[
377
            "--python-resolves={'pylint':'pylint.lock'}",
378
            "--pylint-install-from-resolve=pylint",
379
            "--pylint-args='--load-plugins=pylint_unittest'",
380
        ],
381
    )
382
    assert len(result) == 1
×
383
    assert result[0].exit_code == 4
×
384
    assert f"{PACKAGE}/f.py:10:8: W5301" in result[0].stdout
×
385
    assert result[0].report == EMPTY_DIGEST
×
386

387

388
def test_source_plugin(rule_runner: PythonRuleRunner) -> None:
1✔
389
    # NB: We make this source plugin fairly complex by having it use transitive dependencies.
390
    # This is to ensure that we can correctly support plugins with dependencies.
391
    # The plugin bans `print()`.
392
    rule_runner.write_files(
×
393
        {
394
            "BUILD": dedent(
395
                """\
396
                python_requirement(name='pylint', requirements=['pylint>=2.13.0,<2.15'])
397
                python_requirement(name='colors', requirements=['ansicolors'])
398
                """
399
            ),
400
            "pylint.lock": read_sibling_resource(__name__, "pylint_source_plugin_test.lock"),
401
            "pants-plugins/plugins/subdir/dep.py": dedent(
402
                """\
403
                from colors import red
404

405
                def is_print(node):
406
                    _ = red("Test that transitive deps are loaded.")
407
                    return hasattr(node.func, "name") and node.func.name == "print"
408
                """
409
            ),
410
            "pants-plugins/plugins/subdir/BUILD": "python_sources(dependencies=['//:colors'])",
411
            "pants-plugins/plugins/print_plugin.py": dedent(
412
                """\
413
                '''Docstring.'''
414

415
                from pylint.checkers import BaseChecker
416
                from pylint.interfaces import IAstroidChecker
417

418
                from subdir.dep import is_print
419

420
                class PrintChecker(BaseChecker):
421
                    '''Docstring.'''
422

423
                    __implements__ = IAstroidChecker
424
                    name = "print_plugin"
425
                    msgs = {
426
                        "C9871": ("`print` statements are banned", "print-statement-used", ""),
427
                    }
428

429
                    def visit_call(self, node):
430
                        '''Docstring.'''
431
                        if is_print(node):
432
                            self.add_message("print-statement-used", node=node)
433

434

435
                def register(linter):
436
                    '''Docstring.'''
437
                    linter.register_checker(PrintChecker(linter))
438
                """
439
            ),
440
            "pants-plugins/plugins/BUILD": (
441
                "python_sources(dependencies=['//:pylint', 'pants-plugins/plugins/subdir'])"
442
            ),
443
            "pylintrc": dedent(
444
                """\
445
                [MASTER]
446
                load-plugins=print_plugin
447
                """
448
            ),
449
            f"{PACKAGE}/f.py": "'''Docstring.'''\nprint()\n",
450
            f"{PACKAGE}/BUILD": "python_sources()",
451
        }
452
    )
453

454
    def run_pylint_with_plugin(tgt: Target) -> LintResult:
×
455
        res = run_pylint(
×
456
            rule_runner,
457
            [tgt],
458
            extra_args=[
459
                "--pylint-source-plugins=['pants-plugins/plugins']",
460
                f"--source-root-patterns=['pants-plugins/plugins', '{PACKAGE}']",
461
                "--python-resolves={'pylint':'pylint.lock'}",
462
                "--pylint-install-from-resolve=pylint",
463
            ],
464
        )
465
        assert len(res) == 1
×
466
        return res[0]
×
467

468
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
469
    result = run_pylint_with_plugin(tgt)
×
470
    assert result.exit_code == PYLINT_CONVENTION_FAILURE_RETURN_CODE
×
471
    assert f"{PACKAGE}/f.py:2:0: C9871" in result.stdout
×
472
    assert result.report == EMPTY_DIGEST
×
473

474
    # Ensure that running Pylint on the plugin itself still works.
475
    plugin_tgt = rule_runner.get_target(
×
476
        Address("pants-plugins/plugins", relative_file_path="print_plugin.py")
477
    )
478
    result = run_pylint_with_plugin(plugin_tgt)
×
479
    print(result.stdout)
×
480
    assert result.exit_code == 0
×
481
    assert "Your code has been rated at 10.00/10" in result.stdout
×
482
    assert result.report == EMPTY_DIGEST
×
483

484

485
@skip_unless_python310_and_python311_present
1✔
486
def test_partition_targets(rule_runner: PythonRuleRunner) -> None:
1✔
487
    def create_folder(folder: str, resolve: str, interpreter: str) -> dict[str, str]:
×
488
        return {
×
489
            f"{folder}/dep.py": "",
490
            f"{folder}/root.py": "",
491
            f"{folder}/BUILD": dedent(
492
                f"""\
493
                python_source(
494
                    name='dep',
495
                    source='dep.py',
496
                    resolve='{resolve}',
497
                    interpreter_constraints=['=={interpreter}.*'],
498
                )
499
                python_source(
500
                    name='root',
501
                    source='root.py',
502
                    resolve='{resolve}',
503
                    interpreter_constraints=['=={interpreter}.*'],
504
                    dependencies=[':dep'],
505
                )
506
                """
507
            ),
508
        }
509

510
    files = {
×
511
        **create_folder("resolveA_py310", "a", "3.10"),
512
        **create_folder("resolveA_py311", "a", "3.11"),
513
        **create_folder("resolveB_1", "b", "3.11"),
514
        **create_folder("resolveB_2", "b", "3.11"),
515
    }
516
    rule_runner.write_files(files)
×
517
    rule_runner.set_options(
×
518
        ["--python-resolves={'a': '', 'b': ''}", "--python-enable-resolves"],
519
        env_inherit={"PATH", "PYENV_ROOT", "HOME"},
520
    )
521

522
    resolve_a_py310_dep = rule_runner.get_target(Address("resolveA_py310", target_name="dep"))
×
523
    resolve_a_py310_root = rule_runner.get_target(Address("resolveA_py310", target_name="root"))
×
524
    resolve_a_py311_dep = rule_runner.get_target(Address("resolveA_py311", target_name="dep"))
×
525
    resolve_a_py311_root = rule_runner.get_target(Address("resolveA_py311", target_name="root"))
×
526
    resolve_b_dep1 = rule_runner.get_target(Address("resolveB_1", target_name="dep"))
×
527
    resolve_b_root1 = rule_runner.get_target(Address("resolveB_1", target_name="root"))
×
528
    resolve_b_dep2 = rule_runner.get_target(Address("resolveB_2", target_name="dep"))
×
529
    resolve_b_root2 = rule_runner.get_target(Address("resolveB_2", target_name="root"))
×
530
    request: PylintRequest.PartitionRequest[PylintFieldSet] = PylintRequest.PartitionRequest(
×
531
        tuple(
532
            PylintFieldSet.create(t)
533
            for t in (
534
                resolve_a_py310_root,
535
                resolve_a_py311_root,
536
                resolve_b_root1,
537
                resolve_b_root2,
538
            )
539
        )
540
    )
541

542
    partitions = list(rule_runner.request(Partitions[PylintFieldSet, PartitionMetadata], [request]))
×
543
    assert len(partitions) == 3
×
544

545
    def assert_partition(
×
546
        partition: Partition,
547
        roots: list[Target],
548
        deps: list[Target],
549
        interpreter: str,
550
        resolve: str,
551
    ) -> None:
552
        assert partition.metadata is not None
×
553
        key = partition.metadata
×
554

555
        root_addresses = {t.address for t in roots}
×
556
        assert {t.address for t in key.coarsened_targets.closure()} == {
×
557
            *root_addresses,
558
            *(t.address for t in deps),
559
        }
560
        ics = [f"CPython=={interpreter}.*"]
×
561
        assert key.interpreter_constraints == InterpreterConstraints(ics)
×
562
        assert key.description == f"{resolve}, {ics}"
×
563

564
    assert_partition(partitions[0], [resolve_a_py310_root], [resolve_a_py310_dep], "3.10", "a")
×
565
    assert_partition(partitions[1], [resolve_a_py311_root], [resolve_a_py311_dep], "3.11", "a")
×
566
    assert_partition(
×
567
        partitions[2],
568
        [resolve_b_root1, resolve_b_root2],
569
        [resolve_b_dep1, resolve_b_dep2],
570
        "3.11",
571
        "b",
572
    )
573

574

575
@skip_unless_python39_present
1✔
576
def test_works_on_python39(rule_runner: PythonRuleRunner) -> None:
1✔
577
    rule_runner.write_files(
×
578
        {
579
            f"{PACKAGE}/f.py": dedent(
580
                """\
581
                x = 0
582
                if y := x:
583
                    print("x is truthy and now assigned to y")
584
                """
585
            ),
586
            f"{PACKAGE}/BUILD": "python_sources(interpreter_constraints=['==3.9.*'])",
587
            "pylint.lock": read_sibling_resource(__name__, "pylint_py39.lock"),
588
        }
589
    )
590
    extra_args = [
×
591
        "--python-resolves={'pylint':'pylint.lock'}",
592
        "--pylint-install-from-resolve=pylint",
593
    ]
594
    rule_runner.write_files({f"{PACKAGE}/f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
595
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
596
    result = run_pylint(rule_runner, [tgt], python_ics="CPython==3.9.*", extra_args=extra_args)
×
597
    assert len(result) == 1
×
598
    assert "Your code has been rated at 10.00/10" in result[0].stdout
×
599
    assert result[0].exit_code == 0
×
600
    assert result[0].report == EMPTY_DIGEST
×
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