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

pantsbuild / pants / 19381742489

15 Nov 2025 12:52AM UTC coverage: 49.706% (-30.6%) from 80.29%
19381742489

Pull #22890

github

web-flow
Merge d961abf79 into 42e1ebd41
Pull Request #22890: Updated all python subsystem constraints to 3.14

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

14659 existing lines in 485 files now uncovered.

31583 of 63540 relevant lines covered (49.71%)

0.79 hits per line

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

62.07
/src/python/pants/jvm/resolve/jvm_tool.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
2✔
4

5
import os
2✔
6
from dataclasses import dataclass
2✔
7
from typing import ClassVar
2✔
8

9
from pants.build_graph.address import AddressInput
2✔
10
from pants.core.goals.generate_lockfiles import DEFAULT_TOOL_LOCKFILE
2✔
11
from pants.core.goals.resolves import ExportableTool
2✔
12
from pants.engine.addresses import Addresses
2✔
13
from pants.engine.internals.build_files import resolve_address
2✔
14
from pants.engine.internals.graph import resolve_targets
2✔
15
from pants.engine.internals.selectors import concurrently
2✔
16
from pants.engine.rules import collect_rules, implicitly, rule
2✔
17
from pants.jvm.resolve.common import (
2✔
18
    ArtifactRequirement,
19
    ArtifactRequirements,
20
    GatherJvmCoordinatesRequest,
21
)
22
from pants.jvm.resolve.coordinate import Coordinate
2✔
23
from pants.jvm.target_types import JvmArtifactFieldSet
2✔
24
from pants.option.option_types import StrListOption, StrOption
2✔
25
from pants.option.subsystem import Subsystem
2✔
26
from pants.util.docutil import bin_name, git_url
2✔
27
from pants.util.meta import classproperty
2✔
28
from pants.util.ordered_set import FrozenOrderedSet
2✔
29
from pants.util.strutil import softwrap
2✔
30

31

32
class JvmToolBase(Subsystem, ExportableTool):
2✔
33
    """Base class for subsystems that configure a set of artifact requirements for a JVM tool."""
34

35
    # Default version of the tool. (Subclasses may set.)
36
    default_version: ClassVar[str | None] = None
2✔
37

38
    # Default artifacts for the tool in GROUP:NAME format. The `--version` value will be used for the
39
    # artifact version if it has not been specified for a particular requirement. (Subclasses must set.)
40
    default_artifacts: ClassVar[tuple[str, ...]]
2✔
41

42
    # Default resource for the tool's lockfile. (Subclasses must set.)
43
    default_lockfile_resource: ClassVar[tuple[str, str]]
2✔
44

45
    version = StrOption(
2✔
46
        advanced=True,
47
        default=lambda cls: cls.default_version,
48
        help=lambda cls: softwrap(
49
            f"""
50
            Version string for the tool. This is available for substitution in the
51
            `[{cls.options_scope}].artifacts` option by including the string `{{version}}`.
52
            """
53
        ),
54
    )
55
    artifacts = StrListOption(
2✔
56
        advanced=True,
57
        default=lambda cls: list(cls.default_artifacts),
58
        help=lambda cls: softwrap(
59
            f"""
60
            Artifact requirements for this tool using specified as either the address of a `jvm_artifact`
61
            target or, alternatively, as a colon-separated Maven coordinates (e.g., `group:name:version`).
62
            For Maven coordinates, the string `{{version}}` version will be substituted with the value of the
63
            `[{cls.options_scope}].version` option.
64
            """
65
        ),
66
    )
67
    lockfile = StrOption(
2✔
68
        default=DEFAULT_TOOL_LOCKFILE,
69
        advanced=True,
70
        help=lambda cls: softwrap(
71
            f"""
72
            Path to a lockfile used for installing the tool.
73

74
            Set to the string `{DEFAULT_TOOL_LOCKFILE}` to use a lockfile provided by
75
            Pants, so long as you have not changed the `--version` option.
76
            See {cls.default_lockfile_url} for the default lockfile contents.
77

78
            To use a custom lockfile, set this option to a file path relative to the
79
            build root, then run `{bin_name()} generate-lockfiles
80
            --resolve={cls.options_scope}`.
81
            """
82
        ),
83
    )
84
    jvm_options = StrListOption(
2✔
85
        help=lambda cls: softwrap(
86
            f"""
87
            List of JVM options to pass to `{cls.options_scope}` JVM processes.
88

89
            Options set here will be added to those set in `[jvm].global_options`. Please
90
            check the documentation for the `jvm` subsystem to see what values are accepted here.
91
            """
92
        ),
93
        advanced=True,
94
    )
95

96
    @classproperty
2✔
97
    def default_lockfile_url(cls) -> str:
2✔
98
        return git_url(
2✔
99
            os.path.join(
100
                "src",
101
                "python",
102
                cls.default_lockfile_resource[0].replace(".", os.path.sep),
103
                cls.default_lockfile_resource[1],
104
            )
105
        )
106

107
    @classmethod
2✔
108
    def help_for_generate_lockfile_with_default_location(cls, resolve_name):
