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

pantsbuild / pants / 20632486505

01 Jan 2026 04:21AM UTC coverage: 43.231% (-37.1%) from 80.281%
20632486505

Pull #22962

github

web-flow
Merge 08d5c63b0 into f52ab6675
Pull Request #22962: Bump the gha-deps group across 1 directory with 6 updates

26122 of 60424 relevant lines covered (43.23%)

0.86 hits per line

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

64.71
/src/python/pants/backend/codegen/protobuf/python/rules_integration_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
2✔
5

6
from textwrap import dedent
2✔
7

8
import pytest
2✔
9

10
from pants.backend.codegen.protobuf import protobuf_dependency_inference
2✔
11
from pants.backend.codegen.protobuf.python import additional_fields
2✔
12
from pants.backend.codegen.protobuf.python.python_protobuf_subsystem import PythonProtobufMypyPlugin
2✔
13
from pants.backend.codegen.protobuf.python.python_protobuf_subsystem import (
2✔
14
    rules as protobuf_subsystem_rules,
15
)
16
from pants.backend.codegen.protobuf.python.register import rules as python_protobuf_backend_rules
2✔
17
from pants.backend.codegen.protobuf.python.rules import GeneratePythonFromProtobufRequest
2✔
18
from pants.backend.codegen.protobuf.python.rules import rules as protobuf_rules
2✔
19
from pants.backend.codegen.protobuf.target_types import (
2✔
20
    ProtobufSourceField,
21
    ProtobufSourcesGeneratorTarget,
22
)
23
from pants.backend.codegen.protobuf.target_types import rules as protobuf_target_types_rules
2✔
24
from pants.backend.python import target_types_rules as python_target_types_rules
2✔
25
from pants.backend.python.dependency_inference import module_mapper
2✔
26
from pants.core.target_types import rules as core_target_types_rules
2✔
27
from pants.core.util_rules import stripped_source_files
2✔
28
from pants.engine.addresses import Address
2✔
29
from pants.engine.target import GeneratedSources, HydratedSources, HydrateSourcesRequest
2✔
30
from pants.source.source_root import NoSourceRootError
2✔
31
from pants.testutil.python_interpreter_selection import all_major_minor_python_versions
2✔
32
from pants.testutil.rule_runner import QueryRule, RuleRunner, engine_error
2✔
33
from pants.util.resources import read_sibling_resource
2✔
34

35
GRPC_PROTO_STANZA = """
2✔
36
syntax = "proto3";
37

38
package dir1;
39

40
// The greeter service definition.
41
service Greeter {
42
  // Sends a greeting
43
  rpc SayHello (HelloRequest) returns (HelloReply) {}
44
}
45

46
// The request message containing the user's name.
47
message HelloRequest {
48
  string name = 1;
49
}
50

51
// The response message containing the greetings
52
message HelloReply {
53
  string message = 1;
54
}
55
"""
56

57

58
@pytest.fixture
2✔
59
def rule_runner() -> RuleRunner:
2✔
60
    return RuleRunner(
2✔
61
        rules=[
62
            *protobuf_rules(),
63
            *python_protobuf_backend_rules(),
64
            *protobuf_dependency_inference.rules(),
65
            *protobuf_subsystem_rules(),
66
            *additional_fields.rules(),
67
            *protobuf_target_types_rules(),
68
            *python_target_types_rules.rules(),
69
            *stripped_source_files.rules(),
70
            *module_mapper.rules(),
71
            *core_target_types_rules(),
72
            QueryRule(HydratedSources, [HydrateSourcesRequest]),
73
            QueryRule(GeneratedSources, [GeneratePythonFromProtobufRequest]),
74
        ],
75
        target_types=[ProtobufSourcesGeneratorTarget],
76
    )
77

78

79
def assert_files_generated(
2✔
80
    rule_runner: RuleRunner,
81
    address: Address,
82
    *,
83
    expected_files: list[str],
84
    source_roots: list[str],
85
    mypy: bool = False,
86
    extra_args: list[str] | None = None,
87
) -> None:
88
    args = [
2✔
89
        f"--source-root-patterns={repr(source_roots)}",
90
        "--no-python-protobuf-infer-runtime-dependency",
91
        *(extra_args or ()),
92
    ]
