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

pantsbuild / pants / 18812500213

26 Oct 2025 03:42AM UTC coverage: 80.284% (+0.005%) from 80.279%
18812500213

Pull #22804

github

web-flow
Merge 2a56fdb46 into 4834308dc
Pull Request #22804: test_shell_command: use correct default cache scope for a test's environment

29 of 31 new or added lines in 2 files covered. (93.55%)

1314 existing lines in 64 files now uncovered.

77900 of 97030 relevant lines covered (80.28%)

3.35 hits per line

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

60.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

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.pex import (
12✔
25
    CompletePlatforms,
26
    OptionalPexRequest,
27
    Pex,
28
    PexPlatforms,
29
    PexRequest,
30
    create_optional_pex,
31
)
32
from pants.backend.python.util_rules.pex import rules as pex_rules
12✔
33
from pants.backend.python.util_rules.pex_requirements import (
12✔
34
    EntireLockfile,
35
    LoadedLockfileRequest,
36
    Lockfile,
37
    PexRequirements,
38
    Resolve,
39
    load_lockfile,
40
)
41
from pants.backend.python.util_rules.python_sources import (
12✔
42
    PythonSourceFiles,
43
    PythonSourceFilesRequest,
44
    prepare_python_sources,
45
)
46
from pants.backend.python.util_rules.python_sources import rules as python_sources_rules
12✔
47
from pants.backend.python.util_rules.python_sources import strip_python_sources
12✔
48
from pants.core.goals.generate_lockfiles import NoCompatibleResolveException
12✔
49
from pants.core.goals.package import TraverseIfNotPackageTarget
12✔
50
from pants.core.target_types import FileSourceField
12✔
51
from pants.engine.addresses import Address, Addresses
12✔
52
from pants.engine.collection import DeduplicatedCollection
12✔
53
from pants.engine.fs import Digest, GlobMatchErrorBehavior, MergeDigests, PathGlobs
12✔
54
from pants.engine.internals.graph import OwnersRequest, find_owners, resolve_targets
12✔
55
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
12✔
56
from pants.engine.intrinsics import get_digest_contents, merge_digests
12✔
57
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
12✔
58
from pants.engine.target import (
12✔
59
    Target,
60
    TransitiveTargets,
61
    TransitiveTargetsRequest,
62
    targets_with_sources_types,
63
)
64
from pants.engine.unions import UnionMembership
12✔
65
from pants.util.docutil import doc_url
12✔
66
from pants.util.frozendict import FrozenDict
12✔
67
from pants.util.logging import LogLevel
12✔
68
from pants.util.pip_requirement import PipRequirement
12✔
69
from pants.util.requirements import parse_requirements_file
12✔
70
from pants.util.strutil import path_safe, softwrap
12✔
71

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

74

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

