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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/src/python/pants/backend/python/typecheck/mypy/subsystem.py
1
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import logging
×
UNCOV
7
from collections.abc import Iterable
×
UNCOV
8
from dataclasses import dataclass
×
9

UNCOV
10
from pants.backend.python.subsystems.python_tool_base import PythonToolBase
×
UNCOV
11
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
12
from pants.backend.python.target_types import (
×
13
    ConsoleScript,
14
    InterpreterConstraintsField,
15
    PythonRequirementsField,
16
    PythonResolveField,
17
    PythonSourceField,
18
)
UNCOV
19
from pants.backend.python.typecheck.mypy.skip_field import SkipMyPyField
×
UNCOV
20
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
21
from pants.backend.python.util_rules.partition import _find_all_unique_interpreter_constraints
×
UNCOV
22
from pants.backend.python.util_rules.pex_requirements import PexRequirements
×
UNCOV
23
from pants.backend.python.util_rules.python_sources import (
×
24
    PythonSourceFilesRequest,
25
    prepare_python_sources,
26
)
UNCOV
27
from pants.core.goals.resolves import ExportableTool
×
UNCOV
28
from pants.core.util_rules.config_files import ConfigFilesRequest, find_config_file
×
UNCOV
29
from pants.engine.addresses import UnparsedAddressInputs
×
UNCOV
30
from pants.engine.fs import EMPTY_DIGEST, Digest, FileContent
×
UNCOV
31
from pants.engine.internals.graph import resolve_unparsed_address_inputs
×
UNCOV
32
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
×
UNCOV
33
from pants.engine.intrinsics import get_digest_contents
×
UNCOV
34
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
35
from pants.engine.target import FieldSet, Target, TransitiveTargetsRequest
×
UNCOV
36
from pants.engine.unions import UnionRule
×
UNCOV
37
from pants.option.option_types import (
×
38
    ArgsListOption,
39
    BoolOption,
40
    FileOption,
41
    SkipOption,
42
    TargetListOption,
43
)
UNCOV
44
from pants.util.docutil import doc_url
×
UNCOV
45
from pants.util.logging import LogLevel
×
UNCOV
46
from pants.util.ordered_set import FrozenOrderedSet
×
UNCOV
47
from pants.util.strutil import softwrap
×
48

UNCOV
49
logger = logging.getLogger(__name__)
×
50

51

UNCOV
52
@dataclass(frozen=True)
×
UNCOV
53
class MyPyFieldSet(FieldSet):
×
UNCOV
54
    required_fields = (PythonSourceField,)
×
55

UNCOV
56
    sources: PythonSourceField
×
UNCOV
57
    resolve: PythonResolveField
×
UNCOV
58
    interpreter_constraints: InterpreterConstraintsField
×
59

UNCOV
60
    @classmethod
×
UNCOV
61
    def opt_out(cls, tgt: Target) -> bool:
×
62
        return tgt.get(SkipMyPyField).value
×
63

64

65
# --------------------------------------------------------------------------------------
66
# Subsystem
67
# --------------------------------------------------------------------------------------
68

69

UNCOV
70
class MyPy(PythonToolBase):
×
UNCOV
71
    options_scope = "mypy"
×
UNCOV
72
    name = "MyPy"
×
UNCOV
73
    help_short = "The MyPy Python type checker (http://mypy-lang.org/)."
×
74

UNCOV
75
    default_main = ConsoleScript("mypy")
×
UNCOV
76
    default_requirements = ["mypy>=0.961,<2"]
×
77

78
    # See `mypy/rules.py`. We only use these default constraints in some situations.
UNCOV
79
    register_interpreter_constraints = True
×
80

UNCOV
81
    default_lockfile_resource = ("pants.backend.python.typecheck.mypy", "mypy.lock")
×
82

UNCOV
83
    skip = SkipOption("check")
×
UNCOV
84
    args = ArgsListOption(example="--python-version 3.7 --disallow-any-expr")