93
    if mypy:
2✔
94
        args.append("--python-protobuf-mypy-plugin")
×
95
    rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
2✔
96
    tgt = rule_runner.get_target(address)
2✔
97
    protocol_sources = rule_runner.request(
2✔
98
        HydratedSources, [HydrateSourcesRequest(tgt[ProtobufSourceField])]
99
    )
100
    generated_sources = rule_runner.request(
2✔
101
        GeneratedSources,
102
        [GeneratePythonFromProtobufRequest(protocol_sources.snapshot, tgt)],
103
    )
104
    assert set(generated_sources.snapshot.files) == set(expected_files)
2✔
105

106

107
def test_generates_python(rule_runner: RuleRunner) -> None:
2✔
108
    # This tests a few things:
109
    #  * We generate the correct file names.
110
    #  * Protobuf files can import other protobuf files, and those can import others
111
    #    (transitive dependencies). We'll only generate the requested target, though.
112
    #  * We can handle multiple source roots, which need to be preserved in the final output.
113
    rule_runner.write_files(
×
114
        {
115
            "src/protobuf/dir1/f.proto": dedent(
116
                """\
117
                syntax = "proto3";
118

119
                package dir1;
120

121
                message Person {
122
                  string name = 1;
123
                  int32 id = 2;
124
                  string email = 3;
125
                }
126
                """
127
            ),
128
            "src/protobuf/dir1/f2.proto": dedent(
129
                """\
130
                syntax = "proto3";
131

132
                package dir1;
133
                """
134
            ),
135
            "src/protobuf/dir1/BUILD": "protobuf_sources()",
136
            "src/protobuf/dir2/f.proto": dedent(
137
                """\
138
                syntax = "proto3";
139

140
                package dir2;
141

142
                import "dir1/f.proto";
143
                """
144
            ),
145
            "src/protobuf/dir2/BUILD": dedent(
146
                """\
147
                protobuf_sources(dependencies=['src/protobuf/dir1'],
148
                python_source_root='src/python')
149
                """
150
            ),
151
            # Test another source root.
152
            "tests/protobuf/test_protos/f.proto": dedent(
153
                """\
154
                syntax = "proto3";
155

156
                package test_protos;
157

158
                import "dir2/f.proto";
159
                """
160
            ),
161
            "tests/protobuf/test_protos/BUILD": (
162
                "protobuf_sources(dependencies=['src/protobuf/dir2'])"
163
            ),
164
        }
165
    )
166

167
    def assert_gen(addr: Address, expected: str) -> None:
×
168
        assert_files_generated(
×
169
            rule_runner,
170
            addr,
171
            source_roots=["src/python", "/src/protobuf", "/tests/protobuf"],
172
            expected_files=[expected],
173
        )
174

175
    assert_gen(
×
176
        Address("src/protobuf/dir1", relative_file_path="f.proto"), "src/protobuf/dir1/f_pb2.py"
177
    )
178
    assert_gen(
×
179
        Address("src/protobuf/dir1", relative_file_path="f2.proto"), "src/protobuf/dir1/f2_pb2.py"
180
    )
181
    assert_gen(
×
182
        Address("src/protobuf/dir2", relative_file_path="f.proto"), "src/python/dir2/f_pb2.py"
183
    )
184
    assert_gen(
×
185
        Address("tests/protobuf/test_protos", relative_file_path="f.proto"),
186
        "tests/protobuf/test_protos/f_pb2.py",
187
    )
188

189

190
def test_top_level_proto_root(rule_runner: RuleRunner) -> None:
2✔
191
    rule_runner.write_files(
×
192
        {
193
            "protos/f.proto": dedent(
194
                """\
195
                syntax = "proto3";
196

197
                package protos;
198
                """
199
            ),
200
            "protos/BUILD": "protobuf_sources()",
201
        }
202
    )
