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

pantsbuild / pants / 19068377358

04 Nov 2025 12:18PM UTC coverage: 92.46% (+12.2%) from 80.3%
19068377358

Pull #22816

github

web-flow
Merge a242f1805 into 89462b7ef
Pull Request #22816: Update Pants internal Python to 3.14

13 of 14 new or added lines in 12 files covered. (92.86%)

244 existing lines in 13 files now uncovered.

89544 of 96846 relevant lines covered (92.46%)

3.72 hits per line

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

74.56
/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 subprocess
1✔
8
import sys
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_from_targets import (
1✔
44
    ChosenPythonResolve,
45
    ChosenPythonResolveRequest,
46
    GlobalRequirementConstraints,
47
    PexFromTargetsRequest,
48
    _determine_requirements_for_pex_from_targets,
49
)
50
from pants.backend.python.util_rules.pex_requirements import (
1✔
51
    EntireLockfile,
52
    PexRequirements,
53
    Resolve,
54
)
55
from pants.backend.python.util_rules.pex_test_utils import get_all_data
1✔
56
from pants.build_graph.address import Address
1✔
57
from pants.core.goals.generate_lockfiles import NoCompatibleResolveException
1✔
58
from pants.core.target_types import FileTarget, ResourceTarget
1✔
59
from pants.engine.addresses import Addresses
1✔
60
from pants.engine.fs import Snapshot
1✔
61
from pants.testutil.option_util import create_subsystem
1✔
62
from pants.testutil.python_rule_runner import PythonRuleRunner
1✔
63
from pants.testutil.rule_runner import QueryRule, engine_error, run_rule_with_mocks
1✔
64
from pants.util.contextutil import pushd
1✔
65
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
1✔
66
from pants.util.strutil import softwrap
1✔
67

68

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

96

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

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

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

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

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

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

159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

438

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

444

445
build_deps = ["setuptools==54.1.2", "wheel==0.36.2"]
1✔
446

447

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

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

470

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

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

495

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

499
    pex = workdir / "pex"
1✔
500
    subprocess.run(
1✔
501
        args=[
502
            sys.executable,
503
            "-m",
504
            "pex",
505
            *project_dirs,
506
            *build_deps,
507
            "--include-tools",
508
            "-o",
509
            pex,
510
        ],
511
        check=True,
512
    )
513

UNCOV
514
    find_links = workdir / "find-links"
×
UNCOV
515
    subprocess.run(
×
516
        args=[
517
            sys.executable,
518
            "-m",
519
            "pex.tools",
520
            pex,
521
            "repository",
522
            "extract",
523
            "--find-links",
524
            find_links,
525
        ],
526
        check=True,
527
    )
UNCOV
528
    return find_links
×
529

530

531
def requirements(rule_runner: PythonRuleRunner, pex: Pex) -> list[str]:
1✔
UNCOV
532
    return cast(list[str], get_all_data(rule_runner, pex).info["requirements"])
×
533

534

535
def test_constraints_validation(tmp_path: Path, rule_runner: PythonRuleRunner) -> None:
1✔
536
    sdists = tmp_path / "sdists"
1✔
537
    sdists.mkdir()
1✔
538
    find_links = create_dists(
1✔
539
        sdists,
540
        Project("Foo-Bar", "1.0.0"),
541
        Project("Bar", "5.5.5"),
542
        Project("baz", "2.2.2"),
543
        Project("QUX", "3.4.5"),
544
    )
545

546
    # Turn the project dir into a git repo, so it can be cloned.
UNCOV
547
    gitdir = tmp_path / "git"
×
UNCOV
548
    gitdir.mkdir()
×
UNCOV
549
    foorl_dir = create_project_dir(gitdir, Project("foorl", "9.8.7"))
×
UNCOV
550
    with pushd(str(foorl_dir)):
×
UNCOV
551
        subprocess.check_call(["git", "init"])
×
UNCOV
552
        subprocess.check_call(["git", "config", "user.name", "dummy"])
×
UNCOV
553
        subprocess.check_call(["git", "config", "user.email", "dummy@dummy.com"])
×
UNCOV
554
        subprocess.check_call(["git", "add", "--all"])
×
UNCOV
555
        subprocess.check_call(["git", "commit", "-m", "initial commit"])
×
UNCOV
556
        subprocess.check_call(["git", "branch", "9.8.7"])
×
557

558
    # This string won't parse as a Requirement if it doesn't contain a netloc,
559
    # so we explicitly mention localhost.
