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

pantsbuild / pants / 24399157824

14 Apr 2026 12:34PM UTC coverage: 92.916% (+0.006%) from 92.91%
24399157824

Pull #23227

github

web-flow
Merge b84a925a7 into cae762f00
Pull Request #23227: Fix uv PEX builder to use pex3 lock export

21 of 27 new or added lines in 3 files covered. (77.78%)

48 existing lines in 4 files now uncovered.

91633 of 98619 relevant lines covered (92.92%)

4.04 hits per line

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

94.83
/src/python/pants/backend/python/util_rules/pex_from_targets_test.py
1
# Copyright 2020 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 importlib.resources
1✔
7
import os
1✔
8
import re
1✔
9
import subprocess
1✔
10
from collections.abc import Iterable
1✔
11
from dataclasses import dataclass
1✔
12
from enum import Enum
1✔
13
from pathlib import Path, PurePath
1✔
14
from textwrap import dedent
1✔
15
from typing import cast
1✔
16
from unittest.mock import Mock
1✔
17

18
import pytest
1✔
19

20
from pants.backend.plugin_development import pants_requirements
1✔
21
from pants.backend.plugin_development.pants_requirements import PantsRequirementsTargetGenerator
1✔
22
from pants.backend.python import target_types_rules
1✔
23
from pants.backend.python.goals import package_pex_binary
1✔
24
from pants.backend.python.subsystems import setuptools
1✔
25
from pants.backend.python.subsystems.setup import PythonSetup
1✔
26
from pants.backend.python.target_types import (
1✔
27
    EntryPoint,
28
    PexBinary,
29
    PexLayout,
30
    PythonRequirementTarget,
31
    PythonSourcesGeneratorTarget,
32
    PythonSourceTarget,
33
    PythonTestTarget,
34
)
35
from pants.backend.python.util_rules import pex_from_targets, pex_test_utils
1✔
36
from pants.backend.python.util_rules.pex import (
1✔
37
    OptionalPex,
38
    OptionalPexRequest,
39
    Pex,
40
    PexPlatforms,
41
    PexRequest,
42
    PexRequirementsInfo,
43
)
44
from pants.backend.python.util_rules.pex_cli import PexPEX
1✔
45
from pants.backend.python.util_rules.pex_from_targets import (
1✔
46
    ChosenPythonResolve,
47
    ChosenPythonResolveRequest,
48
    GlobalRequirementConstraints,
49
    PexFromTargetsRequest,
50
    _determine_requirements_for_pex_from_targets,
51
)
52
from pants.backend.python.util_rules.pex_requirements import (
1✔
53
    EntireLockfile,
54
    LockfileFormat,
55
    PexRequirements,
56
    Resolve,
57
)
58
from pants.backend.python.util_rules.pex_test_utils import get_all_data
1✔
59
from pants.build_graph.address import Address
1✔
60
from pants.core.goals.generate_lockfiles import NoCompatibleResolveException
1✔
61
from pants.core.target_types import FileTarget, ResourceTarget
1✔
62
from pants.engine.addresses import Addresses
1✔
63
from pants.engine.fs import Snapshot
1✔
64
from pants.testutil.option_util import create_subsystem
1✔
65
from pants.testutil.python_rule_runner import PythonRuleRunner
1✔
66
from pants.testutil.rule_runner import QueryRule, engine_error, run_rule_with_mocks
1✔
67
from pants.util.contextutil import pushd
1✔
68
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
1✔
69
from pants.util.strutil import softwrap
1✔
70

71

72
@pytest.fixture
1✔
73
def rule_runner() -> PythonRuleRunner:
1✔
74
    return PythonRuleRunner(
1✔
75
        rules=[
76
            *package_pex_binary.rules(),
77
            *pants_requirements.rules(),
78
            *pex_test_utils.rules(),
79
            *pex_from_targets.rules(),
80
            *target_types_rules.rules(),
81
            QueryRule(PexRequest, (PexFromTargetsRequest,)),
82
            QueryRule(PexRequirementsInfo, (PexRequirements,)),
83
            QueryRule(GlobalRequirementConstraints, ()),
84
            QueryRule(ChosenPythonResolve, [ChosenPythonResolveRequest]),
85
            *setuptools.rules(),
86
        ],
87
        target_types=[
88
            PantsRequirementsTargetGenerator,
89
            PexBinary,
90
            PythonSourcesGeneratorTarget,
91
            PythonRequirementTarget,
92
            PythonSourceTarget,
93
            PythonTestTarget,
94
            FileTarget,
95
            ResourceTarget,
96
        ],
97
    )
98

99

100
@pytest.mark.skip(reason="TODO(#15824)")
1✔
101
@pytest.mark.no_error_if_skipped
1✔
102
def test_choose_compatible_resolve(rule_runner: PythonRuleRunner) -> None:
1✔
UNCOV
103
    def create_target_files(
×
104
        directory: str, *, req_resolve: str, source_resolve: str, test_resolve: str
105
    ) -> dict[str, str]:
UNCOV
106
        return {
×
107
            f"{directory}/BUILD": dedent(
108
                f"""\
109
              python_source(name="dep", source="dep.py", resolve="{source_resolve}")
110
              python_requirement(
111
                  name="req", requirements=[], resolve="{req_resolve}"
112
              )
113
              python_test(
114
                  name="test",
115
                  source="tests.py",
116
                  dependencies=[":dep", ":req"],
117
                  resolve="{test_resolve}",
118
              )
119
              """
120
            ),
121
            f"{directory}/tests.py": "",
122
            f"{directory}/dep.py": "",
123
        }