203
    assert_files_generated(
×
204
        rule_runner,
205
        Address("protos", relative_file_path="f.proto"),
206
        source_roots=["/"],
207
        expected_files=["protos/f_pb2.py"],
208
    )
209

210

211
def test_top_level_python_source_root(rule_runner: RuleRunner) -> None:
2✔
212
    rule_runner.write_files(
×
213
        {
214
            "src/proto/protos/f.proto": dedent(
215
                """\
216
                syntax = "proto3";
217

218
                package protos;
219
                """
220
            ),
221
            "src/proto/protos/BUILD": "protobuf_sources(python_source_root='.')",
222
        }
223
    )
224
    assert_files_generated(
×
225
        rule_runner,
226
        Address("src/proto/protos", relative_file_path="f.proto"),
227
        source_roots=["/", "src/proto"],
228
        expected_files=["protos/f_pb2.py"],
229
    )
230

231

232
def test_bad_python_source_root(rule_runner: RuleRunner) -> None:
2✔
233
    rule_runner.write_files(
×
234
        {
235
            "src/protobuf/dir1/f.proto": dedent(
236
                """\
237
                syntax = "proto3";
238

239
                package dir1;
240
                """
241
            ),
242
            "src/protobuf/dir1/BUILD": "protobuf_sources(python_source_root='notasourceroot')",
243
        }
244
    )
245
    with engine_error(NoSourceRootError):
×
246
        assert_files_generated(
×
247
            rule_runner,
248
            Address("src/protobuf/dir1", relative_file_path="f.proto"),
249
            source_roots=["src/protobuf"],
250
            expected_files=[],
251
        )
252

253

254
@pytest.mark.platform_specific_behavior
2✔
255
@pytest.mark.parametrize(
2✔
256
    "major_minor_interpreter",
257
    all_major_minor_python_versions(PythonProtobufMypyPlugin.default_interpreter_constraints),
258
)
259
def test_generate_type_stubs(rule_runner: RuleRunner, major_minor_interpreter: str) -> None:
2✔
260
    rule_runner.write_files(
2✔
261
        {
262
            "src/protobuf/dir1/f.proto": dedent(
263
                """\
264
                syntax = "proto3";
265

266
                package dir1;
267

268
                message Person {
269
                  string name = 1;
270
                  int32 id = 2;
271
                  string email = 3;
272
                }
273
                """
274
            ),
275
            "src/protobuf/dir1/BUILD": "protobuf_sources()",
276
        }
277
    )
278
    assert_files_generated(
2✔
279
        rule_runner,
280
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
281
        source_roots=["src/protobuf"],
282
        extra_args=[
283
            "--python-protobuf-generate-type-stubs",
284
            f"--mypy-protobuf-interpreter-constraints=['=={major_minor_interpreter}.*']",
285
        ],
286
        expected_files=["src/protobuf/dir1/f_pb2.py", "src/protobuf/dir1/f_pb2.pyi"],
287
    )
288

289

290
@pytest.mark.platform_specific_behavior
2✔
291
@pytest.mark.parametrize(
2✔
292
    "major_minor_interpreter",
293
    all_major_minor_python_versions(PythonProtobufMypyPlugin.default_interpreter_constraints),
294
)
295
def test_mypy_plugin(rule_runner: RuleRunner, major_minor_interpreter: str) -> None:
2✔
296
    rule_runner.write_files(
2✔
297
        {
298
            "src/protobuf/dir1/f.proto": dedent(
299
                """\
300
                syntax = "proto3";
301

302
                package dir1;
303

304
                message Person {
305
                  string name = 1;
306
                  int32 id = 2;
307
                  string email = 3;
308
                }
309
                """
310
            ),
311
            "src/protobuf/dir1/BUILD": "protobuf_sources()",
312
        }
313
    )
314
    assert_files_generated(
2✔
315
        rule_runner,
316
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
317
        source_roots=["src/protobuf"],
318
        extra_args=[
319
            "--python-protobuf-mypy-plugin",
320
            f"--mypy-protobuf-interpreter-constraints=['=={major_minor_interpreter}.*']",
321
        ],
322
        expected_files=["src/protobuf/dir1/f_pb2.py", "src/protobuf/dir1/f_pb2.pyi"],
323
    )
