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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

72.73
/src/python/pants/backend/python/dependency_inference/default_module_mapping.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
# NB: The project names must follow the naming scheme at
5
#  https://www.python.org/dev/peps/pep-0503/#normalized-names.
6

7
import re
4✔
8
from collections.abc import Callable
4✔
9
from enum import Enum
4✔
10
from functools import partial
4✔
11
from re import Match
4✔
12

13

14
class PackageSeparator(Enum):
4✔
15
    DOT = "."
4✔
16
    UNDERSCORE = "_"
4✔
17
    NONE = ""
4✔
18

19

20
def all_hyphen_to_separator(m: Match[str], separator: PackageSeparator) -> str:
4✔
21
    """Convert all hyphens to a package separator e.g. azure-foo-bar -> azure.foo.bar or
22
    azure_foo_bar.
23

24
    >>> all_hyphen_to_separator(re.match(r"^azure-.+", "azure-foo-bar"), PackageSeparator.DOT)
25
    'azure.foo.bar'
26
    >>> all_hyphen_to_separator(re.match(r"^azure-.+", "azure-foo-bar"), PackageSeparator.UNDERSCORE)
27
    'azure_foo_bar'
28
    >>> all_hyphen_to_separator(re.match(r"^azure-.+", "azure-foo-bar"), PackageSeparator.NONE)
29
    'azurefoobar'
30
    """
UNCOV
31
    return m.string.replace("-", separator.value)
×
32

33

34
def first_group_hyphen_to_separator(m: Match[str], separator: PackageSeparator) -> str:
4✔
35
    """Convert the first group(regex match group) of hyphens to underscores. Only returns the first
36
    group and must contain at least one group.
37

38
    >>> first_group_hyphen_to_separator(re.match(r"^django-((.+(-.+)?))", "django-admin-cursor-paginator"), separator=PackageSeparator.UNDERSCORE)
39
    'admin_cursor_paginator'
40
    >>> first_group_hyphen_to_separator(re.match(r"^django-((.+(-.+)?))", "django-admin-cursor-paginator"), separator=PackageSeparator.DOT)
41
    'admin.cursor.paginator'
42
    >>> first_group_hyphen_to_separator(re.match(r"^django-((.+(-.+)?))", "django-admin-cursor-paginator"), separator=PackageSeparator.NONE)
43
    'admincursorpaginator'
44
    """
45
    if m.re.groups == 0 or not m.groups():
1✔
UNCOV
46
        raise ValueError(f"expected at least one group in the pattern{m.re.pattern} but got none.")
×
47
    return str(m.groups()[0]).replace("-", separator.value)
1✔
48

49

50
def two_groups_hyphens_two_replacements_with_suffix(
4✔
51
    m: Match[str],
52
    first_group_replacement: PackageSeparator = PackageSeparator.DOT,
53
    second_group_replacement: PackageSeparator = PackageSeparator.NONE,
54
    custom_suffix: str = "",
55
) -> str:
56
    """take two groups, and by default, the first will have '-' replaced with '.', the second will
57
    have '-' replaced with '' e.g. google-cloud-foo-bar -> group1(google.cloud.)group2(foobar)
58

59
    >>> two_groups_hyphens_two_replacements_with_suffix(re.match(r"^(google-cloud-)([^.]+)", "google-cloud-foo-bar"))
60
    'google.cloud.foobar'
61
    >>> two_groups_hyphens_two_replacements_with_suffix(re.match(r"^(google-cloud-)([^.]+)", "google-cloud-foo-bar"), first_group_replacement=PackageSeparator.UNDERSCORE, second_group_replacement=PackageSeparator.DOT)
62
    'google_cloud_foo.bar'
63
    """
UNCOV
64
    if m.re.groups < 2 or not m.groups():
×
UNCOV
65
        raise ValueError(f"expected at least two groups in the pattern{m.re.pattern}.")
×
UNCOV
66
    prefix = m.string[m.start(1) : m.end(1)].replace("-", first_group_replacement.value)
×
UNCOV
67
    suffix = m.string[m.start(2) : m.end(2)].replace("-", second_group_replacement.value)
×
UNCOV
68
    return f"{prefix}{suffix}{custom_suffix}"
×
69

70