124

UNCOV
125
    rule_runner.set_options(
×
126
        ["--python-resolves={'a': '', 'b': ''}", "--python-enable-resolves"], env_inherit={"PATH"}
127
    )
UNCOV
128
    rule_runner.write_files(
×
129
        {
130
            # Note that each of these BUILD files are entirely self-contained.
131
            **create_target_files("valid", req_resolve="a", source_resolve="a", test_resolve="a"),
132
            **create_target_files(
133
                "invalid",
134
                req_resolve="a",
135
                source_resolve="a",
136
                test_resolve="b",
137
            ),
138
        }
139
    )
140

141
    def choose_resolve(addresses: list[Address]) -> str:
×
UNCOV
142
        return rule_runner.request(
×
143
            ChosenPythonResolve, [ChosenPythonResolveRequest(Addresses(addresses))]
144
        ).name
145

146
    assert choose_resolve([Address("valid", target_name="test")]) == "a"
×
147
    assert choose_resolve([Address("valid", target_name="dep")]) == "a"
×
UNCOV
148
    assert choose_resolve([Address("valid", target_name="req")]) == "a"
×
149

150
    with engine_error(NoCompatibleResolveException, contains="its dependencies are not compatible"):
×
151
        choose_resolve([Address("invalid", target_name="test")])
×
152
    with engine_error(NoCompatibleResolveException, contains="its dependencies are not compatible"):
×
UNCOV
153
        choose_resolve([Address("invalid", target_name="dep")])
×
154

UNCOV
155
    with engine_error(
×
156
        NoCompatibleResolveException, contains="input targets did not have a resolve"
157
    ):
UNCOV
158
        choose_resolve(
×
159
            [Address("invalid", target_name="req"), Address("invalid", target_name="dep")]
160
        )
161

162

163
def test_determine_requirements_for_pex_from_targets() -> None:
1✔
164
    class RequirementMode(Enum):
1✔
165
        PEX_LOCKFILE = 1
1✔
166
        NON_PEX_LOCKFILE = 2
1✔
167
        # Note that enable_resolves is mutually exclusive with requirement_constraints.
168
        CONSTRAINTS_RESOLVE_ALL = 3
1✔
169
        CONSTRAINTS_NO_RESOLVE_ALL = 4
1✔
170
        NO_LOCKS = 5
1✔
171

172
    req_strings = ["req1", "req2"]
1✔
173
    global_requirement_constraints = ["constraint1", "constraint2"]
1✔
174

175
    resolve__pex = Resolve("pex", False)
1✔
176
    loaded_lockfile__pex = Mock(lockfile_format=LockfileFormat.Pex, as_constraints_strings=None)
1✔
177
    chosen_resolve__pex = Mock(lockfile=Mock())
1✔
178
    chosen_resolve__pex.name = "pex"  # name has special meaning in Mock(), so must set it here.
1✔
179
    loaded_lockfile__not_pex = Mock(
1✔
180
        lockfile_format=LockfileFormat.ConstraintsDeprecated, as_constraints_strings=req_strings
181
    )
182
    chosen_resolve__not_pex = Mock(lockfile=Mock())
1✔
183
    chosen_resolve__not_pex.name = "not_pex"  # ditto.
1✔
184

185
    repository_pex_request__lockfile = Mock()
1✔
186
    repository_pex_request__constraints = Mock()
1✔
187

188
    repository_pex__lockfile = Mock()
1✔
189
    repository_pex__constraints = Mock()
1✔
190

191
    def assert_setup(
1✔
192
        _mode: RequirementMode,
193
        *,
194
        _internal_only: bool,
195
        _platforms: bool,
196
        include_requirements: bool = True,
197
        run_against_entire_lockfile: bool = False,
198
        expected_reqs: PexRequirements = PexRequirements(),
199
        expected_pexes: Iterable[Pex] = (),
200
    ) -> None:
201
        lockfile_used = _mode in (RequirementMode.PEX_LOCKFILE, RequirementMode.NON_PEX_LOCKFILE)
1✔
202
        requirement_constraints_used = _mode in (
1✔
203
            RequirementMode.CONSTRAINTS_RESOLVE_ALL,
204
            RequirementMode.CONSTRAINTS_NO_RESOLVE_ALL,
205
        )
206

207
        python_setup = create_subsystem(
1✔
208
            PythonSetup,
209
            enable_resolves=lockfile_used,
210
            run_against_entire_lockfile=run_against_entire_lockfile,
211
            resolve_all_constraints=_mode != RequirementMode.CONSTRAINTS_NO_RESOLVE_ALL,
212
            requirement_constraints="foo.constraints" if requirement_constraints_used else None,
213
        )
214
        pex_from_targets_request = PexFromTargetsRequest(
1✔
215
            Addresses(),
216
            output_filename="foo",
217
            include_requirements=include_requirements,
218
            platforms=PexPlatforms(["foo"] if _platforms else []),
219
            internal_only=_internal_only,
220
        )
221
        resolved_pex_requirements = PexRequirements(
1✔
222
            req_strings,
223
            constraints_strings=(
224
                global_requirement_constraints if requirement_constraints_used else ()
225
            ),
226
        )
227

228
        # NB: We recreate that platforms should turn off first creating a repository.pex.
229
        if lockfile_used and not _platforms:
1✔
230
            mock_repository_pex_request = OptionalPexRequest(
1✔
231
                maybe_pex_request=repository_pex_request__lockfile
232
            )
