• 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/pex_from_targets.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 dataclasses
×
UNCOV
7
import logging
×
UNCOV
8
from collections.abc import Iterable, Mapping
×
UNCOV
9
from dataclasses import dataclass
×
10

UNCOV
11
from packaging.utils import canonicalize_name as canonicalize_project_name
×
12

UNCOV
13
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
14
from pants.backend.python.target_types import (
×
15
    Executable,
16
    MainSpecification,
17
    PexLayout,
18
    PythonRequirementsField,
19
    PythonResolveField,
20
)
UNCOV
21
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
22
from pants.backend.python.util_rules.local_dists import LocalDistsPexRequest, build_local_dists
×
UNCOV
23
from pants.backend.python.util_rules.local_dists import rules as local_dists_rules
×
UNCOV
24
from pants.backend.python.util_rules.pex import (
×
25
    CompletePlatforms,
26
    OptionalPexRequest,
27
    Pex,
28
    PexPlatforms,
29
    PexRequest,
30
    create_optional_pex,
31
)
UNCOV
32
from pants.backend.python.util_rules.pex import rules as pex_rules
×
UNCOV
33
from pants.backend.python.util_rules.pex_requirements import (
×
34
    EntireLockfile,
35
    LoadedLockfileRequest,
36
    Lockfile,
37
    PexRequirements,
38
    Resolve,
39
    load_lockfile,
40
)
UNCOV
41
from pants.backend.python.util_rules.python_sources import (
×
42
    PythonSourceFiles,
43
    PythonSourceFilesRequest,
44
    prepare_python_sources,
45
)
UNCOV
46
from pants.backend.python.util_rules.python_sources import rules as python_sources_rules
×
UNCOV
47
from pants.backend.python.util_rules.python_sources import strip_python_sources
×
UNCOV
48
from pants.core.goals.generate_lockfiles import NoCompatibleResolveException
×
UNCOV
49
from pants.core.goals.package import TraverseIfNotPackageTarget
×
UNCOV
50
from pants.core.target_types import FileSourceField
×
UNCOV
51
from pants.engine.addresses import Address, Addresses
×
UNCOV
52
from pants.engine.collection import DeduplicatedCollection
×
UNCOV
53
from pants.engine.fs import Digest, GlobMatchErrorBehavior, MergeDigests, PathGlobs
×
UNCOV
54
from pants.engine.internals.graph import OwnersRequest, find_owners, resolve_targets
×
UNCOV
55
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
×
UNCOV
56
from pants.engine.intrinsics import get_digest_contents, merge_digests
×
UNCOV
57
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
58
from pants.engine.target import (
×
59
    Target,
60
    TransitiveTargets,
61
    TransitiveTargetsRequest,
62
    targets_with_sources_types,
63
)
UNCOV
64
from pants.engine.unions import UnionMembership
×
UNCOV
65
from pants.util.docutil import doc_url
×
UNCOV
66
from pants.util.frozendict import FrozenDict
×
UNCOV
67
from pants.util.logging import LogLevel
×
UNCOV
68
from pants.util.pip_requirement import PipRequirement
×
UNCOV
69
from pants.util.requirements import parse_requirements_file
×
UNCOV
70
from pants.util.strutil import path_safe, softwrap
×
71

UNCOV
72
logger = logging.getLogger(__name__)
×
73

74

UNCOV
75
@dataclass(frozen=True)
×
UNCOV
76
class PexFromTargetsRequest:
×
UNCOV
77
    addresses: Addresses
×
UNCOV
78
    output_filename: str
×
UNCOV
79
    internal_only: bool
×
UNCOV
80
    layout: PexLayout | None
×
UNCOV
81
    main: MainSpecification | None
×
UNCOV
82
    inject_args: tuple[str, ...]
×
UNCOV
83
    inject_env: FrozenDict[str, str]
×
UNCOV
84
    platforms: PexPlatforms
×
UNCOV
85
    complete_platforms: CompletePlatforms
×
UNCOV
86
    additional_args: tuple[str, ...]
×
UNCOV
87
    additional_lockfile_args: tuple[str, ...]
×
UNCOV
88
    include_source_files: bool
×
UNCOV
89
    include_requirements: bool
×
UNCOV
90
    include_local_dists: bool
×
UNCOV
91
    additional_sources: Digest | None
×
UNCOV
92
    additional_inputs: Digest | None
×
UNCOV
93
    hardcoded_interpreter_constraints: InterpreterConstraints | None
×
UNCOV
94
    warn_for_transitive_files_targets: bool
×
95
    # This field doesn't participate in comparison (and therefore hashing), as it doesn't affect
96
    # the result.
UNCOV
97
    description: str | None = dataclasses.field(compare=False)
