• 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

69.19
/src/python/pants/backend/python/dependency_inference/module_mapper_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 re
1✔
7
from collections.abc import Iterable
1✔
8
from pathlib import PurePath
1✔
9
from textwrap import dedent
1✔
10
from types import FunctionType
1✔
11

12
import pytest
1✔
13
from packaging.utils import canonicalize_name as canonicalize_project_name
1✔
14

15
from pants.backend.codegen.protobuf.python import python_protobuf_module_mapper
1✔
16
from pants.backend.codegen.protobuf.python.additional_fields import (
1✔
17
    rules as protobuf_additional_fields_rules,
18
)
19
from pants.backend.codegen.protobuf.target_types import ProtobufSourcesGeneratorTarget
1✔
20
from pants.backend.codegen.protobuf.target_types import rules as protobuf_target_type_rules
1✔
21
from pants.backend.python import target_types_rules
1✔
22
from pants.backend.python.dependency_inference.default_module_mapping import (
1✔
23
    DEFAULT_MODULE_MAPPING,
24
    DEFAULT_MODULE_PATTERN_MAPPING,
25
    DEFAULT_TYPE_STUB_MODULE_MAPPING,
26
    DEFAULT_TYPE_STUB_MODULE_PATTERN_MAPPING,
27
    first_group_hyphen_to_underscore,
28
    two_groups_hyphens_two_replacements_with_suffix,
29
)
30
from pants.backend.python.dependency_inference.module_mapper import (
1✔
31
    FirstPartyPythonModuleMapping,
32
    ModuleProvider,
33
    ModuleProviderType,
34
    PossibleModuleProvider,
35
    PythonModuleOwners,
36
    PythonModuleOwnersRequest,
37
    ThirdPartyPythonModuleMapping,
38
    generate_mappings_from_pattern,
39
    module_from_stripped_path,
40
)
41
from pants.backend.python.dependency_inference.module_mapper import rules as module_mapper_rules
1✔
42
from pants.backend.python.target_types import (
1✔
43
    PythonRequirementTarget,
44
    PythonSourcesGeneratorTarget,
45
    PythonSourceTarget,
46
)
47
from pants.core.util_rules import stripped_source_files
1✔
48
from pants.engine.addresses import Address
1✔
49
from pants.testutil.rule_runner import QueryRule, RuleRunner
1✔
50
from pants.util.frozendict import FrozenDict
1✔
51

52

53
def test_default_module_mapping_is_normalized() -> None:
1✔
54
    for k in DEFAULT_MODULE_MAPPING:
1✔
55
        assert k == canonicalize_project_name(k), (
1✔
56
            "Please update `DEFAULT_MODULE_MAPPING` to use canonical project names"
57
        )
58
    for k in DEFAULT_TYPE_STUB_MODULE_MAPPING:
1✔
59
        assert k == canonicalize_project_name(k), (
1✔
60
            "Please update `DEFAULT_TYPE_STUB_MODULE_MAPPING` to use canonical project names"
61
        )
62

63

64
def test_default_module_mapping_uses_tuples() -> None:
1✔
65
    for modules in [
1✔
66
        *DEFAULT_MODULE_MAPPING.values(),
67
        *DEFAULT_TYPE_STUB_MODULE_MAPPING.values(),
68
    ]:
69
        assert isinstance(modules, tuple)
1✔
70
        assert len(modules) > 0
1✔
71

72

73
def test_default_module_pattern_mapping_keys_and_value_types() -> None:
1✔
74
    for pattern, replacements in [
1✔
75
        *DEFAULT_MODULE_PATTERN_MAPPING.items(),
76
        *DEFAULT_TYPE_STUB_MODULE_PATTERN_MAPPING.items(),
77
    ]:
78
        assert isinstance(pattern, re.Pattern)
1✔
79
        assert isinstance(replacements, Iterable)
1✔
80
        for replacement in replacements:
1✔
81
            assert (
1✔
82
                isinstance(replacement, FunctionType)
83
                or isinstance(replacement, str)
84
                or callable(replacement)
85
            )
86

87

88
@pytest.mark.parametrize(
1✔
89
    "stripped_path,expected",
90
    [
91
        ("top_level.py", "top_level"),
92
        ("top_level.pyi", "top_level"),
93
        ("dir/subdir/__init__.py", "dir.subdir"),
94
        ("dir/subdir/__init__.pyi", "dir.subdir"),
95
        ("dir/subdir/app.py", "dir.subdir.app"),
96
        ("src/python/project/not_stripped.py", "src.python.project.not_stripped"),
97
    ],
98
)
99
def test_module_from_stripped_path(stripped_path: str, expected: str) -> None:
1✔
100
    assert module_from_stripped_path(PurePath(stripped_path)) == expected
1✔
101

102

103
def test_first_party_modules_mapping() -> None:
1✔
104
    root_provider = ModuleProvider(
1✔
105
        Address("", relative_file_path="root.py"), ModuleProviderType.IMPL
106
    )
107
    util_provider = ModuleProvider(
1✔
108
        Address("src/python/util", relative_file_path="strutil.py"),
109
        ModuleProviderType.IMPL,
110
    )
111
    util_stubs_provider = ModuleProvider(
1✔
112
        Address("src/python/util", relative_file_path="strutil.pyi"),
113
        ModuleProviderType.TYPE_STUB,
114
    )
115
    test_provider = ModuleProvider(
1✔
116
        Address("tests/python/project_test", relative_file_path="test.py"),
117
        ModuleProviderType.IMPL,
118
    )