233
            mock_repository_pex = OptionalPex(maybe_pex=repository_pex__lockfile)
1✔
234
        elif _mode == RequirementMode.CONSTRAINTS_RESOLVE_ALL and not _platforms:
1✔
235
            mock_repository_pex_request = OptionalPexRequest(
1✔
236
                maybe_pex_request=repository_pex_request__constraints
237
            )
238
            mock_repository_pex = OptionalPex(maybe_pex=repository_pex__constraints)
1✔
239
        else:
240
            mock_repository_pex_request = OptionalPexRequest(maybe_pex_request=None)
1✔
241
            mock_repository_pex = OptionalPex(maybe_pex=None)
1✔
242

243
        reqs, pexes = run_rule_with_mocks(
1✔
244
            _determine_requirements_for_pex_from_targets,
245
            rule_args=[pex_from_targets_request, python_setup],
246
            mock_calls={
247
                "pants.backend.python.util_rules.pex_from_targets.determine_requirement_strings_in_closure": lambda _: resolved_pex_requirements,
248
                "pants.backend.python.util_rules.pex_from_targets.choose_python_resolve": lambda _: (
249
                    chosen_resolve__pex
250
                    if _mode == RequirementMode.PEX_LOCKFILE
251
                    else chosen_resolve__not_pex
252
                ),
253
                "pants.backend.python.util_rules.pex_requirements.load_lockfile": lambda _: (
254
                    loaded_lockfile__pex
255
                    if _mode == RequirementMode.PEX_LOCKFILE
256
                    else loaded_lockfile__not_pex
257
                ),
258
                "pants.backend.python.util_rules.pex_from_targets.get_repository_pex": lambda _: mock_repository_pex_request,
259
                "pants.backend.python.util_rules.pex.create_optional_pex": lambda _: mock_repository_pex,
260
            },
261
        )
262
        assert expected_reqs == reqs
1✔
263
        assert expected_pexes == pexes
1✔
264

265
    # If include_requirements is False, no matter what, early return.
266
    for mode in RequirementMode:
1✔
267
        assert_setup(
1✔
268
            mode,
269
            include_requirements=False,
270
            _internal_only=False,
271
            _platforms=False,
272
            # Nothing is expected
273
        )
274

275
    # Pex lockfiles: usually, return PexRequirements with from_superset as the resolve.
276
    #   Except for when run_against_entire_lockfile is set and it's an internal_only Pex, then
277
    #   return PexRequest.
278
    for internal_only in (True, False):
1✔
279
        assert_setup(
1✔
280
            RequirementMode.PEX_LOCKFILE,
281
            _internal_only=internal_only,
282
            _platforms=False,
283
            expected_reqs=PexRequirements(req_strings, from_superset=resolve__pex),
284
        )
285

286
    assert_setup(
1✔
287
        RequirementMode.PEX_LOCKFILE,
288
        _internal_only=False,
289
        _platforms=True,
290
        expected_reqs=PexRequirements(req_strings, from_superset=resolve__pex),
291
    )
292
    for platforms in (True, False):
1✔
293
        assert_setup(
1✔
294
            RequirementMode.PEX_LOCKFILE,
295
            _internal_only=False,
296
            run_against_entire_lockfile=True,
297
            _platforms=platforms,
298
            expected_reqs=PexRequirements(req_strings, from_superset=resolve__pex),
299
        )
300
    assert_setup(
1✔
301
        RequirementMode.PEX_LOCKFILE,
302
        _internal_only=True,
303
        run_against_entire_lockfile=True,
304
        _platforms=False,
305
        expected_reqs=repository_pex_request__lockfile.requirements,
306
        expected_pexes=[repository_pex__lockfile],
307
    )
308

309
    # Non-Pex lockfiles: except for when run_against_entire_lockfile is applicable, return
310
    # PexRequirements with from_superset as the lockfile repository Pex and constraint_strings as
311
    # the lockfile's requirements.
312
    for internal_only in (False, True):
1✔
313
        assert_setup(
1✔
314
            RequirementMode.NON_PEX_LOCKFILE,
315
            _internal_only=internal_only,
316
            _platforms=False,
317
            expected_reqs=PexRequirements(
318
                req_strings, constraints_strings=req_strings, from_superset=repository_pex__lockfile
319
            ),
320
        )
321
    assert_setup(
1✔
322
        RequirementMode.NON_PEX_LOCKFILE,
323
        _internal_only=False,
324
        _platforms=True,
325
        expected_reqs=PexRequirements(
326
            req_strings, constraints_strings=req_strings, from_superset=None
327
        ),
328
    )
329
    assert_setup(
1✔
330
        RequirementMode.NON_PEX_LOCKFILE,
331
        _internal_only=False,
332
        run_against_entire_lockfile=True,
333
        _platforms=False,
334
        expected_reqs=PexRequirements(
335
            req_strings, constraints_strings=req_strings, from_superset=repository_pex__lockfile
336
        ),
337
    )
338
    assert_setup(
1✔
339
        RequirementMode.NON_PEX_LOCKFILE,
340
        _internal_only=False,
341
        run_against_entire_lockfile=True,
342
        _platforms=True,
343
        expected_reqs=PexRequirements(
344
            req_strings, constraints_strings=req_strings, from_superset=None
345
        ),
346
    )
347
    assert_setup(
1✔
348
        RequirementMode.NON_PEX_LOCKFILE,
349
        _internal_only=True,
350
        run_against_entire_lockfile=True,
351
        _platforms=False,
352
        expected_reqs=repository_pex_request__lockfile.requirements,
353
        expected_pexes=[repository_pex__lockfile],
354
    )
