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

pantsbuild / pants / 18517631058

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

Pull #22745

github

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

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

32.47
/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
    determine_python_files,
31
)
32
from pants.backend.python.typecheck.mypy.rules import rules as mypy_rules
2✔
33
from pants.backend.python.typecheck.mypy.subsystem import MyPy, MyPyFieldSet
2✔
34
from pants.backend.python.typecheck.mypy.subsystem import rules as mypy_subystem_rules
2✔
35
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
2✔
36
from pants.core.goals.check import CheckResult, CheckResults
2✔
37
from pants.core.util_rules import config_files
2✔
38
from pants.engine.addresses import Address
2✔
39
from pants.engine.fs import EMPTY_DIGEST, DigestContents
2✔
40
from pants.engine.rules import QueryRule
2✔
41
from pants.engine.target import Target
2✔
42
from pants.testutil.python_interpreter_selection import (
2✔
43
    all_major_minor_python_versions,
44
    skip_unless_all_pythons_present,
45
    skip_unless_python38_present,
46
    skip_unless_python39_present,
47
)
48
from pants.testutil.python_rule_runner import PythonRuleRunner
2✔
49
from pants.util.resources import read_sibling_resource
2✔
50

51

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

67

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

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

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

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

94

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

104

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

114

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

129

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

139

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

160

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

181

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

192

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

199

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

211

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

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

234

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

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

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

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

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

295

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

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

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

335

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

360

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

380

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

397

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

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

432

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

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

454

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

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

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

470
        noop()
471

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

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

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

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

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

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

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

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

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

566

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

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

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

619

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

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

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

662

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

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

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

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

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

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

748

749
def test_determine_python_files() -> None:
2✔
750
    assert determine_python_files([]) == ()
×
751
    assert determine_python_files(["f.py"]) == ("f.py",)
×
752
    assert determine_python_files(["f.pyi"]) == ("f.pyi",)
×
753
    assert determine_python_files(["f.py", "f.pyi"]) == ("f.pyi",)
×
754
    assert determine_python_files(["f.pyi", "f.py"]) == ("f.pyi",)
×
755
    assert determine_python_files(["script-without-extension"]) == ("script-without-extension",)
×
756

757

758
def test_colors_and_formatting(rule_runner: PythonRuleRunner) -> None:
2✔
759
    rule_runner.write_files(
×
760
        {
761
            f"{PACKAGE}/f.py": dedent(
762
                """\
763
                class incredibly_long_type_name_to_force_wrapping_if_mypy_wrapped_error_messages_12345678901234567890123456789012345678901234567890:
764
                    pass
765

766
                x = incredibly_long_type_name_to_force_wrapping_if_mypy_wrapped_error_messages_12345678901234567890123456789012345678901234567890()
767
                x.incredibly_long_attribute_name_to_force_wrapping_if_mypy_wrapped_error_messages_12345678901234567890123456789012345678901234567890
768
                """
769
            ),
770
            f"{PACKAGE}/BUILD": "python_sources()",
771
        }
772
    )
773
    tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="f.py"))
×
774

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

777
    assert len(result) == 1
×
778
    assert result[0].exit_code == 1
×
779
    # all one line
780
    assert re.search(
×
781
        "error:.*incredibly_long_type_name.*incredibly_long_attribute_name", result[0].stdout
782
    )
783
    # at least one escape sequence that sets text color (red)
784
    assert "\033[31m" in result[0].stdout
×
785
    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

© 2025 Coveralls, Inc