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

pantsbuild / pants / 26689585807

30 May 2026 04:55PM UTC coverage: 92.742% (-0.05%) from 92.792%
26689585807

Pull #23343

github

web-flow
Merge c42efe377 into c8127c1f4
Pull Request #23343: Add buf as an alternate Python protobuf code generator

767 of 807 new or added lines in 17 files covered. (95.04%)

69 existing lines in 3 files now uncovered.

93753 of 101090 relevant lines covered (92.74%)

4.01 hits per line

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

94.23
/src/python/pants/backend/python/goals/run_python_source_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 json
1✔
7
import os
1✔
8
from textwrap import dedent
1✔
9

10
import pytest
1✔
11

12
from pants.backend.codegen.protobuf.python import additional_fields as protobuf_additional_fields
1✔
13
from pants.backend.codegen.protobuf.python.python_protobuf_module_mapper import (
1✔
14
    rules as protobuf_module_mapper_rules,
15
)
16
from pants.backend.codegen.protobuf.python.python_protobuf_subsystem import (
1✔
17
    rules as protobuf_subsystem_rules,
18
)
19
from pants.backend.codegen.protobuf.python.rules import rules as protobuf_python_rules
1✔
20
from pants.backend.codegen.protobuf.target_types import ProtobufSourcesGeneratorTarget
1✔
21
from pants.backend.codegen.protobuf.target_types import rules as protobuf_target_types_rules
1✔
22
from pants.backend.python import target_types_rules
1✔
23
from pants.backend.python.dependency_inference import rules as dependency_inference_rules
1✔
24
from pants.backend.python.goals import package_dists
1✔
25
from pants.backend.python.goals.run_python_source import PythonSourceFieldSet
1✔
26
from pants.backend.python.goals.run_python_source import rules as run_rules
1✔
27
from pants.backend.python.macros.python_artifact import PythonArtifact
1✔
28
from pants.backend.python.target_types import (
1✔
29
    PythonDistribution,
30
    PythonRequirementTarget,
31
    PythonSourcesGeneratorTarget,
32
)
33
from pants.backend.python.util_rules import local_dists, pex_from_targets
1✔
34
from pants.build_graph.address import Address
1✔
35
from pants.core.goals.run import RunDebugAdapterRequest, RunRequest
1✔
36
from pants.engine.process import InteractiveProcess
1✔
37
from pants.engine.rules import QueryRule
1✔
38
from pants.engine.target import Target
1✔
39
from pants.testutil.debug_adapter_util import debugadapter_port_for_testing
1✔
40
from pants.testutil.pants_integration_test import run_pants
1✔
41
from pants.testutil.python_rule_runner import PythonRuleRunner
1✔
42
from pants.testutil.rule_runner import mock_console
1✔
43

44

45
@pytest.fixture
1✔
46
def rule_runner() -> PythonRuleRunner:
1✔
47
    return PythonRuleRunner(
1✔
48
        rules=[
49
            *run_rules(),
50
            *dependency_inference_rules.rules(),
51
            *target_types_rules.rules(),
52
            *local_dists.rules(),
53
            *pex_from_targets.rules(),
54
            *package_dists.rules(),
55
            *protobuf_subsystem_rules(),
56
            *protobuf_target_types_rules(),
57
            *protobuf_python_rules(),
58
            *protobuf_additional_fields.rules(),
59
            *protobuf_module_mapper_rules(),
60
            QueryRule(RunRequest, (PythonSourceFieldSet,)),
61
            QueryRule(RunDebugAdapterRequest, (PythonSourceFieldSet,)),
62
        ],
63
        target_types=[
64
            ProtobufSourcesGeneratorTarget,
65
            PythonSourcesGeneratorTarget,
66
            PythonRequirementTarget,
67
            PythonDistribution,
68
        ],
69
        objects={"python_artifact": PythonArtifact},
70
    )
71

72