355

356
    # Constraints file with resolve_all_constraints: except for when run_against_entire_lockfile
357
    #   is applicable, return PexRequirements with from_superset as the constraints repository Pex
358
    #   and constraint_strings as the global constraints.
359
    for internal_only in (False, True):
1✔
360
        assert_setup(
1✔
361
            RequirementMode.CONSTRAINTS_RESOLVE_ALL,
362
            _internal_only=internal_only,
363
            _platforms=False,
364
            expected_reqs=PexRequirements(
365
                req_strings,
366
                constraints_strings=global_requirement_constraints,
367
                from_superset=repository_pex__constraints,
368
            ),
369
        )
370
    assert_setup(
1✔
371
        RequirementMode.CONSTRAINTS_RESOLVE_ALL,
372
        _internal_only=False,
373
        _platforms=True,
374
        expected_reqs=PexRequirements(
375
            req_strings, constraints_strings=global_requirement_constraints, from_superset=None
376
        ),
377
    )
378
    assert_setup(
1✔
379
        RequirementMode.CONSTRAINTS_RESOLVE_ALL,
380
        _internal_only=False,
381
        run_against_entire_lockfile=True,
382
        _platforms=False,
383
        expected_reqs=PexRequirements(
384
            req_strings,
385
            constraints_strings=global_requirement_constraints,
386
            from_superset=repository_pex__constraints,
387
        ),
388
    )
389
    assert_setup(
1✔
390
        RequirementMode.CONSTRAINTS_RESOLVE_ALL,
391
        _internal_only=False,
392
        run_against_entire_lockfile=True,
393
        _platforms=True,
394
        expected_reqs=PexRequirements(
395
            req_strings, constraints_strings=global_requirement_constraints, from_superset=None
396
        ),
397
    )
398
    assert_setup(
1✔
399
        RequirementMode.CONSTRAINTS_RESOLVE_ALL,
400
        _internal_only=True,
401
        run_against_entire_lockfile=True,
402
        _platforms=False,
403
        expected_reqs=repository_pex_request__constraints.requirements,
404
        expected_pexes=[repository_pex__constraints],
405
    )
406

407
    # Constraints file without resolve_all_constraints: always PexRequirements with
408
    #   constraint_strings as the global constraints.
409
    for internal_only in (True, False):
1✔
410
        assert_setup(
1✔
411
            RequirementMode.CONSTRAINTS_NO_RESOLVE_ALL,
412
            _internal_only=internal_only,
413
            _platforms=platforms,
414
            expected_reqs=PexRequirements(
415
                req_strings, constraints_strings=global_requirement_constraints
416
            ),
417
        )
418
    for platforms in (True, False):
1✔
419
        assert_setup(
1✔
420
            RequirementMode.CONSTRAINTS_NO_RESOLVE_ALL,
421
            _internal_only=False,
422
            _platforms=platforms,
423
            expected_reqs=PexRequirements(
424
                req_strings, constraints_strings=global_requirement_constraints
425
            ),
426
        )
427

428
    # No constraints and lockfiles: return PexRequirements without modification.
429
    for internal_only in (True, False):
1✔
430
        assert_setup(
1✔
431
            RequirementMode.NO_LOCKS,
432
            _internal_only=internal_only,
433
            _platforms=False,
434
            expected_reqs=PexRequirements(req_strings),
435
        )
436
    assert_setup(
1✔
437
        RequirementMode.NO_LOCKS,
438
        _internal_only=False,
439
        _platforms=True,
440
        expected_reqs=PexRequirements(req_strings),
441
    )
442

443

444
@dataclass(frozen=True)
1✔
445
class Project:
1✔
446
    name: str
1✔
447
    version: str
1✔
448

449

450
build_deps = ["setuptools==66.1.0", "wheel==0.37.0"]
1✔
451

452

453
setuptools_poetry_lockfile = r"""
1✔
454
# This lockfile was autogenerated by Pants. To regenerate, run:
455
#
456
#    ./pants generate-lockfiles --resolve=setuptools
457
#
458
# --- BEGIN PANTS LOCKFILE METADATA: DO NOT EDIT OR REMOVE ---
459
# {
460
#   "version": 2,
461
#   "valid_for_interpreter_constraints": [
462
#     "CPython>=3.7"
463
#   ],
464
#   "generated_with_requirements": [
465
#     "setuptools==54.1.2"
466
#   ]
467
# }
468
# --- END PANTS LOCKFILE METADATA ---
469

470
setuptools==54.1.2; python_version >= "3.6" \
471
    --hash=sha256:dd20743f36b93cbb8724f4d2ccd970dce8b6e6e823a13aa7e5751bb4e674c20b \
472
    --hash=sha256:ebd0148faf627b569c8d2a1b20f5d3b09c873f12739d71c7ee88f037d5be82ff
473
"""
474

475

476
def create_project_dir(workdir: Path, project: Project) -> PurePath:
1✔
477
    project_dir = workdir / "projects" / project.name
1✔
478
    project_dir.mkdir(parents=True)
1✔
479

480
    (project_dir / "pyproject.toml").write_text(
1✔
481
        dedent(
482
            f"""\
483
            [build-system]
484
            requires = {build_deps}
485
            build-backend = "setuptools.build_meta"
486
            """
487
        )
488
    )
489
    (project_dir / "setup.cfg").write_text(
1✔
490
        dedent(
491
            f"""\
492
                [metadata]
493
                name = {project.name}
494
                version = {project.version}
495
                """
496
        )
497
    )
