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

pantsbuild / pants / 21038851336

15 Jan 2026 04:37PM UTC coverage: 78.666% (-1.6%) from 80.269%
21038851336

push

github

web-flow
update to Pex 2.81.0 (#23018)

Release Notes:
 * https://github.com/pex-tool/pex/releases/tag/v2.80.0
 * https://github.com/pex-tool/pex/releases/tag/v2.81.0

3 of 3 new or added lines in 1 file covered. (100.0%)

1077 existing lines in 52 files now uncovered.

75010 of 95353 relevant lines covered (78.67%)

3.17 hits per line

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

79.17
/src/python/pants/init/plugin_resolver.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
11✔
5

6
import importlib.metadata
11✔
7
import logging
11✔
8
import site
11✔
9
import sys
11✔
10
from collections.abc import Iterable
11✔
11
from dataclasses import dataclass
11✔
12
from typing import cast
11✔
13

14
from packaging.requirements import Requirement
11✔
15

16
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
11✔
17
from pants.backend.python.util_rules.pex import PexRequest, VenvPexProcess, create_venv_pex
11✔
18
from pants.backend.python.util_rules.pex_environment import PythonExecutable
11✔
19
from pants.backend.python.util_rules.pex_requirements import PexRequirements
11✔
20
from pants.core.environments.rules import determine_bootstrap_environment
11✔
21
from pants.engine.collection import DeduplicatedCollection
11✔
22
from pants.engine.env_vars import CompleteEnvironmentVars
11✔
23
from pants.engine.environment import EnvironmentName
11✔
24
from pants.engine.internals.selectors import Params
11✔
25
from pants.engine.internals.session import SessionValues
11✔
26
from pants.engine.process import ProcessCacheScope, execute_process_or_raise
11✔
27
from pants.engine.rules import QueryRule, collect_rules, implicitly, rule
11✔
28
from pants.init.bootstrap_scheduler import BootstrapScheduler
11✔
29
from pants.init.import_util import find_matching_distributions
11✔
30
from pants.option.global_options import GlobalOptions
11✔
31
from pants.option.options_bootstrapper import OptionsBootstrapper
11✔
32
from pants.util.logging import LogLevel
11✔
33

34
logger = logging.getLogger(__name__)
11✔
35

36

37
@dataclass(frozen=True)
11✔
38
class PluginsRequest:
11✔
39
    # Interpreter constraints to resolve for, or None to resolve for the interpreter that Pants is
40
    # running under.
41
    interpreter_constraints: InterpreterConstraints | None
11✔
42
    # Requirement constraints to resolve with. If plugins will be loaded into the global working_set
43
    # (i.e., onto the `sys.path`), then these should be the current contents of the working_set.
44
    constraints: tuple[Requirement, ...]
11✔
45
    # Backend requirements to resolve
46
    requirements: tuple[str, ...]
11✔
47

48

49
class ResolvedPluginDistributions(DeduplicatedCollection[str]):
11✔
50
    sort_input = True
11✔
51

52

53
@rule
11✔
54
async def resolve_plugins(
11✔
55
    request: PluginsRequest,
56
    global_options: GlobalOptions,
57
) -> ResolvedPluginDistributions:
58
    """This rule resolves plugins using a VenvPex, and exposes the absolute paths of their dists.
59

60
    NB: This relies on the fact that PEX constructs venvs in a stable location (within the
61
    `named_caches` directory), but consequently needs to disable the process cache: see the
62
    ProcessCacheScope reference in the body.
63
    """
64
    req_strings = sorted(global_options.plugins + request.requirements)
×
65

66
    requirements = PexRequirements(
×
67
        req_strings_or_addrs=req_strings,
68
        constraints_strings=(str(constraint) for constraint in request.constraints),
69
        description_of_origin="configured Pants plugins",
70
    )
71
    if not requirements:
×
72
        return ResolvedPluginDistributions()
×
73

74
    python: PythonExecutable | None = None
×
75
    if not request.interpreter_constraints:
×
76
        python = PythonExecutable.fingerprinted(
×
77
            sys.executable, ".".join(map(str, sys.version_info[:3])).encode("utf8")
78
        )
79

80
    plugins_pex = await create_venv_pex(
×
81
        **implicitly(
82
            PexRequest(
83
                output_filename="pants_plugins.pex",
84
                internal_only=True,
85
                python=python,
86
                requirements=requirements,
87
                interpreter_constraints=request.interpreter_constraints or InterpreterConstraints(),
88
                additional_args=("--preserve-pip-download-log", "pex-pip-download.log"),
89
                description=f"Resolving plugins: {', '.join(req_strings)}",
90
            )
91
        )
92
    )
93

94
    # NB: We run this Process per-restart because it (intentionally) leaks named cache
95
    # paths in a way that invalidates the Process-cache. See the method doc.
96
    cache_scope = (
×
97
        ProcessCacheScope.PER_SESSION
98
        if global_options.plugins_force_resolve
99
        else ProcessCacheScope.PER_RESTART_SUCCESSFUL
100
    )
101

102
    plugins_process_result = await execute_process_or_raise(
×
103
        **implicitly(
104
            VenvPexProcess(
105
                plugins_pex,
106
                argv=("-c", "import os, site; print(os.linesep.join(site.getsitepackages()))"),
107
                description="Extracting plugin locations",
108
                level=LogLevel.DEBUG,
109
                cache_scope=cache_scope,
110
            )
111
        )
112
    )
113
    return ResolvedPluginDistributions(plugins_process_result.stdout.decode().strip().split("\n"))
×
114

115

116
class PluginResolver:
11✔
117
    """Encapsulates the state of plugin loading.
118

119
    Plugin loading is inherently stateful, and so the system enviroment on `sys.path` will be
120
    mutated by each call to `PluginResolver.resolve`.
121
    """
122

123
    def __init__(
11✔
124
        self,
125
        scheduler: BootstrapScheduler,
126
        interpreter_constraints: InterpreterConstraints | None = None,
127
        inherit_existing_constraints: bool = True,
128
    ) -> None:
129
        self._scheduler = scheduler
3✔
130
        self._interpreter_constraints = interpreter_constraints
3✔
131
        self._inherit_existing_constraints = inherit_existing_constraints
3✔
132

133
    def resolve(
11✔
134
        self,
135
        options_bootstrapper: OptionsBootstrapper,
136
        env: CompleteEnvironmentVars,
137
        requirements: Iterable[str] = (),
138
    ) -> list[str]:
139
        """Resolves any configured plugins and adds them to the sys.path as a side effect."""
140

141
        def to_requirement(d):
3✔
142
            return f"{d.name}=={d.version}"
3✔
143

144
        distributions: list[importlib.metadata.Distribution] = []
3✔
145
        if self._inherit_existing_constraints:
3✔
146
            distributions = list(find_matching_distributions(None))
3✔
147

148
        request = PluginsRequest(
3✔
149
            self._interpreter_constraints,
150
            tuple(to_requirement(dist) for dist in distributions),
151
            tuple(requirements),
152
        )
153

154
        result = []
3✔
155
        for resolved_plugin_location in self._resolve_plugins(options_bootstrapper, env, request):
3✔
156
            # Activate any .pth files plugin wheels may have.
UNCOV
157
            orig_sys_path_len = len(sys.path)
×
UNCOV
158
            site.addsitedir(resolved_plugin_location)
×
UNCOV
159
            if len(sys.path) > orig_sys_path_len:
×
UNCOV
160
                result.append(resolved_plugin_location)
×
161

162
        return result
2✔
163

164
    def _resolve_plugins(
11✔
165
        self,
166
        options_bootstrapper: OptionsBootstrapper,
167
        env: CompleteEnvironmentVars,
168
        request: PluginsRequest,
169
    ) -> ResolvedPluginDistributions:
170
        session = self._scheduler.scheduler.new_session(
3✔
171
            "plugin_resolver",
172
            session_values=SessionValues(
173
                {
174
                    OptionsBootstrapper: options_bootstrapper,
175
                    CompleteEnvironmentVars: env,
176
                }
177
            ),
178
        )
179
        params = Params(request, determine_bootstrap_environment(session))
3✔
180
        return cast(
2✔
181
            ResolvedPluginDistributions,
182
            session.product_request(ResolvedPluginDistributions, params)[0],
183
        )
184

185

186
def rules():
11✔
187
    return [
3✔
188
        QueryRule(ResolvedPluginDistributions, [PluginsRequest, EnvironmentName]),
189
        *collect_rules(),
190
    ]
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