119
    mapping = FirstPartyPythonModuleMapping(
1✔
120
        FrozenDict(
121
            {
122
                "default": FrozenDict(
123
                    {
124
                        "root": (root_provider,),
125
                        "util.strutil": (util_provider, util_stubs_provider),
126
                        "project_test.test": (test_provider,),
127
                        "ambiguous": (root_provider, util_provider),
128
                        "util.ambiguous": (util_provider, test_provider),
129
                        "two_resolves": (root_provider,),
130
                    }
131
                ),
132
                "another": FrozenDict({"two_resolves": (test_provider,)}),
133
            }
134
        )
135
    )
136

137
    def assert_addresses(
1✔
138
        mod: str,
139
        expected: tuple[PossibleModuleProvider, ...],
140
        *,
141
        resolve: str | None = None,
142
    ) -> None:
143
        assert mapping.providers_for_module(mod, resolve=resolve) == expected
1✔
144

145
    root_provider0 = PossibleModuleProvider(root_provider, 0)
1✔
146
    root_provider1 = PossibleModuleProvider(root_provider, 1)
1✔
147
    util_provider0 = PossibleModuleProvider(util_provider, 0)
1✔
148
    util_provider1 = PossibleModuleProvider(util_provider, 1)
1✔
149
    util_stubs_provider0 = PossibleModuleProvider(util_stubs_provider, 0)
1✔
150
    util_stubs_provider1 = PossibleModuleProvider(util_stubs_provider, 1)
1✔
151
    test_provider0 = PossibleModuleProvider(test_provider, 0)
1✔
152
    test_provider1 = PossibleModuleProvider(test_provider, 1)
1✔
153

154
    assert_addresses("root", (root_provider0,))
1✔
155
    assert_addresses("root.func", (root_provider1,))
1✔
156
    assert_addresses("root.submodule.func", ())
1✔
157

158
    assert_addresses("util.strutil", (util_provider0, util_stubs_provider0))
1✔
159
    assert_addresses("util.strutil.ensure_text", (util_provider1, util_stubs_provider1))
1✔
160
    assert_addresses("util", ())
1✔
161

162
    assert_addresses("project_test.test", (test_provider0,))
1✔
163
    assert_addresses("project_test.test.TestDemo", (test_provider1,))
1✔
164
    assert_addresses("project_test", ())
1✔
165
    assert_addresses("project.test", ())
1✔
166

167
    assert_addresses("ambiguous", (root_provider0, util_provider0))
1✔
168
    assert_addresses("ambiguous.func", (root_provider1, util_provider1))
1✔
169
    assert_addresses("ambiguous.submodule.func", ())
1✔
170

171
    assert_addresses("util.ambiguous", (util_provider0, test_provider0))
1✔
172
    assert_addresses("util.ambiguous.Foo", (util_provider1, test_provider1))
1✔
173
    assert_addresses("util.ambiguous.Foo.method", ())
1✔
174

175
    assert_addresses("two_resolves", (root_provider0, test_provider0), resolve=None)
1✔
176
    assert_addresses("two_resolves.foo", (root_provider1, test_provider1), resolve=None)
1✔
177
    assert_addresses("two_resolves.foo.bar", (), resolve=None)
1✔
178
    assert_addresses("two_resolves", (root_provider0,), resolve="default")
1✔
179
    assert_addresses("two_resolves", (test_provider0,), resolve="another")
1✔
180

181

182
def test_third_party_modules_mapping() -> None:
1✔
183
    colors_provider = ModuleProvider(Address("", target_name="ansicolors"), ModuleProviderType.IMPL)
1✔
184
    colors_stubs_provider = ModuleProvider(
1✔
185
        Address("", target_name="types-ansicolors"), ModuleProviderType.TYPE_STUB
186
    )
187
    pants_provider = ModuleProvider(Address("", target_name="pantsbuild"), ModuleProviderType.IMPL)
1✔
188
    pants_testutil_provider = ModuleProvider(
1✔
189
        Address("", target_name="pantsbuild.testutil"), ModuleProviderType.IMPL
190
    )
191
    submodule_provider = ModuleProvider(
1✔
192
        Address("", target_name="submodule"), ModuleProviderType.IMPL
193
    )
194
    mapping = ThirdPartyPythonModuleMapping(
1✔
195
        FrozenDict(
196
            {
197
                "default-resolve": FrozenDict(
198
                    {
199
                        "colors": (colors_provider, colors_stubs_provider),
200
                        "pants": (pants_provider,),
201
                        "req.submodule": (submodule_provider,),
202
                        "pants.testutil": (pants_testutil_provider,),
203
                        "two_resolves": (colors_provider,),
204
                    }
205
                ),
206
                "another-resolve": FrozenDict({"two_resolves": (pants_provider,)}),
207
            }
208
        )
209
    )
210

211
    def assert_addresses(
1✔
212
        mod: str,
213
        expected: tuple[PossibleModuleProvider, ...],
214
        *,
215
        resolve: str | None = None,
216
    ) -> None:
217
        assert mapping.providers_for_module(mod, resolve) == expected
1✔
218

219
    colors_provider0 = PossibleModuleProvider(colors_provider, 0)
1✔
220
    colors_provider1 = PossibleModuleProvider(colors_provider, 1)
1✔
221
    colors_provider2 = PossibleModuleProvider(colors_provider, 2)
1✔
222
    colors_stubs_provider0 = PossibleModuleProvider(colors_stubs_provider, 0)
1✔
223
    colors_stubs_provider1 = PossibleModuleProvider(colors_stubs_provider, 1)
1✔
224
    pants_provider0 = PossibleModuleProvider(pants_provider, 0)