×
98

UNCOV
99
    def __init__(
×
100
        self,
101
        addresses: Iterable[Address],
102
        *,
103
        output_filename: str,
104
        internal_only: bool,
105
        layout: PexLayout | None = None,
106
        main: MainSpecification | None = None,
107
        inject_args: Iterable[str] = (),
108
        inject_env: Mapping[str, str] = FrozenDict(),
109
        platforms: PexPlatforms = PexPlatforms(),
110
        complete_platforms: CompletePlatforms = CompletePlatforms(),
111
        additional_args: Iterable[str] = (),
112
        additional_lockfile_args: Iterable[str] = (),
113
        include_source_files: bool = True,
114
        include_requirements: bool = True,
115
        include_local_dists: bool = False,
116
        additional_sources: Digest | None = None,
117
        additional_inputs: Digest | None = None,
118
        hardcoded_interpreter_constraints: InterpreterConstraints | None = None,
119
        description: str | None = None,
120
        warn_for_transitive_files_targets: bool = False,
121
    ) -> None:
122
        """Request to create a Pex from the transitive closure of the given addresses.
123

124
        :param addresses: The addresses to use for determining what is included in the Pex. The
125
            transitive closure of these addresses will be used; you only need to specify the roots.
126
        :param output_filename: The name of the built Pex file, which typically should end in
127
            `.pex`.
128
        :param internal_only: Whether we ever materialize the Pex and distribute it directly
129
            to end users, such as with the `binary` goal. Typically, instead, the user never
130
            directly uses the Pex, e.g. with `lint` and `test`. If True, we will use a Pex setting
131
            that results in faster build time but compatibility with fewer interpreters at runtime.
132
        :param layout: The filesystem layout to create the PEX with.
133
        :param main: The main for the built Pex, equivalent to Pex's `-e` or `-c` flag. If
134
            left off, the Pex will open up as a REPL.
135
        :param inject_args: Command line arguments to freeze in to the PEX.
136
        :param inject_env: Environment variables to freeze in to the PEX.
137
        :param platforms: Which platforms should be supported. Setting this value will cause
138
            interpreter constraints to not be used because platforms already constrain the valid
139
            Python versions, e.g. by including `cp36m` in the platform string.
140
        :param additional_args: Any additional Pex flags.
141
        :param additional_lockfile_args: Any additional Pex flags that should be used with the
142
            lockfile.pex. Many Pex args like `--emit-warnings` do not impact the lockfile, and
143
            setting them would reduce reuse with other call sites. Generally, these should only be
144
            flags that impact lockfile resolution like `--manylinux`.
145
        :param include_source_files: Whether to include source files in the built Pex or not.
146
            Setting this to `False` and loading the source files by instead populating the chroot
147
            and setting the environment variable `PEX_EXTRA_SYS_PATH` will result in substantially
148
            fewer rebuilds of the Pex.
149
        :param include_requirements: Whether to resolve requirements and include them in the Pex.
150
        :param include_local_dists: Whether to build local dists and include them in the built pex.
151
        :param additional_sources: Any additional source files to include in the built Pex.
152
        :param additional_inputs: Any inputs that are not source files and should not be included
153
            directly in the Pex, but should be present in the environment when building the Pex.
154
        :param hardcoded_interpreter_constraints: Use these constraints rather than resolving the
155
            constraints from the input.
156
        :param description: A human-readable description to render in the dynamic UI when building
157
            the Pex.
158
        :param warn_for_transitive_files_targets: If True (and include_source_files is also true),
159
            emit a warning if the pex depends on any `files` targets, since they won't be included.
160
        """
UNCOV
161
        object.__setattr__(self, "addresses", Addresses(addresses))
×
UNCOV
162
        object.__setattr__(self, "output_filename", output_filename)
×
UNCOV
163
        object.__setattr__(self, "internal_only", internal_only)
×
UNCOV
164
        object.__setattr__(self, "layout", layout)
×
UNCOV
165
        object.__setattr__(self, "main", main)
×
UNCOV
166
        object.__setattr__(self, "inject_args", tuple(inject_args))
×
UNCOV
167
        object.__setattr__(self, "inject_env", FrozenDict(inject_env))
×
UNCOV
168
        object.__setattr__(self, "platforms", platforms)
×
UNCOV
169
        object.__setattr__(self, "complete_platforms", complete_platforms)
×
UNCOV
170
        object.__setattr__(self, "additional_args", tuple(additional_args))
×
UNCOV
171
        object.__setattr__(self, "additional_lockfile_args", tuple(additional_lockfile_args))
×
UNCOV
172
        object.__setattr__(self, "include_source_files", include_source_files)