×
UNCOV
85
    config = FileOption(
×
86
        default=None,
87
        advanced=True,
88
        help=lambda cls: softwrap(
89
            f"""
90
            Path to a config file understood by MyPy
91
            (https://mypy.readthedocs.io/en/stable/config_file.html).
92

93
            Setting this option will disable `[{cls.options_scope}].config_discovery`. Use
94
            this option if the config is located in a non-standard location.
95
            """
96
        ),
97
    )
UNCOV
98
    config_discovery = BoolOption(
×
99
        default=True,
100
        advanced=True,
101
        help=lambda cls: softwrap(
102
            f"""
103
            If true, Pants will include any relevant config files during runs
104
            (`mypy.ini`, `.mypy.ini`, and `setup.cfg`).
105

106
            Use `[{cls.options_scope}].config` instead if your config is in a non-standard location.
107
            """
108
        ),
109
    )
UNCOV
110
    _source_plugins = TargetListOption(
×
111
        advanced=True,
112
        help=softwrap(
113
            f"""
114
            An optional list of `python_sources` target addresses to load first-party plugins.
115

116
            You must also set `plugins = path.to.module` in your `mypy.ini`, and
117
            set the `[mypy].config` option in your `pants.toml`.
118

119
            To instead load third-party plugins, set the option `[mypy].install_from_resolve`
120
            to a resolve whose lockfile includes those plugins, and set the `plugins` option
121
            in `mypy.ini`.  See {doc_url("docs/python/goals/check")}.
122
            """
123
        ),
124
    )
125

UNCOV
126
    @property
×
UNCOV
127
    def config_request(self) -> ConfigFilesRequest:
×
128
        # Refer to https://mypy.readthedocs.io/en/stable/config_file.html.
129
        return ConfigFilesRequest(
×
130
            specified=self.config,
131
            specified_option_name=f"{self.options_scope}.config",
132
            discovery=self.config_discovery,
133
            check_existence=["mypy.ini", ".mypy.ini"],
134
            check_content={"setup.cfg": b"[mypy", "pyproject.toml": b"[tool.mypy"},
135
        )
136

UNCOV
137
    @property
×
UNCOV
138
    def source_plugins(self) -> UnparsedAddressInputs:
×
139
        return UnparsedAddressInputs(
×
140
            self._source_plugins,
141
            owning_address=None,
142
            description_of_origin=f"the option `[{self.options_scope}].source_plugins`",
143
        )
144

UNCOV
145
    def check_and_warn_if_python_version_configured(self, config: FileContent | None) -> bool:
×
146
        """Determine if we can dynamically set `--python-version` and warn if not."""
147
        configured = []
×
148
        if config and b"python_version" in config.content:
×
149
            configured.append(
×
150
                softwrap(
151
                    f"""
152
                    `python_version` in {config.path} (which is used because of either config
153
                    discovery or the `[mypy].config` option)
154
                    """
155
                )
156
            )
157
        if "--py2" in self.args:
×
158
            configured.append("`--py2` in the `--mypy-args` option")
×
159
        if any(arg.startswith("--python-version") for arg in self.args):
×
160
            configured.append("`--python-version` in the `--mypy-args` option")
×
161
        if configured:
×
162
            formatted_configured = " and you set ".join(configured)
×
163
            logger.warning(
×
164
                softwrap(
165
                    f"""
166
                    You set {formatted_configured}. Normally, Pants would automatically set this
167
                    for you based on your code's interpreter constraints
168
                    ({doc_url("docs/python/overview/interpreter-compatibility")}). Instead, it will
169
                    use what you set.
170

171
                    (Allowing Pants to automatically set the option allows Pants to partition your
172
                    targets by their constraints, so that, for example, you can run MyPy on
173
                    Python 2-only code and Python 3-only code at the same time. It also allows Pants
174
                    to leverage MyPy's cache, making subsequent runs of MyPy very fast.
175
                    In the future, this feature may no longer work.)
176
                    """
177
                )
178
            )
179
        return bool(configured)
×
180

181

182
# --------------------------------------------------------------------------------------
183
# Config files
184
# --------------------------------------------------------------------------------------
185

