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

pantsbuild / pants / 25405422172

05 May 2026 10:18PM UTC coverage: 92.879% (-0.07%) from 92.944%
25405422172

Pull #23319

github

web-flow
Merge c82d0f333 into e8b784f89
Pull Request #23319: [pants_ng] Scaffolding for a pants_ng mode.

25 of 76 new or added lines in 9 files covered. (32.89%)

209 existing lines in 15 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

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

95.36
/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

4
from __future__ import annotations
12✔
5

6
import dataclasses
12✔
7
import logging
12✔
8
from collections.abc import Iterable, Mapping
12✔
9
from dataclasses import dataclass
12✔
10

11
from packaging.utils import canonicalize_name as canonicalize_project_name
12✔
12

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

73
logger = logging.getLogger(__name__)
12✔
74

75

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

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

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

186
        self.__post_init__()
11✔
187

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

200
    def to_interpreter_constraints_request(self) -> InterpreterConstraintsRequest:
12✔
201
        return InterpreterConstraintsRequest(
11✔
202
            addresses=self.addresses,
203
            hardcoded_interpreter_constraints=self.hardcoded_interpreter_constraints,
204
        )
205

206

207
@dataclass(frozen=True)
12✔
208
class InterpreterConstraintsRequest:
12✔
209
    addresses: Addresses
12✔
210
    hardcoded_interpreter_constraints: InterpreterConstraints | None
12✔
211

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

223

224
@rule
12✔
225
async def interpreter_constraints_for_targets(
12✔
226
    request: InterpreterConstraintsRequest, python_setup: PythonSetup
227
) -> InterpreterConstraints:
228
    if request.hardcoded_interpreter_constraints:
11✔
229
        return request.hardcoded_interpreter_constraints
6✔
230

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

245

246
@dataclass(frozen=True)
12✔
247
class ChosenPythonResolve:
12✔
248
    name: str
12✔
249
    lockfile: Lockfile
12✔
250

251

252
@dataclass(frozen=True)
12✔
253
class ChosenPythonResolveRequest:
12✔
254
    addresses: Addresses
12✔
255

256

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

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

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

285
        chosen_resolve = next(iter(root_resolves))
1✔
286

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

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

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

317

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

321

322
@rule
12✔
323
async def determine_global_requirement_constraints(
12✔
324
    python_setup: PythonSetup,
325
) -> GlobalRequirementConstraints:
326
    if not python_setup.requirement_constraints:
11✔
327
        return GlobalRequirementConstraints()
11✔
328

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

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

346

