Skip to content

Commit 603d528

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

File tree

4 files changed

+72
-55
lines changed

4 files changed

+72
-55
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,

noxfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def default(session, install_grpc=True, prerelease=False, install_async_rest=Fal
127127
"mock; python_version=='3.7'",
128128
"pytest",
129129
"pytest-cov",
130+
"pytest-mock",
130131
"pytest-xdist",
131132
)
132133

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ 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+
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
55+
# `importlib_metadata` once we drop support for Python 3.7
56+
"importlib_metadata>=1.0.0; python_version<'3.8'",
5457
]
5558
dynamic = ["version"]
5659

tests/unit/test_python_package_support.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414

1515
import sys
1616
import warnings
17-
from unittest.mock import patch, MagicMock
17+
from unittest.mock import patch
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,
@@ -28,39 +28,25 @@
2828
)
2929

3030

31-
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
32-
# this mark once we drop support for Python 3.7
33-
@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher")
34-
@patch("importlib.metadata.version")
35-
def test_get_dependency_version_py38_plus(mock_version):
36-
"""Test get_dependency_version on Python 3.8+."""
37-
mock_version.return_value = "1.2.3"
38-
expected = DependencyVersion(parse_version("1.2.3"), "1.2.3")
31+
def test_get_dependency_version(mocker):
32+
"""Test get_dependency_version."""
33+
if sys.version_info >= (3, 8):
34+
mock_importlib = mocker.patch(
35+
"importlib.metadata.version", return_value="1.2.3"
36+
)
37+
else:
38+
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
39+
# `importlib_metadata` once we drop support for Python 3.7
40+
mock_importlib = mocker.patch(
41+
"importlib_metadata.version", return_value="1.2.3"
42+
)
43+
expected = DependencyVersion(_parse_version_to_tuple("1.2.3"), "1.2.3")
3944
assert get_dependency_version("some-package") == expected
40-
mock_version.assert_called_once_with("some-package")
41-
42-
# Test package not found
43-
mock_version.side_effect = ImportError
44-
assert get_dependency_version("not-a-package") == DependencyVersion(None, "--")
4545

46-
47-
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
48-
# this test function once we drop support for Python 3.7
49-
@pytest.mark.skipif(sys.version_info >= (3, 8), reason="requires python3.7")
50-
@patch("pkg_resources.get_distribution")
51-
def test_get_dependency_version_py37(mock_get_distribution):
52-
"""Test get_dependency_version on Python 3.7."""
53-
mock_dist = MagicMock()
54-
mock_dist.version = "4.5.6"
55-
mock_get_distribution.return_value = mock_dist
56-
expected = DependencyVersion(parse_version("4.5.6"), "4.5.6")
57-
assert get_dependency_version("another-package") == expected
58-
mock_get_distribution.assert_called_once_with("another-package")
46+
mock_importlib.assert_called_once_with("some-package")
5947

6048
# Test package not found
61-
mock_get_distribution.side_effect = (
62-
Exception # pkg_resources has its own exception types
63-
)
49+
mock_importlib.side_effect = ImportError
6450
assert get_dependency_version("not-a-package") == DependencyVersion(None, "--")
6551

6652

@@ -74,7 +60,9 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
7460
("my-package (my.package)", "my-package"),
7561
]
7662

77-
mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0")
63+
mock_get_version.return_value = DependencyVersion(
64+
_parse_version_to_tuple("1.0.0"), "1.0.0"
65+
)
7866
with pytest.warns(FutureWarning) as record:
7967
warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0")
8068
assert len(record) == 1
@@ -90,14 +78,14 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
9078
# Case 2: Installed version is equal to required, should not warn.
9179
mock_get_packages.reset_mock()
9280
mock_get_version.return_value = DependencyVersion(
93-
parse_version("2.0.0"), "2.0.0"
81+
_parse_version_to_tuple("2.0.0"), "2.0.0"
9482
)
9583
warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0")
9684

9785
# Case 3: Installed version is greater than required, should not warn.
9886
mock_get_packages.reset_mock()
9987
mock_get_version.return_value = DependencyVersion(
100-
parse_version("3.0.0"), "3.0.0"
88+
_parse_version_to_tuple("3.0.0"), "3.0.0"
10189
)
10290
warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0")
10391

@@ -115,7 +103,9 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack
115103
("dep-package (dep.package)", "dep-package"),
116104
("my-package (my.package)", "my-package"),
117105
]
118-
mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0")
106+
mock_get_version.return_value = DependencyVersion(
107+
_parse_version_to_tuple("1.0.0"), "1.0.0"
108+
)
119109
template = "Custom warning for {dependency_package} used by {consumer_package}."
120110
with pytest.warns(FutureWarning) as record:
121111
warn_deprecation_for_versions_less_than(

0 commit comments

Comments
 (0)