• 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/util_rules/local_dists.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
import shlex
×
UNCOV
8
from collections.abc import Iterable
×
UNCOV
9
from dataclasses import dataclass
×
10

UNCOV
11
from pants.backend.python.subsystems.setuptools import PythonDistributionFieldSet
×
UNCOV
12
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
13
from pants.backend.python.util_rules.pex import Pex, PexRequest, create_pex
×
UNCOV
14
from pants.backend.python.util_rules.pex import rules as pex_rules
×
UNCOV
15
from pants.backend.python.util_rules.pex_requirements import PexRequirements
×
UNCOV
16
from pants.backend.python.util_rules.python_sources import PythonSourceFiles
×
UNCOV
17
from pants.build_graph.address import Address
×
UNCOV
18
from pants.core.goals.package import PackageFieldSet, build_package
×
UNCOV
19
from pants.core.util_rules import system_binaries
×
UNCOV
20
from pants.core.util_rules.source_files import SourceFiles
×
UNCOV
21
from pants.core.util_rules.system_binaries import BashBinary, UnzipBinary
×
UNCOV
22
from pants.engine.addresses import Addresses
×
UNCOV
23
from pants.engine.fs import Digest, DigestSubset, MergeDigests, PathGlobs
×
UNCOV
24
from pants.engine.internals.graph import resolve_target
×
UNCOV
25
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
×
UNCOV
26
from pants.engine.intrinsics import digest_to_snapshot, merge_digests
×
UNCOV
27
from pants.engine.process import Process, fallible_to_exec_result_or_raise
×
UNCOV
28
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
29
from pants.engine.target import TransitiveTargetsRequest, WrappedTargetRequest
×
UNCOV
30
from pants.util.dirutil import fast_relpath_optional
×
UNCOV
31
from pants.util.docutil import doc_url
×
UNCOV
32
from pants.util.strutil import softwrap
×
33

UNCOV
34
logger = logging.getLogger(__name__)
×
35

36

UNCOV
37
@dataclass(frozen=True)
×
UNCOV
38
class LocalDistWheels:
×
39
    """Contains the wheels isolated from a single local Python distribution."""
40

UNCOV
41
    wheel_paths: tuple[str, ...]
×
UNCOV
42
    wheels_digest: Digest
×
UNCOV
43
    provided_files: frozenset[str]
×
44

45

UNCOV
46
@rule
×
UNCOV
47
async def isolate_local_dist_wheels(
×
48
    dist_field_set: PythonDistributionFieldSet,
49
    bash: BashBinary,
50
    unzip_binary: UnzipBinary,
51
) -> LocalDistWheels:
52
    dist = await build_package(**implicitly({dist_field_set: PackageFieldSet}))
×
53
    wheels_snapshot = await digest_to_snapshot(
×
54
        **implicitly(DigestSubset(dist.digest, PathGlobs(["**/*.whl"])))
55
    )
56

57
    # A given local dist might build a wheel and an sdist (and maybe other artifacts -
58
    # we don't know what setup command was run...)
59
    # As long as there is a wheel, we can ignore the other artifacts.
60
    artifacts = {(a.relpath or "") for a in dist.artifacts}
×
61
    wheels = [wheel for wheel in wheels_snapshot.files if wheel in artifacts]
×
62

63
    if not wheels:
×
64
        tgt = await resolve_target(
×
65
            WrappedTargetRequest(dist_field_set.address, description_of_origin="<infallible>"),
66
            **implicitly(),
67
        )
68
        logger.warning(
×
69
            softwrap(
70
                f"""
71
                Encountered a dependency on the {tgt.target.alias} target at {dist_field_set.address},
72
                but this target does not produce a Python wheel artifact. Therefore this target's
73
                code will be used directly from sources, without a distribution being built,
74
                and any native extensions in it will not be built.
75

76
                See {doc_url("docs/python/overview/building-distributions")} for details on how to set up a
77
                {tgt.target.alias} target to produce a wheel.
78
                """
79
            )
80
        )
81

82
    wheels_listing_result = await fallible_to_exec_result_or_raise(
×
83
        **implicitly(
84
            Process(
85
                argv=[
86
                    bash.path,
87
                    "-c",
88
                    f"""
89
                set -ex
90
                for f in {" ".join(shlex.quote(f) for f in wheels)}; do
91
                  {unzip_binary.path} -Z1 "$f"
92
                done
93
                """,
94
                ],
95
                input_digest=wheels_snapshot.digest,
96
                description=f"List contents of artifacts produced by {dist_field_set.address}",
97
            )
98
        )
99
    )
100
    provided_files = set(wheels_listing_result.stdout.decode().splitlines())
×
101

102
    return LocalDistWheels(
×
103
        tuple(sorted(wheels)), wheels_snapshot.digest, frozenset(sorted(provided_files))
104
    )
105

106

UNCOV
107
@dataclass(frozen=True)
×
UNCOV
108
class LocalDistsPexRequest:
×
109
    """Request to build the local dists from the dependency closure of a set of addresses."""
110

UNCOV
111
    addresses: Addresses
×
UNCOV
112
    interpreter_constraints: InterpreterConstraints
×
113
    # The result will return these with the sources provided by the dists subtracted out.
114
    # This will help the caller prevent sources from appearing twice on sys.path.
UNCOV
115
    sources: PythonSourceFiles
×
116

