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

pantsbuild / pants / 20826906489

08 Jan 2026 06:09PM UTC coverage: 80.192% (-0.08%) from 80.274%
20826906489

Pull #22987

github

web-flow
Merge aa52dd80f into 0d471f924
Pull Request #22987: WIP DRAFT: Pex 2.77.1 tests

3 of 3 new or added lines in 1 file covered. (100.0%)

90 existing lines in 15 files now uncovered.

78714 of 98157 relevant lines covered (80.19%)

3.36 hits per line

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

93.73
/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 subprocess
1✔
9
from collections.abc import Iterable
1✔
10
from dataclasses import dataclass
1✔
11
from enum import Enum
1✔
12
from pathlib import Path, PurePath
1✔
13
from textwrap import dedent
1✔
14
from typing import cast
1✔
15
from unittest.mock import Mock
1✔
16

17
import pytest
1✔
18

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

69

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

97

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

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

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

144
    assert choose_resolve([Address("valid", target_name="test")]) == "a"
×
145
    assert choose_resolve([Address("valid", target_name="dep")]) == "a"
×
146
    assert choose_resolve([Address("valid", target_name="req")]) == "a"
×
147

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

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

160

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

170
    req_strings = ["req1", "req2"]
1✔
171
    global_requirement_constraints = ["constraint1", "constraint2"]
1✔
172

173
    resolve__pex = Resolve("pex", False)
1✔
174
    loaded_lockfile__pex = Mock(is_pex_native=True, as_constraints_strings=None)
1✔
175
    chosen_resolve__pex = Mock(lockfile=Mock())
1✔
176
    chosen_resolve__pex.name = "pex"  # name has special meaning in Mock(), so must set it here.
1✔
177
    loaded_lockfile__not_pex = Mock(is_pex_native=False, as_constraints_strings=req_strings)
1✔
178
    chosen_resolve__not_pex = Mock(lockfile=Mock())
1✔
179
    chosen_resolve__not_pex.name = "not_pex"  # ditto.
1✔
180

181
    repository_pex_request__lockfile = Mock()
1✔
182
    repository_pex_request__constraints = Mock()
1✔
183

184
    repository_pex__lockfile = Mock()
1✔
185
    repository_pex__constraints = Mock()
1✔
186

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

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

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

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

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

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

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

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

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

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

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

439

440
@dataclass(frozen=True)
1✔
441
class Project:
1✔
442
    name: str
1✔
443
    version: str
1✔
444

445

446
build_deps = ["setuptools==66.1.0", "wheel==0.37.0"]
1✔
447

448

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

466
setuptools==54.1.2; python_version >= "3.6" \
467
    --hash=sha256:dd20743f36b93cbb8724f4d2ccd970dce8b6e6e823a13aa7e5751bb4e674c20b \
468
    --hash=sha256:ebd0148faf627b569c8d2a1b20f5d3b09c873f12739d71c7ee88f037d5be82ff