347
@dataclass(frozen=True)
12✔
348
class _PexRequirementsRequest:
12✔
349
    """Determine the requirement strings used transitively.
350

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

355
    addresses: Addresses
12✔
356

357

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

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

378

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

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

407
    def to_interpreter_constraints_request(self) -> InterpreterConstraintsRequest:
12✔
408
        return InterpreterConstraintsRequest(
1✔
409
            addresses=self.addresses,
410
            hardcoded_interpreter_constraints=self.hardcoded_interpreter_constraints,
411
        )
412

413

414
@dataclass(frozen=True)
12✔
415
class _ConstraintsRepositoryPexRequest:
12✔
416
    repository_pex_request: _RepositoryPexRequest
12✔
417

418

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

429
    constraints_path = python_setup.requirement_constraints
1✔
430
    assert constraints_path is not None
1✔
431

432
    transitive_targets = await transitive_targets_get(
1✔
433
        TransitiveTargetsRequest(request.addresses), **implicitly()
434
    )
435

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

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

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

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

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

477
    interpreter_constraints = await interpreter_constraints_for_targets(
1✔
478
        request.to_interpreter_constraints_request(), **implicitly()
479
    )
480

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

508

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

518
    if python_setup.requirement_constraints:
11✔
519
        constraints_repository_pex_request = await _setup_constraints_repository_pex(
1✔
520
            _ConstraintsRepositoryPexRequest(request), **implicitly()
521
        )
522
        # 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
523
        return OptionalPexRequest(constraints_repository_pex_request.maybe_pex_request)
1✔
524

525
    if not python_setup.enable_resolves:
11✔
526
        return OptionalPexRequest(None)
11✔
527

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

553

554
async def _determine_requirements_for_pex_from_targets(
12✔
555
    request: PexFromTargetsRequest, python_setup: PythonSetup
556
) -> tuple[PexRequirements | EntireLockfile, Iterable[Pex]]:
557
    if not request.include_requirements:
11✔
558
        return PexRequirements(), ()
2✔
559

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

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

597
    if not should_request_repository_pex:
11✔
598
        if not pex_native_subsetting_supported:
2✔
UNCOV
599
            return requirements, ()
×
600

601
        chosen_resolve = await choose_python_resolve(
2✔
602
            ChosenPythonResolveRequest(request.addresses), **implicitly()
603
        )
604
        return (
2✔
605
            dataclasses.replace(
606
                requirements, from_superset=Resolve(chosen_resolve.name, use_entire_lockfile=False)
607
            ),
608
            (),
609
        )
610

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

635
    repository_pex = await create_optional_pex(repository_pex_request)
11✔
636
    if should_return_entire_lockfile:
11✔
637
        assert repository_pex_request.maybe_pex_request is not None
1✔
638
        assert repository_pex.maybe_pex is not None
1✔
639
        return repository_pex_request.maybe_pex_request.requirements, [repository_pex.maybe_pex]
1✔
640

641
    return dataclasses.replace(requirements, from_superset=repository_pex.maybe_pex), ()
11✔
642

643

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

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

673

674
@rule(level=LogLevel.DEBUG)
12✔
675
async def create_pex_from_targets(
12✔
676
    request: PexFromTargetsRequest,
677
    python_setup: PythonSetup,
678
    union_membership: UnionMembership,
679
) -> PexRequest:
680
    requirements, additional_pexes = await _determine_requirements_for_pex_from_targets(
11✔
681
        request, python_setup
682
    )
683

684
    interpreter_constraints = await interpreter_constraints_for_targets(
11✔
685
        request.to_interpreter_constraints_request(), **implicitly()
686
    )
687

688
    sources_digests = []
11✔
689
    if request.additional_sources:
11✔
690
        sources_digests.append(request.additional_sources)
2✔
691
    if request.include_source_files:
11✔
692
        transitive_targets = await transitive_targets_get(
7✔
693
            TransitiveTargetsRequest(
694
                request.addresses,
695
                should_traverse_deps_predicate=TraverseIfNotPackageTarget(
696
                    roots=request.addresses,
697
                    union_membership=union_membership,
698
                ),
699
            ),
700
            **implicitly(),
701
        )
702
        sources = await prepare_python_sources(
7✔
703
            PythonSourceFilesRequest(transitive_targets.closure), **implicitly()
704
        )
705

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

726
    additional_inputs_digests = []
11✔
727
    if request.additional_inputs:
11✔
UNCOV
728
        additional_inputs_digests.append(request.additional_inputs)
×
729
    additional_args = request.additional_args
11✔
730
    if request.include_local_dists:
11✔
731
        local_dists = await build_local_dists(
11✔
732
            LocalDistsPexRequest(
733
                request.addresses,
734
                interpreter_constraints=interpreter_constraints,
735
                sources=sources,
736
            )
737
        )
738
        remaining_sources = local_dists.remaining_sources
11✔
739
        additional_inputs_digests.append(local_dists.pex.digest)
11✔
740
        additional_args += ("--requirements-pex", local_dists.pex.name)
11✔
741
    else:
742
        remaining_sources = sources
9✔
743

744
    remaining_sources_stripped = await strip_python_sources(remaining_sources)
11✔
745
    sources_digests.append(remaining_sources_stripped.stripped_source_files.snapshot.digest)
11✔
746

747
    merged_sources_digest, additional_inputs = await concurrently(
11✔
748
        merge_digests(MergeDigests(sources_digests)),
749
        merge_digests(MergeDigests(additional_inputs_digests)),
750
    )
751

752
    description = request.description
11✔
753

754
    return PexRequest(
11✔
755
        output_filename=request.output_filename,
756
        internal_only=request.internal_only,
757
        layout=request.layout,
758
        requirements=requirements,
759
        interpreter_constraints=interpreter_constraints,
760
        platforms=request.platforms,
761
        complete_platforms=request.complete_platforms,
762
        main=request.main,
763
        inject_args=request.inject_args,
764
        inject_env=request.inject_env,
765
        sources=merged_sources_digest,
766
        additional_inputs=additional_inputs,
767
        additional_args=additional_args,
768
        description=description,
769
        pex_path=additional_pexes,
770
    )
771

772

773
@dataclass(frozen=True)
12✔
774
class RequirementsPexRequest:
12✔
775
    """Requests a PEX containing only thirdparty requirements for internal/non-portable use.
776

777
    Used as part of an optimization to reduce the "overhead" (in terms of both time and space) of
778
    thirdparty requirements by taking advantage of certain PEX features.
779
    """
780

781
    addresses: tuple[Address, ...]
12✔
782
    hardcoded_interpreter_constraints: InterpreterConstraints | None
12✔
783

784
    def __init__(
12✔
785
        self,
786
        addresses: Iterable[Address],
787
        *,
788
        hardcoded_interpreter_constraints: InterpreterConstraints | None = None,
789
    ) -> None:
790
        object.__setattr__(self, "addresses", Addresses(addresses))
7✔
791
        object.__setattr__(
7✔
792
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
793
        )
794

795

796
@rule
12✔
797
async def generalize_requirements_pex_request(
12✔
798
    request: RequirementsPexRequest,
799
) -> PexFromTargetsRequest:
800
    return PexFromTargetsRequest(
7✔
801
        addresses=sorted(request.addresses),
802
        output_filename="requirements.pex",
803
        internal_only=True,
804
        include_source_files=False,
805
        hardcoded_interpreter_constraints=request.hardcoded_interpreter_constraints,
806
    )
807

808

809
def rules():
12✔
810
    return (*collect_rules(), *pex_rules(), *local_dists_rules(), *python_sources_rules())
11✔
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