73
def run_run_request(
1✔
74
    rule_runner: PythonRuleRunner,
75
    target: Target,
76
    test_debug_adapter: bool = True,
77
) -> tuple[int, str, str]:
78
    run_request = rule_runner.request(RunRequest, [PythonSourceFieldSet.create(target)])
1✔
79
    run_process = InteractiveProcess(
1✔
80
        argv=run_request.args,
81
        env=run_request.extra_env,
82
        input_digest=run_request.digest,
83
        run_in_workspace=True,
84
        immutable_input_digests=run_request.immutable_input_digests,
85
        append_only_caches=run_request.append_only_caches,
86
    )
87
    with mock_console(rule_runner.options_bootstrapper) as mocked_console:
1✔
88
        result = rule_runner.run_interactive_process(run_process)
1✔
89
        stdout = mocked_console[1].get_stdout()
1✔
90
        stderr = mocked_console[1].get_stderr()
1✔
91

92
    if test_debug_adapter:
1✔
93
        debug_adapter_request = rule_runner.request(
1✔
94
            RunDebugAdapterRequest, [PythonSourceFieldSet.create(target)]
95
        )
96
        debug_adapter_process = InteractiveProcess(
1✔
97
            argv=debug_adapter_request.args,
98
            env=debug_adapter_request.extra_env,
99
            input_digest=debug_adapter_request.digest,
100
            run_in_workspace=True,
101
            immutable_input_digests=debug_adapter_request.immutable_input_digests,
102
            append_only_caches=debug_adapter_request.append_only_caches,
103
        )
104
        with mock_console(rule_runner.options_bootstrapper) as mocked_console:
1✔
105
            debug_adapter_result = rule_runner.run_interactive_process(debug_adapter_process)
1✔
106
            assert debug_adapter_result.exit_code == result.exit_code, mocked_console[
1✔
107
                1
108
            ].get_stderr()
109

110
    return result.exit_code, stdout, stderr
1✔
111

112

113
@pytest.mark.parametrize(
1✔
114
    "global_default_value, field_value, run_uses_sandbox, importable_script_name",
115
    [
116
        # Nothing set -> True
117
        (None, None, True, True),
118
        (None, None, True, False),
119
        # Field set -> use field value
120
        (None, True, True, True),
121
        (None, True, True, False),
122
        (None, False, False, True),
123
        (None, False, False, False),
124
        # Global default set -> use default
125
        (True, None, True, True),
126
        (True, None, True, False),
127
        (False, None, False, True),
128
        (False, None, False, False),
129
        # Both set -> use field
130
        (True, True, True, True),
131
        (True, True, True, False),
132
        (True, False, False, True),
133
        (True, False, False, False),
134
        (False, True, True, True),
135
        (False, True, True, False),
136
        (False, False, False, True),
137
        (False, False, False, False),
138
    ],
139
)
140
def test_run_sample_script(
1✔
141
    global_default_value: bool | None,
142
    field_value: bool | None,
143
    run_uses_sandbox: bool,
144
    importable_script_name: bool,
145
    rule_runner: PythonRuleRunner,
146
) -> None:
147
    """Test that we properly run a `python_source` target.
148

149
    This checks a few things:
150
    - We can handle source roots.
151
    - We run in-repo when requested, and handle codegen correctly.
152
    - We propagate the error code.
153
    - We can handle scripts without importable file name.
154
    """
155
    script_name = "app.py" if importable_script_name else "my-executable.py"
1✔
156
    sources = {
1✔
157
        f"src_root1/project/{script_name}": dedent(
158
            """\
159
            import sys
160
            from utils.strutil import my_file
161
            from codegen.hello_pb2 import Hi
162

163
            def main():
164
                print("Hola, mundo.", file=sys.stderr)
165
                print(my_file())
166
                sys.exit(23)
167

168
            if __name__ == "__main__":
169
              main()
170
            """
171
        ),
172
        "src_root1/project/BUILD": dedent(
173
            f"""\
174
            python_sources(
175
                {("run_goal_use_sandbox=" + str(field_value)) if field_value is not None else ""}
176
            )
177
            """
178
        ),
179
        "src_root2/utils/strutil.py": dedent(
180
            """\
181
            import os.path
182

183
            def my_file():
184
                return os.path.abspath(__file__)
185
            """
186
        ),
187
        "src_root2/utils/BUILD": "python_sources()",
188
        "src_root2/codegen/hello.proto": 'syntax = "proto3";\nmessage Hi {string name = 1;}',
189
        "src_root2/codegen/BUILD": dedent(
190
            """\
191
            protobuf_sources()
192
            python_requirement(name='protobuf', requirements=['protobuf'])
193
            """
194
        ),
195
    }