469
"""
470

471

472
def create_project_dir(workdir: Path, project: Project) -> PurePath:
1✔
473
    project_dir = workdir / "projects" / project.name
1✔
474
    project_dir.mkdir(parents=True)
1✔
475

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

496

497
def create_dists(
1✔
498
    workdir: Path, rule_runner: PythonRuleRunner, project: Project, *projects: Project
499
) -> PurePath:
500
    project_dirs = [create_project_dir(workdir, proj) for proj in (project, *projects)]
1✔
501

502
    # Get the pex CLI binary and materialize it
503
    pex_pex = rule_runner.request(PexPEX, [])
1✔
504
    rule_runner.scheduler.write_digest(pex_pex.digest)
1✔
505
    pex_binary = Path(rule_runner.build_root) / pex_pex.exe
1✔
506

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

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

535

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

539

540
def test_constraints_validation(tmp_path: Path, rule_runner: PythonRuleRunner) -> None:
1✔
541
    sdists = tmp_path / "sdists"
1✔
542
    sdists.mkdir()
1✔
543
    find_links = create_dists(
1✔
544
        sdists,
545
        rule_runner,
546
        Project("Foo-Bar", "1.0.0"),
547
        Project("Bar", "5.5.5"),
548
        Project("baz", "2.2.2"),
549
        Project("QUX", "3.4.5"),
550
    )
551

552
    # Turn the project dir into a git repo, so it can be cloned.
553
    gitdir = tmp_path / "git"
1✔
554
    gitdir.mkdir()
1✔
555
    foorl_dir = create_project_dir(gitdir, Project("foorl", "9.8.7"))
1✔
556
    with pushd(str(foorl_dir)):
1✔
557
        subprocess.check_call(["git", "init"])
1✔
558
        subprocess.check_call(["git", "config", "user.name", "dummy"])
1✔
559
        subprocess.check_call(["git", "config", "user.email", "dummy@dummy.com"])
1✔
560
        subprocess.check_call(["git", "add", "--all"])
1✔
561
        subprocess.check_call(["git", "commit", "-m", "initial commit"])
1✔
562
        subprocess.check_call(["git", "branch", "9.8.7"])
1✔
563

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

568
    rule_runner.write_files(
1✔
569
        {
570
            "util.py": "",
571
            "app.py": "",
572
            "BUILD": dedent(
573
                f"""
574
                python_requirement(name="foo", requirements=["foo-bar>=0.1.2"])
575
                python_requirement(name="bar", requirements=["bar==5.5.5"])
576
                python_requirement(name="baz", requirements=["baz"])
577
                python_requirement(name="foorl", requirements=["{url_req}"])
578
                python_sources(name="util", sources=["util.py"], dependencies=[":foo", ":bar"])
579
                python_sources(name="app", sources=["app.py"], dependencies=[":util", ":baz", ":foorl"])
580
                """
581
            ),
582
            "constraints1.txt": dedent(
583
                """
584
                # Comment.
585
                --find-links=https://duckduckgo.com
586
                Foo._-BAR==1.0.0  # Inline comment.
587
                bar==5.5.5
588
                baz==2.2.2
589
                qux==3.4.5
590
                # Note that pip does not allow URL requirements in constraints files,
591
                # so there is no mention of foorl here.