1✔
225
    pants_provider1 = PossibleModuleProvider(pants_provider, 1)
1✔
226
    pants_provider2 = PossibleModuleProvider(pants_provider, 2)
1✔
227
    pants_provider3 = PossibleModuleProvider(pants_provider, 3)
1✔
228
    pants_testutil_provider0 = PossibleModuleProvider(pants_testutil_provider, 0)
1✔
229
    pants_testutil_provider1 = PossibleModuleProvider(pants_testutil_provider, 1)
1✔
230
    submodule_provider0 = PossibleModuleProvider(submodule_provider, 0)
1✔
231
    submodule_provider1 = PossibleModuleProvider(submodule_provider, 1)
1✔
232

233
    assert_addresses("colors", (colors_provider0, colors_stubs_provider0))
1✔
234
    assert_addresses("colors.red", (colors_provider1, colors_stubs_provider1))
1✔
235

236
    assert_addresses("pants", (pants_provider0,))
1✔
237
    assert_addresses("pants.task", (pants_provider1,))
1✔
238
    assert_addresses("pants.task.task", (pants_provider2,))
1✔
239
    assert_addresses("pants.task.task.Task", (pants_provider3,))
1✔
240

241
    assert_addresses("pants.testutil", (pants_testutil_provider0,))
1✔
242
    assert_addresses("pants.testutil.foo", (pants_testutil_provider1,))
1✔
243

244
    assert_addresses("req.submodule", (submodule_provider0,))
1✔
245
    assert_addresses("req.submodule.foo", (submodule_provider1,))
1✔
246
    assert_addresses("req.another", ())
1✔
247
    assert_addresses("req", ())
1✔
248

249
    assert_addresses("unknown", ())
1✔
250
    assert_addresses("unknown.pants", ())
1✔
251

252
    assert_addresses("two_resolves", (colors_provider0, pants_provider0), resolve=None)
1✔
253
    assert_addresses("two_resolves.foo", (colors_provider1, pants_provider1), resolve=None)
1✔
254
    assert_addresses("two_resolves.foo.bar", (colors_provider2, pants_provider2), resolve=None)
1✔
255
    assert_addresses("two_resolves", (colors_provider0,), resolve="default-resolve")
1✔
256
    assert_addresses("two_resolves", (pants_provider0,), resolve="another-resolve")
1✔
257

258

259
@pytest.fixture
1✔
260
def rule_runner() -> RuleRunner:
1✔
261
    return RuleRunner(
1✔
262
        rules=[
263
            *stripped_source_files.rules(),
264
            *module_mapper_rules(),
265
            *python_protobuf_module_mapper.rules(),
266
            *target_types_rules.rules(),
267
            *protobuf_additional_fields_rules(),
268
            *protobuf_target_type_rules(),
269
            QueryRule(FirstPartyPythonModuleMapping, []),
270
            QueryRule(ThirdPartyPythonModuleMapping, []),
271
            QueryRule(PythonModuleOwners, [PythonModuleOwnersRequest]),
272
        ],
273
        target_types=[
274
            PythonSourceTarget,
275
            PythonSourcesGeneratorTarget,
276
            PythonRequirementTarget,
277
            ProtobufSourcesGeneratorTarget,
278
        ],
279
    )
280

281

282
def test_map_first_party_modules_to_addresses(rule_runner: RuleRunner) -> None:
1✔
UNCOV
283
    rule_runner.set_options(
×
284
        [
285
            "--source-root-patterns=['src/python', 'tests/python', 'build-support']",
286
            "--python-enable-resolves",
287
            "--python-resolves={'python-default': '', 'another-resolve': ''}",
288
        ]
289
    )
UNCOV
290
    rule_runner.write_files(
×
291
        {
292
            "src/python/project/util/dirutil.py": "",
293
            "src/python/project/util/tarutil.py": "",
294
            "src/python/project/util/BUILD": "python_sources(resolve='another-resolve')",
295
            # A module with multiple owners, including type stubs.
296
            "src/python/multiple_owners.py": "",
297
            "src/python/multiple_owners.pyi": "",
298
            "src/python/BUILD": "python_sources()",
299
            "build-support/multiple_owners.py": "",
300
            "build-support/BUILD": "python_sources()",
301
            # A package module.
302
            "tests/python/project_test/demo_test/__init__.py": "",
303
            "tests/python/project_test/demo_test/BUILD": "python_sources()",
304
            # Check that plugin mappings work. Note that we duplicate one of the files with a normal
305
            # python_source.
306
            "src/python/protos/f1.proto": "",
307
            "src/python/protos/f2.proto": "",
308
            "src/python/protos/f2_pb2.py": "",
309
            "src/python/protos/BUILD": dedent(
310
                """\
311
                protobuf_sources(name='protos')
312
                python_source(name='py', source="f2_pb2.py")
313
                """
314
            ),
315
        }
316
    )
317

UNCOV
318
    result = rule_runner.request(FirstPartyPythonModuleMapping, [])
