Skip to content

Commit 6446d1e

Browse files
committed
fix: remove dependency on packaging and pkg_resources
1 parent 6206ce0 commit 6446d1e

File tree

3 files changed

+55
-27
lines changed

3 files changed

+55
-27
lines changed

google/api_core/_python_package_support.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import warnings
1818
import sys
19-
from typing import Optional
19+
from typing import Optional, Tuple
2020

2121
from collections import namedtuple
2222

@@ -25,7 +25,14 @@
2525
_get_distribution_and_import_packages,
2626
)
2727

28-
from packaging.version import parse as parse_version
28+
if sys.version_info >= (3, 8):
29+
from importlib import metadata
30+
else:
31+
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
32+
# this code path once we drop support for Python 3.7
33+
import importlib_metadata as metadata
34+
35+
ParsedVersion = Tuple[int]
2936

3037
# Here we list all the packages for which we want to issue warnings
3138
# about deprecated and unsupported versions.
@@ -48,42 +55,53 @@
4855
UNKNOWN_VERSION_STRING = "--"
4956

5057

58+
def _parse_version_to_tuple(version_string: str) -> ParsedVersion:
59+
"""Safely converts a semantic version string to a comparable tuple of integers.
60+
61+
Example: "4.25.8" -> (4, 25, 8)
62+
Ignores non-numeric parts and handles common version formats.
63+
"""
64+
# Simple split and try to convert to int. Non-numeric parts are ignored
65+
# or will raise an exception that is handled in the caller.
66+
parts = []
67+
for part in version_string.split("."):
68+
try:
69+
parts.append(int(part))
70+
except ValueError:
71+
# If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
72+
# This is a simplification compared to 'packaging.parse_version', but sufficient
73+
# for comparing strictly numeric semantic versions.
74+
break
75+
return tuple(parts)
76+
77+
5178
def get_dependency_version(
5279
dependency_name: str,
5380
) -> DependencyVersion:
5481
"""Get the parsed version of an installed package dependency.
5582
5683
This function checks for an installed package and returns its version
57-
as a `packaging.version.Version` object for safe comparison. It handles
84+
as a comparable tuple of integers object for safe comparison. It handles
5885
both modern (Python 3.8+) and legacy (Python 3.7) environments.
5986
6087
Args:
6188
dependency_name: The distribution name of the package (e.g., 'requests').
6289
6390
Returns:
64-
A DependencyVersion namedtuple with `version` and
91+
A DependencyVersion namedtuple with `version` (a tuple of integers) and
6592
`version_string` attributes, or `DependencyVersion(None,
6693
UNKNOWN_VERSION_STRING)` if the package is not found or
6794
another error occurs during version discovery.
6895
6996
"""
7097
try:
71-
if sys.version_info >= (3, 8):
72-
from importlib import metadata
73-
74-
version_string = metadata.version(dependency_name)
75-
return DependencyVersion(parse_version(version_string), version_string)
76-
77-
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
78-
# this code path once we drop support for Python 3.7
79-
else: # pragma: NO COVER
80-
# Use pkg_resources, which is part of setuptools.
81-
import pkg_resources
82-
83-
version_string = pkg_resources.get_distribution(dependency_name).version
84-
return DependencyVersion(parse_version(version_string), version_string)
85-
98+
version_string: str
99+
version_string = metadata.version(dependency_name)
100+
parsed_version = _parse_version_to_tuple(version_string)
101+
return DependencyVersion(parsed_version, version_string)
86102
except Exception:
103+
# Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
104+
# or errors during _parse_version_to_tuple
87105
return DependencyVersion(None, UNKNOWN_VERSION_STRING)
88106

89107