99
    def __init__(
12✔
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
        """
161
        object.__setattr__(self, "addresses", Addresses(addresses))
2✔
162
        object.__setattr__(self, "output_filename", output_filename)
2✔
163
        object.__setattr__(self, "internal_only", internal_only)
2✔
164
        object.__setattr__(self, "layout", layout)
2✔
165
        object.__setattr__(self, "main", main)
2✔
166
        object.__setattr__(self, "inject_args", tuple(inject_args))
2✔
167
        object.__setattr__(self, "inject_env", FrozenDict(inject_env))
2✔
168
        object.__setattr__(self, "platforms", platforms)
2✔
169
        object.__setattr__(self, "complete_platforms", complete_platforms)
2✔
170
        object.__setattr__(self, "additional_args", tuple(additional_args))
2✔
171
        object.__setattr__(self, "additional_lockfile_args", tuple(additional_lockfile_args))
2✔
172
        object.__setattr__(self, "include_source_files", include_source_files)
2✔
173
        object.__setattr__(self, "include_requirements", include_requirements)
2✔
174
        object.__setattr__(self, "include_local_dists", include_local_dists)
2✔
175
        object.__setattr__(self, "additional_sources", additional_sources)
2✔
176
        object.__setattr__(self, "additional_inputs", additional_inputs)
2✔
177
        object.__setattr__(
2✔
178
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
179
        )
180
        object.__setattr__(self, "description", description)
2✔
181
        object.__setattr__(
2✔
182
            self, "warn_for_transitive_files_targets", warn_for_transitive_files_targets
183
        )
184

185
        self.__post_init__()
2✔
186

187
    def __post_init__(self):
12✔
188
        if self.internal_only and (self.platforms or self.complete_platforms):
2✔
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

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

205

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

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

222

223
@rule
12✔
224
async def interpreter_constraints_for_targets(
12✔
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

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

250

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

255

256
@rule
12✔
257
async def choose_python_resolve(
12✔
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

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

320

321
@rule
12✔
322
async def determine_global_requirement_constraints(
12✔
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

346
@dataclass(frozen=True)
12✔
347
class _PexRequirementsRequest:
12✔
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

354
    addresses: Addresses
12✔
355

356

357
@rule
12✔
358
async def determine_requirement_strings_in_closure(
12✔
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

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

387
    def __init__(
12✔
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))
1✔
UNCOV
398
        object.__setattr__(self, "internal_only", internal_only)
1✔
UNCOV
399
        object.__setattr__(
1✔
400
            self, "hardcoded_interpreter_constraints", hardcoded_interpreter_constraints
401
        )
UNCOV
402
        object.__setattr__(self, "platforms", platforms)
1✔
UNCOV
403
        object.__setattr__(self, "complete_platforms", complete_platforms)
1✔
UNCOV
404
        object.__setattr__(self, "additional_lockfile_args", additional_lockfile_args)
1✔
405

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

412

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

417

418
@rule
12✔
419
async def _setup_constraints_repository_pex(
12✔
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

508
@rule
12✔
509
async def get_repository_pex(
12✔
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

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

UNCOV
559
    requirements = await determine_requirement_strings_in_closure(
1✔
560
        _PexRequirementsRequest(request.addresses), **implicitly()
561
    )
UNCOV
562
    pex_native_subsetting_supported = False
1✔
UNCOV
563
    if python_setup.enable_resolves:
1✔
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(
1✔
568
            ChosenPythonResolveRequest(request.addresses), **implicitly()
569
        )
UNCOV
570
        loaded_lockfile = await load_lockfile(
1✔
571
            LoadedLockfileRequest(chosen_resolve.lockfile), **implicitly()
572
        )
UNCOV
573
        pex_native_subsetting_supported = loaded_lockfile.is_pex_native
1✔
UNCOV
574
        if loaded_lockfile.as_constraints_strings:
1✔
UNCOV
575
            requirements = dataclasses.replace(
1✔
576
                requirements,
577
                constraints_strings=loaded_lockfile.as_constraints_strings,
578
            )
579

UNCOV
580
    should_return_entire_lockfile = (
1✔
581
        python_setup.run_against_entire_lockfile and request.internal_only
582
    )
UNCOV
583
    should_request_repository_pex = (
1✔
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:
1✔
UNCOV
594
        if not pex_native_subsetting_supported:
1✔
595
            return requirements, ()
×
596

UNCOV
597
        chosen_resolve = await choose_python_resolve(
1✔
598
            ChosenPythonResolveRequest(request.addresses), **implicitly()
599
        )
UNCOV
600
        return (
1✔
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(
1✔
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:
1✔
UNCOV
620
        if repository_pex_request.maybe_pex_request is None:
1✔
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)
1✔
UNCOV
632
    if should_return_entire_lockfile:
1✔
UNCOV
633
        assert repository_pex_request.maybe_pex_request is not None
1✔
UNCOV
634
        assert repository_pex.maybe_pex is not None
1✔
UNCOV
635
        return repository_pex_request.maybe_pex_request.requirements, [repository_pex.maybe_pex]
1✔
636

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

639

640
async def _warn_about_any_files_targets(
12✔
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

670
@rule(level=LogLevel.DEBUG)
12✔
671
async def create_pex_from_targets(
12✔
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

769
@dataclass(frozen=True)
12✔
770
class RequirementsPexRequest:
12✔
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

777
    addresses: tuple[Address, ...]
12✔
778
    hardcoded_interpreter_constraints: InterpreterConstraints | None
12✔
779

780
    def __init__(
12✔
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

792
@rule
12✔
793
async def generalize_requirements_pex_request(
12✔
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

805
def rules():
12✔
806
    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

© 2025 Coveralls, Inc