71
# common replacement methods
72
all_hyphen_to_dot = partial(all_hyphen_to_separator, separator=PackageSeparator.DOT)
4✔
73
all_hyphen_to_underscore = partial(all_hyphen_to_separator, separator=PackageSeparator.UNDERSCORE)
4✔
74
first_group_hyphen_to_dot = partial(first_group_hyphen_to_separator, separator=PackageSeparator.DOT)
4✔
75
first_group_hyphen_to_underscore = partial(
4✔
76
    first_group_hyphen_to_separator, separator=PackageSeparator.UNDERSCORE
77
)
78

79
"""
4✔
80
A mapping of Patterns and their replacements. will be used with `re.sub`.
81
The match is either a string or a function`(str) -> str`; that takes a re.Match and returns
82
the replacement. see re.sub for more information
83

84
then if an import in the python code is google.cloud.foo, then the package of
85
google-cloud-foo will be used.
86
"""
87
DEFAULT_MODULE_PATTERN_MAPPING: dict[re.Pattern, list[Callable[[Match[str]], str]]] = {
4✔
88
    re.compile(r"""^azure-.+"""): [all_hyphen_to_dot],
89
    re.compile(r"""^django-((.+(-.+)?))"""): [first_group_hyphen_to_underscore],
90
    # See https://github.com/googleapis/google-cloud-python#libraries for all Google cloud
91
    # libraries. We only add libraries in GA, not beta.
92
    re.compile(r"""^(google-cloud-)([^.]+)"""): [
93
        partial(two_groups_hyphens_two_replacements_with_suffix, custom_suffix=custom_suffix)
94
        for custom_suffix in ("", "_v1", "_v2", "_v3")
95
    ],
96
    re.compile(r"""^(opentelemetry-instrumentation-)([^.]+)"""): [
97
        partial(
98
            two_groups_hyphens_two_replacements_with_suffix,
99
            second_group_replacement=PackageSeparator.UNDERSCORE,
100
        ),
101
    ],
102
    re.compile(r"""^oslo-.+"""): [all_hyphen_to_underscore],
103
    re.compile(r"""^python-(.+)"""): [first_group_hyphen_to_underscore],
104
    re.compile(r"""^apache-(airflow-providers-.+)"""): [first_group_hyphen_to_dot],
105
}
106

