• 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/javascript/subsystems/nodejs_tool.py
1
# Copyright 2022 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 re
×
UNCOV
7
from collections.abc import Iterable, Mapping
×
UNCOV
8
from dataclasses import dataclass, field
×
UNCOV
9
from typing import ClassVar
×
10

UNCOV
11
from pants.backend.javascript import install_node_package, nodejs_project_environment
×
UNCOV
12
from pants.backend.javascript.install_node_package import (
×
13
    InstalledNodePackageRequest,
14
    install_node_packages_for_address,
15
)
UNCOV
16
from pants.backend.javascript.nodejs_project_environment import (
×
17
    NodeJsProjectEnvironmentProcess,
18
    setup_nodejs_project_environment_process,
19
)
UNCOV
20
from pants.backend.javascript.package_manager import PackageManager
×
UNCOV
21
from pants.backend.javascript.resolve import (
×
22
    resolve_to_first_party_node_package,
23
    resolve_to_projects,
24
)
UNCOV
25
from pants.backend.javascript.subsystems.nodejs import (
×
26
    NodeJS,
27
    NodeJSToolProcess,
28
    setup_node_tool_process,
29
)
UNCOV
30
from pants.engine.internals.native_engine import Digest, MergeDigests
×
UNCOV
31
from pants.engine.intrinsics import merge_digests
×
UNCOV
32
from pants.engine.process import Process
×
UNCOV
33
from pants.engine.rules import Rule, collect_rules, implicitly, rule
×
UNCOV
34
from pants.engine.unions import UnionRule
×
UNCOV
35
from pants.option.option_types import StrOption
×
UNCOV
36
from pants.option.subsystem import Subsystem
×
UNCOV
37
from pants.util.frozendict import FrozenDict
×
UNCOV
38
from pants.util.logging import LogLevel
×
UNCOV
39
from pants.util.strutil import softwrap
×
40

41

UNCOV
42
class NodeJSToolBase(Subsystem):
×
43
    # Subclasses must set.
UNCOV
44
    default_version: ClassVar[str]
×
45

UNCOV
46
    version = StrOption(
×
47
        advanced=True,
48
        default=lambda cls: cls.default_version,
49
        help="Version string for the tool in the form package@version (e.g. prettier@3.6.2)",
50
    )
51

UNCOV
52
    _binary_name = StrOption(
×
53
        advanced=True,
54
        default=None,
55
        help="Override the binary to run for this tool. Defaults to the package name.",
56
    )
57

UNCOV
58
    @property
×
UNCOV
59
    def binary_name(self) -> str:
×
60
        """The binary name to run for this tool."""
UNCOV
61
        if self._binary_name:
×
UNCOV
62
            return self._binary_name
×
63

64
        # For scoped packages (@scope/package), use the scope name (often matches the binary)
65
        # For regular packages, use the full package name
UNCOV
66
        match = re.match(r"^(?:@([^/]+)/[^@]+|([^@]+))", self.version)
×
UNCOV
67
        if not match:
×
UNCOV
68
            raise ValueError(f"Invalid npm package specification: {self.version}")
×
UNCOV
69
        return match.group(1) or match.group(2)
×
70

UNCOV
71
    install_from_resolve = StrOption(
×
72
        advanced=True,
73
        default=None,
74
        help=lambda cls: softwrap(
75
            f"""\
76
            If specified, install the tool using the lockfile for this named resolve,
77
            instead of the version configured in this subsystem.
78

79
            If unspecified, the tool will use the default configured package manager
80
            [{NodeJS.options_scope}].package_manager`, and install the tool without a
81
            lockfile.
82
            """
83
        ),
84
    )
85

UNCOV
86
    def request(
×
87
        self,
88
        args: tuple[str, ...],
89
        input_digest: Digest,
90
        description: str,
91
        level: LogLevel,
92
        output_files: tuple[str, ...] = (),
93
        output_directories: tuple[str, ...] = (),
94
        append_only_caches: FrozenDict[str, str] | None = None,
95
        project_caches: FrozenDict[str, str] | None = None,
96
        timeout_seconds: int | None = None,
97
        extra_env: Mapping[str, str] | None = None,
98
    ) -> NodeJSToolRequest:
UNCOV
99
        return NodeJSToolRequest(
×
100
            package=self.version,
101
            binary_name=self.binary_name,
102
            resolve=self.install_from_resolve,
103
            args=args,
104
            input_digest=input_digest,
105
            description=description,
106
            level=level,
107
            output_files=output_files,
108
            output_directories=output_directories,
109
            append_only_caches=append_only_caches or FrozenDict(),
110
            project_caches=project_caches or FrozenDict(),
111
            timeout_seconds=timeout_seconds,
112
            extra_env=extra_env or FrozenDict(),
113
            options_scope=self.options_scope,
114
        )
115

116

