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

pantsbuild / pants / 26342152999

23 May 2026 07:59PM UTC coverage: 91.165% (-1.6%) from 92.792%
26342152999

push

github

web-flow
Run Linux ARM CI on Depot runners (#23363)

RunsOn is deprecating their v2 stack, and rather than migrate
to v3 we should use the resources kindly donated by Depot.

GitHub also now has Linux ARM runners, should we need them.

87305 of 95766 relevant lines covered (91.16%)

3.87 hits per line

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

32.61
/src/python/pants/backend/python/typecheck/mypy/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
import os.path
2✔
7
import re
2✔
8
from hashlib import sha256
2✔
9
from pathlib import Path
2✔
10
from textwrap import dedent
2✔
11

12
import pytest
2✔
13

14
from pants.backend.codegen.protobuf.python.python_protobuf_subsystem import (
2✔
15
    rules as protobuf_subsystem_rules,
16
)
17
from pants.backend.codegen.protobuf.python.rules import rules as protobuf_rules
2✔
18
from pants.backend.codegen.protobuf.target_types import ProtobufSourceTarget
2✔
19
from pants.backend.python import target_types_rules
2✔
20
from pants.backend.python.dependency_inference import rules as dependency_inference_rules
2✔
21
from pants.backend.python.target_types import (
2✔
22
    PythonRequirementTarget,
23
    PythonSourcesGeneratorTarget,
24
    PythonSourceTarget,
25
)
26
from pants.backend.python.typecheck.mypy.rules import (
2✔
27
    MyPyPartition,
28
    MyPyPartitions,
29
    MyPyRequest,
30
)
31
from pants.backend.python.typecheck.mypy.rules import rules as mypy_rules
2✔
32
from pants.backend.python.typecheck.mypy.subsystem import MyPy, MyPyFieldSet
2✔
33
from pants.backend.python.typecheck.mypy.subsystem import rules as mypy_subystem_rules
2✔
34
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
2✔
35
from pants.core.goals.check import CheckResult, CheckResults
2✔
36
from pants.core.util_rules import config_files
2✔
37
from pants.engine.addresses import Address
2✔
38
from pants.engine.fs import EMPTY_DIGEST, DigestContents
2✔
39
from pants.engine.rules import QueryRule
2✔
40
from pants.engine.target import Target
2✔
41
from pants.testutil.python_interpreter_selection import (
2✔
42
    all_major_minor_python_versions,
43
    skip_unless_all_pythons_present,
44
    skip_unless_python38_present,
45
    skip_unless_python39_present,
46
)
47
from pants.testutil.python_rule_runner import PythonRuleRunner
2✔
48
from pants.util.resources import read_sibling_resource
2✔
49

50

51
@pytest.fixture
2✔
52
def rule_runner() -> PythonRuleRunner:
2✔
53
    return PythonRuleRunner(
2✔
54
        rules=[
55
            *mypy_rules(),
56
            *mypy_subystem_rules(),
57
            *dependency_inference_rules.rules(),  # Used for import inference.
58
            *config_files.rules(),
59
            *target_types_rules.rules(),
60
            QueryRule(CheckResults, (MyPyRequest,)),
61
            QueryRule(MyPyPartitions, (MyPyRequest,)),
62
        ],
63
        target_types=[PythonSourcesGeneratorTarget, PythonRequirementTarget, PythonSourceTarget],
64
    )
65

66

67
PACKAGE = "src/py/project"
2✔
68
GOOD_FILE = dedent(
2✔
69
    """\
70
    def add(x: int, y: int) -> int:
71
        return x + y
72

73
    result = add(3, 3)
74
    """
75
)
76
BAD_FILE = dedent(
2✔
77
    """\
78
    def add(x: int, y: int) -> int:
79
        return x + y
80

81
    result = add(2.0, 3.0)
82
    """
83
)
84
# This will fail if `--disallow-any-expr` is configured.
85
NEEDS_CONFIG_FILE = dedent(
2✔
86
    """\
87
    from typing import Any, cast
88

89
    x = cast(Any, "hello")
90
    """
91
)
92

93

94
def run_mypy(
2✔
95
    rule_runner: PythonRuleRunner, targets: list[Target], *, extra_args: list[str] | None = None
96
) -> tuple[CheckResult, ...]:
97
    rule_runner.set_options(extra_args or (), env_inherit={"PATH", "PYENV_ROOT", "HOME"})
2✔
98
    result = rule_runner.request(
2✔
99
        CheckResults, [MyPyRequest(MyPyFieldSet.create(tgt) for tgt in targets)]
100
    )
101
    return result.results
2✔
102

103

104
def assert_success(
2✔
105
    rule_runner: PythonRuleRunner, target: Target, *, extra_args: list[str] | None = None
106
) -> None:
107
    result = run_mypy(rule_runner, [target], extra_args=extra_args)
2✔
108
    assert len(result) == 1
2✔
109
    assert result[0].exit_code == 0
2✔
110
    assert "Success: no issues found" in result[0].stdout.strip()
2✔
111
    assert result[0].report == EMPTY_DIGEST
2✔
112

113

114
@pytest.mark.platform_specific_behavior
2✔
115
@pytest.mark.parametrize(
2✔
116
    "major_minor_interpreter",
117
    all_major_minor_python_versions(MyPy.default_interpreter_constraints),
118
)
119
def test_passing(rule_runner: PythonRuleRunner, major_minor_interpreter: str) -> None:
2✔
120
    rule_runner.write_files({f"{PACKAGE}/f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
2✔
121
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
2✔
122
    assert_success(
2✔
123
        rule_runner,
124
        tgt,
125
        extra_args=[f"--mypy-interpreter-constraints=['=={major_minor_interpreter}.*']"],
126
    )
127

128

129
def test_failing(rule_runner: PythonRuleRunner) -> None:
2✔
130
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
131
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
132
    result = run_mypy(rule_runner, [tgt])
×
133
    assert len(result) == 1
×
134
    assert result[0].exit_code == 1
×
135
    assert f"{PACKAGE}/f.py:4" in result[0].stdout
×
136
    assert result[0].report == EMPTY_DIGEST
×
137

138

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

159

160
@pytest.mark.parametrize(
2✔
161
    "config_path,extra_args",
162
    ([".mypy.ini", []], ["custom_config.ini", ["--mypy-config=custom_config.ini"]]),
163
)
164
def test_config_file(
2✔
165
    rule_runner: PythonRuleRunner, config_path: str, extra_args: list[str]
166
) -> None:
167
    rule_runner.write_files(
×
168
        {
169
            f"{PACKAGE}/f.py": NEEDS_CONFIG_FILE,
170
            f"{PACKAGE}/BUILD": "python_sources()",
171
            config_path: "[mypy]\ndisallow_any_expr = True\n",
172
        }
173
    )
174
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
175
    result = run_mypy(rule_runner, [tgt], extra_args=extra_args)
×
176
    assert len(result) == 1
×
177
    assert result[0].exit_code == 1
×
178
    assert f"{PACKAGE}/f.py:3" in result[0].stdout
×
179

180

181
def test_passthrough_args(rule_runner: PythonRuleRunner) -> None:
2✔
182
    rule_runner.write_files(
×
183
        {f"{PACKAGE}/f.py": NEEDS_CONFIG_FILE, f"{PACKAGE}/BUILD": "python_sources()"}
184
    )
185
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
186
    result = run_mypy(rule_runner, [tgt], extra_args=["--mypy-args='--disallow-any-expr'"])
×
187
    assert len(result) == 1
×
188
    assert result[0].exit_code == 1
×
189
    assert f"{PACKAGE}/f.py:3" in result[0].stdout
×
190

191

192
def test_skip(rule_runner: PythonRuleRunner) -> None:
2✔
193
    rule_runner.write_files({f"{PACKAGE}/f.py": BAD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
194
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
195
    result = run_mypy(rule_runner, [tgt], extra_args=["--mypy-skip"])
×
196
    assert not result
×
197

198

199
def test_report_file(rule_runner: PythonRuleRunner) -> None:
2✔
200
    rule_runner.write_files({f"{PACKAGE}/f.py": GOOD_FILE, f"{PACKAGE}/BUILD": "python_sources()"})
×
201
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
202
    result = run_mypy(rule_runner, [tgt], extra_args=["--mypy-args='--linecount-report=reports'"])
×
203
    assert len(result) == 1
×
204
    assert result[0].exit_code == 0
×
205
    assert "Success: no issues found" in result[0].stdout.strip()
×
206
    report_files = rule_runner.request(DigestContents, [result[0].report])
×
207
    assert len(report_files) == 1
×
208
    assert "4       4      1      1 f" in report_files[0].content.decode()
×
209

210

211
def test_thirdparty_dependency(rule_runner: PythonRuleRunner) -> None:
2✔
212
    rule_runner.write_files(
×
213
        {
214
            "BUILD": (
215
                "python_requirement(name='more-itertools', requirements=['more-itertools==8.4.0'])"
216
            ),
217
            f"{PACKAGE}/f.py": dedent(
218
                """\
219
                from more_itertools import flatten
220

221
                assert flatten(42) == [4, 2]
222
                """
223
            ),
224
            f"{PACKAGE}/BUILD": "python_sources()",
225
        }
226
    )
227
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
228
    result = run_mypy(rule_runner, [tgt])
×
229
    assert len(result) == 1
×
230
    assert result[0].exit_code == 1
×
231
    assert f"{PACKAGE}/f.py:3" in result[0].stdout
×
232

233

234
def test_thirdparty_plugin(rule_runner: PythonRuleRunner) -> None:
2✔
235
    rule_runner.write_files(
×
236
        {
237
            "mypy.lock": read_sibling_resource(__name__, "mypy_with_django_stubs.lock"),
238
            f"{PACKAGE}/__init__.py": "",
239
            f"{PACKAGE}/settings.py": dedent(
240
                """\
241
                from django.urls import URLPattern
242

243
                DEBUG = True
244
                DEFAULT_FROM_EMAIL = "webmaster@example.com"
245
                SECRET_KEY = "not so secret"
246
                MY_SETTING = URLPattern(pattern="foo", callback=lambda: None)
247
                """
248
            ),
249
            f"{PACKAGE}/app.py": dedent(
250
                """\
251
                from django.utils import text
252

253
                assert "forty-two" == text.slugify("forty two")
254
                assert "42" == text.slugify(42)
255
                """
256
            ),
257
            f"{PACKAGE}/BUILD": dedent(
258
                """\
259
                python_sources(interpreter_constraints=['>=3.11'])
260

261
                python_requirement(
262
                    name="reqs", requirements=["django==3.2.19", "django-stubs==1.8.0"]
263
                )
264
                """
265
            ),
266
            "mypy.ini": dedent(
267
                """\
268
                [mypy]
269
                plugins =
270
                    mypy_django_plugin.main
271

272
                [mypy.plugins.django-stubs]
273
                django_settings_module = project.settings
274
                """
275
            ),
276
        }
277
    )
278
    result = run_mypy(
×
279
        rule_runner,
280
        [
281
            rule_runner.get_target(Address(PACKAGE, relative_file_path="app.py")),
282
            rule_runner.get_target(Address(PACKAGE, relative_file_path="settings.py")),
283
        ],
284
        extra_args=[
285
            "--source-root-patterns=['src/py']",
286
            "--python-resolves={'mypy':'mypy.lock'}",
287
            "--mypy-install-from-resolve=mypy",
288
        ],
289
    )
290
    assert len(result) == 1
×
291
    assert result[0].exit_code == 1
×
292
    assert f"{PACKAGE}/app.py:4" in result[0].stdout
×
293

294

295
def test_transitive_dependencies(rule_runner: PythonRuleRunner) -> None:
2✔
296
    rule_runner.write_files(
×
297
        {
298
            f"{PACKAGE}/util/__init__.py": "",
299
            f"{PACKAGE}/util/lib.py": dedent(
300
                """\
301
                def capitalize(v: str) -> str:
302
                    return v.capitalize()
303
                """
304
            ),
305
            f"{PACKAGE}/util/BUILD": "python_sources()",
306
            f"{PACKAGE}/math/__init__.py": "",
307
            f"{PACKAGE}/math/add.py": dedent(
308
                """\
309
                from project.util.lib import capitalize
310

311
                def add(x: int, y: int) -> str:
312
                    sum = x + y
313
                    return capitalize(sum)  # This is the wrong type.
314
                """
315
            ),
316
            f"{PACKAGE}/math/BUILD": "python_sources()",
317
            f"{PACKAGE}/__init__.py": "",
318
            f"{PACKAGE}/app.py": dedent(
319
                """\
320
                from project.math.add import add
321

322
                print(add(2, 4))
323
                """
324
            ),
325
            f"{PACKAGE}/BUILD": "python_sources()",
326
        }
327
    )
328
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="app.py"))
×
329
    result = run_mypy(rule_runner, [tgt])
×
330
    assert len(result) == 1
×
331
    assert result[0].exit_code == 1
×
332
    assert f"{PACKAGE}/math/add.py:5" in result[0].stdout
×
333

334

335
@skip_unless_python38_present
2✔
336
def test_works_with_python38(rule_runner: PythonRuleRunner) -> None:
2✔
337
    """MyPy's typed-ast dependency does not understand Python 3.8, so we must instead run MyPy with
338
    Python 3.8 when relevant."""
339
    rule_runner.write_files(
×
340
        {
341
            f"{PACKAGE}/f.py": dedent(
342
                """\
343
                x = 0
344
                if y := x:
345
                    print("x is truthy and now assigned to y")
346
                """
347
            ),
348
            f"{PACKAGE}/BUILD": "python_sources(interpreter_constraints=['>=3.8'])",
349
            "mypy.lock": read_sibling_resource(__name__, "mypy_py38.lock"),
350
        }
351
    )
352
    extra_args = [
×
353
        "--python-resolves={'mypy':'mypy.lock'}",
354
        "--mypy-install-from-resolve=mypy",
355
    ]
356
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
357
    assert_success(rule_runner, tgt, extra_args=extra_args)
×
358

359

360
@skip_unless_python39_present
2✔
361
def test_works_with_python39(rule_runner: PythonRuleRunner) -> None:
2✔
362
    """MyPy's typed-ast dependency does not understand Python 3.9, so we must instead run MyPy with
363
    Python 3.9 when relevant."""
364
    rule_runner.write_files(
×
365
        {
366
            f"{PACKAGE}/f.py": dedent(
367
                """\
368
                @lambda _: int
369
                def replaced(x: bool) -> str:
370
                    return "42" if x is True else "1/137"
371
                """
372
            ),
373
            f"{PACKAGE}/BUILD": "python_sources(interpreter_constraints=['>=3.9'])",
374
        }
375
    )
376
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
377
    assert_success(rule_runner, tgt)
×
378

379

380
def test_run_only_on_specified_files(rule_runner: PythonRuleRunner) -> None:
2✔
381
    rule_runner.write_files(
×
382
        {
383
            f"{PACKAGE}/good.py": GOOD_FILE,
384
            f"{PACKAGE}/bad.py": BAD_FILE,
385
            f"{PACKAGE}/BUILD": dedent(
386
                """\
387
                python_sources(name='good', sources=['good.py'], dependencies=[':bad'])
388
                python_sources(name='bad', sources=['bad.py'])
389
                """
390
            ),
391
        }
392
    )
393
    tgt = rule_runner.get_target(Address(PACKAGE, target_name="good", relative_file_path="good.py"))
×
394
    assert_success(rule_runner, tgt)
×
395

396

397
def test_type_stubs(rule_runner: PythonRuleRunner) -> None:
2✔
398
    """Test that first-party type stubs work for both first-party and third-party code."""
399
    rule_runner.write_files(
×
400
        {
401
            "BUILD": "python_requirement(name='colors', requirements=['ansicolors'])",
402
            "mypy_stubs/__init__.py": "",
403
            "mypy_stubs/colors.pyi": "def red(s: str) -> str: ...",
404
            "mypy_stubs/BUILD": "python_sources()",
405
            f"{PACKAGE}/util/__init__.py": "",
406
            f"{PACKAGE}/util/untyped.py": "def add(x, y):\n    return x + y",
407
            f"{PACKAGE}/util/untyped.pyi": "def add(x: int, y: int) -> int: ...",
408
            f"{PACKAGE}/util/BUILD": "python_sources()",
409
            f"{PACKAGE}/__init__.py": "",
410
            f"{PACKAGE}/app.py": dedent(
411
                """\
412
                from colors import red
413
                from project.util.untyped import add
414

415
                z = add(2, 2.0)
416
                print(red(z))
417
                """
418
            ),
419
            f"{PACKAGE}/BUILD": "python_sources()",
420
        }
421
    )
422
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="app.py"))
×
423
    result = run_mypy(
×
424
        rule_runner, [tgt], extra_args=["--source-root-patterns=['mypy_stubs', 'src/py']"]
425
    )
426
    assert len(result) == 1
×
427
    assert result[0].exit_code == 1
×
428
    assert f"{PACKAGE}/app.py:4: error: Argument 2 to" in result[0].stdout
×
429
    assert f"{PACKAGE}/app.py:5: error: Argument 1 to" in result[0].stdout
×
430

431

432
def test_mypy_shadows_requirements(rule_runner: PythonRuleRunner) -> None:
2✔
433
    """Test the behavior of a MyPy requirement shadowing a user's requirement.
434

435
    The way we load requirements is complex. We want to ensure that things still work properly in
436
    this edge case.
437
    """
438
    rule_runner.write_files(
×
439
        {
440
            "mypy.lock": read_sibling_resource(__name__, "mypy_shadowing_tomli.lock"),
441
            "BUILD": "python_requirement(name='ta', requirements=['tomli==2.1.0'])",
442
            f"{PACKAGE}/f.py": "import tomli",
443
            f"{PACKAGE}/BUILD": "python_sources()",
444
        }
445
    )
446
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
447
    extra_args = [
×
448
        "--python-resolves={'mypy':'mypy.lock'}",
449
        "--mypy-install-from-resolve=mypy",
450
    ]
451
    assert_success(rule_runner, tgt, extra_args=extra_args)
×
452

453

454
def test_source_plugin(rule_runner: PythonRuleRunner) -> None:
2✔
455
    # NB: We make this source plugin fairly complex by having it use transitive dependencies.
456
    # This is to ensure that we can correctly support plugins with dependencies.
457
    # The plugin changes the return type of functions ending in `__overridden_by_plugin` to have a
458
    # return type of `None`.
459
    plugin_file = dedent(
×
460
        """\
461
        from typing import Callable, Optional, Type
462

463
        from mypy.plugin import FunctionContext, Plugin
464
        from mypy.types import NoneType, Type as MyPyType
465

466
        from plugins.subdir.dep import is_overridable_function
467
        from project.subdir.util import noop
468

469
        noop()
470

471
        class ChangeReturnTypePlugin(Plugin):
472
            def get_function_hook(
473
                self, fullname: str
474
            ) -> Optional[Callable[[FunctionContext], MyPyType]]:
475
                return hook if is_overridable_function(fullname) else None
476

477
        def hook(ctx: FunctionContext) -> MyPyType:
478
            return NoneType()
479

480
        def plugin(_version: str) -> Type[Plugin]:
481
            return ChangeReturnTypePlugin
482
        """
483
    )
484
    rule_runner.write_files(
×
485
        {
486
            "mypy.lock": read_sibling_resource(__name__, "mypy_with_more_itertools.lock"),
487
            "BUILD": dedent(
488
                """\
489
                python_requirement(name='mypy', requirements=['mypy==1.1.1'])
490
                python_requirement(name="more-itertools", requirements=["more-itertools==8.4.0"])
491
                """
492
            ),
493
            "pants-plugins/plugins/subdir/__init__.py": "",
494
            "pants-plugins/plugins/subdir/dep.py": dedent(
495
                """\
496
                from more_itertools import flatten
497

498
                def is_overridable_function(name: str) -> bool:
499
                    assert list(flatten([[1, 2], [3, 4]])) == [1, 2, 3, 4]
500
                    return name.endswith("__overridden_by_plugin")
501
                """
502
            ),
503
            "pants-plugins/plugins/subdir/BUILD": "python_sources()",
504
            # The plugin can depend on code located anywhere in the project; its dependencies need
505
            # not be in the same directory.
506
            f"{PACKAGE}/subdir/__init__.py": "",
507
            f"{PACKAGE}/subdir/util.py": "def noop() -> None:\n    pass\n",
508
            f"{PACKAGE}/subdir/BUILD": "python_sources()",
509
            "pants-plugins/plugins/__init__.py": "",
510
            "pants-plugins/plugins/change_return_type.py": plugin_file,
511
            "pants-plugins/plugins/BUILD": "python_sources()",
512
            f"{PACKAGE}/__init__.py": "",
513
            f"{PACKAGE}/f.py": dedent(
514
                """\
515
                def add(x: int, y: int) -> int:
516
                    return x + y
517

518
                def add__overridden_by_plugin(x: int, y: int) -> int:
519
                    return x  + y
520

521
                result = add__overridden_by_plugin(1, 1)
522
                assert add(result, 2) == 4
523
                """
524
            ),
525
            f"{PACKAGE}/BUILD": "python_sources()",
526
            "mypy.ini": dedent(
527
                """\
528
                [mypy]
529
                plugins =
530
                    plugins.change_return_type
531
                """
532
            ),
533
        }
534
    )
535

536
    def run_mypy_with_plugin(tgt: Target) -> CheckResult:
×
537
        result = run_mypy(
×
538
            rule_runner,
539
            [tgt],
540
            extra_args=[
541
                "--python-resolves={'mypy':'mypy.lock'}",
542
                "--mypy-source-plugins=['pants-plugins/plugins']",
543
                "--mypy-install-from-resolve=mypy",
544
                "--source-root-patterns=['pants-plugins', 'src/py']",
545
            ],
546
        )
547
        assert len(result) == 1
×
548
        return result[0]
×
549

550
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
551
    result = run_mypy_with_plugin(tgt)
×
552
    assert result.exit_code == 1
×
553
    assert f"{PACKAGE}/f.py:8" in result.stdout
×
554
    # Ensure we don't accidentally check the source plugin itself.
555
    assert "(checked 1 source file)" in result.stdout
×
556

557
    # Ensure that running MyPy on the plugin itself still works.
558
    plugin_tgt = rule_runner.get_target(
×
559
        Address("pants-plugins/plugins", relative_file_path="change_return_type.py")
560
    )
561
    result = run_mypy_with_plugin(plugin_tgt)
×
562
    assert result.exit_code == 0
×
563
    assert "Success: no issues found in 1 source file" in result.stdout
×
564

565

566
@pytest.mark.parametrize("protoc_type_stubs", (False, True), ids=("mypy_plugin", "protoc_direct"))
2✔
567
def test_protobuf_mypy(rule_runner: PythonRuleRunner, protoc_type_stubs: bool) -> None:
2✔
568
    rule_runner = PythonRuleRunner(
×
569
        rules=[*rule_runner.rules, *protobuf_rules(), *protobuf_subsystem_rules()],
570
        target_types=[*rule_runner.target_types, ProtobufSourceTarget],
571
    )
572
    rule_runner.write_files(
×
573
        {
574
            "BUILD": ("python_requirement(name='protobuf', requirements=['protobuf==6.31.1'])"),
575
            f"{PACKAGE}/__init__.py": "",
576
            f"{PACKAGE}/proto.proto": dedent(
577
                """\
578
                syntax = "proto3";
579
                package project;
580

581
                message Person {
582
                    string name = 1;
583
                    int32 id = 2;
584
                    string email = 3;
585
                }
586
                """
587
            ),
588
            f"{PACKAGE}/f.py": dedent(
589
                """\
590
                from project.proto_pb2 import Person
591

592
                x = Person(name=123, id="abc", email=None)
593
                """
594
            ),
595
            f"{PACKAGE}/BUILD": dedent(
596
                """\
597
                python_sources(dependencies=[':proto'])
598
                protobuf_source(name='proto', source='proto.proto')
599
                """
600
            ),
601
        }
602
    )
603
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
604
    result = run_mypy(
×
605
        rule_runner,
606
        [tgt],
607
        extra_args=[
608
            "--python-protobuf-generate-type-stubs"
609
            if protoc_type_stubs
610
            else "--python-protobuf-mypy-plugin"
611
        ],
612
    )
613
    assert len(result) == 1
×
614
    assert 'Argument "name" to "Person" has incompatible type "int"' in result[0].stdout
×
615
    assert 'Argument "id" to "Person" has incompatible type "str"' in result[0].stdout
×
616
    assert result[0].exit_code == 1
×
617

618

619
def test_cache_directory_per_resolve(rule_runner: PythonRuleRunner) -> None:
2✔
620
    build_multiple_resolves = dedent(
×
621
        """\
622
        python_source(
623
            name='f_from_a',
624
            source='f.py',
625
            resolve='a',
626
        )
627
        python_source(
628
           name='f_from_b',
629
           source='f.py',
630
           resolve='b',
631
        )
632
        """
633
    )
634
    rule_runner.write_files(
×
635
        {
636
            f"{PACKAGE}/f.py": GOOD_FILE,
637
            f"{PACKAGE}/BUILD": build_multiple_resolves,
638
            "mypy.lock": read_sibling_resource(__name__, "mypy_with_django_stubs.lock"),
639
        }
640
    )
641
    target_a = rule_runner.get_target(Address(PACKAGE, target_name="f_from_a"))
×
642
    target_b = rule_runner.get_target(Address(PACKAGE, target_name="f_from_b"))
×
643

644
    runner_options = [
×
645
        "--python-resolves={'a': 'mypy.lock', 'b': 'mypy.lock'}",
646
        "--python-enable-resolves",
647
    ]
648
    run_mypy(rule_runner, [target_a, target_b], extra_args=runner_options)
×
649

650
    with rule_runner.pushd():
×
651
        Path("BUILDROOT").touch()
×
652
        bootstrap_options = rule_runner.options_bootstrapper.bootstrap_options.for_global_scope()
×
653
    named_cache_dir = bootstrap_options.named_caches_dir
×
654
    mypy_cache_dir = (
×
655
        f"{named_cache_dir}/mypy_cache/{sha256(rule_runner.build_root.encode()).hexdigest()}"
656
    )
657
    for resolve in ["a", "b"]:
×
658
        expected_cache_dir = f"{mypy_cache_dir}/{resolve}"
×
659
        assert os.path.exists(expected_cache_dir)
×
660

661

662
@skip_unless_all_pythons_present("3.8", "3.9")
2✔
663
def test_partition_targets(rule_runner: PythonRuleRunner) -> None:
2✔
664
    def create_folder(folder: str, resolve: str, interpreter: str) -> dict[str, str]:
×
665
        return {
×
666
            f"{folder}/dep.py": "",
667
            f"{folder}/root.py": "",
668
            f"{folder}/BUILD": dedent(
669
                f"""\
670
                python_source(
671
                    name='dep',
672
                    source='dep.py',
673
                    resolve='{resolve}',
674
                    interpreter_constraints=['=={interpreter}.*'],
675
                )
676
                python_source(
677
                    name='root',
678
                    source='root.py',
679
                    resolve='{resolve}',
680
                    interpreter_constraints=['=={interpreter}.*'],
681
                    dependencies=[':dep'],
682
                )
683
                """
684
            ),
685
        }
686

687
    files = {
×
688
        **create_folder("resolveA_py38", "a", "3.8"),
689
        **create_folder("resolveA_py39", "a", "3.9"),
690
        **create_folder("resolveB_1", "b", "3.9"),
691
        **create_folder("resolveB_2", "b", "3.9"),
692
    }
693
    rule_runner.write_files(files)
×
694
    rule_runner.set_options(
×
695
        ["--python-resolves={'a': '', 'b': ''}", "--python-enable-resolves"],
696
        env_inherit={"PATH", "PYENV_ROOT", "HOME"},
697
    )
698

699
    resolve_a_py38_dep = rule_runner.get_target(Address("resolveA_py38", target_name="dep"))
×
700
    resolve_a_py38_root = rule_runner.get_target(Address("resolveA_py38", target_name="root"))
×
701
    resolve_a_py39_dep = rule_runner.get_target(Address("resolveA_py39", target_name="dep"))
×
702
    resolve_a_py39_root = rule_runner.get_target(Address("resolveA_py39", target_name="root"))
×
703
    resolve_b_dep1 = rule_runner.get_target(Address("resolveB_1", target_name="dep"))
×
704
    resolve_b_root1 = rule_runner.get_target(Address("resolveB_1", target_name="root"))
×
705
    resolve_b_dep2 = rule_runner.get_target(Address("resolveB_2", target_name="dep"))
×
706
    resolve_b_root2 = rule_runner.get_target(Address("resolveB_2", target_name="root"))
×
707
    request = MyPyRequest(
×
708
        MyPyFieldSet.create(t)
709
        for t in (
710
            resolve_a_py38_root,
711
            resolve_a_py39_root,
712
            resolve_b_root1,
713
            resolve_b_root2,
714
        )
715
    )
716

717
    partitions = rule_runner.request(MyPyPartitions, [request])
×
718
    assert len(partitions) == 3
×
719

720
    def assert_partition(
×
721
        partition: MyPyPartition,
722
        roots: list[Target],
723
        deps: list[Target],
724
        interpreter: str,
725
        resolve: str,
726
    ) -> None:
727
        root_addresses = {t.address for t in roots}
×
728
        assert {fs.address for fs in partition.field_sets} == root_addresses
×
729
        assert {t.address for t in partition.root_targets.closure()} == {
×
730
            *root_addresses,
731
            *(t.address for t in deps),
732
        }
733
        ics = [f"CPython=={interpreter}.*"]
×
734
        assert partition.interpreter_constraints == InterpreterConstraints(ics)
×
735
        assert partition.description() == f"{resolve}, {ics}"
×
736

737
    assert_partition(partitions[0], [resolve_a_py38_root], [resolve_a_py38_dep], "3.8", "a")
×
738
    assert_partition(partitions[1], [resolve_a_py39_root], [resolve_a_py39_dep], "3.9", "a")
×
739
    assert_partition(
×
740
        partitions[2],
741
        [resolve_b_root1, resolve_b_root2],
742
        [resolve_b_dep1, resolve_b_dep2],
743
        "3.9",
744
        "b",
745
    )
746

747

748
def test_colors_and_formatting(rule_runner: PythonRuleRunner) -> None:
2✔
749
    rule_runner.write_files(
×
750
        {
751
            f"{PACKAGE}/f.py": dedent(
752
                """\
753
                class incredibly_long_type_name_to_force_wrapping_if_mypy_wrapped_error_messages_12345678901234567890123456789012345678901234567890:
754
                    pass
755

756
                x = incredibly_long_type_name_to_force_wrapping_if_mypy_wrapped_error_messages_12345678901234567890123456789012345678901234567890()
757
                x.incredibly_long_attribute_name_to_force_wrapping_if_mypy_wrapped_error_messages_12345678901234567890123456789012345678901234567890
758
                """
759
            ),
760
            f"{PACKAGE}/BUILD": "python_sources()",
761
        }
762
    )
763
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
764

765
    result = run_mypy(rule_runner, [tgt], extra_args=["--colors=true", "--mypy-args=--pretty"])
×
766

767
    assert len(result) == 1
×
768
    assert result[0].exit_code == 1
×
769
    # all one line
770
    assert re.search(
×
771
        "error:.*incredibly_long_type_name.*incredibly_long_attribute_name", result[0].stdout
772
    )
773
    # at least one escape sequence that sets text color (red)
774
    assert "\033[31m" in result[0].stdout
×
775
    assert result[0].report == EMPTY_DIGEST
×
776

777

778
def test_force(rule_runner: PythonRuleRunner) -> None:
2✔
779
    rule_runner.write_files(
×
780
        {
781
            f"{PACKAGE}/f.py": GOOD_FILE,
782
            f"{PACKAGE}/BUILD": "python_sources()",
783
        }
784
    )
785
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
786
    result = run_mypy(rule_runner, [tgt], extra_args=["--check-force"])
×
787
    assert len(result) == 1
×
788
    assert result[0].exit_code == 0
×
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