×
UNCOV
319
    assert result == FirstPartyPythonModuleMapping(
×
320
        FrozenDict(
321
            {
322
                "another-resolve": FrozenDict(
323
                    {
324
                        "project.util.dirutil": (
325
                            ModuleProvider(
326
                                Address(
327
                                    "src/python/project/util",
328
                                    relative_file_path="dirutil.py",
329
                                ),
330
                                ModuleProviderType.IMPL,
331
                            ),
332
                        ),
333
                        "project.util.tarutil": (
334
                            ModuleProvider(
335
                                Address(
336
                                    "src/python/project/util",
337
                                    relative_file_path="tarutil.py",
338
                                ),
339
                                ModuleProviderType.IMPL,
340
                            ),
341
                        ),
342
                    }
343
                ),
344
                "python-default": FrozenDict(
345
                    {
346
                        "multiple_owners": (
347
                            ModuleProvider(
348
                                Address(
349
                                    "build-support",
350
                                    relative_file_path="multiple_owners.py",
351
                                ),
352
                                ModuleProviderType.IMPL,
353
                            ),
354
                            ModuleProvider(
355
                                Address(
356
                                    "src/python",
357
                                    relative_file_path="multiple_owners.py",
358
                                ),
359
                                ModuleProviderType.IMPL,
360
                            ),
361
                            ModuleProvider(
362
                                Address(
363
                                    "src/python",
364
                                    relative_file_path="multiple_owners.pyi",
365
                                ),
366
                                ModuleProviderType.TYPE_STUB,
367
                            ),
368
                        ),
369
                        "project_test.demo_test": (
370
                            ModuleProvider(
371
                                Address(
372
                                    "tests/python/project_test/demo_test",
373
                                    relative_file_path="__init__.py",
374
                                ),
375
                                ModuleProviderType.IMPL,
376
                            ),
377
                        ),
378
                        "protos.f1_pb2": (
379
                            ModuleProvider(
380
                                Address(
381
                                    "src/python/protos",
382
                                    relative_file_path="f1.proto",
383
                                    target_name="protos",
384
                                ),
385
                                ModuleProviderType.IMPL,
386
                            ),
387
                        ),
388
                        "protos.f2_pb2": (
389
                            ModuleProvider(
390
                                Address("src/python/protos", target_name="py"),
391
                                ModuleProviderType.IMPL,
392
                            ),
393
                            ModuleProvider(
394
                                Address(
395
                                    "src/python/protos",
396
                                    relative_file_path="f2.proto",
397
                                    target_name="protos",
398
                                ),
399
                                ModuleProviderType.IMPL,
400
                            ),
401
                        ),
402
                    }
403
                ),
404
            }
405
        )
406
    )
407

408

409
def test_map_third_party_modules_to_addresses(rule_runner: RuleRunner) -> None:
1✔
UNCOV
410
    def req(
×
411
        tgt_name: str,
412
        req_str: str,
413
        *,
414
        modules: list[str] | None = None,
415
        stub_modules: list[str] | None = None,
416
        resolve: str = "default",
417
    ) -> str:
UNCOV
418
        return dedent(
×
419
            f"""\
420
            python_requirement(name='{tgt_name}', requirements=['{req_str}'],
421
            modules={modules or []},
422
            type_stub_modules={stub_modules or []},
423
            resolve={repr(resolve)})
424
            """
425
        )
426

UNCOV
427
    build_file = "\n\n".join(
×
428
        [
429
            req("req1", "req1==1.2"),
430
            req("un_normalized", "Un-Normalized-Project>3"),
431
            req("file_dist", "file_dist@ file:///path/to/dist.whl"),
432
            req("vcs_dist", "vcs_dist@ git+https://github.com/vcs/dist.git"),
433
            req("modules", "foo==1", modules=["mapped_module"]),
434
            # We extract the module from type stub dependencies.
435
            req("typed-dep1", "typed-dep1-types"),
436
            req("typed-dep2", "types-typed-dep2"),
437
            req("typed-dep3", "typed-dep3-stubs"),
438
            req("typed-dep4", "stubs-typed-dep4"),
439
            req("typed-dep5", "typed-dep5-foo", stub_modules=["typed_dep5"]),
440
            # A 3rd-party dependency can have both a type stub and implementation.
441
            req("multiple_owners1", "multiple_owners==1"),
442
            req("multiple_owners2", "multiple_owners==2", resolve="another"),
443
            req("multiple_owners_types", "types-multiple_owners==1", resolve="another"),
444
            # Only assume it's a type stubs dep if we are certain it's not an implementation.
445
            req(
446
                "looks_like_stubs",
447
                "looks-like-stubs-types",
448
                modules=["looks_like_stubs"],
449
            ),
450
            req("google-cloud-hardyhar", "google-cloud-hardyhar"),
451
            req("google-cloud-secret-manager", "google-cloud-secret-manager"),
452
            req("azure-keyvault-secrets", "azure-keyvault-secrets"),
453
            req("django-model-utils", "model_utils"),
454
            req("django-taggit", "taggit"),
455
            req(
456
                "opentelemetry-instrumentation-botocore",
457
                "opentelemetry-instrumentation-botocore",
458
            ),
459
            req("apache-airflow", "apache-airflow"),
460
            req("apache-airflow-providers-apache-beam", "apache-airflow-providers-apache-beam"),
461
        ]
462
    )
UNCOV
463
    rule_runner.write_files({"BUILD": build_file})
×
UNCOV
464
    rule_runner.set_options(
×
465
        ["--python-resolves={'default': '', 'another': ''}", "--python-enable-resolves"]
466
    )
UNCOV
467
    result = rule_runner.request(ThirdPartyPythonModuleMapping, [])