592
                """
593
            ),
594
        }
595
    )
596

597
    # Create and parse the constraints file.
598
    constraints1_filename = "constraints1.txt"
1✔
599
    rule_runner.set_options(
1✔
600
        [f"--python-requirement-constraints={constraints1_filename}"], env_inherit={"PATH"}
601
    )
602
    constraints1_strings = [str(c) for c in rule_runner.request(GlobalRequirementConstraints, [])]
1✔
603

604
    def get_pex_request(
1✔
605
        constraints_file: str | None,
606
        resolve_all_constraints: bool | None,
607
        *,
608
        _additional_args: Iterable[str] = (),
609
        _additional_lockfile_args: Iterable[str] = (),
610
    ) -> PexRequest:
611
        args = ["--backend-packages=pants.backend.python"]
1✔
612
        request = PexFromTargetsRequest(
1✔
613
            [Address("", target_name="app")],
614
            output_filename="demo.pex",
615
            internal_only=True,
616
            additional_args=_additional_args,
617
            additional_lockfile_args=_additional_lockfile_args,
618
        )
619
        if resolve_all_constraints is not None:
1✔
620
            args.append(f"--python-resolve-all-constraints={resolve_all_constraints!r}")
1✔
621
        if constraints_file:
1✔
622
            args.append(f"--python-requirement-constraints={constraints_file}")
1✔
623
        args.append("--python-repos-indexes=[]")
1✔
624
        args.append(f"--python-repos-repos={find_links}")
1✔
625
        rule_runner.set_options(args, env_inherit={"PATH"})
1✔
626
        pex_request = rule_runner.request(PexRequest, [request])
1✔
627
        assert OrderedSet(_additional_args).issubset(OrderedSet(pex_request.additional_args))
1✔
628
        return pex_request
1✔
629

630
    additional_args = ["--strip-pex-env"]
1✔
631
    additional_lockfile_args = ["--no-strip-pex-env"]
1✔
632

633
    pex_req1 = get_pex_request(constraints1_filename, resolve_all_constraints=False)
1✔
634
    assert isinstance(pex_req1.requirements, PexRequirements)
1✔
635
    assert pex_req1.requirements.constraints_strings == FrozenOrderedSet(constraints1_strings)
1✔
636
    req_strings_obj1 = rule_runner.request(PexRequirementsInfo, (pex_req1.requirements,))
1✔
637
    assert req_strings_obj1.req_strings == ("bar==5.5.5", "baz", "foo-bar>=0.1.2", url_req)
1✔
638

639
    pex_req2 = get_pex_request(
1✔
640
        constraints1_filename,
641
        resolve_all_constraints=True,
642
        _additional_args=additional_args,
643
        _additional_lockfile_args=additional_lockfile_args,
644
    )
645
    pex_req2_reqs = pex_req2.requirements
1✔
646
    assert isinstance(pex_req2_reqs, PexRequirements)
1✔
647
    req_strings_obj2 = rule_runner.request(PexRequirementsInfo, (pex_req2_reqs,))
1✔
648
    assert req_strings_obj2.req_strings == ("bar==5.5.5", "baz", "foo-bar>=0.1.2", url_req)
1✔
649
    assert isinstance(pex_req2_reqs.from_superset, Pex)
1✔
650
    repository_pex = pex_req2_reqs.from_superset
1✔
651
    assert not get_all_data(rule_runner, repository_pex).info["strip_pex_env"]
1✔
652
    assert ["Foo._-BAR==1.0.0", "bar==5.5.5", "baz==2.2.2", url_req, "qux==3.4.5"] == requirements(
1✔
653
        rule_runner, repository_pex
654
    )
655

UNCOV
656
    with engine_error(
×
657
        ValueError,
658
        contains=softwrap(
659
            """
660
            `[python].resolve_all_constraints` is enabled, so
661
            `[python].requirement_constraints` must also be set.
662
            """
663
        ),
664
    ):
UNCOV
665
        get_pex_request(None, resolve_all_constraints=True)
×
666

667
    # Shouldn't error, as we don't explicitly set --resolve-all-constraints.
UNCOV
668
    get_pex_request(None, resolve_all_constraints=None)
×
669

670

671
def test_pants_requirement(rule_runner: PythonRuleRunner) -> None:
1✔
672
    rule_runner.write_files(
1✔
673
        {
674
            "app.py": "",
675
            "BUILD": dedent(
676
                """
677
                pants_requirements(name="pants")
678
                python_source(name="app", source="app.py", dependencies=[":pants"])
679
                """
680
            ),
681
        }
682
    )
683
    args = [
1✔
684
        "--backend-packages=pants.backend.python",
685
        "--backend-packages=pants.backend.plugin_development",
686
        "--python-repos-indexes=[]",
687
    ]
688
    request = PexFromTargetsRequest(
1✔
689
        [Address("", target_name="app")],
690
        output_filename="demo.pex",
691
        internal_only=False,
692
    )
693
    rule_runner.set_options(args, env_inherit={"PATH"})
1✔
694
    pex_req = rule_runner.request(PexRequest, [request])
1✔
695
    pex_reqs_info = rule_runner.request(PexRequirementsInfo, [pex_req.requirements])
1✔
696
    assert pex_reqs_info.find_links == ("https://wheels.pantsbuild.org/simple",)
1✔
697

698

699
@pytest.mark.parametrize("include_requirements", [False, True])
1✔
700
def test_exclude_requirements(
1✔
701
    include_requirements: bool, tmp_path: Path, rule_runner: PythonRuleRunner
702
) -> None:
703
    sdists = tmp_path / "sdists"
1✔
704
    sdists.mkdir()
1✔
705
    find_links = create_dists(sdists, rule_runner, Project("baz", "2.2.2"))
1✔
706

707
    rule_runner.write_files(
1✔
708
        {
709
            "BUILD": dedent(
710
                """
