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

pantsbuild / pants / 24145062409

08 Apr 2026 03:56PM UTC coverage: 52.369% (-40.5%) from 92.91%
24145062409

Pull #23233

github

web-flow
Merge 7a7652ebe into 9036734c9
Pull Request #23233: Introduce a LockfileFormat enum.

7 of 10 new or added lines in 3 files covered. (70.0%)

23048 existing lines in 605 files now uncovered.

31656 of 60448 relevant lines covered (52.37%)

0.52 hits per line

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

70.61
/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
1✔
5

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

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

13
from pants.backend.python.subsystems.setup import PythonSetup
1✔
14
from pants.backend.python.target_types import (
1✔
15
    Executable,
16
    MainSpecification,
17
    PexLayout,
18
    PythonRequirementsField,
19
    PythonResolveField,
20
)
21
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
1✔
22
from pants.backend.python.util_rules.local_dists import LocalDistsPexRequest, build_local_dists
1✔
23
from pants.backend.python.util_rules.local_dists import rules as local_dists_rules
1✔
24
from pants.backend.python.util_rules.pex import (
1✔
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
1✔
33
from pants.backend.python.util_rules.pex_requirements import (
1✔
34
    EntireLockfile,
35
    LoadedLockfileRequest,
36
    Lockfile,
37
    LockfileFormat,
38
    PexRequirements,
39
    Resolve,
40
    load_lockfile,
41
)
42
from pants.backend.python.util_rules.python_sources import (
1✔
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
1✔
49
from pants.core.goals.generate_lockfiles import NoCompatibleResolveException
1✔
50
from pants.core.goals.package import TraverseIfNotPackageTarget
1✔
51
from pants.core.target_types import FileSourceField
1✔
52
from pants.engine.addresses import Address, Addresses
1✔
53
from pants.engine.collection import DeduplicatedCollection
1✔
54
from pants.engine.fs import Digest, GlobMatchErrorBehavior, MergeDigests, PathGlobs
1✔
55
from pants.engine.internals.graph import OwnersRequest, find_owners, resolve_targets
1✔
56
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
1✔
57
from pants.engine.intrinsics import get_digest_contents, merge_digests
1✔
58
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
1✔
59
from pants.engine.target import (
1✔
60
    Target,
61
    TransitiveTargets,
62
    TransitiveTargetsRequest,
63
    targets_with_sources_types,
64
)
65
from pants.engine.unions import UnionMembership
1✔
66
from pants.util.docutil import doc_url
1✔
67
from pants.util.frozendict import FrozenDict
1✔
68
from pants.util.logging import LogLevel
1✔
69
from pants.util.pip_requirement import PipRequirement
1✔
70
from pants.util.requirements import parse_requirements_file
1✔
71
from pants.util.strutil import path_safe, softwrap
1✔
72

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

75

76
@dataclass(frozen=True)
1✔
77
class PexFromTargetsRequest:
1✔
78
    addresses: Addresses
1✔
79
    output_filename: str
1✔
80
    internal_only: bool
1✔
81
    layout: PexLayout | None
1✔
82
    main: MainSpecification | None
1✔
83
    inject_args: tuple[str, ...]
1✔
84
    inject_env: FrozenDict[str, str]
1✔
85
    platforms: PexPlatforms
1✔
86
    complete_platforms: CompletePlatforms
1✔
87
    additional_args: tuple[str, ...]
1✔
88
    additional_lockfile_args: tuple[str, ...]
1✔
89
    include_source_files: bool
1✔
90
    include_requirements: bool
1✔
91
    include_local_dists: bool
1✔
92
    additional_sources: Digest | None
1✔
93
    additional_inputs: Digest | None
1✔
94
    hardcoded_interpreter_constraints: InterpreterConstraints | None
1✔
95
    warn_for_transitive_files_targets: bool
1✔
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)
1✔
99

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

186
        self.__post_init__()
1✔
187

188
    def __post_init__(self):
1✔
189
        if self.internal_only and (self.platforms or self.complete_platforms):
1✔
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:
1✔
201
        return InterpreterConstraintsRequest(
1✔
202
            addresses=self.addresses,
203
            hardcoded_interpreter_constraints=self.hardcoded_interpreter_constraints,
204
        )
205

206

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

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

223

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

231
    transitive_targets = await transitive_targets_get(
1✔
232
        TransitiveTargetsRequest(request.addresses), **implicitly()
233
    )
234
    calculated_constraints = InterpreterConstraints.create_from_targets(
1✔
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(
1✔
241
        python_setup.interpreter_constraints
242
    )
243
    return interpreter_constraints
1✔
244

245

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

251

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

256

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

UNCOV
265
    def maybe_get_resolve(t: Target) -> str | None:
×
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.
UNCOV
271
    root_resolves = {
×
272
        root[PythonResolveField].normalized_value(python_setup)
273
        for root in transitive_targets.roots
274
        if root.has_field(PythonResolveField)
275
    }
UNCOV
276
    if root_resolves:
×
UNCOV
277
        if len(root_resolves) > 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

UNCOV
285
        chosen_resolve = next(iter(root_resolves))
×
286

287
        # Then, validate that all transitive deps are compatible.
UNCOV
288
        for tgt in transitive_targets.dependencies:
×
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.
UNCOV
304
        chosen_resolve = python_setup.default_resolve
×
305

UNCOV
306
    return ChosenPythonResolve(
×
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]):
1✔
319
    """Global constraints specified by the `[python].requirement_constraints` setting, if any."""
320

321

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

UNCOV
329
    constraints_file_contents = await get_digest_contents(
×
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

UNCOV
339
    return GlobalRequirementConstraints(
×
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)
1✔
348
class _PexRequirementsRequest:
1✔
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
1✔
356

357

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

370
    return PexRequirements(
1✔
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)
1✔
380
class _RepositoryPexRequest:
1✔
381
    addresses: Addresses
1✔
382
    hardcoded_interpreter_constraints: InterpreterConstraints | None
1✔
383
    platforms: PexPlatforms
1✔
384
    complete_platforms: CompletePlatforms
1✔
385
    internal_only: bool
1✔
386
    additional_lockfile_args: tuple[str, ...]
1✔
387

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

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

413

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

418

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

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

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

UNCOV
436
    req_strings = PexRequirements.req_strings_from_requirement_fields(
×
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
UNCOV
445
    url_reqs = set()  # E.g., 'foobar@ git+https://github.com/foo/bar.git@branch'
×
UNCOV
446
    name_reqs = set()  # E.g., foobar>=1.2.3
×
UNCOV
447
    name_req_projects = set()
×
UNCOV
448
    constraints_file_reqs = set(global_requirement_constraints)
×
449

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

UNCOV
458
    constraint_file_projects = {
×
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.
UNCOV
463
    unconstrained_projects = name_req_projects - constraint_file_projects
×
UNCOV
464
    if unconstrained_projects:
×
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

UNCOV
477
    interpreter_constraints = await interpreter_constraints_for_targets(
×
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.
UNCOV
489
    all_constraints = {str(req) for req in (constraints_file_reqs | url_reqs)}
×
UNCOV
490
    repository_pex = PexRequest(
×
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
    )
UNCOV
506
    return OptionalPexRequest(repository_pex)
×
507

508

509
@rule
1✔
510
async def get_repository_pex(
1✔
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:
1✔
UNCOV
516
        return OptionalPexRequest(None)
×
517

518
    if python_setup.requirement_constraints:
1✔
UNCOV
519
        constraints_repository_pex_request = await _setup_constraints_repository_pex(
×
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
UNCOV
523
        return OptionalPexRequest(constraints_repository_pex_request.maybe_pex_request)
×
524

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

UNCOV
528
    chosen_resolve, interpreter_constraints = await concurrently(
×
529
        choose_python_resolve(ChosenPythonResolveRequest(request.addresses), **implicitly()),
530
        interpreter_constraints_for_targets(
531
            request.to_interpreter_constraints_request(), **implicitly()
532
        ),
533
    )
UNCOV
534
    return OptionalPexRequest(
×
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(
1✔
555
    request: PexFromTargetsRequest, python_setup: PythonSetup
556
) -> tuple[PexRequirements | EntireLockfile, Iterable[Pex]]:
557
    if not request.include_requirements:
1✔
UNCOV
558
        return PexRequirements(), ()
×
559

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

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

594
    if not should_request_repository_pex:
1✔
UNCOV
595
        if not pex_native_subsetting_supported:
×
596
            return requirements, ()
×
597

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

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

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

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

640

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

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

670

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

681
    interpreter_constraints = await interpreter_constraints_for_targets(
1✔
682
        request.to_interpreter_constraints_request(), **implicitly()
683
    )
684

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

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

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

741
    remaining_sources_stripped = await strip_python_sources(remaining_sources)
1✔
742
    sources_digests.append(remaining_sources_stripped.stripped_source_files.snapshot.digest)
1✔
743

744
    merged_sources_digest, additional_inputs = await concurrently(
1✔
745
        merge_digests(MergeDigests(sources_digests)),
746
        merge_digests(MergeDigests(additional_inputs_digests)),
747
    )
748

749
    description = request.description
1✔
750

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

769

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

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

778
    addresses: tuple[Address, ...]
1✔
779
    hardcoded_interpreter_constraints: InterpreterConstraints | None
1✔
780

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

792

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

805

806
def rules():
1✔
807
    return (*collect_rules(), *pex_rules(), *local_dists_rules(), *python_sources_rules())
1✔
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