498
    return project_dir
1✔
499

500

501
def create_dists(
1✔
502
    workdir: Path, rule_runner: PythonRuleRunner, project: Project, *projects: Project
503
) -> PurePath:
504
    project_dirs = [create_project_dir(workdir, proj) for proj in (project, *projects)]
1✔
505

506
    # Get the pex CLI binary and materialize it
507
    pex_pex = rule_runner.request(PexPEX, [])
1✔
508
    rule_runner.scheduler.write_digest(pex_pex.digest)
1✔
509
    pex_binary = Path(rule_runner.build_root) / pex_pex.exe
1✔
510

511
    pex_output = workdir / "output.pex"
1✔
512
    subprocess.run(
1✔
513
        args=[
514
            pex_binary,
515
            *project_dirs,
516
            *build_deps,
517
            "--include-tools",
518
            "-o",
519
            pex_output,
520
        ],
521
        check=True,
522
    )
523

524
    find_links = workdir / "find-links"
1✔
525
    subprocess.run(
1✔
526
        args=[
527
            pex_binary,
528
            pex_output,
529
            "repository",
530
            "extract",
531
            "--find-links",
532
            find_links,
533
        ],
534
        check=True,
535
        env=os.environ.copy() | {"PEX_MODULE": "pex.tools"},
536
    )
537
    return find_links
1✔
538

539

540
def requirements(rule_runner: PythonRuleRunner, pex: Pex) -> list[str]:
1✔
541
    return cast(list[str], get_all_data(rule_runner, pex).info["requirements"])
1✔
542

543

544
def _normalize_url_req(s: str) -> str:
1✔
545
    """See https://github.com/pypa/packaging/issues/935.
546

547
    Several tests here are brittle and rely on Pex/Pants being on the same packaging version.  These
548
    are pretty low value.  Back this out after upgrading packaging.
549
    """
550
    return re.sub(r"\s*@\s*", "@ ", s)
1✔
551

552

553
def test_constraints_validation(tmp_path: Path, rule_runner: PythonRuleRunner) -> None:
1✔
554
    sdists = tmp_path / "sdists"
1✔
555
    sdists.mkdir()
1✔
556
    find_links = create_dists(
1✔
557
        sdists,
558
        rule_runner,
559
        Project("Foo-Bar", "1.0.0"),
560
        Project("Bar", "5.5.5"),
561
        Project("baz", "2.2.2"),
562
        Project("QUX", "3.4.5"),
563
    )
564

565
    # Turn the project dir into a git repo, so it can be cloned.
566
    gitdir = tmp_path / "git"
1✔
567
    gitdir.mkdir()
1✔
568
    foorl_dir = create_project_dir(gitdir, Project("foorl", "9.8.7"))
1✔
569
    with pushd(str(foorl_dir)):
1✔
570
        subprocess.check_call(["git", "init"])
1✔
571
        subprocess.check_call(["git", "config", "user.name", "dummy"])
1✔
572
        subprocess.check_call(["git", "config", "user.email", "dummy@dummy.com"])
1✔
573
        subprocess.check_call(["git", "add", "--all"])
1✔
574
        subprocess.check_call(["git", "commit", "-m", "initial commit"])
1✔
575
        subprocess.check_call(["git", "branch", "9.8.7"])
1✔
576

577
    # This string won't parse as a Requirement if it doesn't contain a netloc,
578
    # so we explicitly mention localhost.
579
    url_req = f"foorl @ git+file://localhost{foorl_dir.as_posix()}@9.8.7"
1✔
580

581
    rule_runner.write_files(
1✔
582
        {
583
            "util.py": "",
584
            "app.py": "",
585
            "BUILD": dedent(
586
                f"""
587
                python_requirement(name="foo", requirements=["foo-bar>=0.1.2"])
588
                python_requirement(name="bar", requirements=["bar==5.5.5"])
589
                python_requirement(name="baz", requirements=["baz"])
590
                python_requirement(name="foorl", requirements=["{url_req}"])
591
                python_sources(name="util", sources=["util.py"], dependencies=[":foo", ":bar"])
592
                python_sources(name="app", sources=["app.py"], dependencies=[":util", ":baz", ":foorl"])
593
                """
594
            ),
595
            "constraints1.txt": dedent(
596
                """
597
                # Comment.
598
                --find-links=https://duckduckgo.com
599
                Foo._-BAR==1.0.0  # Inline comment.
600
                bar==5.5.5
601
                baz==2.2.2
602
                qux==3.4.5
603
                # Note that pip does not allow URL requirements in constraints files,
604
                # so there is no mention of foorl here.
605
                """
606
            ),
607
        }
608
    )
609

610
    # Create and parse the constraints file.
611
    constraints1_filename = "constraints1.txt"
1✔
612
    rule_runner.set_options(
1✔
613
        [f"--python-requirement-constraints={constraints1_filename}"], env_inherit={"PATH"}
614
    )
615
    constraints1_strings = [str(c) for c in rule_runner.request(GlobalRequirementConstraints, [])]
1✔
616

617
    def get_pex_request(
1✔
618
        constraints_file: str | None,
619
        resolve_all_constraints: bool | None,
620
        *,
621
        _additional_args: Iterable[str] = (),
622
        _additional_lockfile_args: Iterable[str] = (),
623
    ) -> PexRequest:
624
        args = ["--backend-packages=pants.backend.python"]
