• 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

77.78
/src/python/pants/backend/python/goals/export_integration_test.py
1
# Copyright 2022 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 os
1✔
7
import platform
1✔
8
import re
1✔
9
import shutil
1✔
10
from collections.abc import Mapping, MutableMapping
1✔
11
from textwrap import dedent
1✔
12

13
import pytest
1✔
14

15
from pants.backend.python.goals.export import PythonResolveExportFormat
1✔
16
from pants.testutil.pants_integration_test import run_pants, setup_tmpdir
1✔
17
from pants.util.contextutil import temporary_dir
1✔
18

19
SOURCES = {
1✔
20
    "3rdparty/BUILD": dedent(
21
        """\
22
        python_requirement(name='req1', requirements=['ansicolors==1.1.8'], resolve='a', modules=['colors'])
23
        python_requirement(name='req2', requirements=['ansicolors==1.0.2'], resolve='b', modules=['colors'])
24
        python_requirement(name='req3', requirements=['wheel'], resolve=parametrize('a', 'b'))
25
        """
26
    ),
27
    "src/python/foo.py": "from colors import *",
28
    "src/python/BUILD": dedent(
29
        """\
30
        python_source(name='foo', source='foo.py', resolve=parametrize('a', 'b'))
31
        python_distribution(
32
            name='dist',
33
            provides=python_artifact(name='foo-dist', version='1.2.3'),
34
            dependencies=[':foo@resolve=a'],
35
        )
36
        """
37
    ),
38
}
39

40

41
def build_config(
1✔
42
    tmpdir: str, py_resolve_format: PythonResolveExportFormat, py_hermetic_scripts: bool = True
43
) -> Mapping:
44
    cfg: MutableMapping = {
1✔
45
        "GLOBAL": {
46
            "backend_packages": ["pants.backend.python"],
47
        },
48
        "python": {
49
            "enable_resolves": True,
50
            "interpreter_constraints": [f"=={platform.python_version()}"],
51
            "resolves": {
52
                "a": f"{tmpdir}/3rdparty/a.lock",
53
                "b": f"{tmpdir}/3rdparty/b.lock",
54
            },
55
        },
56
        "export": {
57
            "py_resolve_format": py_resolve_format.value,
58
            "py_non_hermetic_scripts_in_resolve": [] if py_hermetic_scripts else ["a", "b"],
59
        },
60
    }
61

62
    return cfg
1✔
63

64

65
@pytest.mark.parametrize(
1✔
66
    "py_resolve_format,py_hermetic_scripts",
67
    [
68
        (PythonResolveExportFormat.mutable_virtualenv, True),
69
        (PythonResolveExportFormat.mutable_virtualenv, False),
70
        (PythonResolveExportFormat.symlinked_immutable_virtualenv, True),
71
    ],
72
)
73
def test_export(py_resolve_format: PythonResolveExportFormat, py_hermetic_scripts: bool) -> None:
1✔
74
    with setup_tmpdir(SOURCES) as tmpdir:
1✔
75
        resolve_names = ["a", "b"]
1✔
76
        run_pants(
1✔
77
            [
78
                "--print-stacktrace",
79
                "generate-lockfiles",
80
                "export",
81
                *(f"--resolve={name}" for name in resolve_names),
82
                "--export-py-editable-in-resolve=['a']",
83
            ],
84
            config=build_config(tmpdir, py_resolve_format, py_hermetic_scripts),
85
        ).assert_success()
86

87
    export_prefix = os.path.join("dist", "export", "python", "virtualenvs")
1✔
88
    assert os.path.isdir(export_prefix), (
1✔
89
        f"expected export prefix dir '{export_prefix}' does not exist"
90
    )
91
    py_minor_version = f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}"
1✔
92
    for resolve, ansicolors_version in [("a", "1.1.8"), ("b", "1.0.2")]:
1✔
93
        export_resolve_dir = os.path.join(export_prefix, resolve)
1✔
94
        assert os.path.isdir(export_resolve_dir), (
1✔
95
            f"expected export resolve dir '{export_resolve_dir}' does not exist"
96
        )
97

98
        export_dir = os.path.join(export_resolve_dir, platform.python_version())
1✔
99
        assert os.path.isdir(export_dir), f"expected export dir '{export_dir}' does not exist"
1✔
100
        if py_resolve_format == PythonResolveExportFormat.symlinked_immutable_virtualenv:
1✔
101
            assert os.path.islink(export_dir), (
1✔
102
                f"expected export dir '{export_dir}' is not a symlink"
103
            )
104

105
        lib_dir = os.path.join(export_dir, "lib", f"python{py_minor_version}", "site-packages")