×
UNCOV
468
    expected = ThirdPartyPythonModuleMapping(
×
469
        FrozenDict(
470
            {
471
                "another": FrozenDict(
472
                    {
473
                        "multiple_owners": (
474
                            ModuleProvider(
475
                                Address("", target_name="multiple_owners2"),
476
                                ModuleProviderType.IMPL,
477
                            ),
478
                            ModuleProvider(
479
                                Address("", target_name="multiple_owners_types"),
480
                                ModuleProviderType.TYPE_STUB,
481
                            ),
482
                        ),
483
                    }
484
                ),
485
                "default": FrozenDict(
486
                    {
487
                        "airflow": (
488
                            ModuleProvider(
489
                                Address("", target_name="apache-airflow"), ModuleProviderType.IMPL
490
                            ),
491
                        ),
492
                        "airflow.providers.apache.beam": (
493
                            ModuleProvider(
494
                                Address("", target_name="apache-airflow-providers-apache-beam"),
495
                                ModuleProviderType.IMPL,
496
                            ),
497
                        ),
498
                        "azure.keyvault.secrets": (
499
                            ModuleProvider(
500
                                Address("", target_name="azure-keyvault-secrets"),
501
                                ModuleProviderType.IMPL,
502
                            ),
503
                        ),
504
                        "file_dist": (
505
                            ModuleProvider(
506
                                Address("", target_name="file_dist"),
507
                                ModuleProviderType.IMPL,
508
                            ),
509
                        ),
510
                        "google.cloud.hardyhar": (
511
                            ModuleProvider(
512
                                Address("", target_name="google-cloud-hardyhar"),
513
                                ModuleProviderType.IMPL,
514
                            ),
515
                        ),
516
                        "google.cloud.hardyhar_v1": (
517
                            ModuleProvider(
518
                                Address("", target_name="google-cloud-hardyhar"),
519
                                ModuleProviderType.IMPL,
520
                            ),
521
                        ),
522
                        "google.cloud.hardyhar_v2": (
523
                            ModuleProvider(
524
                                Address("", target_name="google-cloud-hardyhar"),
525
                                ModuleProviderType.IMPL,
526
                            ),
527
                        ),
528
                        "google.cloud.hardyhar_v3": (
529
                            ModuleProvider(
530
                                Address("", target_name="google-cloud-hardyhar"),
531
                                ModuleProviderType.IMPL,
532
                            ),
533
                        ),
534
                        "google.cloud.secretmanager": (
535
                            ModuleProvider(
536
                                Address("", target_name="google-cloud-secret-manager"),
537
                                ModuleProviderType.IMPL,
538
                            ),
539
                        ),
540
                        "google.cloud.secretmanager_v1": (
541
                            ModuleProvider(
542
                                Address("", target_name="google-cloud-secret-manager"),
543
                                ModuleProviderType.IMPL,
544
                            ),
545
                        ),
546
                        "google.cloud.secretmanager_v2": (
547
                            ModuleProvider(
548
                                Address("", target_name="google-cloud-secret-manager"),
549
                                ModuleProviderType.IMPL,
550
                            ),
551
                        ),
552
                        "google.cloud.secretmanager_v3": (
553
                            ModuleProvider(
554
                                Address("", target_name="google-cloud-secret-manager"),
555
                                ModuleProviderType.IMPL,
556
                            ),
557
                        ),
558
                        "looks_like_stubs": (
559
                            ModuleProvider(
560
                                Address("", target_name="looks_like_stubs"),
561
                                ModuleProviderType.IMPL,
562
                            ),
563
                        ),
564
                        "mapped_module": (
565
                            ModuleProvider(
566
                                Address("", target_name="modules"),
567
                                ModuleProviderType.IMPL,
568
                            ),
569
                        ),
570
                        "model_utils": (
571
                            ModuleProvider(
572
                                Address("", target_name="django-model-utils"),
573
                                ModuleProviderType.IMPL,
574
                            ),
575
                        ),
576
                        "multiple_owners": (
577
                            ModuleProvider(
578
                                Address("", target_name="multiple_owners1"),
579
                                ModuleProviderType.IMPL,
580
                            ),
581
                        ),
582
                        "opentelemetry.instrumentation.botocore": (
583
                            ModuleProvider(
584
                                Address(
585
                                    "",
586
                                    target_name="opentelemetry-instrumentation-botocore",
587
                                ),
588
                                ModuleProviderType.IMPL,
589
                            ),
590
                        ),
591
                        "req1": (
592
                            ModuleProvider(
593
                                Address("", target_name="req1"), ModuleProviderType.IMPL
594
                            ),
595
                        ),
596
                        "taggit": (
597
                            ModuleProvider(
598
                                Address("", target_name="django-taggit"),
599
                                ModuleProviderType.IMPL,
600
                            ),
601
                        ),
602
                        "typed_dep1": (
603
                            ModuleProvider(
604
                                Address("", target_name="typed-dep1"),
605
                                ModuleProviderType.TYPE_STUB,
606
                            ),
607
                        ),
608
                        "typed_dep2": (
609
                            ModuleProvider(
610
                                Address("", target_name="typed-dep2"),
611
                                ModuleProviderType.TYPE_STUB,
612
                            ),
613
                        ),
614
                        "typed_dep3": (
615
                            ModuleProvider(
616
                                Address("", target_name="typed-dep3"),
617
                                ModuleProviderType.TYPE_STUB,
618
                            ),
619
                        ),
620
                        "typed_dep4": (
621
                            ModuleProvider(
622
                                Address("", target_name="typed-dep4"),
623
                                ModuleProviderType.TYPE_STUB,
624
                            ),
625
                        ),
626
                        "typed_dep5": (
627
                            ModuleProvider(
628
                                Address("", target_name="typed-dep5"),
629
                                ModuleProviderType.TYPE_STUB,
630
                            ),
631
                        ),
632
                        "un_normalized_project": (
633
                            ModuleProvider(
634
                                Address("", target_name="un_normalized"),
635
                                ModuleProviderType.IMPL,
636
                            ),
637
                        ),
638
                        "vcs_dist": (
639
                            ModuleProvider(
640
                                Address("", target_name="vcs_dist"),
641
                                ModuleProviderType.IMPL,
642
                            ),
643
                        ),
644
                    }
645
                ),
646
            }
647
        )
648
    )
