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

pantsbuild / pants / 25259185675

02 May 2026 06:47PM UTC coverage: 92.141% (-0.8%) from 92.955%
25259185675

push

github

web-flow
Fix the dynamic UI. (#23306)

In #23114 we upgraded to indicatif 0.18.4,
which included a fix to respect TERM, and 
display nothing if it's unset.

Since we did not pass TERM through pantsd, the
dynamic ui is now not shown. 

This change fixes that, and also pass NO_COLOR
through, since indicatif inspects it too.

88773 of 96345 relevant lines covered (92.14%)

3.83 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
2✔
5

6
from textwrap import dedent
2✔
7

8
import pytest
2✔
9

10
from pants.backend.python import target_types_rules
2✔
11
from pants.backend.python.lint.pylint import subsystem
2✔
12
from pants.backend.python.lint.pylint.rules import PartitionMetadata, PylintRequest
2✔
13
from pants.backend.python.lint.pylint.rules import rules as pylint_rules
2✔
14
from pants.backend.python.lint.pylint.subsystem import Pylint, PylintFieldSet
2✔
15
from pants.backend.python.target_types import (
2✔
16
    PythonRequirementTarget,
17
    PythonSourcesGeneratorTarget,
18
    PythonSourceTarget,
19
)
20
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
2✔
21
from pants.core.goals.lint import LintResult, Partitions
2✔
22
from pants.core.util_rules import config_files
2✔
23
from pants.core.util_rules.partitions import Partition
2✔
24
from pants.engine.addresses import Address
2✔
25
from pants.engine.fs import DigestContents
2✔
26
from pants.engine.internals.native_engine import EMPTY_DIGEST
2✔
27
from pants.engine.target import Target
2✔
28
from pants.testutil.python_interpreter_selection import (
2✔
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
2✔
34
from pants.testutil.rule_runner import QueryRule
2✔
35
from pants.util.resources import read_resource, read_sibling_resource
2✔
36

37

38
@pytest.fixture
2✔
39
def rule_runner() -> PythonRuleRunner:
2✔
40
    return PythonRuleRunner(
2✔
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
2✔
55
PYLINT_CONVENTION_FAILURE_RETURN_CODE = 16
2✔
56

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

61

62
def run_pylint(
2✔
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 []
2✔
70
    if python_ics:
2✔
71
        args.extend([f"--python-interpreter-constraints=['{python_ics}']"])
2✔
72
    rule_runner.set_options(
2✔
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(
2✔
81
        Partitions[PylintFieldSet, PartitionMetadata],
82
        [PylintRequest.PartitionRequest(tuple(PylintFieldSet.create(tgt) for tgt in targets))],
83
    )
84
    results: list[LintResult] = []
2✔
85
    for partition in partitions:
2✔
86
        result = rule_runner.request(
2✔
87
            LintResult,
88
            [PylintRequest.Batch("", partition.elements, partition.metadata)],
89
        )
90
        results.append(result)
2✔
91
    return tuple(results)
2✔
92

93

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

101

102
@pytest.mark.platform_specific_behavior
2✔
103
@pytest.mark.parametrize(
2✔
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:
2✔
108
    rule_runner.write_files({f"{PACKAGE}/f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
2✔
109
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
2✔
110
    assert_success(
2✔
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:
2✔
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:
2✔
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:
2✔
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
2✔
162
def test_uses_correct_python_version(rule_runner: PythonRuleRunner) -> None:
2✔
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(
2✔
213
    "config_path,extra_args",
214
    (["pylintrc", []], ["custom_config.ini", ["--pylint-config=custom_config.ini"]]),
215
)
216
def test_config_file(
2✔
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:
2✔
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:
2✔
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:
2✔
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:
2✔
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:
2✔
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:
2✔
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:
2✔
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
2✔
486
def test_partition_targets(rule_runner: PythonRuleRunner) -> None:
2✔
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
2✔
576
def test_works_on_python39(rule_runner: PythonRuleRunner) -> None:
2✔
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