711
                python_requirement(name="baz", requirements=["foo==1.2.3"])
712
                python_sources(name="app", sources=["app.py"], dependencies=[":baz"])
713
                """
714
            ),
715
            "constraints.txt": dedent("foo==1.2.3"),
716
            "app.py": "",
717
        }
718
    )
719

720
    rule_runner.set_options(
1✔
721
        [
722
            "--backend-packages=pants.backend.python",
723
            "--python-repos-indexes=[]",
724
            f"--python-repos-repos={find_links}",
725
        ],
726
        env_inherit={"PATH"},
727
    )
728

729
    request = PexFromTargetsRequest(
1✔
730
        [Address("", target_name="app")],
731
        output_filename="demo.pex",
732
        internal_only=True,
733
        include_requirements=include_requirements,
734
    )
735
    pex_request = rule_runner.request(PexRequest, [request])
1✔
736
    assert isinstance(pex_request.requirements, PexRequirements)
1✔
737
    assert len(pex_request.requirements.req_strings_or_addrs) == (1 if include_requirements else 0)
1✔
738

739

740
@pytest.mark.parametrize("include_sources", [False, True])
1✔
741
def test_exclude_sources(include_sources: bool, rule_runner: PythonRuleRunner) -> None:
1✔
742
    rule_runner.write_files(
1✔
743
        {
744
            "BUILD": dedent(
745
                """
746
                python_sources(name="app", sources=["app.py"])
747
                """
748
            ),
749
            "app.py": "",
750
        }
751
    )
752

753
    rule_runner.set_options(
1✔
754
        [
755
            "--backend-packages=pants.backend.python",
756
            "--python-repos-indexes=[]",
757
        ],
758
        env_inherit={"PATH"},
759
    )
760

761
    request = PexFromTargetsRequest(
1✔
762
        [Address("", target_name="app")],
763
        output_filename="demo.pex",
764
        internal_only=True,
765
        include_source_files=include_sources,
766
    )
767
    pex_request = rule_runner.request(PexRequest, [request])
1✔
768
    snapshot = rule_runner.request(Snapshot, [pex_request.sources])
1✔
769
    assert len(snapshot.files) == (1 if include_sources else 0)
1✔
770

771

772
def test_include_sources_without_transitive_package_sources(rule_runner: PythonRuleRunner) -> None:
1✔
773
    rule_runner.write_files(
1✔
774
        {
775
            "src/app/BUILD": dedent(
776
                """
777
                python_sources(
778
                    name="app",
779
                    sources=["app.py"],
780
                    dependencies=["//src/dep:pkg"],
781
                )
782
                """
783
            ),
784
            "src/app/app.py": "",
785
            "src/dep/BUILD": dedent(
786
                # This test requires a package that has a standard dependencies field.
787
                # 'pex_binary' has a dependencies field; 'archive' does not.
788
                """
789
                pex_binary(name="pkg", dependencies=[":dep"])
790
                python_sources(name="dep", sources=["dep.py"])