UNCOV
560
    url_req = f"foorl@ git+file://localhost{foorl_dir.as_posix()}@9.8.7"
×
561

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

591
    # Create and parse the constraints file.
UNCOV
592
    constraints1_filename = "constraints1.txt"
×
UNCOV
593
    rule_runner.set_options(
×
594
        [f"--python-requirement-constraints={constraints1_filename}"], env_inherit={"PATH"}
595
    )
UNCOV
596
    constraints1_strings = [str(c) for c in rule_runner.request(GlobalRequirementConstraints, [])]
×
597

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

UNCOV
624
    additional_args = ["--strip-pex-env"]
×
UNCOV
625
    additional_lockfile_args = ["--no-strip-pex-env"]
×
626

UNCOV
627
    pex_req1 = get_pex_request(constraints1_filename, resolve_all_constraints=False)
×
UNCOV
628
    assert isinstance(pex_req1.requirements, PexRequirements)
×
UNCOV
629
    assert pex_req1.requirements.constraints_strings == FrozenOrderedSet(constraints1_strings)
×
UNCOV
630
    req_strings_obj1 = rule_runner.request(PexRequirementsInfo, (pex_req1.requirements,))
×
UNCOV
631
    assert req_strings_obj1.req_strings == ("bar==5.5.5", "baz", "foo-bar>=0.1.2", url_req)
×
632

UNCOV
633
    pex_req2 = get_pex_request(
×
634
        constraints1_filename,
635
        resolve_all_constraints=True,
636
        _additional_args=additional_args,
637
        _additional_lockfile_args=additional_lockfile_args,
638
    )
UNCOV
639
    pex_req2_reqs = pex_req2.requirements
×
UNCOV
640
    assert isinstance(pex_req2_reqs, PexRequirements)
×
UNCOV
641
    req_strings_obj2 = rule_runner.request(PexRequirementsInfo, (pex_req2_reqs,))
×
UNCOV
642
    assert req_strings_obj2.req_strings == ("bar==5.5.5", "baz", "foo-bar>=0.1.2", url_req)
×
UNCOV
643
    assert isinstance(pex_req2_reqs.from_superset, Pex)
×
UNCOV
644
    repository_pex = pex_req2_reqs.from_superset
×
UNCOV
645
    assert not get_all_data(rule_runner, repository_pex).info["strip_pex_env"]
×
UNCOV
646
    assert ["Foo._-BAR==1.0.0", "bar==5.5.5", "baz==2.2.2", url_req, "qux==3.4.5"] == requirements(
×
647
        rule_runner, repository_pex
648
    )
649

UNCOV
650
    with engine_error(
×
651
        ValueError,
652
        contains=softwrap(
653
            """
654
            `[python].resolve_all_constraints` is enabled, so
655
            `[python].requirement_constraints` must also be set.
656
            """
657
        ),
658
    ):
UNCOV
659
        get_pex_request(None, resolve_all_constraints=True)
×
660

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

664

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

692

693
@pytest.mark.parametrize("include_requirements", [False, True])
1✔
694
def test_exclude_requirements(
1✔
695
    include_requirements: bool, tmp_path: Path, rule_runner: PythonRuleRunner
696
) -> None:
697
    sdists = tmp_path / "sdists"
1✔
698
    sdists.mkdir()
1✔
699
    find_links = create_dists(sdists, Project("baz", "2.2.2"))
1✔
700

UNCOV
701
    rule_runner.write_files(
×
702
        {
703
            "BUILD": dedent(
704
                """
705
                python_requirement(name="baz", requirements=["foo==1.2.3"])
706
                python_sources(name="app", sources=["app.py"], dependencies=[":baz"])
707
                """
708
            ),
709
            "constraints.txt": dedent("foo==1.2.3"),
710
            "app.py": "",
711
        }
712
    )
713

UNCOV
714
    rule_runner.set_options(
×
715
        [
716
            "--backend-packages=pants.backend.python",
717
            "--python-repos-indexes=[]",
718
            f"--python-repos-repos={find_links}",
719
        ],
720
        env_inherit={"PATH"},
721
    )
722

UNCOV
723
    request = PexFromTargetsRequest(
×
724
        [Address("", target_name="app")],
725
        output_filename="demo.pex",
726
        internal_only=True,
727
        include_requirements=include_requirements,
728
    )
UNCOV
729
    pex_request = rule_runner.request(PexRequest, [request])
×
UNCOV
730
    assert isinstance(pex_request.requirements, PexRequirements)