@@ -132,10 +150,15 @@ def warn_deprecation_for_versions_less_than(
132150
or not minimum_fully_supported_version
133151
): # pragma: NO COVER
134152
return
153+
135154
dependency_version = get_dependency_version(dependency_import_package)
136155
if not dependency_version.version:
137156
return
138-
if dependency_version.version < parse_version(minimum_fully_supported_version):
157+
# Parse the minimum required version using the new custom function
158+
minimum_version_tuple = _parse_version_to_tuple(minimum_fully_supported_version)
159+
160+
# Compare the version tuples directly
161+
if dependency_version.version < minimum_version_tuple:
139162
(
140163
dependency_package,
141164
dependency_distribution_package,

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ dependencies = [
5151
"proto-plus >= 1.25.0, < 2.0.0; python_version >= '3.13'",
5252
"google-auth >= 2.14.1, < 3.0.0",
5353
"requests >= 2.18.0, < 3.0.0",
54+
"importlib_metadata>=1.0.0; python_version<'3.8'",
5455
]
5556
dynamic = ["version"]
5657

tests/unit/test_python_package_support.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
from unittest.mock import patch, MagicMock
1818

1919
import pytest
20-
from packaging.version import parse as parse_version
2120

2221
from google.api_core._python_package_support import (
22+
_parse_version_to_tuple,
2323
get_dependency_version,
2424
warn_deprecation_for_versions_less_than,
2525
check_dependency_versions,
@@ -35,7 +35,7 @@
3535
def test_get_dependency_version_py38_plus(mock_version):
3636
"""Test get_dependency_version on Python 3.8+."""
3737
mock_version.return_value = "1.2.3"
38-
expected = DependencyVersion(parse_version("1.2.3"), "1.2.3")
38+
expected = DependencyVersion(_parse_version_to_tuple("1.2.3"), "1.2.3")
3939
assert get_dependency_version("some-package") == expected
4040
mock_version.assert_called_once_with("some-package")
4141

@@ -53,7 +53,7 @@ def test_get_dependency_version_py37(mock_get_distribution):
5353
mock_dist = MagicMock()
5454
mock_dist.version = "4.5.6"
5555
mock_get_distribution.return_value = mock_dist
56-
expected = DependencyVersion(parse_version("4.5.6"), "4.5.6")
56+
expected = DependencyVersion(_parse_version_to_tuple("4.5.6"), "4.5.6")
5757
assert get_dependency_version("another-package") == expected
5858
mock_get_distribution.assert_called_once_with("another-package")
5959

@@ -74,7 +74,9 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
7474
("my-package (my.package)", "my-package"),
7575
]
7676

77-
mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0")
77+
mock_get_version.return_value = DependencyVersion(
78+
_parse_version_to_tuple("1.0.0"), "1.0.0"
79+
)
7880
with pytest.warns(FutureWarning) as record:
7981
warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0")
8082
assert len(record) == 1
@@ -90,14 +92,14 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
9092
# Case 2: Installed version is equal to required, should not warn.
9193
mock_get_packages.reset_mock()
9294
mock_get_version.return_value = DependencyVersion(
93-
parse_version("2.0.0"), "2.0.0"
95+
_parse_version_to_tuple("2.0.0"), "2.0.0"
9496
)
9597
warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0")
9698

9799
# Case 3: Installed version is greater than required, should not warn.
98100
mock_get_packages.reset_mock()
99101
mock_get_version.return_value = DependencyVersion(
100-
parse_version("3.0.0"), "3.0.0"
102+
_parse_version_to_tuple("3.0.0"), "3.0.0"
101103
)
102104
warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0")
103105

@@ -115,7 +117,9 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
115117
("dep-package (dep.package)", "dep-package"),
116118
("my-package (my.package)", "my-package"),
117119
]
118-
mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0")
120+
mock_get_version.return_value = DependencyVersion(
121+
_parse_version_to_tuple("1.0.0"), "1.0.0"
122+
)
119123
template = "Custom warning for {dependency_package} used by {consumer_package}."
120124
with pytest.warns(FutureWarning) as record:
121125
warn_deprecation_for_versions_less_than(

0 commit comments

Comments
 (0)