791
                """
792
            ),
793
            "src/dep/dep.py": "",
794
        }
795
    )
796

797
    rule_runner.set_options(
1✔
798
        [
799
            "--backend-packages=pants.backend.python",
800
            "--python-repos-indexes=[]",
801
        ],
802
        env_inherit={"PATH"},
803
    )
804

805
    request = PexFromTargetsRequest(
1✔
806
        [Address("src/app", target_name="app")],
807
        output_filename="demo.pex",
808
        internal_only=True,
809
        include_source_files=True,
810
    )
811
    pex_request = rule_runner.request(PexRequest, [request])
1✔
812
    snapshot = rule_runner.request(Snapshot, [pex_request.sources])
1✔
813

814
    # the packaged transitive dep is excluded
815
    assert snapshot.files == ("app/app.py",)
1✔
816

817

818
@pytest.mark.parametrize("enable_resolves", [False, True])
1✔
819
def test_cross_platform_pex_disables_subsetting(
1✔
820
    rule_runner: PythonRuleRunner, enable_resolves: bool
821
) -> None:
822
    # See https://github.com/pantsbuild/pants/issues/12222.
823
    lockfile = "3rdparty/python/default.lock"
1✔
824
    constraints = ["foo==1.0", "bar==1.0"]
1✔
825
    rule_runner.write_files(
1✔
826
        {
827
            lockfile: "\n".join(constraints),
828
            "a.py": "",
829
            "BUILD": dedent(
830
                """
831
                python_requirement(name="foo",requirements=["foo"])
832
                python_requirement(name="bar",requirements=["bar"])
833
                python_sources(name="lib",dependencies=[":foo"])
834
                """
835
            ),
836
        }
837
    )
838

839
    if enable_resolves:
1✔
840
        options = [
1✔
841
            "--python-enable-resolves",
842
            # NB: Because this is a synthetic lockfile without a header.
843
            "--python-invalid-lockfile-behavior=ignore",
844
        ]
845
    else:
846
        options = [
1✔
847
            f"--python-requirement-constraints={lockfile}",
848
            "--python-resolve-all-constraints",
849
        ]
850
    rule_runner.set_options(options, env_inherit={"PATH"})
1✔
851

852
    request = PexFromTargetsRequest(
1✔
853
        [Address("", target_name="lib")],
854
        output_filename="demo.pex",
855
        internal_only=False,
856
        platforms=PexPlatforms(["some-platform-x86_64"]),
857
    )
858
    result = rule_runner.request(PexRequest, [request])
1✔
859

860
    assert result.requirements == PexRequirements(
1✔
861
        request.addresses,
862
        constraints_strings=constraints,
863
        description_of_origin="//:lib",
864
    )
865

866

867
class ResolveMode(Enum):
1✔
868
    resolve_all_constraints = "resolve_all_constraints"
1✔
869
    poetry_or_manual = "poetry_or_manual"
1✔
870
    pex = "pex"
1✔
871

872

873
@pytest.mark.parametrize(
1✔
874
    "mode,internal_only,run_against_entire_lockfile",
875
    [(m, io, rael) for m in ResolveMode for io in [True, False] for rael in [True, False]],
876
)
877
def test_lockfile_requirements_selection(
1✔
878
    rule_runner: PythonRuleRunner,
879
    mode: ResolveMode,
880
    internal_only: bool,
881
    run_against_entire_lockfile: bool,
882
) -> None:
883
    mode_files: dict[str, str | bytes] = {
1✔
884
        "a.py": "",
885
        "BUILD": dedent(
886
            """
887
                python_sources(name="lib", dependencies=[":setuptools"])
888
                python_requirement(name="setuptools", requirements=["setuptools"])