UNCOV
649
    assert result == expected
×
650

651

652
def test_map_module_to_address(rule_runner: RuleRunner) -> None:
1✔
UNCOV
653
    def assert_owners(
×
654
        module: str,
655
        expected: list[Address],
656
        expected_ambiguous: list[Address] | None = None,
657
    ) -> None:
UNCOV
658
        owners = rule_runner.request(
×
659
            PythonModuleOwners,
660
            [PythonModuleOwnersRequest(module, resolve="python-default")],
661
        )
UNCOV
662
        assert list(owners.unambiguous) == expected
×
UNCOV
663
        assert list(owners.ambiguous) == (expected_ambiguous or [])
×
664

UNCOV
665
        from_import_owners = rule_runner.request(
×
666
            PythonModuleOwners,
667
            [PythonModuleOwnersRequest(f"{module}.Class", resolve="python-default")],
668
        )
UNCOV
669
        assert list(from_import_owners.unambiguous) == expected
×
UNCOV
670
        assert list(from_import_owners.ambiguous) == (expected_ambiguous or [])
×
671

UNCOV
672
    rule_runner.set_options(["--source-root-patterns=['root', '/']", "--python-enable-resolves"])
×
UNCOV
673
    rule_runner.write_files(
×
674
        {
675
            # A root-level module.
676
            "script.py": "",
677
            "BUILD": dedent(
678
                """\
679
                python_source(name="script", source="script.py")
680
                python_requirement(name="valid_dep", requirements=["valid_dep"])
681
                # Dependency with a type stub.
682
                python_requirement(name="dep_w_stub", requirements=["dep_w_stub"])
683
                python_requirement(name="dep_w_stub-types", requirements=["dep_w_stub-types"])
684
                """
685
            ),
686
            # Normal first-party module.
687
            "root/no_stub/app.py": "",
688
            "root/no_stub/BUILD": "python_sources()",
689
            # First-party module with type stub.
690
            "root/stub/app.py": "",
691
            "root/stub/app.pyi": "",
692
            "root/stub/BUILD": "python_sources()",
693
            # Package path.
694
            "root/package/subdir/__init__.py": "",
695
            "root/package/subdir/BUILD": "python_sources()",
696
            # Third-party requirement with first-party type stub.
697
            "root/dep_with_stub.pyi": "",
698
            "root/BUILD": dedent(
699
                """\
700
                python_sources()
701
                python_requirement(name="dep", requirements=["dep_with_stub"])
702
                """
703
            ),
704
            # Namespace package split between first- and third-party, disambiguated by ancestry level.
705
            "root/namespace/__init__.py": "",
706
            "root/namespace/BUILD": dedent(
707
                """\
708
                python_requirement(name="thirdparty", requirements=["namespace.thirdparty"])
709
                python_source(name="init", source="__init__.py")
710
                """
711
            ),
712
            # Ambiguity.
713
            "root/ambiguous/f1.py": "",
714
            "root/ambiguous/f2.py": "",
715
            "root/ambiguous/f3.py": "",
716
            "root/ambiguous/f4.pyi": "",
717
            "root/ambiguous/BUILD": dedent(
718
                """\
719
                # Ambiguity purely within third-party deps.
720
                python_requirement(name='thirdparty1', requirements=['ambiguous_3rdparty'])
721
                python_requirement(name='thirdparty2', requirements=['ambiguous_3rdparty'])
722

723
                # Ambiguity purely within first-party deps.
724
                python_source(name="firstparty1", source="f1.py")
725
                python_source(name="firstparty2", source="f1.py")
726

727
                # Ambiguity within third-party, which should result in ambiguity for first-party
728
                # too. These all share the module `ambiguous.f2`.
729
                python_requirement(
730
                    name='thirdparty3', requirements=['bar'], modules=['ambiguous.f2']
731
                )
732
                python_requirement(
733
                    name='thirdparty4', requirements=['bar'], modules=['ambiguous.f2']
734
                )
735
                python_source(name="firstparty3", source="f2.py")
736

737
                # Ambiguity within first-party, which should result in ambiguity for third-party
738
                # too. These all share the module `ambiguous.f3`.
739
                python_source(name="firstparty4", source="f3.py")
740
                python_source(name="firstparty5", source="f3.py")
741
                python_requirement(
742
                    name='thirdparty5', requirements=['baz'], modules=['ambiguous.f3']
743
                )
744

745
                # You can only write a first-party type stub for a third-party requirement if
746
                # there are not third-party type stubs already.
747
                python_requirement(
748
                    name='ambiguous-stub',
749
                    requirements=['ambiguous-stub'],
750
                    modules=["ambiguous.f4"],
751
                )
752
                python_requirement(
753
                    name='ambiguous-stub-types',
754
                    requirements=['ambiguous-stub-types'],
755
                    type_stub_modules=["ambiguous.f4"],
756
                )
757
                python_source(name='ambiguous-stub-1stparty', source='f4.pyi')
758
                """
759
            ),
760
        }
761
    )
762

UNCOV
763
    assert_owners("pathlib", [])
×
UNCOV
764
    assert_owners("typing", [])
×
UNCOV
765
    assert_owners("valid_dep", [Address("", target_name="valid_dep")])