UNCOV
117
    def __init__(
×
118
        self,
119
        addresses: Iterable[Address],
120
        *,
121
        interpreter_constraints: InterpreterConstraints,
122
        sources: PythonSourceFiles = PythonSourceFiles.empty(),
123
    ) -> None:
UNCOV
124
        object.__setattr__(self, "addresses", Addresses(addresses))
×
UNCOV
125
        object.__setattr__(self, "interpreter_constraints", interpreter_constraints)
×
UNCOV
126
        object.__setattr__(self, "sources", sources)
×
127

128

UNCOV
129
@dataclass(frozen=True)
×
UNCOV
130
class LocalDistsPex:
×
131
    """A PEX file containing locally-built dists.
132

133
    Can be consumed from another PEX, e.g., by adding to PEX_PATH.
134

135
    The PEX will only contain locally built dists and not their dependencies. For Pants generated
136
    `setup.py` / `pyproject.toml`, the dependencies will be included in the standard resolve process
137
    that the locally-built dists PEX is adjoined to via PEX_PATH. For hand-made `setup.py` /
138
    `pyproject.toml` with 3rdparty dependencies not hand-mirrored into BUILD file dependencies, this
139
    will lead to issues. See https://github.com/pantsbuild/pants/issues/13587#issuecomment-974863636
140
    for one way to fix this corner which is intentionally punted on for now.
141

142
    Lists the files provided by the dists on sys.path, so they can be subtracted from
143
    sources digests, to prevent the same file ending up on sys.path twice.
144
    """
145

UNCOV
146
    pex: Pex
×
147
    # The sources from the request, but with any files provided by the local dists subtracted out.
UNCOV
148
    remaining_sources: PythonSourceFiles
×
149

150

UNCOV
151
@rule(desc="Building local distributions")
×
UNCOV
152
async def build_local_dists(
×
153
    request: LocalDistsPexRequest,
154
) -> LocalDistsPex:
155
    transitive_targets = await transitive_targets_get(
×
156
        TransitiveTargetsRequest(request.addresses), **implicitly()
157
    )
158
    applicable_targets = [
×
159
        tgt for tgt in transitive_targets.closure if PythonDistributionFieldSet.is_applicable(tgt)
160
    ]
161

162
    local_dists_wheels = await concurrently(
×
163
        isolate_local_dist_wheels(PythonDistributionFieldSet.create(target), **implicitly())
164
        for target in applicable_targets
165
    )
166

167
    # The primary use-case of the "local dists" feature is to support consuming native extensions
168
    # as wheels without having to publish them first.
169
    # It doesn't seem very useful to consume locally-built sdists, and it makes it hard to
170
    # reason about possible sys.path collisions between the in-repo sources and whatever the
171
    # sdist will place on the sys.path when it's installed.
172
    # So for now we simply ignore sdists, with a warning if necessary.
173
    provided_files: set[str] = set()
×
174
    wheels: list[str] = []
×
175
    wheels_digests = []
×
176
    for local_dist_wheels in local_dists_wheels:
×
177
        wheels.extend(local_dist_wheels.wheel_paths)
×
178
        wheels_digests.append(local_dist_wheels.wheels_digest)
×
179
        provided_files.update(local_dist_wheels.provided_files)
×
180

181
    wheels_digest = await merge_digests(MergeDigests(wheels_digests))
×
182

183
    dists_pex = await create_pex(
×
184
        PexRequest(
185
            output_filename="local_dists.pex",
186
            requirements=PexRequirements(wheels),
187
            interpreter_constraints=request.interpreter_constraints,
188
            additional_inputs=wheels_digest,
189
            # a "local dists" PEX is always just for consumption by some downstream Pants process,
190
            # i.e. internal
191
            internal_only=True,
192
            additional_args=["--intransitive"],
193
        )
194
    )
195

196
    if not wheels:
×
197
        # The source calculations below are not (always) cheap, so we skip them if no wheels were
198
        # produced. See https://github.com/pantsbuild/pants/issues/14561 for one possible approach
199
        # to sharing the cost of these calculations.
200
        return LocalDistsPex(dists_pex, request.sources)
×
201

202
    # We check source roots in reverse lexicographic order,
203
    # so we'll find the innermost root that matches.
204
    source_roots = sorted(request.sources.source_roots, reverse=True)
×
205
    remaining_sources = set(request.sources.source_files.files)
×
206
    unrooted_files_set = set(request.sources.source_files.unrooted_files)
×
207
    for source in request.sources.source_files.files:
×
208
        if source not in unrooted_files_set:
×
209
            for source_root in source_roots:
×
210
                source_relpath = fast_relpath_optional(source, source_root)
×
211
                if source_relpath is not None and source_relpath in provided_files:
×
212
                    remaining_sources.remove(source)
×
213
    remaining_sources_snapshot = await digest_to_snapshot(
×
214
        **implicitly(
215
            DigestSubset(
216
                request.sources.source_files.snapshot.digest, PathGlobs(sorted(remaining_sources))
217
            )
218
        )
219
    )
220
    subtracted_sources = PythonSourceFiles(
×
221
        SourceFiles(remaining_sources_snapshot, request.sources.source_files.unrooted_files),
222
        request.sources.source_roots,
223
    )
224

225
    return LocalDistsPex(dists_pex, subtracted_sources)
×
226

227

UNCOV
228
def rules():
×
UNCOV
229
    return (
×
230
        *collect_rules(),
231
        *pex_rules(),
232
        *system_binaries.rules(),
233
    )
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