1✔
625
        request = PexFromTargetsRequest(
1✔
626
            [Address("", target_name="app")],
627
            output_filename="demo.pex",
628
            internal_only=True,
629
            additional_args=_additional_args,
630
            additional_lockfile_args=_additional_lockfile_args,
631
        )
632
        if resolve_all_constraints is not None:
1✔
633
            args.append(f"--python-resolve-all-constraints={resolve_all_constraints!r}")
1✔
634
        if constraints_file:
1✔
635
            args.append(f"--python-requirement-constraints={constraints_file}")
1✔
636
        args.append("--python-repos-indexes=[]")
1✔
637
        args.append(f"--python-repos-repos={find_links}")
1✔
638
        rule_runner.set_options(args, env_inherit={"PATH"})
1✔
639
        pex_request = rule_runner.request(PexRequest, [request])
1✔
640
        assert OrderedSet(_additional_args).issubset(OrderedSet(pex_request.additional_args))
1✔
641
        return pex_request
1✔
642

643
    additional_args = ["--strip-pex-env"]
1✔
644
    additional_lockfile_args = ["--no-strip-pex-env"]
1✔
645

646
    pex_req1 = get_pex_request(constraints1_filename, resolve_all_constraints=False)
1✔
647
    assert isinstance(pex_req1.requirements, PexRequirements)
1✔
648
    assert pex_req1.requirements.constraints_strings == FrozenOrderedSet(constraints1_strings)
1✔
649
    req_strings_obj1 = rule_runner.request(PexRequirementsInfo, (pex_req1.requirements,))
1✔
650
    assert tuple(_normalize_url_req(s) for s in req_strings_obj1.req_strings) == (
1✔
651
        "bar==5.5.5",
652
        "baz",
653
        "foo-bar>=0.1.2",
654
        _normalize_url_req(url_req),
655
    )
656

657
    pex_req2 = get_pex_request(
1✔
658
        constraints1_filename,
659
        resolve_all_constraints=True,
660
        _additional_args=additional_args,
661
        _additional_lockfile_args=additional_lockfile_args,
662
    )
663
    pex_req2_reqs = pex_req2.requirements
1✔
664
    assert isinstance(pex_req2_reqs, PexRequirements)
1✔
665
    req_strings_obj2 = rule_runner.request(PexRequirementsInfo, (pex_req2_reqs,))
1✔
666
    assert tuple(_normalize_url_req(s) for s in req_strings_obj2.req_strings) == (
1✔
667
        "bar==5.5.5",
668
        "baz",
669
        "foo-bar>=0.1.2",
670
        _normalize_url_req(url_req),
671
    )
672
    assert isinstance(pex_req2_reqs.from_superset, Pex)
1✔
673
    repository_pex = pex_req2_reqs.from_superset
1✔
674
    assert not get_all_data(rule_runner, repository_pex).info["strip_pex_env"]
1✔
675
    assert [
1✔
676
        "Foo._-BAR==1.0.0",
677
        "bar==5.5.5",
678
        "baz==2.2.2",
679
        _normalize_url_req(url_req),
680
        "qux==3.4.5",
681
    ] == [_normalize_url_req(r) for r in requirements(rule_runner, repository_pex)]
682

683
    with engine_error(
1✔
684
        ValueError,
685
        contains=softwrap(
686
            """
687
            `[python].resolve_all_constraints` is enabled, so
688
            `[python].requirement_constraints` must also be set.
689
            """
690
        ),
691
    ):
692
        get_pex_request(None, resolve_all_constraints=True)
1✔
693

694
    # Shouldn't error, as we don't explicitly set --resolve-all-constraints.
695
    get_pex_request(None, resolve_all_constraints=None)
1✔
696

697

698
def test_pants_requirement(rule_runner: PythonRuleRunner) -> None:
1✔
699
    rule_runner.write_files(
1✔
700
        {
701
            "app.py": "",
702
            "BUILD": dedent(
703
                """
704
                pants_requirements(name="pants")
705
                python_source(name="app", source="app.py", dependencies=[":pants"])
706
                """
707
            ),
708
        }
709
    )
710
    args = [
1✔
711
        "--backend-packages=pants.backend.python",
712
        "--backend-packages=pants.backend.plugin_development",
713
        "--python-repos-indexes=[]",
714
    ]
715
    request = PexFromTargetsRequest(
1✔
716
        [Address("", target_name="app")],
717
        output_filename="demo.pex",
718
        internal_only=False,
719
    )
720
    rule_runner.set_options(args, env_inherit={"PATH"})
1✔
721
    pex_req = rule_runner.request(PexRequest, [request])
1✔
722
    pex_reqs_info = rule_runner.request(PexRequirementsInfo, [pex_req.requirements])
1✔
723
    assert pex_reqs_info.find_links == ("https://wheels.pantsbuild.org/simple",)
1✔
724

725

726
@pytest.mark.parametrize("include_requirements", [False, True])
1✔
727
def test_exclude_requirements(
1✔
728
    include_requirements: bool, tmp_path: Path, rule_runner: PythonRuleRunner
729
) -> None:
730
    sdists = tmp_path / "sdists"
1✔
731
    sdists.mkdir()
1✔
732
    find_links = create_dists(sdists, rule_runner, Project("baz", "2.2.2"))
1✔
733

734
    rule_runner.write_files(
1✔
735
        {
736
            "BUILD": dedent(
737
                """
738
                python_requirement(name="baz", requirements=["foo==1.2.3"])
739
                python_sources(name="app", sources=["app.py"], dependencies=[":baz"])
740
                """
741
            ),
742
            "constraints.txt": dedent("foo==1.2.3"),
743
            "app.py": "",
744
        }
745
    )