UNCOV
117
@dataclass(frozen=True)
×
UNCOV
118
class NodeJSToolRequest:
×
UNCOV
119
    package: str
×
UNCOV
120
    binary_name: str
×
UNCOV
121
    resolve: str | None
×
UNCOV
122
    args: tuple[str, ...]
×
UNCOV
123
    input_digest: Digest
×
UNCOV
124
    description: str
×
UNCOV
125
    level: LogLevel
×
UNCOV
126
    options_scope: str
×
UNCOV
127
    output_files: tuple[str, ...] = ()
×
UNCOV
128
    output_directories: tuple[str, ...] = ()
×
UNCOV
129
    append_only_caches: FrozenDict[str, str] = field(default_factory=FrozenDict)
×
UNCOV
130
    project_caches: FrozenDict[str, str] = field(default_factory=FrozenDict)
×
UNCOV
131
    timeout_seconds: int | None = None
×
UNCOV
132
    extra_env: Mapping[str, str] = field(default_factory=FrozenDict)
×
133

134

UNCOV
135
async def _run_tool_without_resolve(request: NodeJSToolRequest, nodejs: NodeJS) -> Process:
×
136
    pkg_manager_version = nodejs.package_managers.get(nodejs.package_manager)
×
137
    pkg_manager_and_version = nodejs.default_package_manager
×
138
    if pkg_manager_version is None or pkg_manager_and_version is None:
×
139
        # Occurs when a user configures a custom package manager but without a resolve.
140
        # Corepack requires a package.json to make a decision on a "good known release".
141
        raise ValueError(
×
142
            softwrap(
143
                f"""
144
                Version for {nodejs.package_manager} has to be configured
145
                in [{nodejs.options_scope}].package_managers when running
146
                the tool '{request.binary_name}' without setting [{request.options_scope}].install_from_resolve.
147
                """
148
            )
149
        )
150
    pkg_manager = PackageManager.from_string(pkg_manager_and_version)
×
151

152
    return await setup_node_tool_process(
×
153
        NodeJSToolProcess(
154
            pkg_manager.name,
155
            pkg_manager.version,
156
            args=pkg_manager.make_download_and_execute_args(
157
                request.package,
158
                request.binary_name,
159
                request.args,
160
            ),
161
            description=request.description,
162
            input_digest=request.input_digest,
163
            output_files=request.output_files,
164
            output_directories=request.output_directories,
165
            append_only_caches=request.append_only_caches,
166
            timeout_seconds=request.timeout_seconds,
167
            extra_env=FrozenDict({**pkg_manager.extra_env, **request.extra_env}),
168
        ),
169
        **implicitly(),
170
    )
171

172

UNCOV
173
async def _run_tool_with_resolve(request: NodeJSToolRequest, resolve: str) -> Process:
×
174
    resolves = await resolve_to_projects(**implicitly())
×
175

176
    if request.resolve not in resolves:
×
177
        reason = (
×
178
            f"Available resolves are {', '.join(resolves.keys())}."
179
            if resolves
180
            else "This project contains no resolves."
181
        )
182
        raise ValueError(f"{resolve} is not a named NodeJS resolve. {reason}")
×
183

184
    all_first_party = await resolve_to_first_party_node_package(**implicitly())
×
185
    package_for_resolve = all_first_party[resolve]
×
186
    project = resolves[resolve]
×
187

188
    installed = await install_node_packages_for_address(
×
189
        InstalledNodePackageRequest(package_for_resolve.address), **implicitly()
190
    )
191
    merged_input_digest = await merge_digests(
×
192
        MergeDigests([request.input_digest, installed.digest])
193
    )
194

195
    return await setup_nodejs_project_environment_process(
×
196
        NodeJsProjectEnvironmentProcess(
197
            env=installed.project_env,
198
            args=(*project.package_manager.execute_args, request.binary_name, *request.args),
199
            description=request.description,
200
            input_digest=merged_input_digest,
201
            output_files=request.output_files,
202
            output_directories=request.output_directories,
203
            per_package_caches=request.append_only_caches,
204
            project_caches=request.project_caches,
205
            timeout_seconds=request.timeout_seconds,
206
            extra_env=FrozenDict(request.extra_env),
207
        ),
208
        **implicitly(),
209
    )
210

211

UNCOV
212
@rule
×
UNCOV
213
async def prepare_tool_process(request: NodeJSToolRequest, nodejs: NodeJS) -> Process:
×
214
    if request.resolve is None:
×
215
        return await _run_tool_without_resolve(request, nodejs)
×
216
    return await _run_tool_with_resolve(request, request.resolve)
×
217

218

UNCOV
219
def rules() -> Iterable[Rule | UnionRule]:
×
UNCOV
220
    return [*collect_rules(), *nodejs_project_environment.rules(), *install_node_package.rules()]
×
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