196

197
    rule_runner.write_files(sources)
1✔
198
    args = [
1✔
199
        "--backend-packages=pants.backend.python",
200
        "--backend-packages=pants.backend.codegen.protobuf.python",
201
        "--source-root-patterns=['src_root1', 'src_root2']",
202
        f"--debug-adapter-port={debugadapter_port_for_testing()}",
203
        *(
204
            (
205
                (
206
                    "--python-default-run-goal-use-sandbox"
207
                    if global_default_value
208
                    else "--no-python-default-run-goal-use-sandbox"
209
                ),
210
            )
211
            if global_default_value is not None
212
            else ()
213
        ),
214
    ]
215
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
1✔
216
    target = rule_runner.get_target(Address("src_root1/project", relative_file_path=script_name))
1✔
217
    exit_code, stdout, stderr = run_run_request(rule_runner, target)
1✔
218

219
    assert "Hola, mundo.\n" in stderr
1✔
UNCOV
220
    file = stdout.strip()
×
UNCOV
221
    if run_uses_sandbox:
×
UNCOV
222
        assert file.endswith("src_root2/utils/strutil.py")
×
UNCOV
223
        assert "pants-sandbox-" in file
×
224
    else:
UNCOV
225
        assert file == os.path.join(rule_runner.build_root, "src_root2/utils/strutil.py")
×
UNCOV
226
    assert exit_code == 23
×
227

228

229
def test_no_strip_pex_env_issues_12057(rule_runner: PythonRuleRunner) -> None:
1✔
230
    sources = {
1✔
231
        "src/app.py": dedent(
232
            """\
233
            import os
234
            import sys
235

236

237
            if __name__ == "__main__":
238
                exit_code = os.environ.get("PANTS_ISSUES_12057")
239
                if exit_code is None:
240
                    os.environ["PANTS_ISSUES_12057"] = "42"
241
                    os.execv(sys.executable, [sys.executable, *sys.argv])
242
                sys.exit(int(exit_code))
243
            """
244
        ),
245
        "src/BUILD": dedent(
246
            """\
247
            python_sources()
248
            """
249
        ),
250
    }
251
    rule_runner.write_files(sources)
1✔
252
    args = [
1✔
253
        "--backend-packages=pants.backend.python",
254
        "--source-root-patterns=['src']",
255
    ]
256
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
1✔
257
    target = rule_runner.get_target(Address("src", relative_file_path="app.py"))
1✔
258
    exit_code, _, stderr = run_run_request(rule_runner, target, test_debug_adapter=False)
1✔
259
    assert exit_code == 42, stderr
1✔
260

261

262
@pytest.mark.parametrize("run_in_sandbox", [False, True])
1✔
263
def test_pex_root_location(rule_runner: PythonRuleRunner, run_in_sandbox: bool) -> None:
1✔
264
    # See issues #12055 and #17750.
265
    read_config_result = run_pants(["help-all"])
1✔
266
    read_config_result.assert_success()
1✔
267
    config_data = json.loads(read_config_result.stdout)
1✔
268
    global_advanced_options = {
1✔
269
        option["config_key"]: [
270
            ranked_value["value"] for ranked_value in option["value_history"]["ranked_values"]
271
        ][-1]
272
        for option in config_data["scope_to_help_info"][""]["advanced"]
273
    }
274
    named_caches_dir = global_advanced_options["named_caches_dir"]