324

325

326
def test_grpc(rule_runner: RuleRunner) -> None:
2✔
327
    rule_runner.write_files(
×
328
        {
329
            "src/protobuf/dir1/f.proto": dedent(GRPC_PROTO_STANZA),
330
            "src/protobuf/dir1/BUILD": "protobuf_sources(grpc=True)",
331
        }
332
    )
333
    assert_files_generated(
×
334
        rule_runner,
335
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
336
        source_roots=["src/protobuf"],
337
        expected_files=["src/protobuf/dir1/f_pb2.py", "src/protobuf/dir1/f_pb2_grpc.py"],
338
    )
339

340

341
def test_grpc_mypy_plugin(rule_runner: RuleRunner) -> None:
2✔
342
    rule_runner.write_files(
×
343
        {
344
            "src/protobuf/dir1/f.proto": dedent(GRPC_PROTO_STANZA),
345
            "src/protobuf/dir1/BUILD": "protobuf_sources(grpc=True)",
346
        }
347
    )
348
    assert_files_generated(
×
349
        rule_runner,
350
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
351
        source_roots=["src/protobuf"],
352
        mypy=True,
353
        expected_files=[
354
            "src/protobuf/dir1/f_pb2.py",
355
            "src/protobuf/dir1/f_pb2.pyi",
356
            "src/protobuf/dir1/f_pb2_grpc.py",
357
            "src/protobuf/dir1/f_pb2_grpc.pyi",
358
        ],
359
    )
360

361

362
def test_grpc_pre_v2_mypy_plugin(rule_runner: RuleRunner) -> None:
2✔
363
    rule_runner.write_files(
×
364
        {
365
            "src/protobuf/dir1/f.proto": dedent(GRPC_PROTO_STANZA),
366
            "src/protobuf/dir1/BUILD": "protobuf_sources(grpc=True)",
367
            "mypy-protobuf.lock": read_sibling_resource(
368
                __name__, "test_grpc_pre_v2_mypy_plugin.lock"
369
            ),
370
        }
371
    )
372
    assert_files_generated(
×
373
        rule_runner,
374
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
375
        source_roots=["src/protobuf"],
376
        extra_args=[
377
            "--python-protobuf-mypy-plugin",
378
            "--python-resolves={'mypy-protobuf':'mypy-protobuf.lock'}",
379
            "--mypy-protobuf-install-from-resolve=mypy-protobuf",
380
        ],
381
        expected_files=[
382
            "src/protobuf/dir1/f_pb2.py",
383
            "src/protobuf/dir1/f_pb2.pyi",
384
            "src/protobuf/dir1/f_pb2_grpc.py",
385
        ],
386
    )
387

388

389
def test_grpclib_plugin(rule_runner: RuleRunner) -> None:
2✔
390
    rule_runner.write_files(
×
391
        {
392
            "src/protobuf/dir1/f.proto": dedent(GRPC_PROTO_STANZA),
393
            "src/protobuf/dir1/BUILD": "protobuf_sources(grpc=True)",
394
        }
395
    )
396
    assert_files_generated(
×
397
        rule_runner,
398
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
399
        source_roots=["src/protobuf"],
400
        extra_args=[
401
            "--python-protobuf-grpclib-plugin",
402
            "--no-python-protobuf-grpcio-plugin",
403
        ],
404
        expected_files=[
405
            "src/protobuf/dir1/f_pb2.py",
406
            "src/protobuf/dir1/f_grpc.py",
407
        ],
408
    )
409

410

411
def test_all_plugins(rule_runner: RuleRunner) -> None:
2✔
412
    rule_runner.write_files(
×
413
        {
414
            "src/protobuf/dir1/f.proto": dedent(GRPC_PROTO_STANZA),
415
            "src/protobuf/dir1/BUILD": "protobuf_sources(grpc=True)",
416
        }
417
    )