2✔
109
        return softwrap(
×
110
            f"""
111
            You requested to generate a lockfile for {resolve_name} because
112
            you included it in `--generate-lockfiles-resolve`, but
113
            {resolve_name} is a tool using its default lockfile.
114

115
            If you would like to generate a lockfile for {resolve_name}, please
116
            set `[{resolve_name}].lockfile` to the path where it should be
117
                    generated and run again.
118
            """
119
        )
120

121
    @property
2✔
122
    def artifact_inputs(self) -> tuple[str, ...]:
2✔
UNCOV
123
        return tuple(s.format(version=self.version) for s in self.artifacts)
×
124

125

126
@rule
2✔
127
async def gather_coordinates_for_jvm_lockfile(
2✔
128
    request: GatherJvmCoordinatesRequest,
129
) -> ArtifactRequirements:
130
    # Separate `artifact_inputs` by whether the strings parse as an `Address` or not.
131
    requirements: set[ArtifactRequirement] = set()
×
132
    candidate_address_inputs: set[AddressInput] = set()
×
133
    bad_artifact_inputs = []
×
134
    for artifact_input in request.artifact_inputs:
×
135
        # Try parsing as a `Coordinate` first since otherwise `AddressInput.parse` will try to see if the
136
        # group name is a file on disk.
137
        if 2 <= artifact_input.count(":"):
×
138
            try:
×
139
                maybe_coord = ArtifactRequirement(Coordinate.from_coord_str(artifact_input))
×
140
                requirements.add(maybe_coord)
×
141
                continue
×
142
            except Exception:
×
143
                pass
×
144

145
        try:
×
146
            address_input = AddressInput.parse(
×
147
                artifact_input, description_of_origin=f"the option `{request.option_name}`"
148
            )
149
            candidate_address_inputs.add(address_input)
×
150
        except Exception:
×
151
            bad_artifact_inputs.append(artifact_input)
×
152

153
    if bad_artifact_inputs:
×
154
        raise ValueError(
×
155
            softwrap(
156
                f"""
157
                The following values could not be parsed as an address nor as a JVM coordinate string.
158
                The problematic inputs supplied to the `{request.option_name}` option were:
159
                {", ".join(bad_artifact_inputs)}.
160
                """
161
            )
162
        )
163

164
    # Gather coordinates from the provided addresses.
165
    addresses = await concurrently(
×
166
        resolve_address(**implicitly({ai: AddressInput})) for ai in candidate_address_inputs
167
    )
168
    all_supplied_targets = await resolve_targets(**implicitly(Addresses(addresses)))
×
169
    other_targets = []
×
170
    for tgt in all_supplied_targets:
×
171
        if JvmArtifactFieldSet.is_applicable(tgt):
×
172
            requirements.add(ArtifactRequirement.from_jvm_artifact_target(tgt))
×
173
        else:
174
            other_targets.append(tgt)
×
175

176
    if other_targets:
×
177
        raise ValueError(
×
178
            softwrap(
179
                f"""
180
                The following addresses reference targets that are not `jvm_artifact` targets.
181
                Please only supply the addresses of `jvm_artifact` for the `{request.option_name}`
182
                option. The problematic addresses are: {", ".join(str(tgt.address) for tgt in other_targets)}.
183
                """
184
            )
185
        )
186

187
    return ArtifactRequirements(requirements)
×
188

189

190
@dataclass(frozen=True)
2✔
191
class GenerateJvmLockfileFromTool:
2✔
192
    """Create a `GenerateJvmLockfile` request for a JVM tool.
193

194
    We allow tools to either use coordinates or addresses to `jvm_artifact` targets for the artifact
195
    inputs. This is a convenience to parse those artifact inputs to create a standardized
196
    `GenerateJvmLockfile`.
197
    """
198

199
    artifact_inputs: FrozenOrderedSet[str]
2✔
200
    options_scope: str
2✔
201
    resolve_name: str
2✔
202
    lockfile: str
2✔
203
    default_lockfile_resource: tuple[str, str]
2✔
204

205
    @classmethod
2✔
206
    def create(cls, tool: JvmToolBase) -> GenerateJvmLockfileFromTool:
2✔
UNCOV
207
        return GenerateJvmLockfileFromTool(
×
208
            FrozenOrderedSet(tool.artifact_inputs),
209
            options_scope=tool.options_scope,
210
            resolve_name=tool.options_scope,
211
            lockfile=tool.lockfile,
212
            default_lockfile_resource=tool.default_lockfile_resource,
213
        )
214

215
    @property
2✔
216
    def artifact_option_name(self) -> str:
2✔
217
        return f"[{self.options_scope}].artifacts"
×
218

219
    @property
2✔
220
    def lockfile_option_name(self):
2✔
221
        return f"[{self.options_scope}].lockfile"
×
222

223

224
def rules():
2✔
225
    from pants.jvm.goals import lockfile  # TODO: Shim to avoid import cycle
2✔
226

227
    return (*collect_rules(), *lockfile.rules())
2✔
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