×
UNCOV
766
    assert_owners(
×
767
        "dep_w_stub",
768
        [
769
            Address("", target_name="dep_w_stub"),
770
            Address("", target_name="dep_w_stub-types"),
771
        ],
772
    )
UNCOV
773
    assert_owners("script", [Address("", target_name="script")])
×
UNCOV
774
    assert_owners("no_stub.app", expected=[Address("root/no_stub", relative_file_path="app.py")])
×
UNCOV
775
    assert_owners(
×
776
        "stub.app",
777
        [
778
            Address("root/stub", relative_file_path="app.py"),
779
            Address("root/stub", relative_file_path="app.pyi"),
780
        ],
781
    )
UNCOV
782
    assert_owners(
×
783
        "package.subdir",
784
        [Address("root/package/subdir", relative_file_path="__init__.py")],
785
    )
UNCOV
786
    assert_owners(
×
787
        "dep_with_stub",
788
        [
789
            Address("root", target_name="dep"),
790
            Address("root", relative_file_path="dep_with_stub.pyi"),
791
        ],
792
    )
UNCOV
793
    assert_owners("namespace.thirdparty", [Address("root/namespace", target_name="thirdparty")])
×
794

UNCOV
795
    assert_owners(
×
796
        "ambiguous_3rdparty",
797
        [],
798
        expected_ambiguous=[
799
            Address("root/ambiguous", target_name="thirdparty1"),
800
            Address("root/ambiguous", target_name="thirdparty2"),
801
        ],
802
    )
UNCOV
803
    assert_owners(
×
804
        "ambiguous.f1",
805
        [],
806
        expected_ambiguous=[
807
            Address("root/ambiguous", target_name="firstparty1"),
808
            Address("root/ambiguous", target_name="firstparty2"),
809
        ],
810
    )
UNCOV
811
    assert_owners(
×
812
        "ambiguous.f2",
813
        [],
814
        expected_ambiguous=[
815
            Address("root/ambiguous", target_name="thirdparty3"),
816
            Address("root/ambiguous", target_name="thirdparty4"),
817
            Address("root/ambiguous", target_name="firstparty3"),
818
        ],
819
    )
UNCOV
820
    assert_owners(
×
821
        "ambiguous.f3",
822
        [],
823
        expected_ambiguous=[
824
            Address("root/ambiguous", target_name="thirdparty5"),
825
            Address("root/ambiguous", target_name="firstparty4"),
826
            Address("root/ambiguous", target_name="firstparty5"),
827
        ],
828
    )
UNCOV
829
    assert_owners(
×
830
        "ambiguous.f4",
831
        [],
832
        expected_ambiguous=[
833
            Address("root/ambiguous", target_name="ambiguous-stub"),
834
            Address("root/ambiguous", target_name="ambiguous-stub-types"),
835
            Address("root/ambiguous", target_name="ambiguous-stub-1stparty"),
836
        ],
837
    )
838

839

840
def test_resolving_ambiguity_by_filesystem_proximity(rule_runner: RuleRunner) -> None:
1✔
UNCOV
841
    rule_runner.set_options(
×
842
        [
843
            "--source-root-patterns=['root1', 'root2', 'root3']",
844
            "--python-infer-ambiguity-resolution=by_source_root",
845
        ]
846
    )
UNCOV
847
    rule_runner.write_files(
×
848
        {
849
            "root1/aa/bb/BUILD": "python_sources()",
850
            "root1/aa/bb/foo.py": "",
851
            "root1/aa/cc/BUILD": "python_sources()",
852
            "root1/aa/cc/bar.py": "from aa.bb import foo",
853
            "root2/aa/bb/BUILD": "python_sources()",
854
            "root2/aa/bb/foo.py": "",
855
            "root2/aa/dd/baz.py": "from aa.bb import foo",
856
            "root3/aa/ee/BUILD": "python_sources()",
857
            "root3/aa/ee/foo.py": "from aa.bb import foo",
858
        }
859
    )
860

UNCOV
861
    owners = rule_runner.request(
×
862
        PythonModuleOwners,
863
        [PythonModuleOwnersRequest("aa.bb.foo", None, locality=None)],
864
    )
UNCOV
865
    assert list(owners.unambiguous) == []
×
UNCOV
866
    assert list(owners.ambiguous) == [
×
867
        Address("root1/aa/bb", relative_file_path="foo.py"),
868
        Address("root2/aa/bb", relative_file_path="foo.py"),
869
    ]
870

UNCOV
871
    owners = rule_runner.request(
×
872
        PythonModuleOwners,
873
        [PythonModuleOwnersRequest("aa.bb.foo", None, locality="root1/")],
874
    )
UNCOV
875
    assert list(owners.unambiguous) == [Address("root1/aa/bb", relative_file_path="foo.py")]
×
UNCOV
876
    assert list(owners.ambiguous) == []
×
877

UNCOV
878
    owners = rule_runner.request(
×
879
        PythonModuleOwners,
880
        [PythonModuleOwnersRequest("aa.bb.foo", None, locality="root2/")],
881
    )
UNCOV
882
    assert list(owners.unambiguous) == [Address("root2/aa/bb", relative_file_path="foo.py")]
×
UNCOV
883
    assert list(owners.ambiguous) == []
×
884

UNCOV
885
    owners = rule_runner.request(
×
886
        PythonModuleOwners,
887
        [PythonModuleOwnersRequest("aa.bb.foo", None, locality="root3/")],
888
    )
UNCOV
889
    assert list(owners.unambiguous) == []