1✔
275

276
    sources = {
1✔
277
        "src/app.py": "import os; print(__file__ + '\\n' + os.environ['PEX_ROOT'])",
278
        "src/BUILD": dedent(
279
            f"""\
280
            python_sources(run_goal_use_sandbox={run_in_sandbox})
281
            """
282
        ),
283
    }
284
    rule_runner.write_files(sources)
1✔
285
    args = [
1✔
286
        "--backend-packages=pants.backend.python",
287
        "--source-root-patterns=['src']",
288
    ]
289
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
1✔
290
    target = rule_runner.get_target(Address("src", relative_file_path="app.py"))
1✔
291
    exit_code, stdout, _ = run_run_request(rule_runner, target, test_debug_adapter=False)
1✔
292
    assert exit_code == 0
1✔
293
    app_file, pex_root = stdout.splitlines(keepends=False)
1✔
294
    sandbox = os.path.dirname(os.path.dirname(app_file))
1✔
295
    expected_pex_root = (
1✔
296
        os.path.join(sandbox, ".", ".cache", "pex_root")
297
        if run_in_sandbox
298
        else os.path.join(named_caches_dir, "pex_root")
299
    )
300
    assert expected_pex_root == pex_root
1✔
301

302

303
def test_local_dist(rule_runner: PythonRuleRunner) -> None:
1✔
304
    sources = {
1✔
305
        "foo/bar.py": "BAR = 'LOCAL DIST'",
306
        "foo/setup.py": dedent(
307
            """\
308
            from setuptools import setup
309

310
            setup(name="foo", version="9.8.7", packages=["foo"], package_dir={"foo": "."},)
311
            """
312
        ),
313
        "foo/main.py": "from foo.bar import BAR; print(BAR)",
314
        "foo/BUILD": dedent(
315
            """\
316
            python_sources(name="lib", sources=["bar.py", "setup.py"])
317

318
            python_distribution(
319
                name="dist",
320
                dependencies=[":lib"],
321
                provides=python_artifact(name="foo", version="9.8.7"),
322
                sdist=False,
323
                generate_setup=False,
324
            )
325

326
            python_sources(
327
                sources=["main.py"],
328
                # Force-exclude any dep on bar.py, so the only way to consume it is via the dist.
329
                dependencies=[":dist", "!:lib"],
330
            )
331
            """
332
        ),
333
    }
334
    rule_runner.write_files(sources)
1✔
335
    args = [
1✔
336
        "--backend-packages=pants.backend.python",
337
        "--source-root-patterns=['/']",
338
    ]
339
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
1✔
340
    target = rule_runner.get_target(Address("foo", relative_file_path="main.py"))
1✔
341
    exit_code, stdout, stderr = run_run_request(rule_runner, target)
1✔
342
    assert exit_code == 0
1✔
343
    assert stdout == "LOCAL DIST\n", stderr
1✔
344

345

346
def test_runs_in_venv(rule_runner: PythonRuleRunner) -> None:
1✔
347
    # NB: We aren't just testing an implementation detail, users can and should expect their code to
348
    # be run just as if they ran their code in a virtualenv (as is common in the Python ecosystem).
349
    sources = {
1✔
350
        "src/app.py": dedent(
351
            """\
352
            import os
353
            import sys
354

355
            if __name__ == "__main__":
356
                sys.exit(0 if "VIRTUAL_ENV" in os.environ else 1)
357
            """
358
        ),
359
        "src/BUILD": dedent(
360
            """\
361
            python_sources()
362
            """
363
        ),
364
    }
365
    rule_runner.write_files(sources)
1✔
366
    args = [
1✔
367
        "--backend-packages=pants.backend.python",
368
        "--source-root-patterns=['src']",
369
    ]
370
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
1✔
371
    target = rule_runner.get_target(Address("src", relative_file_path="app.py"))
1✔
372
    exit_code, stdout, _ = run_run_request(rule_runner, target)
1✔
373
    assert exit_code == 0, stdout
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