107
DEFAULT_MODULE_MAPPING: dict[str, tuple[str, ...]] = {
4✔
108
    "absl-py": ("absl",),
109
    "acryl-datahub": ("datahub",),
110
    "amplitude-analytics": ("amplitude",),
111
    "ansicolors": ("colors",),
112
    "antlr4-python3-runtime": ("antlr4",),
113
    "apache-airflow": ("airflow",),
114
    "apache-airflow-client": ("airflow_client",),
115
    "atlassian-python-api": ("atlassian",),
116
    "attrs": ("attr", "attrs"),
117
    "auth0-python": ("auth0",),
118
    "bandwidth-sdk": ("bandwidth",),
119
    "beautifulsoup4": ("bs4",),
120
    "biopython": ("Bio", "BioSQL"),
121
    "bitvector": ("BitVector",),
122
    "cattrs": ("cattr", "cattrs"),
123
    "cloud-sql-python-connector": ("google.cloud.sql.connector",),
124
    "confluent-kafka": ("confluent_kafka",),
125
    "coolprop": ("CoolProp",),
126
    "databricks-sdk": ("databricks.sdk",),
127
    "databricks-sql-connector": (
128
        "databricks.sql",
129
        "databricks.sqlalchemy",
130
    ),
131
    "drf-openapi-tester": ("openapi_tester",),
132
    "delta-spark": ("delta",),
133
    "discord-py": ("discord",),
134
    "django-activity-stream": ("actstream",),
135
    "django-cors-headers": ("corsheaders",),
136
    "django-countries": ("django_countries",),
137
    "django-extensions": ("django_extensions",),
138
    "django-fernet-fields-v2": ("fernet_fields",),
139
    "django-filter": ("django_filters",),
140
    "django-fsm": ("django_fsm",),
141
    "django-money": ("djmoney",),
142
    "django-notifications-hq": ("notifications",),
143
    "django-oauth-toolkit": ("oauth2_provider",),
144
    "django-object-actions": ("django_object_actions",),
145
    "django-postgres-extra": ("psqlextra",),
146
    "django-pylibmc": ("django_pylibmc",),
147
    "django-redis": ("django_redis",),
148
    "django-scim2": ("django_scim",),
149
    "django-two-factor-auth": ("two_factor",),
150
    "django-user-agents": ("django_user_agents",),
151
    "djangorestframework": ("rest_framework",),
152
    "djangorestframework-api-key": ("rest_framework_api_key",),
153
    "djangorestframework-dataclasses": ("rest_framework_dataclasses",),
154
    "djangorestframework-jwt": ("rest_framework_jwt",),
155
    "djangorestframework-queryfields": ("drf_queryfields",),
156
    "djangorestframework-simplejwt": ("rest_framework_simplejwt",),
157
    "dnspython": ("dns",),
158
    "drf-api-tracking": ("rest_framework_tracking",),
159
    "elastic-apm": ("elasticapm",),
160
    "enum34": ("enum",),
161
    "factory-boy": ("factory",),
162
    "faiss-cpu": ("faiss",),
163
    "faiss-gpu": ("faiss",),
164
    "fluent-logger": ("fluent",),
165
    "fonttools": ("fontTools",),
166
    "gitpython": ("git",),
167
    "google-api-python-client": ("googleapiclient",),
168
    "google-auth": (
169
        "google.auth",
170
        "google.oauth2",
171
    ),
172
    "graphql-core": ("graphql",),
173
    "grpcio": ("grpc",),
174
    "grpcio-channelz": ("grpcio_channelz",),
175
    "grpcio-health-checking": ("grpc_health",),
176
    "grpcio-reflection": ("grpc_reflection",),
177
    "grpcio-status": ("grpc_status",),
178
    "grpcio-testing": ("grpc_testing",),
179
    "hdrhistogram": ("hdrh",),
180
    "honeycomb-opentelemetry": ("honeycomb.opentelemetry",),
181
    "ipython": ("IPython",),
182
    "jack-client": ("jack",),
183
    "kafka-python": ("kafka",),
184
    "lark-parser": ("lark",),
185
    "launchdarkly-server-sdk": ("ldclient",),
186
    "mail-parser": ("mailparser",),
187
    "matplotlib": ("matplotlib", "mpl_toolkits"),
188
    "matrix-nio": ("nio",),
189
    "mysql-connector-python": ("mysql.connector",),
190
    "mysqlclient": ("MySQLdb",),
191
    "netcdf4": ("netCDF4",),
192
    "o365": ("O365",),
193
    "opencv-python": ("cv2",),
194
    "opencv-python-headless": ("cv2",),
195
    "opensearch-py": ("opensearchpy",),
196
    # opentelemetry
197
    "opentelemetry-api": (
198
        "opentelemetry._logs",
199
        "opentelemetry.attributes",
200
        "opentelemetry.baggage",
201
        "opentelemetry.context",
202
        "opentelemetry.environment_variables",
203
        "opentelemetry.metrics",
204
        "opentelemetry.propagate",
205
        "opentelemetry.propagators",
206
        "opentelemetry.trace",
207
        "opentelemetry.util",
208
        "opentelemetry.version",
209
    ),
210
    "opentelemetry-exporter-otlp": ("opentelemetry.exporter.otlp",),
211
    "opentelemetry-exporter-otlp-proto-grpc": ("opentelemetry.exporter.otlp.proto.grpc",),
212
    "opentelemetry-exporter-otlp-proto-http": ("opentelemetry.exporter.otlp.proto.http",),
213
    "opentelemetry-instrumentation-kafka-python": ("opentelemetry.instrumentation.kafka",),
214
    "opentelemetry-proto": ("opentelemetry.proto",),
215
    "opentelemetry-sdk": ("opentelemetry.sdk",),
216
    "opentelemetry-semantic-conventions": ("opentelemetry.semconv",),
217
    "opentelemetry-test-utils": ("opentelemetry.test",),
218
    "paho-mqtt": ("paho",),
219
    "phonenumberslite": ("phonenumbers",),
220
    "pillow": ("PIL",),
221
    "pip-tools": ("piptools",),
222
    "progressbar2": ("progressbar",),
223
    "protobuf": ("google.protobuf",),
224
    "psycopg2-binary": ("psycopg2",),
225
    "py-healthcheck": ("healthcheck",),
226
    "pycrypto": ("Crypto",),
227
    "pycryptodome": ("Crypto",),
228
    "pyerfa": ("erfa",),
229
    "pygithub": ("github",),
230
    "pygobject": ("gi",),
231
    "pyhamcrest": ("hamcrest",),
232
    "pyicu": ("icu",),
233
    "pyjwt": ("jwt",),
234
    "pykube-ng": ("pykube",),
235
    "pymongo": ("bson", "gridfs", "pymongo"),
236
    "pymupdf": ("fitz", "pymupdf"),
237
    "pynacl": ("nacl",),
238
    "pyopenssl": ("OpenSSL",),
239
    "pypdf2": ("PyPDF2",),
240
    "pypi-kenlm": ("kenlm",),
241
    "pyshp": ("shapefile",),
242
    "pysocks": ("socks",),
243
    "pytest": ("pytest", "_pytest"),
244
    "pytest-runner": ("ptr",),
245
    "python-sat": ("pysat",),
246
    "python-json-logger": ("pythonjsonlogger",),
247
    "python-levenshtein": ("Levenshtein",),
248
    "python-lsp-jsonrpc": ("pylsp_jsonrpc",),
249
    "pywinrm": ("winrm",),
250
    "pywavelets": ("pywt",),
251
    "pyyaml": ("yaml",),
252
    "randomwords": ("random_words",),
253
    "robotraconteur": ("RobotRaconteur",),
254
    "scikit-image": ("skimage",),
255
    "scikit-learn": ("sklearn",),
256
    "scikit-video": ("skvideo",),
257
    "sisl": ("sisl", "sisl_toolbox"),
258
    "setuptools": ("easy_install", "pkg_resources", "setuptools"),
259
    "snowflake-connector-python": ("snowflake.connector",),
260
    "snowflake-snowpark-python": ("snowflake.snowpark",),
261
    "snowflake-sqlalchemy": ("snowflake.sqlalchemy",),
262
    "social-auth-app-django": ("social_django",),
263
    "social-auth-core": ("social_core",),
264
    "sseclient-py": ("sseclient",),
265
    "strawberry-graphql": ("strawberry",),
266
    "streamlit-aggrid": ("st_aggrid",),
267
    "unittest-xml-reporting": ("xmlrunner",),
268
    "unleashclient": ("UnleashClient",),
269
    "websocket-client": ("websocket",),
270
}
271