1✔
106
        assert os.path.isdir(lib_dir), f"expected export lib dir '{lib_dir}' does not exist"
1✔
107
        expected_ansicolors_dir = os.path.join(
1✔
108
            lib_dir, f"ansicolors-{ansicolors_version}.dist-info"
109
        )
110
        assert os.path.isdir(expected_ansicolors_dir), (
1✔
111
            f"expected dist-info for ansicolors '{expected_ansicolors_dir}' does not exist"
112
        )
113

114
        if py_resolve_format == PythonResolveExportFormat.mutable_virtualenv:
1✔
115
            activate_path = os.path.join(export_dir, "bin", "activate")
1✔
116
            assert os.path.isfile(activate_path), "virtualenv's bin/activate is missing"
1✔
117
            with open(activate_path) as activate_file:
1✔
118
                activate_content = activate_file.read()
1✔
119

120
            prompt_re = re.compile(rf"""PS1=('|")\({resolve}/{platform.python_version()}\) """)
1✔
121
            assert prompt_re.search(activate_content) is not None, (
1✔
122
                "Expected PS1 prompt not defined in bin/activate."
123
            )
124

UNCOV
125
            script_path = os.path.join(export_dir, "bin", "wheel")
×
UNCOV
126
            assert os.path.isfile(script_path), (
×
127
                "expected wheel to be installed, but bin/wheel is missing"
128
            )
UNCOV
129
            with open(script_path) as script_file:
×
UNCOV
130
                shebang = script_file.readline().strip()
×
UNCOV
131
            if py_hermetic_scripts:
×
UNCOV
132
                assert shebang.endswith(" -sE")
×
133
            else:
UNCOV
134
                assert not shebang.endswith(" -sE")
×
135

UNCOV
136
            expected_foo_dir = os.path.join(lib_dir, "foo_dist-1.2.3.dist-info")
×
UNCOV
137
            if resolve == "b":
×
UNCOV
138
                assert not os.path.isdir(expected_foo_dir), (
×
139
                    f"unexpected dist-info for foo-dist '{expected_foo_dir}' exists"
140
                )
UNCOV
141
            elif resolve == "a":
×
142
                # make sure the editable wheel for the python_distribution is installed
UNCOV
143
                assert os.path.isdir(expected_foo_dir), (
×
144
                    f"expected dist-info for foo-dist '{expected_foo_dir}' does not exist"
145
                )
146
                # direct_url__pants__.json should be moved to direct_url.json
UNCOV
147
                expected_foo_direct_url_pants = os.path.join(
×
148
                    expected_foo_dir, "direct_url__pants__.json"
149
                )
UNCOV
150
                assert not os.path.isfile(expected_foo_direct_url_pants), (
×
151
                    f"expected direct_url__pants__.json for foo-dist '{expected_foo_direct_url_pants}' was not removed"
152
                )
UNCOV
153
                expected_foo_direct_url = os.path.join(expected_foo_dir, "direct_url.json")
×
UNCOV
154
                assert os.path.isfile(expected_foo_direct_url), (
×
155
                    f"expected direct_url.json for foo-dist '{expected_foo_direct_url}' does not exist"
156
                )
157

158

159
def test_symlinked_venv_resilience() -> None:
1✔
160
    with temporary_dir() as named_caches:
1✔
161
        pex_root = os.path.join(os.path.realpath(named_caches), "pex_root")
1✔
162
        with setup_tmpdir(SOURCES) as tmpdir:
1✔
163
            run_pants(
1✔
164
                [
165
                    f"--named-caches-dir={named_caches}",
166
                    "generate-lockfiles",
167
                    "export",
168
                    "--resolve=a",
169
                ],
170
                config=build_config(
171
                    tmpdir, PythonResolveExportFormat.symlinked_immutable_virtualenv
172
                ),
173
            ).assert_success()
174

175
            def check():
1✔
176
                export_dir = os.path.join(
1✔
177
                    "dist", "export", "python", "virtualenvs", "a", platform.python_version()
178
                )
179
                assert os.path.islink(export_dir)
1✔
180
                export_dir_tgt = os.readlink(export_dir)
1✔
181
                assert os.path.isdir(export_dir_tgt)
1✔
182
                assert os.path.commonpath([pex_root, export_dir_tgt]) == pex_root
1✔
183

184
            check()
1✔
185

186
            shutil.rmtree(pex_root)
1✔
187

188
            run_pants(
1✔
189
                [f"--named-caches-dir={named_caches}", "export", "--resolve=a"],
190
                config=build_config(
191
                    tmpdir, PythonResolveExportFormat.symlinked_immutable_virtualenv
192
                ),
193
            ).assert_success()
194

195
            check()
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