×
UNCOV
731
    assert len(pex_request.requirements.req_strings_or_addrs) == (1 if include_requirements else 0)
×
732

733

734
@pytest.mark.parametrize("include_sources", [False, True])
1✔
735
def test_exclude_sources(include_sources: bool, rule_runner: PythonRuleRunner) -> None:
1✔
736
    rule_runner.write_files(
1✔
737
        {
738
            "BUILD": dedent(
739
                """
740
                python_sources(name="app", sources=["app.py"])
741
                """
742
            ),
743
            "app.py": "",
744
        }
745
    )
746

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

755
    request = PexFromTargetsRequest(
1✔
756
        [Address("", target_name="app")],
757
        output_filename="demo.pex",
758
        internal_only=True,
759
        include_source_files=include_sources,
760
    )
761
    pex_request = rule_runner.request(PexRequest, [request])
1✔
762
    snapshot = rule_runner.request(Snapshot, [pex_request.sources])
1✔
763
    assert len(snapshot.files) == (1 if include_sources else 0)
1✔
764

765

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

791
    rule_runner.set_options(
1✔
792
        [
793
            "--backend-packages=pants.backend.python",
794
            "--python-repos-indexes=[]",
795
        ],
796
        env_inherit={"PATH"},
797
    )
798

799
    request = PexFromTargetsRequest(
1✔
800
        [Address("src/app", target_name="app")],
801
        output_filename="demo.pex",
802
        internal_only=True,
803
        include_source_files=True,
804
    )
805
    pex_request = rule_runner.request(PexRequest, [request])
1✔
806
    snapshot = rule_runner.request(Snapshot, [pex_request.sources])
1✔
807

808
    # the packaged transitive dep is excluded
809
    assert snapshot.files == ("app/app.py",)
1✔
810

811

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

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

846
    request = PexFromTargetsRequest(
1✔
847
        [Address("", target_name="lib")],
848
        output_filename="demo.pex",
849
        internal_only=False,
850
        platforms=PexPlatforms(["some-platform-x86_64"]),
851
    )
852
    result = rule_runner.request(PexRequest, [request])
1✔
853

854
    assert result.requirements == PexRequirements(
1✔
855
        request.addresses,
856
        constraints_strings=constraints,
857
        description_of_origin="//:lib",
858
    )
859

860

861
class ResolveMode(Enum):
1✔
862
    resolve_all_constraints = "resolve_all_constraints"
1✔
863
    poetry_or_manual = "poetry_or_manual"
1✔
864
    pex = "pex"
1✔
865

866

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

897
    rule_runner.write_files(mode_files)
1✔
898

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

911
    if run_against_entire_lockfile:
1✔
912
        options.append("--python-run-against-entire-lockfile")
1✔
913

914
    request = PexFromTargetsRequest(
1✔
915
        [Address("", target_name="lib")],
916
        output_filename="demo.pex",
917
        internal_only=internal_only,
918
        main=EntryPoint("a"),
919
    )
920
    rule_runner.set_options(options, env_inherit={"PATH"})
1✔
921
    result = rule_runner.request(PexRequest, [request])
1✔
922
    assert result.layout == (PexLayout.PACKED if internal_only else PexLayout.ZIPAPP)
1✔
923
    assert result.main == EntryPoint("a")
1✔
924

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

947

948
def test_warn_about_files_targets(rule_runner: PythonRuleRunner, caplog) -> None:
1✔
949
    rule_runner.write_files(
1✔
950
        {
951
            "app.py": "",
952
            "file.txt": "",
953
            "resource.txt": "",
954
            "BUILD": dedent(
955
                """
956
                file(name="file_target", source="file.txt")
957
                resource(name="resource_target", source="resource.txt")
958
                python_sources(name="app", dependencies=[":file_target", ":resource_target"])
959
                """
960
            ),
961
        }
962
    )
963

964
    rule_runner.request(
1✔
965
        PexRequest,
966
        [
967
            PexFromTargetsRequest(
968
                [Address("", target_name="app")],
969
                output_filename="app.pex",
970
                internal_only=True,
971
                warn_for_transitive_files_targets=True,
972
            )
973
        ],
974
    )
975

976
    assert "The target //:app (`python_source`) transitively depends on" in caplog.text
1✔
977
    # files are not fine:
978
    assert "//:file_target" in caplog.text
1✔
979
    # resources are fine:
980
    assert "resource_target" not in caplog.text
1✔
981
    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

© 2025 Coveralls, Inc