×
UNCOV
173
        object.__setattr__(self, "include_requirements", include_requirements)
×
UNCOV
174
        object.__setattr__(self, "include_local_dists", include_local_dists)
×
UNCOV
175
        object.__setattr__(self, "additional_sources", additional_sources)
×
UNCOV
176
        object.__setattr__(self, "additional_inputs", additional_inputs)
×
UNCOV
177
        object.__setattr__(
×
178
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
179
        )
UNCOV
180
        object.__setattr__(self, "description", description)
×
UNCOV
181
        object.__setattr__(
×
182
            self, "warn_for_transitive_files_targets", warn_for_transitive_files_targets
183
        )
184

UNCOV
185
        self.__post_init__()
×
186

UNCOV
187
    def __post_init__(self):
×
UNCOV
188
        if self.internal_only and (self.platforms or self.complete_platforms):
×
189
            raise AssertionError(
×
190
                softwrap(
191
                    """
192
                    PexFromTargetsRequest set internal_only at the same time as setting
193
                    `platforms` and/or `complete_platforms`. Platforms can only be used when
194
                    `internal_only=False`.
195
                    """
196
                )
197
            )
198

UNCOV
199
    def to_interpreter_constraints_request(self) -> InterpreterConstraintsRequest:
×
200
        return InterpreterConstraintsRequest(
×
201
            addresses=self.addresses,
202
            hardcoded_interpreter_constraints=self.hardcoded_interpreter_constraints,
203
        )
204

205

UNCOV
206
@dataclass(frozen=True)
×
UNCOV
207
class InterpreterConstraintsRequest:
×
UNCOV
208
    addresses: Addresses
×
UNCOV
209
    hardcoded_interpreter_constraints: InterpreterConstraints | None
×
210

UNCOV
211
    def __init__(
×
212
        self,
213
        addresses: Iterable[Address],
214
        *,
215
        hardcoded_interpreter_constraints: InterpreterConstraints | None = None,
216
    ) -> None:
UNCOV
217
        object.__setattr__(self, "addresses", Addresses(addresses))
×
UNCOV
218
        object.__setattr__(
×
219
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
220
        )
221

222

UNCOV
223
@rule
×
UNCOV
224
async def interpreter_constraints_for_targets(
×
225
    request: InterpreterConstraintsRequest, python_setup: PythonSetup
226
) -> InterpreterConstraints:
227
    if request.hardcoded_interpreter_constraints:
×
228
        return request.hardcoded_interpreter_constraints
×
229

230
    transitive_targets = await transitive_targets_get(
×
231
        TransitiveTargetsRequest(request.addresses), **implicitly()
232
    )
233
    calculated_constraints = InterpreterConstraints.create_from_targets(
×
234
        transitive_targets.closure, python_setup
235
    )
236
    # If there are no targets, we fall back to the global constraints. This is relevant,
237
    # for example, when running `./pants repl` with no specs or only on targets without
238
    # `interpreter_constraints` (e.g. `python_requirement`).
239
    interpreter_constraints = calculated_constraints or InterpreterConstraints(
×
240
        python_setup.interpreter_constraints
241
    )
242
    return interpreter_constraints
×
243

244

UNCOV
245
@dataclass(frozen=True)
×
UNCOV
246
class ChosenPythonResolve:
×
UNCOV
247
    name: str
×
UNCOV
248
    lockfile: Lockfile
×
249

250

UNCOV
251
@dataclass(frozen=True)
×
UNCOV
252
class ChosenPythonResolveRequest:
×
UNCOV
253
    addresses: Addresses
×
254

255

UNCOV
256
@rule
×
UNCOV
257
async def choose_python_resolve(
×
258
    request: ChosenPythonResolveRequest, python_setup: PythonSetup
259
) -> ChosenPythonResolve:
260
    transitive_targets = await transitive_targets_get(
×
261
        TransitiveTargetsRequest(request.addresses), **implicitly()
262
    )
263

264
    def maybe_get_resolve(t: Target) -> str | None:
×
265
        if not t.has_field(PythonResolveField):
×
266
            return None
×
267
        return t[PythonResolveField].normalized_value(python_setup)
×
268

269
    # First, choose the resolve by inspecting the root targets.
270
    root_resolves = {
×
271
        root[PythonResolveField].normalized_value(python_setup)
272
        for root in transitive_targets.roots
273
        if root.has_field(PythonResolveField)
274
    }
275
    if root_resolves:
×
276
        if len(root_resolves) > 1:
×
277
            raise NoCompatibleResolveException.bad_input_roots(
×
278
                transitive_targets.roots,
279
                maybe_get_resolve=maybe_get_resolve,
280
                doc_url_slug="docs/python/overview/lockfiles#multiple-lockfiles",
281
                workaround=None,
282
            )