746

747
    rule_runner.set_options(
1✔
748
        [
749
            "--backend-packages=pants.backend.python",
750
            "--python-repos-indexes=[]",
751
            f"--python-repos-repos={find_links}",
752
        ],
753
        env_inherit={"PATH"},
754
    )
755

756
    request = PexFromTargetsRequest(
1✔
757
        [Address("", target_name="app")],
758
        output_filename="demo.pex",
759
        internal_only=True,
760
        include_requirements=include_requirements,
761
    )
762
    pex_request = rule_runner.request(PexRequest, [request])
1✔
763
    assert isinstance(pex_request.requirements, PexRequirements)
1✔
764
    assert len(pex_request.requirements.req_strings_or_addrs) == (1 if include_requirements else 0)
1✔
765

766

767
@pytest.mark.parametrize("include_sources", [False, True])
1✔
768
def test_exclude_sources(include_sources: bool, rule_runner: PythonRuleRunner) -> None:
1✔
769
    rule_runner.write_files(
1✔
770
        {
771
            "BUILD": dedent(
772
                """
773
                python_sources(name="app", sources=["app.py"])
774
                """
775
            ),
776
            "app.py": "",
777
        }
778
    )
779

780
    rule_runner.set_options(
1✔
781
        [
782
            "--backend-packages=pants.backend.python",
783
            "--python-repos-indexes=[]",
784
        ],
785
        env_inherit={"PATH"},
786
    )
787

788
    request = PexFromTargetsRequest(
1✔
789
        [Address("", target_name="app")],
790
        output_filename="demo.pex",
791
        internal_only=True,
792
        include_source_files=include_sources,
793
    )
794
    pex_request = rule_runner.request(PexRequest, [request])
1✔
795
    snapshot = rule_runner.request(Snapshot, [pex_request.sources])
1✔
796
    assert len(snapshot.files) == (1 if include_sources else 0)
1✔
797

798

799
def test_include_sources_without_transitive_package_sources(rule_runner: PythonRuleRunner) -> None:
1✔
800
    rule_runner.write_files(
1✔
801
        {
802
            "src/app/BUILD": dedent(
803
                """
804
                python_sources(
805
                    name="app",
806
                    sources=["app.py"],
807
                    dependencies=["//src/dep:pkg"],
808
                )
809
                """
810
            ),
811
            "src/app/app.py": "",
812
            "src/dep/BUILD": dedent(
813
                # This test requires a package that has a standard dependencies field.
814
                # 'pex_binary' has a dependencies field; 'archive' does not.
815
                """
816
                pex_binary(name="pkg", dependencies=[":dep"])
817
                python_sources(name="dep", sources=["dep.py"])
818
                """
819
            ),
820
            "src/dep/dep.py": "",
821
        }
822
    )
823

824
    rule_runner.set_options(
1✔
825
        [
826
            "--backend-packages=pants.backend.python",
827
            "--python-repos-indexes=[]",
828
        ],
829
        env_inherit={"PATH"},
830
    )
831

832
    request = PexFromTargetsRequest(
1✔
833
        [Address("src/app", target_name="app")],
834
        output_filename="demo.pex",
835
        internal_only=True,
836
        include_source_files=True,
837
    )
838
    pex_request = rule_runner.request(PexRequest, [request])
1✔
839
    snapshot = rule_runner.request(Snapshot, [pex_request.sources])
1✔
840

841
    # the packaged transitive dep is excluded
842
    assert snapshot.files == ("app/app.py",)
1✔
843

844

845
@pytest.mark.parametrize("enable_resolves", [False, True])
1✔
846
def test_cross_platform_pex_disables_subsetting(
1✔
847
    rule_runner: PythonRuleRunner, enable_resolves: bool
848
) -> None:
849
    # See https://github.com/pantsbuild/pants/issues/12222.
850
    lockfile = "3rdparty/python/default.lock"
1✔
851
    constraints = ["foo==1.0", "bar==1.0"]
1✔
852
    rule_runner.write_files(
1✔
853
        {
854
            lockfile: "\n".join(constraints),
855
            "a.py": "",
856
            "BUILD": dedent(
857
                """
858
                python_requirement(name="foo",requirements=["foo"])
859
                python_requirement(name="bar",requirements=["bar"])
860
                python_sources(name="lib",dependencies=[":foo"])
861
                """
862
            ),
863
        }
864
    )
865

866
    if enable_resolves:
1✔
867
        options = [
1✔
868
            "--python-enable-resolves",
869
            # NB: Because this is a synthetic lockfile without a header.
870
            "--python-invalid-lockfile-behavior=ignore",
871
        ]
872
    else:
873
        options = [
1✔
874
            f"--python-requirement-constraints={lockfile}",
875
            "--python-resolve-all-constraints",
876
        ]
877
    rule_runner.set_options(options, env_inherit={"PATH"})
1✔
878

879
    request = PexFromTargetsRequest(
1✔
880
        [Address("", target_name="lib")],
881
        output_filename="demo.pex",
882
        internal_only=False,
883
        platforms=PexPlatforms(["some-platform-x86_64"]),
884
    )
885
    result = rule_runner.request(PexRequest, [request])
1✔
886

887
    assert result.requirements == PexRequirements(
1✔
888
        request.addresses,
889
        constraints_strings=constraints,
890
        description_of_origin="//:lib",
891
    )
892

893

894
class ResolveMode(Enum):
1✔
895
    resolve_all_constraints = "resolve_all_constraints"