186

UNCOV
187
@dataclass(frozen=True)
×
UNCOV
188
class MyPyConfigFile:
×
UNCOV
189
    digest: Digest
×
UNCOV
190
    _python_version_configured: bool
×
191

UNCOV
192
    def python_version_to_autoset(
×
193
        self, interpreter_constraints: InterpreterConstraints, interpreter_universe: Iterable[str]
194
    ) -> str | None:
195
        """If the user did not already set `--python-version`, return the major.minor version to
196
        use."""
UNCOV
197
        if self._python_version_configured:
×
UNCOV
198
            return None
×
UNCOV
199
        return interpreter_constraints.minimum_python_version(interpreter_universe)
×
200

201

UNCOV
202
@rule
×
UNCOV
203
async def setup_mypy_config(mypy: MyPy) -> MyPyConfigFile:
×
204
    config_files = await find_config_file(mypy.config_request)
×
205
    digest_contents = await get_digest_contents(config_files.snapshot.digest)
×
206
    python_version_configured = mypy.check_and_warn_if_python_version_configured(
×
207
        digest_contents[0] if digest_contents else None
208
    )
209
    return MyPyConfigFile(config_files.snapshot.digest, python_version_configured)
×
210

211

212
# --------------------------------------------------------------------------------------
213
# First party plugins
214
# --------------------------------------------------------------------------------------
215

216

UNCOV
217
@dataclass(frozen=True)
×
UNCOV
218
class MyPyFirstPartyPlugins:
×
UNCOV
219
    requirement_strings: FrozenOrderedSet[str]
×
UNCOV
220
    sources_digest: Digest
×
UNCOV
221
    source_roots: tuple[str, ...]
×
222

223

UNCOV
224
@rule(desc="Prepare [mypy].source_plugins", level=LogLevel.DEBUG)
×
UNCOV
225
async def mypy_first_party_plugins(
×
226
    mypy: MyPy,
227
) -> MyPyFirstPartyPlugins:
228
    if not mypy.source_plugins:
×
229
        return MyPyFirstPartyPlugins(FrozenOrderedSet(), EMPTY_DIGEST, ())
×
230

231
    plugin_target_addresses = await resolve_unparsed_address_inputs(
×
232
        mypy.source_plugins, **implicitly()
233
    )
234
    transitive_targets = await transitive_targets_get(
×
235
        TransitiveTargetsRequest(plugin_target_addresses), **implicitly()
236
    )
237

238
    requirements = PexRequirements.req_strings_from_requirement_fields(
×
239
        (
240
            plugin_tgt[PythonRequirementsField]
241
            for plugin_tgt in transitive_targets.closure
242
            if plugin_tgt.has_field(PythonRequirementsField)
243
        ),
244
    )
245

246
    sources = await prepare_python_sources(
×
247
        PythonSourceFilesRequest(transitive_targets.closure), **implicitly()
248
    )
249
    return MyPyFirstPartyPlugins(
×
250
        requirement_strings=requirements,
251
        sources_digest=sources.source_files.snapshot.digest,
252
        source_roots=sources.source_roots,
253
    )
254

255

256
# --------------------------------------------------------------------------------------
257
# Interpreter constraints
258
# --------------------------------------------------------------------------------------
259

260

UNCOV
261
async def _mypy_interpreter_constraints(
×
262
    mypy: MyPy, python_setup: PythonSetup
263
) -> InterpreterConstraints:
264
    constraints = mypy.interpreter_constraints
×
265
    if mypy.options.is_default("interpreter_constraints"):
×
266
        code_constraints = await _find_all_unique_interpreter_constraints(
×
267
            python_setup, MyPyFieldSet
268
        )
269
        if code_constraints.requires_python38_or_newer(python_setup.interpreter_versions_universe):
×
270
            constraints = code_constraints
×
271
    return constraints
×
272

273

UNCOV
274
def rules():
×
UNCOV
275
    return (
×
276
        *collect_rules(),
277
        UnionRule(ExportableTool, MyPy),
278
    )
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