889
                """
890
        ),
891
    }
892
    if mode == ResolveMode.resolve_all_constraints:
1✔
893
        mode_files.update({"constraints.txt": "setuptools==54.1.2"})
1✔
894
    elif mode == ResolveMode.poetry_or_manual:
1✔
895
        mode_files.update({"3rdparty/python/default.lock": setuptools_poetry_lockfile})
1✔
896
    else:
897
        assert mode == ResolveMode.pex
1✔
898
        lock_content = (
1✔
899
            importlib.resources.files("pants.backend.python.subsystems") / "setuptools.lock"
900
        ).read_bytes()
901
        mode_files.update({"3rdparty/python/default.lock": lock_content})
1✔
902

903
    rule_runner.write_files(mode_files)
1✔
904

905
    if mode == ResolveMode.resolve_all_constraints:
1✔
906
        options = [
1✔
907
            "--python-requirement-constraints=constraints.txt",
908
        ]
909
    else:
910
        # NB: It doesn't matter what the lockfile generator is set to: only what is actually on disk.
911
        options = [
1✔
912
            "--python-enable-resolves",
913
            "--python-default-resolve=myresolve",
914
            "--python-resolves={'myresolve':'3rdparty/python/default.lock'}",
915
        ]
916

917
    if run_against_entire_lockfile:
1✔
918
        options.append("--python-run-against-entire-lockfile")
1✔
919

920
    request = PexFromTargetsRequest(
1✔
921
        [Address("", target_name="lib")],
922
        output_filename="demo.pex",
923
        internal_only=internal_only,
924
        main=EntryPoint("a"),
925
    )
926
    rule_runner.set_options(options, env_inherit={"PATH"})
1✔
927
    result = rule_runner.request(PexRequest, [request])
1✔
928
    assert result.layout == (PexLayout.PACKED if internal_only else PexLayout.ZIPAPP)
1✔
929
    assert result.main == EntryPoint("a")
1✔
930

931
    if run_against_entire_lockfile and internal_only:
1✔
932
        # With `run_against_entire_lockfile`, all internal requests result in the full set
933
        # of requirements, but that is encoded slightly differently per mode.
934
        if mode == ResolveMode.resolve_all_constraints:
1✔
935
            # NB: The use of the legacy constraints file with `resolve_all_constraints` requires parsing
936
            # and manipulation of the constraints, and needs to include transitive deps (unlike other
937
            # lockfile requests). So it is emitted as `PexRequirements` rather than EntireLockfile.
938
            assert isinstance(result.requirements, PexRequirements)
1✔
939
            assert not result.requirements.from_superset
1✔
940
        else:
941
            assert mode in (ResolveMode.poetry_or_manual, ResolveMode.pex)
1✔
942
            assert isinstance(result.requirements, EntireLockfile)
1✔
943
    else:
944
        assert isinstance(result.requirements, PexRequirements)
1✔
945
        if mode in (ResolveMode.resolve_all_constraints, ResolveMode.poetry_or_manual):
1✔
946
            assert isinstance(result.requirements.from_superset, Pex)
1✔
947
            assert not get_all_data(rule_runner, result.requirements.from_superset).is_zipapp
1✔
948
        else:
949
            assert mode == ResolveMode.pex
1✔
950
            assert isinstance(result.requirements.from_superset, Resolve)
1✔
951
            assert result.requirements.from_superset.name == "myresolve"
1✔
952

953

954
def test_warn_about_files_targets(rule_runner: PythonRuleRunner, caplog) -> None:
1✔
955
    rule_runner.write_files(
1✔
956
        {
957
            "app.py": "",
958
            "file.txt": "",
959
            "resource.txt": "",
960
            "BUILD": dedent(
961
                """
962
                file(name="file_target", source="file.txt")
963
                resource(name="resource_target", source="resource.txt")
964
                python_sources(name="app", dependencies=[":file_target", ":resource_target"])
965
                """
966
            ),
967
        }
968
    )
969

970
    rule_runner.request(
1✔
971
        PexRequest,
972
        [
973
            PexFromTargetsRequest(
974
                [Address("", target_name="app")],
975
                output_filename="app.pex",
976
                internal_only=True,
977
                warn_for_transitive_files_targets=True,
978
            )
979
        ],
980
    )
981

982
    assert "The target //:app (`python_source`) transitively depends on" in caplog.text
1✔
983
    # files are not fine:
984
    assert "//:file_target" in caplog.text
1✔
985
    # resources are fine:
986
    assert "resource_target" not in caplog.text
1✔
987
    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