1✔
896
    poetry_or_manual = "poetry_or_manual"
1✔
897
    pex = "pex"
1✔
898

899

900
@pytest.mark.parametrize(
1✔
901
    "mode,internal_only,run_against_entire_lockfile",
902
    [(m, io, rael) for m in ResolveMode for io in [True, False] for rael in [True, False]],
903
)
904
def test_lockfile_requirements_selection(
1✔
905
    rule_runner: PythonRuleRunner,
906
    mode: ResolveMode,
907
    internal_only: bool,
908
    run_against_entire_lockfile: bool,
909
) -> None:
910
    mode_files: dict[str, str | bytes] = {
1✔
911
        "a.py": "",
912
        "BUILD": dedent(
913
            """
914
                python_sources(name="lib", dependencies=[":setuptools"])
915
                python_requirement(name="setuptools", requirements=["setuptools"])
916
                """
917
        ),
918
    }
919
    if mode == ResolveMode.resolve_all_constraints:
1✔
920
        mode_files.update({"constraints.txt": "setuptools==54.1.2"})
1✔
921
    elif mode == ResolveMode.poetry_or_manual:
1✔
922
        mode_files.update({"3rdparty/python/default.lock": setuptools_poetry_lockfile})
1✔
923
    else:
924
        assert mode == ResolveMode.pex
1✔
925
        lock_content = (
1✔
926
            importlib.resources.files("pants.backend.python.subsystems") / "setuptools.lock"
927
        ).read_bytes()
928
        mode_files.update({"3rdparty/python/default.lock": lock_content})
1✔
929

930
    rule_runner.write_files(mode_files)
1✔
931

932
    if mode == ResolveMode.resolve_all_constraints:
1✔
933
        options = [
1✔
934
            "--python-requirement-constraints=constraints.txt",
935
        ]
936
    else:
937
        # NB: It doesn't matter what the lockfile generator is set to: only what is actually on disk.
938
        options = [
1✔
939
            "--python-enable-resolves",
940
            "--python-default-resolve=myresolve",
941
            "--python-resolves={'myresolve':'3rdparty/python/default.lock'}",
942
        ]
943

944
    if run_against_entire_lockfile:
1✔
945
        options.append("--python-run-against-entire-lockfile")
1✔
946

947
    request = PexFromTargetsRequest(
1✔
948
        [Address("", target_name="lib")],
949
        output_filename="demo.pex",
950
        internal_only=internal_only,
951
        main=EntryPoint("a"),
952
    )
953
    rule_runner.set_options(options, env_inherit={"PATH"})
1✔
954
    result = rule_runner.request(PexRequest, [request])
1✔
955
    assert result.layout == (PexLayout.PACKED if internal_only else PexLayout.ZIPAPP)
1✔
956
    assert result.main == EntryPoint("a")
1✔
957

958
    if run_against_entire_lockfile and internal_only:
1✔
959
        # With `run_against_entire_lockfile`, all internal requests result in the full set
960
        # of requirements, but that is encoded slightly differently per mode.
961
        if mode == ResolveMode.resolve_all_constraints:
1✔
962
            # NB: The use of the legacy constraints file with `resolve_all_constraints` requires parsing
963
            # and manipulation of the constraints, and needs to include transitive deps (unlike other
964
            # lockfile requests). So it is emitted as `PexRequirements` rather than EntireLockfile.
965
            assert isinstance(result.requirements, PexRequirements)
1✔
966
            assert not result.requirements.from_superset
1✔
967
        else:
968
            assert mode in (ResolveMode.poetry_or_manual, ResolveMode.pex)
1✔
969
            assert isinstance(result.requirements, EntireLockfile)
1✔
970
    else:
971
        assert isinstance(result.requirements, PexRequirements)
1✔
972
        if mode in (ResolveMode.resolve_all_constraints, ResolveMode.poetry_or_manual):
1✔
973
            assert isinstance(result.requirements.from_superset, Pex)
1✔
974
            assert not get_all_data(rule_runner, result.requirements.from_superset).is_zipapp
1✔
975
        else:
976
            assert mode == ResolveMode.pex
1✔
977
            assert isinstance(result.requirements.from_superset, Resolve)
1✔
978
            assert result.requirements.from_superset.name == "myresolve"
1✔
979

980

981
def test_warn_about_files_targets(rule_runner: PythonRuleRunner, caplog) -> None:
1✔
982
    rule_runner.write_files(
1✔
983
        {
984
            "app.py": "",
985
            "file.txt": "",
986
            "resource.txt": "",
987
            "BUILD": dedent(
988
                """
989
                file(name="file_target", source="file.txt")
990
                resource(name="resource_target", source="resource.txt")
991
                python_sources(name="app", dependencies=[":file_target", ":resource_target"])
992
                """
993
            ),
994
        }
995
    )
996

997
    rule_runner.request(
1✔
998
        PexRequest,
999
        [
1000
            PexFromTargetsRequest(
1001
                [Address("", target_name="app")],
1002
                output_filename="app.pex",
1003
                internal_only=True,
1004
                warn_for_transitive_files_targets=True,
1005
            )
1006
        ],
1007
    )
1008

1009
    assert "The target //:app (`python_source`) transitively depends on" in caplog.text
1✔
1010
    # files are not fine:
1011
    assert "//:file_target" in caplog.text
1✔
1012
    # resources are fine:
1013
    assert "resource_target" not in caplog.text
1✔
1014
    assert "resource.txt" not in caplog.text
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