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

pantsbuild / pants / 18987624565

31 Oct 2025 11:28PM UTC coverage: 80.299% (+0.02%) from 80.275%
18987624565

push

github

web-flow
Allow setting Python resolve interpreter_constraints as defaults for targets (#22676)

Closes #22574 

This PR is intended to solve a long-felt annoyance of mine when working
in repos with multiple Python resolves, which is having to configure
resolve interpreter constraints and source interpreter constraints
separately. It adds a new option,
`[python].default_to_resolve_interpreter_constraints`, which when set to
true, tells Pants to use the interpreter constraints of the resolve,
rather than the global interpreter constraints, if no interpreter
constraints are provided. If resolves are not enabled or no interpreter
constraints are set for the resolve, it still falls back to the global
default.

71 of 95 new or added lines in 17 files covered. (74.74%)

2 existing lines in 2 files now uncovered.

77993 of 97128 relevant lines covered (80.3%)

3.35 hits per line

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

92.16
/src/python/pants/backend/python/lint/pylint/subsystem.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
4✔
5

6
import os.path
4✔
7
from collections.abc import Iterable
4✔
8
from dataclasses import dataclass
4✔
9

10
from pants.backend.python.lint.first_party_plugins import (
4✔
11
    BaseFirstPartyPlugins,
12
    resolve_first_party_plugins,
13
)
14
from pants.backend.python.lint.pylint.skip_field import SkipPylintField
4✔
15
from pants.backend.python.subsystems.python_tool_base import PythonToolBase
4✔
16
from pants.backend.python.target_types import (
4✔
17
    ConsoleScript,
18
    InterpreterConstraintsField,
19
    PythonResolveField,
20
    PythonSourceField,
21
)
22
from pants.core.goals.resolves import ExportableTool
4✔
23
from pants.core.util_rules.config_files import ConfigFilesRequest
4✔
24
from pants.engine.addresses import UnparsedAddressInputs
4✔
25
from pants.engine.rules import collect_rules, rule
4✔
26
from pants.engine.target import FieldSet, Target
4✔
27
from pants.engine.unions import UnionRule
4✔
28
from pants.option.option_types import (
4✔
29
    ArgsListOption,
30
    BoolOption,
31
    FileOption,
32
    SkipOption,
33
    TargetListOption,
34
)
35
from pants.util.docutil import doc_url
4✔
36
from pants.util.logging import LogLevel
4✔
37
from pants.util.strutil import softwrap
4✔
38

39

40
@dataclass(frozen=True)
4✔
41
class PylintFieldSet(FieldSet):
4✔
42
    required_fields = (PythonSourceField,)
4✔
43

44
    source: PythonSourceField
4✔
45
    resolve: PythonResolveField
4✔
46
    interpreter_constraints: InterpreterConstraintsField
4✔
47

48
    @classmethod
4✔
49
    def opt_out(cls, tgt: Target) -> bool:
4✔
50
        return tgt.get(SkipPylintField).value
×
51

52

53
# --------------------------------------------------------------------------------------
54
# Subsystem
55
# --------------------------------------------------------------------------------------
56

57

58
class Pylint(PythonToolBase):
4✔
59
    options_scope = "pylint"
4✔
60
    name = "Pylint"
4✔
61
    help_short = "The Pylint linter for Python code (https://www.pylint.org/)."
4✔
62

63
    default_main = ConsoleScript("pylint")
4✔
64
    default_requirements = ["pylint>=2.13.0,<3"]
4✔
65

66
    default_lockfile_resource = ("pants.backend.python.lint.pylint", "pylint.lock")
4✔
67

68
    skip = SkipOption("lint")
4✔
69
    args = ArgsListOption(example="--ignore=foo.py,bar.py --disable=C0330,W0311")
4✔
70
    config = FileOption(
4✔
71
        default=None,
72
        advanced=True,
73
        help=lambda cls: softwrap(
74
            f"""
75
            Path to a config file understood by Pylint
76
            (http://pylint.pycqa.org/en/latest/user_guide/run.html#command-line-options).
77

78
            Setting this option will disable `[{cls.options_scope}].config_discovery`. Use
79
            this option if the config is located in a non-standard location.
80
            """
81
        ),
82
    )
83
    config_discovery = BoolOption(
4✔
84
        default=True,
85
        advanced=True,
86
        help=lambda cls: softwrap(
87
            f"""
88
            If true, Pants will include any relevant config files during
89
            runs (`.pylintrc`, `pylintrc`, `pyproject.toml`, and `setup.cfg`).
90

91
            Use `[{cls.options_scope}].config` instead if your config is in a
92
            non-standard location.
93
            """
94
        ),
95
    )
96
    _source_plugins = TargetListOption(
4✔
97
        advanced=True,
98
        help=softwrap(
99
            f"""
100
            An optional list of `python_sources` target addresses to load first-party plugins.
101

102
            You must set the plugin's parent directory as a source root. For
103
            example, if your plugin is at `build-support/pylint/custom_plugin.py`, add
104
            `'build-support/pylint'` to `[source].root_patterns` in `pants.toml`. This is
105
            necessary for Pants to know how to tell Pylint to discover your plugin. See
106
            {doc_url("docs/using-pants/key-concepts/source-roots")}
107

108
            You must also set `load-plugins=$module_name` in your Pylint config file.
109

110
            While your plugin's code can depend on other first-party code and third-party
111
            requirements, all first-party dependencies of the plugin must live in the same
112
            directory or a subdirectory.
113

114
            To instead load third-party plugins, add them to a custom resolve alongside
115
            pylint itself, as described in {doc_url("docs/python/overview/lockfiles#lockfiles-for-tools")}.
116
            """
117
        ),
118
    )
119

120
    def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest:
4✔
121
        # Refer to http://pylint.pycqa.org/en/latest/user_guide/run.html#command-line-options for
122
        # how config files are discovered.
123
        return ConfigFilesRequest(
×
124
            specified=self.config,
125
            specified_option_name=f"[{self.options_scope}].config",
126
            discovery=self.config_discovery,
127
            check_existence=[".pylintrc", *(os.path.join(d, "pylintrc") for d in ("", *dirs))],
128
            check_content={"pyproject.toml": b"[tool.pylint.", "setup.cfg": b"[pylint."},
129
        )
130

131
    @property
4✔
132
    def source_plugins(self) -> UnparsedAddressInputs:
4✔
133
        return UnparsedAddressInputs(
×
134
            self._source_plugins,
135
            owning_address=None,
136
            description_of_origin=f"the option `[{self.options_scope}].source_plugins`",
137
        )
138

139

140
# --------------------------------------------------------------------------------------
141
# First-party plugins
142
# --------------------------------------------------------------------------------------
143

144

145
class PylintFirstPartyPlugins(BaseFirstPartyPlugins):
4✔
146
    pass
4✔
147

148

149
@rule(desc="Prepare [pylint].source_plugins", level=LogLevel.DEBUG)
4✔
150
async def pylint_first_party_plugins(pylint: Pylint) -> PylintFirstPartyPlugins:
4✔
NEW
151
    return await resolve_first_party_plugins(pylint.source_plugins, PylintFirstPartyPlugins)
×
152

153

154
def rules():
4✔
155
    return (
4✔
156
        *collect_rules(),
157
        UnionRule(ExportableTool, Pylint),
158
    )
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