283

284
        chosen_resolve = next(iter(root_resolves))
×
285

286
        # Then, validate that all transitive deps are compatible.
287
        for tgt in transitive_targets.dependencies:
×
288
            if (
×
289
                tgt.has_field(PythonResolveField)
290
                and tgt[PythonResolveField].normalized_value(python_setup) != chosen_resolve
291
            ):
292
                raise NoCompatibleResolveException.bad_dependencies(
×
293
                    maybe_get_resolve=maybe_get_resolve,
294
                    doc_url_slug="docs/python/overview/lockfiles#multiple-lockfiles",
295
                    root_resolve=chosen_resolve,
296
                    root_targets=transitive_targets.roots,
297
                    dependencies=transitive_targets.dependencies,
298
                )
299

300
    else:
301
        # If there are no relevant targets, we fall back to the default resolve. This is relevant,
302
        # for example, when running `./pants repl` with no specs or only on non-Python targets.
303
        chosen_resolve = python_setup.default_resolve
×
304

305
    return ChosenPythonResolve(
×
306
        name=chosen_resolve,
307
        lockfile=Lockfile(
308
            url=python_setup.resolves[chosen_resolve],
309
            url_description_of_origin=(
310
                f"the resolve `{chosen_resolve}` (from `[python].resolves`)"
311
            ),
312
            resolve_name=chosen_resolve,
313
        ),
314
    )
315

316

UNCOV
317
class GlobalRequirementConstraints(DeduplicatedCollection[PipRequirement]):
×
318
    """Global constraints specified by the `[python].requirement_constraints` setting, if any."""
319

320

UNCOV
321
@rule
×
UNCOV
322
async def determine_global_requirement_constraints(
×
323
    python_setup: PythonSetup,
324
) -> GlobalRequirementConstraints:
325
    if not python_setup.requirement_constraints:
×
326
        return GlobalRequirementConstraints()
×
327

328
    constraints_file_contents = await get_digest_contents(
×
329
        **implicitly(
330
            PathGlobs(
331
                [python_setup.requirement_constraints],
332
                glob_match_error_behavior=GlobMatchErrorBehavior.error,
333
                description_of_origin="the option `[python].requirement_constraints`",
334
            )
335
        )
336
    )
337

338
    return GlobalRequirementConstraints(
×
339
        parse_requirements_file(
340
            constraints_file_contents[0].content.decode(),
341
            rel_path=constraints_file_contents[0].path,
342
        )
343
    )
344

345

UNCOV
346
@dataclass(frozen=True)
×
UNCOV
347
class _PexRequirementsRequest:
×
348
    """Determine the requirement strings used transitively.
349

350
    This type is private because callers should likely use `RequirementsPexRequest` or
351
    `PexFromTargetsRequest` instead.
352
    """
353

UNCOV
354
    addresses: Addresses
×
355

356

UNCOV
357
@rule
×
UNCOV
358
async def determine_requirement_strings_in_closure(
×
359
    request: _PexRequirementsRequest, global_requirement_constraints: GlobalRequirementConstraints
360
) -> PexRequirements:
361
    addrs = request.addresses
×
362
    if len(addrs) == 0:
×
363
        description_of_origin = ""
×
364
    elif len(addrs) == 1:
×
365
        description_of_origin = addrs[0].spec
×
366
    else:
367
        description_of_origin = f"{addrs[0].spec} and {len(addrs) - 1} other targets"
×
368

369
    return PexRequirements(
×
370
        request.addresses,
371
        # This is only set if `[python].requirement_constraints` is configured, which is mutually
372
        # exclusive with resolves.
373
        constraints_strings=(str(constraint) for constraint in global_requirement_constraints),
374
        description_of_origin=description_of_origin,
375
    )
376

377

UNCOV
378
@dataclass(frozen=True)
×
UNCOV
379
class _RepositoryPexRequest:
×
UNCOV
380
    addresses: Addresses
×
UNCOV
381
    hardcoded_interpreter_constraints: InterpreterConstraints | None
×
UNCOV
382
    platforms: PexPlatforms
×
UNCOV
383
    complete_platforms: CompletePlatforms
×
UNCOV
384
    internal_only: bool
×
UNCOV
385
    additional_lockfile_args: tuple[str, ...]
×
386

UNCOV
387
    def __init__(
×
388
        self,
389
        addresses: Iterable[Address],
390
        *,
391
        internal_only: bool,
392
        hardcoded_interpreter_constraints: InterpreterConstraints | None = None,
393
        platforms: PexPlatforms = PexPlatforms(),
394
        complete_platforms: CompletePlatforms = CompletePlatforms(),
395
        additional_lockfile_args: tuple[str, ...] = (),
396
    ) -> None:
UNCOV
397
        object.__setattr__(self, "addresses", Addresses(addresses))
×
UNCOV
398
        object.__setattr__(self, "internal_only", internal_only)
×
UNCOV
399
        object.__setattr__(
×
400
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
401
        )
UNCOV
402
        object.__setattr__(self, "platforms", platforms)
×
UNCOV
403
        object.__setattr__(self, "complete_platforms", complete_platforms)
×
UNCOV
404
        object.__setattr__(self, "additional_lockfile_args", additional_lockfile_args)
×
405

UNCOV
406
    def to_interpreter_constraints_request(self) -> InterpreterConstraintsRequest:
×
407
        return InterpreterConstraintsRequest(
×
408
            addresses=self.addresses,
409
            hardcoded_interpreter_constraints=self.hardcoded_interpreter_constraints,
410
        )
411

412

UNCOV
413
@dataclass(frozen=True)
×
UNCOV
414
class _ConstraintsRepositoryPexRequest:
×
UNCOV
415
    repository_pex_request: _RepositoryPexRequest
×
416

417

UNCOV
418
@rule
×
UNCOV
419
async def _setup_constraints_repository_pex(
×
420
    constraints_request: _ConstraintsRepositoryPexRequest,
421
    python_setup: PythonSetup,
422
    global_requirement_constraints: GlobalRequirementConstraints,
423
) -> OptionalPexRequest:
424
    request = constraints_request.repository_pex_request
×
425
    if not python_setup.resolve_all_constraints:
×
426
        return OptionalPexRequest(None)
×
427

428
    constraints_path = python_setup.requirement_constraints
×
429
    assert constraints_path is not None
×
430

431
    transitive_targets = await transitive_targets_get(
×
432
        TransitiveTargetsRequest(request.addresses), **implicitly()
433
    )
434

435
    req_strings = PexRequirements.req_strings_from_requirement_fields(
×
436
        tgt[PythonRequirementsField]
437
        for tgt in transitive_targets.closure
438
        if tgt.has_field(PythonRequirementsField)
439
    )
440

441
    # In requirement strings, Foo_-Bar.BAZ and foo-bar-baz refer to the same project. We let
442
    # packaging canonicalize for us.
443
    # See: https://www.python.org/dev/peps/pep-0503/#normalized-names
444
    url_reqs = set()  # E.g., 'foobar@ git+https://github.com/foo/bar.git@branch'
×
445
    name_reqs = set()  # E.g., foobar>=1.2.3
×
446
    name_req_projects = set()
×
447
    constraints_file_reqs = set(global_requirement_constraints)
×
448

449
    for req_str in req_strings:
×
450
        req = PipRequirement.parse(req_str)
×
451
        if req.url:
×
452
            url_reqs.add(req)
×
453
        else:
454
            name_reqs.add(req)
×
455
            name_req_projects.add(canonicalize_project_name(req.name))
×
456

457
    constraint_file_projects = {
×
458
        canonicalize_project_name(req.name) for req in constraints_file_reqs
459
    }
460
    # Constraints files must only contain name reqs, not URL reqs (those are already
461
    # constrained by their very nature). See https://github.com/pypa/pip/issues/8210.
462
    unconstrained_projects = name_req_projects - constraint_file_projects
×
463
    if unconstrained_projects:
×
464
        logger.warning(
×
465
            softwrap(
466
                f"""
467
                The constraints file {constraints_path} does not contain
468
                entries for the following requirements: {", ".join(unconstrained_projects)}.
469

470
                Ignoring `[python].resolve_all_constraints` option.
471
                """
472
            )
473
        )
474
        return OptionalPexRequest(None)
×
475

476
    interpreter_constraints = await interpreter_constraints_for_targets(
×
477
        request.to_interpreter_constraints_request(), **implicitly()
478
    )
479

480
    # To get a full set of requirements we must add the URL requirements to the
481
    # constraints file, since the latter cannot contain URL requirements.
482
    # NB: We can only add the URL requirements we know about here, i.e., those that
483
    #  are transitive deps of the targets in play. There may be others in the repo.
484
    #  So we may end up creating a few different repository pexes, each with identical
485
    #  name requirements but different subsets of URL requirements. Fortunately since
486
    #  all these repository pexes will have identical pinned versions of everything,
487
    #  this is not a correctness issue, only a performance one.
488
    all_constraints = {str(req) for req in (constraints_file_reqs | url_reqs)}