272
DEFAULT_TYPE_STUB_MODULE_PATTERN_MAPPING: dict[re.Pattern, list[Callable[[Match[str]], str]]] = {
4✔
273
    re.compile(r"""^stubs[_-](.+)"""): [first_group_hyphen_to_underscore],
274
    re.compile(r"""^types[_-](.+)"""): [first_group_hyphen_to_underscore],
275
    re.compile(r"""^(.+)[_-]stubs"""): [first_group_hyphen_to_underscore],
276
    re.compile(r"""^(.+)[_-]types"""): [first_group_hyphen_to_underscore],
277
}
278

279
DEFAULT_TYPE_STUB_MODULE_MAPPING: dict[str, tuple[str, ...]] = {
4✔
280
    "djangorestframework-types": ("rest_framework",),
281
    "lark-stubs": ("lark",),
282
    "types-beautifulsoup4": ("bs4",),
283
    "types-enum34": ("enum34",),
284
    "types-grpcio": ("grpc",),
285
    "types-grpcio-channelz": ("grpcio_channelz",),
286
    "types-grpcio-health-checking": ("grpc_health",),
287
    "types-grpcio-reflection": ("grpc_reflection",),
288
    "types-grpcio-status": ("grpc_status",),
289
    "types-pillow": ("PIL",),
290
    "types-protobuf": ("google.protobuf",),
291
    "types-pycrypto": ("Crypto",),
292
    "types-pyopenssl": ("OpenSSL",),
293
    "types-pyyaml": ("yaml",),
294
    "types-python-dateutil": ("dateutil",),
295
    "types-setuptools": ("easy_install", "pkg_resources", "setuptools"),
296
}
297

298
if __name__ == "__main__":
4✔
299
    import doctest
×
300

301
    doctest.testmod()
×
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