×
UNCOV
890
    assert list(owners.ambiguous) == [
×
891
        Address("root1/aa/bb", relative_file_path="foo.py"),
892
        Address("root2/aa/bb", relative_file_path="foo.py"),
893
    ]
894

895

896
def test_map_module_considers_resolves(rule_runner: RuleRunner) -> None:
1✔
UNCOV
897
    rule_runner.write_files(
×
898
        {
899
            "BUILD": dedent(
900
                """\
901
                # Note that both `python_requirements` have the same `dep`, which would normally
902
                # result in ambiguity.
903
                python_requirement(
904
                    name="dep1",
905
                    resolve="a",
906
                    requirements=["dep"],
907
                )
908

909
                python_requirement(
910
                    name="dep2",
911
                    resolve="b",
912
                    requirements=["dep"],
913
                )
914
                """
915
            )
916
        }
917
    )
UNCOV
918
    rule_runner.set_options(["--python-resolves={'a': '', 'b': ''}", "--python-enable-resolves"])
×
919

UNCOV
920
    def get_owners(resolve: str | None) -> PythonModuleOwners:
×
UNCOV
921
        return rule_runner.request(PythonModuleOwners, [PythonModuleOwnersRequest("dep", resolve)])
×
922

UNCOV
923
    assert get_owners("a").unambiguous == (Address("", target_name="dep1"),)
×
UNCOV
924
    assert get_owners("b").unambiguous == (Address("", target_name="dep2"),)
×
UNCOV
925
    assert get_owners(None).ambiguous == (
×
926
        Address("", target_name="dep1"),
927
        Address("", target_name="dep2"),
928
    )
929

930

931
def test_issue_15111(rule_runner: RuleRunner) -> None:
1✔
932
    """Ensure we can handle when a single address provides multiple modules.
933

934
    This is currently only possible with third-party targets.
935
    """
UNCOV
936
    rule_runner.write_files(
×
937
        {"BUILD": "python_requirement(name='req', requirements=['docopt', 'types-docopt'])"}
938
    )
UNCOV
939
    rule_runner.set_options(["--python-enable-resolves"])
×
UNCOV
940
    result = rule_runner.request(ThirdPartyPythonModuleMapping, [])
×
UNCOV
941
    assert result == ThirdPartyPythonModuleMapping(
×
942
        FrozenDict(
943
            {
944
                "python-default": FrozenDict(
945
                    {
946
                        "docopt": (
947
                            ModuleProvider(Address("", target_name="req"), ModuleProviderType.IMPL),
948
                            ModuleProvider(
949
                                Address("", target_name="req"),
950
                                ModuleProviderType.TYPE_STUB,
951
                            ),
952
                        ),
953
                    }
954
                )
955
            }
956
        )
957
    )
958

959

960
@pytest.mark.parametrize(
1✔
961
    ("proj_name", "expected_modules"),
962
    [
963
        (
964
            "google-cloud-hardyhar",
965
            (
966
                "google.cloud.hardyhar",
967
                "google.cloud.hardyhar_v1",
968
                "google.cloud.hardyhar_v2",
969
                "google.cloud.hardyhar_v3",
970
            ),
971
        ),
972
        (
973
            "python-jose",
974
            ("jose",),
975
        ),
976
        (
977
            "opentelemetry-instrumentation-tornado",
978
            ("opentelemetry.instrumentation.tornado",),
979
        ),
980
        ("azure-mgmt-consumption", ("azure.mgmt.consumption",)),
981
        ("azure-keyvault", ("azure.keyvault",)),
982
        (
983
            "django-admin-cursor-paginator",
984
            ("admin_cursor_paginator",),
985
        ),
986
        (
987
            "django-dotenv",
988
            ("dotenv",),
989
        ),
990
        ("oslo-service", ("oslo_service",)),
991
        ("apache-airflow-providers-apache-beam", ("airflow.providers.apache.beam",)),
992
        ("pyopenssl", tuple()),
993
        ("", tuple()),
994
    ],
995
)
996
def test_generate_mappings_from_pattern_matches_para(
1✔
997
    proj_name: str, expected_modules: tuple[str]
998
) -> None:
999
    assert generate_mappings_from_pattern(proj_name, is_type_stub=False) == expected_modules
1✔
1000

1001

1002
@pytest.mark.parametrize(
1✔
1003
    ("proj_name", "expected_modules"),
1004
    [
1005
        (
1006
            "types-requests",
1007
            ("requests",),
1008
        ),
1009
        (
1010
            "botocore-stubs",
1011
            ("botocore",),
1012
        ),
1013
        (
1014
            "django-types",
1015
            ("django",),
1016
        ),
1017
        (
1018
            "types_requests",
1019
            ("requests",),
1020
        ),
1021
        (
1022
            "botocore_stubs",
1023
            ("botocore",),
1024
        ),
1025
        (
1026
            "django_types",
1027
            ("django",),
1028
        ),
1029
        ("", tuple()),
1030
    ],
1031
)
1032
def test_generate_type_stub_mappings_from_pattern_matches_para(
1✔
1033
    proj_name: str, expected_modules: tuple[str]
1034
) -> None:
1035
    assert generate_mappings_from_pattern(proj_name, is_type_stub=True) == expected_modules
1✔
1036

1037

1038
def test_number_of_capture_groups_for_functions() -> None:
1✔
1039
    with pytest.raises(ValueError):
1✔
1040
        re.sub("foo", first_group_hyphen_to_underscore, "foo")
1✔
1041
    with pytest.raises(ValueError):
1✔
1042
        re.sub("foo", two_groups_hyphens_two_replacements_with_suffix, "foo")
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