×
489
    repository_pex = PexRequest(
×
490
        description=f"Resolving {constraints_path}",
491
        output_filename="repository.pex",
492
        internal_only=request.internal_only,
493
        requirements=PexRequirements(
494
            all_constraints,
495
            constraints_strings=(str(constraint) for constraint in global_requirement_constraints),
496
            description_of_origin=constraints_path,
497
        ),
498
        # Monolithic PEXes like the repository PEX should always use the Packed layout.
499
        layout=PexLayout.PACKED,
500
        interpreter_constraints=interpreter_constraints,
501
        platforms=request.platforms,
502
        complete_platforms=request.complete_platforms,
503
        additional_args=request.additional_lockfile_args,
504
    )
505
    return OptionalPexRequest(repository_pex)
×
506

507

UNCOV
508
@rule
×
UNCOV
509
async def get_repository_pex(
×
510
    request: _RepositoryPexRequest, python_setup: PythonSetup
511
) -> OptionalPexRequest:
512
    # NB: It isn't safe to resolve against an entire lockfile or constraints file if
513
    # platforms are in use. See https://github.com/pantsbuild/pants/issues/12222.
514
    if request.platforms or request.complete_platforms:
×
515
        return OptionalPexRequest(None)
×
516

517
    if python_setup.requirement_constraints:
×
518
        constraints_repository_pex_request = await _setup_constraints_repository_pex(
×
519
            _ConstraintsRepositoryPexRequest(request), **implicitly()
520
        )
521
        # TODO: ... Not really sure why we've just re-wrapped an object with the same object ... Leaving alone in case it causes some crazy obscure bug
522
        return OptionalPexRequest(constraints_repository_pex_request.maybe_pex_request)
×
523

524
    if not python_setup.enable_resolves:
×
525
        return OptionalPexRequest(None)
×
526

527
    chosen_resolve, interpreter_constraints = await concurrently(
×
528
        choose_python_resolve(ChosenPythonResolveRequest(request.addresses), **implicitly()),
529
        interpreter_constraints_for_targets(
530
            request.to_interpreter_constraints_request(), **implicitly()
531
        ),
532
    )
533
    return OptionalPexRequest(
×
534
        PexRequest(
535
            description=softwrap(
536
                f"""
537
                Installing {chosen_resolve.lockfile.url} for the resolve
538
                `{chosen_resolve.name}`
539
                """
540
            ),
541
            output_filename=f"{path_safe(chosen_resolve.name)}_lockfile.pex",
542
            internal_only=request.internal_only,
543
            requirements=EntireLockfile(chosen_resolve.lockfile),
544
            interpreter_constraints=interpreter_constraints,
545
            layout=PexLayout.PACKED,
546
            platforms=request.platforms,
547
            complete_platforms=request.complete_platforms,
548
            additional_args=request.additional_lockfile_args,
549
        )
550
    )
551

552

UNCOV
553
async def _determine_requirements_for_pex_from_targets(
×
554
    request: PexFromTargetsRequest, python_setup: PythonSetup
555
) -> tuple[PexRequirements | EntireLockfile, Iterable[Pex]]:
UNCOV
556
    if not request.include_requirements:
×
UNCOV
557
        return PexRequirements(), ()
×
558

UNCOV
559
    requirements = await determine_requirement_strings_in_closure(
×
560
        _PexRequirementsRequest(request.addresses), **implicitly()
561
    )
UNCOV
562
    pex_native_subsetting_supported = False
×
UNCOV
563
    if python_setup.enable_resolves:
×
564
        # TODO: Once `requirement_constraints` is removed in favor of `enable_resolves`,
565
        # `ChosenPythonResolveRequest` and `_PexRequirementsRequest` should merge and
566
        # do a single transitive walk to replace this method.
UNCOV
567
        chosen_resolve = await choose_python_resolve(
×
568
            ChosenPythonResolveRequest(request.addresses), **implicitly()
569
        )
UNCOV
570
        loaded_lockfile = await load_lockfile(
×
571
            LoadedLockfileRequest(chosen_resolve.lockfile), **implicitly()
572
        )
UNCOV
573
        pex_native_subsetting_supported = loaded_lockfile.is_pex_native
×
UNCOV
574
        if loaded_lockfile.as_constraints_strings:
×
UNCOV
575
            requirements = dataclasses.replace(
×
576
                requirements,
577
                constraints_strings=loaded_lockfile.as_constraints_strings,
578
            )
579

UNCOV
580
    should_return_entire_lockfile = (
×
581
        python_setup.run_against_entire_lockfile and request.internal_only
582
    )
UNCOV
583
    should_request_repository_pex = (
×
584
        # The entire lockfile was explicitly requested.
585
        should_return_entire_lockfile
586
        # The legacy `resolve_all_constraints`
587
        or (python_setup.resolve_all_constraints and python_setup.requirement_constraints)
588
        # A non-PEX-native lockfile was used, and so we cannot directly subset it from a
589
        # LoadedLockfile.
590
        or not pex_native_subsetting_supported
591
    )
