Skip to content

Commit e63555e

Browse files
bonauer-pfclaude
andcommitted
perf: avoid O(n) packages_distributions() scan at import time
Replace `importlib.metadata.packages_distributions()` with targeted O(1) `metadata.distribution()` lookups by trying common distribution name conventions (dot-to-dash, dot-to-underscore, last component). `packages_distributions()` scans every installed package in the environment to build a complete module-to-distribution mapping. In large venvs (500+ packages, common with many google-cloud-* libs), this causes multi-second import delays for google.api_core and every library that depends on it. Also defer the package label resolution in `check_python_version()` so it only runs when a warning is actually emitted (not on the common happy path of a supported Python version). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 281eaae commit e63555e

File tree

1 file changed

+29
-24
lines changed

1 file changed

+29
-24
lines changed

packages/google-api-core/google/api_core/_python_version_support.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import datetime
1818
import enum
19+
from importlib import metadata
1920
import logging
2021
import warnings
2122
import sys
@@ -139,37 +140,33 @@ def _flatten_message(text: str) -> str:
139140
return " ".join(textwrap.dedent(text).strip().split())
140141

141142

142-
# TODO(https://github.com/googleapis/python-api-core/issues/835):
143-
# Remove once we no longer support Python 3.9.
144-
# `importlib.metadata.packages_distributions()` is only supported in Python 3.10 and newer
145-
# https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.packages_distributions
146-
if sys.version_info < (3, 10):
143+
def _get_pypi_package_name(module_name):
144+
"""Determine the PyPI package name for a given module name.
147145
148-
def _get_pypi_package_name(module_name): # pragma: NO COVER
149-
"""Determine the PyPI package name for a given module name."""
150-
return None
151-
152-
else:
153-
from importlib import metadata
154-
155-
def _get_pypi_package_name(module_name):
156-
"""Determine the PyPI package name for a given module name."""
146+
Uses targeted O(1) lookups via importlib.metadata.distribution()
147+
instead of packages_distributions(), which does a full O(n) scan
148+
of every installed package and is very slow in large environments.
149+
"""
150+
# Try common distribution name conventions. Each
151+
# metadata.distribution() call is an O(1) lookup by name.
152+
candidates = [
153+
module_name.replace(".", "-"), # google.api_core -> google-api-core
154+
module_name.replace(".", "_"), # google.api_core -> google_api_core
155+
module_name.split(".")[-1], # google.protobuf -> protobuf
156+
]
157+
for candidate in candidates:
157158
try:
158-
# Get the mapping of modules to distributions
159-
module_to_distributions = metadata.packages_distributions()
160-
161-
# Check if the module is found in the mapping
162-
if module_name in module_to_distributions: # pragma: NO COVER
163-
# The value is a list of distribution names, take the first one
164-
return module_to_distributions[module_name][0]
159+
return metadata.distribution(candidate).metadata["Name"]
160+
except metadata.PackageNotFoundError:
161+
continue
165162
except Exception as e: # pragma: NO COVER
166163
_LOGGER.info(
167164
"An error occurred while determining PyPI package name for %s: %s",
168165
module_name,
169166
e,
170167
)
171-
172-
return None
168+
return None
169+
return None
173170

174171

175172
def _get_distribution_and_import_packages(import_package: str) -> Tuple[str, Any]:
@@ -195,7 +192,6 @@ def check_python_version(
195192
The support status of the current Python version.
196193
"""
197194
today = today or datetime.date.today()
198-
package_label, _ = _get_distribution_and_import_packages(package)
199195

200196
python_version = sys.version_info
201197
version_tuple = (python_version.major, python_version.minor)
@@ -221,7 +217,14 @@ def min_python(date: datetime.date) -> str:
221217
return f"{version[0]}.{version[1]}"
222218
return "at a currently supported version [https://devguide.python.org/versions]"
223219

220+
# Resolve the pretty package label lazily so we avoid any work on
221+
# the happy path (supported Python version, no warning needed).
222+
def get_package_label():
223+
label, _ = _get_distribution_and_import_packages(package)
224+
return label
225+
224226
if gapic_end < today:
227+
package_label = get_package_label()
225228
message = _flatten_message(
226229
f"""
227230
You are using a non-supported Python version ({py_version_str}).
@@ -236,6 +239,7 @@ def min_python(date: datetime.date) -> str:
236239

237240
eol_date = version_info.python_eol + EOL_GRACE_PERIOD
238241
if eol_date <= today <= gapic_end:
242+
package_label = get_package_label()
239243
message = _flatten_message(
240244
f"""
241245
You are using a Python version ({py_version_str})
@@ -250,6 +254,7 @@ def min_python(date: datetime.date) -> str:
250254
return PythonVersionStatus.PYTHON_VERSION_EOL
251255

252256
if gapic_deprecation <= today <= gapic_end:
257+
package_label = get_package_label()
253258
message = _flatten_message(
254259
f"""
255260
You are using a Python version ({py_version_str}) which Google will

0 commit comments

Comments
 (0)