418
    assert_files_generated(
×
419
        rule_runner,
420
        Address("src/protobuf/dir1", relative_file_path="f.proto"),
421
        source_roots=["src/protobuf"],
422
        extra_args=[
423
            "--python-protobuf-grpclib-plugin",
424
            "--python-protobuf-grpcio-plugin",
425
            "--python-protobuf-mypy-plugin",
426
        ],
427
        expected_files=[
428
            "src/protobuf/dir1/f_pb2.py",
429
            "src/protobuf/dir1/f_pb2.pyi",
430
            "src/protobuf/dir1/f_pb2_grpc.py",
431
            "src/protobuf/dir1/f_pb2_grpc.pyi",
432
            "src/protobuf/dir1/f_grpc.py",
433
        ],
434
    )
435

436

437
def test_code_generation_with_multiple_resolves(rule_runner: RuleRunner) -> None:
2✔
438
    """End-to-end test that code is generated correctly for different resolves."""
439
    rule_runner.write_files(
×
440
        {
441
            "src/protobuf/a/service.proto": dedent(
442
                """\
443
                syntax = "proto3";
444

445
                package a;
446

447
                message Request {
448
                  string name = 1;
449
                }
450
                """
451
            ),
452
            "src/protobuf/a/BUILD": "protobuf_sources(python_resolve='a')",
453
            "src/protobuf/b/service.proto": dedent(
454
                """\
455
                syntax = "proto3";
456

457
                package b;
458

459
                message Request {
460
                  string name = 1;
461
                }
462
                """
463
            ),
464
            "src/protobuf/b/BUILD": "protobuf_sources(python_resolve='b')",
465
        }
466
    )
467

468
    # Test code generation for prod resolve
469
    assert_files_generated(
×
470
        rule_runner,
471
        Address("src/protobuf/a", relative_file_path="service.proto"),
472
        source_roots=["src/protobuf"],
473
        extra_args=[
474
            "--python-enable-resolves",
475
            "--python-resolves={'a': '', 'b': ''}",
476
        ],
477
        expected_files=["src/protobuf/a/service_pb2.py"],
478
    )
479

480
    # Test code generation for dev resolve
481
    assert_files_generated(
×
482
        rule_runner,
483
        Address("src/protobuf/b", relative_file_path="service.proto"),
484
        source_roots=["src/protobuf"],
485
        extra_args=[
486
            "--python-enable-resolves",
487
            "--python-resolves={'a': '', 'b': ''}",
488
        ],
489
        expected_files=["src/protobuf/b/service_pb2.py"],
490
    )
491

492

493
def test_transitive_dependencies_within_resolve(rule_runner: RuleRunner) -> None:
2✔
494
    """Test transitive proto imports within a resolve."""
495
    rule_runner.write_files(
×
496
        {
497
            "src/protobuf/a.proto": dedent(
498
                """\
499
                syntax = "proto3";
500

501
                import "b.proto";
502

503
                message A {
504
                  B b = 1;
505
                }
506
                """
507
            ),
508
            "src/protobuf/b.proto": dedent(
509
                """\
510
                syntax = "proto3";
511

512
                import "c.proto";
513

514
                message B {
515
                  C c = 1;
516
                }
517
                """
518
            ),
519
            "src/protobuf/c.proto": dedent(
520
                """\
521
                syntax = "proto3";
522

523
                message C {
524
                  string value = 1;
525
                }
526
                """
527
            ),
528
            "src/protobuf/BUILD": "protobuf_sources(python_resolve='python-default')",
529
        }
530
    )
531

532
    # Test that code generation succeeds with transitive dependencies resolved
533
    assert_files_generated(
×
534
        rule_runner,
535
        Address("src/protobuf", relative_file_path="a.proto"),
536
        source_roots=["src/protobuf"],
537
        extra_args=[
538
            "--python-enable-resolves",
539
            "--python-resolves={'python-default': ''}",
540
        ],
541
        expected_files=["src/protobuf/a_pb2.py"],
542
    )
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