592

UNCOV
593
    if not should_request_repository_pex:
×
UNCOV
594
        if not pex_native_subsetting_supported:
×
595
            return requirements, ()
×
596

UNCOV
597
        chosen_resolve = await choose_python_resolve(
×
598
            ChosenPythonResolveRequest(request.addresses), **implicitly()
599
        )
UNCOV
600
        return (
×
601
            dataclasses.replace(
602
                requirements, from_superset=Resolve(chosen_resolve.name, use_entire_lockfile=False)
603
            ),
604
            (),
605
        )
606

607
    # Else, request the repository PEX and possibly subset it.
UNCOV
608
    repository_pex_request = await get_repository_pex(
×
609
        _RepositoryPexRequest(
610
            request.addresses,
611
            hardcoded_interpreter_constraints=request.hardcoded_interpreter_constraints,
612
            platforms=request.platforms,
613
            complete_platforms=request.complete_platforms,
614
            internal_only=request.internal_only,
615
            additional_lockfile_args=request.additional_lockfile_args,
616
        ),
617
        **implicitly(),
618
    )
UNCOV
619
    if should_return_entire_lockfile:
×
UNCOV
620
        if repository_pex_request.maybe_pex_request is None:
×
621
            raise ValueError(
×
622
                softwrap(
623
                    f"""
624
                    [python].run_against_entire_lockfile was set, but could not find a
625
                    lockfile or constraints file for this target set. See
626
                    {doc_url("docs/python/overview/third-party-dependencies")} for details.
627
                    """
628
                )
629
            )
630

UNCOV
631
    repository_pex = await create_optional_pex(repository_pex_request)
×
UNCOV
632
    if should_return_entire_lockfile:
×
UNCOV
633
        assert repository_pex_request.maybe_pex_request is not None
×
UNCOV
634
        assert repository_pex.maybe_pex is not None
×
UNCOV
635
        return repository_pex_request.maybe_pex_request.requirements, [repository_pex.maybe_pex]
×
636

UNCOV
637
    return dataclasses.replace(requirements, from_superset=repository_pex.maybe_pex), ()
×
638

639

UNCOV
640
async def _warn_about_any_files_targets(
×
641
    addresses: Addresses, transitive_targets: TransitiveTargets, union_membership: UnionMembership
642
) -> None:
643
    # Warn if users depend on `files` targets, which won't be included in the PEX and is a common
644
    # gotcha.
645
    file_tgts = targets_with_sources_types(
×
646
        [FileSourceField], transitive_targets.dependencies, union_membership
647
    )
648
    if file_tgts:
×
649
        # make it easier for the user to find which targets are problematic by including the alias
650
        targets = await resolve_targets(**implicitly(addresses))
×
651
        # targets = await resolve_targets(**implicitly(addresses))
652
        formatted_addresses = ", ".join(
×
653
            f"{a} (`{tgt.alias}`)" for a, tgt in zip(addresses, targets)
654
        )
655

656
        files_addresses = sorted(tgt.address.spec for tgt in file_tgts)
×
657
        targets_text, depend_text = (
×
658
            ("target", "depends") if len(addresses) == 1 else ("targets", "depend")
659
        )
660
        logger.warning(
×
661
            f"The {targets_text} {formatted_addresses} transitively {depend_text} "
662
            "on the below `files` targets, but Pants will not include them in the built package. "
663
            "Filesystem APIs like `open()` may be not able to load files within the binary "
664
            "itself; instead, they read from the current working directory."
665
            f"\n\nInstead, use `resources` targets. See {doc_url('resources')}."
666
            f"\n\nFiles targets dependencies: {files_addresses}"
667
        )
668

669

UNCOV
670
@rule(level=LogLevel.DEBUG)
×
UNCOV
671
async def create_pex_from_targets(
×
672
    request: PexFromTargetsRequest,
673
    python_setup: PythonSetup,
674
    union_membership: UnionMembership,
675
) -> PexRequest:
676
    requirements, additional_pexes = await _determine_requirements_for_pex_from_targets(
×
677
        request, python_setup
678
    )
679

680
    interpreter_constraints = await interpreter_constraints_for_targets(
×
681
        request.to_interpreter_constraints_request(), **implicitly()
682
    )
683

684
    sources_digests = []
×
685
    if request.additional_sources:
×
686
        sources_digests.append(request.additional_sources)
×
687
    if request.include_source_files:
×
688
        transitive_targets = await transitive_targets_get(
×
689
            TransitiveTargetsRequest(
690
                request.addresses,
691
                should_traverse_deps_predicate=TraverseIfNotPackageTarget(
692
                    roots=request.addresses,
693
                    union_membership=union_membership,
694
                ),
695
            ),
696
            **implicitly(),
697
        )
698
        sources = await prepare_python_sources(
×
699
            PythonSourceFilesRequest(transitive_targets.closure), **implicitly()
700
        )
701

702
        if request.warn_for_transitive_files_targets:
×
703
            await _warn_about_any_files_targets(
×
704
                request.addresses, transitive_targets, union_membership
705
            )
706
    elif isinstance(request.main, Executable):
×
707
        # The source for an --executable main must be embedded in the pex even if request.include_source_files is False.
708
        # If include_source_files is True, the executable source should be included in the (transitive) dependencies.
709
        owners = await find_owners(
×
710
            OwnersRequest(
711
                (request.main.spec,), owners_not_found_behavior=GlobMatchErrorBehavior.error
712
            ),
713
            **implicitly(),
714
        )
715
        owning_targets = await resolve_targets(**implicitly(Addresses(owners)))
×
716
        sources = await prepare_python_sources(
×
717
            PythonSourceFilesRequest(owning_targets), **implicitly()
718
        )
719
    else:
720
        sources = PythonSourceFiles.empty()
×
721

722
    additional_inputs_digests = []
×
723
    if request.additional_inputs:
×
724
        additional_inputs_digests.append(request.additional_inputs)
×
725
    additional_args = request.additional_args
×
726
    if request.include_local_dists:
×
727
        local_dists = await build_local_dists(
×
728
            LocalDistsPexRequest(
729
                request.addresses,
730
                interpreter_constraints=interpreter_constraints,
731
                sources=sources,
732
            )
733
        )
734
        remaining_sources = local_dists.remaining_sources
×
735
        additional_inputs_digests.append(local_dists.pex.digest)
×
736
        additional_args += ("--requirements-pex", local_dists.pex.name)
×
737
    else:
738
        remaining_sources = sources
×
739

740
    remaining_sources_stripped = await strip_python_sources(remaining_sources)
×
741
    sources_digests.append(remaining_sources_stripped.stripped_source_files.snapshot.digest)
×
742

743
    merged_sources_digest, additional_inputs = await concurrently(
×
744
        merge_digests(MergeDigests(sources_digests)),
745
        merge_digests(MergeDigests(additional_inputs_digests)),
746
    )
747

748
    description = request.description
×
749

750
    return PexRequest(
×
751
        output_filename=request.output_filename,
752
        internal_only=request.internal_only,
753
        layout=request.layout,
754
        requirements=requirements,
755
        interpreter_constraints=interpreter_constraints,
756
        platforms=request.platforms,
757
        complete_platforms=request.complete_platforms,
758
        main=request.main,
759
        inject_args=request.inject_args,
760
        inject_env=request.inject_env,
761
        sources=merged_sources_digest,
762
        additional_inputs=additional_inputs,
763
        additional_args=additional_args,
764
        description=description,
765
        pex_path=additional_pexes,
766
    )
767

768

UNCOV
769
@dataclass(frozen=True)
×
UNCOV
770
class RequirementsPexRequest:
×
771
    """Requests a PEX containing only thirdparty requirements for internal/non-portable use.
772

773
    Used as part of an optimization to reduce the "overhead" (in terms of both time and space) of
774
    thirdparty requirements by taking advantage of certain PEX features.
775
    """
776

UNCOV
777
    addresses: tuple[Address, ...]
×
UNCOV
778
    hardcoded_interpreter_constraints: InterpreterConstraints | None
×
779

UNCOV
780
    def __init__(
×
781
        self,
782
        addresses: Iterable[Address],
783
        *,
784
        hardcoded_interpreter_constraints: InterpreterConstraints | None = None,
785
    ) -> None:
786
        object.__setattr__(self, "addresses", Addresses(addresses))
×
787
        object.__setattr__(
×
788
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
789
        )
790

791

UNCOV
792
@rule
×
UNCOV
793
async def generalize_requirements_pex_request(
×
794
    request: RequirementsPexRequest,
795
) -> PexFromTargetsRequest:
796
    return PexFromTargetsRequest(
×
797
        addresses=sorted(request.addresses),
798
        output_filename="requirements.pex",
799
        internal_only=True,
800
        include_source_files=False,
801
        hardcoded_interpreter_constraints=request.hardcoded_interpreter_constraints,
802
    )
803

804

UNCOV
805
def rules():
×
UNCOV
806
    return (*collect_rules(), *pex_rules(), *local_dists_rules(), *python_sources_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